diff --git a/src/renderer/api/endpoints/helm-charts.api.ts b/src/renderer/api/endpoints/helm-charts.api.ts index a1fd497798..8beff01779 100644 --- a/src/renderer/api/endpoints/helm-charts.api.ts +++ b/src/renderer/api/endpoints/helm-charts.api.ts @@ -86,7 +86,7 @@ export class HelmChart { tillerVersion?: string; getId() { - return this.digest; + return `${this.apiVersion}/${this.name}@${this.getAppVersion()}`; } getName() { diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index d1c496e59c..3b7b795580 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -62,27 +62,37 @@ export class KubeWatchApi { }); } - protected getQuery(): Partial { - const { isAdmin, allowedNamespaces } = getHostedCluster(); - + // FIXME: use POST to send apis for subscribing (list could be huge) + // TODO: try to use normal fetch res.body stream to consume watch-api updates + // https://github.com/lensapp/lens/issues/1898 + protected async getQuery() { + const { namespaceStore } = await import("../components/+namespaces/namespace.store"); + await namespaceStore.whenReady; + const { isAdmin } = getHostedCluster(); return { api: this.activeApis.map(api => { - if (isAdmin) return api.getWatchUrl(); - - return allowedNamespaces.map(namespace => api.getWatchUrl(namespace)); + if (isAdmin && !api.isNamespaced) { + return api.getWatchUrl(); + } + if (api.isNamespaced) { + return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace)); + } + return []; }).flat() }; } // todo: maybe switch to websocket to avoid often reconnects @autobind() - protected connect() { + protected async connect() { if (this.evtSource) this.disconnect(); // close previous connection - if (!this.activeApis.length) { + const query = await this.getQuery(); + + if (!this.activeApis.length || !query.api.length) { return; } - const query = this.getQuery(); + const apiUrl = `${apiPrefix}/watch?${stringify(query)}`; this.evtSource = new EventSource(apiUrl); diff --git a/src/renderer/components/+apps-releases/release.store.ts b/src/renderer/components/+apps-releases/release.store.ts index b6d5c2fb5f..7222fd2aa4 100644 --- a/src/renderer/components/+apps-releases/release.store.ts +++ b/src/renderer/components/+apps-releases/release.store.ts @@ -5,7 +5,7 @@ import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayl import { ItemStore } from "../../item.store"; import { Secret } from "../../api/endpoints"; import { secretsStore } from "../+config-secrets/secrets.store"; -import { getHostedCluster } from "../../../common/cluster-store"; +import { namespaceStore } from "../+namespaces/namespace.store"; @autobind() export class ReleaseStore extends ItemStore { @@ -60,30 +60,24 @@ export class ReleaseStore extends ItemStore { @action async loadAll() { this.isLoading = true; - let items; + let items: HelmRelease[]; try { - const { isAdmin, allowedNamespaces } = getHostedCluster(); - - items = await this.loadItems(!isAdmin ? allowedNamespaces : null); - } finally { - if (items) { - items = this.sortItems(items); - this.items.replace(items); - } + items = await this.loadItems(namespaceStore.getContextNamespaces()); + items = this.sortItems(items); + this.items.replace(items); this.isLoaded = true; + } catch (error) { + console.error(`Loading Helm Chart releases has failed: ${error}`); + } finally { this.isLoading = false; } } - async loadItems(namespaces?: string[]) { - if (!namespaces) { - return helmReleasesApi.list(); - } else { - return Promise - .all(namespaces.map(namespace => helmReleasesApi.list(namespace))) - .then(items => items.flat()); - } + async loadItems(namespaces: string[]) { + return Promise + .all(namespaces.map(namespace => helmReleasesApi.list(namespace))) + .then(items => items.flat()); } async create(payload: IReleaseCreatePayload) { diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index 09877aea4b..a7c5ed4cc2 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -1,11 +1,11 @@ -import { action, comparer, observable, reaction } from "mobx"; +import { action, comparer, observable, reaction, when } from "mobx"; import { autobind, createStorage } from "../../utils"; import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api"; import { createPageParam } from "../../navigation"; import { apiManager } from "../../api/api-manager"; import { isAllowedResource } from "../../../common/rbac"; -import { getHostedCluster } from "../../../common/cluster-store"; +import { clusterStore, getHostedCluster } from "../../../common/cluster-store"; const storage = createStorage("context_namespaces", []); @@ -21,14 +21,25 @@ export const namespaceUrlParam = createPageParam({ @autobind() export class NamespaceStore extends KubeObjectStore { api = namespacesApi; - contextNs = observable.array(); + + @observable contextNs = observable.array(); + @observable isReady = false; + + whenReady = when(() => this.isReady); constructor() { super(); this.init(); } - private init() { + private async init() { + await clusterStore.whenLoaded; + if (!getHostedCluster()) return; + + await getHostedCluster().whenReady; // wait for cluster-state from main + await this.loadAll(); // auto-load allowed namespaces + this.isReady = true; + this.setContext(this.initNamespaces); return reaction(() => this.contextNs.toJS(), namespaces => { @@ -40,13 +51,52 @@ export class NamespaceStore extends KubeObjectStore { }); } - get initNamespaces() { - return namespaceUrlParam.get(); + get allowedNamespaces(): string[] { + return getHostedCluster().allowedNamespaces; } + // FIXME: page/app reload doesn't restore previously selected namespaces + get initNamespaces() { + const allowedNamespaces = new Set(this.allowedNamespaces); + const lastUsedNamespaces = new Set(storage.get()); + + // remove previously saved, but currently disallowed namespaces + lastUsedNamespaces.forEach(namespace => { + if (!allowedNamespaces.has(namespace)) { + lastUsedNamespaces.delete(namespace); + } + }); + + // return previously saved and currently allowed namespaces + if (lastUsedNamespaces.size) { + return Array.from(lastUsedNamespaces); + } + // otherwise select "default" or first allowed namespace + else { + if (allowedNamespaces.has("default")) { + return ["default"]; + } else if (allowedNamespaces.size) { + return [Array.from(allowedNamespaces)[0]]; + } + } + + return []; + } + + getContextNamespaces(): string[] { + let namespaces = this.contextNs.toJS(); + if (!namespaces.length) { + return [...this.allowedNamespaces]; // show all namespaces when nothing selected + } + return namespaces; + } + + /** + * @deprecated + */ getContextParams() { return { - namespaces: this.contextNs.toJS(), + namespaces: this.getContextNamespaces(), }; } @@ -61,6 +111,12 @@ export class NamespaceStore extends KubeObjectStore { return super.subscribe(apis); } + async loadAll() { + return super.loadAll({ + namespaces: this.allowedNamespaces, + }); + } + protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams) { if (!isAllowedResource("namespaces")) { return namespaces.map(this.getDummyNamespace); diff --git a/src/renderer/components/+workloads-overview/overview-statuses.tsx b/src/renderer/components/+workloads-overview/overview-statuses.tsx index 78adecb6df..33e5aa37c5 100644 --- a/src/renderer/components/+workloads-overview/overview-statuses.tsx +++ b/src/renderer/components/+workloads-overview/overview-statuses.tsx @@ -27,7 +27,7 @@ export class OverviewStatuses extends React.Component { @autobind() renderWorkload(resource: KubeResource): React.ReactElement { const store = workloadStores[resource]; - const items = store.getAllByNs(namespaceStore.contextNs); + const items = store.getAllByNs(namespaceStore.getContextNamespaces()); return (
diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index bdd6302f5a..ae702b8d74 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -110,6 +110,7 @@ export class ItemListLayout extends React.Component { ]); } + // FIXME: reload and re-subscribe stores when context namespaces changed async componentDidMount() { const { store, dependentStores, isClusterScoped } = this.props; const stores = [store, ...dependentStores]; diff --git a/src/renderer/item.store.ts b/src/renderer/item.store.ts index 2105954d32..94f93c1958 100644 --- a/src/renderer/item.store.ts +++ b/src/renderer/item.store.ts @@ -9,6 +9,7 @@ export interface ItemObject { @autobind() export abstract class ItemStore { + abstract loadAll(...args: any[]): any; abstract loadAll(): Promise; protected defaultSorting = (item: T) => item.getName(); @@ -40,8 +41,7 @@ export abstract class ItemStore { if (item) { return item; - } - else { + } else { const items = this.sortItems([...this.items, newItem]); this.items.replace(items); @@ -83,8 +83,7 @@ export abstract class ItemStore { const index = this.items.findIndex(item => item === existingItem); this.items.splice(index, 1, item); - } - else { + } else { let items = [...this.items, item]; if (sortItems) items = this.sortItems(items); @@ -130,8 +129,7 @@ export abstract class ItemStore { toggleSelection(item: T) { if (this.isSelected(item)) { this.unselect(item); - } - else { + } else { this.select(item); } } @@ -142,8 +140,7 @@ export abstract class ItemStore { if (allSelected) { visibleItems.forEach(this.unselect); - } - else { + } else { visibleItems.forEach(this.select); } } diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index b19ef3f9a0..eb57814ce5 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -93,13 +93,20 @@ export abstract class KubeObjectStore extends ItemSt } @action - async loadAll() { + async loadAll(params: { namespaces?: string[] } = {}) { this.isLoading = true; let items: T[]; try { - const { allowedNamespaces: namespaces, isAdmin } = getHostedCluster(); - items = await this.loadItems({ isAdmin, namespaces, api: this.api }); + const { namespaceStore } = await import("./components/+namespaces/namespace.store"); + const contextNamespaces = params.namespaces || namespaceStore.getContextNamespaces(); + + items = await this.loadItems({ + isAdmin: getHostedCluster().isAdmin, + namespaces: contextNamespaces, + api: this.api, + }); + items = this.filterItemsOnLoad(items); items = this.sortItems(items); this.items.replace(items);