diff --git a/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb b/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb index cb13c8112d..763649f4f1 100644 --- a/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb +++ b/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb @@ -39,7 +39,7 @@ spec: serviceAccountName: kube-state-metrics containers: - name: kube-state-metrics - image: quay.io/coreos/kube-state-metrics:v1.9.5 + image: quay.io/coreos/kube-state-metrics:v1.9.7 ports: - name: metrics containerPort: 8080 diff --git a/extensions/metrics-cluster-feature/src/metrics-feature.ts b/extensions/metrics-cluster-feature/src/metrics-feature.ts index e29a9156bd..886cabe6dd 100644 --- a/extensions/metrics-cluster-feature/src/metrics-feature.ts +++ b/extensions/metrics-cluster-feature/src/metrics-feature.ts @@ -26,7 +26,7 @@ export interface MetricsConfiguration { export class MetricsFeature extends ClusterFeature.Feature { name = "metrics"; - latestVersion = "v2.17.2-lens1"; + latestVersion = "v2.17.2-lens2"; templateContext: MetricsConfiguration = { persistence: { diff --git a/package.json b/package.json index 1631c98a87..a7af4123d1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.0.4", + "version": "4.0.5", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", @@ -239,6 +239,7 @@ "node-pty": "^0.9.0", "npm": "^6.14.8", "openid-client": "^3.15.2", + "p-limit": "^3.1.0", "path-to-regexp": "^6.1.0", "proper-lockfile": "^4.1.1", "request": "^2.88.2", @@ -290,7 +291,6 @@ "@types/jsonpath": "^0.2.0", "@types/lodash": "^4.14.155", "@types/marked": "^0.7.4", - "@types/material-ui": "^0.21.7", "@types/md5-file": "^4.0.2", "@types/mini-css-extract-plugin": "^0.9.1", "@types/mock-fs": "^4.10.0", diff --git a/src/extensions/__tests__/extension-discovery.test.ts b/src/extensions/__tests__/extension-discovery.test.ts index 0317319329..d0066c3a7e 100644 --- a/src/extensions/__tests__/extension-discovery.test.ts +++ b/src/extensions/__tests__/extension-discovery.test.ts @@ -10,7 +10,7 @@ jest.mock("chokidar", () => ({ jest.mock("../extension-installer", () => ({ extensionInstaller: { extensionPackagesRoot: "", - installPackages: jest.fn() + installPackage: jest.fn() } })); @@ -41,7 +41,7 @@ describe("ExtensionDiscovery", () => { // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; - await extensionDiscovery.initMain(); + await extensionDiscovery.watchExtensions(); extensionDiscovery.events.on("add", (extension: InstalledExtension) => { expect(extension).toEqual({ @@ -81,7 +81,7 @@ describe("ExtensionDiscovery", () => { // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; - await extensionDiscovery.initMain(); + await extensionDiscovery.watchExtensions(); const onAdd = jest.fn(); diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 7d0da112bb..0994f89995 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -55,6 +55,7 @@ export class ExtensionDiscovery { protected bundledFolderPath: string; private loadStarted = false; + private extensions: Map = new Map(); // True if extensions have been loaded from the disk after app startup @observable isLoaded = false; @@ -69,13 +70,6 @@ export class ExtensionDiscovery { this.events = new EventEmitter(); } - // Each extension is added as a single dependency to this object, which is written as package.json. - // Each dependency key is the name of the dependency, and - // each dependency value is the non-symlinked path to the dependency (folder). - protected packagesJson: PackageJson = { - dependencies: {} - }; - get localFolderPath(): string { return path.join(os.homedir(), ".k8slens", "extensions"); } @@ -119,7 +113,6 @@ export class ExtensionDiscovery { } async initMain() { - this.watchExtensions(); handleRequest(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON()); reaction(() => this.toJSON(), () => { @@ -141,6 +134,7 @@ export class ExtensionDiscovery { watch(this.localFolderPath, { // For adding and removing symlinks to work, the depth has to be 1. depth: 1, + ignoreInitial: true, // Try to wait until the file has been completely copied. // The OS might emit an event for added file even it's not completely written to the filesysten. awaitWriteFinish: { @@ -176,8 +170,9 @@ export class ExtensionDiscovery { await this.removeSymlinkByManifestPath(manifestPath); // Install dependencies for the new extension - await this.installPackages(); + await this.installPackage(extension.absolutePath); + this.extensions.set(extension.id, extension); logger.info(`${logModule} Added extension ${extension.manifest.name}`); this.events.emit("add", extension); } @@ -197,23 +192,19 @@ export class ExtensionDiscovery { const extensionFolderName = path.basename(filePath); if (path.relative(this.localFolderPath, filePath) === extensionFolderName) { - const extensionName: string | undefined = Object - .entries(this.packagesJson.dependencies) - .find(([, extensionFolder]) => filePath === extensionFolder)?.[0]; + const extension = Array.from(this.extensions.values()).find((extension) => extension.absolutePath === filePath); + + if (extension) { + const extensionName = extension.manifest.name; - if (extensionName !== undefined) { // If the extension is deleted manually while the application is running, also remove the symlink await this.removeSymlinkByPackageName(extensionName); - delete this.packagesJson.dependencies[extensionName]; - - // Reinstall dependencies to remove the extension from package.json - await this.installPackages(); - // The path to the manifest file is the lens extension id // Note that we need to use the symlinked path - const lensExtensionId = path.join(this.nodeModulesPath, extensionName, manifestFilename); + const lensExtensionId = extension.manifestPath; + this.extensions.delete(extension.id); logger.info(`${logModule} removed extension ${extensionName}`); this.events.emit("remove", lensExtensionId as LensExtensionId); } else { @@ -296,7 +287,7 @@ export class ExtensionDiscovery { await fs.ensureDir(this.nodeModulesPath); await fs.ensureDir(this.localFolderPath); - const extensions = await this.loadExtensions(); + const extensions = await this.ensureExtensions(); this.isLoaded = true; @@ -335,7 +326,6 @@ export class ExtensionDiscovery { manifestJson = __non_webpack_require__(manifestPath); const installedManifestPath = this.getInstalledManifestPath(manifestJson.name); - this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath); const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath); return { @@ -347,29 +337,46 @@ export class ExtensionDiscovery { isEnabled }; } catch (error) { - logger.error(`${logModule}: can't install extension at ${manifestPath}: ${error}`, { manifestJson }); + logger.error(`${logModule}: can't load extension manifest at ${manifestPath}: ${error}`, { manifestJson }); return null; } } - async loadExtensions(): Promise> { + async ensureExtensions(): Promise> { const bundledExtensions = await this.loadBundledExtensions(); - await this.installPackages(); // install in-tree as a separate step - const localExtensions = await this.loadFromFolder(this.localFolderPath); + await this.installBundledPackages(this.packageJsonPath, bundledExtensions); - await this.installPackages(); - const extensions = bundledExtensions.concat(localExtensions); + const userExtensions = await this.loadFromFolder(this.localFolderPath); - return new Map(extensions.map(extension => [extension.id, extension])); + for (const extension of userExtensions) { + if (await fs.pathExists(extension.manifestPath) === false) { + await this.installPackage(extension.absolutePath); + } + } + const extensions = bundledExtensions.concat(userExtensions); + + return this.extensions = new Map(extensions.map(extension => [extension.id, extension])); } /** * Write package.json to file system and install dependencies. */ - installPackages() { - return extensionInstaller.installPackages(this.packageJsonPath, this.packagesJson); + async installBundledPackages(packageJsonPath: string, extensions: InstalledExtension[]) { + const packagesJson: PackageJson = { + dependencies: {} + }; + + extensions.forEach((extension) => { + packagesJson.dependencies[extension.manifest.name] = extension.absolutePath; + }); + + return await extensionInstaller.installPackages(packageJsonPath, packagesJson); + } + + async installPackage(name: string) { + return extensionInstaller.installPackage(name); } async loadBundledExtensions() { diff --git a/src/extensions/extension-installer.ts b/src/extensions/extension-installer.ts index 75b30d0b9a..04b78bbe1a 100644 --- a/src/extensions/extension-installer.ts +++ b/src/extensions/extension-installer.ts @@ -30,12 +30,49 @@ export class ExtensionInstaller { return __non_webpack_require__.resolve("npm/bin/npm-cli"); } - installDependencies(): Promise { - return new Promise((resolve, reject) => { + /** + * Write package.json to the file system and execute npm install for it. + */ + async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise { + // Mutual exclusion to install packages in sequence + await this.installLock.acquireAsync(); + + try { + // Write the package.json which will be installed in .installDependencies() + await fs.writeFile(path.join(packageJsonPath), JSON.stringify(packagesJson, null, 2), { + mode: 0o600 + }); + logger.info(`${logModule} installing dependencies at ${extensionPackagesRoot()}`); - const child = child_process.fork(this.npmPath, ["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], { + await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"]); + logger.info(`${logModule} dependencies installed at ${extensionPackagesRoot()}`); + } finally { + this.installLock.release(); + } + } + + /** + * Install single package using npm + */ + async installPackage(name: string): Promise { + // Mutual exclusion to install packages in sequence + await this.installLock.acquireAsync(); + + try { + logger.info(`${logModule} installing package from ${name} to ${extensionPackagesRoot()}`); + await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock", "--no-save", name]); + logger.info(`${logModule} package ${name} installed to ${extensionPackagesRoot()}`); + } finally { + this.installLock.release(); + } + } + + private npm(args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = child_process.fork(this.npmPath, args, { cwd: extensionPackagesRoot(), - silent: true + silent: true, + env: {} }); let stderr = ""; @@ -56,25 +93,6 @@ export class ExtensionInstaller { }); }); } - - /** - * Write package.json to the file system and execute npm install for it. - */ - async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise { - // Mutual exclusion to install packages in sequence - await this.installLock.acquireAsync(); - - try { - // Write the package.json which will be installed in .installDependencies() - await fs.writeFile(path.join(packageJsonPath), JSON.stringify(packagesJson, null, 2), { - mode: 0o600 - }); - - await this.installDependencies(); - } finally { - this.installLock.release(); - } - } } export const extensionInstaller = new ExtensionInstaller(); diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 966289f157..98697d252c 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -12,6 +12,7 @@ import type { LensExtension, LensExtensionConstructor, LensExtensionId } from ". import type { LensMainExtension } from "./lens-main-extension"; import type { LensRendererExtension } from "./lens-renderer-extension"; import * as registries from "./registries"; +import fs from "fs"; // lazy load so that we get correct userData export function extensionPackagesRoot() { @@ -71,7 +72,7 @@ export class ExtensionLoader { } await Promise.all([this.whenLoaded, extensionsStore.whenLoaded]); - + // save state on change `extension.isEnabled` reaction(() => this.storeState, extensionsState => { extensionsStore.mergeState(extensionsState); @@ -115,7 +116,6 @@ export class ExtensionLoader { protected async initMain() { this.isLoaded = true; this.loadOnMain(); - this.broadcastExtensions(); reaction(() => this.toJSON(), () => { this.broadcastExtensions(); @@ -136,7 +136,7 @@ export class ExtensionLoader { this.syncExtensions(extensions); const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId); - + // Remove deleted extensions in renderer side only this.extensions.forEach((_, lensExtensionId) => { if (!receivedExtensionIds.includes(lensExtensionId)) { @@ -276,6 +276,12 @@ export class ExtensionLoader { } if (extEntrypoint !== "") { + if (!fs.existsSync(extEntrypoint)) { + console.log(`${logModule}: entrypoint ${extEntrypoint} not found, skipping ...`); + + return; + } + return __non_webpack_require__(extEntrypoint).default; } } catch (err) { diff --git a/src/extensions/npm/extensions/package.json b/src/extensions/npm/extensions/package.json index a4c3f13d6e..5b2b4475ae 100644 --- a/src/extensions/npm/extensions/package.json +++ b/src/extensions/npm/extensions/package.json @@ -16,8 +16,10 @@ "name": "Mirantis, Inc.", "email": "info@k8slens.dev" }, - "devDependencies": { - "@types/node": "^14.14.6", + "dependencies": { + "@types/node": "*", + "@types/react-select": "*", + "@material-ui/core": "*", "conf": "^7.0.1" } } diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts index 041a8b9158..eb0680dd00 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster-detectors/distribution-detector.ts @@ -36,10 +36,26 @@ export class DistributionDetector extends BaseClusterDetector { return { value: "digitalocean", accuracy: 90}; } + if (this.isVMWare()) { + return { value: "vmware", accuracy: 90}; + } + if (this.isMirantis()) { return { value: "mirantis", accuracy: 90}; } + if (this.isAlibaba()) { + return { value: "alibaba", accuracy: 90}; + } + + if (this.isHuawei()) { + return { value: "huawei", accuracy: 90}; + } + + if (this.isTke()) { + return { value: "tencent", accuracy: 90}; + } + if (this.isMinikube()) { return { value: "minikube", accuracy: 80}; } @@ -115,10 +131,18 @@ export class DistributionDetector extends BaseClusterDetector { return this.cluster.contextName === "docker-desktop"; } + protected isTke() { + return this.version.includes("-tke."); + } + protected isCustom() { return this.version.includes("+"); } + protected isVMWare() { + return this.version.includes("+vmware"); + } + protected isRke() { return this.version.includes("-rancher"); } @@ -127,6 +151,14 @@ export class DistributionDetector extends BaseClusterDetector { return this.version.includes("+k3s"); } + protected isAlibaba() { + return this.version.includes("-aliyun"); + } + + protected isHuawei() { + return this.version.includes("-CCE"); + } + protected async isOpenshift() { try { const response = await this.k8sRequest(""); diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 79e28fa9c4..b9ff62e8ac 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -11,10 +11,11 @@ import { Kubectl } from "./kubectl"; import { KubeconfigManager } from "./kubeconfig-manager"; import { loadConfig } from "../common/kube-helpers"; import request, { RequestPromiseOptions } from "request-promise-native"; -import { apiResources } from "../common/rbac"; +import { apiResources, KubeApiResource } from "../common/rbac"; import logger from "./logger"; import { VersionDetector } from "./cluster-detectors/version-detector"; import { detectorRegistry } from "./cluster-detectors/detector-registry"; +import plimit from "p-limit"; export enum ClusterStatus { AccessGranted = 2, @@ -78,6 +79,7 @@ export class Cluster implements ClusterModel, ClusterState { protected kubeconfigManager: KubeconfigManager; protected eventDisposers: Function[] = []; protected activated = false; + private resourceAccessStatuses: Map = new Map(); whenInitialized = when(() => this.initialized); whenReady = when(() => this.ready); @@ -379,6 +381,7 @@ export class Cluster implements ClusterModel, ClusterState { this.accessible = false; this.ready = false; this.activated = false; + this.resourceAccessStatuses.clear(); this.pushState(); } @@ -484,6 +487,8 @@ export class Cluster implements ClusterModel, ClusterState { this.metadata.version = versionData.value; + this.failureReason = null; + return ClusterStatus.AccessGranted; } catch (error) { logger.error(`Failed to connect cluster "${this.contextName}": ${error}`); @@ -643,17 +648,30 @@ export class Cluster implements ClusterModel, ClusterState { if (!this.allowedNamespaces.length) { return []; } - const resourceAccessStatuses = await Promise.all( - apiResources.map(apiResource => this.canI({ - resource: apiResource.resource, - group: apiResource.group, - verb: "list", - namespace: this.allowedNamespaces[0] - })) - ); + const resources = apiResources.filter((resource) => this.resourceAccessStatuses.get(resource) === undefined); + const apiLimit = plimit(5); // 5 concurrent api requests + const requests = []; + + for (const apiResource of resources) { + requests.push(apiLimit(async () => { + for (const namespace of this.allowedNamespaces.slice(0, 10)) { + if (!this.resourceAccessStatuses.get(apiResource)) { + const result = await this.canI({ + resource: apiResource.resource, + group: apiResource.group, + verb: "list", + namespace + }); + + this.resourceAccessStatuses.set(apiResource, result); + } + } + })); + } + await Promise.all(requests); return apiResources - .filter((resource, i) => resourceAccessStatuses[i]) + .filter((resource) => this.resourceAccessStatuses.get(resource)) .map(apiResource => apiResource.resource); } catch (error) { return []; diff --git a/src/main/index.ts b/src/main/index.ts index 8da9be1a01..b0ba60d029 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -103,7 +103,6 @@ app.on("ready", async () => { } extensionLoader.init(); - extensionDiscovery.init(); windowManager = WindowManager.getInstance(proxyPort); @@ -111,6 +110,9 @@ app.on("ready", async () => { try { const extensions = await extensionDiscovery.load(); + // Start watching after bundled extensions are loaded + extensionDiscovery.watchExtensions(); + // Subscribe to extensions that are copied or deleted to/from the extensions folder extensionDiscovery.events.on("add", (extension: InstalledExtension) => { extensionLoader.addExtension(extension); @@ -122,6 +124,8 @@ app.on("ready", async () => { extensionLoader.initExtensions(extensions); } catch (error) { dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`); + console.error(error); + console.trace(); } setTimeout(() => { diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 0a96ee354b..ebfd2a6a98 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -22,10 +22,11 @@ const kubectlMap: Map = new Map([ ["1.13", "1.13.12"], ["1.14", "1.14.10"], ["1.15", "1.15.11"], - ["1.16", "1.16.14"], + ["1.16", "1.16.15"], ["1.17", bundledVersion], - ["1.18", "1.18.8"], - ["1.19", "1.19.0"] + ["1.18", "1.18.14"], + ["1.19", "1.19.5"], + ["1.20", "1.20.0"] ]); const packageMirrors: Map = new Map([ ["default", "https://storage.googleapis.com/kubernetes-release/release"], diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 5770429a7e..e4f6ab4a34 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -120,6 +120,14 @@ export class LensProxy { protected createProxy(): httpProxy { const proxy = httpProxy.createProxyServer(); + proxy.on("proxyRes", (proxyRes, req) => { + const retryCounterId = this.getRequestId(req); + + if (this.retryCounters.has(retryCounterId)) { + this.retryCounters.delete(retryCounterId); + } + }); + proxy.on("error", (error, req, res, target) => { if (this.closed) { return; diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index 19170695fc..be04649a31 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -120,6 +120,7 @@ export class ShellSession extends EventEmitter { if(path.basename(env["PTYSHELL"]) === "zsh") { env["OLD_ZDOTDIR"] = env.ZDOTDIR || env.HOME; env["ZDOTDIR"] = this.kubectlBinDir; + env["DISABLE_AUTO_UPDATE"] = "true"; } env["PTYPID"] = process.pid.toString(); diff --git a/src/renderer/components/+custom-resources/crd-resource-details.tsx b/src/renderer/components/+custom-resources/crd-resource-details.tsx index 6610fb4b56..412293eee1 100644 --- a/src/renderer/components/+custom-resources/crd-resource-details.tsx +++ b/src/renderer/components/+custom-resources/crd-resource-details.tsx @@ -13,6 +13,7 @@ import { crdStore } from "./crd.store"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { Input } from "../input"; import { AdditionalPrinterColumnsV1, CustomResourceDefinition } from "../../api/endpoints/crd.api"; +import { parseJsonPath } from "../../utils/jsonPath"; interface Props extends KubeObjectDetailsProps { } @@ -46,7 +47,7 @@ export class CrdResourceDetails extends React.Component { renderAdditionalColumns(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) { return columns.map(({ name, jsonPath: jp }) => ( - {convertSpecValue(jsonPath.value(crd, jp.slice(1)))} + {convertSpecValue(jsonPath.value(crd, parseJsonPath(jp.slice(1))))} )); } diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index cca7ac7015..72209c80f7 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -12,6 +12,7 @@ import { autorun, computed } from "mobx"; import { crdStore } from "./crd.store"; import { TableSortCallback } from "../table"; import { apiManager } from "../../api/api-manager"; +import { parseJsonPath } from "../../utils/jsonPath"; interface Props extends RouteComponentProps { } @@ -61,7 +62,7 @@ export class CrdResources extends React.Component { }; extraColumns.forEach(column => { - sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.value(item, column.jsonPath.slice(1)); + sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.value(item, parseJsonPath(column.jsonPath.slice(1))); }); return ( @@ -91,10 +92,18 @@ export class CrdResources extends React.Component { renderTableContents={(crdInstance: KubeObject) => [ crdInstance.getName(), isNamespaced && crdInstance.getNs(), - ...extraColumns.map(column => ({ - renderBoolean: true, - children: JSON.stringify(jsonPath.value(crdInstance, column.jsonPath.slice(1))), - })), + ...extraColumns.map((column) => { + let value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1))); + + if (Array.isArray(value) || typeof value === "object") { + value = JSON.stringify(value); + } + + return { + renderBoolean: true, + children: value, + }; + }), crdInstance.getAge(), ]} /> diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index 79c82bd48d..0f0d79e47d 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -43,10 +43,10 @@ export class NamespaceStore extends KubeObjectStore { } subscribe(apis = [this.api]) { - const { allowedNamespaces } = getHostedCluster(); + const { accessibleNamespaces } = getHostedCluster(); // if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted - if (allowedNamespaces.length > 0) { + if (accessibleNamespaces.length > 0) { return () => { return; }; } diff --git a/src/renderer/components/+nodes/nodes.tsx b/src/renderer/components/+nodes/nodes.tsx index da0e9e8c4c..32885a4c66 100644 --- a/src/renderer/components/+nodes/nodes.tsx +++ b/src/renderer/components/+nodes/nodes.tsx @@ -134,7 +134,7 @@ export class Nodes extends React.Component { { @observer export class WorkloadsOverview extends React.Component { - @observable isReady = false; @observable isUnmounting = false; async componentDidMount() { @@ -61,10 +59,13 @@ export class WorkloadsOverview extends React.Component { if (isAllowedResource("events")) { stores.push(eventStore); } - this.isReady = stores.every(store => store.isLoaded); - await Promise.all(stores.map(store => store.loadAll())); - this.isReady = true; - const unsubscribeList = stores.map(store => store.subscribe()); + + const unsubscribeList: Array<() => void> = []; + + for (const store of stores) { + await store.loadAll(); + unsubscribeList.push(store.subscribe()); + } await when(() => this.isUnmounting); unsubscribeList.forEach(dispose => dispose()); @@ -74,11 +75,7 @@ export class WorkloadsOverview extends React.Component { this.isUnmounting = true; } - renderContents() { - if (!this.isReady) { - return ; - } - + get contents() { return ( <> @@ -94,7 +91,7 @@ export class WorkloadsOverview extends React.Component { render() { return (
- {this.renderContents()} + {this.contents}
); } diff --git a/src/renderer/components/dock/info-panel.tsx b/src/renderer/components/dock/info-panel.tsx index 4f36d47fc3..34e456fdd6 100644 --- a/src/renderer/components/dock/info-panel.tsx +++ b/src/renderer/components/dock/info-panel.tsx @@ -27,6 +27,7 @@ interface OptionalProps { showSubmitClose?: boolean; showInlineInfo?: boolean; showNotifications?: boolean; + showStatusPanel?: boolean; } @observer @@ -38,6 +39,7 @@ export class InfoPanel extends Component { showSubmitClose: true, showInlineInfo: true, showNotifications: true, + showStatusPanel: true, }; @observable error = ""; @@ -93,7 +95,7 @@ export class InfoPanel extends Component { } render() { - const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose } = this.props; + const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose, showStatusPanel } = this.props; const { submit, close, submitAndClose, waiting } = this; const isDisabled = !!(disableSubmit || waiting || error); @@ -102,9 +104,11 @@ export class InfoPanel extends Component {
{controls}
-
- {waiting ? <> {submittingMessage} : this.renderErrorIcon()} -
+ {showStatusPanel && ( +
+ {waiting ? <> {submittingMessage} : this.renderErrorIcon()} +
+ )} {showButtons && ( <>