1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' of github.com:lensapp/lens into feature/protocol-handler

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-02-04 08:29:16 -05:00
commit 4ad4e47006
28 changed files with 317 additions and 226 deletions

View File

@ -12,6 +12,7 @@ categories:
- 'chore'
- 'area/ci'
- 'area/tests'
- 'dependencies'
template: |
## Changes since $PREVIOUS_TAG
@ -20,8 +21,10 @@ template: |
### Download
- [Lens v$RESOLVED_VERSION - Linux](https://snapcraft.io/kontena-lens)
- [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.AppImage)
- Lens v$RESOLVED_VERSION - Linux
- [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.AppImage)
- [DEB](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.amd64.deb)
- [RPM](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.rpm)
- [Snapcraft](https://snapcraft.io/kontena-lens)
- [Lens v$RESOLVED_VERSION - MacOS](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.dmg)
- [Lens v$RESOLVED_VERSION - Windows](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-Setup-$RESOLVED_VERSION.exe)

View File

@ -2,7 +2,7 @@
"name": "kontena-lens",
"productName": "Lens",
"description": "Lens - The Kubernetes IDE",
"version": "4.1.0-alpha.1",
"version": "4.1.0-alpha.2",
"main": "static/build/main.js",
"copyright": "© 2020, Mirantis, Inc.",
"license": "MIT",
@ -103,7 +103,6 @@
],
"linux": {
"category": "Network",
"executableName": "lens",
"artifactName": "${productName}-${version}.${arch}.${ext}",
"target": [
"deb",
@ -162,6 +161,9 @@
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"snap": {
"confinement": "classic"
},
"publish": [
{
"provider": "github",
@ -169,9 +171,6 @@
"owner": "lensapp"
}
],
"snap": {
"confinement": "classic"
},
"protocols": {
"name": "Lens Protocol Handler",
"schemes": [
@ -302,7 +301,7 @@
"@types/webpack-dev-server": "^3.11.1",
"@types/webpack-env": "^1.15.2",
"@types/webpack-node-externals": "^1.7.1",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.0.0",
"ace-builds": "^1.4.11",
"ansi_up": "^4.0.4",

View File

@ -126,6 +126,7 @@ describe("create clusters", () => {
};
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
jest.spyOn(Cluster.prototype, "canUseWatchApi").mockReturnValue(Promise.resolve(true));
jest.spyOn(Cluster.prototype, "canI")
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
expect(attr.namespace).toBe("default");

View File

@ -48,6 +48,7 @@ export interface ClusterState {
isAdmin: boolean;
allowedNamespaces: string[]
allowedResources: string[]
isGlobalWatchEnabled: boolean;
}
/**
@ -91,7 +92,6 @@ export class Cluster implements ClusterModel, ClusterState {
*/
@observable initializing = false;
/**
* Is cluster object initialized
*
@ -177,6 +177,12 @@ export class Cluster implements ClusterModel, ClusterState {
* @observable
*/
@observable isAdmin = false;
/**
* Global watch-api accessibility , e.g. "/api/v1/services?watch=1"
*
* @observable
*/
@observable isGlobalWatchEnabled = false;
/**
* Preferences
*
@ -353,9 +359,7 @@ export class Cluster implements ClusterModel, ClusterState {
await this.refreshConnectionStatus();
if (this.accessible) {
await this.refreshAllowedResources();
this.isAdmin = await this.isClusterAdmin();
this.ready = true;
await this.refreshAccessibility();
this.ensureKubectl();
}
this.activated = true;
@ -410,13 +414,11 @@ export class Cluster implements ClusterModel, ClusterState {
await this.refreshConnectionStatus();
if (this.accessible) {
this.isAdmin = await this.isClusterAdmin();
await this.refreshAllowedResources();
await this.refreshAccessibility();
if (opts.refreshMetadata) {
this.refreshMetadata();
}
this.ready = true;
}
this.pushState();
}
@ -433,6 +435,18 @@ export class Cluster implements ClusterModel, ClusterState {
this.metadata = Object.assign(existingMetadata, metadata);
}
/**
* @internal
*/
private async refreshAccessibility(): Promise<void> {
this.isAdmin = await this.isClusterAdmin();
this.isGlobalWatchEnabled = await this.canUseWatchApi({ resource: "*" });
await this.refreshAllowedResources();
this.ready = true;
}
/**
* @internal
*/
@ -571,6 +585,17 @@ export class Cluster implements ClusterModel, ClusterState {
});
}
/**
* @internal
*/
async canUseWatchApi(customizeResource: V1ResourceAttributes = {}): Promise<boolean> {
return this.canI({
verb: "watch",
resource: "*",
...customizeResource,
});
}
toJSON(): ClusterModel {
const model: ClusterModel = {
id: this.id,
@ -604,6 +629,7 @@ export class Cluster implements ClusterModel, ClusterState {
isAdmin: this.isAdmin,
allowedNamespaces: this.allowedNamespaces,
allowedResources: this.allowedResources,
isGlobalWatchEnabled: this.isGlobalWatchEnabled,
};
return toJS(state, {

View File

@ -62,16 +62,6 @@ function buildTray(icon: string | NativeImage, menu: Menu, windowManager: Window
function createTrayMenu(windowManager: WindowManager): Menu {
return Menu.buildFromTemplate([
{
label: "About Lens",
async click() {
// note: argument[1] (browserWindow) not available when app is not focused / hidden
const browserWindow = await windowManager.ensureMainWindow();
showAbout(browserWindow);
},
},
{ type: "separator" },
{
label: "Open Lens",
async click() {
@ -124,6 +114,15 @@ function createTrayMenu(windowManager: WindowManager): Menu {
}
},
},
{
label: "About Lens",
async click() {
// note: argument[1] (browserWindow) not available when app is not focused / hidden
const browserWindow = await windowManager.ensureMainWindow();
showAbout(browserWindow);
},
},
{ type: "separator" },
{
label: "Quit App",

View File

@ -5,12 +5,11 @@ import type { Cluster } from "../../main/cluster";
import type { IKubeWatchEvent, IKubeWatchEventStreamEnd, IWatchRoutePayload } from "../../main/routes/watch-route";
import type { KubeObject } from "./kube-object";
import type { KubeObjectStore } from "../kube-object.store";
import type { NamespaceStore } from "../components/+namespaces/namespace.store";
import plimit from "p-limit";
import debounce from "lodash/debounce";
import { comparer, computed, observable, reaction } from "mobx";
import { autobind, EventEmitter } from "../utils";
import { autorun, comparer, computed, IReactionDisposer, observable, reaction } from "mobx";
import { autobind, EventEmitter, noop } from "../utils";
import { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api";
import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api";
import { apiPrefix, isDebugging, isProduction } from "../../common/vars";
@ -19,6 +18,7 @@ import { apiManager } from "./api-manager";
export { IKubeWatchEvent, IKubeWatchEventStreamEnd };
export interface IKubeWatchMessage<T extends KubeObject = any> {
namespace?: string;
data?: IKubeWatchEvent<KubeJsonApiData>
error?: IKubeWatchEvent<KubeJsonApiError>;
api?: KubeApi<T>;
@ -28,7 +28,7 @@ export interface IKubeWatchMessage<T extends KubeObject = any> {
export interface IKubeWatchSubscribeStoreOptions {
preload?: boolean; // preload store items, default: true
waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true
cacheLoading?: boolean; // when enabled loading store will be skipped, default: false
loadOnce?: boolean; // check store.isLoaded to skip loading if done already, default: false
}
export interface IKubeWatchReconnectOptions {
@ -43,50 +43,49 @@ export interface IKubeWatchLog {
@autobind()
export class KubeWatchApi {
private cluster: Cluster;
private namespaceStore: NamespaceStore;
private requestId = 0;
private isConnected = false;
private reader: ReadableStreamReader<string>;
private subscribers = observable.map<KubeApi, number>();
// events
public onMessage = new EventEmitter<[IKubeWatchMessage]>();
@observable.ref private cluster: Cluster;
@observable.ref private namespaces: string[] = [];
@observable subscribers = observable.map<KubeApi, number>();
@observable isConnected = false;
@computed get isReady(): boolean {
return Boolean(this.cluster && this.namespaces);
}
@computed get isActive(): boolean {
return this.apis.length > 0;
}
@computed get apis(): string[] {
const { cluster, namespaceStore } = this;
const activeApis = Array.from(this.subscribers.keys());
if (!this.isReady) {
return [];
}
return activeApis.map(api => {
if (!cluster.isAllowedResource(api.kind)) {
return Array.from(this.subscribers.keys()).map(api => {
if (!this.isAllowedApi(api)) {
return [];
}
if (api.isNamespaced) {
return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace));
} else {
return api.getWatchUrl();
if (api.isNamespaced && !this.cluster.isGlobalWatchEnabled) {
return this.namespaces.map(namespace => api.getWatchUrl(namespace));
}
return api.getWatchUrl();
}).flat();
}
constructor() {
this.init();
}
private async init() {
const { getHostedCluster } = await import("../../common/cluster-store");
const { namespaceStore } = await import("../components/+namespaces/namespace.store");
await namespaceStore.whenReady;
this.cluster = getHostedCluster();
this.namespaceStore = namespaceStore;
async init({ getCluster, getNamespaces }: {
getCluster: () => Cluster,
getNamespaces: () => string[],
}): Promise<void> {
autorun(() => {
this.cluster = getCluster();
this.namespaces = getNamespaces();
});
this.bindAutoConnect();
}
@ -108,7 +107,7 @@ export class KubeWatchApi {
}
isAllowedApi(api: KubeApi): boolean {
return !!this?.cluster.isAllowedResource(api.kind);
return Boolean(this?.cluster.isAllowedResource(api.kind));
}
subscribeApi(api: KubeApi | KubeApi[]): () => void {
@ -129,45 +128,66 @@ export class KubeWatchApi {
};
}
subscribeStores(stores: KubeObjectStore[], options: IKubeWatchSubscribeStoreOptions = {}): () => void {
const { preload = true, waitUntilLoaded = true, cacheLoading = false } = options;
preloadStores(stores: KubeObjectStore[], { loadOnce = false } = {}) {
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
const preloading: Promise<any>[] = [];
for (const store of stores) {
preloading.push(limitRequests(async () => {
if (store.isLoaded && loadOnce) return; // skip
return store.loadAll(this.namespaces);
}));
}
return {
loading: Promise.allSettled(preloading),
cancelLoading: () => limitRequests.clearQueue(),
};
}
subscribeStores(stores: KubeObjectStore[], options: IKubeWatchSubscribeStoreOptions = {}): () => void {
const { preload = true, waitUntilLoaded = true, loadOnce = false } = options;
const apis = new Set(stores.map(store => store.getSubscribeApis()).flat());
const unsubscribeList: (() => void)[] = [];
let isUnsubscribed = false;
const load = () => this.preloadStores(stores, { loadOnce });
let preloading = preload && load();
let cancelReloading: IReactionDisposer = noop;
const subscribe = () => {
if (isUnsubscribed) return;
apis.forEach(api => unsubscribeList.push(this.subscribeApi(api)));
};
if (preload) {
for (const store of stores) {
preloading.push(limitRequests(async () => {
if (cacheLoading && store.isLoaded) return; // skip
return store.loadAll();
}));
}
}
if (waitUntilLoaded) {
Promise.all(preloading).then(subscribe, error => {
this.log({
message: new Error("Loading stores has failed"),
meta: { stores, error, options },
if (preloading) {
if (waitUntilLoaded) {
preloading.loading.then(subscribe, error => {
this.log({
message: new Error("Loading stores has failed"),
meta: { stores, error, options },
});
});
} else {
subscribe();
}
// reload when context namespaces changes
cancelReloading = reaction(() => this.namespaces, () => {
preloading?.cancelLoading();
preloading = load();
}, {
equals: comparer.shallow,
});
} else {
subscribe();
}
// unsubscribe
return () => {
if (isUnsubscribed) return;
isUnsubscribed = true;
limitRequests.clearQueue();
cancelReloading();
preloading?.cancelLoading();
unsubscribeList.forEach(unsubscribe => unsubscribe());
};
}
@ -254,6 +274,10 @@ export class KubeWatchApi {
const kubeEvent: IKubeWatchEvent = JSON.parse(json);
const message = this.getMessage(kubeEvent);
if (!this.namespaces.includes(message.namespace)) {
continue; // skip updates from non-watching resources context
}
this.onMessage.emit(message);
} catch (error) {
return json;
@ -286,6 +310,7 @@ export class KubeWatchApi {
message.api = api;
message.store = apiManager.getStore(api);
message.namespace = namespace;
}
break;
}

View File

@ -58,11 +58,11 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
@action
async loadAll() {
async loadAll(namespaces = namespaceStore.allowedNamespaces) {
this.isLoading = true;
try {
const items = await this.loadItems(namespaceStore.getContextNamespaces());
const items = await this.loadItems(namespaces);
this.items.replace(this.sortItems(items));
this.isLoaded = true;
@ -73,6 +73,10 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
}
async loadSelectedNamespaces(): Promise<void> {
return this.loadAll(namespaceStore.getContextNamespaces());
}
async loadItems(namespaces: string[]) {
return Promise
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
@ -82,7 +86,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
async create(payload: IReleaseCreatePayload) {
const response = await helmReleasesApi.create(payload);
if (this.isLoaded) this.loadAll();
if (this.isLoaded) this.loadSelectedNamespaces();
return response;
}
@ -90,7 +94,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
const response = await helmReleasesApi.update(name, namespace, payload);
if (this.isLoaded) this.loadAll();
if (this.isLoaded) this.loadSelectedNamespaces();
return response;
}
@ -98,7 +102,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
async rollback(name: string, namespace: string, revision: number) {
const response = await helmReleasesApi.rollback(name, namespace, revision);
if (this.isLoaded) this.loadAll();
if (this.isLoaded) this.loadSelectedNamespaces();
return response;
}

View File

@ -30,7 +30,7 @@ export class CrdResources extends React.Component<Props> {
const { store } = this;
if (store && !store.isLoading && !store.isLoaded) {
store.loadAll();
store.loadSelectedNamespaces();
}
})
]);

View File

@ -14,7 +14,7 @@ export interface KubeEventDetailsProps {
@observer
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
async componentDidMount() {
eventStore.loadAll();
eventStore.loadSelectedNamespaces();
}
render() {

View File

@ -32,8 +32,8 @@ export class NamespaceDetails extends React.Component<Props> {
}
componentDidMount() {
resourceQuotaStore.loadAll();
limitRangeStore.loadAll();
resourceQuotaStore.loadSelectedNamespaces();
limitRangeStore.loadSelectedNamespaces();
}
render() {

View File

@ -13,17 +13,14 @@ import { kubeWatchApi } from "../../api/kube-watch-api";
interface Props extends SelectProps {
showIcons?: boolean;
showClusterOption?: boolean; // show cluster option on the top (default: false)
clusterOptionLabel?: React.ReactNode; // label for cluster option (default: "Cluster")
customizeOptions?(nsOptions: SelectOption[]): SelectOption[];
showClusterOption?: boolean; // show "Cluster" option on the top (default: false)
showAllNamespacesOption?: boolean; // show "All namespaces" option on the top (default: false)
customizeOptions?(options: SelectOption[]): SelectOption[];
}
const defaultProps: Partial<Props> = {
showIcons: true,
showClusterOption: false,
get clusterOptionLabel() {
return `Cluster`;
},
};
@observer
@ -39,13 +36,17 @@ export class NamespaceSelect extends React.Component<Props> {
}
@computed get options(): SelectOption[] {
const { customizeOptions, showClusterOption, clusterOptionLabel } = this.props;
const { customizeOptions, showClusterOption, showAllNamespacesOption } = this.props;
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
options = customizeOptions ? customizeOptions(options) : options;
if (showAllNamespacesOption) {
options.unshift({ label: "All Namespaces", value: "" });
} else if (showClusterOption) {
options.unshift({ label: "Cluster", value: "" });
}
if (showClusterOption) {
options.unshift({ value: null, label: clusterOptionLabel });
if (customizeOptions) {
options = customizeOptions(options);
}
return options;
@ -64,7 +65,7 @@ export class NamespaceSelect extends React.Component<Props> {
};
render() {
const { className, showIcons, showClusterOption, clusterOptionLabel, customizeOptions, ...selectProps } = this.props;
const { className, showIcons, customizeOptions, ...selectProps } = this.props;
return (
<Select
@ -80,32 +81,56 @@ export class NamespaceSelect extends React.Component<Props> {
@observer
export class NamespaceSelectFilter extends React.Component {
@computed get placeholder(): React.ReactNode {
const namespaces = namespaceStore.getContextNamespaces();
switch (namespaces.length) {
case namespaceStore.allowedNamespaces.length:
return <>All namespaces</>;
case 0:
return <>Select a namespace</>;
case 1:
return <>Namespace: {namespaces[0]}</>;
default:
return <>Namespaces: {namespaces.join(", ")}</>;
}
}
formatOptionLabel = ({ value: namespace, label }: SelectOption) => {
if (namespace) {
const isSelected = namespaceStore.hasContext(namespace);
return (
<div className="flex gaps align-center">
<FilterIcon type={FilterType.NAMESPACE}/>
<span>{namespace}</span>
{isSelected && <Icon small material="check" className="box right"/>}
</div>
);
}
return label;
};
onChange = ([{ value: namespace }]: SelectOption[]) => {
if (namespace) {
namespaceStore.toggleContext(namespace);
} else {
namespaceStore.toggleAll(); // "All namespaces" option clicked
}
};
render() {
const { contextNs, hasContext, toggleContext } = namespaceStore;
let placeholder = <>All namespaces</>;
if (contextNs.length == 1) placeholder = <>Namespace: {contextNs[0]}</>;
if (contextNs.length >= 2) placeholder = <>Namespaces: {contextNs.join(", ")}</>;
return (
<NamespaceSelect
placeholder={placeholder}
isMulti={true}
showAllNamespacesOption={true}
closeMenuOnSelect={false}
isOptionSelected={() => false}
controlShouldRenderValue={false}
isMulti
onChange={([{ value }]: SelectOption[]) => toggleContext(value)}
formatOptionLabel={({ value: namespace }: SelectOption) => {
const isSelected = hasContext(namespace);
return (
<div className="flex gaps align-center">
<FilterIcon type={FilterType.NAMESPACE}/>
<span>{namespace}</span>
{isSelected && <Icon small material="check" className="box right"/>}
</div>
);
}}
placeholder={this.placeholder}
onChange={this.onChange}
formatOptionLabel={this.formatOptionLabel}
/>
);
}

View File

@ -1,4 +1,4 @@
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
import { action, comparer, computed, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
import { autobind, createStorage } from "../../utils";
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
@ -6,7 +6,7 @@ import { createPageParam } from "../../navigation";
import { apiManager } from "../../api/api-manager";
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
const storage = createStorage<string[]>("context_namespaces");
const storage = createStorage<string[]>("context_namespaces", []);
export const namespaceUrlParam = createPageParam<string[]>({
name: "namespaces",
@ -34,7 +34,7 @@ export function getDummyNamespace(name: string) {
export class NamespaceStore extends KubeObjectStore<Namespace> {
api = namespacesApi;
@observable contextNs = observable.array<string>();
@observable private contextNs = observable.set<string>();
@observable isReady = false;
whenReady = when(() => this.isReady);
@ -57,7 +57,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
return reaction(() => this.contextNs.toJS(), callback, {
return reaction(() => Array.from(this.contextNs), callback, {
equals: comparer.shallow,
...opts,
});
@ -73,48 +73,38 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
private autoLoadAllowedNamespaces(): IReactionDisposer {
return reaction(() => this.allowedNamespaces, () => this.loadAll(), {
return reaction(() => this.allowedNamespaces, namespaces => this.loadAll(namespaces), {
fireImmediately: true,
equals: comparer.shallow,
});
}
get allowedNamespaces(): string[] {
@computed get allowedNamespaces(): string[] {
return toJS(getHostedCluster().allowedNamespaces);
}
@computed
private get initialNamespaces(): string[] {
const allowed = new Set(this.allowedNamespaces);
const prevSelected = storage.get();
const namespaces = new Set(this.allowedNamespaces);
const prevSelected = storage.get().filter(namespace => namespaces.has(namespace));
if (Array.isArray(prevSelected)) {
return prevSelected.filter(namespace => allowed.has(namespace));
// return previously saved namespaces from local-storage
if (prevSelected.length > 0) {
return prevSelected;
}
// otherwise select "default" or first allowed namespace
if (allowed.has("default")) {
if (namespaces.has("default")) {
return ["default"];
} else if (allowed.size) {
return [Array.from(allowed)[0]];
} else if (namespaces.size) {
return [Array.from(namespaces)[0]];
}
return [];
}
getContextNamespaces(): string[] {
const namespaces = this.contextNs.toJS();
// show all namespaces when nothing selected
if (!namespaces.length) {
if (this.isLoaded) {
// return actual namespaces list since "allowedNamespaces" updating every 30s in cluster and thus might be stale
return this.items.map(namespace => namespace.getName());
}
return this.allowedNamespaces;
}
return namespaces;
public getContextNamespaces(): string[] {
return Array.from(this.contextNs);
}
getSubscribeApis() {
@ -143,26 +133,46 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
@action
setContext(namespaces: string[]) {
setContext(namespace: string | string[]) {
const namespaces = [namespace].flat();
this.contextNs.replace(namespaces);
}
hasContext(namespace: string | string[]) {
const context = Array.isArray(namespace) ? namespace : [namespace];
hasContext(namespaces: string | string[]) {
return [namespaces].flat().every(namespace => this.contextNs.has(namespace));
}
return context.every(namespace => this.contextNs.includes(namespace));
@computed get hasAllContexts(): boolean {
return this.contextNs.size === this.allowedNamespaces.length;
}
@action
toggleContext(namespace: string) {
if (this.hasContext(namespace)) this.contextNs.remove(namespace);
else this.contextNs.push(namespace);
if (this.hasContext(namespace)) {
this.contextNs.delete(namespace);
} else {
this.contextNs.add(namespace);
}
}
@action
toggleAll(showAll?: boolean) {
if (typeof showAll === "boolean") {
if (showAll) {
this.setContext(this.allowedNamespaces);
} else {
this.contextNs.clear();
}
} else {
this.toggleAll(!this.hasAllContexts);
}
}
@action
async remove(item: Namespace) {
await super.remove(item);
this.contextNs.remove(item.getName());
this.contextNs.delete(item.getName());
}
}

View File

@ -29,9 +29,7 @@ export class NodeDetails extends React.Component<Props> {
});
async componentDidMount() {
if (!podsStore.isLoaded) {
podsStore.loadAll();
}
podsStore.loadSelectedNamespaces();
}
componentWillUnmount() {

View File

@ -80,7 +80,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
];
this.isLoading = true;
await Promise.all(stores.map(store => store.loadAll()));
await Promise.all(stores.map(store => store.loadSelectedNamespaces()));
this.isLoading = false;
}

View File

@ -20,9 +20,7 @@ interface Props extends KubeObjectDetailsProps<CronJob> {
@observer
export class CronJobDetails extends React.Component<Props> {
async componentDidMount() {
if (!jobStore.isLoaded) {
jobStore.loadAll();
}
jobStore.loadSelectedNamespaces();
}
render() {

View File

@ -30,9 +30,7 @@ export class DaemonSetDetails extends React.Component<Props> {
});
componentDidMount() {
if (!podsStore.isLoaded) {
podsStore.loadAll();
}
podsStore.loadSelectedNamespaces();
}
componentWillUnmount() {

View File

@ -31,9 +31,7 @@ export class DeploymentDetails extends React.Component<Props> {
});
componentDidMount() {
if (!podsStore.isLoaded) {
podsStore.loadAll();
}
podsStore.loadSelectedNamespaces();
}
componentWillUnmount() {

View File

@ -25,9 +25,7 @@ interface Props extends KubeObjectDetailsProps<Job> {
@observer
export class JobDetails extends React.Component<Props> {
async componentDidMount() {
if (!podsStore.isLoaded) {
podsStore.loadAll();
}
podsStore.loadSelectedNamespaces();
}
render() {

View File

@ -29,9 +29,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
});
async componentDidMount() {
if (!podsStore.isLoaded) {
podsStore.loadAll();
}
podsStore.loadSelectedNamespaces();
}
componentWillUnmount() {

View File

@ -30,9 +30,7 @@ export class StatefulSetDetails extends React.Component<Props> {
});
componentDidMount() {
if (!podsStore.isLoaded) {
podsStore.loadAll();
}
podsStore.loadSelectedNamespaces();
}
componentWillUnmount() {

View File

@ -1,4 +1,5 @@
import React from "react";
import { computed, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { Redirect, Route, Router, Switch } from "react-router";
import { history } from "../navigation";
@ -42,7 +43,7 @@ import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../exte
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
import { eventStore } from "./+events/event.store";
import { computed, reaction, observable } from "mobx";
import { namespaceStore } from "./+namespaces/namespace.store";
import { nodesStore } from "./+nodes/nodes.store";
import { podsStore } from "./+workloads-pods/pods.store";
import { kubeWatchApi } from "../api/kube-watch-api";
@ -74,6 +75,12 @@ export class App extends React.Component {
window.location.reload();
});
whatInput.ask(); // Start to monitor user input device
await namespaceStore.whenReady;
await kubeWatchApi.init({
getCluster: getHostedCluster,
getNamespaces: namespaceStore.getContextNamespaces,
});
}
componentDidMount() {

View File

@ -80,7 +80,7 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
const values = this.values.getData(tabId);
await Promise.all([
!releaseStore.isLoaded && releaseStore.loadAll(),
!releaseStore.isLoaded && releaseStore.loadSelectedNamespaces(),
!values && this.loadValues(tabId)
]);
}

View File

@ -138,7 +138,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
const { store, dependentStores } = this.props;
const stores = Array.from(new Set([store, ...dependentStores]));
stores.forEach(store => store.loadAll());
stores.forEach(store => store.loadAll(namespaceStore.getContextNamespaces()));
}
private filterCallbacks: { [type: string]: ItemsFilter } = {

View File

@ -30,7 +30,7 @@ export class PageFiltersStore {
protected syncWithContextNamespace() {
const disposers = [
reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => {
if (filteredNs.length !== namespaceStore.contextNs.length) {
if (filteredNs.length !== namespaceStore.getContextNamespaces().length) {
namespaceStore.setContext(filteredNs);
}
}),

View File

@ -40,9 +40,7 @@ interface Props {
@observer
export class Sidebar extends React.Component<Props> {
async componentDidMount() {
if (!crdStore.isLoaded && isAllowedResource("customresourcedefinitions")) {
crdStore.loadAll();
}
crdStore.loadSelectedNamespaces();
}
renderCustomResources() {

View File

@ -106,17 +106,18 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
}
@action
async loadAll({ namespaces: contextNamespaces }: { namespaces?: string[] } = {}) {
async loadAll(namespaces: string[] = []): Promise<void> {
this.isLoading = true;
try {
if (!contextNamespaces) {
if (!namespaces.length) {
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
contextNamespaces = namespaceStore.getContextNamespaces();
// load all available namespaces by default
namespaces.push(...namespaceStore.allowedNamespaces);
}
let items = await this.loadItems({ namespaces: contextNamespaces, api: this.api });
let items = await this.loadItems({ namespaces, api: this.api });
items = this.filterItemsOnLoad(items);
items = this.sortItems(items);
@ -131,6 +132,12 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
}
}
async loadSelectedNamespaces(): Promise<void> {
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
return this.loadAll(namespaceStore.getContextNamespaces());
}
protected resetOnError(error: any) {
if (error) this.reset();
}

View File

@ -2,13 +2,16 @@
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
## 4.1.0-alpha.1 (current version)
## 4.1.0-alpha.2 (current version)
- Change: list views default to a namespace (insted of listing resources from all namespaces)
- Command palette
- Generic logs view with Pod selector
- Possibility to add custom Helm repository through Lens
- Possibility to change visibility of Pod list columns
- Possibility to change visibility of common resource list columns
- Suspend / resume buttons for CronJobs
- Allow namespace to specified on role creation
- Allow for changing installation directory on Windows
- Dock tabs context menu
- Display node column in Pod list
- Unify age column output with kubectl
@ -16,6 +19,8 @@ Here you can find description of changes we've built into each release. While we
- Improve Pod tolerations layout
- Lens metrics: scrape only lens-metrics namespace
- Lens metrics: Prometheus v2.19.3
- Update bundled kubectl to v1.18.15
- Improve how watch requests are handled
- Export PodDetailsList component to extension API
- Export Wizard components to extension API
- Export NamespaceSelect component to extension API

View File

@ -1843,28 +1843,29 @@
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@^4.12.0", "@typescript-eslint/eslint-plugin@^4.5.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.12.0.tgz#00d1b23b40b58031e6d7c04a5bc6c1a30a2e834a"
integrity sha512-wHKj6q8s70sO5i39H2g1gtpCXCvjVszzj6FFygneNFyIAxRvNSVz9GML7XpqrB9t7hNutXw+MHnLN/Ih6uyB8Q==
"@typescript-eslint/eslint-plugin@^4.14.2", "@typescript-eslint/eslint-plugin@^4.5.0":
version "4.14.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.2.tgz#47a15803cfab89580b96933d348c2721f3d2f6fe"
integrity sha512-uMGfG7GFYK/nYutK/iqYJv6K/Xuog/vrRRZX9aEP4Zv1jsYXuvFUMDFLhUnc8WFv3D2R5QhNQL3VYKmvLS5zsQ==
dependencies:
"@typescript-eslint/experimental-utils" "4.12.0"
"@typescript-eslint/scope-manager" "4.12.0"
"@typescript-eslint/experimental-utils" "4.14.2"
"@typescript-eslint/scope-manager" "4.14.2"
debug "^4.1.1"
functional-red-black-tree "^1.0.1"
lodash "^4.17.15"
regexpp "^3.0.0"
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/experimental-utils@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.12.0.tgz#372838e76db76c9a56959217b768a19f7129546b"
integrity sha512-MpXZXUAvHt99c9ScXijx7i061o5HEjXltO+sbYfZAAHxv3XankQkPaNi5myy0Yh0Tyea3Hdq1pi7Vsh0GJb0fA==
"@typescript-eslint/experimental-utils@4.14.2":
version "4.14.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.2.tgz#9df35049d1d36b6cbaba534d703648b9e1f05cbb"
integrity sha512-mV9pmET4C2y2WlyHmD+Iun8SAEqkLahHGBkGqDVslHkmoj3VnxnGP4ANlwuxxfq1BsKdl/MPieDbohCEQgKrwA==
dependencies:
"@types/json-schema" "^7.0.3"
"@typescript-eslint/scope-manager" "4.12.0"
"@typescript-eslint/types" "4.12.0"
"@typescript-eslint/typescript-estree" "4.12.0"
"@typescript-eslint/scope-manager" "4.14.2"
"@typescript-eslint/types" "4.14.2"
"@typescript-eslint/typescript-estree" "4.14.2"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
@ -1878,13 +1879,13 @@
"@typescript-eslint/typescript-estree" "4.8.2"
debug "^4.1.1"
"@typescript-eslint/scope-manager@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.12.0.tgz#beeb8beca895a07b10c593185a5612f1085ef279"
integrity sha512-QVf9oCSVLte/8jvOsxmgBdOaoe2J0wtEmBr13Yz0rkBNkl5D8bfnf6G4Vhox9qqMIoG7QQoVwd2eG9DM/ge4Qg==
"@typescript-eslint/scope-manager@4.14.2":
version "4.14.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.2.tgz#64cbc9ca64b60069aae0c060b2bf81163243b266"
integrity sha512-cuV9wMrzKm6yIuV48aTPfIeqErt5xceTheAgk70N1V4/2Ecj+fhl34iro/vIssJlb7XtzcaD07hWk7Jk0nKghg==
dependencies:
"@typescript-eslint/types" "4.12.0"
"@typescript-eslint/visitor-keys" "4.12.0"
"@typescript-eslint/types" "4.14.2"
"@typescript-eslint/visitor-keys" "4.14.2"
"@typescript-eslint/scope-manager@4.8.2":
version "4.8.2"
@ -1894,23 +1895,23 @@
"@typescript-eslint/types" "4.8.2"
"@typescript-eslint/visitor-keys" "4.8.2"
"@typescript-eslint/types@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.12.0.tgz#fb891fe7ccc9ea8b2bbd2780e36da45d0dc055e5"
integrity sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g==
"@typescript-eslint/types@4.14.2":
version "4.14.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.2.tgz#d96da62be22dc9dc6a06647f3633815350fb3174"
integrity sha512-LltxawRW6wXy4Gck6ZKlBD05tCHQUj4KLn4iR69IyRiDHX3d3NCAhO+ix5OR2Q+q9bjCrHE/HKt+riZkd1At8Q==
"@typescript-eslint/types@4.8.2":
version "4.8.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.8.2.tgz#c862dd0e569d9478eb82d6aee662ea53f5661a36"
integrity sha512-z1/AVcVF8ju5ObaHe2fOpZYEQrwHyZ7PTOlmjd3EoFeX9sv7UekQhfrCmgUO7PruLNfSHrJGQvrW3Q7xQ8EoAw==
"@typescript-eslint/typescript-estree@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz#3963418c850f564bdab3882ae23795d115d6d32e"
integrity sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w==
"@typescript-eslint/typescript-estree@4.14.2":
version "4.14.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.2.tgz#9c5ebd8cae4d7b014f890acd81e8e17f309c9df9"
integrity sha512-ESiFl8afXxt1dNj8ENEZT12p+jl9PqRur+Y19m0Z/SPikGL6rqq4e7Me60SU9a2M28uz48/8yct97VQYaGl0Vg==
dependencies:
"@typescript-eslint/types" "4.12.0"
"@typescript-eslint/visitor-keys" "4.12.0"
"@typescript-eslint/types" "4.14.2"
"@typescript-eslint/visitor-keys" "4.14.2"
debug "^4.1.1"
globby "^11.0.1"
is-glob "^4.0.1"
@ -1932,12 +1933,12 @@
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/visitor-keys@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz#a470a79be6958075fa91c725371a83baf428a67a"
integrity sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw==
"@typescript-eslint/visitor-keys@4.14.2":
version "4.14.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.2.tgz#997cbe2cb0690e1f384a833f64794e98727c70c6"
integrity sha512-KBB+xLBxnBdTENs/rUgeUKO0UkPBRs2vD09oMRRIkj5BEN8PX1ToXV532desXfpQnZsYTyLLviS7JrPhdL154w==
dependencies:
"@typescript-eslint/types" "4.12.0"
"@typescript-eslint/types" "4.14.2"
eslint-visitor-keys "^2.0.0"
"@typescript-eslint/visitor-keys@4.8.2":
@ -8671,12 +8672,7 @@ lodash.without@~4.4.0:
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.3.0, lodash@^4.8.0, lodash@~4.17.10:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
lodash@^4.17.19:
lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.3.0, lodash@^4.8.0, lodash@~4.17.10:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==