From d8e088f352b63550108094fe1f3a8e595abb902b Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 19 Jan 2021 14:15:37 +0200 Subject: [PATCH 01/56] fix: chart.digest is the same for all charts and not suited as unique id (#1964) Signed-off-by: Roman --- src/renderer/api/endpoints/helm-charts.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() { From 1e8359851c1276ef75aef5b2b0093571810aeafa Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Wed, 20 Jan 2021 13:17:16 +0200 Subject: [PATCH 02/56] Upgrade shell-env to 3.0.1 (#1994) Signed-off-by: Lauri Nevala --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 798bfb7193..57c7a8a657 100644 --- a/package.json +++ b/package.json @@ -219,7 +219,7 @@ "request-promise-native": "^1.0.8", "semver": "^7.3.2", "serializr": "^2.0.3", - "shell-env": "^3.0.0", + "shell-env": "^3.0.1", "spdy": "^4.0.2", "tar": "^6.0.5", "tcp-port-used": "^1.0.1", diff --git a/yarn.lock b/yarn.lock index dc50f585ef..4cfa01fdf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12141,10 +12141,10 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-env@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shell-env/-/shell-env-3.0.0.tgz#42484ebd0798ee321ba69f6151f2aeab13fde1d4" - integrity sha512-zE0lGldowbCLnnorLnOUO6gLSwEoW4u+qWcEV1HH2qje5sIg0PvBd+8ro74EgSZv0MBEP2dROD6vSKhGDbUIMQ== +shell-env@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shell-env/-/shell-env-3.0.1.tgz#515a62f6cbd5e139365be2535745e8e53438ce77" + integrity sha512-b09fpMipAQ9ObwvIeKoQFLDXcEcCpYUUZanlad4OYQscw2I49C/u97OPQg9jWYo36bRDn62fbe07oWYqovIvKA== dependencies: default-shell "^1.0.1" execa "^1.0.0" From 55759fb3b8e7061f67ff1414160430c7910fbe27 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 20 Jan 2021 14:31:20 +0200 Subject: [PATCH 03/56] Add age column to cluster overview (#1970) Signed-off-by: Alex Culliere --- .../components/+cluster/cluster-issues.tsx | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/renderer/components/+cluster/cluster-issues.tsx b/src/renderer/components/+cluster/cluster-issues.tsx index eb85bf79f8..0aabebaa27 100644 --- a/src/renderer/components/+cluster/cluster-issues.tsx +++ b/src/renderer/components/+cluster/cluster-issues.tsx @@ -23,11 +23,13 @@ interface IWarning extends ItemObject { kind: string; message: string; selfLink: string; + age: string | number; } enum sortBy { type = "type", - object = "object" + object = "object", + age = "age", } @observer @@ -35,6 +37,7 @@ export class ClusterIssues extends React.Component { private sortCallbacks = { [sortBy.type]: (warning: IWarning) => warning.kind, [sortBy.object]: (warning: IWarning) => warning.getName(), + [sortBy.age]: (warning: IWarning) => warning.age || "", }; @computed get warnings() { @@ -42,15 +45,16 @@ export class ClusterIssues extends React.Component { // Node bad conditions nodesStore.items.forEach(node => { - const { kind, selfLink, getId, getName } = node; + const { kind, selfLink, getId, getName, getAge } = node; node.getWarningConditions().forEach(({ message }) => { warnings.push({ - kind, + age: getAge(), getId, getName, - selfLink, + kind, message, + selfLink, }); }); }); @@ -59,12 +63,13 @@ export class ClusterIssues extends React.Component { const events = eventStore.getWarnings(); events.forEach(error => { - const { message, involvedObject } = error; + const { message, involvedObject, getAge } = error; const { uid, name, kind } = involvedObject; warnings.push({ getId: () => uid, getName: () => name, + age: getAge(), message, kind, selfLink: lookupApiLink(involvedObject, error), @@ -78,7 +83,7 @@ export class ClusterIssues extends React.Component { getTableRow(uid: string) { const { warnings } = this; const warning = warnings.find(warn => warn.getId() == uid); - const { getId, getName, message, kind, selfLink } = warning; + const { getId, getName, message, kind, selfLink, age } = warning; return ( { {kind} + + {age} + ); } @@ -139,6 +147,7 @@ export class ClusterIssues extends React.Component { Message Object Type + Age From 64be4ee948344c427cd23aecd8c3ef144063def7 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Thu, 21 Jan 2021 10:38:49 +0300 Subject: [PATCH 04/56] Fixing log tab layout colors (#1995) * Making "since" date visible as bolded text Signed-off-by: Alex Andreev * Fixing colors in log tab elements Signed-off-by: Alex Andreev --- src/renderer/components/dock/info-panel.scss | 14 +++++++++----- src/renderer/components/dock/log-controls.tsx | 7 ++++++- src/renderer/components/select/select.scss | 5 +---- src/renderer/themes/lens-dark.json | 2 ++ src/renderer/themes/lens-light.json | 4 +++- src/renderer/themes/theme-vars.scss | 4 +++- 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/renderer/components/dock/info-panel.scss b/src/renderer/components/dock/info-panel.scss index 482dbee02d..cf9b3268b2 100644 --- a/src/renderer/components/dock/info-panel.scss +++ b/src/renderer/components/dock/info-panel.scss @@ -1,12 +1,16 @@ .InfoPanel { @include hidden-scrollbar; - background: $dockInfoBackground; - padding: $padding $padding * 2; + background: var(--dockInfoBackground); + padding: var(--padding) calc(var(--padding) * 2); flex-shrink: 0; .Spinner { - margin-right: $padding; + margin-right: var(--padding); + } + + .Badge { + background-color: var(--dockBadgeBackground); } > .controls { @@ -15,8 +19,8 @@ &:not(:empty) + .info { min-height: 25px; - padding-left: $padding; - padding-right: $padding; + padding-left: var(--padding); + padding-right: var(--padding); } } } \ No newline at end of file diff --git a/src/renderer/components/dock/log-controls.tsx b/src/renderer/components/dock/log-controls.tsx index cedff7fbb9..06bbf12863 100644 --- a/src/renderer/components/dock/log-controls.tsx +++ b/src/renderer/components/dock/log-controls.tsx @@ -41,7 +41,12 @@ export const LogControls = observer((props: Props) => { return (
- {since && `Logs from ${new Date(since[0]).toLocaleString()}`} + {since && ( + + Logs from{" "} + {new Date(since[0]).toLocaleString()} + + )}
Date: Thu, 21 Jan 2021 08:09:41 -0500 Subject: [PATCH 05/56] enfore unix line endings and always ending files with line endings (#1997) Signed-off-by: Sebastian Malton --- .eslintrc.js | 6 ++++++ __mocks__/imageMock.ts | 2 +- __mocks__/styleMock.ts | 2 +- extensions/kube-object-event-status/src/resolver.tsx | 2 +- src/common/__tests__/search-store.test.ts | 2 +- src/common/__tests__/user-store.test.ts | 2 +- src/common/custom-errors.ts | 2 +- src/common/kube-helpers.ts | 2 +- src/common/prometheus-providers.ts | 2 +- src/common/search-store.ts | 2 +- src/common/utils/__tests__/splitArray.test.ts | 2 +- src/common/utils/singleton.ts | 2 +- src/extensions/interfaces/index.ts | 2 +- src/extensions/interfaces/registrations.ts | 2 +- src/extensions/renderer-api/kube-object-status.ts | 2 +- src/extensions/renderer-api/theming.ts | 2 +- src/main/cluster-detectors/base-cluster-detector.ts | 2 +- src/main/cluster-detectors/cluster-id-detector.ts | 2 +- src/main/cluster-detectors/detector-registry.ts | 2 +- src/main/cluster-detectors/last-seen-detector.ts | 2 +- src/main/cluster-detectors/nodes-count-detector.ts | 2 +- src/main/cluster-detectors/version-detector.ts | 2 +- src/migrations/cluster-store/index.ts | 2 +- src/renderer/api/__tests__/kube-api.test.ts | 2 +- src/renderer/api/workload-kube-object.ts | 2 +- .../components/+apps-helm-charts/helm-charts.route.ts | 2 +- src/renderer/components/+apps-helm-charts/index.ts | 2 +- src/renderer/components/+apps-releases/index.ts | 2 +- src/renderer/components/+apps/index.ts | 2 +- .../components/cluster-home-dir-setting.tsx | 2 +- .../+cluster-settings/components/cluster-name-setting.tsx | 2 +- .../+cluster-settings/components/cluster-proxy-setting.tsx | 2 +- src/renderer/components/+cluster-settings/general.tsx | 2 +- src/renderer/components/+cluster-settings/removal.tsx | 2 +- src/renderer/components/+cluster-settings/status.tsx | 2 +- src/renderer/components/+cluster/cluster-metrics.tsx | 2 +- src/renderer/components/+landing-page/index.tsx | 2 +- src/renderer/components/+pod-security-policies/index.ts | 2 +- .../components/+user-management-roles/roles.store.ts | 2 +- .../components/+user-management-service-accounts/index.ts | 2 +- src/renderer/components/+user-management/index.ts | 2 +- src/renderer/components/+workloads-pods/index.ts | 2 +- .../components/+workloads-pods/pod-details-statuses.tsx | 2 +- src/renderer/components/+workloads-statefulsets/index.ts | 2 +- src/renderer/components/ace-editor/ace-editor.tsx | 2 +- src/renderer/components/ace-editor/index.ts | 2 +- src/renderer/components/add-remove-buttons/index.ts | 2 +- src/renderer/components/animate/index.ts | 2 +- src/renderer/components/chart/background-block.plugin.ts | 2 +- src/renderer/components/chart/bar-chart.tsx | 2 +- src/renderer/components/chart/chart.tsx | 2 +- src/renderer/components/chart/index.ts | 2 +- src/renderer/components/chart/pie-chart.tsx | 2 +- src/renderer/components/chart/useRealTimeMetrics.ts | 2 +- src/renderer/components/chart/zebra-stripes.plugin.ts | 2 +- src/renderer/components/checkbox/checkbox.tsx | 2 +- src/renderer/components/checkbox/index.ts | 2 +- src/renderer/components/cluster-icon/index.ts | 2 +- src/renderer/components/confirm-dialog/index.ts | 2 +- src/renderer/components/dock/dock-tabs.tsx | 2 +- src/renderer/components/editable-list/index.ts | 2 +- src/renderer/components/error-boundary/index.ts | 2 +- src/renderer/components/file-picker/file-picker.tsx | 2 +- src/renderer/components/file-picker/index.ts | 2 +- src/renderer/components/icon/index.ts | 2 +- src/renderer/components/input/search-input-url.tsx | 2 +- src/renderer/components/item-object-list/index.tsx | 2 +- src/renderer/components/kube-object-status-icon/index.ts | 2 +- src/renderer/components/kubeconfig-dialog/index.ts | 2 +- .../components/layout/__test__/main-layout-header.test.tsx | 2 +- src/renderer/components/layout/login-layout.tsx | 2 +- src/renderer/components/layout/sidebar-context.ts | 2 +- src/renderer/components/line-progress/index.ts | 2 +- src/renderer/components/markdown-viewer/index.ts | 2 +- src/renderer/components/markdown-viewer/markdown-viewer.tsx | 2 +- src/renderer/components/radio/index.ts | 2 +- src/renderer/components/resource-metrics/index.ts | 2 +- src/renderer/components/slider/index.ts | 2 +- src/renderer/components/status-brick/index.ts | 2 +- src/renderer/components/status-brick/status-brick.tsx | 2 +- src/renderer/components/virtual-list/index.ts | 2 +- src/renderer/components/wizard/index.ts | 2 +- src/renderer/hooks/useInterval.ts | 2 +- src/renderer/hooks/useOnUnmount.ts | 2 +- src/renderer/hooks/useStorage.ts | 2 +- src/renderer/navigation/events.ts | 2 +- src/renderer/navigation/helpers.ts | 2 +- src/renderer/navigation/index.ts | 2 +- src/renderer/utils/__tests__/metricUnitsToNumber.test.ts | 2 +- src/renderer/utils/formatDuration.ts | 2 +- src/renderer/utils/jsonPath.ts | 2 +- types/command-exists.d.ts | 2 +- 92 files changed, 97 insertions(+), 91 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 3fd52c2465..57ee07348f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,6 +46,8 @@ module.exports = { "avoidEscape": true, "allowTemplateLiterals": true, }], + "linebreak-style": ["error", "unix"], + "eol-last": ["error", "always"], "semi": ["error", "always"], "object-shorthand": "error", "prefer-template": "error", @@ -101,6 +103,8 @@ module.exports = { }], "semi": "off", "@typescript-eslint/semi": ["error"], + "linebreak-style": ["error", "unix"], + "eol-last": ["error", "always"], "object-shorthand": "error", "prefer-template": "error", "template-curly-spacing": "error", @@ -162,6 +166,8 @@ module.exports = { }], "semi": "off", "@typescript-eslint/semi": ["error"], + "linebreak-style": ["error", "unix"], + "eol-last": ["error", "always"], "object-shorthand": "error", "prefer-template": "error", "template-curly-spacing": "error", diff --git a/__mocks__/imageMock.ts b/__mocks__/imageMock.ts index a099545376..f053ebf797 100644 --- a/__mocks__/imageMock.ts +++ b/__mocks__/imageMock.ts @@ -1 +1 @@ -module.exports = {}; \ No newline at end of file +module.exports = {}; diff --git a/__mocks__/styleMock.ts b/__mocks__/styleMock.ts index a099545376..f053ebf797 100644 --- a/__mocks__/styleMock.ts +++ b/__mocks__/styleMock.ts @@ -1 +1 @@ -module.exports = {}; \ No newline at end of file +module.exports = {}; diff --git a/extensions/kube-object-event-status/src/resolver.tsx b/extensions/kube-object-event-status/src/resolver.tsx index 69691c2e79..5e9151288f 100644 --- a/extensions/kube-object-event-status/src/resolver.tsx +++ b/extensions/kube-object-event-status/src/resolver.tsx @@ -56,4 +56,4 @@ export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeOb text: `${event.message}`, timestamp: event.metadata.creationTimestamp }; -} \ No newline at end of file +} diff --git a/src/common/__tests__/search-store.test.ts b/src/common/__tests__/search-store.test.ts index 7939ef1d8c..d361c858fd 100644 --- a/src/common/__tests__/search-store.test.ts +++ b/src/common/__tests__/search-store.test.ts @@ -77,4 +77,4 @@ describe("search store tests", () => { searchStore.onSearch(logs, "Starting"); expect(searchStore.totalFinds).toBe(2); }); -}); \ No newline at end of file +}); diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index 08ca359ce5..b74941a790 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -101,4 +101,4 @@ describe("user store tests", () => { expect(us.lastSeenAppVersion).toBe("0.0.0"); }); }); -}); \ No newline at end of file +}); diff --git a/src/common/custom-errors.ts b/src/common/custom-errors.ts index 9bcf3a998a..177ef7578f 100644 --- a/src/common/custom-errors.ts +++ b/src/common/custom-errors.ts @@ -10,4 +10,4 @@ export class ExecValidationNotFoundError extends Error { this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); } -} \ No newline at end of file +} diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index bb0e6b86d2..02a9faef92 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -175,4 +175,4 @@ export function validateKubeConfig (config: KubeConfig) { throw new ExecValidationNotFoundError(execCommand, isAbsolute); } } -} \ No newline at end of file +} diff --git a/src/common/prometheus-providers.ts b/src/common/prometheus-providers.ts index a5c515b338..5496163c38 100644 --- a/src/common/prometheus-providers.ts +++ b/src/common/prometheus-providers.ts @@ -10,4 +10,4 @@ import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry PrometheusProviderRegistry.registerProvider(provider.id, provider); }); -export const prometheusProviders = PrometheusProviderRegistry.getProviders(); \ No newline at end of file +export const prometheusProviders = PrometheusProviderRegistry.getProviders(); diff --git a/src/common/search-store.ts b/src/common/search-store.ts index a3aba9dcbe..eb2517ca0f 100644 --- a/src/common/search-store.ts +++ b/src/common/search-store.ts @@ -133,4 +133,4 @@ export class SearchStore { } } -export const searchStore = new SearchStore; \ No newline at end of file +export const searchStore = new SearchStore; diff --git a/src/common/utils/__tests__/splitArray.test.ts b/src/common/utils/__tests__/splitArray.test.ts index a401e07701..1e1589fee2 100644 --- a/src/common/utils/__tests__/splitArray.test.ts +++ b/src/common/utils/__tests__/splitArray.test.ts @@ -28,4 +28,4 @@ describe("split array on element tests", () => { test("ten elements, in end array", () => { expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]); }); -}); \ No newline at end of file +}); diff --git a/src/common/utils/singleton.ts b/src/common/utils/singleton.ts index 61269d10b1..caa5471072 100644 --- a/src/common/utils/singleton.ts +++ b/src/common/utils/singleton.ts @@ -26,4 +26,4 @@ class Singleton { } export { Singleton }; -export default Singleton; \ No newline at end of file +export default Singleton; diff --git a/src/extensions/interfaces/index.ts b/src/extensions/interfaces/index.ts index c91d8cdd19..7b1c601537 100644 --- a/src/extensions/interfaces/index.ts +++ b/src/extensions/interfaces/index.ts @@ -1 +1 @@ -export * from "./registrations"; \ No newline at end of file +export * from "./registrations"; diff --git a/src/extensions/interfaces/registrations.ts b/src/extensions/interfaces/registrations.ts index 47c63062ea..ff51d9a824 100644 --- a/src/extensions/interfaces/registrations.ts +++ b/src/extensions/interfaces/registrations.ts @@ -5,4 +5,4 @@ export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../re export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry"; export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry"; export type { PageMenuRegistration, ClusterPageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry"; -export type { StatusBarRegistration } from "../registries/status-bar-registry"; \ No newline at end of file +export type { StatusBarRegistration } from "../registries/status-bar-registry"; diff --git a/src/extensions/renderer-api/kube-object-status.ts b/src/extensions/renderer-api/kube-object-status.ts index f609d736fe..616ead1bb2 100644 --- a/src/extensions/renderer-api/kube-object-status.ts +++ b/src/extensions/renderer-api/kube-object-status.ts @@ -8,4 +8,4 @@ export enum KubeObjectStatusLevel { INFO = 1, WARNING = 2, CRITICAL = 3 -} \ No newline at end of file +} diff --git a/src/extensions/renderer-api/theming.ts b/src/extensions/renderer-api/theming.ts index f819036803..b3da69bdbc 100644 --- a/src/extensions/renderer-api/theming.ts +++ b/src/extensions/renderer-api/theming.ts @@ -2,4 +2,4 @@ import { themeStore } from "../../renderer/theme.store"; export function getActiveTheme() { return themeStore.activeTheme; -} \ No newline at end of file +} diff --git a/src/main/cluster-detectors/base-cluster-detector.ts b/src/main/cluster-detectors/base-cluster-detector.ts index 9d52e1a70e..885f96c33e 100644 --- a/src/main/cluster-detectors/base-cluster-detector.ts +++ b/src/main/cluster-detectors/base-cluster-detector.ts @@ -31,4 +31,4 @@ export class BaseClusterDetector { }, }); } -} \ No newline at end of file +} diff --git a/src/main/cluster-detectors/cluster-id-detector.ts b/src/main/cluster-detectors/cluster-id-detector.ts index 2e0cc694ff..810955afae 100644 --- a/src/main/cluster-detectors/cluster-id-detector.ts +++ b/src/main/cluster-detectors/cluster-id-detector.ts @@ -23,4 +23,4 @@ export class ClusterIdDetector extends BaseClusterDetector { return response.metadata.uid; } -} \ No newline at end of file +} diff --git a/src/main/cluster-detectors/detector-registry.ts b/src/main/cluster-detectors/detector-registry.ts index 43c56153c9..b1d1b73447 100644 --- a/src/main/cluster-detectors/detector-registry.ts +++ b/src/main/cluster-detectors/detector-registry.ts @@ -48,4 +48,4 @@ detectorRegistry.add(ClusterIdDetector); detectorRegistry.add(LastSeenDetector); detectorRegistry.add(VersionDetector); detectorRegistry.add(DistributionDetector); -detectorRegistry.add(NodesCountDetector); \ No newline at end of file +detectorRegistry.add(NodesCountDetector); diff --git a/src/main/cluster-detectors/last-seen-detector.ts b/src/main/cluster-detectors/last-seen-detector.ts index e648d5f2f9..0a9bcf9f74 100644 --- a/src/main/cluster-detectors/last-seen-detector.ts +++ b/src/main/cluster-detectors/last-seen-detector.ts @@ -11,4 +11,4 @@ export class LastSeenDetector extends BaseClusterDetector { return { value: new Date().toJSON(), accuracy: 100 }; } -} \ No newline at end of file +} diff --git a/src/main/cluster-detectors/nodes-count-detector.ts b/src/main/cluster-detectors/nodes-count-detector.ts index 0ece5dd080..45584df5bd 100644 --- a/src/main/cluster-detectors/nodes-count-detector.ts +++ b/src/main/cluster-detectors/nodes-count-detector.ts @@ -16,4 +16,4 @@ export class NodesCountDetector extends BaseClusterDetector { return response.items.length; } -} \ No newline at end of file +} diff --git a/src/main/cluster-detectors/version-detector.ts b/src/main/cluster-detectors/version-detector.ts index 8080ef57a1..b19979db8a 100644 --- a/src/main/cluster-detectors/version-detector.ts +++ b/src/main/cluster-detectors/version-detector.ts @@ -16,4 +16,4 @@ export class VersionDetector extends BaseClusterDetector { return response.gitVersion; } -} \ No newline at end of file +} diff --git a/src/migrations/cluster-store/index.ts b/src/migrations/cluster-store/index.ts index c546fdaeda..4a71d4f7ad 100644 --- a/src/migrations/cluster-store/index.ts +++ b/src/migrations/cluster-store/index.ts @@ -18,4 +18,4 @@ export default { ...version270Beta1, ...version360Beta1, ...snap -}; \ No newline at end of file +}; diff --git a/src/renderer/api/__tests__/kube-api.test.ts b/src/renderer/api/__tests__/kube-api.test.ts index 41078e77a3..7481bd096a 100644 --- a/src/renderer/api/__tests__/kube-api.test.ts +++ b/src/renderer/api/__tests__/kube-api.test.ts @@ -79,4 +79,4 @@ describe("KubeApi", () => { expect(kubeApi.apiPrefix).toEqual("/apis"); expect(kubeApi.apiGroup).toEqual("extensions"); }); -}); \ No newline at end of file +}); diff --git a/src/renderer/api/workload-kube-object.ts b/src/renderer/api/workload-kube-object.ts index e0c6d3f121..c6786aa99f 100644 --- a/src/renderer/api/workload-kube-object.ts +++ b/src/renderer/api/workload-kube-object.ts @@ -82,4 +82,4 @@ export class WorkloadKubeObject extends KubeObject { return Object.keys(affinity).length; } -} \ No newline at end of file +} diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.route.ts b/src/renderer/components/+apps-helm-charts/helm-charts.route.ts index 9b8aecc499..97a0923d97 100644 --- a/src/renderer/components/+apps-helm-charts/helm-charts.route.ts +++ b/src/renderer/components/+apps-helm-charts/helm-charts.route.ts @@ -11,4 +11,4 @@ export interface IHelmChartsRouteParams { repo?: string; } -export const helmChartsURL = buildURL(helmChartsRoute.path); \ No newline at end of file +export const helmChartsURL = buildURL(helmChartsRoute.path); diff --git a/src/renderer/components/+apps-helm-charts/index.ts b/src/renderer/components/+apps-helm-charts/index.ts index a9403c097c..c0649f3f38 100644 --- a/src/renderer/components/+apps-helm-charts/index.ts +++ b/src/renderer/components/+apps-helm-charts/index.ts @@ -1,2 +1,2 @@ export * from "./helm-charts"; -export * from "./helm-charts.route"; \ No newline at end of file +export * from "./helm-charts.route"; diff --git a/src/renderer/components/+apps-releases/index.ts b/src/renderer/components/+apps-releases/index.ts index 32a4871769..bd80c60404 100644 --- a/src/renderer/components/+apps-releases/index.ts +++ b/src/renderer/components/+apps-releases/index.ts @@ -1,2 +1,2 @@ export * from "./releases"; -export * from "./release.route"; \ No newline at end of file +export * from "./release.route"; diff --git a/src/renderer/components/+apps/index.ts b/src/renderer/components/+apps/index.ts index 70c0169777..330891b2b1 100644 --- a/src/renderer/components/+apps/index.ts +++ b/src/renderer/components/+apps/index.ts @@ -1,2 +1,2 @@ export * from "./apps"; -export * from "./apps.route"; \ No newline at end of file +export * from "./apps.route"; diff --git a/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx index 35c18cc5e5..10aabf3ff7 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-home-dir-setting.tsx @@ -48,4 +48,4 @@ export class ClusterHomeDirSetting extends React.Component { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx index 631c6d54ef..9d953ef9ca 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-name-setting.tsx @@ -45,4 +45,4 @@ export class ClusterNameSetting extends React.Component { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx index 3887487816..eb122ac444 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-proxy-setting.tsx @@ -45,4 +45,4 @@ export class ClusterProxySetting extends React.Component { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/+cluster-settings/general.tsx b/src/renderer/components/+cluster-settings/general.tsx index 1d498bc94b..91fce05164 100644 --- a/src/renderer/components/+cluster-settings/general.tsx +++ b/src/renderer/components/+cluster-settings/general.tsx @@ -25,4 +25,4 @@ export class General extends React.Component {
; } -} \ No newline at end of file +} diff --git a/src/renderer/components/+cluster-settings/removal.tsx b/src/renderer/components/+cluster-settings/removal.tsx index 7d97e9c515..495fb71fe8 100644 --- a/src/renderer/components/+cluster-settings/removal.tsx +++ b/src/renderer/components/+cluster-settings/removal.tsx @@ -17,4 +17,4 @@ export class Removal extends React.Component {
); } -} \ No newline at end of file +} diff --git a/src/renderer/components/+cluster-settings/status.tsx b/src/renderer/components/+cluster-settings/status.tsx index 7f21d19aba..d43cfe5c35 100644 --- a/src/renderer/components/+cluster-settings/status.tsx +++ b/src/renderer/components/+cluster-settings/status.tsx @@ -58,4 +58,4 @@ export class Status extends React.Component { ; } -} \ No newline at end of file +} diff --git a/src/renderer/components/+cluster/cluster-metrics.tsx b/src/renderer/components/+cluster/cluster-metrics.tsx index 6461bae7f3..2bdaded8b7 100644 --- a/src/renderer/components/+cluster/cluster-metrics.tsx +++ b/src/renderer/components/+cluster/cluster-metrics.tsx @@ -95,4 +95,4 @@ export const ClusterMetrics = observer(() => { {renderMetrics()} ); -}); \ No newline at end of file +}); diff --git a/src/renderer/components/+landing-page/index.tsx b/src/renderer/components/+landing-page/index.tsx index 4bdb2a706c..c7eacf1bd0 100644 --- a/src/renderer/components/+landing-page/index.tsx +++ b/src/renderer/components/+landing-page/index.tsx @@ -1,2 +1,2 @@ export * from "./landing-page.route"; -export * from "./landing-page"; \ No newline at end of file +export * from "./landing-page"; diff --git a/src/renderer/components/+pod-security-policies/index.ts b/src/renderer/components/+pod-security-policies/index.ts index d037873b5b..c9379d3381 100644 --- a/src/renderer/components/+pod-security-policies/index.ts +++ b/src/renderer/components/+pod-security-policies/index.ts @@ -1,3 +1,3 @@ export * from "./pod-security-policies.route"; export * from "./pod-security-policies"; -export * from "./pod-security-policy-details"; \ No newline at end of file +export * from "./pod-security-policy-details"; diff --git a/src/renderer/components/+user-management-roles/roles.store.ts b/src/renderer/components/+user-management-roles/roles.store.ts index 6af33deacb..7b6c6c2397 100644 --- a/src/renderer/components/+user-management-roles/roles.store.ts +++ b/src/renderer/components/+user-management-roles/roles.store.ts @@ -49,4 +49,4 @@ export const rolesStore = new RolesStore(); apiManager.registerStore(rolesStore, [ roleApi, clusterRoleApi, -]); \ No newline at end of file +]); diff --git a/src/renderer/components/+user-management-service-accounts/index.ts b/src/renderer/components/+user-management-service-accounts/index.ts index fd45e28288..bd81292bf1 100644 --- a/src/renderer/components/+user-management-service-accounts/index.ts +++ b/src/renderer/components/+user-management-service-accounts/index.ts @@ -1,3 +1,3 @@ export * from "./service-accounts"; export * from "./service-accounts-details"; -export * from "./create-service-account-dialog"; \ No newline at end of file +export * from "./create-service-account-dialog"; diff --git a/src/renderer/components/+user-management/index.ts b/src/renderer/components/+user-management/index.ts index 6f29869b9b..4ff825df97 100644 --- a/src/renderer/components/+user-management/index.ts +++ b/src/renderer/components/+user-management/index.ts @@ -1,2 +1,2 @@ export * from "./user-management"; -export * from "./user-management.route"; \ No newline at end of file +export * from "./user-management.route"; diff --git a/src/renderer/components/+workloads-pods/index.ts b/src/renderer/components/+workloads-pods/index.ts index f3181cb3a2..cc7782911c 100644 --- a/src/renderer/components/+workloads-pods/index.ts +++ b/src/renderer/components/+workloads-pods/index.ts @@ -1,2 +1,2 @@ export * from "./pods"; -export * from "./pod-details"; \ No newline at end of file +export * from "./pod-details"; diff --git a/src/renderer/components/+workloads-pods/pod-details-statuses.tsx b/src/renderer/components/+workloads-pods/pod-details-statuses.tsx index 5ce8465e72..1e0f765381 100644 --- a/src/renderer/components/+workloads-pods/pod-details-statuses.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-statuses.tsx @@ -27,4 +27,4 @@ export class PodDetailsStatuses extends React.Component { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/+workloads-statefulsets/index.ts b/src/renderer/components/+workloads-statefulsets/index.ts index 1cb72d701a..af942b604f 100644 --- a/src/renderer/components/+workloads-statefulsets/index.ts +++ b/src/renderer/components/+workloads-statefulsets/index.ts @@ -1,2 +1,2 @@ export * from "./statefulsets"; -export * from "./statefulset-details"; \ No newline at end of file +export * from "./statefulset-details"; diff --git a/src/renderer/components/ace-editor/ace-editor.tsx b/src/renderer/components/ace-editor/ace-editor.tsx index 68ef92635a..2dceb48bba 100644 --- a/src/renderer/components/ace-editor/ace-editor.tsx +++ b/src/renderer/components/ace-editor/ace-editor.tsx @@ -154,4 +154,4 @@ export class AceEditor extends React.Component { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/ace-editor/index.ts b/src/renderer/components/ace-editor/index.ts index 7bfc7c01ea..173845abab 100644 --- a/src/renderer/components/ace-editor/index.ts +++ b/src/renderer/components/ace-editor/index.ts @@ -1 +1 @@ -export * from "./ace-editor"; \ No newline at end of file +export * from "./ace-editor"; diff --git a/src/renderer/components/add-remove-buttons/index.ts b/src/renderer/components/add-remove-buttons/index.ts index 825c59d7d2..fa2deb84ec 100644 --- a/src/renderer/components/add-remove-buttons/index.ts +++ b/src/renderer/components/add-remove-buttons/index.ts @@ -1 +1 @@ -export * from "./add-remove-buttons"; \ No newline at end of file +export * from "./add-remove-buttons"; diff --git a/src/renderer/components/animate/index.ts b/src/renderer/components/animate/index.ts index 080c5446c8..36d812de20 100644 --- a/src/renderer/components/animate/index.ts +++ b/src/renderer/components/animate/index.ts @@ -1 +1 @@ -export * from "./animate"; \ No newline at end of file +export * from "./animate"; diff --git a/src/renderer/components/chart/background-block.plugin.ts b/src/renderer/components/chart/background-block.plugin.ts index 1d39f71aed..ff4816c4dd 100644 --- a/src/renderer/components/chart/background-block.plugin.ts +++ b/src/renderer/components/chart/background-block.plugin.ts @@ -39,4 +39,4 @@ export const BackgroundBlock = { ctx.stroke(); ctx.restore(); } -}; \ No newline at end of file +}; diff --git a/src/renderer/components/chart/bar-chart.tsx b/src/renderer/components/chart/bar-chart.tsx index 69b65b10c9..4f80258703 100644 --- a/src/renderer/components/chart/bar-chart.tsx +++ b/src/renderer/components/chart/bar-chart.tsx @@ -220,4 +220,4 @@ export const cpuOptions: ChartOptions = { } } } -}; \ No newline at end of file +}; diff --git a/src/renderer/components/chart/chart.tsx b/src/renderer/components/chart/chart.tsx index b7e621be22..e42a308848 100644 --- a/src/renderer/components/chart/chart.tsx +++ b/src/renderer/components/chart/chart.tsx @@ -213,4 +213,4 @@ export class Chart extends React.Component { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/chart/index.ts b/src/renderer/components/chart/index.ts index a9db66c298..d75ddf7a2f 100644 --- a/src/renderer/components/chart/index.ts +++ b/src/renderer/components/chart/index.ts @@ -1,3 +1,3 @@ export * from "./chart"; export * from "./pie-chart"; -export * from "./bar-chart"; \ No newline at end of file +export * from "./bar-chart"; diff --git a/src/renderer/components/chart/pie-chart.tsx b/src/renderer/components/chart/pie-chart.tsx index 1c629ab505..939d6bb612 100644 --- a/src/renderer/components/chart/pie-chart.tsx +++ b/src/renderer/components/chart/pie-chart.tsx @@ -64,4 +64,4 @@ export class PieChart extends React.Component { ChartJS.Tooltip.positioners.cursor = function (elements: any, position: { x: number; y: number }) { return position; -}; \ No newline at end of file +}; diff --git a/src/renderer/components/chart/useRealTimeMetrics.ts b/src/renderer/components/chart/useRealTimeMetrics.ts index 69e4e3da7f..b01629e8e9 100644 --- a/src/renderer/components/chart/useRealTimeMetrics.ts +++ b/src/renderer/components/chart/useRealTimeMetrics.ts @@ -42,4 +42,4 @@ export function useRealTimeMetrics(metrics: IMetricValues, chartData: IChartData } return data; -} \ No newline at end of file +} diff --git a/src/renderer/components/chart/zebra-stripes.plugin.ts b/src/renderer/components/chart/zebra-stripes.plugin.ts index f934f88fb2..3a85f8d0a2 100644 --- a/src/renderer/components/chart/zebra-stripes.plugin.ts +++ b/src/renderer/components/chart/zebra-stripes.plugin.ts @@ -95,4 +95,4 @@ export const ZebraStripes = { cover.style.backgroundPositionX = `${-step * minutes}px`; } } -}; \ No newline at end of file +}; diff --git a/src/renderer/components/checkbox/checkbox.tsx b/src/renderer/components/checkbox/checkbox.tsx index f97740a874..8d452a1198 100644 --- a/src/renderer/components/checkbox/checkbox.tsx +++ b/src/renderer/components/checkbox/checkbox.tsx @@ -50,4 +50,4 @@ export class Checkbox extends React.PureComponent { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/checkbox/index.ts b/src/renderer/components/checkbox/index.ts index 7af8873e06..057f167821 100644 --- a/src/renderer/components/checkbox/index.ts +++ b/src/renderer/components/checkbox/index.ts @@ -1 +1 @@ -export * from "./checkbox"; \ No newline at end of file +export * from "./checkbox"; diff --git a/src/renderer/components/cluster-icon/index.ts b/src/renderer/components/cluster-icon/index.ts index 4e1858939f..7879490b85 100644 --- a/src/renderer/components/cluster-icon/index.ts +++ b/src/renderer/components/cluster-icon/index.ts @@ -1 +1 @@ -export * from "./cluster-icon"; \ No newline at end of file +export * from "./cluster-icon"; diff --git a/src/renderer/components/confirm-dialog/index.ts b/src/renderer/components/confirm-dialog/index.ts index dfcd83ded3..4627fd6882 100644 --- a/src/renderer/components/confirm-dialog/index.ts +++ b/src/renderer/components/confirm-dialog/index.ts @@ -1 +1 @@ -export * from "./confirm-dialog"; \ No newline at end of file +export * from "./confirm-dialog"; diff --git a/src/renderer/components/dock/dock-tabs.tsx b/src/renderer/components/dock/dock-tabs.tsx index 6bf9280d59..554411024b 100644 --- a/src/renderer/components/dock/dock-tabs.tsx +++ b/src/renderer/components/dock/dock-tabs.tsx @@ -48,4 +48,4 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: Props) = {tabs.map(tab => {renderTab(tab)})} ); -}; \ No newline at end of file +}; diff --git a/src/renderer/components/editable-list/index.ts b/src/renderer/components/editable-list/index.ts index cc0293acd6..1dc93d5df7 100644 --- a/src/renderer/components/editable-list/index.ts +++ b/src/renderer/components/editable-list/index.ts @@ -1 +1 @@ -export * from "./editable-list"; \ No newline at end of file +export * from "./editable-list"; diff --git a/src/renderer/components/error-boundary/index.ts b/src/renderer/components/error-boundary/index.ts index cdcf838466..90e954fb2e 100644 --- a/src/renderer/components/error-boundary/index.ts +++ b/src/renderer/components/error-boundary/index.ts @@ -1 +1 @@ -export * from "./error-boundary"; \ No newline at end of file +export * from "./error-boundary"; diff --git a/src/renderer/components/file-picker/file-picker.tsx b/src/renderer/components/file-picker/file-picker.tsx index 14cd6c07e8..1a52fe4973 100644 --- a/src/renderer/components/file-picker/file-picker.tsx +++ b/src/renderer/components/file-picker/file-picker.tsx @@ -209,4 +209,4 @@ export class FilePicker extends React.Component { return ; } } -} \ No newline at end of file +} diff --git a/src/renderer/components/file-picker/index.ts b/src/renderer/components/file-picker/index.ts index f58aec1470..28c490afab 100644 --- a/src/renderer/components/file-picker/index.ts +++ b/src/renderer/components/file-picker/index.ts @@ -1 +1 @@ -export * from "./file-picker"; \ No newline at end of file +export * from "./file-picker"; diff --git a/src/renderer/components/icon/index.ts b/src/renderer/components/icon/index.ts index 5cdcefa69c..b975409af4 100644 --- a/src/renderer/components/icon/index.ts +++ b/src/renderer/components/icon/index.ts @@ -1 +1 @@ -export * from "./icon"; \ No newline at end of file +export * from "./icon"; diff --git a/src/renderer/components/input/search-input-url.tsx b/src/renderer/components/input/search-input-url.tsx index 2b1045ede2..c0e00d6e56 100644 --- a/src/renderer/components/input/search-input-url.tsx +++ b/src/renderer/components/input/search-input-url.tsx @@ -54,4 +54,4 @@ export class SearchInputUrl extends React.Component { /> ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/item-object-list/index.tsx b/src/renderer/components/item-object-list/index.tsx index b0b106b298..87ba0e908a 100644 --- a/src/renderer/components/item-object-list/index.tsx +++ b/src/renderer/components/item-object-list/index.tsx @@ -1 +1 @@ -export * from "./item-list-layout"; \ No newline at end of file +export * from "./item-list-layout"; diff --git a/src/renderer/components/kube-object-status-icon/index.ts b/src/renderer/components/kube-object-status-icon/index.ts index 36751596a0..3ef2e6b29c 100644 --- a/src/renderer/components/kube-object-status-icon/index.ts +++ b/src/renderer/components/kube-object-status-icon/index.ts @@ -1 +1 @@ -export * from "./kube-object-status-icon"; \ No newline at end of file +export * from "./kube-object-status-icon"; diff --git a/src/renderer/components/kubeconfig-dialog/index.ts b/src/renderer/components/kubeconfig-dialog/index.ts index fdd244fe98..cb8c90cc14 100644 --- a/src/renderer/components/kubeconfig-dialog/index.ts +++ b/src/renderer/components/kubeconfig-dialog/index.ts @@ -1 +1 @@ -export * from "./kubeconfig-dialog"; \ No newline at end of file +export * from "./kubeconfig-dialog"; diff --git a/src/renderer/components/layout/__test__/main-layout-header.test.tsx b/src/renderer/components/layout/__test__/main-layout-header.test.tsx index b2a7bb5d93..499839072c 100644 --- a/src/renderer/components/layout/__test__/main-layout-header.test.tsx +++ b/src/renderer/components/layout/__test__/main-layout-header.test.tsx @@ -46,4 +46,4 @@ describe("", () => { expect(getByText("minikube")).toBeInTheDocument(); }); -}); \ No newline at end of file +}); diff --git a/src/renderer/components/layout/login-layout.tsx b/src/renderer/components/layout/login-layout.tsx index 8aa9c08e0b..669f783769 100755 --- a/src/renderer/components/layout/login-layout.tsx +++ b/src/renderer/components/layout/login-layout.tsx @@ -34,4 +34,4 @@ export class LoginLayout extends React.Component { ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/layout/sidebar-context.ts b/src/renderer/components/layout/sidebar-context.ts index fff192cba3..7001bbc319 100644 --- a/src/renderer/components/layout/sidebar-context.ts +++ b/src/renderer/components/layout/sidebar-context.ts @@ -4,4 +4,4 @@ export const SidebarContext = React.createContext({ pinned: export type SidebarContextValue = { pinned: boolean; -}; \ No newline at end of file +}; diff --git a/src/renderer/components/line-progress/index.ts b/src/renderer/components/line-progress/index.ts index 91942d706a..bd76106dbb 100644 --- a/src/renderer/components/line-progress/index.ts +++ b/src/renderer/components/line-progress/index.ts @@ -1 +1 @@ -export * from "./line-progress"; \ No newline at end of file +export * from "./line-progress"; diff --git a/src/renderer/components/markdown-viewer/index.ts b/src/renderer/components/markdown-viewer/index.ts index e82c6ba3c3..3c42af15f4 100644 --- a/src/renderer/components/markdown-viewer/index.ts +++ b/src/renderer/components/markdown-viewer/index.ts @@ -1 +1 @@ -export * from "./markdown-viewer"; \ No newline at end of file +export * from "./markdown-viewer"; diff --git a/src/renderer/components/markdown-viewer/markdown-viewer.tsx b/src/renderer/components/markdown-viewer/markdown-viewer.tsx index b1a2334b97..08478cb5a9 100644 --- a/src/renderer/components/markdown-viewer/markdown-viewer.tsx +++ b/src/renderer/components/markdown-viewer/markdown-viewer.tsx @@ -34,4 +34,4 @@ export class MarkdownViewer extends Component { /> ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/radio/index.ts b/src/renderer/components/radio/index.ts index 577923ef9c..0df0f30e50 100644 --- a/src/renderer/components/radio/index.ts +++ b/src/renderer/components/radio/index.ts @@ -1 +1 @@ -export * from "./radio"; \ No newline at end of file +export * from "./radio"; diff --git a/src/renderer/components/resource-metrics/index.ts b/src/renderer/components/resource-metrics/index.ts index 5438f760b4..a50f74ea8e 100644 --- a/src/renderer/components/resource-metrics/index.ts +++ b/src/renderer/components/resource-metrics/index.ts @@ -1,2 +1,2 @@ export * from "./resource-metrics"; -export * from "./resource-metrics-text"; \ No newline at end of file +export * from "./resource-metrics-text"; diff --git a/src/renderer/components/slider/index.ts b/src/renderer/components/slider/index.ts index bc79daa3ff..67c45bb063 100644 --- a/src/renderer/components/slider/index.ts +++ b/src/renderer/components/slider/index.ts @@ -1 +1 @@ -export * from "./slider"; \ No newline at end of file +export * from "./slider"; diff --git a/src/renderer/components/status-brick/index.ts b/src/renderer/components/status-brick/index.ts index e16a2a8093..cc6d3e8879 100644 --- a/src/renderer/components/status-brick/index.ts +++ b/src/renderer/components/status-brick/index.ts @@ -1 +1 @@ -export * from "./status-brick"; \ No newline at end of file +export * from "./status-brick"; diff --git a/src/renderer/components/status-brick/status-brick.tsx b/src/renderer/components/status-brick/status-brick.tsx index 34c835c9fa..ced04acca4 100644 --- a/src/renderer/components/status-brick/status-brick.tsx +++ b/src/renderer/components/status-brick/status-brick.tsx @@ -19,4 +19,4 @@ export class StatusBrick extends React.Component { /> ); } -} \ No newline at end of file +} diff --git a/src/renderer/components/virtual-list/index.ts b/src/renderer/components/virtual-list/index.ts index 4e5b065f43..3fad81848e 100644 --- a/src/renderer/components/virtual-list/index.ts +++ b/src/renderer/components/virtual-list/index.ts @@ -1 +1 @@ -export * from "./virtual-list"; \ No newline at end of file +export * from "./virtual-list"; diff --git a/src/renderer/components/wizard/index.ts b/src/renderer/components/wizard/index.ts index b217e311a9..da693bd87f 100644 --- a/src/renderer/components/wizard/index.ts +++ b/src/renderer/components/wizard/index.ts @@ -1 +1 @@ -export * from "./wizard"; \ No newline at end of file +export * from "./wizard"; diff --git a/src/renderer/hooks/useInterval.ts b/src/renderer/hooks/useInterval.ts index d195fa279f..7ab604511b 100644 --- a/src/renderer/hooks/useInterval.ts +++ b/src/renderer/hooks/useInterval.ts @@ -16,4 +16,4 @@ export function useInterval(callback: () => void, delay: number) { return () => clearInterval(id); }, [delay]); -} \ No newline at end of file +} diff --git a/src/renderer/hooks/useOnUnmount.ts b/src/renderer/hooks/useOnUnmount.ts index 5af04e39b1..a8b6fdd1b4 100644 --- a/src/renderer/hooks/useOnUnmount.ts +++ b/src/renderer/hooks/useOnUnmount.ts @@ -2,4 +2,4 @@ import { useEffect } from "react"; export function useOnUnmount(callback: () => void) { useEffect(() => callback, []); -} \ No newline at end of file +} diff --git a/src/renderer/hooks/useStorage.ts b/src/renderer/hooks/useStorage.ts index 2af730fec8..97b0588d29 100644 --- a/src/renderer/hooks/useStorage.ts +++ b/src/renderer/hooks/useStorage.ts @@ -10,4 +10,4 @@ export function useStorage(key: string, initialValue?: T, options?: IStorageH }; return [storageValue, setValue] as [T, (value: T) => void]; -} \ No newline at end of file +} diff --git a/src/renderer/navigation/events.ts b/src/renderer/navigation/events.ts index 971465706d..1766a1e0d3 100644 --- a/src/renderer/navigation/events.ts +++ b/src/renderer/navigation/events.ts @@ -28,4 +28,4 @@ export function bindEvents() { subscribeToBroadcast("renderer:reload", () => { location.reload(); }); -} \ No newline at end of file +} diff --git a/src/renderer/navigation/helpers.ts b/src/renderer/navigation/helpers.ts index 378f6edb96..0eda77c629 100644 --- a/src/renderer/navigation/helpers.ts +++ b/src/renderer/navigation/helpers.ts @@ -33,4 +33,4 @@ export function getMatchedClusterId(): string { }); return matched?.params.clusterId; -} \ No newline at end of file +} diff --git a/src/renderer/navigation/index.ts b/src/renderer/navigation/index.ts index 70959c2dbd..94930fc994 100644 --- a/src/renderer/navigation/index.ts +++ b/src/renderer/navigation/index.ts @@ -5,4 +5,4 @@ import { bindEvents } from "./events"; export * from "./history"; export * from "./helpers"; -bindEvents(); \ No newline at end of file +bindEvents(); diff --git a/src/renderer/utils/__tests__/metricUnitsToNumber.test.ts b/src/renderer/utils/__tests__/metricUnitsToNumber.test.ts index e94c2f3b67..a22aa46790 100644 --- a/src/renderer/utils/__tests__/metricUnitsToNumber.test.ts +++ b/src/renderer/utils/__tests__/metricUnitsToNumber.test.ts @@ -12,4 +12,4 @@ describe("metricUnitsToNumber tests", () => { test("with m suffix", () => { expect(metricUnitsToNumber("124m")).toStrictEqual(124000000); }); -}); \ No newline at end of file +}); diff --git a/src/renderer/utils/formatDuration.ts b/src/renderer/utils/formatDuration.ts index 8864aba393..87da6cfa64 100644 --- a/src/renderer/utils/formatDuration.ts +++ b/src/renderer/utils/formatDuration.ts @@ -83,4 +83,4 @@ function getMeaningfulValues(values: number[], suffixes: string[], separator = " .filter(([dur]) => dur > 0) .map(([dur, suf]) => dur + suf) .join(separator); -} \ No newline at end of file +} diff --git a/src/renderer/utils/jsonPath.ts b/src/renderer/utils/jsonPath.ts index ea31ffa80e..79075500f9 100644 --- a/src/renderer/utils/jsonPath.ts +++ b/src/renderer/utils/jsonPath.ts @@ -32,4 +32,4 @@ function convertToIndexNotation(key: string, firstItem = false) { return `${prefix}${key}`; } -} \ No newline at end of file +} diff --git a/types/command-exists.d.ts b/types/command-exists.d.ts index b5375ae390..634d2a035e 100644 --- a/types/command-exists.d.ts +++ b/types/command-exists.d.ts @@ -13,4 +13,4 @@ declare function commandExists( declare namespace commandExists { function sync(commandName: string): boolean; -} \ No newline at end of file +} From a92ed46f0d0c0bb9fd247bb01094781de4efc243 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Fri, 22 Jan 2021 10:27:54 +0300 Subject: [PATCH 06/56] Fixing tolerations list layout (#2002) * Expanding tolerations div width Signed-off-by: Alex Andreev * Adding tolerations table Signed-off-by: Alex Andreev * Fixing tolerations table styles Signed-off-by: Alex Andreev * Adding tests Signed-off-by: Alex Andreev * Add new line at the end of the file for linter Signed-off-by: Alex Andreev --- src/renderer/api/workload-kube-object.ts | 2 +- .../__tests__/pod-tolerations.test.tsx | 59 +++++++++++++++++ .../pod-details-tolerations.scss | 22 ++++++- .../pod-details-tolerations.tsx | 20 ++---- .../+workloads-pods/pod-tolerations.scss | 14 +++++ .../+workloads-pods/pod-tolerations.tsx | 63 +++++++++++++++++++ .../components/drawer/drawer-item.scss | 4 +- .../drawer/drawer-param-toggler.tsx | 2 +- src/renderer/components/drawer/drawer.scss | 1 - 9 files changed, 165 insertions(+), 22 deletions(-) create mode 100644 src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx create mode 100644 src/renderer/components/+workloads-pods/pod-tolerations.scss create mode 100644 src/renderer/components/+workloads-pods/pod-tolerations.tsx diff --git a/src/renderer/api/workload-kube-object.ts b/src/renderer/api/workload-kube-object.ts index c6786aa99f..185d3d502c 100644 --- a/src/renderer/api/workload-kube-object.ts +++ b/src/renderer/api/workload-kube-object.ts @@ -1,7 +1,7 @@ import get from "lodash/get"; import { KubeObject } from "./kube-object"; -interface IToleration { +export interface IToleration { key?: string; operator?: string; effect?: string; diff --git a/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx new file mode 100644 index 0000000000..dbde813e5a --- /dev/null +++ b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx @@ -0,0 +1,59 @@ +/** + * @jest-environment jsdom + */ + +import React from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { fireEvent, render } from "@testing-library/react"; +import { IToleration } from "../../../api/workload-kube-object"; +import { PodTolerations } from "../pod-tolerations"; + +const tolerations: IToleration[] =[ + { + key: "CriticalAddonsOnly", + operator: "Exist", + effect: "NoExecute", + tolerationSeconds: 3600 + }, + { + key: "node.kubernetes.io/not-ready", + operator: "NoExist", + effect: "NoSchedule", + tolerationSeconds: 7200 + }, +]; + +describe("", () => { + it("renders w/o errors", () => { + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("shows all tolerations", () => { + const { container } = render(); + const rows = container.querySelectorAll(".TableRow"); + + expect(rows[0].querySelector(".key").textContent).toBe("CriticalAddonsOnly"); + expect(rows[0].querySelector(".operator").textContent).toBe("Exist"); + expect(rows[0].querySelector(".effect").textContent).toBe("NoExecute"); + expect(rows[0].querySelector(".seconds").textContent).toBe("3600"); + + expect(rows[1].querySelector(".key").textContent).toBe("node.kubernetes.io/not-ready"); + expect(rows[1].querySelector(".operator").textContent).toBe("NoExist"); + expect(rows[1].querySelector(".effect").textContent).toBe("NoSchedule"); + expect(rows[1].querySelector(".seconds").textContent).toBe("7200"); + }); + + it("sorts table properly", () => { + const { container, getByText } = render(); + const headCell = getByText("Key"); + + fireEvent.click(headCell); + fireEvent.click(headCell); + + const rows = container.querySelectorAll(".TableRow"); + + expect(rows[0].querySelector(".key").textContent).toBe("node.kubernetes.io/not-ready"); + }); +}); diff --git a/src/renderer/components/+workloads-pods/pod-details-tolerations.scss b/src/renderer/components/+workloads-pods/pod-details-tolerations.scss index 0aa68fa1d6..1ac932cd9d 100644 --- a/src/renderer/components/+workloads-pods/pod-details-tolerations.scss +++ b/src/renderer/components/+workloads-pods/pod-details-tolerations.scss @@ -1,5 +1,23 @@ .PodDetailsTolerations { - .toleration { - margin-bottom: $margin; + grid-template-columns: auto; + + .PodTolerations { + margin-top: var(--margin); + } + + // Expanding value cell to cover 2 columns (whole Drawer width) + + > .name { + grid-row-start: 1; + grid-column-start: 1; + } + + > .value { + grid-row-start: 1; + grid-column-start: 1; + } + + .DrawerParamToggler > .params { + margin-left: var(--drawer-item-title-width); } } \ No newline at end of file diff --git a/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx b/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx index 8b67502e26..67bd5a07d0 100644 --- a/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-tolerations.tsx @@ -1,10 +1,11 @@ import "./pod-details-tolerations.scss"; import React from "react"; -import { Pod, Deployment, DaemonSet, StatefulSet, ReplicaSet, Job } from "../../api/endpoints"; import { DrawerParamToggler, DrawerItem } from "../drawer"; +import { WorkloadKubeObject } from "../../api/workload-kube-object"; +import { PodTolerations } from "./pod-tolerations"; interface Props { - workload: Pod | Deployment | DaemonSet | StatefulSet | ReplicaSet | Job; + workload: WorkloadKubeObject; } export class PodDetailsTolerations extends React.Component { @@ -17,20 +18,7 @@ export class PodDetailsTolerations extends React.Component { return ( - { - tolerations.map((toleration, index) => { - const { key, operator, effect, tolerationSeconds } = toleration; - - return ( -
- {key} - {operator && {operator}} - {effect && {effect}} - {!!tolerationSeconds && {tolerationSeconds}} -
- ); - }) - } +
); diff --git a/src/renderer/components/+workloads-pods/pod-tolerations.scss b/src/renderer/components/+workloads-pods/pod-tolerations.scss new file mode 100644 index 0000000000..b840697685 --- /dev/null +++ b/src/renderer/components/+workloads-pods/pod-tolerations.scss @@ -0,0 +1,14 @@ +.PodTolerations { + .TableHead { + background-color: var(--drawerSubtitleBackground); + } + + .TableCell { + white-space: normal; + word-break: normal; + + &.key { + flex-grow: 3; + } + } +} \ No newline at end of file diff --git a/src/renderer/components/+workloads-pods/pod-tolerations.tsx b/src/renderer/components/+workloads-pods/pod-tolerations.tsx new file mode 100644 index 0000000000..e8d3d7d099 --- /dev/null +++ b/src/renderer/components/+workloads-pods/pod-tolerations.tsx @@ -0,0 +1,63 @@ +import "./pod-tolerations.scss"; +import React from "react"; +import uniqueId from "lodash/uniqueId"; + +import { IToleration } from "../../api/workload-kube-object"; +import { Table, TableCell, TableHead, TableRow } from "../table"; + +interface Props { + tolerations: IToleration[]; +} + +enum sortBy { + Key = "key", + Operator = "operator", + Effect = "effect", + Seconds = "seconds", +} + +const sortingCallbacks = { + [sortBy.Key]: (toleration: IToleration) => toleration.key, + [sortBy.Operator]: (toleration: IToleration) => toleration.operator, + [sortBy.Effect]: (toleration: IToleration) => toleration.effect, + [sortBy.Seconds]: (toleration: IToleration) => toleration.tolerationSeconds, +}; + +const getTableRow = (toleration: IToleration) => { + const { key, operator, effect, tolerationSeconds } = toleration; + + return ( + + {key} + {operator} + {effect} + {tolerationSeconds} + + ); +}; + +export function PodTolerations({ tolerations }: Props) { + return ( + + + Key + Operator + Effect + Seconds + + { + tolerations.map(getTableRow) + } +
+ ); +} diff --git a/src/renderer/components/drawer/drawer-item.scss b/src/renderer/components/drawer/drawer-item.scss index f7727414a3..a9c54120df 100644 --- a/src/renderer/components/drawer/drawer-item.scss +++ b/src/renderer/components/drawer/drawer-item.scss @@ -1,6 +1,8 @@ .DrawerItem { + --drawer-item-title-width: 30%; + display: grid; - grid-template-columns: minmax(30%, min-content) auto; + grid-template-columns: minmax(var(--drawer-item-title-width), min-content) auto; border-bottom: 1px solid $borderFaintColor; padding: $padding 0; diff --git a/src/renderer/components/drawer/drawer-param-toggler.tsx b/src/renderer/components/drawer/drawer-param-toggler.tsx index 85772d0855..df97a4d1bd 100644 --- a/src/renderer/components/drawer/drawer-param-toggler.tsx +++ b/src/renderer/components/drawer/drawer-param-toggler.tsx @@ -25,7 +25,7 @@ export class DrawerParamToggler extends React.Component -
+
{label}
{link} diff --git a/src/renderer/components/drawer/drawer.scss b/src/renderer/components/drawer/drawer.scss index b34b3b5965..8b7fc27993 100644 --- a/src/renderer/components/drawer/drawer.scss +++ b/src/renderer/components/drawer/drawer.scss @@ -69,7 +69,6 @@ padding: var(--spacing); .Table .TableHead { - background-color: $contentColor; border-bottom: 1px solid $borderFaintColor; } } From f8c111ddd8031f568fa2f6b97790541ee568b9a5 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 22 Jan 2021 13:18:46 +0200 Subject: [PATCH 07/56] Load k8s resources only for selected namespaces (#1918) * loading k8s resources into stores per selected namespaces -- part 1 Signed-off-by: Roman * loading k8s resources into stores per selected namespaces -- part 2 - fix: generating helm chart id Signed-off-by: Roman * loading k8s resources into stores per selected namespaces -- part 3 Signed-off-by: Roman * fixes Signed-off-by: Roman * fixes / responding to comments Signed-off-by: Roman * chore / small fixes Signed-off-by: Roman * fixes & refactoring Signed-off-by: Roman * make lint happy Signed-off-by: Roman * reset store on loading error Signed-off-by: Roman * added new cluster method: cluster.isAllowedResource Signed-off-by: Roman * fix: loading namespaces optimizations Signed-off-by: Roman * fixes & refactoring Signed-off-by: Roman --- src/common/rbac.ts | 53 ++-- src/common/user-store.ts | 9 + src/main/cluster.ts | 41 ++- src/renderer/api/kube-watch-api.ts | 34 ++- .../+apps-releases/release.store.ts | 27 +- .../components/+namespaces/namespace.store.ts | 140 ++++++---- .../role-bindings.store.ts | 18 +- .../+user-management-roles/roles.store.ts | 18 +- .../+workloads-overview/overview-statuses.tsx | 2 +- .../+workloads-overview/overview.tsx | 86 +++--- .../components/+workloads-pods/pods.tsx | 41 ++- .../item-object-list/item-list-layout.scss | 11 + .../item-object-list/item-list-layout.tsx | 250 +++++++++--------- .../item-object-list/page-filters.store.ts | 6 +- .../item-object-list/table-menu.scss | 4 - src/renderer/components/table/table-cell.tsx | 5 +- src/renderer/item.store.ts | 14 +- src/renderer/kube-object.store.ts | 66 +++-- 18 files changed, 465 insertions(+), 360 deletions(-) delete mode 100644 src/renderer/components/item-object-list/table-menu.scss diff --git a/src/common/rbac.ts b/src/common/rbac.ts index fbcf7c98d8..de242b114a 100644 --- a/src/common/rbac.ts +++ b/src/common/rbac.ts @@ -7,37 +7,38 @@ export type KubeResource = "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets"; export interface KubeApiResource { - resource: KubeResource; // valid resource name + kind: string; // resource type (e.g. "Namespace") + apiName: KubeResource; // valid api resource name (e.g. "namespaces") group?: string; // api-group } // TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7) export const apiResources: KubeApiResource[] = [ - { resource: "configmaps" }, - { resource: "cronjobs", group: "batch" }, - { resource: "customresourcedefinitions", group: "apiextensions.k8s.io" }, - { resource: "daemonsets", group: "apps" }, - { resource: "deployments", group: "apps" }, - { resource: "endpoints" }, - { resource: "events" }, - { resource: "horizontalpodautoscalers" }, - { resource: "ingresses", group: "networking.k8s.io" }, - { resource: "jobs", group: "batch" }, - { resource: "limitranges" }, - { resource: "namespaces" }, - { resource: "networkpolicies", group: "networking.k8s.io" }, - { resource: "nodes" }, - { resource: "persistentvolumes" }, - { resource: "persistentvolumeclaims" }, - { resource: "pods" }, - { resource: "poddisruptionbudgets" }, - { resource: "podsecuritypolicies" }, - { resource: "resourcequotas" }, - { resource: "replicasets", group: "apps" }, - { resource: "secrets" }, - { resource: "services" }, - { resource: "statefulsets", group: "apps" }, - { resource: "storageclasses", group: "storage.k8s.io" }, + { kind: "ConfigMap", apiName: "configmaps" }, + { kind: "CronJob", apiName: "cronjobs", group: "batch" }, + { kind: "CustomResourceDefinition", apiName: "customresourcedefinitions", group: "apiextensions.k8s.io" }, + { kind: "DaemonSet", apiName: "daemonsets", group: "apps" }, + { kind: "Deployment", apiName: "deployments", group: "apps" }, + { kind: "Endpoint", apiName: "endpoints" }, + { kind: "Event", apiName: "events" }, + { kind: "HorizontalPodAutoscaler", apiName: "horizontalpodautoscalers" }, + { kind: "Ingress", apiName: "ingresses", group: "networking.k8s.io" }, + { kind: "Job", apiName: "jobs", group: "batch" }, + { kind: "Namespace", apiName: "namespaces" }, + { kind: "LimitRange", apiName: "limitranges" }, + { kind: "NetworkPolicy", apiName: "networkpolicies", group: "networking.k8s.io" }, + { kind: "Node", apiName: "nodes" }, + { kind: "PersistentVolume", apiName: "persistentvolumes" }, + { kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" }, + { kind: "Pod", apiName: "pods" }, + { kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets" }, + { kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" }, + { kind: "ResourceQuota", apiName: "resourcequotas" }, + { kind: "ReplicaSet", apiName: "replicasets", group: "apps" }, + { kind: "Secret", apiName: "secrets" }, + { kind: "Service", apiName: "services" }, + { kind: "StatefulSet", apiName: "statefulsets", group: "apps" }, + { kind: "StorageClass", apiName: "storageclasses", group: "storage.k8s.io" }, ]; export function isAllowedResource(resources: KubeResource | KubeResource[]) { diff --git a/src/common/user-store.ts b/src/common/user-store.ts index cf271a011d..b0294d9e5a 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -84,6 +84,15 @@ export class UserStore extends BaseStore { return semver.gt(getAppVersion(), this.lastSeenAppVersion); } + @action + setHiddenTableColumns(tableId: string, names: Set | string[]) { + this.preferences.hiddenTableColumns[tableId] = Array.from(names); + } + + getHiddenTableColumns(tableId: string): Set { + return new Set(this.preferences.hiddenTableColumns[tableId]); + } + @action resetKubeConfigPath() { this.kubeConfigPath = kubeConfigDefaultPath; diff --git a/src/main/cluster.ts b/src/main/cluster.ts index c6c14f6406..956164e10c 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -190,7 +190,7 @@ export class Cluster implements ClusterModel, ClusterState { */ @observable metadata: ClusterMetadata = {}; /** - * List of allowed namespaces + * List of allowed namespaces verified via K8S::SelfSubjectAccessReview api * * @observable */ @@ -203,7 +203,7 @@ export class Cluster implements ClusterModel, ClusterState { */ @observable allowedResources: string[] = []; /** - * List of accessible namespaces + * List of accessible namespaces provided by user in the Cluster Settings * * @observable */ @@ -224,7 +224,7 @@ export class Cluster implements ClusterModel, ClusterState { * @computed */ @computed get name() { - return this.preferences.clusterName || this.contextName; + return this.preferences.clusterName || this.contextName; } /** @@ -279,7 +279,8 @@ export class Cluster implements ClusterModel, ClusterState { * @param port port where internal auth proxy is listening * @internal */ - @action async init(port: number) { + @action + async init(port: number) { try { this.initializing = true; this.contextHandler = new ContextHandler(this); @@ -334,7 +335,8 @@ export class Cluster implements ClusterModel, ClusterState { * @param force force activation * @internal */ - @action async activate(force = false) { + @action + async activate(force = false) { if (this.activated && !force) { return this.pushState(); } @@ -373,7 +375,8 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action async reconnect() { + @action + async reconnect() { logger.info(`[CLUSTER]: reconnect`, this.getMeta()); this.contextHandler?.stopServer(); await this.contextHandler?.ensureServer(); @@ -400,7 +403,8 @@ export class Cluster implements ClusterModel, ClusterState { * @internal * @param opts refresh options */ - @action async refresh(opts: ClusterRefreshOptions = {}) { + @action + async refresh(opts: ClusterRefreshOptions = {}) { logger.info(`[CLUSTER]: refresh`, this.getMeta()); await this.whenInitialized; await this.refreshConnectionStatus(); @@ -420,7 +424,8 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action async refreshMetadata() { + @action + async refreshMetadata() { logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); const metadata = await detectorRegistry.detectForCluster(this); const existingMetadata = this.metadata; @@ -431,7 +436,8 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action async refreshConnectionStatus() { + @action + async refreshConnectionStatus() { const connectionStatus = await this.getConnectionStatus(); this.online = connectionStatus > ClusterStatus.Offline; @@ -441,7 +447,8 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - @action async refreshAllowedResources() { + @action + async refreshAllowedResources() { this.allowedNamespaces = await this.getAllowedNamespaces(); this.allowedResources = await this.getAllowedResources(); } @@ -668,7 +675,7 @@ export class Cluster implements ClusterModel, ClusterState { for (const namespace of this.allowedNamespaces.slice(0, 10)) { if (!this.resourceAccessStatuses.get(apiResource)) { const result = await this.canI({ - resource: apiResource.resource, + resource: apiResource.apiName, group: apiResource.group, verb: "list", namespace @@ -683,9 +690,19 @@ export class Cluster implements ClusterModel, ClusterState { return apiResources .filter((resource) => this.resourceAccessStatuses.get(resource)) - .map(apiResource => apiResource.resource); + .map(apiResource => apiResource.apiName); } catch (error) { return []; } } + + isAllowedResource(kind: string): boolean { + const apiResource = apiResources.find(resource => resource.kind === kind || resource.apiName === kind); + + if (apiResource) { + return this.allowedResources.includes(apiResource.apiName); + } + + return true; // allowed by default for other resources + } } diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index 78ca25256e..fe35a04baa 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -11,7 +11,7 @@ import { apiPrefix, isDevelopment } from "../../common/vars"; import { getHostedCluster } from "../../common/cluster-store"; export interface IKubeWatchEvent { - type: "ADDED" | "MODIFIED" | "DELETED"; + type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR"; object?: T; } @@ -62,27 +62,41 @@ 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(); + if (isAdmin && !api.isNamespaced) { + return api.getWatchUrl(); + } - return allowedNamespaces.map(namespace => api.getWatchUrl(namespace)); + 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); @@ -158,6 +172,10 @@ export class KubeWatchApi { addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) { const listener = (evt: IKubeWatchEvent) => { + if (evt.type === "ERROR") { + return; // e.g. evt.object.message == "too old resource version" + } + const { namespace, resourceVersion } = evt.object.metadata; const api = apiManager.getApiByKind(evt.object.kind, evt.object.apiVersion); diff --git a/src/renderer/components/+apps-releases/release.store.ts b/src/renderer/components/+apps-releases/release.store.ts index b6d5c2fb5f..6f7ed39fed 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,23 @@ export class ReleaseStore extends ItemStore { @action async loadAll() { this.isLoading = true; - let items; try { - const { isAdmin, allowedNamespaces } = getHostedCluster(); + const items = await this.loadItems(namespaceStore.getContextNamespaces()); - items = await this.loadItems(!isAdmin ? allowedNamespaces : null); - } finally { - if (items) { - items = this.sortItems(items); - this.items.replace(items); - } + this.items.replace(this.sortItems(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 ad02dd137c..50ec2c8038 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -1,53 +1,120 @@ -import { action, comparer, observable, reaction } from "mobx"; +import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx"; import { autobind, createStorage } from "../../utils"; -import { KubeObjectStore } from "../../kube-object.store"; -import { Namespace, namespacesApi } from "../../api/endpoints"; +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", []); +const storage = createStorage("context_namespaces"); export const namespaceUrlParam = createPageParam({ name: "namespaces", isSystem: true, multiValues: true, get defaultValue() { - return storage.get(); // initial namespaces coming from URL or local-storage (default) + return storage.get() ?? []; // initial namespaces coming from URL or local-storage (default) } }); +export function getDummyNamespace(name: string) { + return new Namespace({ + kind: Namespace.kind, + apiVersion: "v1", + metadata: { + name, + uid: "", + resourceVersion: "", + selfLink: `/api/v1/namespaces/${name}` + } + }); +} + @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() { - this.setContext(this.initNamespaces); + private async init() { + await clusterStore.whenLoaded; + if (!getHostedCluster()) return; + await getHostedCluster().whenReady; // wait for cluster-state from main - return reaction(() => this.contextNs.toJS(), namespaces => { + this.setContext(this.initialNamespaces); + this.autoLoadAllowedNamespaces(); + this.autoUpdateUrlAndLocalStorage(); + + this.isReady = true; + } + + public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer { + return reaction(() => this.contextNs.toJS(), callback, { + equals: comparer.shallow, + ...opts, + }); + } + + private autoUpdateUrlAndLocalStorage(): IReactionDisposer { + return this.onContextChange(namespaces => { storage.set(namespaces); // save to local-storage namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url }, { fireImmediately: true, - equals: comparer.identity, }); } - get initNamespaces() { - return namespaceUrlParam.get(); + private autoLoadAllowedNamespaces(): IReactionDisposer { + return reaction(() => this.allowedNamespaces, () => this.loadAll(), { + fireImmediately: true, + equals: comparer.shallow, + }); } - getContextParams() { - return { - namespaces: this.contextNs.toJS(), - }; + get allowedNamespaces(): string[] { + return toJS(getHostedCluster().allowedNamespaces); + } + + private get initialNamespaces(): string[] { + const allowed = new Set(this.allowedNamespaces); + const prevSelected = storage.get(); + + if (Array.isArray(prevSelected)) { + return prevSelected.filter(namespace => allowed.has(namespace)); + } + + // otherwise select "default" or first allowed namespace + if (allowed.has("default")) { + return ["default"]; + } else if (allowed.size) { + return [Array.from(allowed)[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; } subscribe(apis = [this.api]) { @@ -61,31 +128,18 @@ export class NamespaceStore extends KubeObjectStore { return super.subscribe(apis); } - protected async loadItems(namespaces?: string[]) { - if (!isAllowedResource("namespaces")) { - if (namespaces) return namespaces.map(this.getDummyNamespace); + protected async loadItems(params: KubeObjectStoreLoadingParams) { + const { allowedNamespaces } = this; - return []; + let namespaces = await super.loadItems(params); + + namespaces = namespaces.filter(namespace => allowedNamespaces.includes(namespace.getName())); + + if (!namespaces.length && allowedNamespaces.length > 0) { + return allowedNamespaces.map(getDummyNamespace); } - if (namespaces) { - return Promise.all(namespaces.map(name => this.api.get({ name }))); - } else { - return super.loadItems(); - } - } - - protected getDummyNamespace(name: string) { - return new Namespace({ - kind: "Namespace", - apiVersion: "v1", - metadata: { - name, - uid: "", - resourceVersion: "", - selfLink: `/api/v1/namespaces/${name}` - } - }); + return namespaces; } @action @@ -105,12 +159,6 @@ export class NamespaceStore extends KubeObjectStore { else this.contextNs.push(namespace); } - @action - reset() { - super.reset(); - this.contextNs.clear(); - } - @action async remove(item: Namespace) { await super.remove(item); diff --git a/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts b/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts index f293dea6f0..71890acc44 100644 --- a/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts +++ b/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts @@ -1,7 +1,7 @@ import difference from "lodash/difference"; import uniqBy from "lodash/uniqBy"; import { clusterRoleBindingApi, IRoleBindingSubject, RoleBinding, roleBindingApi } from "../../api/endpoints"; -import { KubeObjectStore } from "../../kube-object.store"; +import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; import { autobind } from "../../utils"; import { apiManager } from "../../api/api-manager"; @@ -26,15 +26,13 @@ export class RoleBindingsStore extends KubeObjectStore { return clusterRoleBindingApi.get(params); } - protected loadItems(namespaces?: string[]) { - if (namespaces) { - return Promise.all( - namespaces.map(namespace => roleBindingApi.list({ namespace })) - ).then(items => items.flat()); - } else { - return Promise.all([clusterRoleBindingApi.list(), roleBindingApi.list()]) - .then(items => items.flat()); - } + protected async loadItems(params: KubeObjectStoreLoadingParams): Promise { + const items = await Promise.all([ + super.loadItems({ ...params, api: clusterRoleBindingApi }), + super.loadItems({ ...params, api: roleBindingApi }), + ]); + + return items.flat(); } protected async createItem(params: { name: string; namespace?: string }, data?: Partial) { diff --git a/src/renderer/components/+user-management-roles/roles.store.ts b/src/renderer/components/+user-management-roles/roles.store.ts index 7b6c6c2397..7d2e90dd38 100644 --- a/src/renderer/components/+user-management-roles/roles.store.ts +++ b/src/renderer/components/+user-management-roles/roles.store.ts @@ -1,6 +1,6 @@ import { clusterRoleApi, Role, roleApi } from "../../api/endpoints"; import { autobind } from "../../utils"; -import { KubeObjectStore } from "../../kube-object.store"; +import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; import { apiManager } from "../../api/api-manager"; @autobind() @@ -24,15 +24,13 @@ export class RolesStore extends KubeObjectStore { return clusterRoleApi.get(params); } - protected loadItems(namespaces?: string[]): Promise { - if (namespaces) { - return Promise.all( - namespaces.map(namespace => roleApi.list({ namespace })) - ).then(items => items.flat()); - } else { - return Promise.all([clusterRoleApi.list(), roleApi.list()]) - .then(items => items.flat()); - } + protected async loadItems(params: KubeObjectStoreLoadingParams): Promise { + const items = await Promise.all([ + super.loadItems({ ...params, api: clusterRoleApi }), + super.loadItems({ ...params, api: roleApi }), + ]); + + return items.flat(); } protected async createItem(params: { name: string; namespace?: string }, data?: Partial) { 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/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index 318ad53f77..351b57462c 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -17,81 +17,65 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; import { Events } from "../+events"; import { KubeObjectStore } from "../../kube-object.store"; import { isAllowedResource } from "../../../common/rbac"; +import { namespaceStore } from "../+namespaces/namespace.store"; interface Props extends RouteComponentProps { } @observer export class WorkloadsOverview extends React.Component { + @observable isLoading = false; @observable isUnmounting = false; async componentDidMount() { - const stores: KubeObjectStore[] = []; + const stores: KubeObjectStore[] = [ + isAllowedResource("pods") && podsStore, + isAllowedResource("deployments") && deploymentStore, + isAllowedResource("daemonsets") && daemonSetStore, + isAllowedResource("statefulsets") && statefulSetStore, + isAllowedResource("replicasets") && replicaSetStore, + isAllowedResource("jobs") && jobStore, + isAllowedResource("cronjobs") && cronJobStore, + isAllowedResource("events") && eventStore, + ].filter(Boolean); - if (isAllowedResource("pods")) { - stores.push(podsStore); - } + const unsubscribeMap = new Map void>(); - if (isAllowedResource("deployments")) { - stores.push(deploymentStore); - } + const loadStores = async () => { + this.isLoading = true; - if (isAllowedResource("daemonsets")) { - stores.push(daemonSetStore); - } + for (const store of stores) { + if (this.isUnmounting) break; - if (isAllowedResource("statefulsets")) { - stores.push(statefulSetStore); - } + try { + await store.loadAll(); + unsubscribeMap.get(store)?.(); // unsubscribe previous watcher + unsubscribeMap.set(store, store.subscribe()); + } catch (error) { + console.error("loading store error", error); + } + } + this.isLoading = false; + }; - if (isAllowedResource("replicasets")) { - stores.push(replicaSetStore); - } + namespaceStore.onContextChange(loadStores, { + fireImmediately: true, + }); - if (isAllowedResource("jobs")) { - stores.push(jobStore); - } - - if (isAllowedResource("cronjobs")) { - stores.push(cronJobStore); - } - - if (isAllowedResource("events")) { - stores.push(eventStore); - } - - const unsubscribeList: Array<() => void> = []; - - for (const store of stores) { - await store.loadAll(); - unsubscribeList.push(store.subscribe()); - } - - await when(() => this.isUnmounting); - unsubscribeList.forEach(dispose => dispose()); + await when(() => this.isUnmounting && !this.isLoading); + unsubscribeMap.forEach(dispose => dispose()); + unsubscribeMap.clear(); } componentWillUnmount() { this.isUnmounting = true; } - get contents() { - return ( - <> - - { isAllowedResource("events") && } - - ); - } - render() { return (
- {this.contents} + + {isAllowedResource("events") && }
); } diff --git a/src/renderer/components/+workloads-pods/pods.tsx b/src/renderer/components/+workloads-pods/pods.tsx index 2296b98317..a59c9d79d2 100644 --- a/src/renderer/components/+workloads-pods/pods.tsx +++ b/src/renderer/components/+workloads-pods/pods.tsx @@ -19,8 +19,7 @@ import { lookupApiLink } from "../../api/kube-api"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { Badge } from "../badge"; - -enum sortBy { +enum columnId { name = "name", namespace = "namespace", containers = "containers", @@ -77,15 +76,15 @@ export class Pods extends React.Component { tableId = "workloads_pods" isConfigurable sortingCallbacks={{ - [sortBy.name]: (pod: Pod) => pod.getName(), - [sortBy.namespace]: (pod: Pod) => pod.getNs(), - [sortBy.containers]: (pod: Pod) => pod.getContainers().length, - [sortBy.restarts]: (pod: Pod) => pod.getRestartsCount(), - [sortBy.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind), - [sortBy.qos]: (pod: Pod) => pod.getQosClass(), - [sortBy.node]: (pod: Pod) => pod.getNodeName(), - [sortBy.age]: (pod: Pod) => pod.metadata.creationTimestamp, - [sortBy.status]: (pod: Pod) => pod.getStatusMessage(), + [columnId.name]: (pod: Pod) => pod.getName(), + [columnId.namespace]: (pod: Pod) => pod.getNs(), + [columnId.containers]: (pod: Pod) => pod.getContainers().length, + [columnId.restarts]: (pod: Pod) => pod.getRestartsCount(), + [columnId.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind), + [columnId.qos]: (pod: Pod) => pod.getQosClass(), + [columnId.node]: (pod: Pod) => pod.getNodeName(), + [columnId.age]: (pod: Pod) => pod.metadata.creationTimestamp, + [columnId.status]: (pod: Pod) => pod.getStatusMessage(), }} searchFilters={[ (pod: Pod) => pod.getSearchFields(), @@ -95,16 +94,16 @@ export class Pods extends React.Component { ]} renderHeaderTitle="Pods" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning", showWithColumn: "name" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Containers", className: "containers", sortBy: sortBy.containers }, - { title: "Restarts", className: "restarts", sortBy: sortBy.restarts }, - { title: "Controlled By", className: "owners", sortBy: sortBy.owners }, - { title: "Node", className: "node", sortBy: sortBy.node }, - { title: "QoS", className: "qos", sortBy: sortBy.qos }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Status", className: "status", sortBy: sortBy.status }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Containers", className: "containers", sortBy: columnId.containers, id: columnId.containers }, + { title: "Restarts", className: "restarts", sortBy: columnId.restarts, id: columnId.restarts }, + { title: "Controlled By", className: "owners", sortBy: columnId.owners, id: columnId.owners }, + { title: "Node", className: "node", sortBy: columnId.node, id: columnId.node }, + { title: "QoS", className: "qos", sortBy: columnId.qos, id: columnId.qos }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, ]} renderTableContents={(pod: Pod) => [ , diff --git a/src/renderer/components/item-object-list/item-list-layout.scss b/src/renderer/components/item-object-list/item-list-layout.scss index 9bdc2f943d..0008ffd527 100644 --- a/src/renderer/components/item-object-list/item-list-layout.scss +++ b/src/renderer/components/item-object-list/item-list-layout.scss @@ -36,3 +36,14 @@ } } +.ItemListLayoutVisibilityMenu { + .MenuItem { + padding: 0; + } + + .Checkbox { + width: 100%; + padding: var(--spacing); + cursor: pointer; + } +} 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 38e0e0218d..aaeb7438ea 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -1,12 +1,11 @@ import "./item-list-layout.scss"; -import "./table-menu.scss"; import groupBy from "lodash/groupBy"; import React, { ReactNode } from "react"; -import { computed, observable, reaction, toJS, when } from "mobx"; +import { computed, IReactionDisposer, observable, reaction, toJS } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog"; -import { TableSortCallback, Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps } from "../table"; +import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallback } from "../table"; import { autobind, createStorage, cssNames, IClassName, isReactNode, noop, prevDefault, stopPropagation } from "../../utils"; import { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons"; import { NoItems } from "../no-items"; @@ -19,11 +18,10 @@ import { PageFiltersList } from "./page-filters-list"; import { PageFiltersSelect } from "./page-filters-select"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select"; import { themeStore } from "../../theme.store"; -import { MenuActions} from "../menu/menu-actions"; +import { MenuActions } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Checkbox } from "../checkbox"; import { userStore } from "../../../common/user-store"; -import logger from "../../../main/logger"; // todo: refactor, split to small re-usable components @@ -98,10 +96,11 @@ interface ItemListLayoutUserSettings { @observer export class ItemListLayout extends React.Component { static defaultProps = defaultProps as object; - @observable hiddenColumnNames = new Set(); + + private watchDisposers: IReactionDisposer[] = []; + @observable isUnmounting = false; - // default user settings (ui show-hide tweaks mostly) @observable userSettings: ItemListLayoutUserSettings = { showAppliedFilters: false, }; @@ -120,31 +119,54 @@ export class ItemListLayout extends React.Component { } async componentDidMount() { - const { store, dependentStores, isClusterScoped, tableId } = this.props; + const { isClusterScoped, isConfigurable, tableId } = this.props; - if (this.canBeConfigured) this.hiddenColumnNames = new Set(userStore.preferences?.hiddenTableColumns?.[tableId]); + if (isConfigurable && !tableId) { + throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified"); + } - const stores = [store, ...dependentStores]; + this.loadStores(); - if (!isClusterScoped) stores.push(namespaceStore); - - try { - stores.map(store => store.reset()); - await Promise.all(stores.map(store => store.loadAll())); - const subscriptions = stores.map(store => store.subscribe()); - - await when(() => this.isUnmounting); - subscriptions.forEach(dispose => dispose()); // unsubscribe all - } catch (error) { - console.log("catched", error); + if (!isClusterScoped) { + disposeOnUnmount(this, [ + namespaceStore.onContextChange(() => this.loadStores()) + ]); } } - componentWillUnmount() { + async componentWillUnmount() { this.isUnmounting = true; - const { store, isSelectable } = this.props; + this.unsubscribeStores(); + } - if (isSelectable) store.resetSelection(); + @computed get stores() { + const { store, dependentStores } = this.props; + + return new Set([store, ...dependentStores]); + } + + async loadStores() { + this.unsubscribeStores(); // reset first + + // load + for (const store of this.stores) { + if (this.isUnmounting) { + this.unsubscribeStores(); + break; + } + + try { + await store.loadAll(); + this.watchDisposers.push(store.subscribe()); + } catch (error) { + console.error("loading store error", error); + } + } + } + + unsubscribeStores() { + this.watchDisposers.forEach(dispose => dispose()); + this.watchDisposers.length = 0; } private filterCallbacks: { [type: string]: ItemsFilter } = { @@ -180,9 +202,7 @@ export class ItemListLayout extends React.Component { }; @computed get isReady() { - const { isReady, store } = this.props; - - return typeof isReady == "boolean" ? isReady : store.isLoaded; + return this.props.isReady ?? this.props.store.isLoaded; } @computed get filters() { @@ -228,42 +248,6 @@ export class ItemListLayout extends React.Component { return this.applyFilters(filterItems, allItems); } - updateColumnFilter(checkboxValue: boolean, columnName: string) { - if (checkboxValue){ - this.hiddenColumnNames.delete(columnName); - } else { - this.hiddenColumnNames.add(columnName); - } - - if (this.canBeConfigured) { - userStore.preferences.hiddenTableColumns[this.props.tableId] = Array.from(this.hiddenColumnNames); - } - } - - columnIsVisible(index: number): boolean { - const {renderTableHeader} = this.props; - - if (!this.canBeConfigured) return true; - - return !this.hiddenColumnNames.has(renderTableHeader[index].showWithColumn ?? renderTableHeader[index].className); - } - - get canBeConfigured(): boolean { - const { isConfigurable, tableId, renderTableHeader } = this.props; - - if (!isConfigurable || !tableId) { - return false; - } - - if (!renderTableHeader?.every(({ className }) => className)) { - logger.warning("[ItemObjectList]: cannot configure an object list without all headers being identifiable"); - - return false; - } - - return true; - } - @autobind() getRow(uid: string) { const { @@ -295,20 +279,18 @@ export class ItemListLayout extends React.Component { /> )} { - renderTableContents(item) - .map((content, index) => { - const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content; + renderTableContents(item).map((content, index) => { + const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content; + const headCell = renderTableHeader?.[index]; - if (copyClassNameFromHeadCells && renderTableHeader) { - const headCell = renderTableHeader[index]; + if (copyClassNameFromHeadCells && headCell) { + cellProps.className = cssNames(cellProps.className, headCell.className); + } - if (headCell) { - cellProps.className = cssNames(cellProps.className, headCell.className); - } - } - - return this.columnIsVisible(index) ? : null; - }) + if (!headCell || !this.isHiddenColumn(headCell)) { + return ; + } + }) } {renderItemMenu && ( @@ -347,16 +329,11 @@ export class ItemListLayout extends React.Component { return; } - return ; + return ; } renderNoItems() { - const { allItems, items, filters } = this; - const allItemsCount = allItems.length; - const itemsCount = items.length; - const isFiltered = filters.length > 0 && allItemsCount > itemsCount; - - if (isFiltered) { + if (this.filters.length > 0) { return ( No items found. @@ -369,7 +346,7 @@ export class ItemListLayout extends React.Component { ); } - return ; + return ; } renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode { @@ -413,12 +390,12 @@ export class ItemListLayout extends React.Component { title:
{title}
, info: this.renderInfo(), filters: <> - {!isClusterScoped && } + {!isClusterScoped && } + }}/> , - search: , + search: , }; let header = this.renderHeaderContent(placeholders); @@ -442,10 +419,40 @@ export class ItemListLayout extends React.Component { ); } + renderTableHeader() { + const { renderTableHeader, isSelectable, isConfigurable, store } = this.props; + + if (!renderTableHeader) { + return; + } + + return ( + + {isSelectable && ( + store.toggleSelectionAll(this.items))} + /> + )} + {renderTableHeader.map((cellProps, index) => { + if (!this.isHiddenColumn(cellProps)) { + return ; + } + })} + {isConfigurable && ( + + {this.renderColumnVisibilityMenu()} + + )} + + ); + } + renderList() { const { - isSelectable, tableProps = {}, renderTableHeader, renderItemMenu, - store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem + store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem, + tableProps = {}, } = this.props; const { isReady, removeItemsDialog, items } = this; const { selectedItems } = store; @@ -454,7 +461,7 @@ export class ItemListLayout extends React.Component { return (
{!isReady && ( - + )} {isReady && ( { className: cssNames("box grow", tableProps.className, themeStore.activeTheme.type), })} > - {renderTableHeader && ( - - {isSelectable && ( - store.toggleSelectionAll(items))} - /> - )} - {renderTableHeader.map((cellProps, index) => this.columnIsVisible(index) ? : null)} - { renderItemMenu && - - {this.canBeConfigured && this.renderColumnMenu()} - - } - - )} + {this.renderTableHeader()} { !virtual && items.map(item => this.getRow(item.getId())) } @@ -502,24 +493,47 @@ export class ItemListLayout extends React.Component { ); } - renderColumnMenu() { - const { renderTableHeader} = this.props; + @computed get hiddenColumns() { + return userStore.getHiddenTableColumns(this.props.tableId); + } + + isHiddenColumn({ id: columnId, showWithColumn }: TableCellProps): boolean { + if (!this.props.isConfigurable) { + return false; + } + + return this.hiddenColumns.has(columnId) || ( + showWithColumn && this.hiddenColumns.has(showWithColumn) + ); + } + + updateColumnVisibility({ id: columnId }: TableCellProps, isVisible: boolean) { + const hiddenColumns = new Set(this.hiddenColumns); + + if (!isVisible) { + hiddenColumns.add(columnId); + } else { + hiddenColumns.delete(columnId); + } + + userStore.setHiddenTableColumns(this.props.tableId, hiddenColumns); + } + + renderColumnVisibilityMenu() { + const { renderTableHeader } = this.props; return ( - + {renderTableHeader.map((cellProps, index) => ( - !cellProps.showWithColumn && + !cellProps.showWithColumn && ( - `} - className = "MenuCheckbox" - value ={!this.hiddenColumnNames.has(cellProps.className)} - onChange = {(v) => this.updateColumnFilter(v, cellProps.className)} + `} + value={!this.isHiddenColumn(cellProps)} + onChange={isVisible => this.updateColumnVisibility(cellProps, isVisible)} /> + ) ))} ); diff --git a/src/renderer/components/item-object-list/page-filters.store.ts b/src/renderer/components/item-object-list/page-filters.store.ts index 9bff008aa6..d931cd2575 100644 --- a/src/renderer/components/item-object-list/page-filters.store.ts +++ b/src/renderer/components/item-object-list/page-filters.store.ts @@ -34,14 +34,14 @@ export class PageFiltersStore { namespaceStore.setContext(filteredNs); } }), - reaction(() => namespaceStore.contextNs.toJS(), contextNs => { + namespaceStore.onContextChange(namespaces => { const filteredNs = this.getValues(FilterType.NAMESPACE); - const isChanged = contextNs.length !== filteredNs.length; + const isChanged = namespaces.length !== filteredNs.length; if (isChanged) { this.filters.replace([ ...this.filters.filter(({ type }) => type !== FilterType.NAMESPACE), - ...contextNs.map(ns => ({ type: FilterType.NAMESPACE, value: ns })), + ...namespaces.map(ns => ({ type: FilterType.NAMESPACE, value: ns })), ]); } }, { diff --git a/src/renderer/components/item-object-list/table-menu.scss b/src/renderer/components/item-object-list/table-menu.scss deleted file mode 100644 index b7e41f54ca..0000000000 --- a/src/renderer/components/item-object-list/table-menu.scss +++ /dev/null @@ -1,4 +0,0 @@ -.MenuCheckbox { - width: 100%; - height: 100%; -} diff --git a/src/renderer/components/table/table-cell.tsx b/src/renderer/components/table/table-cell.tsx index 97335078f1..81e2f9f85f 100644 --- a/src/renderer/components/table/table-cell.tsx +++ b/src/renderer/components/table/table-cell.tsx @@ -9,13 +9,14 @@ import { Checkbox } from "../checkbox"; export type TableCellElem = React.ReactElement; export interface TableCellProps extends React.DOMAttributes { + id?: string; // used for configuration visibility of columns className?: string; title?: ReactNode; checkbox?: boolean; // render cell with a checkbox isChecked?: boolean; // mark checkbox as checked or not renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean" sortBy?: TableSortBy; // column name, must be same as key in sortable object
- showWithColumn?: string // className of another column, if it is not empty the current column is not shown in the filter menu, visibility of this one is the same as a specified column, applicable to headers only + showWithColumn?: string // id of the column which follow same visibility rules _sorting?: Partial; //
sorting state, don't use this prop outside (!) _sort?(sortBy: TableSortBy): void; //
sort function, don't use this prop outside (!) _nowrap?: boolean; // indicator, might come from parent , don't use this prop outside (!) @@ -73,7 +74,7 @@ export class TableCell extends React.Component { const content = displayBooleans(displayBoolean, title || children); return ( -
+
{this.renderCheckbox()} {_nowrap ?
{content}
: content} {this.renderSortIcon()} diff --git a/src/renderer/item.store.ts b/src/renderer/item.store.ts index 2105954d32..eccd2b52df 100644 --- a/src/renderer/item.store.ts +++ b/src/renderer/item.store.ts @@ -9,7 +9,7 @@ export interface ItemObject { @autobind() export abstract class ItemStore { - abstract loadAll(): Promise; + abstract loadAll(...args: any[]): Promise; protected defaultSorting = (item: T) => item.getName(); @@ -40,8 +40,7 @@ export abstract class ItemStore { if (item) { return item; - } - else { + } else { const items = this.sortItems([...this.items, newItem]); this.items.replace(items); @@ -83,8 +82,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 +128,7 @@ export abstract class ItemStore { toggleSelection(item: T) { if (this.isSelected(item)) { this.unselect(item); - } - else { + } else { this.select(item); } } @@ -142,8 +139,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 bb2fffd819..956f5aa5f6 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -1,3 +1,4 @@ +import type { Cluster } from "../main/cluster"; import { action, observable, reaction } from "mobx"; import { autobind } from "./utils"; import { KubeObject } from "./api/kube-object"; @@ -6,7 +7,11 @@ import { ItemStore } from "./item.store"; import { apiManager } from "./api/api-manager"; import { IKubeApiQueryParams, KubeApi } from "./api/kube-api"; import { KubeJsonApiData } from "./api/kube-json-api"; -import { getHostedCluster } from "../common/cluster-store"; + +export interface KubeObjectStoreLoadingParams { + namespaces: string[]; + api?: KubeApi; +} @autobind() export abstract class KubeObjectStore extends ItemStore { @@ -71,14 +76,26 @@ export abstract class KubeObjectStore extends ItemSt } } - protected async loadItems(allowedNamespaces?: string[]): Promise { - if (!this.api.isNamespaced || !allowedNamespaces) { - return this.api.list({}, this.query); - } else { - return Promise - .all(allowedNamespaces.map(namespace => this.api.list({ namespace }))) - .then(items => items.flat()); + protected async resolveCluster(): Promise { + const { getHostedCluster } = await import("../common/cluster-store"); + + return getHostedCluster(); + } + + protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise { + const cluster = await this.resolveCluster(); + + if (cluster.isAllowedResource(api.kind)) { + if (api.isNamespaced) { + return Promise + .all(namespaces.map(namespace => api.list({ namespace }))) + .then(items => items.flat()); + } + + return api.list({}, this.query); } + + return []; } protected filterItemsOnLoad(items: T[]) { @@ -86,30 +103,35 @@ export abstract class KubeObjectStore extends ItemSt } @action - async loadAll() { + async loadAll({ namespaces: contextNamespaces }: { namespaces?: string[] } = {}) { this.isLoading = true; - let items: T[]; try { - const { allowedNamespaces, accessibleNamespaces, isAdmin } = getHostedCluster(); + if (!contextNamespaces) { + const { namespaceStore } = await import("./components/+namespaces/namespace.store"); - if (isAdmin && accessibleNamespaces.length == 0) { - items = await this.loadItems(); - } else { - items = await this.loadItems(allowedNamespaces); + contextNamespaces = namespaceStore.getContextNamespaces(); } + let items = await this.loadItems({ namespaces: contextNamespaces, api: this.api }); + items = this.filterItemsOnLoad(items); - } finally { - if (items) { - items = this.sortItems(items); - this.items.replace(items); - } - this.isLoading = false; + items = this.sortItems(items); + + this.items.replace(items); this.isLoaded = true; + } catch (error) { + console.error("Loading store items failed", { error, store: this }); + this.resetOnError(error); + } finally { + this.isLoading = false; } } + protected resetOnError(error: any) { + if (error) this.reset(); + } + protected async loadItem(params: { name: string; namespace?: string }): Promise { return this.api.get(params); } @@ -194,7 +216,7 @@ export abstract class KubeObjectStore extends ItemSt // create latest non-observable copy of items to apply updates in one action (==single render) const items = this.items.toJS(); - for (const {type, object} of this.eventsBuffer.clear()) { + for (const { type, object } of this.eventsBuffer.clear()) { const index = items.findIndex(item => item.getId() === object.metadata?.uid); const item = items[index]; const api = apiManager.getApiByKind(object.kind, object.apiVersion); From 9da349ce42aedd714904e7a4eb17b093eb7fb72e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 22 Jan 2021 16:51:42 +0200 Subject: [PATCH 08/56] Display CPU usage percentage with 2 decimal points (#2000) Signed-off-by: Alex Culliere --- src/renderer/components/+nodes/nodes.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/+nodes/nodes.tsx b/src/renderer/components/+nodes/nodes.tsx index edfa5c4026..1ca12343b5 100644 --- a/src/renderer/components/+nodes/nodes.tsx +++ b/src/renderer/components/+nodes/nodes.tsx @@ -51,6 +51,10 @@ export class Nodes extends React.Component { if (!metrics || !metrics[1]) return ; const usage = metrics[0]; const cores = metrics[1]; + const cpuUsagePercent = Math.ceil(usage * 100) / cores; + const cpuUsagePercentLabel: String = cpuUsagePercent % 1 === 0 + ? cpuUsagePercent.toString() + : cpuUsagePercent.toFixed(2); return ( { value={usage} tooltip={{ preferredPositions: TooltipPosition.BOTTOM, - children: `CPU: ${Math.ceil(usage * 100) / cores}\%, cores: ${cores}` + children: `CPU: ${cpuUsagePercentLabel}\%, cores: ${cores}` }} /> ); From 79db7bbbe44eb70d6e7f5c027b238fcd0d3b4344 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 25 Jan 2021 09:33:08 +0200 Subject: [PATCH 09/56] Fix azure pipeline integration test exit code (#1980) Signed-off-by: Jari Kolehmainen --- .azure-pipelines.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 6bb07489ec..d7ed058fa3 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -58,6 +58,7 @@ jobs: - script: make test-extensions displayName: Run In-tree Extension tests - bash: | + set -e rm -rf extensions/telemetry make integration-win git checkout extensions/telemetry @@ -102,6 +103,7 @@ jobs: - script: make test-extensions displayName: Run In-tree Extension tests - bash: | + set -e rm -rf extensions/telemetry make integration-mac git checkout extensions/telemetry @@ -159,6 +161,7 @@ jobs: sudo chown -R $USER $HOME/.kube $HOME/.minikube displayName: Install integration test dependencies - bash: | + set -e rm -rf extensions/telemetry xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux git checkout extensions/telemetry From 3a4da8793355ebc81120c08bf1f805a0c959cb8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 13:00:23 -0500 Subject: [PATCH 10/56] Bump marked from 1.1.0 to 1.2.7 (#1976) Bumps [marked](https://github.com/markedjs/marked) from 1.1.0 to 1.2.7. - [Release notes](https://github.com/markedjs/marked/releases) - [Changelog](https://github.com/markedjs/marked/blob/master/release.config.js) - [Commits](https://github.com/markedjs/marked/compare/v1.1.0...v1.2.7) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 57c7a8a657..a483f6b1c5 100644 --- a/package.json +++ b/package.json @@ -200,7 +200,7 @@ "jsonpath": "^1.0.2", "lodash": "^4.17.15", "mac-ca": "^1.0.4", - "marked": "^1.1.0", + "marked": "^1.2.7", "md5-file": "^5.0.0", "mobx": "^5.15.7", "mobx-observable-history": "^1.0.3", diff --git a/yarn.lock b/yarn.lock index 4cfa01fdf4..c61391d58d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8807,10 +8807,10 @@ marked@^0.8.0: resolved "https://registry.yarnpkg.com/marked/-/marked-0.8.2.tgz#4faad28d26ede351a7a1aaa5fec67915c869e355" integrity sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw== -marked@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/marked/-/marked-1.1.0.tgz#62504ad4d11550c942935ccc5e39d64e5a4c4e50" - integrity sha512-EkE7RW6KcXfMHy2PA7Jg0YJE1l8UPEZE8k45tylzmZM30/r1M1MUXWQfJlrSbsTeh7m/XTwHbWUENvAJZpp1YA== +marked@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.7.tgz#6e14b595581d2319cdcf033a24caaf41455a01fb" + integrity sha512-No11hFYcXr/zkBvL6qFmAp1z6BKY3zqLMHny/JN/ey+al7qwCM2+CMBL9BOgqMxZU36fz4cCWfn2poWIf7QRXA== matcher@^3.0.0: version "3.0.0" From 75f4d0df75a58f62eb6d6a4aa1f331a87287c581 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 13:36:20 -0500 Subject: [PATCH 11/56] Bump elliptic from 6.5.2 to 6.5.3 (#633) Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3. - [Release notes](https://github.com/indutny/elliptic/releases) - [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c61391d58d..6dcc19a63c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4811,9 +4811,9 @@ electron@^9.4.0: extract-zip "^1.0.3" elliptic@^6.0.0, elliptic@^6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" - integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== dependencies: bn.js "^4.4.0" brorand "^1.0.1" From 09dc2790db366f74a6a96a5cf95558be580bb479 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jan 2021 13:36:35 -0500 Subject: [PATCH 12/56] Bump make-plural from 6.2.1 to 6.2.2 (#1982) Bumps [make-plural](https://github.com/eemeli/make-plural/tree/HEAD/packages/plurals) from 6.2.1 to 6.2.2. - [Release notes](https://github.com/eemeli/make-plural/releases) - [Changelog](https://github.com/eemeli/make-plural/blob/master/packages/plurals/CHANGELOG.md) - [Commits](https://github.com/eemeli/make-plural/commits/make-plural@6.2.2/packages/plurals) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a483f6b1c5..1b3cc13c32 100644 --- a/package.json +++ b/package.json @@ -313,7 +313,7 @@ "jest-canvas-mock": "^2.3.0", "jest-fetch-mock": "^3.0.3", "jest-mock-extended": "^1.0.10", - "make-plural": "^6.2.1", + "make-plural": "^6.2.2", "mini-css-extract-plugin": "^0.9.0", "moment": "^2.26.0", "node-loader": "^0.6.0", diff --git a/yarn.lock b/yarn.lock index 6dcc19a63c..5e1cfa376d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8766,10 +8766,10 @@ make-fetch-happen@^5.0.0: socks-proxy-agent "^4.0.0" ssri "^6.0.0" -make-plural@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-6.2.1.tgz#2790af1d05fb2fc35a111ce759ffdb0aca1339a3" - integrity sha512-AmkruwJ9EjvyTv6AM8MBMK3TAeOJvhgTv5YQXzF0EP2qawhpvMjDpHvsdOIIT0Vn+BB0+IogmYZ1z+Ulm/m0Fg== +make-plural@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-6.2.2.tgz#beb5fd751355e72660eeb2218bb98eec92853c6c" + integrity sha512-8iTuFioatnTTmb/YJjywkVIHLjcwkFD9Ms0JpxjEm9Mo8eQYkh1z+55dwv4yc1jQ8ftVBxWQbihvZL1DfzGGWA== makeerror@1.0.x: version "1.0.11" From 5f1960612964793ca075618d4b0a78c8f336f6c7 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Tue, 26 Jan 2021 13:26:45 +0200 Subject: [PATCH 13/56] Fix Helm repositories and pod logs integration tests (#2015) * Fix Helm repository and pod logs integration tests Signed-off-by: Lauri Nevala * Return parsed object Signed-off-by: Lauri Nevala --- integration/__tests__/app.tests.ts | 42 +++++++++++++++++++++++------- integration/helpers/utils.ts | 23 ++++++++++++++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index c986e4804e..ca30015fa1 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -6,10 +6,10 @@ */ import { Application } from "spectron"; import * as utils from "../helpers/utils"; -import { spawnSync, exec } from "child_process"; -import * as util from "util"; +import { spawnSync } from "child_process"; +import { listHelmRepositories } from "../helpers/utils"; +import { fail } from "assert"; -export const promiseExec = util.promisify(exec); jest.setTimeout(60000); @@ -96,8 +96,11 @@ describe("Lens integration tests", () => { }); it("ensures helm repos", async () => { - const { stdout: reposJson } = await promiseExec("helm repo list -o json"); - const repos = JSON.parse(reposJson); + const repos = await listHelmRepositories(); + + if (!repos[0]) { + fail("Lens failed to add Bitnami repository"); + } await app.client.waitUntilTextExists("div.repos #message-bitnami", repos[0].name); // wait for the helm-cli to fetch the repo(s) await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down @@ -505,19 +508,35 @@ describe("Lens integration tests", () => { await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); await app.client.click('a[href^="/pods"]'); + await app.client.click(".NamespaceSelect"); + await app.client.keys("kube-system"); + await app.client.keys("Enter");// "\uE007" await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); + let podMenuItemEnabled = false; + + // Wait until extensions are enabled on renderer + while (!podMenuItemEnabled) { + const logs = await app.client.getRenderProcessLogs(); + + podMenuItemEnabled = !!logs.find(entry => entry.message.includes("[EXTENSION]: enabled lens-pod-menu@")); + + if (!podMenuItemEnabled) { + await new Promise(r => setTimeout(r, 1000)); + } + } + await new Promise(r => setTimeout(r, 500)); // Give some extra time to prepare extensions // Open logs tab in dock await app.client.click(".list .TableRow:first-child"); await app.client.waitForVisible(".Drawer"); await app.client.click(".drawer-title .Menu li:nth-child(2)"); // Check if controls are available - await app.client.waitForVisible(".Logs .VirtualList"); + await app.client.waitForVisible(".LogList .VirtualList"); await app.client.waitForVisible(".LogResourceSelector"); - await app.client.waitForVisible(".LogResourceSelector .SearchInput"); - await app.client.waitForVisible(".LogResourceSelector .SearchInput input"); + //await app.client.waitForVisible(".LogSearch .SearchInput"); + await app.client.waitForVisible(".LogSearch .SearchInput input"); // Search for semicolon await app.client.keys(":"); - await app.client.waitForVisible(".Logs .list span.active"); + await app.client.waitForVisible(".LogList .list span.active"); // Click through controls await app.client.click(".LogControls .show-timestamps"); await app.client.click(".LogControls .show-previous"); @@ -556,7 +575,10 @@ describe("Lens integration tests", () => { await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); await app.client.click('a[href^="/pods"]'); - await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); + + await app.client.click(".NamespaceSelect"); + await app.client.keys(TEST_NAMESPACE); + await app.client.keys("Enter");// "\uE007" await app.client.click(".Icon.new-dock-tab"); await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource"); await app.client.click("li.MenuItem.create-resource-tab"); diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index f445a9ae48..a865280fed 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -1,4 +1,6 @@ import { Application } from "spectron"; +import * as util from "util"; +import { exec } from "child_process"; const AppPaths: Partial> = { "win32": "./dist/win-unpacked/Lens.exe", @@ -26,7 +28,12 @@ export function setup(): Application { }); } +type HelmRepository = { + name: string; + url: string; +}; type AsyncPidGetter = () => Promise; +export const promiseExec = util.promisify(exec); export async function tearDown(app: Application) { const pid = await (app.mainProcess.pid as any as AsyncPidGetter)(); @@ -39,3 +46,19 @@ export async function tearDown(app: Application) { console.error(e); } } + +export async function listHelmRepositories(retries = 0): Promise{ + if (retries < 5) { + try { + const { stdout: reposJson } = await promiseExec("helm repo list -o json"); + + return JSON.parse(reposJson); + } catch { + await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository + + return await listHelmRepositories((retries + 1)); + } + } + + return []; +} From 9191d6bfd98caa39572365e51c8904f7375959fc Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Tue, 26 Jan 2021 16:30:26 +0300 Subject: [PATCH 14/56] Always shows .menu table column (#2024) Signed-off-by: Alex Andreev --- .../components/item-object-list/item-list-layout.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 aaeb7438ea..6b4ff4fd16 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -440,11 +440,9 @@ export class ItemListLayout extends React.Component { return ; } })} - {isConfigurable && ( - - {this.renderColumnVisibilityMenu()} - - )} + + {isConfigurable && this.renderColumnVisibilityMenu()} + ); } From 724c6c326587e76a46c4ce2d440976ef8bfe34dc Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 27 Jan 2021 16:15:13 +0200 Subject: [PATCH 15/56] Fix extension cluster submenu re-render (#1996) (#2027) * fix extension cluster pages re-render when they are registered as a sub-menu item Signed-off-by: Jari Kolehmainen * lint fix Signed-off-by: Jari Kolehmainen * refactor Signed-off-by: Jari Kolehmainen --- src/renderer/components/app.tsx | 46 +++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 495182b2a9..595506fcf9 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -42,7 +42,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 } from "mobx"; +import { reaction, computed, observable } from "mobx"; import { nodesStore } from "./+nodes/nodes.store"; import { podsStore } from "./+workloads-pods/pods.store"; import { sum } from "lodash"; @@ -75,6 +75,8 @@ export class App extends React.Component { whatInput.ask(); // Start to monitor user input device } + @observable extensionRoutes: Map = new Map(); + async componentDidMount() { const cluster = getHostedCluster(); const promises: Promise[] = []; @@ -101,6 +103,12 @@ export class App extends React.Component { reaction(() => this.warningsCount, (count) => { broadcastMessage(`cluster-warning-event-count:${cluster.id}`, count); }); + + reaction(() => clusterPageMenuRegistry.getRootItems(), (rootItems) => { + this.generateExtensionTabLayoutRoutes(rootItems); + }, { + fireImmediately: true + }); } @computed @@ -143,22 +151,38 @@ export class App extends React.Component { return routes; } - renderExtensionTabLayoutRoutes() { - return clusterPageMenuRegistry.getRootItems().map((menu, index) => { - const tabRoutes = this.getTabLayoutRoutes(menu); + generateExtensionTabLayoutRoutes(rootItems: ClusterPageMenuRegistration[]) { + rootItems.forEach((menu, index) => { + let route = this.extensionRoutes.get(menu); - if (tabRoutes.length > 0) { - const pageComponent = () => ; + if (!route) { + const tabRoutes = this.getTabLayoutRoutes(menu); - return tab.routePath)}/>; - } else { - const page = clusterPageRegistry.getByPageTarget(menu.target); + if (tabRoutes.length > 0) { + const pageComponent = () => ; - if (page) { - return ; + route = tab.routePath)} />; + this.extensionRoutes.set(menu, route); + } else { + const page = clusterPageRegistry.getByPageTarget(menu.target); + + if (page) { + route = ; + this.extensionRoutes.set(menu, route); + } } } }); + + Array.from(this.extensionRoutes.keys()).forEach((menu) => { + if (!rootItems.includes(menu)) { + this.extensionRoutes.delete(menu); + } + }); + } + + renderExtensionTabLayoutRoutes() { + return Array.from(this.extensionRoutes.values()); } renderExtensionRoutes() { From a157eb03e6c10376fc45c7062b290da7b5a442b5 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Wed, 27 Jan 2021 17:20:02 +0300 Subject: [PATCH 16/56] Generic logs view with Pod selector (#1984) * Adding LogTabStore Signed-off-by: Alex Andreev * Adding Pod selector in logs tab Signed-off-by: Alex Andreev * Refresh containers on pod change Signed-off-by: Alex Andreev * Adding tests Signed-off-by: Alex Andreev * Adding LogTabStore tests Signed-off-by: Alex Andreev * Clearn getPodsByOwnerId method Signed-off-by: Alex Andreev * Extracting dummy pods into mock file Signed-off-by: Alex Andreev * Eliminating containers and initContainers from store Signed-off-by: Alex Andreev * Refreshing tab pods if pod amount is changed in store Signed-off-by: Alex Andreev * A bit of cleaning up, fixing tests Signed-off-by: Alex Andreev * Fix lint newline errors Signed-off-by: Alex Andreev * Return getPodsByOwner() method Signed-off-by: Alex Andreev * Rename log tab when pod changes Signed-off-by: Alex Andreev --- extensions/pod-menu/package-lock.json | 656 +++++++++++++++++- extensions/pod-menu/src/logs-menu.tsx | 8 +- package.json | 1 + src/common/search-store.ts | 9 +- src/extensions/renderer-api/components.ts | 2 +- src/renderer/api/kube-json-api.ts | 2 +- .../+workloads-daemonsets/daemonsets.store.ts | 2 +- .../components/+workloads-jobs/job.store.ts | 2 +- .../components/+workloads-pods/pods.store.ts | 8 +- .../replicasets.store.ts | 2 +- .../statefulset.store.ts | 2 +- .../__test__/log-resource-selector.test.tsx | 103 +++ .../dock/__test__/log-tab.store.test.ts | 113 +++ .../components/dock/__test__/pod.mock.ts | 203 ++++++ src/renderer/components/dock/dock-tabs.tsx | 2 +- src/renderer/components/dock/dock.store.ts | 6 + src/renderer/components/dock/dock.tsx | 2 +- src/renderer/components/dock/log-controls.tsx | 13 +- src/renderer/components/dock/log-list.tsx | 7 +- .../components/dock/log-resource-selector.tsx | 65 +- src/renderer/components/dock/log-tab.store.ts | 123 ++++ src/renderer/components/dock/log.store.ts | 88 +-- src/renderer/components/dock/logs.tsx | 20 +- src/renderer/kube-object.store.ts | 4 + yarn.lock | 51 +- 25 files changed, 1359 insertions(+), 135 deletions(-) create mode 100644 src/renderer/components/dock/__test__/log-resource-selector.test.tsx create mode 100644 src/renderer/components/dock/__test__/log-tab.store.test.ts create mode 100644 src/renderer/components/dock/__test__/pod.mock.ts create mode 100644 src/renderer/components/dock/log-tab.store.ts diff --git a/extensions/pod-menu/package-lock.json b/extensions/pod-menu/package-lock.json index ea98213fb3..4409d2a89d 100644 --- a/extensions/pod-menu/package-lock.json +++ b/extensions/pod-menu/package-lock.json @@ -626,7 +626,644 @@ }, "@k8slens/extensions": { "version": "file:../../src/extensions/npm/extensions", - "dev": true + "dev": true, + "requires": { + "@material-ui/core": "*", + "@types/node": "*", + "@types/react-select": "*", + "conf": "^7.0.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "dev": true + }, + "@material-ui/core": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.2.tgz", + "integrity": "sha512-/D1+AQQeYX/WhT/FUk78UCRj8ch/RCglsQLYujYTIqPSJlwZHKcvHidNeVhODXeApojeXjkl0tWdk5C9ofwOkQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.2", + "@material-ui/system": "^4.11.2", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + } + }, + "@material-ui/styles": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.2.tgz", + "integrity": "sha512-xbItf8zkfD3FuGoD9f2vlcyPf9jTEtj9YTJoNNV+NMWaSAHXgrW6geqRoo/IwBuMjqpwqsZhct13e2nUyU9Ljw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.0.3", + "jss-plugin-camel-case": "^10.0.3", + "jss-plugin-default-unit": "^10.0.3", + "jss-plugin-global": "^10.0.3", + "jss-plugin-nested": "^10.0.3", + "jss-plugin-props-sort": "^10.0.3", + "jss-plugin-rule-value-function": "^10.0.3", + "jss-plugin-vendor-prefixer": "^10.0.3", + "prop-types": "^15.7.2" + } + }, + "@material-ui/system": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.2.tgz", + "integrity": "sha512-BELFJEel5E+5DMiZb6XXT3peWRn6UixRvBtKwSxqntmD0+zwbbfCij6jtGwwdJhN1qX/aXrKu10zX31GBaeR7A==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + } + }, + "@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "dev": true + }, + "@material-ui/utils": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", + "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, + "@types/node": { + "version": "14.14.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz", + "integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, + "@types/react": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", + "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", + "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==", + "dev": true + } + } + }, + "@types/react-dom": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz", + "integrity": "sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-select": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.1.2.tgz", + "integrity": "sha512-ygvR/2FL87R2OLObEWFootYzkvm67LRA+URYEAcBuvKk7IXmdsnIwSGm60cVXGaqkJQHozb2Cy1t94tCYb6rJA==", + "dev": true, + "requires": { + "@types/react": "*", + "@types/react-dom": "*", + "@types/react-transition-group": "*" + } + }, + "@types/react-transition-group": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", + "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "dev": true + }, + "clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "dev": true + }, + "conf": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-7.1.2.tgz", + "integrity": "sha512-r8/HEoWPFn4CztjhMJaWNAe5n+gPUCSaJ0oufbqDLFKsA1V8JjAG7G+p0pgoDFAws9Bpk2VtVLLXqOBA7WxLeg==", + "dev": true, + "requires": { + "ajv": "^6.12.2", + "atomically": "^1.3.1", + "debounce-fn": "^4.0.0", + "dot-prop": "^5.2.0", + "env-paths": "^2.2.0", + "json-schema-typed": "^7.0.3", + "make-dir": "^3.1.0", + "onetime": "^5.1.0", + "pkg-up": "^3.1.0", + "semver": "^7.3.2" + } + }, + "css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, + "csstype": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.14.tgz", + "integrity": "sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==", + "dev": true + }, + "debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dev": true, + "requires": { + "mimic-fn": "^3.0.0" + } + }, + "dom-helpers": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", + "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", + "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==", + "dev": true + } + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "requires": { + "react-is": "^16.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } + } + }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==", + "dev": true + }, + "indefinite-observable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz", + "integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==", + "dev": true, + "requires": { + "symbol-observable": "1.2.0" + } + }, + "is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=", + "dev": true + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", + "dev": true + }, + "jss": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.5.0.tgz", + "integrity": "sha512-B6151NvG+thUg3murLNHRPLxTLwQ13ep4SH5brj4d8qKtogOx/jupnpfkPGSHPqvcwKJaCLctpj2lEk+5yGwMw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "indefinite-observable": "^2.0.1", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", + "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==", + "dev": true + } + } + }, + "jss-plugin-camel-case": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.0.tgz", + "integrity": "sha512-GSjPL0adGAkuoqeYiXTgO7PlIrmjv5v8lA6TTBdfxbNYpxADOdGKJgIEkffhlyuIZHlPuuiFYTwUreLUmSn7rg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.5.0" + } + }, + "jss-plugin-default-unit": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.0.tgz", + "integrity": "sha512-rsbTtZGCMrbcb9beiDd+TwL991NGmsAgVYH0hATrYJtue9e+LH/Gn4yFD1ENwE+3JzF3A+rPnM2JuD9L/SIIWw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-global": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.5.0.tgz", + "integrity": "sha512-FZd9+JE/3D7HMefEG54fEC0XiQ9rhGtDHAT/ols24y8sKQ1D5KIw6OyXEmIdKFmACgxZV2ARQ5pAUypxkk2IFQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-nested": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.5.0.tgz", + "integrity": "sha512-ejPlCLNlEGgx8jmMiDk/zarsCZk+DV0YqXfddpgzbO9Toamo0HweCFuwJ3ZO40UFOfqKwfpKMVH/3HUXgxkTMg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-props-sort": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.0.tgz", + "integrity": "sha512-kTLRvrOetFKz5vM88FAhLNeJIxfjhCepnvq65G7xsAQ/Wgy7HwO1BS/2wE5mx8iLaAWC6Rj5h16mhMk9sKdZxg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "jss-plugin-rule-value-function": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.0.tgz", + "integrity": "sha512-jXINGr8BSsB13JVuK274oEtk0LoooYSJqTBCGeBu2cG/VJ3+4FPs1gwLgsq24xTgKshtZ+WEQMVL34OprLidRA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-vendor-prefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.0.tgz", + "integrity": "sha512-rux3gmfwDdOKCLDx0IQjTwTm03IfBa+Rm/hs747cOw5Q7O3RaTUIMPKjtVfc31Xr/XI9Abz2XEupk1/oMQ7zRA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.5.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + } + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==", + "dev": true + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + }, + "react-transition-group": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } }, "@sinonjs/commons": { "version": "1.8.1", @@ -2796,7 +3433,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3226,6 +3864,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4382,6 +5021,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4396,6 +5036,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "optional": true, "requires": { "yallist": "^4.0.0" } @@ -4405,6 +5046,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -4414,6 +5056,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -4422,7 +5065,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true } } }, @@ -5438,7 +6082,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -6315,7 +6960,8 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", - "dev": true + "dev": true, + "optional": true }, "v8-to-istanbul": { "version": "7.0.0", diff --git a/extensions/pod-menu/src/logs-menu.tsx b/extensions/pod-menu/src/logs-menu.tsx index 706efcf128..1063207d0c 100644 --- a/extensions/pod-menu/src/logs-menu.tsx +++ b/extensions/pod-menu/src/logs-menu.tsx @@ -9,13 +9,9 @@ export class PodLogsMenu extends React.Component { Navigation.hideDetails(); const pod = this.props.object; - Component.createPodLogsTab({ - pod, - containers: pod.getContainers(), - initContainers: pod.getInitContainers(), + Component.logTabStore.createPodTab({ + selectedPod: pod, selectedContainer: container, - showTimestamps: false, - previous: false, }); } diff --git a/package.json b/package.json index 1b3cc13c32..3a06db621b 100644 --- a/package.json +++ b/package.json @@ -328,6 +328,7 @@ "react-refresh": "^0.9.0", "react-router-dom": "^5.2.0", "react-select": "^3.1.0", + "react-select-event": "^5.1.0", "react-window": "^1.8.5", "sass-loader": "^8.0.2", "sharp": "^0.26.1", diff --git a/src/common/search-store.ts b/src/common/search-store.ts index eb2517ca0f..86a6054af3 100644 --- a/src/common/search-store.ts +++ b/src/common/search-store.ts @@ -1,4 +1,5 @@ -import { action, computed, observable } from "mobx"; +import { action, computed, observable,reaction } from "mobx"; +import { dockStore } from "../renderer/components/dock/dock.store"; import { autobind } from "../renderer/utils"; export class SearchStore { @@ -6,6 +7,12 @@ export class SearchStore { @observable occurrences: number[] = []; // Array with line numbers, eg [0, 0, 10, 21, 21, 40...] @observable activeOverlayIndex = -1; // Index withing the occurences array. Showing where is activeOverlay currently located + constructor() { + reaction(() => dockStore.selectedTabId, () => { + searchStore.reset(); + }); + } + /** * Sets default activeOverlayIndex * @param text An array of any textual data (logs, for example) diff --git a/src/extensions/renderer-api/components.ts b/src/extensions/renderer-api/components.ts index a9a519498b..49c747da3a 100644 --- a/src/extensions/renderer-api/components.ts +++ b/src/extensions/renderer-api/components.ts @@ -38,4 +38,4 @@ export * from "../../renderer/components/+events/kube-event-details"; // specific exports export * from "../../renderer/components/status-brick"; export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store"; -export { createPodLogsTab } from "../../renderer/components/dock/log.store"; +export { logTabStore } from "../../renderer/components/dock/log-tab.store"; diff --git a/src/renderer/api/kube-json-api.ts b/src/renderer/api/kube-json-api.ts index 3026a9a956..362ee5438e 100644 --- a/src/renderer/api/kube-json-api.ts +++ b/src/renderer/api/kube-json-api.ts @@ -21,7 +21,7 @@ export interface KubeJsonApiData extends JsonApiData { resourceVersion: string; continue?: string; finalizers?: string[]; - selfLink: string; + selfLink?: string; labels?: { [label: string]: string; }; diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts b/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts index ad9713e96b..b8c8dee573 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts +++ b/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts @@ -18,7 +18,7 @@ export class DaemonSetStore extends KubeObjectStore { } getChildPods(daemonSet: DaemonSet): Pod[] { - return podsStore.getPodsByOwner(daemonSet); + return podsStore.getPodsByOwnerId(daemonSet.getId()); } getStatuses(daemonSets?: DaemonSet[]) { diff --git a/src/renderer/components/+workloads-jobs/job.store.ts b/src/renderer/components/+workloads-jobs/job.store.ts index 569c9efb13..41d514df8d 100644 --- a/src/renderer/components/+workloads-jobs/job.store.ts +++ b/src/renderer/components/+workloads-jobs/job.store.ts @@ -10,7 +10,7 @@ export class JobStore extends KubeObjectStore { api = jobApi; getChildPods(job: Job): Pod[] { - return podsStore.getPodsByOwner(job); + return podsStore.getPodsByOwnerId(job.getId()); } getJobsByOwner(cronJob: CronJob) { diff --git a/src/renderer/components/+workloads-pods/pods.store.ts b/src/renderer/components/+workloads-pods/pods.store.ts index 9cd3c3b2f9..5a535cec66 100644 --- a/src/renderer/components/+workloads-pods/pods.store.ts +++ b/src/renderer/components/+workloads-pods/pods.store.ts @@ -3,8 +3,8 @@ import { action, observable } from "mobx"; import { KubeObjectStore } from "../../kube-object.store"; import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils"; import { IPodMetrics, Pod, PodMetrics, podMetricsApi, podsApi } from "../../api/endpoints"; -import { WorkloadKubeObject } from "../../api/workload-kube-object"; import { apiManager } from "../../api/api-manager"; +import { WorkloadKubeObject } from "../../api/workload-kube-object"; @autobind() export class PodsStore extends KubeObjectStore { @@ -44,6 +44,12 @@ export class PodsStore extends KubeObjectStore { }); } + getPodsByOwnerId(workloadId: string): Pod[] { + return this.items.filter(pod => { + return pod.getOwnerRefs().find(owner => owner.uid === workloadId); + }); + } + getPodsByNode(node: string) { if (!this.isLoaded) return []; diff --git a/src/renderer/components/+workloads-replicasets/replicasets.store.ts b/src/renderer/components/+workloads-replicasets/replicasets.store.ts index 337f9c0ae1..ca58006930 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.store.ts +++ b/src/renderer/components/+workloads-replicasets/replicasets.store.ts @@ -18,7 +18,7 @@ export class ReplicaSetStore extends KubeObjectStore { } getChildPods(replicaSet: ReplicaSet) { - return podsStore.getPodsByOwner(replicaSet); + return podsStore.getPodsByOwnerId(replicaSet.getId()); } getStatuses(replicaSets: ReplicaSet[]) { diff --git a/src/renderer/components/+workloads-statefulsets/statefulset.store.ts b/src/renderer/components/+workloads-statefulsets/statefulset.store.ts index 12f1f663b9..6ee4bb5c28 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulset.store.ts +++ b/src/renderer/components/+workloads-statefulsets/statefulset.store.ts @@ -17,7 +17,7 @@ export class StatefulSetStore extends KubeObjectStore { } getChildPods(statefulSet: StatefulSet) { - return podsStore.getPodsByOwner(statefulSet); + return podsStore.getPodsByOwnerId(statefulSet.getId()); } getStatuses(statefulSets: StatefulSet[]) { diff --git a/src/renderer/components/dock/__test__/log-resource-selector.test.tsx b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx new file mode 100644 index 0000000000..22d97b7216 --- /dev/null +++ b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx @@ -0,0 +1,103 @@ +/** + * @jest-environment jsdom + */ + +import React from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { render } from "@testing-library/react"; +import selectEvent from "react-select-event"; + +import { Pod } from "../../../api/endpoints"; +import { LogResourceSelector } from "../log-resource-selector"; +import { LogTabData } from "../log-tab.store"; +import { dockerPod, deploymentPod1 } from "./pod.mock"; + +const getComponent = (tabData: LogTabData) => { + return ( + + ); +}; + +const getOnePodTabData = (): LogTabData => { + const selectedPod = new Pod(dockerPod); + + return { + pods: [] as Pod[], + selectedPod, + selectedContainer: selectedPod.getContainers()[0], + }; +}; + +const getFewPodsTabData = (): LogTabData => { + const selectedPod = new Pod(deploymentPod1); + const anotherPod = new Pod(dockerPod); + + return { + pods: [anotherPod], + selectedPod, + selectedContainer: selectedPod.getContainers()[0], + }; +}; + +describe("", () => { + it("renders w/o errors", () => { + const tabData = getOnePodTabData(); + const { container } = render(getComponent(tabData)); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("renders proper namespace", () => { + const tabData = getOnePodTabData(); + const { getByTestId } = render(getComponent(tabData)); + const ns = getByTestId("namespace-badge"); + + expect(ns).toHaveTextContent("default"); + }); + + it("renders proper selected items within dropdowns", () => { + const tabData = getOnePodTabData(); + const { getByText } = render(getComponent(tabData)); + + expect(getByText("dockerExporter")).toBeInTheDocument(); + expect(getByText("docker-exporter")).toBeInTheDocument(); + }); + + it("renders sibling pods in dropdown", () => { + const tabData = getFewPodsTabData(); + const { container, getByText } = render(getComponent(tabData)); + const podSelector: HTMLElement = container.querySelector(".pod-selector"); + + selectEvent.openMenu(podSelector); + + expect(getByText("dockerExporter")).toBeInTheDocument(); + expect(getByText("deploymentPod1")).toBeInTheDocument(); + }); + + it("renders sibling containers in dropdown", () => { + const tabData = getFewPodsTabData(); + const { getByText, container } = render(getComponent(tabData)); + const containerSelector: HTMLElement = container.querySelector(".container-selector"); + + selectEvent.openMenu(containerSelector); + + expect(getByText("node-exporter-1")).toBeInTheDocument(); + expect(getByText("init-node-exporter")).toBeInTheDocument(); + expect(getByText("init-node-exporter-1")).toBeInTheDocument(); + }); + + it("renders pod owner as dropdown title", () => { + const tabData = getFewPodsTabData(); + const { getByText, container } = render(getComponent(tabData)); + const podSelector: HTMLElement = container.querySelector(".pod-selector"); + + selectEvent.openMenu(podSelector); + + expect(getByText("super-deployment")).toBeInTheDocument(); + }); +}); diff --git a/src/renderer/components/dock/__test__/log-tab.store.test.ts b/src/renderer/components/dock/__test__/log-tab.store.test.ts new file mode 100644 index 0000000000..79b93af623 --- /dev/null +++ b/src/renderer/components/dock/__test__/log-tab.store.test.ts @@ -0,0 +1,113 @@ +/** + * @jest-environment jsdom + */ + +import { podsStore } from "../../+workloads-pods/pods.store"; +import { Pod } from "../../../api/endpoints"; +import { dockStore } from "../dock.store"; +import { logTabStore } from "../log-tab.store"; +import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock"; + + +podsStore.items.push(new Pod(dockerPod)); +podsStore.items.push(new Pod(deploymentPod1)); +podsStore.items.push(new Pod(deploymentPod2)); + +describe("log tab store", () => { + afterEach(() => { + logTabStore.reset(); + dockStore.reset(); + }); + + it("creates log tab without sibling pods", () => { + const selectedPod = new Pod(dockerPod); + const selectedContainer = selectedPod.getAllContainers()[0]; + + logTabStore.createPodTab({ + selectedPod, + selectedContainer + }); + + expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({ + pods: [selectedPod], + selectedPod, + selectedContainer, + showTimestamps: false, + previous: false + }); + }); + + it("creates log tab with sibling pods", () => { + const selectedPod = new Pod(deploymentPod1); + const siblingPod = new Pod(deploymentPod2); + const selectedContainer = selectedPod.getInitContainers()[0]; + + logTabStore.createPodTab({ + selectedPod, + selectedContainer + }); + + expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({ + pods: [selectedPod, siblingPod], + selectedPod, + selectedContainer, + showTimestamps: false, + previous: false + }); + }); + + it("removes item from pods list if pod deleted from store", () => { + const selectedPod = new Pod(deploymentPod1); + const selectedContainer = selectedPod.getInitContainers()[0]; + + logTabStore.createPodTab({ + selectedPod, + selectedContainer + }); + + podsStore.items.pop(); + + expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({ + pods: [selectedPod], + selectedPod, + selectedContainer, + showTimestamps: false, + previous: false + }); + }); + + it("adds item into pods list if new sibling pod added to store", () => { + const selectedPod = new Pod(deploymentPod1); + const selectedContainer = selectedPod.getInitContainers()[0]; + + logTabStore.createPodTab({ + selectedPod, + selectedContainer + }); + + podsStore.items.push(new Pod(deploymentPod3)); + + expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({ + pods: [selectedPod, deploymentPod3], + selectedPod, + selectedContainer, + showTimestamps: false, + previous: false + }); + }); + + it("closes tab if no pods left in store", () => { + const selectedPod = new Pod(deploymentPod1); + const selectedContainer = selectedPod.getInitContainers()[0]; + + logTabStore.createPodTab({ + selectedPod, + selectedContainer + }); + + podsStore.items.clear(); + + expect(logTabStore.getData(dockStore.selectedTabId)).toBeUndefined(); + expect(dockStore.getTabById(dockStore.selectedTabId)).toBeUndefined(); + }); +}); diff --git a/src/renderer/components/dock/__test__/pod.mock.ts b/src/renderer/components/dock/__test__/pod.mock.ts new file mode 100644 index 0000000000..acb4704395 --- /dev/null +++ b/src/renderer/components/dock/__test__/pod.mock.ts @@ -0,0 +1,203 @@ +export const dockerPod = { + apiVersion: "v1", + kind: "dummy", + metadata: { + uid: "dockerExporter", + name: "dockerExporter", + creationTimestamp: "dummy", + resourceVersion: "dummy", + namespace: "default" + }, + spec: { + initContainers: [] as any, + containers: [ + { + name: "docker-exporter", + image: "docker.io/prom/node-exporter:v1.0.0-rc.0", + imagePullPolicy: "pull" + } + ], + serviceAccountName: "dummy", + serviceAccount: "dummy", + }, + status: { + phase: "Running", + conditions: [{ + type: "Running", + status: "Running", + lastProbeTime: 1, + lastTransitionTime: "Some time", + }], + hostIP: "dummy", + podIP: "dummy", + startTime: "dummy", + } +}; + +export const deploymentPod1 = { + apiVersion: "v1", + kind: "dummy", + metadata: { + uid: "deploymentPod1", + name: "deploymentPod1", + creationTimestamp: "dummy", + resourceVersion: "dummy", + namespace: "default", + ownerReferences: [{ + apiVersion: "v1", + kind: "Deployment", + name: "super-deployment", + uid: "uuid", + controller: true, + blockOwnerDeletion: true, + }] + }, + spec: { + initContainers: [ + { + name: "init-node-exporter", + image: "docker.io/prom/node-exporter:v1.0.0-rc.0", + imagePullPolicy: "pull" + }, + { + name: "init-node-exporter-1", + image: "docker.io/prom/node-exporter:v1.0.0-rc.0", + imagePullPolicy: "pull" + } + ], + containers: [ + { + name: "node-exporter", + image: "docker.io/prom/node-exporter:v1.0.0-rc.0", + imagePullPolicy: "pull" + }, + { + name: "node-exporter-1", + image: "docker.io/prom/node-exporter:v1.0.0-rc.0", + imagePullPolicy: "pull" + } + ], + serviceAccountName: "dummy", + serviceAccount: "dummy", + }, + status: { + phase: "Running", + conditions: [{ + type: "Running", + status: "Running", + lastProbeTime: 1, + lastTransitionTime: "Some time", + }], + hostIP: "dummy", + podIP: "dummy", + startTime: "dummy", + } +}; + +export const deploymentPod2 = { + apiVersion: "v1", + kind: "dummy", + metadata: { + uid: "deploymentPod2", + name: "deploymentPod2", + creationTimestamp: "dummy", + resourceVersion: "dummy", + namespace: "default", + ownerReferences: [{ + apiVersion: "v1", + kind: "Deployment", + name: "super-deployment", + uid: "uuid", + controller: true, + blockOwnerDeletion: true, + }] + }, + spec: { + initContainers: [ + { + name: "init-node-exporter", + image: "docker.io/prom/node-exporter:v1.0.0-rc.0", + imagePullPolicy: "pull" + }, + { + name: "init-node-exporter-1", + image: "docker.io/prom/node-exporter:v1.0.0-rc.0", + imagePullPolicy: "pull" + } + ], + containers: [ + { + name: "node-exporter", + image: "docker.io/prom/node-exporter:v1.0.0-rc.0", + imagePullPolicy: "pull" + }, + { + name: "node-exporter-1", + image: "docker.io/prom/node-exporter:v1.0.0-rc.0", + imagePullPolicy: "pull" + } + ], + serviceAccountName: "dummy", + serviceAccount: "dummy", + }, + status: { + phase: "Running", + conditions: [{ + type: "Running", + status: "Running", + lastProbeTime: 1, + lastTransitionTime: "Some time", + }], + hostIP: "dummy", + podIP: "dummy", + startTime: "dummy", + } +}; + +export const deploymentPod3 = { + apiVersion: "v1", + kind: "dummy", + metadata: { + uid: "deploymentPod3", + name: "deploymentPod3", + creationTimestamp: "dummy", + resourceVersion: "dummy", + namespace: "default", + ownerReferences: [{ + apiVersion: "v1", + kind: "Deployment", + name: "super-deployment", + uid: "uuid", + controller: true, + blockOwnerDeletion: true, + }] + }, + spec: { + containers: [ + { + name: "node-exporter", + image: "docker.io/prom/node-exporter:v1.0.0-rc.0", + imagePullPolicy: "pull" + }, + { + name: "node-exporter-1", + image: "docker.io/prom/node-exporter:v1.0.0-rc.0", + imagePullPolicy: "pull" + } + ], + serviceAccountName: "dummy", + serviceAccount: "dummy", + }, + status: { + phase: "Running", + conditions: [{ + type: "Running", + status: "Running", + lastProbeTime: 1, + lastTransitionTime: "Some time", + }], + hostIP: "dummy", + podIP: "dummy", + startTime: "dummy", + } +}; diff --git a/src/renderer/components/dock/dock-tabs.tsx b/src/renderer/components/dock/dock-tabs.tsx index 554411024b..d0a3c3d125 100644 --- a/src/renderer/components/dock/dock-tabs.tsx +++ b/src/renderer/components/dock/dock-tabs.tsx @@ -7,7 +7,7 @@ import { DockTab } from "./dock-tab"; import { IDockTab } from "./dock.store"; import { isEditResourceTab } from "./edit-resource.store"; import { isInstallChartTab } from "./install-chart.store"; -import { isLogsTab } from "./log.store"; +import { isLogsTab } from "./log-tab.store"; import { TerminalTab } from "./terminal-tab"; import { isTerminalTab } from "./terminal.store"; import { isUpgradeChartTab } from "./upgrade-chart.store"; diff --git a/src/renderer/components/dock/dock.store.ts b/src/renderer/components/dock/dock.store.ts index 91d72d98d9..423367093e 100644 --- a/src/renderer/components/dock/dock.store.ts +++ b/src/renderer/components/dock/dock.store.ts @@ -208,6 +208,12 @@ export class DockStore { this.closeTabs(tabs); } + renameTab(tabId: TabId, title: string) { + const tab = this.getTabById(tabId); + + tab.title = title; + } + @action selectTab(tabId: TabId) { this.selectedTabId = this.getTabById(tabId)?.id ?? null; diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx index c8adf82992..45e294b3c6 100644 --- a/src/renderer/components/dock/dock.tsx +++ b/src/renderer/components/dock/dock.tsx @@ -17,7 +17,7 @@ import { isEditResourceTab } from "./edit-resource.store"; import { InstallChart } from "./install-chart"; import { isInstallChartTab } from "./install-chart.store"; import { Logs } from "./logs"; -import { isLogsTab } from "./log.store"; +import { isLogsTab } from "./log-tab.store"; import { TerminalWindow } from "./terminal-window"; import { createTerminalTab, isTerminalTab } from "./terminal.store"; import { UpgradeChart } from "./upgrade-chart"; diff --git a/src/renderer/components/dock/log-controls.tsx b/src/renderer/components/dock/log-controls.tsx index 06bbf12863..8400aef584 100644 --- a/src/renderer/components/dock/log-controls.tsx +++ b/src/renderer/components/dock/log-controls.tsx @@ -5,22 +5,23 @@ import { observer } from "mobx-react"; import { Pod } from "../../api/endpoints"; import { cssNames, saveFileDialog } from "../../utils"; -import { IPodLogsData, podLogsStore } from "./log.store"; +import { logStore } from "./log.store"; import { Checkbox } from "../checkbox"; import { Icon } from "../icon"; +import { LogTabData } from "./log-tab.store"; interface Props { - tabData: IPodLogsData + tabData: LogTabData logs: string[] - save: (data: Partial) => void + save: (data: Partial) => void reload: () => void } export const LogControls = observer((props: Props) => { const { tabData, save, reload, logs } = props; const { showTimestamps, previous } = tabData; - const since = logs.length ? podLogsStore.getTimestamps(logs[0]) : null; - const pod = new Pod(tabData.pod); + const since = logs.length ? logStore.getTimestamps(logs[0]) : null; + const pod = new Pod(tabData.selectedPod); const toggleTimestamps = () => { save({ showTimestamps: !showTimestamps }); @@ -33,7 +34,7 @@ export const LogControls = observer((props: Props) => { const downloadLogs = () => { const fileName = pod.getName(); - const logsToDownload = showTimestamps ? logs : podLogsStore.logsWithoutTimestamps; + const logsToDownload = showTimestamps ? logs : logStore.logsWithoutTimestamps; saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain"); }; diff --git a/src/renderer/components/dock/log-list.tsx b/src/renderer/components/dock/log-list.tsx index 74d64d2f58..3b66f42d86 100644 --- a/src/renderer/components/dock/log-list.tsx +++ b/src/renderer/components/dock/log-list.tsx @@ -14,7 +14,8 @@ import { Button } from "../button"; import { Icon } from "../icon"; import { Spinner } from "../spinner"; import { VirtualList } from "../virtual-list"; -import { podLogsStore } from "./log.store"; +import { logStore } from "./log.store"; +import { logTabStore } from "./log-tab.store"; interface Props { logs: string[] @@ -77,10 +78,10 @@ export class LogList extends React.Component { */ @computed get logs() { - const showTimestamps = podLogsStore.getData(this.props.id).showTimestamps; + const showTimestamps = logTabStore.getData(this.props.id).showTimestamps; if (!showTimestamps) { - return podLogsStore.logsWithoutTimestamps; + return logStore.logsWithoutTimestamps; } return this.props.logs; diff --git a/src/renderer/components/dock/log-resource-selector.tsx b/src/renderer/components/dock/log-resource-selector.tsx index a4b79bcfad..c6f1bee300 100644 --- a/src/renderer/components/dock/log-resource-selector.tsx +++ b/src/renderer/components/dock/log-resource-selector.tsx @@ -1,27 +1,30 @@ import "./log-resource-selector.scss"; -import React from "react"; +import React, { useEffect } from "react"; import { observer } from "mobx-react"; -import { IPodContainer, Pod } from "../../api/endpoints"; +import { Pod } from "../../api/endpoints"; import { Badge } from "../badge"; import { Select, SelectOption } from "../select"; -import { IPodLogsData } from "./log.store"; +import { LogTabData, logTabStore } from "./log-tab.store"; +import { podsStore } from "../+workloads-pods/pods.store"; +import { TabId } from "./dock.store"; interface Props { - tabData: IPodLogsData - save: (data: Partial) => void + tabId: TabId + tabData: LogTabData + save: (data: Partial) => void reload: () => void } export const LogResourceSelector = observer((props: Props) => { - const { tabData, save, reload } = props; - const { selectedContainer, containers, initContainers } = tabData; - const pod = new Pod(tabData.pod); + const { tabData, save, reload, tabId } = props; + const { selectedPod, selectedContainer, pods } = tabData; + const pod = new Pod(selectedPod); + const containers = pod.getContainers(); + const initContainers = pod.getInitContainers(); const onContainerChange = (option: SelectOption) => { - const { containers, initContainers } = tabData; - save({ selectedContainer: containers .concat(initContainers) @@ -30,11 +33,18 @@ export const LogResourceSelector = observer((props: Props) => { reload(); }; - const getSelectOptions = (containers: IPodContainer[]) => { - return containers.map(container => { + const onPodChange = (option: SelectOption) => { + const selectedPod = podsStore.getByName(option.value, pod.getNs()); + + save({ selectedPod }); + logTabStore.renameTab(tabId); + }; + + const getSelectOptions = (items: string[]) => { + return items.map(item => { return { - value: container.name, - label: container.name + value: item, + label: item }; }); }; @@ -42,24 +52,43 @@ export const LogResourceSelector = observer((props: Props) => { const containerSelectOptions = [ { label: `Containers`, - options: getSelectOptions(containers) + options: getSelectOptions(containers.map(container => container.name)) }, { label: `Init Containers`, - options: getSelectOptions(initContainers), + options: getSelectOptions(initContainers.map(container => container.name)), } ]; + const podSelectOptions = [ + { + label: pod.getOwnerRefs()[0]?.name, + options: getSelectOptions(pods.map(pod => pod.metadata.name)) + } + ]; + + useEffect(() => { + reload(); + }, [selectedPod]); + return (
- Namespace - Pod + Namespace + Pod +
); diff --git a/src/renderer/components/dock/log-tab.store.ts b/src/renderer/components/dock/log-tab.store.ts new file mode 100644 index 0000000000..3eec7812be --- /dev/null +++ b/src/renderer/components/dock/log-tab.store.ts @@ -0,0 +1,123 @@ +import uniqueId from "lodash/uniqueId"; +import { reaction } from "mobx"; +import { podsStore } from "../+workloads-pods/pods.store"; + +import { IPodContainer, Pod } from "../../api/endpoints"; +import { WorkloadKubeObject } from "../../api/workload-kube-object"; +import { DockTabStore } from "./dock-tab.store"; +import { dockStore, IDockTab, TabKind } from "./dock.store"; + +export interface LogTabData { + pods: Pod[]; + selectedPod: Pod; + selectedContainer: IPodContainer + showTimestamps?: boolean + previous?: boolean +} + +interface PodLogsTabData { + selectedPod: Pod + selectedContainer: IPodContainer +} + +interface WorkloadLogsTabData { + workload: WorkloadKubeObject +} + +export class LogTabStore extends DockTabStore { + constructor() { + super({ + storageName: "pod_logs" + }); + + reaction(() => podsStore.items.length, () => { + this.updateTabsData(); + }); + } + + createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): void { + const podOwner = selectedPod.getOwnerRefs()[0]; + const pods = podsStore.getPodsByOwnerId(podOwner?.uid); + const title = `Pod ${selectedPod.getName()}`; + + this.createLogsTab(title, { + pods: pods.length ? pods : [selectedPod], + selectedPod, + selectedContainer + }); + } + + createWorkloadTab({ workload }: WorkloadLogsTabData): void { + const pods = podsStore.getPodsByOwnerId(workload.getId()); + + if (!pods.length) return; + + const selectedPod = pods[0]; + const selectedContainer = selectedPod.getAllContainers()[0]; + const title = `${workload.kind} ${selectedPod.getName()}`; + + this.createLogsTab(title, { + pods, + selectedPod, + selectedContainer + }); + } + + renameTab(tabId: string) { + const { selectedPod } = this.getData(tabId); + + dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`); + } + + private createDockTab(tabParams: Partial) { + dockStore.createTab({ + kind: TabKind.POD_LOGS, + ...tabParams + }, false); + } + + private createLogsTab(title: string, data: LogTabData) { + const id = uniqueId("log-tab-"); + + this.createDockTab({ id, title }); + this.setData(id, { + ...data, + showTimestamps: false, + previous: false + }); + } + + private updateTabsData() { + this.data.forEach((tabData, tabId) => { + const pod = new Pod(tabData.selectedPod); + const pods = podsStore.getPodsByOwnerId(pod.getOwnerRefs()[0]?.uid); + const isSelectedPodInList = pods.find(item => item.getId() == pod.getId()); + const selectedPod = isSelectedPodInList ? pod : pods[0]; + const selectedContainer = isSelectedPodInList ? tabData.selectedContainer : pod.getAllContainers()[0]; + + if (pods.length) { + this.setData(tabId, { + ...tabData, + selectedPod, + selectedContainer, + pods + }); + + this.renameTab(tabId); + } else { + this.closeTab(tabId); + } + }); + } + + private closeTab(tabId: string) { + this.clearData(tabId); + dockStore.closeTab(tabId); + } +} + +export const logTabStore = new LogTabStore(); + +export function isLogsTab(tab: IDockTab) { + return tab && tab.kind === TabKind.POD_LOGS; +} diff --git a/src/renderer/components/dock/log.store.ts b/src/renderer/components/dock/log.store.ts index 4dcfccf981..14dc9efdd0 100644 --- a/src/renderer/components/dock/log.store.ts +++ b/src/renderer/components/dock/log.store.ts @@ -1,27 +1,16 @@ -import { autorun, computed, observable, reaction } from "mobx"; -import { Pod, IPodContainer, podsApi, IPodLogsQuery } from "../../api/endpoints"; +import { autorun, computed, observable } from "mobx"; + +import { IPodLogsQuery, Pod, podsApi } from "../../api/endpoints"; import { autobind, interval } from "../../utils"; -import { DockTabStore } from "./dock-tab.store"; -import { dockStore, IDockTab, TabKind } from "./dock.store"; -import { searchStore } from "../../../common/search-store"; +import { dockStore, TabId } from "./dock.store"; +import { isLogsTab, logTabStore } from "./log-tab.store"; -export interface IPodLogsData { - pod: Pod; - selectedContainer: IPodContainer - containers: IPodContainer[] - initContainers: IPodContainer[] - showTimestamps: boolean - previous: boolean -} - -type TabId = string; type PodLogLine = string; -// Number for log lines to load -export const logRange = 500; +const logLinesToLoad = 500; @autobind() -export class LogStore extends DockTabStore { +export class LogStore { private refresher = interval(10, () => { const id = dockStore.selectedTabId; @@ -30,12 +19,8 @@ export class LogStore extends DockTabStore { }); @observable podLogs = observable.map(); - @observable newLogSince = observable.map(); // Timestamp after which all logs are considered to be new constructor() { - super({ - storageName: "pod_logs" - }); autorun(() => { const { selectedTab, isOpen } = dockStore; @@ -45,15 +30,6 @@ export class LogStore extends DockTabStore { this.refresher.stop(); } }, { delay: 500 }); - - reaction(() => this.podLogs.get(dockStore.selectedTabId), () => { - this.setNewLogSince(dockStore.selectedTabId); - }); - - reaction(() => dockStore.selectedTabId, () => { - // Clear search query on tab change - searchStore.reset(); - }); } /** @@ -66,7 +42,7 @@ export class LogStore extends DockTabStore { load = async (tabId: TabId) => { try { const logs = await this.loadLogs(tabId, { - tailLines: this.lines + logRange + tailLines: this.lines + logLinesToLoad }); this.refresher.start(); @@ -107,9 +83,9 @@ export class LogStore extends DockTabStore { * @returns {Promise} A fetch request promise */ loadLogs = async (tabId: TabId, params: Partial) => { - const data = this.getData(tabId); + const data = logTabStore.getData(tabId); const { selectedContainer, previous } = data; - const pod = new Pod(data.pod); + const pod = new Pod(data.selectedPod); const namespace = pod.getNs(); const name = pod.getName(); @@ -127,17 +103,6 @@ export class LogStore extends DockTabStore { }); }; - /** - * Sets newLogSince separator timestamp to split old logs from new ones - * @param tabId - */ - setNewLogSince(tabId: TabId) { - if (!this.podLogs.has(tabId) || !this.podLogs.get(tabId).length || this.newLogSince.has(tabId)) return; - const timestamp = this.getLastSinceTime(tabId); - - this.newLogSince.set(tabId, timestamp.split(".")[0]); // Removing milliseconds from string - } - /** * Converts logs into a string array * @returns {number} Length of log lines @@ -196,37 +161,6 @@ export class LogStore extends DockTabStore { clearLogs(tabId: TabId) { this.podLogs.delete(tabId); } - - clearData(tabId: TabId) { - this.data.delete(tabId); - this.clearLogs(tabId); - } } -export const podLogsStore = new LogStore(); - -export function createPodLogsTab(data: IPodLogsData, tabParams: Partial = {}) { - const podId = data.pod.getId(); - let tab = dockStore.getTabById(podId); - - if (tab) { - dockStore.open(); - dockStore.selectTab(tab.id); - - return; - } - // If no existent tab found - tab = dockStore.createTab({ - id: podId, - kind: TabKind.POD_LOGS, - title: data.pod.getName(), - ...tabParams - }, false); - podLogsStore.setData(tab.id, data); - - return tab; -} - -export function isLogsTab(tab: IDockTab) { - return tab && tab.kind === TabKind.POD_LOGS; -} +export const logStore = new LogStore(); diff --git a/src/renderer/components/dock/logs.tsx b/src/renderer/components/dock/logs.tsx index c8ccd6d249..0aa31f95fb 100644 --- a/src/renderer/components/dock/logs.tsx +++ b/src/renderer/components/dock/logs.tsx @@ -8,9 +8,10 @@ import { IDockTab } from "./dock.store"; import { InfoPanel } from "./info-panel"; import { LogResourceSelector } from "./log-resource-selector"; import { LogList } from "./log-list"; -import { IPodLogsData, podLogsStore } from "./log.store"; +import { logStore } from "./log.store"; import { LogSearch } from "./log-search"; import { LogControls } from "./log-controls"; +import { LogTabData, logTabStore } from "./log-tab.store"; interface Props { className?: string @@ -30,7 +31,7 @@ export class Logs extends React.Component { } get tabData() { - return podLogsStore.getData(this.tabId); + return logTabStore.getData(this.tabId); } get tabId() { @@ -38,18 +39,18 @@ export class Logs extends React.Component { } @autobind() - save(data: Partial) { - podLogsStore.setData(this.tabId, { ...this.tabData, ...data }); + save(data: Partial) { + logTabStore.setData(this.tabId, { ...this.tabData, ...data }); } load = async () => { this.isLoading = true; - await podLogsStore.load(this.tabId); + await logStore.load(this.tabId); this.isLoading = false; }; reload = async () => { - podLogsStore.clearLogs(this.tabId); + logStore.clearLogs(this.tabId); await this.load(); }; @@ -82,11 +83,12 @@ export class Logs extends React.Component { } renderResourceSelector() { - const logs = podLogsStore.logs; - const searchLogs = this.tabData.showTimestamps ? logs : podLogsStore.logsWithoutTimestamps; + const logs = logStore.logs; + const searchLogs = this.tabData.showTimestamps ? logs : logStore.logsWithoutTimestamps; const controls = (
{ } render() { - const logs = podLogsStore.logs; + const logs = logStore.logs; return (
diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index 956f5aa5f6..8a75bc7ae6 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -47,6 +47,10 @@ export abstract class KubeObjectStore extends ItemSt } } + getById(id: string) { + return this.items.find(item => item.getId() === id); + } + getByName(name: string, namespace?: string): T { return this.items.find(item => { return item.getName() === name && ( diff --git a/yarn.lock b/yarn.lock index 5e1cfa376d..dd9ec0c1c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -267,6 +267,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.1", "@babel/template@^7.3.3": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811" @@ -775,6 +782,17 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@kubernetes/client-node@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@kubernetes/client-node/-/client-node-0.12.0.tgz#79120311bced206ac8fa36435fb4cc2c1828fff2" @@ -955,6 +973,20 @@ dependencies: defer-to-connect "^1.0.1" +"@testing-library/dom@>=7": + version "7.29.4" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.29.4.tgz#1647c2b478789621ead7a50614ad81ab5ae5b86c" + integrity sha512-CtrJRiSYEfbtNGtEsd78mk1n1v2TUbeABlNIcOCJdDfkN5/JTOwQEbbQpoSRxGqzcWPgStMvJ4mNolSuBRv1NA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^4.2.2" + chalk "^4.1.0" + dom-accessibility-api "^0.5.4" + lz-string "^1.4.4" + pretty-format "^26.6.2" + "@testing-library/dom@^7.26.0": version "7.26.3" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.26.3.tgz#5554ee985f712d621bd676104b879f85d9a7a0ef" @@ -4552,7 +4584,7 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.1: +dom-accessibility-api@^0.5.1, dom-accessibility-api@^0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166" integrity sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ== @@ -10822,6 +10854,16 @@ pretty-format@^26.0.1: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -11198,6 +11240,13 @@ react-router@5.2.0, react-router@^5.2.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-select-event@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/react-select-event/-/react-select-event-5.1.0.tgz#d45ef68f2a9c872903e8c9725f3ae6e7576f7be0" + integrity sha512-D5DzJlYCdZsGbDVFMQFynrG0OLalJM3ZzDT7KQADNVWE604JCeQF9bIuvPZqVD7IzhnPsFzOUCsilzDA6w6WRQ== + dependencies: + "@testing-library/dom" ">=7" + react-select@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.1.0.tgz#ab098720b2e9fe275047c993f0d0caf5ded17c27" From 227a1497825afa362d543d5393d075efb7d11ae1 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 27 Jan 2021 20:04:50 +0200 Subject: [PATCH 17/56] Release v4.1.0-alpha.1 (#2026) Signed-off-by: Jari Kolehmainen --- package.json | 2 +- static/RELEASE_NOTES.md | 75 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3a06db621b..19689cc6a3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.1.0-alpha.0", + "version": "4.1.0-alpha.1", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index b030c0d8b5..cdcf919304 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,80 @@ 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.0.2 (current version) +## 4.1.0-alpha.1 (current version) + +- Change: list views default to a namespace (insted of listing resources from all namespaces) +- Generic logs view with Pod selector +- Possibility to add custom Helm repository through Lens +- Possibility to change visibility of Pod list columns +- Suspend / resume buttons for CronJobs +- Dock tabs context menu +- Display node column in Pod list +- Unify age column output with kubectl +- Use dark colors in Dock regardless of active theme +- Improve Pod tolerations layout +- Lens metrics: scrape only lens-metrics namespace +- Lens metrics: Prometheus v2.19.3 +- Export PodDetailsList component to extension API +- Export Wizard components to extension API +- Export NamespaceSelect component to extension API + +## 4.0.8 + +- Fix: extension cluster sub-menu/page periodic re-render +- Fix: app hang on boot if started from command line & oh-my-zsh prompts for auto-update + +## 4.0.7 + +- Fix: typo in Prometheus Ingress metrics +- Fix: catch xterm.js fit error +- Fix: Windows tray icon click +- Fix: error on Kubernetes >= 1.20 on object edit +- Fix: multiline log wrapping +- Fix: prevent clusters from initializing multiple times +- Fix: show default workspace on first boot + +## 4.0.6 + +- Don't open Lens at OS login by default +- Disable GPU acceleration by setting an env variable +- Catch HTTP Errors in case pod metrics resources do not exist or access is forbidden +- Check is persistent volume claims resource to allowed for user +- Share react-router and react-router-dom libraries to extensions +- Fix: long list cropping in sidebar +- Fix: k0s distribution detection +- Fix: Preserve line breaks when copying logs +- Fix: error on api watch on complex api versions + +## 4.0.5 + +- Fix: add missing Kubernetes distro detectors +- Fix: improve how Workloads Overview is loaded +- Fix: race conditions on extension loader +- Fix: pod logs scrolling issues +- Fix: render node list before metrics are available +- Fix: kube-state-metrics v1.9.7 +- Fix: CRD sidebar expand/collapse +- Fix: disable oh-my-zsh auto-update prompt when resolving shell environment +- Add kubectl 1.20 support to Lens Smart Terminal +- Optimise performance during cluster connect + +## 4.0.4 + +- Fix errors on Kubernetes v1.20 +- Update bundled kubectl to v1.17.15 +- Fix: MacOS error on shutdown +- Fix: Kubernetes distribution detection +- Fix: error while displaying CRDs with column which type is an object + +## 4.0.3 + +- Fix: install in-tree extensions before others +- Fix: bundle all dependencies in in-tree extensions +- Fix: display error dialog if extensions couldn't be loaded +- Fix: ensure only one app instance + +## 4.0.2 We are aware some users are encountering issues and regressions from previous version. Many of these issues are something we have not seen as part of our automated or manual testing process. To make it worse, some of them are really difficult to reproduce. We want to ensure we are putting all our energy and effort trying to resolve these issues. We hope you are patient. Expect to see new patch releases still in the coming days! Fixes in this version: From e3db77f7ab319bd78993e3f4c6fa675683808c33 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 27 Jan 2021 20:05:12 +0200 Subject: [PATCH 18/56] Better extensionRoutes.keys() iteration (#2031) Signed-off-by: Jari Kolehmainen --- src/renderer/components/app.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 595506fcf9..958ab4b73d 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -174,11 +174,11 @@ export class App extends React.Component { } }); - Array.from(this.extensionRoutes.keys()).forEach((menu) => { + for (const menu of this.extensionRoutes.keys()) { if (!rootItems.includes(menu)) { this.extensionRoutes.delete(menu); } - }); + } } renderExtensionTabLayoutRoutes() { From a102ebad622a06f1070cc4db355989cf81b8eedb Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 27 Jan 2021 20:09:11 +0200 Subject: [PATCH 19/56] Bundle kubectl 1.18.15 (#2028) * bundle kubectl v1.18.15 Signed-off-by: Jari Kolehmainen * bump kubectl version map Signed-off-by: Jari Kolehmainen --- package.json | 2 +- src/main/kubectl.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 19689cc6a3..80d3c1d229 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts" }, "config": { - "bundledKubectlVersion": "1.17.15", + "bundledKubectlVersion": "1.18.15", "bundledHelmVersion": "3.4.2" }, "engines": { diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index ebfd2a6a98..7e0d6ed5c7 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -23,10 +23,10 @@ const kubectlMap: Map = new Map([ ["1.14", "1.14.10"], ["1.15", "1.15.11"], ["1.16", "1.16.15"], - ["1.17", bundledVersion], - ["1.18", "1.18.14"], - ["1.19", "1.19.5"], - ["1.20", "1.20.0"] + ["1.17", "1.17.17"], + ["1.18", bundledVersion], + ["1.19", "1.19.7"], + ["1.20", "1.20.2"] ]); const packageMirrors: Map = new Map([ ["default", "https://storage.googleapis.com/kubernetes-release/release"], From 3640f313b393cb7c4f5fda404713867cc80f94de Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Thu, 28 Jan 2021 12:18:45 +0300 Subject: [PATCH 20/56] Enabling configurable columns for all major tables (#2029) * Configurable columns in Deployments table Signed-off-by: Alex Andreev * Configurable columns in DaemonSets table Signed-off-by: Alex Andreev * Configurable columns in StatefulSets table Signed-off-by: Alex Andreev * Configurable columns in ReplicaSets table Signed-off-by: Alex Andreev * Configurable columns in Jobs table Signed-off-by: Alex Andreev * Configurable columns in CronJobs table Signed-off-by: Alex Andreev * Configurable columns in Nodes table Signed-off-by: Alex Andreev * Configurable columns in ConfigMaps table Signed-off-by: Alex Andreev * Configurable columns in Secrets table Signed-off-by: Alex Andreev * Configurable columns in ResourceQuota table Signed-off-by: Alex Andreev * Configurable columns in LimitRanges table Signed-off-by: Alex Andreev * Configurable columns in HPAs table Signed-off-by: Alex Andreev * Configurable columns in PodDistributionBudget table Signed-off-by: Alex Andreev * Configurable columns in Services table Signed-off-by: Alex Andreev * Configurable columns in Endpoints table Signed-off-by: Alex Andreev * Configurable columns in Ingresses table Signed-off-by: Alex Andreev * Configurable columns in NetworkPolicies table Signed-off-by: Alex Andreev * Configurable columns in Storage section Signed-off-by: Alex Andreev * Configurable columns in Namespaces table Signed-off-by: Alex Andreev * Configurable columns in Events table Signed-off-by: Alex Andreev * Configurable columns in Apps section Signed-off-by: Alex Andreev * Configurable columns in Access Control section Signed-off-by: Alex Andreev * Configurable columns in CRDs tables Signed-off-by: Alex Andreev --- .../+apps-helm-charts/helm-charts.tsx | 27 +++++++----- .../components/+apps-releases/releases.tsx | 34 ++++++++------- .../components/+config-autoscalers/hpa.tsx | 34 ++++++++------- .../+config-limit-ranges/limit-ranges.tsx | 18 ++++---- .../components/+config-maps/config-maps.tsx | 22 +++++----- .../pod-disruption-budgets.tsx | 34 ++++++++------- .../resource-quotas.tsx | 18 ++++---- .../components/+config-secrets/secrets.tsx | 30 ++++++------- .../components/+custom-resources/crd-list.tsx | 22 +++++----- .../+custom-resources/crd-resources.tsx | 19 +++++---- src/renderer/components/+events/events.tsx | 30 +++++++------ .../components/+namespaces/namespaces.tsx | 22 +++++----- .../+network-endpoints/endpoints.tsx | 21 ++++++---- .../+network-ingresses/ingresses.tsx | 24 ++++++----- .../+network-policies/network-policies.tsx | 21 ++++++---- .../components/+network-services/services.tsx | 41 +++++++++--------- src/renderer/components/+nodes/nodes.tsx | 42 ++++++++++--------- .../pod-security-policies.tsx | 22 +++++----- .../+storage-classes/storage-classes.tsx | 25 ++++++----- .../+storage-volume-claims/volume-claims.tsx | 34 ++++++++------- .../components/+storage-volumes/volumes.tsx | 29 +++++++------ .../role-bindings.tsx | 22 +++++----- .../+user-management-roles/roles.tsx | 18 ++++---- .../service-accounts.tsx | 18 ++++---- .../+workloads-cronjobs/cronjobs.tsx | 35 +++++++++------- .../+workloads-daemonsets/daemonsets.tsx | 25 ++++++----- .../+workloads-deployments/deployments.tsx | 29 +++++++------ .../components/+workloads-jobs/jobs.tsx | 25 ++++++----- .../+workloads-replicasets/replicasets.tsx | 30 ++++++------- .../+workloads-statefulsets/statefulsets.tsx | 25 ++++++----- 30 files changed, 439 insertions(+), 357 deletions(-) diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.tsx b/src/renderer/components/+apps-helm-charts/helm-charts.tsx index 8bf5486a55..348ce00969 100644 --- a/src/renderer/components/+apps-helm-charts/helm-charts.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-charts.tsx @@ -11,8 +11,11 @@ import { navigation } from "../../navigation"; import { ItemListLayout } from "../item-object-list/item-list-layout"; import { SearchInputUrl } from "../input"; -enum sortBy { +enum columnId { name = "name", + description = "description", + version = "version", + appVersion = "app-version", repo = "repo", } @@ -53,13 +56,15 @@ export class HelmCharts extends Component { return ( <> chart.getName(), - [sortBy.repo]: (chart: HelmChart) => chart.getRepository(), + [columnId.name]: (chart: HelmChart) => chart.getName(), + [columnId.repo]: (chart: HelmChart) => chart.getRepository(), }} searchFilters={[ (chart: HelmChart) => chart.getName(), @@ -74,13 +79,12 @@ export class HelmCharts extends Component { )} renderTableHeader={[ - { className: "icon" }, - { title: "Name", className: "name", sortBy: sortBy.name }, - { title: "Description", className: "description" }, - { title: "Version", className: "version" }, - { title: "App Version", className: "app-version" }, - { title: "Repository", className: "repository", sortBy: sortBy.repo }, - + { className: "icon", showWithColumn: columnId.name }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { title: "Description", className: "description", id: columnId.description }, + { title: "Version", className: "version", id: columnId.version }, + { title: "App Version", className: "app-version", id: columnId.appVersion }, + { title: "Repository", className: "repository", sortBy: columnId.repo, id: columnId.repo }, ]} renderTableContents={(chart: HelmChart) => [
@@ -93,7 +97,8 @@ export class HelmCharts extends Component { chart.getDescription(), chart.getVersion(), chart.getAppVersion(), - { title: chart.getRepository(), className: chart.getRepository().toLowerCase() } + { title: chart.getRepository(), className: chart.getRepository().toLowerCase() }, + { className: "menu" } ]} detailsItem={this.selectedChart} onDetails={this.showDetails} diff --git a/src/renderer/components/+apps-releases/releases.tsx b/src/renderer/components/+apps-releases/releases.tsx index 709c6f9bbd..71cf3d954f 100644 --- a/src/renderer/components/+apps-releases/releases.tsx +++ b/src/renderer/components/+apps-releases/releases.tsx @@ -14,11 +14,13 @@ import { ItemListLayout } from "../item-object-list/item-list-layout"; import { HelmReleaseMenu } from "./release-menu"; import { secretsStore } from "../+config-secrets/secrets.store"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", revision = "revision", chart = "chart", + version = "version", + appVersion = "app-version", status = "status", updated = "update" } @@ -81,16 +83,18 @@ export class HelmReleases extends Component { return ( <> release.getName(), - [sortBy.namespace]: (release: HelmRelease) => release.getNs(), - [sortBy.revision]: (release: HelmRelease) => release.getRevision(), - [sortBy.chart]: (release: HelmRelease) => release.getChart(), - [sortBy.status]: (release: HelmRelease) => release.getStatus(), - [sortBy.updated]: (release: HelmRelease) => release.getUpdated(false, false), + [columnId.name]: (release: HelmRelease) => release.getName(), + [columnId.namespace]: (release: HelmRelease) => release.getNs(), + [columnId.revision]: (release: HelmRelease) => release.getRevision(), + [columnId.chart]: (release: HelmRelease) => release.getChart(), + [columnId.status]: (release: HelmRelease) => release.getStatus(), + [columnId.updated]: (release: HelmRelease) => release.getUpdated(false, false), }} searchFilters={[ (release: HelmRelease) => release.getName(), @@ -101,14 +105,14 @@ export class HelmReleases extends Component { ]} renderHeaderTitle="Releases" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Chart", className: "chart", sortBy: sortBy.chart }, - { title: "Revision", className: "revision", sortBy: sortBy.revision }, - { title: "Version", className: "version" }, - { title: "App Version", className: "app-version" }, - { title: "Status", className: "status", sortBy: sortBy.status }, - { title: "Updated", className: "updated", sortBy: sortBy.updated }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Chart", className: "chart", sortBy: columnId.chart, id: columnId.chart }, + { title: "Revision", className: "revision", sortBy: columnId.revision, id: columnId.revision }, + { title: "Version", className: "version", id: columnId.version }, + { title: "App Version", className: "app-version", id: columnId.appVersion }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, + { title: "Updated", className: "updated", sortBy: columnId.updated, id: columnId.updated }, ]} renderTableContents={(release: HelmRelease) => { const version = release.getVersion(); diff --git a/src/renderer/components/+config-autoscalers/hpa.tsx b/src/renderer/components/+config-autoscalers/hpa.tsx index 023a28f156..2e2a78fc82 100644 --- a/src/renderer/components/+config-autoscalers/hpa.tsx +++ b/src/renderer/components/+config-autoscalers/hpa.tsx @@ -11,13 +11,15 @@ import { Badge } from "../badge"; import { cssNames } from "../../utils"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + metrics = "metrics", minPods = "min-pods", maxPods = "max-pods", replicas = "replicas", age = "age", + status = "status" } interface Props extends RouteComponentProps { @@ -37,28 +39,30 @@ export class HorizontalPodAutoscalers extends React.Component { render() { return ( item.getName(), - [sortBy.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(), - [sortBy.minPods]: (item: HorizontalPodAutoscaler) => item.getMinPods(), - [sortBy.maxPods]: (item: HorizontalPodAutoscaler) => item.getMaxPods(), - [sortBy.replicas]: (item: HorizontalPodAutoscaler) => item.getReplicas() + [columnId.name]: (item: HorizontalPodAutoscaler) => item.getName(), + [columnId.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(), + [columnId.minPods]: (item: HorizontalPodAutoscaler) => item.getMinPods(), + [columnId.maxPods]: (item: HorizontalPodAutoscaler) => item.getMaxPods(), + [columnId.replicas]: (item: HorizontalPodAutoscaler) => item.getReplicas() }} searchFilters={[ (item: HorizontalPodAutoscaler) => item.getSearchFields() ]} renderHeaderTitle="Horizontal Pod Autoscalers" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Metrics", className: "metrics" }, - { title: "Min Pods", className: "min-pods", sortBy: sortBy.minPods }, - { title: "Max Pods", className: "max-pods", sortBy: sortBy.maxPods }, - { title: "Replicas", className: "replicas", sortBy: sortBy.replicas }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Status", className: "status" }, + { title: "Name", className: "name", sortBy: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Metrics", className: "metrics", id: columnId.metrics }, + { title: "Min Pods", className: "min-pods", sortBy: columnId.minPods, id: columnId.minPods }, + { title: "Max Pods", className: "max-pods", sortBy: columnId.maxPods, id: columnId.maxPods }, + { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", id: columnId.status }, ]} renderTableContents={(hpa: HorizontalPodAutoscaler) => [ hpa.getName(), diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.tsx b/src/renderer/components/+config-limit-ranges/limit-ranges.tsx index 8bb498c1c0..a3b111929a 100644 --- a/src/renderer/components/+config-limit-ranges/limit-ranges.tsx +++ b/src/renderer/components/+config-limit-ranges/limit-ranges.tsx @@ -9,7 +9,7 @@ import React from "react"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { LimitRange } from "../../api/endpoints/limit-range.api"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", age = "age", @@ -23,12 +23,14 @@ export class LimitRanges extends React.Component { render() { return ( item.getName(), - [sortBy.namespace]: (item: LimitRange) => item.getNs(), - [sortBy.age]: (item: LimitRange) => item.metadata.creationTimestamp, + [columnId.name]: (item: LimitRange) => item.getName(), + [columnId.namespace]: (item: LimitRange) => item.getNs(), + [columnId.age]: (item: LimitRange) => item.metadata.creationTimestamp, }} searchFilters={[ (item: LimitRange) => item.getName(), @@ -36,10 +38,10 @@ export class LimitRanges extends React.Component { ]} renderHeaderTitle={"Limit Ranges"} renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(limitRange: LimitRange) => [ limitRange.getName(), diff --git a/src/renderer/components/+config-maps/config-maps.tsx b/src/renderer/components/+config-maps/config-maps.tsx index 128c583fc8..532532bf53 100644 --- a/src/renderer/components/+config-maps/config-maps.tsx +++ b/src/renderer/components/+config-maps/config-maps.tsx @@ -9,7 +9,7 @@ import { KubeObjectListLayout } from "../kube-object"; import { IConfigMapsRouteParams } from "./config-maps.route"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", keys = "keys", @@ -24,12 +24,14 @@ export class ConfigMaps extends React.Component { render() { return ( item.getName(), - [sortBy.namespace]: (item: ConfigMap) => item.getNs(), - [sortBy.keys]: (item: ConfigMap) => item.getKeys(), - [sortBy.age]: (item: ConfigMap) => item.metadata.creationTimestamp, + [columnId.name]: (item: ConfigMap) => item.getName(), + [columnId.namespace]: (item: ConfigMap) => item.getNs(), + [columnId.keys]: (item: ConfigMap) => item.getKeys(), + [columnId.age]: (item: ConfigMap) => item.metadata.creationTimestamp, }} searchFilters={[ (item: ConfigMap) => item.getSearchFields(), @@ -37,11 +39,11 @@ export class ConfigMaps extends React.Component { ]} renderHeaderTitle="Config Maps" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Keys", className: "keys", sortBy: sortBy.keys }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Keys", className: "keys", sortBy: columnId.keys, id: columnId.keys }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(configMap: ConfigMap) => [ configMap.getName(), diff --git a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx index f0754e0be8..8136225f11 100644 --- a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx +++ b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx @@ -7,7 +7,7 @@ import { PodDisruptionBudget } from "../../api/endpoints/poddisruptionbudget.api import { KubeObjectDetailsProps, KubeObjectListLayout } from "../kube-object"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", minAvailable = "min-available", @@ -25,30 +25,32 @@ export class PodDisruptionBudgets extends React.Component { render() { return ( pdb.getName(), - [sortBy.namespace]: (pdb: PodDisruptionBudget) => pdb.getNs(), - [sortBy.minAvailable]: (pdb: PodDisruptionBudget) => pdb.getMinAvailable(), - [sortBy.maxUnavailable]: (pdb: PodDisruptionBudget) => pdb.getMaxUnavailable(), - [sortBy.currentHealthy]: (pdb: PodDisruptionBudget) => pdb.getCurrentHealthy(), - [sortBy.desiredHealthy]: (pdb: PodDisruptionBudget) => pdb.getDesiredHealthy(), - [sortBy.age]: (pdb: PodDisruptionBudget) => pdb.getAge(), + [columnId.name]: (pdb: PodDisruptionBudget) => pdb.getName(), + [columnId.namespace]: (pdb: PodDisruptionBudget) => pdb.getNs(), + [columnId.minAvailable]: (pdb: PodDisruptionBudget) => pdb.getMinAvailable(), + [columnId.maxUnavailable]: (pdb: PodDisruptionBudget) => pdb.getMaxUnavailable(), + [columnId.currentHealthy]: (pdb: PodDisruptionBudget) => pdb.getCurrentHealthy(), + [columnId.desiredHealthy]: (pdb: PodDisruptionBudget) => pdb.getDesiredHealthy(), + [columnId.age]: (pdb: PodDisruptionBudget) => pdb.getAge(), }} searchFilters={[ (pdb: PodDisruptionBudget) => pdb.getSearchFields(), ]} renderHeaderTitle="Pod Disruption Budgets" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Min Available", className: "min-available", sortBy: sortBy.minAvailable }, - { title: "Max Unavailable", className: "max-unavailable", sortBy: sortBy.maxUnavailable }, - { title: "Current Healthy", className: "current-healthy", sortBy: sortBy.currentHealthy }, - { title: "Desired Healthy", className: "desired-healthy", sortBy: sortBy.desiredHealthy }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Min Available", className: "min-available", sortBy: columnId.minAvailable, id: columnId.minAvailable }, + { title: "Max Unavailable", className: "max-unavailable", sortBy: columnId.maxUnavailable, id: columnId.maxUnavailable }, + { title: "Current Healthy", className: "current-healthy", sortBy: columnId.currentHealthy, id: columnId.currentHealthy }, + { title: "Desired Healthy", className: "desired-healthy", sortBy: columnId.desiredHealthy, id: columnId.desiredHealthy }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(pdb: PodDisruptionBudget) => { return [ diff --git a/src/renderer/components/+config-resource-quotas/resource-quotas.tsx b/src/renderer/components/+config-resource-quotas/resource-quotas.tsx index 1aed2c9d24..5adfef2edc 100644 --- a/src/renderer/components/+config-resource-quotas/resource-quotas.tsx +++ b/src/renderer/components/+config-resource-quotas/resource-quotas.tsx @@ -10,7 +10,7 @@ import { resourceQuotaStore } from "./resource-quotas.store"; import { IResourceQuotaRouteParams } from "./resource-quotas.route"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", age = "age" @@ -25,11 +25,13 @@ export class ResourceQuotas extends React.Component { return ( <> item.getName(), - [sortBy.namespace]: (item: ResourceQuota) => item.getNs(), - [sortBy.age]: (item: ResourceQuota) => item.metadata.creationTimestamp, + [columnId.name]: (item: ResourceQuota) => item.getName(), + [columnId.namespace]: (item: ResourceQuota) => item.getNs(), + [columnId.age]: (item: ResourceQuota) => item.metadata.creationTimestamp, }} searchFilters={[ (item: ResourceQuota) => item.getSearchFields(), @@ -37,10 +39,10 @@ export class ResourceQuotas extends React.Component { ]} renderHeaderTitle="Resource Quotas" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(resourceQuota: ResourceQuota) => [ resourceQuota.getName(), diff --git a/src/renderer/components/+config-secrets/secrets.tsx b/src/renderer/components/+config-secrets/secrets.tsx index f2c88fda58..60158cfb55 100644 --- a/src/renderer/components/+config-secrets/secrets.tsx +++ b/src/renderer/components/+config-secrets/secrets.tsx @@ -11,7 +11,7 @@ import { Badge } from "../badge"; import { secretsStore } from "./secrets.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", labels = "labels", @@ -29,14 +29,16 @@ export class Secrets extends React.Component { return ( <> item.getName(), - [sortBy.namespace]: (item: Secret) => item.getNs(), - [sortBy.labels]: (item: Secret) => item.getLabels(), - [sortBy.keys]: (item: Secret) => item.getKeys(), - [sortBy.type]: (item: Secret) => item.type, - [sortBy.age]: (item: Secret) => item.metadata.creationTimestamp, + [columnId.name]: (item: Secret) => item.getName(), + [columnId.namespace]: (item: Secret) => item.getNs(), + [columnId.labels]: (item: Secret) => item.getLabels(), + [columnId.keys]: (item: Secret) => item.getKeys(), + [columnId.type]: (item: Secret) => item.type, + [columnId.age]: (item: Secret) => item.metadata.creationTimestamp, }} searchFilters={[ (item: Secret) => item.getSearchFields(), @@ -44,13 +46,13 @@ export class Secrets extends React.Component { ]} renderHeaderTitle="Secrets" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Labels", className: "labels", sortBy: sortBy.labels }, - { title: "Keys", className: "keys", sortBy: sortBy.keys }, - { title: "Type", className: "type", sortBy: sortBy.type }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Labels", className: "labels", sortBy: columnId.labels, id: columnId.labels }, + { title: "Keys", className: "keys", sortBy: columnId.keys, id: columnId.keys }, + { title: "Type", className: "type", sortBy: columnId.type, id: columnId.type }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(secret: Secret) => [ secret.getName(), diff --git a/src/renderer/components/+custom-resources/crd-list.tsx b/src/renderer/components/+custom-resources/crd-list.tsx index 8868231235..f8b77c09a9 100644 --- a/src/renderer/components/+custom-resources/crd-list.tsx +++ b/src/renderer/components/+custom-resources/crd-list.tsx @@ -19,7 +19,7 @@ export const crdGroupsUrlParam = createPageParam({ defaultValue: [], }); -enum sortBy { +enum columnId { kind = "kind", group = "group", version = "version", @@ -47,14 +47,16 @@ export class CrdList extends React.Component { render() { const selectedGroups = this.groups; const sortingCallbacks = { - [sortBy.kind]: (crd: CustomResourceDefinition) => crd.getResourceKind(), - [sortBy.group]: (crd: CustomResourceDefinition) => crd.getGroup(), - [sortBy.version]: (crd: CustomResourceDefinition) => crd.getVersion(), - [sortBy.scope]: (crd: CustomResourceDefinition) => crd.getScope(), + [columnId.kind]: (crd: CustomResourceDefinition) => crd.getResourceKind(), + [columnId.group]: (crd: CustomResourceDefinition) => crd.getGroup(), + [columnId.version]: (crd: CustomResourceDefinition) => crd.getVersion(), + [columnId.scope]: (crd: CustomResourceDefinition) => crd.getScope(), }; return ( [ diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index e6a7f2aac6..b9008b410d 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -16,7 +16,7 @@ import { parseJsonPath } from "../../utils/jsonPath"; interface Props extends RouteComponentProps { } -enum sortBy { +enum columnId { name = "name", namespace = "namespace", age = "age", @@ -55,9 +55,9 @@ export class CrdResources extends React.Component { const isNamespaced = crd.isNamespaced(); const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details const sortingCallbacks: { [sortBy: string]: TableSortCallback } = { - [sortBy.name]: (item: KubeObject) => item.getName(), - [sortBy.namespace]: (item: KubeObject) => item.getNs(), - [sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp, + [columnId.name]: (item: KubeObject) => item.getName(), + [columnId.namespace]: (item: KubeObject) => item.getNs(), + [columnId.age]: (item: KubeObject) => item.metadata.creationTimestamp, }; extraColumns.forEach(column => { @@ -66,6 +66,8 @@ export class CrdResources extends React.Component { return ( { ]} renderHeaderTitle={crd.getResourceTitle()} renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - isNamespaced && { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, ...extraColumns.map(column => { const { name } = column; return { title: name, className: name.toLowerCase(), - sortBy: name + sortBy: name, + id: name }; }), - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(crdInstance: KubeObject) => [ crdInstance.getName(), diff --git a/src/renderer/components/+events/events.tsx b/src/renderer/components/+events/events.tsx index c4e6920bc8..8e2af5d9a8 100644 --- a/src/renderer/components/+events/events.tsx +++ b/src/renderer/components/+events/events.tsx @@ -12,11 +12,13 @@ import { cssNames, IClassName, stopPropagation } from "../../utils"; import { Icon } from "../icon"; import { lookupApiLink } from "../../api/kube-api"; -enum sortBy { +enum columnId { + message = "message", namespace = "namespace", object = "object", type = "type", count = "count", + source = "source", age = "age", } @@ -39,15 +41,17 @@ export class Events extends React.Component { const events = ( event.getNs(), - [sortBy.type]: (event: KubeEvent) => event.involvedObject.kind, - [sortBy.object]: (event: KubeEvent) => event.involvedObject.name, - [sortBy.count]: (event: KubeEvent) => event.count, - [sortBy.age]: (event: KubeEvent) => event.metadata.creationTimestamp, + [columnId.namespace]: (event: KubeEvent) => event.getNs(), + [columnId.type]: (event: KubeEvent) => event.involvedObject.kind, + [columnId.object]: (event: KubeEvent) => event.involvedObject.name, + [columnId.count]: (event: KubeEvent) => event.count, + [columnId.age]: (event: KubeEvent) => event.metadata.creationTimestamp, }} searchFilters={[ (event: KubeEvent) => event.getSearchFields(), @@ -72,13 +76,13 @@ export class Events extends React.Component { }) )} renderTableHeader={[ - { title: "Message", className: "message" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Type", className: "type", sortBy: sortBy.type }, - { title: "Involved Object", className: "object", sortBy: sortBy.object }, - { title: "Source", className: "source" }, - { title: "Count", className: "count", sortBy: sortBy.count }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Message", className: "message", id: columnId.message }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Type", className: "type", sortBy: columnId.type, id: columnId.type }, + { title: "Involved Object", className: "object", sortBy: columnId.object, id: columnId.object }, + { title: "Source", className: "source", id: columnId.source }, + { title: "Count", className: "count", sortBy: columnId.count, id: columnId.count }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(event: KubeEvent) => { const { involvedObject, type, message } = event; diff --git a/src/renderer/components/+namespaces/namespaces.tsx b/src/renderer/components/+namespaces/namespaces.tsx index f097657493..3972f3d180 100644 --- a/src/renderer/components/+namespaces/namespaces.tsx +++ b/src/renderer/components/+namespaces/namespaces.tsx @@ -11,7 +11,7 @@ import { INamespacesRouteParams } from "./namespaces.route"; import { namespaceStore } from "./namespace.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", labels = "labels", age = "age", @@ -27,12 +27,14 @@ export class Namespaces extends React.Component { ns.getName(), - [sortBy.labels]: (ns: Namespace) => ns.getLabels(), - [sortBy.age]: (ns: Namespace) => ns.metadata.creationTimestamp, - [sortBy.status]: (ns: Namespace) => ns.getStatus(), + [columnId.name]: (ns: Namespace) => ns.getName(), + [columnId.labels]: (ns: Namespace) => ns.getLabels(), + [columnId.age]: (ns: Namespace) => ns.metadata.creationTimestamp, + [columnId.status]: (ns: Namespace) => ns.getStatus(), }} searchFilters={[ (item: Namespace) => item.getSearchFields(), @@ -40,11 +42,11 @@ export class Namespaces extends React.Component { ]} renderHeaderTitle="Namespaces" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Labels", className: "labels", sortBy: sortBy.labels }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Status", className: "status", sortBy: sortBy.status }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Labels", className: "labels", sortBy: columnId.labels, id: columnId.labels }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, ]} renderTableContents={(item: Namespace) => [ item.getName(), diff --git a/src/renderer/components/+network-endpoints/endpoints.tsx b/src/renderer/components/+network-endpoints/endpoints.tsx index 3b859c46f3..ce87c14a4a 100644 --- a/src/renderer/components/+network-endpoints/endpoints.tsx +++ b/src/renderer/components/+network-endpoints/endpoints.tsx @@ -9,9 +9,10 @@ import { endpointStore } from "./endpoints.store"; import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + endpoints = "endpoints", age = "age", } @@ -23,22 +24,24 @@ export class Endpoints extends React.Component { render() { return ( endpoint.getName(), - [sortBy.namespace]: (endpoint: Endpoint) => endpoint.getNs(), - [sortBy.age]: (endpoint: Endpoint) => endpoint.metadata.creationTimestamp, + [columnId.name]: (endpoint: Endpoint) => endpoint.getName(), + [columnId.namespace]: (endpoint: Endpoint) => endpoint.getNs(), + [columnId.age]: (endpoint: Endpoint) => endpoint.metadata.creationTimestamp, }} searchFilters={[ (endpoint: Endpoint) => endpoint.getSearchFields() ]} renderHeaderTitle="Endpoints" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Endpoints", className: "endpoints" }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Endpoints", className: "endpoints", id: columnId.endpoints }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(endpoint: Endpoint) => [ endpoint.getName(), diff --git a/src/renderer/components/+network-ingresses/ingresses.tsx b/src/renderer/components/+network-ingresses/ingresses.tsx index adb6c84528..945f2b8f0a 100644 --- a/src/renderer/components/+network-ingresses/ingresses.tsx +++ b/src/renderer/components/+network-ingresses/ingresses.tsx @@ -9,9 +9,11 @@ import { ingressStore } from "./ingress.store"; import { KubeObjectListLayout } from "../kube-object"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + loadBalancers ="load-balancers", + rules = "rules", age = "age", } @@ -23,11 +25,13 @@ export class Ingresses extends React.Component { render() { return ( ingress.getName(), - [sortBy.namespace]: (ingress: Ingress) => ingress.getNs(), - [sortBy.age]: (ingress: Ingress) => ingress.metadata.creationTimestamp, + [columnId.name]: (ingress: Ingress) => ingress.getName(), + [columnId.namespace]: (ingress: Ingress) => ingress.getNs(), + [columnId.age]: (ingress: Ingress) => ingress.metadata.creationTimestamp, }} searchFilters={[ (ingress: Ingress) => ingress.getSearchFields(), @@ -35,12 +39,12 @@ export class Ingresses extends React.Component { ]} renderHeaderTitle="Ingresses" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "LoadBalancers", className: "loadbalancers" }, - { title: "Rules", className: "rules" }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "LoadBalancers", className: "loadbalancers", id: columnId.loadBalancers }, + { title: "Rules", className: "rules", id: columnId.rules }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(ingress: Ingress) => [ ingress.getName(), diff --git a/src/renderer/components/+network-policies/network-policies.tsx b/src/renderer/components/+network-policies/network-policies.tsx index d4dc0e2fa9..6899c14558 100644 --- a/src/renderer/components/+network-policies/network-policies.tsx +++ b/src/renderer/components/+network-policies/network-policies.tsx @@ -9,9 +9,10 @@ import { INetworkPoliciesRouteParams } from "./network-policies.route"; import { networkPolicyStore } from "./network-policy.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + types = "types", age = "age", } @@ -23,22 +24,24 @@ export class NetworkPolicies extends React.Component { render() { return ( item.getName(), - [sortBy.namespace]: (item: NetworkPolicy) => item.getNs(), - [sortBy.age]: (item: NetworkPolicy) => item.metadata.creationTimestamp, + [columnId.name]: (item: NetworkPolicy) => item.getName(), + [columnId.namespace]: (item: NetworkPolicy) => item.getNs(), + [columnId.age]: (item: NetworkPolicy) => item.metadata.creationTimestamp, }} searchFilters={[ (item: NetworkPolicy) => item.getSearchFields(), ]} renderHeaderTitle="Network Policies" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Policy Types", className: "type" }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Policy Types", className: "type", id: columnId.types }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(item: NetworkPolicy) => [ item.getName(), diff --git a/src/renderer/components/+network-services/services.tsx b/src/renderer/components/+network-services/services.tsx index 3452c10a68..740e0bfdf1 100644 --- a/src/renderer/components/+network-services/services.tsx +++ b/src/renderer/components/+network-services/services.tsx @@ -10,12 +10,13 @@ import { Badge } from "../badge"; import { serviceStore } from "./services.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", selector = "selector", ports = "port", clusterIp = "cluster-ip", + externalIp = "external-ip", age = "age", type = "type", status = "status", @@ -29,16 +30,18 @@ export class Services extends React.Component { render() { return ( service.getName(), - [sortBy.namespace]: (service: Service) => service.getNs(), - [sortBy.selector]: (service: Service) => service.getSelector(), - [sortBy.ports]: (service: Service) => (service.spec.ports || []).map(({ port }) => port)[0], - [sortBy.clusterIp]: (service: Service) => service.getClusterIp(), - [sortBy.type]: (service: Service) => service.getType(), - [sortBy.age]: (service: Service) => service.metadata.creationTimestamp, - [sortBy.status]: (service: Service) => service.getStatus(), + [columnId.name]: (service: Service) => service.getName(), + [columnId.namespace]: (service: Service) => service.getNs(), + [columnId.selector]: (service: Service) => service.getSelector(), + [columnId.ports]: (service: Service) => (service.spec.ports || []).map(({ port }) => port)[0], + [columnId.clusterIp]: (service: Service) => service.getClusterIp(), + [columnId.type]: (service: Service) => service.getType(), + [columnId.age]: (service: Service) => service.metadata.creationTimestamp, + [columnId.status]: (service: Service) => service.getStatus(), }} searchFilters={[ (service: Service) => service.getSearchFields(), @@ -47,16 +50,16 @@ export class Services extends React.Component { ]} renderHeaderTitle="Services" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Type", className: "type", sortBy: sortBy.type }, - { title: "Cluster IP", className: "clusterIp", sortBy: sortBy.clusterIp, }, - { title: "Ports", className: "ports", sortBy: sortBy.ports }, - { title: "External IP", className: "externalIp" }, - { title: "Selector", className: "selector", sortBy: sortBy.selector }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Status", className: "status", sortBy: sortBy.status }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Type", className: "type", sortBy: columnId.type, id: columnId.type }, + { title: "Cluster IP", className: "clusterIp", sortBy: columnId.clusterIp, id: columnId.clusterIp }, + { title: "Ports", className: "ports", sortBy: columnId.ports, id: columnId.ports }, + { title: "External IP", className: "externalIp", id: columnId.externalIp }, + { title: "Selector", className: "selector", sortBy: columnId.selector, id: columnId.selector }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, ]} renderTableContents={(service: Service) => [ service.getName(), diff --git a/src/renderer/components/+nodes/nodes.tsx b/src/renderer/components/+nodes/nodes.tsx index 1ca12343b5..32da6a13db 100644 --- a/src/renderer/components/+nodes/nodes.tsx +++ b/src/renderer/components/+nodes/nodes.tsx @@ -17,7 +17,7 @@ import upperFirst from "lodash/upperFirst"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { Badge } from "../badge/badge"; -enum sortBy { +enum columnId { name = "name", cpu = "cpu", memory = "memory", @@ -135,21 +135,23 @@ export class Nodes extends React.Component { return ( node.getName(), - [sortBy.cpu]: (node: Node) => nodesStore.getLastMetricValues(node, ["cpuUsage"]), - [sortBy.memory]: (node: Node) => nodesStore.getLastMetricValues(node, ["memoryUsage"]), - [sortBy.disk]: (node: Node) => nodesStore.getLastMetricValues(node, ["fsUsage"]), - [sortBy.conditions]: (node: Node) => node.getNodeConditionText(), - [sortBy.taints]: (node: Node) => node.getTaints().length, - [sortBy.roles]: (node: Node) => node.getRoleLabels(), - [sortBy.age]: (node: Node) => node.metadata.creationTimestamp, - [sortBy.version]: (node: Node) => node.getKubeletVersion(), + [columnId.name]: (node: Node) => node.getName(), + [columnId.cpu]: (node: Node) => nodesStore.getLastMetricValues(node, ["cpuUsage"]), + [columnId.memory]: (node: Node) => nodesStore.getLastMetricValues(node, ["memoryUsage"]), + [columnId.disk]: (node: Node) => nodesStore.getLastMetricValues(node, ["fsUsage"]), + [columnId.conditions]: (node: Node) => node.getNodeConditionText(), + [columnId.taints]: (node: Node) => node.getTaints().length, + [columnId.roles]: (node: Node) => node.getRoleLabels(), + [columnId.age]: (node: Node) => node.metadata.creationTimestamp, + [columnId.version]: (node: Node) => node.getKubeletVersion(), }} searchFilters={[ (node: Node) => node.getSearchFields(), @@ -159,16 +161,16 @@ export class Nodes extends React.Component { ]} renderHeaderTitle="Nodes" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "CPU", className: "cpu", sortBy: sortBy.cpu }, - { title: "Memory", className: "memory", sortBy: sortBy.memory }, - { title: "Disk", className: "disk", sortBy: sortBy.disk }, - { title: "Taints", className: "taints", sortBy: sortBy.taints }, - { title: "Roles", className: "roles", sortBy: sortBy.roles }, - { title: "Version", className: "version", sortBy: sortBy.version }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Conditions", className: "conditions", sortBy: sortBy.conditions }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "CPU", className: "cpu", sortBy: columnId.cpu, id: columnId.cpu }, + { title: "Memory", className: "memory", sortBy: columnId.memory, id: columnId.memory }, + { title: "Disk", className: "disk", sortBy: columnId.disk, id: columnId.disk }, + { title: "Taints", className: "taints", sortBy: columnId.taints, id: columnId.taints }, + { title: "Roles", className: "roles", sortBy: columnId.roles, id: columnId.roles }, + { title: "Version", className: "version", sortBy: columnId.version, id: columnId.version }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Conditions", className: "conditions", sortBy: columnId.conditions, id: columnId.conditions }, ]} renderTableContents={(node: Node) => { const tooltipId = `node-taints-${node.getId()}`; diff --git a/src/renderer/components/+pod-security-policies/pod-security-policies.tsx b/src/renderer/components/+pod-security-policies/pod-security-policies.tsx index 30ec1d6304..a91e0114d6 100644 --- a/src/renderer/components/+pod-security-policies/pod-security-policies.tsx +++ b/src/renderer/components/+pod-security-policies/pod-security-policies.tsx @@ -7,7 +7,7 @@ import { podSecurityPoliciesStore } from "./pod-security-policies.store"; import { PodSecurityPolicy } from "../../api/endpoints"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", volumes = "volumes", privileged = "privileged", @@ -19,14 +19,16 @@ export class PodSecurityPolicies extends React.Component { render() { return ( item.getName(), - [sortBy.volumes]: (item: PodSecurityPolicy) => item.getVolumes(), - [sortBy.privileged]: (item: PodSecurityPolicy) => +item.isPrivileged(), - [sortBy.age]: (item: PodSecurityPolicy) => item.metadata.creationTimestamp, + [columnId.name]: (item: PodSecurityPolicy) => item.getName(), + [columnId.volumes]: (item: PodSecurityPolicy) => item.getVolumes(), + [columnId.privileged]: (item: PodSecurityPolicy) => +item.isPrivileged(), + [columnId.age]: (item: PodSecurityPolicy) => item.metadata.creationTimestamp, }} searchFilters={[ (item: PodSecurityPolicy) => item.getSearchFields(), @@ -35,11 +37,11 @@ export class PodSecurityPolicies extends React.Component { ]} renderHeaderTitle="Pod Security Policies" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Privileged", className: "privileged", sortBy: sortBy.privileged }, - { title: "Volumes", className: "volumes", sortBy: sortBy.volumes }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Privileged", className: "privileged", sortBy: columnId.privileged, id: columnId.privileged }, + { title: "Volumes", className: "volumes", sortBy: columnId.volumes, id: columnId.volumes }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(item: PodSecurityPolicy) => { return [ diff --git a/src/renderer/components/+storage-classes/storage-classes.tsx b/src/renderer/components/+storage-classes/storage-classes.tsx index ec7e1c8e05..1a8ed346fd 100644 --- a/src/renderer/components/+storage-classes/storage-classes.tsx +++ b/src/renderer/components/+storage-classes/storage-classes.tsx @@ -9,10 +9,11 @@ import { IStorageClassesRouteParams } from "./storage-classes.route"; import { storageClassStore } from "./storage-class.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", age = "age", provisioner = "provision", + default = "default", reclaimPolicy = "reclaim", } @@ -24,13 +25,15 @@ export class StorageClasses extends React.Component { render() { return ( item.getName(), - [sortBy.age]: (item: StorageClass) => item.metadata.creationTimestamp, - [sortBy.provisioner]: (item: StorageClass) => item.provisioner, - [sortBy.reclaimPolicy]: (item: StorageClass) => item.reclaimPolicy, + [columnId.name]: (item: StorageClass) => item.getName(), + [columnId.age]: (item: StorageClass) => item.metadata.creationTimestamp, + [columnId.provisioner]: (item: StorageClass) => item.provisioner, + [columnId.reclaimPolicy]: (item: StorageClass) => item.reclaimPolicy, }} searchFilters={[ (item: StorageClass) => item.getSearchFields(), @@ -38,12 +41,12 @@ export class StorageClasses extends React.Component { ]} renderHeaderTitle="Storage Classes" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Provisioner", className: "provisioner", sortBy: sortBy.provisioner }, - { title: "Reclaim Policy", className: "reclaim-policy", sortBy: sortBy.reclaimPolicy }, - { title: "Default", className: "is-default" }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Provisioner", className: "provisioner", sortBy: columnId.provisioner, id: columnId.provisioner }, + { title: "Reclaim Policy", className: "reclaim-policy", sortBy: columnId.reclaimPolicy, id: columnId.reclaimPolicy }, + { title: "Default", className: "is-default", id: columnId.default }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(storageClass: StorageClass) => [ storageClass.getName(), diff --git a/src/renderer/components/+storage-volume-claims/volume-claims.tsx b/src/renderer/components/+storage-volume-claims/volume-claims.tsx index bb9a4a05a7..e93529b8d2 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claims.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claims.tsx @@ -13,7 +13,7 @@ import { stopPropagation } from "../../utils"; import { storageClassApi } from "../../api/endpoints"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", pods = "pods", @@ -31,17 +31,19 @@ export class PersistentVolumeClaims extends React.Component { render() { return ( pvc.getName(), - [sortBy.namespace]: (pvc: PersistentVolumeClaim) => pvc.getNs(), - [sortBy.pods]: (pvc: PersistentVolumeClaim) => pvc.getPods(podsStore.items).map(pod => pod.getName()), - [sortBy.status]: (pvc: PersistentVolumeClaim) => pvc.getStatus(), - [sortBy.size]: (pvc: PersistentVolumeClaim) => unitsToBytes(pvc.getStorage()), - [sortBy.storageClass]: (pvc: PersistentVolumeClaim) => pvc.spec.storageClassName, - [sortBy.age]: (pvc: PersistentVolumeClaim) => pvc.metadata.creationTimestamp, + [columnId.name]: (pvc: PersistentVolumeClaim) => pvc.getName(), + [columnId.namespace]: (pvc: PersistentVolumeClaim) => pvc.getNs(), + [columnId.pods]: (pvc: PersistentVolumeClaim) => pvc.getPods(podsStore.items).map(pod => pod.getName()), + [columnId.status]: (pvc: PersistentVolumeClaim) => pvc.getStatus(), + [columnId.size]: (pvc: PersistentVolumeClaim) => unitsToBytes(pvc.getStorage()), + [columnId.storageClass]: (pvc: PersistentVolumeClaim) => pvc.spec.storageClassName, + [columnId.age]: (pvc: PersistentVolumeClaim) => pvc.metadata.creationTimestamp, }} searchFilters={[ (item: PersistentVolumeClaim) => item.getSearchFields(), @@ -49,14 +51,14 @@ export class PersistentVolumeClaims extends React.Component { ]} renderHeaderTitle="Persistent Volume Claims" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Storage class", className: "storageClass", sortBy: sortBy.storageClass }, - { title: "Size", className: "size", sortBy: sortBy.size }, - { title: "Pods", className: "pods", sortBy: sortBy.pods }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Status", className: "status", sortBy: sortBy.status }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Storage class", className: "storageClass", sortBy: columnId.storageClass, id: columnId.storageClass }, + { title: "Size", className: "size", sortBy: columnId.size, id: columnId.size }, + { title: "Pods", className: "pods", sortBy: columnId.pods, id: columnId.pods }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, ]} renderTableContents={(pvc: PersistentVolumeClaim) => { const pods = pvc.getPods(podsStore.items); diff --git a/src/renderer/components/+storage-volumes/volumes.tsx b/src/renderer/components/+storage-volumes/volumes.tsx index 412093a7ad..6822e3d4f7 100644 --- a/src/renderer/components/+storage-volumes/volumes.tsx +++ b/src/renderer/components/+storage-volumes/volumes.tsx @@ -11,10 +11,11 @@ import { volumesStore } from "./volumes.store"; import { pvcApi, storageClassApi } from "../../api/endpoints"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", storageClass = "storage-class", capacity = "capacity", + claim = "claim", status = "status", age = "age", } @@ -27,14 +28,16 @@ export class PersistentVolumes extends React.Component { render() { return ( item.getName(), - [sortBy.storageClass]: (item: PersistentVolume) => item.spec.storageClassName, - [sortBy.capacity]: (item: PersistentVolume) => item.getCapacity(true), - [sortBy.status]: (item: PersistentVolume) => item.getStatus(), - [sortBy.age]: (item: PersistentVolume) => item.metadata.creationTimestamp, + [columnId.name]: (item: PersistentVolume) => item.getName(), + [columnId.storageClass]: (item: PersistentVolume) => item.spec.storageClassName, + [columnId.capacity]: (item: PersistentVolume) => item.getCapacity(true), + [columnId.status]: (item: PersistentVolume) => item.getStatus(), + [columnId.age]: (item: PersistentVolume) => item.metadata.creationTimestamp, }} searchFilters={[ (item: PersistentVolume) => item.getSearchFields(), @@ -42,13 +45,13 @@ export class PersistentVolumes extends React.Component { ]} renderHeaderTitle="Persistent Volumes" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Storage Class", className: "storageClass", sortBy: sortBy.storageClass }, - { title: "Capacity", className: "capacity", sortBy: sortBy.capacity }, - { title: "Claim", className: "claim" }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Status", className: "status", sortBy: sortBy.status }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Storage Class", className: "storageClass", sortBy: columnId.storageClass, id: columnId.storageClass }, + { title: "Capacity", className: "capacity", sortBy: columnId.capacity, id: columnId.capacity }, + { title: "Claim", className: "claim", id: columnId.claim }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, ]} renderTableContents={(volume: PersistentVolume) => { const { claimRef, storageClassName } = volume.spec; diff --git a/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx b/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx index 3d64562047..f55e781e0e 100644 --- a/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx +++ b/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx @@ -10,7 +10,7 @@ import { KubeObjectListLayout } from "../kube-object"; import { AddRoleBindingDialog } from "./add-role-binding-dialog"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", bindings = "bindings", @@ -25,13 +25,15 @@ export class RoleBindings extends React.Component { render() { return ( binding.getName(), - [sortBy.namespace]: (binding: RoleBinding) => binding.getNs(), - [sortBy.bindings]: (binding: RoleBinding) => binding.getSubjectNames(), - [sortBy.age]: (binding: RoleBinding) => binding.metadata.creationTimestamp, + [columnId.name]: (binding: RoleBinding) => binding.getName(), + [columnId.namespace]: (binding: RoleBinding) => binding.getNs(), + [columnId.bindings]: (binding: RoleBinding) => binding.getSubjectNames(), + [columnId.age]: (binding: RoleBinding) => binding.metadata.creationTimestamp, }} searchFilters={[ (binding: RoleBinding) => binding.getSearchFields(), @@ -39,11 +41,11 @@ export class RoleBindings extends React.Component { ]} renderHeaderTitle="Role Bindings" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Bindings", className: "bindings", sortBy: sortBy.bindings }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Bindings", className: "bindings", sortBy: columnId.bindings, id: columnId.bindings }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(binding: RoleBinding) => [ binding.getName(), diff --git a/src/renderer/components/+user-management-roles/roles.tsx b/src/renderer/components/+user-management-roles/roles.tsx index 21ad3bdf8a..a990cbfa7e 100644 --- a/src/renderer/components/+user-management-roles/roles.tsx +++ b/src/renderer/components/+user-management-roles/roles.tsx @@ -10,7 +10,7 @@ import { KubeObjectListLayout } from "../kube-object"; import { AddRoleDialog } from "./add-role-dialog"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", age = "age", @@ -25,22 +25,24 @@ export class Roles extends React.Component { return ( <> role.getName(), - [sortBy.namespace]: (role: Role) => role.getNs(), - [sortBy.age]: (role: Role) => role.metadata.creationTimestamp, + [columnId.name]: (role: Role) => role.getName(), + [columnId.namespace]: (role: Role) => role.getNs(), + [columnId.age]: (role: Role) => role.metadata.creationTimestamp, }} searchFilters={[ (role: Role) => role.getSearchFields(), ]} renderHeaderTitle="Roles" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(role: Role) => [ role.getName(), diff --git a/src/renderer/components/+user-management-service-accounts/service-accounts.tsx b/src/renderer/components/+user-management-service-accounts/service-accounts.tsx index 37bed40ba9..4ea78904c0 100644 --- a/src/renderer/components/+user-management-service-accounts/service-accounts.tsx +++ b/src/renderer/components/+user-management-service-accounts/service-accounts.tsx @@ -15,7 +15,7 @@ import { CreateServiceAccountDialog } from "./create-service-account-dialog"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", age = "age", @@ -30,21 +30,23 @@ export class ServiceAccounts extends React.Component { return ( <> account.getName(), - [sortBy.namespace]: (account: ServiceAccount) => account.getNs(), - [sortBy.age]: (account: ServiceAccount) => account.metadata.creationTimestamp, + [columnId.name]: (account: ServiceAccount) => account.getName(), + [columnId.namespace]: (account: ServiceAccount) => account.getNs(), + [columnId.age]: (account: ServiceAccount) => account.metadata.creationTimestamp, }} searchFilters={[ (account: ServiceAccount) => account.getSearchFields(), ]} renderHeaderTitle="Service Accounts" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(account: ServiceAccount) => [ account.getName(), diff --git a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx index 19d35e4b3a..08b84c0671 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx @@ -18,12 +18,13 @@ import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { ConfirmDialog } from "../confirm-dialog/confirm-dialog"; import { Notifications } from "../notifications/notifications"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + schedule = "schedule", suspend = "suspend", active = "active", - lastSchedule = "schedule", + lastSchedule = "last-schedule", age = "age", } @@ -35,15 +36,17 @@ export class CronJobs extends React.Component { render() { return ( cronJob.getName(), - [sortBy.namespace]: (cronJob: CronJob) => cronJob.getNs(), - [sortBy.suspend]: (cronJob: CronJob) => cronJob.getSuspendFlag(), - [sortBy.active]: (cronJob: CronJob) => cronJobStore.getActiveJobsNum(cronJob), - [sortBy.lastSchedule]: (cronJob: CronJob) => cronJob.getLastScheduleTime(), - [sortBy.age]: (cronJob: CronJob) => cronJob.metadata.creationTimestamp, + [columnId.name]: (cronJob: CronJob) => cronJob.getName(), + [columnId.namespace]: (cronJob: CronJob) => cronJob.getNs(), + [columnId.suspend]: (cronJob: CronJob) => cronJob.getSuspendFlag(), + [columnId.active]: (cronJob: CronJob) => cronJobStore.getActiveJobsNum(cronJob), + [columnId.lastSchedule]: (cronJob: CronJob) => cronJob.getLastScheduleTime(), + [columnId.age]: (cronJob: CronJob) => cronJob.metadata.creationTimestamp, }} searchFilters={[ (cronJob: CronJob) => cronJob.getSearchFields(), @@ -51,14 +54,14 @@ export class CronJobs extends React.Component { ]} renderHeaderTitle="Cron Jobs" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Schedule", className: "schedule" }, - { title: "Suspend", className: "suspend", sortBy: sortBy.suspend }, - { title: "Active", className: "active", sortBy: sortBy.active }, - { title: "Last schedule", className: "last-schedule", sortBy: sortBy.lastSchedule }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Schedule", className: "schedule", id: columnId.schedule }, + { title: "Suspend", className: "suspend", sortBy: columnId.suspend, id: columnId.suspend }, + { title: "Active", className: "active", sortBy: columnId.active, id: columnId.active }, + { title: "Last schedule", className: "last-schedule", sortBy: columnId.lastSchedule, id: columnId.lastSchedule }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(cronJob: CronJob) => [ cronJob.getName(), diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx index ff061f7877..e2d1e30e17 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx @@ -13,10 +13,11 @@ import { IDaemonSetsRouteParams } from "../+workloads"; import { Badge } from "../badge"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", pods = "pods", + labels = "labels", age = "age", } @@ -38,13 +39,15 @@ export class DaemonSets extends React.Component { render() { return ( daemonSet.getName(), - [sortBy.namespace]: (daemonSet: DaemonSet) => daemonSet.getNs(), - [sortBy.pods]: (daemonSet: DaemonSet) => this.getPodsLength(daemonSet), - [sortBy.age]: (daemonSet: DaemonSet) => daemonSet.metadata.creationTimestamp, + [columnId.name]: (daemonSet: DaemonSet) => daemonSet.getName(), + [columnId.namespace]: (daemonSet: DaemonSet) => daemonSet.getNs(), + [columnId.pods]: (daemonSet: DaemonSet) => this.getPodsLength(daemonSet), + [columnId.age]: (daemonSet: DaemonSet) => daemonSet.metadata.creationTimestamp, }} searchFilters={[ (daemonSet: DaemonSet) => daemonSet.getSearchFields(), @@ -52,12 +55,12 @@ export class DaemonSets extends React.Component { ]} renderHeaderTitle="Daemon Sets" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Pods", className: "pods", sortBy: sortBy.pods }, - { className: "warning" }, - { title: "Node Selector", className: "labels" }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Pods", className: "pods", sortBy: columnId.pods, id: columnId.pods }, + { className: "warning", showWithColumn: columnId.pods }, + { title: "Node Selector", className: "labels", id: columnId.labels }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(daemonSet: DaemonSet) => [ daemonSet.getName(), diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index b84cd7b340..0147c238b9 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -23,9 +23,10 @@ import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-obje import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { Notifications } from "../notifications"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + pods = "pods", replicas = "replicas", age = "age", condition = "condition", @@ -55,14 +56,16 @@ export class Deployments extends React.Component { render() { return ( deployment.getName(), - [sortBy.namespace]: (deployment: Deployment) => deployment.getNs(), - [sortBy.replicas]: (deployment: Deployment) => deployment.getReplicas(), - [sortBy.age]: (deployment: Deployment) => deployment.metadata.creationTimestamp, - [sortBy.condition]: (deployment: Deployment) => deployment.getConditionsText(), + [columnId.name]: (deployment: Deployment) => deployment.getName(), + [columnId.namespace]: (deployment: Deployment) => deployment.getNs(), + [columnId.replicas]: (deployment: Deployment) => deployment.getReplicas(), + [columnId.age]: (deployment: Deployment) => deployment.metadata.creationTimestamp, + [columnId.condition]: (deployment: Deployment) => deployment.getConditionsText(), }} searchFilters={[ (deployment: Deployment) => deployment.getSearchFields(), @@ -70,13 +73,13 @@ export class Deployments extends React.Component { ]} renderHeaderTitle="Deployments" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Pods", className: "pods" }, - { title: "Replicas", className: "replicas", sortBy: sortBy.replicas }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Conditions", className: "conditions", sortBy: sortBy.condition }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Pods", className: "pods", id: columnId.pods }, + { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Conditions", className: "conditions", sortBy: columnId.condition, id: columnId.condition }, ]} renderTableContents={(deployment: Deployment) => [ deployment.getName(), diff --git a/src/renderer/components/+workloads-jobs/jobs.tsx b/src/renderer/components/+workloads-jobs/jobs.tsx index 00c1ee0db5..6301c287d0 100644 --- a/src/renderer/components/+workloads-jobs/jobs.tsx +++ b/src/renderer/components/+workloads-jobs/jobs.tsx @@ -12,9 +12,10 @@ import { IJobsRouteParams } from "../+workloads"; import kebabCase from "lodash/kebabCase"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + completions = "completions", conditions = "conditions", age = "age", } @@ -27,25 +28,27 @@ export class Jobs extends React.Component { render() { return ( job.getName(), - [sortBy.namespace]: (job: Job) => job.getNs(), - [sortBy.conditions]: (job: Job) => job.getCondition() != null ? job.getCondition().type : "", - [sortBy.age]: (job: Job) => job.metadata.creationTimestamp, + [columnId.name]: (job: Job) => job.getName(), + [columnId.namespace]: (job: Job) => job.getNs(), + [columnId.conditions]: (job: Job) => job.getCondition() != null ? job.getCondition().type : "", + [columnId.age]: (job: Job) => job.metadata.creationTimestamp, }} searchFilters={[ (job: Job) => job.getSearchFields(), ]} renderHeaderTitle="Jobs" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Completions", className: "completions" }, - { className: "warning" }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Conditions", className: "conditions", sortBy: sortBy.conditions }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Completions", className: "completions", id: columnId.completions }, + { className: "warning", showWithColumn: columnId.completions }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Conditions", className: "conditions", sortBy: columnId.conditions, id: columnId.conditions }, ]} renderTableContents={(job: Job) => { const condition = job.getCondition(); diff --git a/src/renderer/components/+workloads-replicasets/replicasets.tsx b/src/renderer/components/+workloads-replicasets/replicasets.tsx index 55f607e3c3..fa6ee5cef4 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.tsx +++ b/src/renderer/components/+workloads-replicasets/replicasets.tsx @@ -14,7 +14,7 @@ import { Icon } from "../icon/icon"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry"; import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", desired = "desired", @@ -31,27 +31,29 @@ export class ReplicaSets extends React.Component { render() { return ( replicaSet.getName(), - [sortBy.namespace]: (replicaSet: ReplicaSet) => replicaSet.getNs(), - [sortBy.desired]: (replicaSet: ReplicaSet) => replicaSet.getDesired(), - [sortBy.current]: (replicaSet: ReplicaSet) => replicaSet.getCurrent(), - [sortBy.ready]: (replicaSet: ReplicaSet) => replicaSet.getReady(), - [sortBy.age]: (replicaSet: ReplicaSet) => replicaSet.metadata.creationTimestamp, + [columnId.name]: (replicaSet: ReplicaSet) => replicaSet.getName(), + [columnId.namespace]: (replicaSet: ReplicaSet) => replicaSet.getNs(), + [columnId.desired]: (replicaSet: ReplicaSet) => replicaSet.getDesired(), + [columnId.current]: (replicaSet: ReplicaSet) => replicaSet.getCurrent(), + [columnId.ready]: (replicaSet: ReplicaSet) => replicaSet.getReady(), + [columnId.age]: (replicaSet: ReplicaSet) => replicaSet.metadata.creationTimestamp, }} searchFilters={[ (replicaSet: ReplicaSet) => replicaSet.getSearchFields(), ]} renderHeaderTitle="Replica Sets" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Desired", className: "desired", sortBy: sortBy.desired }, - { title: "Current", className: "current", sortBy: sortBy.current }, - { title: "Ready", className: "ready", sortBy: sortBy.ready }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Desired", className: "desired", sortBy: columnId.desired, id: columnId.desired }, + { title: "Current", className: "current", sortBy: columnId.current, id: columnId.current }, + { title: "Ready", className: "ready", sortBy: columnId.ready, id: columnId.ready }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(replicaSet: ReplicaSet) => [ replicaSet.getName(), diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx index 9e6011e156..7c91c9905c 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx @@ -17,9 +17,10 @@ import { MenuItem } from "../menu/menu"; import { Icon } from "../icon/icon"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry"; -enum sortBy { +enum columnId { name = "name", namespace = "namespace", + pods = "pods", age = "age", replicas = "replicas", } @@ -38,25 +39,27 @@ export class StatefulSets extends React.Component { render() { return ( statefulSet.getName(), - [sortBy.namespace]: (statefulSet: StatefulSet) => statefulSet.getNs(), - [sortBy.age]: (statefulSet: StatefulSet) => statefulSet.metadata.creationTimestamp, - [sortBy.replicas]: (statefulSet: StatefulSet) => statefulSet.getReplicas(), + [columnId.name]: (statefulSet: StatefulSet) => statefulSet.getName(), + [columnId.namespace]: (statefulSet: StatefulSet) => statefulSet.getNs(), + [columnId.age]: (statefulSet: StatefulSet) => statefulSet.metadata.creationTimestamp, + [columnId.replicas]: (statefulSet: StatefulSet) => statefulSet.getReplicas(), }} searchFilters={[ (statefulSet: StatefulSet) => statefulSet.getSearchFields(), ]} renderHeaderTitle="Stateful Sets" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, - { title: "Pods", className: "pods" }, - { title: "Replicas", className: "replicas", sortBy: sortBy.replicas }, - { className: "warning" }, - { title: "Age", className: "age", sortBy: sortBy.age }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Pods", className: "pods", id: columnId.pods }, + { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas }, + { className: "warning", showWithColumn: columnId.replicas }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} renderTableContents={(statefulSet: StatefulSet) => [ statefulSet.getName(), From 27439907b48891e91f2c23952a3dbd6f8e94d41a Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 28 Jan 2021 19:03:33 +0200 Subject: [PATCH 21/56] makefile: regenerate node_modules if yarn.lock changes (#2041) Signed-off-by: Jari Kolehmainen --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 362ef3b830..000682e039 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ endif binaries/client: yarn download-bins -node_modules: +node_modules: yarn.lock yarn install --frozen-lockfile yarn check --verify-tree --integrity From 79234dcbf945ba102f04bfd3c0adb86e9d1bb815 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Fri, 29 Jan 2021 09:18:25 +0300 Subject: [PATCH 22/56] Fix jest window.matchMedia() error warnings (#2037) Signed-off-by: Alex Andreev --- .../dock/__test__/dock-tabs.test.tsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/renderer/components/dock/__test__/dock-tabs.test.tsx b/src/renderer/components/dock/__test__/dock-tabs.test.tsx index bcf6b94a2b..f893e06540 100644 --- a/src/renderer/components/dock/__test__/dock-tabs.test.tsx +++ b/src/renderer/components/dock/__test__/dock-tabs.test.tsx @@ -4,8 +4,6 @@ import "@testing-library/jest-dom/extend-expect"; import { DockTabs } from "../dock-tabs"; import { dockStore, IDockTab, TabKind } from "../dock.store"; -import { createResourceTab } from "../create-resource.store"; -import { createTerminalTab } from "../terminal.store"; import { observable } from "mobx"; const onChangeTab = jest.fn(); @@ -25,11 +23,19 @@ const getTabKinds = () => dockStore.tabs.map(tab => tab.kind); describe("", () => { beforeEach(() => { - createTerminalTab(); - createResourceTab(); - createTerminalTab(); - createResourceTab(); - createTerminalTab(); + const terminalTab: IDockTab = { id: "terminal1", kind: TabKind.TERMINAL, title: "Terminal" }; + const createResourceTab: IDockTab = { id: "create", kind: TabKind.CREATE_RESOURCE, title: "Create resource" }; + const editResourceTab: IDockTab = { id: "edit", kind: TabKind.EDIT_RESOURCE, title: "Edit resource" }; + const installChartTab: IDockTab = { id: "install", kind: TabKind.INSTALL_CHART, title: "Install chart" }; + const logsTab: IDockTab = { id: "logs", kind: TabKind.POD_LOGS, title: "Logs" }; + + dockStore.tabs.push( + terminalTab, + createResourceTab, + editResourceTab, + installChartTab, + logsTab + ); }); afterEach(() => { @@ -72,9 +78,9 @@ describe("", () => { expect(getTabKinds()).toEqual([ TabKind.TERMINAL, TabKind.CREATE_RESOURCE, - TabKind.TERMINAL, - TabKind.CREATE_RESOURCE, - TabKind.TERMINAL + TabKind.EDIT_RESOURCE, + TabKind.INSTALL_CHART, + TabKind.POD_LOGS ]); }); @@ -90,7 +96,7 @@ describe("", () => { const tabs = container.querySelectorAll(".Tab"); expect(tabs.length).toBe(1); - expect(getTabKinds()).toEqual([TabKind.TERMINAL]); + expect(getTabKinds()).toEqual([TabKind.EDIT_RESOURCE]); }); it("closes all tabs", () => { @@ -123,7 +129,7 @@ describe("", () => { TabKind.TERMINAL, TabKind.TERMINAL, TabKind.CREATE_RESOURCE, - TabKind.TERMINAL + TabKind.EDIT_RESOURCE ]); }); From 7490b15aad8c930a0caf35e3d3f45b8cdbf839d7 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 1 Feb 2021 08:40:58 -0500 Subject: [PATCH 23/56] update extensions' package-lock files (#2043) Signed-off-by: Sebastian Malton --- extensions/example-extension/package-lock.json | 17 +++++++++++++---- extensions/license-menu-item/package-lock.json | 17 +++++++++++++---- .../metrics-cluster-feature/package-lock.json | 12 +++++++++--- extensions/node-menu/package-lock.json | 17 +++++++++++++---- extensions/telemetry/package-lock.json | 17 +++++++++++++---- 5 files changed, 61 insertions(+), 19 deletions(-) diff --git a/extensions/example-extension/package-lock.json b/extensions/example-extension/package-lock.json index 954ba2e41f..16febd433c 100644 --- a/extensions/example-extension/package-lock.json +++ b/extensions/example-extension/package-lock.json @@ -2796,7 +2796,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3226,6 +3227,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4367,6 +4369,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4381,6 +4384,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "optional": true, "requires": { "yallist": "^4.0.0" } @@ -4390,6 +4394,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -4399,6 +4404,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -4407,7 +4413,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true } } }, @@ -5398,7 +5405,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -6275,7 +6283,8 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", - "dev": true + "dev": true, + "optional": true }, "v8-to-istanbul": { "version": "7.0.0", diff --git a/extensions/license-menu-item/package-lock.json b/extensions/license-menu-item/package-lock.json index 071d2f62a6..5d6de53633 100644 --- a/extensions/license-menu-item/package-lock.json +++ b/extensions/license-menu-item/package-lock.json @@ -2868,7 +2868,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3298,6 +3299,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4460,6 +4462,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4474,6 +4477,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "optional": true, "requires": { "yallist": "^4.0.0" } @@ -4483,6 +4487,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -4492,6 +4497,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -4500,7 +4506,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true } } }, @@ -5516,7 +5523,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -6406,7 +6414,8 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", - "dev": true + "dev": true, + "optional": true }, "v8-to-istanbul": { "version": "7.0.0", diff --git a/extensions/metrics-cluster-feature/package-lock.json b/extensions/metrics-cluster-feature/package-lock.json index 334135b4eb..ea68169ca9 100644 --- a/extensions/metrics-cluster-feature/package-lock.json +++ b/extensions/metrics-cluster-feature/package-lock.json @@ -2816,7 +2816,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3246,6 +3247,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4394,6 +4396,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4408,6 +4411,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -5434,7 +5438,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -6311,7 +6316,8 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", - "dev": true + "dev": true, + "optional": true }, "v8-to-istanbul": { "version": "7.0.0", diff --git a/extensions/node-menu/package-lock.json b/extensions/node-menu/package-lock.json index 5e1eec009a..2595e825f7 100644 --- a/extensions/node-menu/package-lock.json +++ b/extensions/node-menu/package-lock.json @@ -2796,7 +2796,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3226,6 +3227,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4382,6 +4384,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4396,6 +4399,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "optional": true, "requires": { "yallist": "^4.0.0" } @@ -4405,6 +4409,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -4414,6 +4419,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -4422,7 +4428,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true } } }, @@ -5438,7 +5445,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", @@ -6315,7 +6323,8 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", - "dev": true + "dev": true, + "optional": true }, "v8-to-istanbul": { "version": "7.0.0", diff --git a/extensions/telemetry/package-lock.json b/extensions/telemetry/package-lock.json index 89e30dc2f4..9829eebb6b 100644 --- a/extensions/telemetry/package-lock.json +++ b/extensions/telemetry/package-lock.json @@ -2901,7 +2901,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true + "dev": true, + "optional": true }, "har-schema": { "version": "2.0.0", @@ -3337,6 +3338,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "optional": true, "requires": { "is-docker": "^2.0.0" } @@ -4533,6 +4535,7 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.1.tgz", "integrity": "sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==", "dev": true, + "optional": true, "requires": { "growly": "^1.3.0", "is-wsl": "^2.2.0", @@ -4547,6 +4550,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "optional": true, "requires": { "yallist": "^4.0.0" } @@ -4556,6 +4560,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, + "optional": true, "requires": { "lru-cache": "^6.0.0" } @@ -4564,13 +4569,15 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "dev": true, + "optional": true }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "optional": true, "requires": { "isexe": "^2.0.0" } @@ -4579,7 +4586,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true } } }, @@ -5595,7 +5603,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.3", From 078f952b363df6492f37ff833185cd1c620e0902 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 1 Feb 2021 15:49:32 +0200 Subject: [PATCH 24/56] Watch-api streaming reworks (#1990) * loading k8s resources into stores per selected namespaces -- part 1 Signed-off-by: Roman * loading k8s resources into stores per selected namespaces -- part 2 - fix: generating helm chart id Signed-off-by: Roman * loading k8s resources into stores per selected namespaces -- part 3 Signed-off-by: Roman * fixes Signed-off-by: Roman * fixes / responding to comments Signed-off-by: Roman * chore / small fixes Signed-off-by: Roman * Watch api does not work for non-admins with lots of namespaces #1898 -- part 1 Signed-off-by: Roman * fixes & refactoring Signed-off-by: Roman * make lint happy Signed-off-by: Roman * reset store on loading error Signed-off-by: Roman * added new cluster method: cluster.isAllowedResource Signed-off-by: Roman * fix: loading namespaces optimizations Signed-off-by: Roman * fixes & refactoring Signed-off-by: Roman * fix: parse multiple kube-events from stream's chunk Signed-off-by: Roman * fix: mobx issue with accessing empty observable array by index (removes warning), use common logger Signed-off-by: Roman * fine-tuning Signed-off-by: Roman * fix: parse json stream chunks at client-side (might be partial, depends on network speed) Signed-off-by: Roman * store subscribing refactoring -- part 1 Signed-off-by: Roman * store subscribing refactoring -- part 2 Signed-off-by: Roman * store subscribing refactoring -- part 3 Signed-off-by: Roman * store subscribing refactoring -- part 4 Signed-off-by: Roman * auto-reconnect on online/offline status change, interval connection check Signed-off-by: Roman * check connection every 5m Signed-off-by: Roman * split concurrent watch-api requests by 10 at a time + 150ms delay before next call Signed-off-by: Roman * refactoring / clean up Signed-off-by: Roman * use `plimit` + delay for k8s watch requests Signed-off-by: Roman * lint fix Signed-off-by: Roman * added explicit `preload: true` when subscribing stores Signed-off-by: Roman * kubeWatchApi refactoring / fine-tuning Signed-off-by: Roman * clean up Signed-off-by: Roman --- src/common/utils/delay.ts | 6 + src/common/utils/index.ts | 1 + src/main/router.ts | 2 +- src/main/routes/watch-route.ts | 97 +++- src/renderer/api/api-manager.ts | 4 +- src/renderer/api/kube-api.ts | 14 +- src/renderer/api/kube-watch-api.ts | 431 ++++++++++++------ .../components/+cluster/cluster-overview.tsx | 44 +- .../components/+events/event.store.ts | 4 + .../+namespaces/namespace-select.tsx | 27 +- .../components/+namespaces/namespace.store.ts | 6 +- .../+network-services/service-details.tsx | 16 +- src/renderer/components/+nodes/nodes.store.ts | 5 + .../role-bindings.store.ts | 4 +- .../+user-management-roles/roles.store.ts | 4 +- .../+workloads-overview/overview.tsx | 60 +-- src/renderer/components/app.tsx | 86 ++-- .../item-object-list/item-list-layout.tsx | 58 +-- .../kube-object/kube-object-list-layout.tsx | 16 +- src/renderer/kube-object.store.ts | 29 +- 20 files changed, 519 insertions(+), 395 deletions(-) create mode 100644 src/common/utils/delay.ts diff --git a/src/common/utils/delay.ts b/src/common/utils/delay.ts new file mode 100644 index 0000000000..208e042759 --- /dev/null +++ b/src/common/utils/delay.ts @@ -0,0 +1,6 @@ +// Create async delay for provided timeout in milliseconds + +export async function delay(timeoutMs = 1000) { + if (!timeoutMs) return; + await new Promise(resolve => setTimeout(resolve, timeoutMs)); +} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 582135d7f0..942c675f0a 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -7,6 +7,7 @@ export * from "./autobind"; export * from "./base64"; export * from "./camelCase"; export * from "./cloneJson"; +export * from "./delay"; export * from "./debouncePromise"; export * from "./defineGlobal"; export * from "./getRandId"; diff --git a/src/main/router.ts b/src/main/router.ts index 896893a592..6e98d0ce0c 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -146,7 +146,7 @@ export class Router { this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute)); // Watch API - this.router.add({ method: "get", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute)); + this.router.add({ method: "post", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute)); // Metrics API this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute)); diff --git a/src/main/routes/watch-route.ts b/src/main/routes/watch-route.ts index eb9f007eae..2c86314908 100644 --- a/src/main/routes/watch-route.ts +++ b/src/main/routes/watch-route.ts @@ -1,10 +1,29 @@ +import type { KubeJsonApiData, KubeJsonApiError } from "../../renderer/api/kube-json-api"; + +import plimit from "p-limit"; +import { delay } from "../../common/utils"; import { LensApiRequest } from "../router"; import { LensApi } from "../lens-api"; -import { Watch, KubeConfig } from "@kubernetes/client-node"; +import { KubeConfig, Watch } from "@kubernetes/client-node"; import { ServerResponse } from "http"; import { Request } from "request"; import logger from "../logger"; +export interface IKubeWatchEvent { + type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR" | "STREAM_END"; + object?: T; +} + +export interface IKubeWatchEventStreamEnd extends IKubeWatchEvent { + type: "STREAM_END"; + url: string; + status: number; +} + +export interface IWatchRoutePayload { + apis: string[]; // kube-api url list for subscribing to watch events +} + class ApiWatcher { private apiUrl: string; private response: ServerResponse; @@ -24,6 +43,7 @@ class ApiWatcher { clearInterval(this.processor); } this.processor = setInterval(() => { + if (this.response.finished) return; const events = this.eventBuffer.splice(0); events.map(event => this.sendEvent(event)); @@ -33,7 +53,9 @@ class ApiWatcher { } public stop() { - if (!this.watchRequest) { return; } + if (!this.watchRequest) { + return; + } if (this.processor) { clearInterval(this.processor); @@ -42,11 +64,14 @@ class ApiWatcher { try { this.watchRequest.abort(); - this.sendEvent({ + + const event: IKubeWatchEventStreamEnd = { type: "STREAM_END", url: this.apiUrl, status: 410, - }); + }; + + this.sendEvent(event); logger.debug("watch aborted"); } catch (error) { logger.error(`Watch abort errored:${error}`); @@ -65,50 +90,72 @@ class ApiWatcher { this.watchRequest.abort(); } - private sendEvent(evt: any) { - // convert to "text/event-stream" format - this.response.write(`data: ${JSON.stringify(evt)}\n\n`); + private sendEvent(evt: IKubeWatchEvent) { + this.response.write(`${JSON.stringify(evt)}\n`); } } class WatchRoute extends LensApi { + private response: ServerResponse; - public async routeWatch(request: LensApiRequest) { - const { response, cluster} = request; - const apis: string[] = request.query.getAll("api"); - const watchers: ApiWatcher[] = []; + private setResponse(response: ServerResponse) { + // clean up previous connection and stop all corresponding watch-api requests + // otherwise it happens only by request timeout or something else.. + this.response?.destroy(); + this.response = response; + } - if (!apis.length) { + public async routeWatch(request: LensApiRequest) { + const { response, cluster, payload: { apis } = {} } = request; + + if (!apis?.length) { this.respondJson(response, { - message: "Empty request. Query params 'api' are not provided.", - example: "?api=/api/v1/pods&api=/api/v1/nodes", + message: "watch apis list is empty" }, 400); return; } - response.setHeader("Content-Type", "text/event-stream"); + this.setResponse(response); + response.setHeader("Content-Type", "application/json"); response.setHeader("Cache-Control", "no-cache"); response.setHeader("Connection", "keep-alive"); logger.debug(`watch using kubeconfig:${JSON.stringify(cluster.getProxyKubeconfig(), null, 2)}`); + // limit concurrent k8s requests to avoid possible ECONNRESET-error + const requests = plimit(5); + const watchers = new Map(); + let isWatchRequestEnded = false; + apis.forEach(apiUrl => { const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response); - watcher.start(); - watchers.push(watcher); + watchers.set(apiUrl, watcher); + + requests(async () => { + if (isWatchRequestEnded) return; + await watcher.start(); + await delay(100); + }); + }); + + function onRequestEnd() { + if (isWatchRequestEnded) return; + isWatchRequestEnded = true; + requests.clearQueue(); + watchers.forEach(watcher => watcher.stop()); + watchers.clear(); + } + + request.raw.req.on("end", () => { + logger.info("Watch request end"); + onRequestEnd(); }); request.raw.req.on("close", () => { - logger.debug("Watch request closed"); - watchers.map(watcher => watcher.stop()); + logger.info("Watch request close"); + onRequestEnd(); }); - - request.raw.req.on("end", () => { - logger.debug("Watch request ended"); - watchers.map(watcher => watcher.stop()); - }); - } } diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index 629a0f29c2..47500adf79 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -2,7 +2,7 @@ import type { KubeObjectStore } from "../kube-object.store"; import { action, observable } from "mobx"; import { autobind } from "../utils"; -import { KubeApi } from "./kube-api"; +import { KubeApi, parseKubeApi } from "./kube-api"; @autobind() export class ApiManager { @@ -11,7 +11,7 @@ export class ApiManager { getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) { if (typeof pathOrCallback === "string") { - return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase); + return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase); } return Array.from(this.apis.values()).find(pathOrCallback ?? (() => true)); diff --git a/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts index 8a3a2517c2..e62603b14f 100644 --- a/src/renderer/api/kube-api.ts +++ b/src/renderer/api/kube-api.ts @@ -92,14 +92,6 @@ export function ensureObjectSelfLink(api: KubeApi, object: KubeJsonApiData) { } export class KubeApi { - static parseApi = parseKubeApi; - - static watchAll(...apis: KubeApi[]) { - const disposers = apis.map(api => api.watch()); - - return () => disposers.forEach(unwatch => unwatch()); - } - readonly kind: string; readonly apiBase: string; readonly apiPrefix: string; @@ -124,7 +116,7 @@ export class KubeApi { if (!options.apiBase) { options.apiBase = objectConstructor.apiBase; } - const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = KubeApi.parseApi(options.apiBase); + const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase); this.kind = kind; this.isNamespaced = isNamespaced; @@ -157,7 +149,7 @@ export class KubeApi { for (const apiUrl of apiBases) { // Split e.g. "/apis/extensions/v1beta1/ingresses" to parts - const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = KubeApi.parseApi(apiUrl); + const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = parseKubeApi(apiUrl); // Request available resources try { @@ -366,7 +358,7 @@ export class KubeApi { } watch(): () => void { - return kubeWatchApi.subscribe(this); + return kubeWatchApi.subscribeApi(this); } } diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index fe35a04baa..8adf58676f 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -1,202 +1,349 @@ -// Kubernetes watch-api consumer +// Kubernetes watch-api client +// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams -import { computed, observable, reaction } from "mobx"; -import { stringify } from "querystring"; -import { autobind, EventEmitter } from "../utils"; -import { KubeJsonApiData } from "./kube-json-api"; +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 { ensureObjectSelfLink, KubeApi } from "./kube-api"; +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 { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api"; +import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api"; +import { apiPrefix, isDebugging, isProduction } from "../../common/vars"; import { apiManager } from "./api-manager"; -import { apiPrefix, isDevelopment } from "../../common/vars"; -import { getHostedCluster } from "../../common/cluster-store"; -export interface IKubeWatchEvent { - type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR"; - object?: T; +export { IKubeWatchEvent, IKubeWatchEventStreamEnd }; + +export interface IKubeWatchMessage { + data?: IKubeWatchEvent + error?: IKubeWatchEvent; + api?: KubeApi; + store?: KubeObjectStore; } -export interface IKubeWatchRouteEvent { - type: "STREAM_END"; - url: string; - status: number; +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 } -export interface IKubeWatchRouteQuery { - api: string | string[]; +export interface IKubeWatchReconnectOptions { + reconnectAttempts: number; + timeout: number; +} + +export interface IKubeWatchLog { + message: string | Error; + meta?: object; } @autobind() export class KubeWatchApi { - protected evtSource: EventSource; - protected onData = new EventEmitter<[IKubeWatchEvent]>(); - protected subscribers = observable.map(); - protected reconnectTimeoutMs = 5000; - protected maxReconnectsOnError = 10; - protected reconnectAttempts = this.maxReconnectsOnError; + private cluster: Cluster; + private namespaceStore: NamespaceStore; - constructor() { - reaction(() => this.activeApis, () => this.connect(), { - fireImmediately: true, - delay: 500, - }); + private requestId = 0; + private isConnected = false; + private reader: ReadableStreamReader; + private subscribers = observable.map(); + + // events + public onMessage = new EventEmitter<[IKubeWatchMessage]>(); + + @computed get isActive(): boolean { + return this.apis.length > 0; } - @computed get activeApis() { - return Array.from(this.subscribers.keys()); + @computed get apis(): string[] { + const { cluster, namespaceStore } = this; + const activeApis = Array.from(this.subscribers.keys()); + + return activeApis.map(api => { + if (!cluster.isAllowedResource(api.kind)) { + return []; + } + + if (api.isNamespaced) { + return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace)); + } else { + 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; + this.bindAutoConnect(); + } + + private bindAutoConnect() { + const connect = debounce(() => this.connect(), 1000); + + reaction(() => this.apis, connect, { + fireImmediately: true, + equals: comparer.structural, + }); + + window.addEventListener("online", () => this.connect()); + window.addEventListener("offline", () => this.disconnect()); + setInterval(() => this.connectionCheck(), 60000 * 5); // every 5m } getSubscribersCount(api: KubeApi) { return this.subscribers.get(api) || 0; } - subscribe(...apis: KubeApi[]) { + isAllowedApi(api: KubeApi): boolean { + return !!this?.cluster.isAllowedResource(api.kind); + } + + subscribeApi(api: KubeApi | KubeApi[]): () => void { + const apis: KubeApi[] = [api].flat(); + apis.forEach(api => { + if (!this.isAllowedApi(api)) return; // skip this.subscribers.set(api, this.getSubscribersCount(api) + 1); }); - return () => apis.forEach(api => { - const count = this.getSubscribersCount(api) - 1; + return () => { + apis.forEach(api => { + const count = this.getSubscribersCount(api) - 1; - if (count <= 0) this.subscribers.delete(api); - else this.subscribers.set(api, count); - }); - } - - // 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 && !api.isNamespaced) { - return api.getWatchUrl(); - } - - if (api.isNamespaced) { - return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace)); - } - - return []; - }).flat() + if (count <= 0) this.subscribers.delete(api); + else this.subscribers.set(api, count); + }); }; } - // todo: maybe switch to websocket to avoid often reconnects - @autobind() - protected async connect() { - if (this.evtSource) this.disconnect(); // close previous connection + subscribeStores(stores: KubeObjectStore[], options: IKubeWatchSubscribeStoreOptions = {}): () => void { + const { preload = true, waitUntilLoaded = true, cacheLoading = false } = options; + const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages + const preloading: Promise[] = []; + const apis = new Set(stores.map(store => store.getSubscribeApis()).flat()); + const unsubscribeList: (() => void)[] = []; + let isUnsubscribed = false; - const query = await this.getQuery(); + 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 }, + }); + }); + } else { + subscribe(); + } + + // unsubscribe + return () => { + if (isUnsubscribed) return; + isUnsubscribed = true; + limitRequests.clearQueue(); + unsubscribeList.forEach(unsubscribe => unsubscribe()); + }; + } + + protected async connectionCheck() { + if (!this.isConnected) { + this.log({ message: "Offline: reconnecting.." }); + await this.connect(); + } + + this.log({ + message: `Connection check: ${this.isConnected ? "online" : "offline"}`, + meta: { connected: this.isConnected }, + }); + } + + protected async connect(apis = this.apis) { + this.disconnect(); // close active connections first + + if (!navigator.onLine || !apis.length) { + this.isConnected = false; - if (!this.activeApis.length || !query.api.length) { return; } - const apiUrl = `${apiPrefix}/watch?${stringify(query)}`; + this.log({ + message: "Connecting", + meta: { apis } + }); - this.evtSource = new EventSource(apiUrl); - this.evtSource.onmessage = this.onMessage; - this.evtSource.onerror = this.onError; - this.writeLog("CONNECTING", query.api); - } + try { + const requestId = ++this.requestId; + const abortController = new AbortController(); - reconnect() { - if (!this.evtSource || this.evtSource.readyState !== EventSource.OPEN) { - this.reconnectAttempts = this.maxReconnectsOnError; - this.connect(); + const request = await fetch(`${apiPrefix}/watch`, { + method: "POST", + body: JSON.stringify({ apis } as IWatchRoutePayload), + signal: abortController.signal, + headers: { + "content-type": "application/json" + } + }); + + // request above is stale since new request-id has been issued + if (this.requestId !== requestId) { + abortController.abort(); + + return; + } + + let jsonBuffer = ""; + const stream = request.body.pipeThrough(new TextDecoderStream()); + const reader = stream.getReader(); + + this.isConnected = true; + this.reader = reader; + + while (true) { + const { done, value } = await reader.read(); + + if (done) break; // exit + + const events = (jsonBuffer + value).split("\n"); + + jsonBuffer = this.processBuffer(events); + } + } catch (error) { + this.log({ message: error }); + } finally { + this.isConnected = false; } } protected disconnect() { - if (!this.evtSource) return; - this.evtSource.close(); - this.evtSource.onmessage = null; - this.evtSource = null; + this.reader?.cancel(); + this.reader = null; + this.isConnected = false; } - protected onMessage(evt: MessageEvent) { - if (!evt.data) return; - const data = JSON.parse(evt.data); + // process received stream events, returns unprocessed buffer chunk if any + protected processBuffer(events: string[]): string { + for (const json of events) { + try { + const kubeEvent: IKubeWatchEvent = JSON.parse(json); + const message = this.getMessage(kubeEvent); - if ((data as IKubeWatchEvent).object) { - this.onData.emit(data); - } else { - this.onRouteEvent(data); + this.onMessage.emit(message); + } catch (error) { + return json; + } } + + return ""; } - protected async onRouteEvent(event: IKubeWatchRouteEvent) { - if (event.type === "STREAM_END") { - this.disconnect(); - const { apiBase, namespace } = KubeApi.parseApi(event.url); - const api = apiManager.getApi(apiBase); + protected getMessage(event: IKubeWatchEvent): IKubeWatchMessage { + const message: IKubeWatchMessage = {}; - if (api) { - try { - await api.refreshResourceVersion({ namespace }); - this.reconnect(); - } catch (error) { - console.error("failed to refresh resource version", error); + switch (event.type) { + case "ADDED": + case "DELETED": - if (this.subscribers.size > 0) { - setTimeout(() => { - this.onRouteEvent(event); - }, 1000); - } + case "MODIFIED": { + const data = event as IKubeWatchEvent; + const api = apiManager.getApiByKind(data.object.kind, data.object.apiVersion); + + message.data = data; + + if (api) { + ensureObjectSelfLink(api, data.object); + + const { namespace, resourceVersion } = data.object.metadata; + + api.setResourceVersion(namespace, resourceVersion); + api.setResourceVersion("", resourceVersion); + + message.api = api; + message.store = apiManager.getStore(api); } + break; + } + + case "ERROR": + message.error = event as IKubeWatchEvent; + break; + + case "STREAM_END": { + this.onServerStreamEnd(event as IKubeWatchEventStreamEnd, { + reconnectAttempts: 5, + timeout: 1000, + }); + break; + } + } + + return message; + } + + protected async onServerStreamEnd(event: IKubeWatchEventStreamEnd, opts?: IKubeWatchReconnectOptions) { + const { apiBase, namespace } = parseKubeApi(event.url); + const api = apiManager.getApi(apiBase); + + if (!api) return; + + try { + await api.refreshResourceVersion({ namespace }); + this.connect(); + } catch (error) { + this.log({ + message: new Error(`Failed to connect on single stream end: ${error}`), + meta: { event, error }, + }); + + if (this.isActive && opts?.reconnectAttempts > 0) { + opts.reconnectAttempts--; + setTimeout(() => this.onServerStreamEnd(event, opts), opts.timeout); // repeat event } } } - protected onError(evt: MessageEvent) { - const { reconnectAttempts: attemptsRemain, reconnectTimeoutMs } = this; - - if (evt.eventPhase === EventSource.CLOSED) { - if (attemptsRemain > 0) { - this.reconnectAttempts--; - setTimeout(() => this.connect(), reconnectTimeoutMs); - } + protected log({ message, meta = {} }: IKubeWatchLog) { + if (isProduction && !isDebugging) { + return; } - } - protected writeLog(...data: any[]) { - if (isDevelopment) { - console.log("%cKUBE-WATCH-API:", `font-weight: bold`, ...data); + const logMessage = `%c[KUBE-WATCH-API]: ${String(message).toUpperCase()}`; + const isError = message instanceof Error; + const textStyle = `font-weight: bold;`; + const time = new Date().toLocaleString(); + + if (isError) { + console.error(logMessage, textStyle, { time, ...meta }); + } else { + console.info(logMessage, textStyle, { time, ...meta }); } } - - addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) { - const listener = (evt: IKubeWatchEvent) => { - if (evt.type === "ERROR") { - return; // e.g. evt.object.message == "too old resource version" - } - - const { namespace, resourceVersion } = evt.object.metadata; - const api = apiManager.getApiByKind(evt.object.kind, evt.object.apiVersion); - - api.setResourceVersion(namespace, resourceVersion); - api.setResourceVersion("", resourceVersion); - - ensureObjectSelfLink(api, evt.object); - - if (store == apiManager.getStore(api)) { - callback(evt); - } - }; - - this.onData.addListener(listener); - - return () => this.onData.removeListener(listener); - } - - reset() { - this.subscribers.clear(); - } } export const kubeWatchApi = new KubeWatchApi(); diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx index 104c6fd022..1f7a7f6b78 100644 --- a/src/renderer/components/+cluster/cluster-overview.tsx +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -3,13 +3,9 @@ import "./cluster-overview.scss"; import React from "react"; import { reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; - -import { eventStore } from "../+events/event.store"; import { nodesStore } from "../+nodes/nodes.store"; import { podsStore } from "../+workloads-pods/pods.store"; import { getHostedCluster } from "../../../common/cluster-store"; -import { isAllowedResource } from "../../../common/rbac"; -import { KubeObjectStore } from "../../kube-object.store"; import { interval } from "../../utils"; import { TabLayout } from "../layout/tab-layout"; import { Spinner } from "../spinner"; @@ -17,45 +13,33 @@ import { ClusterIssues } from "./cluster-issues"; import { ClusterMetrics } from "./cluster-metrics"; import { clusterOverviewStore } from "./cluster-overview.store"; import { ClusterPieCharts } from "./cluster-pie-charts"; +import { eventStore } from "../+events/event.store"; +import { kubeWatchApi } from "../../api/kube-watch-api"; @observer export class ClusterOverview extends React.Component { - private stores: KubeObjectStore[] = []; - private subscribers: Array<() => void> = []; - private metricPoller = interval(60, this.loadMetrics); - - @disposeOnUnmount - fetchMetrics = reaction( - () => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher - () => this.metricPoller.restart(true) - ); + private metricPoller = interval(60, () => this.loadMetrics()); loadMetrics() { getHostedCluster().available && clusterOverviewStore.loadMetrics(); } - async componentDidMount() { - if (isAllowedResource("nodes")) { - this.stores.push(nodesStore); - } + componentDidMount() { + this.metricPoller.start(true); - if (isAllowedResource("pods")) { - this.stores.push(podsStore); - } + disposeOnUnmount(this, [ + kubeWatchApi.subscribeStores([nodesStore, podsStore, eventStore], { + preload: true, + }), - if (isAllowedResource("events")) { - this.stores.push(eventStore); - } - - await Promise.all(this.stores.map(store => store.loadAll())); - this.loadMetrics(); - - this.subscribers = this.stores.map(store => store.subscribe()); - this.metricPoller.start(); + reaction( + () => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher + () => this.metricPoller.restart(true) + ), + ]); } componentWillUnmount() { - this.subscribers.forEach(dispose => dispose()); // unsubscribe all this.metricPoller.stop(); } diff --git a/src/renderer/components/+events/event.store.ts b/src/renderer/components/+events/event.store.ts index 3651ce1549..d6090be947 100644 --- a/src/renderer/components/+events/event.store.ts +++ b/src/renderer/components/+events/event.store.ts @@ -52,6 +52,10 @@ export class EventStore extends KubeObjectStore { return compact(eventsWithError); } + + getWarningsCount() { + return this.getWarnings().length; + } } export const eventStore = new EventStore(); diff --git a/src/renderer/components/+namespaces/namespace-select.tsx b/src/renderer/components/+namespaces/namespace-select.tsx index 079a9cd0b6..6ee7ea2d57 100644 --- a/src/renderer/components/+namespaces/namespace-select.tsx +++ b/src/renderer/components/+namespaces/namespace-select.tsx @@ -2,13 +2,14 @@ import "./namespace-select.scss"; import React from "react"; import { computed } from "mobx"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import { Select, SelectOption, SelectProps } from "../select"; -import { cssNames, noop } from "../../utils"; +import { cssNames } from "../../utils"; import { Icon } from "../icon"; import { namespaceStore } from "./namespace.store"; import { FilterIcon } from "../item-object-list/filter-icon"; import { FilterType } from "../item-object-list/page-filters.store"; +import { kubeWatchApi } from "../../api/kube-watch-api"; interface Props extends SelectProps { showIcons?: boolean; @@ -28,17 +29,13 @@ const defaultProps: Partial = { @observer export class NamespaceSelect extends React.Component { static defaultProps = defaultProps as object; - private unsubscribe = noop; - async componentDidMount() { - if (!namespaceStore.isLoaded) { - await namespaceStore.loadAll(); - } - this.unsubscribe = namespaceStore.subscribe(); - } - - componentWillUnmount() { - this.unsubscribe(); + componentDidMount() { + disposeOnUnmount(this, [ + kubeWatchApi.subscribeStores([namespaceStore], { + preload: true, + }) + ]); } @computed get options(): SelectOption[] { @@ -60,7 +57,7 @@ export class NamespaceSelect extends React.Component { return label || ( <> - {showIcons && } + {showIcons && } {value} ); @@ -103,9 +100,9 @@ export class NamespaceSelectFilter extends React.Component { return (
- + {namespace} - {isSelected && } + {isSelected && }
); }} diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index 50ec2c8038..63bb7525de 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -117,15 +117,15 @@ export class NamespaceStore extends KubeObjectStore { return namespaces; } - subscribe(apis = [this.api]) { + getSubscribeApis() { 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 (accessibleNamespaces.length > 0) { - return Function; // no-op + return []; } - return super.subscribe(apis); + return super.getSubscribeApis(); } protected async loadItems(params: KubeObjectStoreLoadingParams) { diff --git a/src/renderer/components/+network-services/service-details.tsx b/src/renderer/components/+network-services/service-details.tsx index 58cbe0a86e..80c9bd4dc7 100644 --- a/src/renderer/components/+network-services/service-details.tsx +++ b/src/renderer/components/+network-services/service-details.tsx @@ -1,17 +1,18 @@ import "./service-details.scss"; import React from "react"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import { DrawerItem, DrawerTitle } from "../drawer"; import { Badge } from "../badge"; import { KubeEventDetails } from "../+events/kube-event-details"; import { KubeObjectDetailsProps } from "../kube-object"; -import { Service, endpointApi } from "../../api/endpoints"; +import { Service } from "../../api/endpoints"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { ServicePortComponent } from "./service-port-component"; import { endpointStore } from "../+network-endpoints/endpoints.store"; import { ServiceDetailsEndpoint } from "./service-details-endpoint"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { kubeWatchApi } from "../../api/kube-watch-api"; interface Props extends KubeObjectDetailsProps { } @@ -19,10 +20,11 @@ interface Props extends KubeObjectDetailsProps { @observer export class ServiceDetails extends React.Component { componentDidMount() { - if (!endpointStore.isLoaded) { - endpointStore.loadAll(); - } - endpointApi.watch(); + disposeOnUnmount(this, [ + kubeWatchApi.subscribeStores([endpointStore], { + preload: true, + }), + ]); } render() { @@ -77,7 +79,7 @@ export class ServiceDetails extends React.Component { )} - +
); } diff --git a/src/renderer/components/+nodes/nodes.store.ts b/src/renderer/components/+nodes/nodes.store.ts index c0385b078b..b301015747 100644 --- a/src/renderer/components/+nodes/nodes.store.ts +++ b/src/renderer/components/+nodes/nodes.store.ts @@ -1,3 +1,4 @@ +import { sum } from "lodash"; import { action, computed, observable } from "mobx"; import { clusterApi, IClusterMetrics, INodeMetrics, Node, nodesApi } from "../../api/endpoints"; import { autobind } from "../../utils"; @@ -62,6 +63,10 @@ export class NodesStore extends KubeObjectStore { }); } + getWarningsCount(): number { + return sum(this.items.map((node: Node) => node.getWarningConditions().length)); + } + reset() { super.reset(); this.metrics = {}; diff --git a/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts b/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts index 71890acc44..620fbd86ac 100644 --- a/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts +++ b/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts @@ -9,8 +9,8 @@ import { apiManager } from "../../api/api-manager"; export class RoleBindingsStore extends KubeObjectStore { api = clusterRoleBindingApi; - subscribe() { - return super.subscribe([clusterRoleBindingApi, roleBindingApi]); + getSubscribeApis() { + return [clusterRoleBindingApi, roleBindingApi]; } protected sortItems(items: RoleBinding[]) { diff --git a/src/renderer/components/+user-management-roles/roles.store.ts b/src/renderer/components/+user-management-roles/roles.store.ts index 7d2e90dd38..82b0e66612 100644 --- a/src/renderer/components/+user-management-roles/roles.store.ts +++ b/src/renderer/components/+user-management-roles/roles.store.ts @@ -7,8 +7,8 @@ import { apiManager } from "../../api/api-manager"; export class RolesStore extends KubeObjectStore { api = clusterRoleApi; - subscribe() { - return super.subscribe([roleApi, clusterRoleApi]); + getSubscribeApis() { + return [roleApi, clusterRoleApi]; } protected sortItems(items: Role[]) { diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index 351b57462c..50a25ef87c 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -1,8 +1,7 @@ import "./overview.scss"; import React from "react"; -import { observable, when } from "mobx"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import { OverviewStatuses } from "./overview-statuses"; import { RouteComponentProps } from "react-router"; import { IWorkloadsOverviewRouteParams } from "../+workloads"; @@ -15,60 +14,23 @@ import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { jobStore } from "../+workloads-jobs/job.store"; import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; import { Events } from "../+events"; -import { KubeObjectStore } from "../../kube-object.store"; import { isAllowedResource } from "../../../common/rbac"; -import { namespaceStore } from "../+namespaces/namespace.store"; +import { kubeWatchApi } from "../../api/kube-watch-api"; interface Props extends RouteComponentProps { } @observer export class WorkloadsOverview extends React.Component { - @observable isLoading = false; - @observable isUnmounting = false; - - async componentDidMount() { - const stores: KubeObjectStore[] = [ - isAllowedResource("pods") && podsStore, - isAllowedResource("deployments") && deploymentStore, - isAllowedResource("daemonsets") && daemonSetStore, - isAllowedResource("statefulsets") && statefulSetStore, - isAllowedResource("replicasets") && replicaSetStore, - isAllowedResource("jobs") && jobStore, - isAllowedResource("cronjobs") && cronJobStore, - isAllowedResource("events") && eventStore, - ].filter(Boolean); - - const unsubscribeMap = new Map void>(); - - const loadStores = async () => { - this.isLoading = true; - - for (const store of stores) { - if (this.isUnmounting) break; - - try { - await store.loadAll(); - unsubscribeMap.get(store)?.(); // unsubscribe previous watcher - unsubscribeMap.set(store, store.subscribe()); - } catch (error) { - console.error("loading store error", error); - } - } - this.isLoading = false; - }; - - namespaceStore.onContextChange(loadStores, { - fireImmediately: true, - }); - - await when(() => this.isUnmounting && !this.isLoading); - unsubscribeMap.forEach(dispose => dispose()); - unsubscribeMap.clear(); - } - - componentWillUnmount() { - this.isUnmounting = true; + componentDidMount() { + disposeOnUnmount(this, [ + kubeWatchApi.subscribeStores([ + podsStore, deploymentStore, daemonSetStore, statefulSetStore, replicaSetStore, + jobStore, cronJobStore, eventStore, + ], { + preload: true, + }), + ]); } render() { diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 958ab4b73d..767a905e4a 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import { Redirect, Route, Router, Switch } from "react-router"; import { history } from "../navigation"; import { Notifications } from "./notifications"; @@ -42,10 +42,10 @@ 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 { reaction, computed, observable } from "mobx"; +import { computed, reaction, observable } from "mobx"; import { nodesStore } from "./+nodes/nodes.store"; import { podsStore } from "./+workloads-pods/pods.store"; -import { sum } from "lodash"; +import { kubeWatchApi } from "../api/kube-watch-api"; import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog"; @observer @@ -75,50 +75,26 @@ export class App extends React.Component { whatInput.ask(); // Start to monitor user input device } - @observable extensionRoutes: Map = new Map(); + componentDidMount() { + disposeOnUnmount(this, [ + kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], { + preload: true, + }), - async componentDidMount() { - const cluster = getHostedCluster(); - const promises: Promise[] = []; + reaction(() => this.warningsTotal, (count: number) => { + broadcastMessage(`cluster-warning-event-count:${getHostedCluster().id}`, count); + }), - if (isAllowedResource("events") && isAllowedResource("pods")) { - promises.push(eventStore.loadAll()); - promises.push(podsStore.loadAll()); - } - - if (isAllowedResource("nodes")) { - promises.push(nodesStore.loadAll()); - } - await Promise.all(promises); - - if (eventStore.isLoaded && podsStore.isLoaded) { - eventStore.subscribe(); - podsStore.subscribe(); - } - - if (nodesStore.isLoaded) { - nodesStore.subscribe(); - } - - reaction(() => this.warningsCount, (count) => { - broadcastMessage(`cluster-warning-event-count:${cluster.id}`, count); - }); - - reaction(() => clusterPageMenuRegistry.getRootItems(), (rootItems) => { - this.generateExtensionTabLayoutRoutes(rootItems); - }, { - fireImmediately: true - }); + reaction(() => clusterPageMenuRegistry.getRootItems(), (rootItems) => { + this.generateExtensionTabLayoutRoutes(rootItems); + }, { + fireImmediately: true + }) + ]); } - @computed - get warningsCount() { - let warnings = sum(nodesStore.items - .map(node => node.getWarningConditions().length)); - - warnings = warnings + eventStore.getWarnings().length; - - return warnings; + @computed get warningsTotal(): number { + return nodesStore.getWarningsCount() + eventStore.getWarningsCount(); } get startURL() { @@ -151,6 +127,26 @@ export class App extends React.Component { return routes; } + renderExtensionTabLayoutRoutes() { + return clusterPageMenuRegistry.getRootItems().map((menu, index) => { + const tabRoutes = this.getTabLayoutRoutes(menu); + + if (tabRoutes.length > 0) { + const pageComponent = () => ; + + return tab.routePath)}/>; + } else { + const page = clusterPageRegistry.getByPageTarget(menu.target); + + if (page) { + return ; + } + } + }); + } + + @observable extensionRoutes: Map = new Map(); + generateExtensionTabLayoutRoutes(rootItems: ClusterPageMenuRegistration[]) { rootItems.forEach((menu, index) => { let route = this.extensionRoutes.get(menu); @@ -181,10 +177,6 @@ export class App extends React.Component { } } - renderExtensionTabLayoutRoutes() { - return Array.from(this.extensionRoutes.values()); - } - renderExtensionRoutes() { return clusterPageRegistry.getItems().map((page, index) => { const menu = clusterPageMenuRegistry.getByPage(page); 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 6b4ff4fd16..b13d496064 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -2,7 +2,7 @@ import "./item-list-layout.scss"; import groupBy from "lodash/groupBy"; import React, { ReactNode } from "react"; -import { computed, IReactionDisposer, observable, reaction, toJS } from "mobx"; +import { computed, observable, reaction, toJS } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog"; import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallback } from "../table"; @@ -12,7 +12,6 @@ import { NoItems } from "../no-items"; import { Spinner } from "../spinner"; import { ItemObject, ItemStore } from "../../item.store"; import { SearchInputUrl } from "../input"; -import { namespaceStore } from "../+namespaces/namespace.store"; import { Filter, FilterType, pageFilters } from "./page-filters.store"; import { PageFiltersList } from "./page-filters-list"; import { PageFiltersSelect } from "./page-filters-select"; @@ -22,6 +21,7 @@ import { MenuActions } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Checkbox } from "../checkbox"; import { userStore } from "../../../common/user-store"; +import { namespaceStore } from "../+namespaces/namespace.store"; // todo: refactor, split to small re-usable components @@ -40,6 +40,7 @@ export interface ItemListLayoutProps { className: IClassName; store: ItemStore; dependentStores?: ItemStore[]; + preloadStores?: boolean; isClusterScoped?: boolean; hideFilters?: boolean; searchFilters?: SearchFilter[]; @@ -82,6 +83,7 @@ const defaultProps: Partial = { isSelectable: true, isConfigurable: false, copyClassNameFromHeadCells: true, + preloadStores: true, dependentStores: [], filterItems: [], hasDetailsView: true, @@ -97,10 +99,6 @@ interface ItemListLayoutUserSettings { export class ItemListLayout extends React.Component { static defaultProps = defaultProps as object; - private watchDisposers: IReactionDisposer[] = []; - - @observable isUnmounting = false; - @observable userSettings: ItemListLayoutUserSettings = { showAppliedFilters: false, }; @@ -119,54 +117,28 @@ export class ItemListLayout extends React.Component { } async componentDidMount() { - const { isClusterScoped, isConfigurable, tableId } = this.props; + const { isClusterScoped, isConfigurable, tableId, preloadStores } = this.props; if (isConfigurable && !tableId) { throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified"); } - this.loadStores(); + if (preloadStores) { + this.loadStores(); - if (!isClusterScoped) { - disposeOnUnmount(this, [ - namespaceStore.onContextChange(() => this.loadStores()) - ]); + if (!isClusterScoped) { + disposeOnUnmount(this, [ + namespaceStore.onContextChange(() => this.loadStores()) + ]); + } } } - async componentWillUnmount() { - this.isUnmounting = true; - this.unsubscribeStores(); - } - - @computed get stores() { + private loadStores() { const { store, dependentStores } = this.props; + const stores = Array.from(new Set([store, ...dependentStores])); - return new Set([store, ...dependentStores]); - } - - async loadStores() { - this.unsubscribeStores(); // reset first - - // load - for (const store of this.stores) { - if (this.isUnmounting) { - this.unsubscribeStores(); - break; - } - - try { - await store.loadAll(); - this.watchDisposers.push(store.subscribe()); - } catch (error) { - console.error("loading store error", error); - } - } - } - - unsubscribeStores() { - this.watchDisposers.forEach(dispose => dispose()); - this.watchDisposers.length = 0; + stores.forEach(store => store.loadAll()); } private filterCallbacks: { [type: string]: ItemsFilter } = { diff --git a/src/renderer/components/kube-object/kube-object-list-layout.tsx b/src/renderer/components/kube-object/kube-object-list-layout.tsx index 25922f0f72..226023fc8d 100644 --- a/src/renderer/components/kube-object/kube-object-list-layout.tsx +++ b/src/renderer/components/kube-object/kube-object-list-layout.tsx @@ -1,15 +1,17 @@ import React from "react"; import { computed } from "mobx"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import { cssNames } from "../../utils"; import { KubeObject } from "../../api/kube-object"; import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout"; import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectMenu } from "./kube-object-menu"; import { kubeSelectedUrlParam, showDetails } from "./kube-object-details"; +import { kubeWatchApi } from "../../api/kube-watch-api"; export interface KubeObjectListLayoutProps extends ItemListLayoutProps { store: KubeObjectStore; + dependentStores?: KubeObjectStore[]; } @observer @@ -18,6 +20,17 @@ export class KubeObjectListLayout extends React.Component { if (this.props.onDetails) { this.props.onDetails(item); @@ -33,6 +46,7 @@ export class KubeObjectListLayout extends React.Component { diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index 8a75bc7ae6..760ebd3335 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -2,10 +2,10 @@ import type { Cluster } from "../main/cluster"; import { action, observable, reaction } from "mobx"; import { autobind } from "./utils"; import { KubeObject } from "./api/kube-object"; -import { IKubeWatchEvent, kubeWatchApi } from "./api/kube-watch-api"; +import { IKubeWatchEvent, IKubeWatchMessage, kubeWatchApi } from "./api/kube-watch-api"; import { ItemStore } from "./item.store"; import { apiManager } from "./api/api-manager"; -import { IKubeApiQueryParams, KubeApi } from "./api/kube-api"; +import { IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api"; import { KubeJsonApiData } from "./api/kube-json-api"; export interface KubeObjectStoreLoadingParams { @@ -22,7 +22,6 @@ export abstract class KubeObjectStore extends ItemSt constructor() { super(); this.bindWatchEventsUpdater(); - kubeWatchApi.addListener(this, this.onWatchApiEvent); } get query(): IKubeApiQueryParams { @@ -157,7 +156,7 @@ export abstract class KubeObjectStore extends ItemSt @action async loadFromPath(resourcePath: string) { - const { namespace, name } = KubeApi.parseApi(resourcePath); + const { namespace, name } = parseKubeApi(resourcePath); return this.load({ name, namespace }); } @@ -195,29 +194,29 @@ export abstract class KubeObjectStore extends ItemSt } // collect items from watch-api events to avoid UI blowing up with huge streams of data - protected eventsBuffer = observable>([], { deep: false }); + protected eventsBuffer = observable.array>([], { deep: false }); protected bindWatchEventsUpdater(delay = 1000) { - return reaction(() => this.eventsBuffer.toJS()[0], this.updateFromEventsBuffer, { + kubeWatchApi.onMessage.addListener(({ store, data }: IKubeWatchMessage) => { + if (!this.isLoaded || store !== this) return; + this.eventsBuffer.push(data); + }); + + reaction(() => this.eventsBuffer.length > 0, this.updateFromEventsBuffer, { delay }); } - subscribe(apis = [this.api]) { - return KubeApi.watchAll(...apis); + getSubscribeApis(): KubeApi[] { + return [this.api]; } - protected onWatchApiEvent(evt: IKubeWatchEvent) { - if (!this.isLoaded) return; - this.eventsBuffer.push(evt); + subscribe(apis = this.getSubscribeApis()) { + return kubeWatchApi.subscribeApi(apis); } @action protected updateFromEventsBuffer() { - if (!this.eventsBuffer.length) { - return; - } - // create latest non-observable copy of items to apply updates in one action (==single render) const items = this.items.toJS(); for (const { type, object } of this.eventsBuffer.clear()) { From 1599ee4f6abffe0c3d914e5ef32d3244299319f4 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 1 Feb 2021 16:51:27 +0200 Subject: [PATCH 25/56] Add deb & rpm packages (#2053) Signed-off-by: Jari Kolehmainen --- integration/helpers/utils.ts | 2 +- package.json | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index a865280fed..195de2d073 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -4,7 +4,7 @@ import { exec } from "child_process"; const AppPaths: Partial> = { "win32": "./dist/win-unpacked/Lens.exe", - "linux": "./dist/linux-unpacked/kontena-lens", + "linux": "./dist/linux-unpacked/lens", "darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens", }; diff --git a/package.json b/package.json index 80d3c1d229..735a5fa341 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,11 @@ ], "linux": { "category": "Network", + "executableName": "lens", + "artifactName": "${productName}-${version}.${arch}.${ext}", "target": [ + "deb", + "rpm", "snap", "AppImage" ], From f8412eaf13a658a3e01ce2d3e041800adda7576e Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 2 Feb 2021 09:32:41 +0200 Subject: [PATCH 26/56] Add release-drafter (#2055) * add release-drafter Signed-off-by: Jari Kolehmainen * add release-drafter Signed-off-by: Jari Kolehmainen * fix token Signed-off-by: Jari Kolehmainen --- .github/release-drafter.yml | 14 ++++++++++++++ .github/workflows/release-drafter.yml | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/release-drafter.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000000..606bf962a6 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,14 @@ +categories: + - title: '🚀 Features' + labels: + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'bug' + - title: '🧰 Maintenance' + label: 'chore' + +template: | + ## Changes since $PREVIOUS_TAG + + $CHANGES diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000000..8c3183b988 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,19 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + with: + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + # config-name: my-config.yml + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} From 06b884f3fba26add72d915c1f8868b88d69eb3fc Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 2 Feb 2021 09:42:14 +0200 Subject: [PATCH 27/56] Fix release-drafter yaml error (#2058) Signed-off-by: Jari Kolehmainen --- .github/workflows/release-drafter.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 8c3183b988..ec49fec6e5 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -12,8 +12,5 @@ jobs: steps: # Drafts your next Release notes as Pull Requests are merged into "master" - uses: release-drafter/release-drafter@v5 - with: - # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml - # config-name: my-config.yml env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} From 9e1487685ce98255204428862870a7c97e017cc6 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Tue, 2 Feb 2021 11:45:45 +0300 Subject: [PATCH 28/56] Fix: charts stripes alignment (#2054) * Align chart stripe on every dataset update Signed-off-by: Alex Andreev * Removing default stripes interval Signed-off-by: Alex Andreev --- src/renderer/components/chart/bar-chart.tsx | 3 ++- .../components/chart/zebra-stripes.plugin.ts | 23 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/renderer/components/chart/bar-chart.tsx b/src/renderer/components/chart/bar-chart.tsx index 4f80258703..19ef031ba6 100644 --- a/src/renderer/components/chart/bar-chart.tsx +++ b/src/renderer/components/chart/bar-chart.tsx @@ -136,7 +136,8 @@ export class BarChart extends React.Component { }, plugins: { ZebraStripes: { - stripeColor: chartStripesColor + stripeColor: chartStripesColor, + interval: chartData.datasets[0].data.length } } }; diff --git a/src/renderer/components/chart/zebra-stripes.plugin.ts b/src/renderer/components/chart/zebra-stripes.plugin.ts index 3a85f8d0a2..f190401066 100644 --- a/src/renderer/components/chart/zebra-stripes.plugin.ts +++ b/src/renderer/components/chart/zebra-stripes.plugin.ts @@ -6,8 +6,6 @@ import moment, { Moment } from "moment"; import get from "lodash/get"; const defaultOptions = { - interval: 61, - stripeMinutes: 10, stripeColor: "#ffffff08", }; @@ -36,12 +34,23 @@ export const ZebraStripes = { chart.canvas.parentElement.removeChild(elem); }, + updateOptions(chart: ChartJS) { + this.options = { + ...defaultOptions, + ...this.getOptions(chart) + }; + }, + + getStripeMinutes() { + return this.options.interval < 10 ? 0 : 10; + }, + renderStripes(chart: ChartJS) { if (!chart.data.datasets.length) return; - const { interval, stripeMinutes, stripeColor } = this.options; + const { interval, stripeColor } = this.options; const { top, left, bottom, right } = chart.chartArea; const step = (right - left) / interval; - const stripeWidth = step * stripeMinutes; + const stripeWidth = step * this.getStripeMinutes(); const cover = document.createElement("div"); const styles = cover.style; @@ -61,14 +70,12 @@ export const ZebraStripes = { afterInit(chart: ChartJS) { if (!chart.data.datasets.length) return; - this.options = { - ...defaultOptions, - ...this.getOptions(chart) - }; + this.updateOptions(chart); this.updated = this.getLastUpdate(chart); }, afterUpdate(chart: ChartJS) { + this.updateOptions(chart); this.renderStripes(chart); }, From b5e7be759100f20794e86594b9105eda761379e2 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 2 Feb 2021 12:34:13 +0200 Subject: [PATCH 29/56] Initial command palette feature (#1957) * wip: command palette Signed-off-by: Jari Kolehmainen * register shortcut to global menu Signed-off-by: Jari Kolehmainen * introduce openCommandDialog & closeCommandDialog Signed-off-by: Jari Kolehmainen * fix ipc broadcast to frames from renderer Signed-off-by: Jari Kolehmainen * tweak Signed-off-by: Jari Kolehmainen * add more commands Signed-off-by: Jari Kolehmainen * cleanup Signed-off-by: Jari Kolehmainen * ipc fix Signed-off-by: Jari Kolehmainen * add integration tests Signed-off-by: Jari Kolehmainen * ipc fix Signed-off-by: Jari Kolehmainen * implement workspace edit Signed-off-by: Jari Kolehmainen * workspace edit fixes Signed-off-by: Jari Kolehmainen * make tests green Signed-off-by: Jari Kolehmainen * fixes from code review Signed-off-by: Jari Kolehmainen * cleanup ipc Signed-off-by: Jari Kolehmainen * cleanup CommandRegistry Signed-off-by: Jari Kolehmainen * ipc fix Signed-off-by: Jari Kolehmainen * fix ClusterManager cluster auto-init Signed-off-by: Jari Kolehmainen * ensure cluster view is active before sending a command Signed-off-by: Jari Kolehmainen * switch to last active cluster when workspace change Signed-off-by: Jari Kolehmainen * tweak integration tests Signed-off-by: Jari Kolehmainen * run integration tests serially Signed-off-by: Jari Kolehmainen * cleanup Signed-off-by: Jari Kolehmainen * fixes based on code review Signed-off-by: Jari Kolehmainen * cleanup Signed-off-by: Jari Kolehmainen * cleanup more Signed-off-by: Jari Kolehmainen * cleanup Signed-off-by: Jari Kolehmainen * add workspace fixes Signed-off-by: Jari Kolehmainen * cleanup Signed-off-by: Jari Kolehmainen --- integration/__tests__/app.tests.ts | 554 +----------------- integration/__tests__/cluster-pages.tests.ts | 450 ++++++++++++++ .../__tests__/command-palette.tests.ts | 25 + integration/__tests__/workspace.tests.ts | 75 +++ integration/helpers/minikube.ts | 59 ++ integration/helpers/utils.ts | 32 +- package.json | 2 +- src/common/ipc.ts | 52 +- src/extensions/lens-renderer-extension.ts | 2 + src/extensions/registries/command-registry.ts | 37 ++ src/extensions/renderer-api/components.ts | 3 + src/main/cluster-manager.ts | 8 +- src/main/index.ts | 3 + src/main/menu.ts | 9 + src/renderer/components/+apps/apps.command.ts | 18 + src/renderer/components/+apps/index.ts | 1 + .../cluster-settings.command.ts | 16 + .../components/cluster-workspace-setting.tsx | 7 +- .../components/+cluster-settings/index.ts | 1 + .../components/+config/config.command.ts | 50 ++ src/renderer/components/+config/index.ts | 1 + src/renderer/components/+network/index.ts | 1 + .../components/+network/network.command.ts | 34 ++ src/renderer/components/+nodes/index.ts | 1 + .../components/+nodes/node.command.ts | 10 + .../+preferences/preferences.route.ts | 9 + src/renderer/components/+workloads/index.ts | 1 + .../+workloads/workloads.command.ts | 45 ++ .../components/+workspaces/add-workspace.tsx | 64 ++ .../components/+workspaces/edit-workspace.tsx | 82 +++ src/renderer/components/+workspaces/index.ts | 1 - .../+workspaces/remove-workspace.tsx | 68 +++ .../+workspaces/workspace-menu.scss | 7 - .../components/+workspaces/workspace-menu.tsx | 66 --- .../+workspaces/workspaces.route.ts | 8 - .../components/+workspaces/workspaces.scss | 14 - .../components/+workspaces/workspaces.tsx | 255 ++------ src/renderer/components/app.tsx | 4 + .../components/cluster-manager/bottom-bar.tsx | 10 +- .../cluster-manager/cluster-manager.tsx | 2 - .../cluster-manager/clusters-menu.tsx | 42 ++ .../command-palette/command-container.scss | 11 + .../command-palette/command-container.tsx | 87 +++ .../command-palette/command-dialog.tsx | 88 +++ .../components/command-palette/index.ts | 2 + src/renderer/components/dock/dock.tsx | 9 + src/renderer/components/input/input.tsx | 2 +- src/renderer/lens-app.tsx | 2 + 48 files changed, 1446 insertions(+), 884 deletions(-) create mode 100644 integration/__tests__/cluster-pages.tests.ts create mode 100644 integration/__tests__/command-palette.tests.ts create mode 100644 integration/__tests__/workspace.tests.ts create mode 100644 integration/helpers/minikube.ts create mode 100644 src/extensions/registries/command-registry.ts create mode 100644 src/renderer/components/+apps/apps.command.ts create mode 100644 src/renderer/components/+cluster-settings/cluster-settings.command.ts create mode 100644 src/renderer/components/+config/config.command.ts create mode 100644 src/renderer/components/+network/network.command.ts create mode 100644 src/renderer/components/+nodes/node.command.ts create mode 100644 src/renderer/components/+workloads/workloads.command.ts create mode 100644 src/renderer/components/+workspaces/add-workspace.tsx create mode 100644 src/renderer/components/+workspaces/edit-workspace.tsx create mode 100644 src/renderer/components/+workspaces/remove-workspace.tsx delete mode 100644 src/renderer/components/+workspaces/workspace-menu.scss delete mode 100644 src/renderer/components/+workspaces/workspace-menu.tsx delete mode 100644 src/renderer/components/+workspaces/workspaces.route.ts delete mode 100644 src/renderer/components/+workspaces/workspaces.scss create mode 100644 src/renderer/components/command-palette/command-container.scss create mode 100644 src/renderer/components/command-palette/command-container.tsx create mode 100644 src/renderer/components/command-palette/command-dialog.tsx create mode 100644 src/renderer/components/command-palette/index.ts diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index ca30015fa1..af029a23e9 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -1,12 +1,5 @@ -/* - Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE - namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the - TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube - cluster and vice versa. -*/ import { Application } from "spectron"; import * as utils from "../helpers/utils"; -import { spawnSync } from "child_process"; import { listHelmRepositories } from "../helpers/utils"; import { fail } from "assert"; @@ -15,62 +8,10 @@ jest.setTimeout(60000); // FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) describe("Lens integration tests", () => { - const TEST_NAMESPACE = "integration-tests"; - const BACKSPACE = "\uE003"; let app: Application; - const appStart = async () => { - app = utils.setup(); - await app.start(); - // Wait for splash screen to be closed - while (await app.client.getWindowCount() > 1); - await app.client.windowByIndex(0); - await app.client.waitUntilWindowLoaded(); - }; - const clickWhatsNew = async (app: Application) => { - await app.client.waitUntilTextExists("h1", "What's new?"); - await app.client.click("button.primary"); - await app.client.waitUntilTextExists("h1", "Welcome"); - }; - const minikubeReady = (): boolean => { - // determine if minikube is running - { - const { status } = spawnSync("minikube status", { shell: true }); - - if (status !== 0) { - console.warn("minikube not running"); - - return false; - } - } - - // Remove TEST_NAMESPACE if it already exists - { - const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }); - - if (status === 0) { - console.warn(`Removing existing ${TEST_NAMESPACE} namespace`); - - const { status, stdout, stderr } = spawnSync( - `minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, - { shell: true }, - ); - - if (status !== 0) { - console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`); - - return false; - } - - console.log(stdout.toString()); - } - } - - return true; - }; - const ready = minikubeReady(); describe("app start", () => { - beforeAll(appStart, 20000); + beforeAll(async () => app = await utils.appStart(), 20000); afterAll(async () => { if (app?.isRunning()) { @@ -79,7 +20,7 @@ describe("Lens integration tests", () => { }); it('shows "whats new"', async () => { - await clickWhatsNew(app); + await utils.clickWhatsNew(app); }); it('shows "add cluster"', async () => { @@ -113,495 +54,4 @@ describe("Lens integration tests", () => { await app.client.keys("Meta"); }); }); - - utils.describeIf(ready)("workspaces", () => { - beforeAll(appStart, 20000); - - afterAll(async () => { - if (app && app.isRunning()) { - return utils.tearDown(app); - } - }); - - it("creates new workspace", async () => { - await clickWhatsNew(app); - await app.client.click("#current-workspace .Icon"); - await app.client.click('a[href="/workspaces"]'); - await app.client.click(".Workspaces button.Button"); - await app.client.keys("test-workspace"); - await app.client.click(".Workspaces .Input.description input"); - await app.client.keys("test description"); - await app.client.click(".Workspaces .workspace.editing .Icon"); - await app.client.waitUntilTextExists(".workspace .name a", "test-workspace"); - }); - - it("adds cluster in default workspace", async () => { - await addMinikubeCluster(app); - await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); - await app.client.waitForExist(`iframe[name="minikube"]`); - await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active"); - }); - - it("adds cluster in test-workspace", async () => { - await app.client.click("#current-workspace .Icon"); - await app.client.waitForVisible('.WorkspaceMenu li[title="test description"]'); - await app.client.click('.WorkspaceMenu li[title="test description"]'); - await addMinikubeCluster(app); - await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); - await app.client.waitForExist(`iframe[name="minikube"]`); - }); - - it("checks if default workspace has active cluster", async () => { - await app.client.click("#current-workspace .Icon"); - await app.client.waitForVisible(".WorkspaceMenu > li:first-of-type"); - await app.client.click(".WorkspaceMenu > li:first-of-type"); - await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active"); - }); - }); - - const addMinikubeCluster = async (app: Application) => { - await app.client.click("div.add-cluster"); - await app.client.waitUntilTextExists("div", "Select kubeconfig file"); - await app.client.click("div.Select__control"); // show the context drop-down list - await app.client.waitUntilTextExists("div", "minikube"); - - if (!await app.client.$("button.primary").isEnabled()) { - await app.client.click("div.minikube"); // select minikube context - } // else the only context, which must be 'minikube', is automatically selected - await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button) - await app.client.click("button.primary"); // add minikube cluster - }; - const waitForMinikubeDashboard = async (app: Application) => { - await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); - await app.client.waitForExist(`iframe[name="minikube"]`); - await app.client.frame("minikube"); - await app.client.waitUntilTextExists("span.link-text", "Cluster"); - }; - - utils.describeIf(ready)("cluster tests", () => { - let clusterAdded = false; - const addCluster = async () => { - await clickWhatsNew(app); - await addMinikubeCluster(app); - await waitForMinikubeDashboard(app); - await app.client.click('a[href="/nodes"]'); - await app.client.waitUntilTextExists("div.TableCell", "Ready"); - }; - - describe("cluster add", () => { - beforeAll(appStart, 20000); - - afterAll(async () => { - if (app && app.isRunning()) { - return utils.tearDown(app); - } - }); - - it("allows to add a cluster", async () => { - await addCluster(); - clusterAdded = true; - }); - }); - - const appStartAddCluster = async () => { - if (clusterAdded) { - await appStart(); - await addCluster(); - } - }; - - describe("cluster pages", () => { - - beforeAll(appStartAddCluster, 40000); - - afterAll(async () => { - if (app && app.isRunning()) { - return utils.tearDown(app); - } - }); - - const tests: { - drawer?: string - drawerId?: string - pages: { - name: string, - href: string, - expectedSelector: string, - expectedText: string - }[] - }[] = [{ - drawer: "", - drawerId: "", - pages: [{ - name: "Cluster", - href: "cluster", - expectedSelector: "div.ClusterOverview div.label", - expectedText: "Master" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Nodes", - href: "nodes", - expectedSelector: "h5.title", - expectedText: "Nodes" - }] - }, - { - drawer: "Workloads", - drawerId: "workloads", - pages: [{ - name: "Overview", - href: "workloads", - expectedSelector: "h5.box", - expectedText: "Overview" - }, - { - name: "Pods", - href: "pods", - expectedSelector: "h5.title", - expectedText: "Pods" - }, - { - name: "Deployments", - href: "deployments", - expectedSelector: "h5.title", - expectedText: "Deployments" - }, - { - name: "DaemonSets", - href: "daemonsets", - expectedSelector: "h5.title", - expectedText: "Daemon Sets" - }, - { - name: "StatefulSets", - href: "statefulsets", - expectedSelector: "h5.title", - expectedText: "Stateful Sets" - }, - { - name: "ReplicaSets", - href: "replicasets", - expectedSelector: "h5.title", - expectedText: "Replica Sets" - }, - { - name: "Jobs", - href: "jobs", - expectedSelector: "h5.title", - expectedText: "Jobs" - }, - { - name: "CronJobs", - href: "cronjobs", - expectedSelector: "h5.title", - expectedText: "Cron Jobs" - }] - }, - { - drawer: "Configuration", - drawerId: "config", - pages: [{ - name: "ConfigMaps", - href: "configmaps", - expectedSelector: "h5.title", - expectedText: "Config Maps" - }, - { - name: "Secrets", - href: "secrets", - expectedSelector: "h5.title", - expectedText: "Secrets" - }, - { - name: "Resource Quotas", - href: "resourcequotas", - expectedSelector: "h5.title", - expectedText: "Resource Quotas" - }, - { - name: "Limit Ranges", - href: "limitranges", - expectedSelector: "h5.title", - expectedText: "Limit Ranges" - }, - { - name: "HPA", - href: "hpa", - expectedSelector: "h5.title", - expectedText: "Horizontal Pod Autoscalers" - }, - { - name: "Pod Disruption Budgets", - href: "poddisruptionbudgets", - expectedSelector: "h5.title", - expectedText: "Pod Disruption Budgets" - }] - }, - { - drawer: "Network", - drawerId: "networks", - pages: [{ - name: "Services", - href: "services", - expectedSelector: "h5.title", - expectedText: "Services" - }, - { - name: "Endpoints", - href: "endpoints", - expectedSelector: "h5.title", - expectedText: "Endpoints" - }, - { - name: "Ingresses", - href: "ingresses", - expectedSelector: "h5.title", - expectedText: "Ingresses" - }, - { - name: "Network Policies", - href: "network-policies", - expectedSelector: "h5.title", - expectedText: "Network Policies" - }] - }, - { - drawer: "Storage", - drawerId: "storage", - pages: [{ - name: "Persistent Volume Claims", - href: "persistent-volume-claims", - expectedSelector: "h5.title", - expectedText: "Persistent Volume Claims" - }, - { - name: "Persistent Volumes", - href: "persistent-volumes", - expectedSelector: "h5.title", - expectedText: "Persistent Volumes" - }, - { - name: "Storage Classes", - href: "storage-classes", - expectedSelector: "h5.title", - expectedText: "Storage Classes" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Namespaces", - href: "namespaces", - expectedSelector: "h5.title", - expectedText: "Namespaces" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Events", - href: "events", - expectedSelector: "h5.title", - expectedText: "Events" - }] - }, - { - drawer: "Apps", - drawerId: "apps", - pages: [{ - name: "Charts", - href: "apps/charts", - expectedSelector: "div.HelmCharts input", - expectedText: "" - }, - { - name: "Releases", - href: "apps/releases", - expectedSelector: "h5.title", - expectedText: "Releases" - }] - }, - { - drawer: "Access Control", - drawerId: "users", - pages: [{ - name: "Service Accounts", - href: "service-accounts", - expectedSelector: "h5.title", - expectedText: "Service Accounts" - }, - { - name: "Role Bindings", - href: "role-bindings", - expectedSelector: "h5.title", - expectedText: "Role Bindings" - }, - { - name: "Roles", - href: "roles", - expectedSelector: "h5.title", - expectedText: "Roles" - }, - { - name: "Pod Security Policies", - href: "pod-security-policies", - expectedSelector: "h5.title", - expectedText: "Pod Security Policies" - }] - }, - { - drawer: "Custom Resources", - drawerId: "custom-resources", - pages: [{ - name: "Definitions", - href: "crd/definitions", - expectedSelector: "h5.title", - expectedText: "Custom Resources" - }] - }]; - - tests.forEach(({ drawer = "", drawerId = "", pages }) => { - if (drawer !== "") { - it(`shows ${drawer} drawer`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`); - await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name); - }); - } - pages.forEach(({ name, href, expectedSelector, expectedText }) => { - it(`shows ${drawer}->${name} page`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`a[href^="/${href}"]`); - await app.client.waitUntilTextExists(expectedSelector, expectedText); - }); - }); - - if (drawer !== "") { - // hide the drawer - it(`hides ${drawer} drawer`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`); - await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow(); - }); - } - }); - }); - - describe("viewing pod logs", () => { - beforeEach(appStartAddCluster, 40000); - - afterEach(async () => { - if (app && app.isRunning()) { - return utils.tearDown(app); - } - }); - - it(`shows a logs for a pod`, async () => { - expect(clusterAdded).toBe(true); - // Go to Pods page - await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); - await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); - await app.client.click('a[href^="/pods"]'); - await app.client.click(".NamespaceSelect"); - await app.client.keys("kube-system"); - await app.client.keys("Enter");// "\uE007" - await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); - let podMenuItemEnabled = false; - - // Wait until extensions are enabled on renderer - while (!podMenuItemEnabled) { - const logs = await app.client.getRenderProcessLogs(); - - podMenuItemEnabled = !!logs.find(entry => entry.message.includes("[EXTENSION]: enabled lens-pod-menu@")); - - if (!podMenuItemEnabled) { - await new Promise(r => setTimeout(r, 1000)); - } - } - await new Promise(r => setTimeout(r, 500)); // Give some extra time to prepare extensions - // Open logs tab in dock - await app.client.click(".list .TableRow:first-child"); - await app.client.waitForVisible(".Drawer"); - await app.client.click(".drawer-title .Menu li:nth-child(2)"); - // Check if controls are available - await app.client.waitForVisible(".LogList .VirtualList"); - await app.client.waitForVisible(".LogResourceSelector"); - //await app.client.waitForVisible(".LogSearch .SearchInput"); - await app.client.waitForVisible(".LogSearch .SearchInput input"); - // Search for semicolon - await app.client.keys(":"); - await app.client.waitForVisible(".LogList .list span.active"); - // Click through controls - await app.client.click(".LogControls .show-timestamps"); - await app.client.click(".LogControls .show-previous"); - }); - }); - - describe("cluster operations", () => { - beforeEach(appStartAddCluster, 40000); - - afterEach(async () => { - if (app && app.isRunning()) { - return utils.tearDown(app); - } - }); - - it("shows default namespace", async () => { - expect(clusterAdded).toBe(true); - await app.client.click('a[href="/namespaces"]'); - await app.client.waitUntilTextExists("div.TableCell", "default"); - await app.client.waitUntilTextExists("div.TableCell", "kube-system"); - }); - - it(`creates ${TEST_NAMESPACE} namespace`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click('a[href="/namespaces"]'); - await app.client.waitUntilTextExists("div.TableCell", "default"); - await app.client.waitUntilTextExists("div.TableCell", "kube-system"); - await app.client.click("button.add-button"); - await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace"); - await app.client.keys(`${TEST_NAMESPACE}\n`); - await app.client.waitForExist(`.name=${TEST_NAMESPACE}`); - }); - - it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); - await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); - await app.client.click('a[href^="/pods"]'); - - await app.client.click(".NamespaceSelect"); - await app.client.keys(TEST_NAMESPACE); - await app.client.keys("Enter");// "\uE007" - await app.client.click(".Icon.new-dock-tab"); - await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource"); - await app.client.click("li.MenuItem.create-resource-tab"); - await app.client.waitForVisible(".CreateResource div.ace_content"); - // Write pod manifest to editor - await app.client.keys("apiVersion: v1\n"); - await app.client.keys("kind: Pod\n"); - await app.client.keys("metadata:\n"); - await app.client.keys(" name: nginx-create-pod-test\n"); - await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`); - await app.client.keys(`${BACKSPACE}spec:\n`); - await app.client.keys(" containers:\n"); - await app.client.keys("- name: nginx-create-pod-test\n"); - await app.client.keys(" image: nginx:alpine\n"); - // Create deployment - await app.client.waitForEnabled("button.Button=Create & Close"); - await app.client.click("button.Button=Create & Close"); - // Wait until first bits of pod appears on dashboard - await app.client.waitForExist(".name=nginx-create-pod-test"); - // Open pod details - await app.client.click(".name=nginx-create-pod-test"); - await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test"); - }); - }); - }); }); diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts new file mode 100644 index 0000000000..e73774f86a --- /dev/null +++ b/integration/__tests__/cluster-pages.tests.ts @@ -0,0 +1,450 @@ +/* + Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE + namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the + TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube + cluster and vice versa. +*/ +import { Application } from "spectron"; +import * as utils from "../helpers/utils"; +import { addMinikubeCluster, minikubeReady, waitForMinikubeDashboard } from "../helpers/minikube"; +import { exec } from "child_process"; +import * as util from "util"; + +export const promiseExec = util.promisify(exec); + +jest.setTimeout(60000); + +// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) +describe("Lens cluster pages", () => { + const TEST_NAMESPACE = "integration-tests"; + const BACKSPACE = "\uE003"; + let app: Application; + const ready = minikubeReady(TEST_NAMESPACE); + + utils.describeIf(ready)("test common pages", () => { + let clusterAdded = false; + const addCluster = async () => { + await utils.clickWhatsNew(app); + await addMinikubeCluster(app); + await waitForMinikubeDashboard(app); + await app.client.click('a[href="/nodes"]'); + await app.client.waitUntilTextExists("div.TableCell", "Ready"); + }; + + describe("cluster add", () => { + beforeAll(async () => app = await utils.appStart(), 20000); + + afterAll(async () => { + if (app && app.isRunning()) { + return utils.tearDown(app); + } + }); + + it("allows to add a cluster", async () => { + await addCluster(); + clusterAdded = true; + }); + }); + + const appStartAddCluster = async () => { + if (clusterAdded) { + app = await utils.appStart(); + await addCluster(); + } + }; + + describe("cluster pages", () => { + + beforeAll(appStartAddCluster, 40000); + + afterAll(async () => { + if (app && app.isRunning()) { + return utils.tearDown(app); + } + }); + + const tests: { + drawer?: string + drawerId?: string + pages: { + name: string, + href: string, + expectedSelector: string, + expectedText: string + }[] + }[] = [{ + drawer: "", + drawerId: "", + pages: [{ + name: "Cluster", + href: "cluster", + expectedSelector: "div.ClusterOverview div.label", + expectedText: "Master" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Nodes", + href: "nodes", + expectedSelector: "h5.title", + expectedText: "Nodes" + }] + }, + { + drawer: "Workloads", + drawerId: "workloads", + pages: [{ + name: "Overview", + href: "workloads", + expectedSelector: "h5.box", + expectedText: "Overview" + }, + { + name: "Pods", + href: "pods", + expectedSelector: "h5.title", + expectedText: "Pods" + }, + { + name: "Deployments", + href: "deployments", + expectedSelector: "h5.title", + expectedText: "Deployments" + }, + { + name: "DaemonSets", + href: "daemonsets", + expectedSelector: "h5.title", + expectedText: "Daemon Sets" + }, + { + name: "StatefulSets", + href: "statefulsets", + expectedSelector: "h5.title", + expectedText: "Stateful Sets" + }, + { + name: "ReplicaSets", + href: "replicasets", + expectedSelector: "h5.title", + expectedText: "Replica Sets" + }, + { + name: "Jobs", + href: "jobs", + expectedSelector: "h5.title", + expectedText: "Jobs" + }, + { + name: "CronJobs", + href: "cronjobs", + expectedSelector: "h5.title", + expectedText: "Cron Jobs" + }] + }, + { + drawer: "Configuration", + drawerId: "config", + pages: [{ + name: "ConfigMaps", + href: "configmaps", + expectedSelector: "h5.title", + expectedText: "Config Maps" + }, + { + name: "Secrets", + href: "secrets", + expectedSelector: "h5.title", + expectedText: "Secrets" + }, + { + name: "Resource Quotas", + href: "resourcequotas", + expectedSelector: "h5.title", + expectedText: "Resource Quotas" + }, + { + name: "Limit Ranges", + href: "limitranges", + expectedSelector: "h5.title", + expectedText: "Limit Ranges" + }, + { + name: "HPA", + href: "hpa", + expectedSelector: "h5.title", + expectedText: "Horizontal Pod Autoscalers" + }, + { + name: "Pod Disruption Budgets", + href: "poddisruptionbudgets", + expectedSelector: "h5.title", + expectedText: "Pod Disruption Budgets" + }] + }, + { + drawer: "Network", + drawerId: "networks", + pages: [{ + name: "Services", + href: "services", + expectedSelector: "h5.title", + expectedText: "Services" + }, + { + name: "Endpoints", + href: "endpoints", + expectedSelector: "h5.title", + expectedText: "Endpoints" + }, + { + name: "Ingresses", + href: "ingresses", + expectedSelector: "h5.title", + expectedText: "Ingresses" + }, + { + name: "Network Policies", + href: "network-policies", + expectedSelector: "h5.title", + expectedText: "Network Policies" + }] + }, + { + drawer: "Storage", + drawerId: "storage", + pages: [{ + name: "Persistent Volume Claims", + href: "persistent-volume-claims", + expectedSelector: "h5.title", + expectedText: "Persistent Volume Claims" + }, + { + name: "Persistent Volumes", + href: "persistent-volumes", + expectedSelector: "h5.title", + expectedText: "Persistent Volumes" + }, + { + name: "Storage Classes", + href: "storage-classes", + expectedSelector: "h5.title", + expectedText: "Storage Classes" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Namespaces", + href: "namespaces", + expectedSelector: "h5.title", + expectedText: "Namespaces" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Events", + href: "events", + expectedSelector: "h5.title", + expectedText: "Events" + }] + }, + { + drawer: "Apps", + drawerId: "apps", + pages: [{ + name: "Charts", + href: "apps/charts", + expectedSelector: "div.HelmCharts input", + expectedText: "" + }, + { + name: "Releases", + href: "apps/releases", + expectedSelector: "h5.title", + expectedText: "Releases" + }] + }, + { + drawer: "Access Control", + drawerId: "users", + pages: [{ + name: "Service Accounts", + href: "service-accounts", + expectedSelector: "h5.title", + expectedText: "Service Accounts" + }, + { + name: "Role Bindings", + href: "role-bindings", + expectedSelector: "h5.title", + expectedText: "Role Bindings" + }, + { + name: "Roles", + href: "roles", + expectedSelector: "h5.title", + expectedText: "Roles" + }, + { + name: "Pod Security Policies", + href: "pod-security-policies", + expectedSelector: "h5.title", + expectedText: "Pod Security Policies" + }] + }, + { + drawer: "Custom Resources", + drawerId: "custom-resources", + pages: [{ + name: "Definitions", + href: "crd/definitions", + expectedSelector: "h5.title", + expectedText: "Custom Resources" + }] + }]; + + tests.forEach(({ drawer = "", drawerId = "", pages }) => { + if (drawer !== "") { + it(`shows ${drawer} drawer`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`); + await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name); + }); + } + pages.forEach(({ name, href, expectedSelector, expectedText }) => { + it(`shows ${drawer}->${name} page`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(`a[href^="/${href}"]`); + await app.client.waitUntilTextExists(expectedSelector, expectedText); + }); + }); + + if (drawer !== "") { + // hide the drawer + it(`hides ${drawer} drawer`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`); + await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow(); + }); + } + }); + }); + + describe("viewing pod logs", () => { + beforeEach(appStartAddCluster, 40000); + + afterEach(async () => { + if (app && app.isRunning()) { + return utils.tearDown(app); + } + }); + + it(`shows a logs for a pod`, async () => { + expect(clusterAdded).toBe(true); + // Go to Pods page + await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); + await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); + await app.client.click('a[href^="/pods"]'); + await app.client.click(".NamespaceSelect"); + await app.client.keys("kube-system"); + await app.client.keys("Enter");// "\uE007" + await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); + let podMenuItemEnabled = false; + + // Wait until extensions are enabled on renderer + while (!podMenuItemEnabled) { + const logs = await app.client.getRenderProcessLogs(); + + podMenuItemEnabled = !!logs.find(entry => entry.message.includes("[EXTENSION]: enabled lens-pod-menu@")); + + if (!podMenuItemEnabled) { + await new Promise(r => setTimeout(r, 1000)); + } + } + await new Promise(r => setTimeout(r, 500)); // Give some extra time to prepare extensions + // Open logs tab in dock + await app.client.click(".list .TableRow:first-child"); + await app.client.waitForVisible(".Drawer"); + await app.client.click(".drawer-title .Menu li:nth-child(2)"); + // Check if controls are available + await app.client.waitForVisible(".LogList .VirtualList"); + await app.client.waitForVisible(".LogResourceSelector"); + //await app.client.waitForVisible(".LogSearch .SearchInput"); + await app.client.waitForVisible(".LogSearch .SearchInput input"); + // Search for semicolon + await app.client.keys(":"); + await app.client.waitForVisible(".LogList .list span.active"); + // Click through controls + await app.client.click(".LogControls .show-timestamps"); + await app.client.click(".LogControls .show-previous"); + }); + }); + + describe("cluster operations", () => { + beforeEach(appStartAddCluster, 40000); + + afterEach(async () => { + if (app && app.isRunning()) { + return utils.tearDown(app); + } + }); + + it("shows default namespace", async () => { + expect(clusterAdded).toBe(true); + await app.client.click('a[href="/namespaces"]'); + await app.client.waitUntilTextExists("div.TableCell", "default"); + await app.client.waitUntilTextExists("div.TableCell", "kube-system"); + }); + + it(`creates ${TEST_NAMESPACE} namespace`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click('a[href="/namespaces"]'); + await app.client.waitUntilTextExists("div.TableCell", "default"); + await app.client.waitUntilTextExists("div.TableCell", "kube-system"); + await app.client.click("button.add-button"); + await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace"); + await app.client.keys(`${TEST_NAMESPACE}\n`); + await app.client.waitForExist(`.name=${TEST_NAMESPACE}`); + }); + + it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); + await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); + await app.client.click('a[href^="/pods"]'); + + await app.client.click(".NamespaceSelect"); + await app.client.keys(TEST_NAMESPACE); + await app.client.keys("Enter");// "\uE007" + await app.client.click(".Icon.new-dock-tab"); + await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource"); + await app.client.click("li.MenuItem.create-resource-tab"); + await app.client.waitForVisible(".CreateResource div.ace_content"); + // Write pod manifest to editor + await app.client.keys("apiVersion: v1\n"); + await app.client.keys("kind: Pod\n"); + await app.client.keys("metadata:\n"); + await app.client.keys(" name: nginx-create-pod-test\n"); + await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`); + await app.client.keys(`${BACKSPACE}spec:\n`); + await app.client.keys(" containers:\n"); + await app.client.keys("- name: nginx-create-pod-test\n"); + await app.client.keys(" image: nginx:alpine\n"); + // Create deployment + await app.client.waitForEnabled("button.Button=Create & Close"); + await app.client.click("button.Button=Create & Close"); + // Wait until first bits of pod appears on dashboard + await app.client.waitForExist(".name=nginx-create-pod-test"); + // Open pod details + await app.client.click(".name=nginx-create-pod-test"); + await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test"); + }); + }); + }); +}); diff --git a/integration/__tests__/command-palette.tests.ts b/integration/__tests__/command-palette.tests.ts new file mode 100644 index 0000000000..789806c445 --- /dev/null +++ b/integration/__tests__/command-palette.tests.ts @@ -0,0 +1,25 @@ +import { Application } from "spectron"; +import * as utils from "../helpers/utils"; + +jest.setTimeout(60000); + +describe("Lens command palette", () => { + let app: Application; + + describe("menu", () => { + beforeAll(async () => app = await utils.appStart(), 20000); + + afterAll(async () => { + if (app?.isRunning()) { + await utils.tearDown(app); + } + }); + + it("opens command dialog from menu", async () => { + await utils.clickWhatsNew(app); + await app.electron.ipcRenderer.send("test-menu-item-click", "View", "Command Palette..."); + await app.client.waitUntilTextExists(".Select__option", "Preferences: Open"); + await app.client.keys("Escape"); + }); + }); +}); diff --git a/integration/__tests__/workspace.tests.ts b/integration/__tests__/workspace.tests.ts new file mode 100644 index 0000000000..4164151b0f --- /dev/null +++ b/integration/__tests__/workspace.tests.ts @@ -0,0 +1,75 @@ +import { Application } from "spectron"; +import * as utils from "../helpers/utils"; +import { addMinikubeCluster, minikubeReady } from "../helpers/minikube"; +import { exec } from "child_process"; +import * as util from "util"; + +export const promiseExec = util.promisify(exec); + +jest.setTimeout(60000); + +describe("Lens integration tests", () => { + let app: Application; + const ready = minikubeReady("workspace-int-tests"); + + utils.describeIf(ready)("workspaces", () => { + beforeAll(async () => { + app = await utils.appStart(); + await utils.clickWhatsNew(app); + }, 20000); + + afterAll(async () => { + if (app && app.isRunning()) { + return utils.tearDown(app); + } + }); + + const switchToWorkspace = async (name: string) => { + await app.client.click("[data-test-id=current-workspace]"); + await app.client.keys(name); + await app.client.keys("Enter"); + await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", name); + }; + + const createWorkspace = async (name: string) => { + await app.client.click("[data-test-id=current-workspace]"); + await app.client.keys("add workspace"); + await app.client.keys("Enter"); + await app.client.keys(name); + await app.client.keys("Enter"); + await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", name); + }; + + it("creates new workspace", async () => { + const name = "test-workspace"; + + await createWorkspace(name); + await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", name); + }); + + it("edits current workspaces", async () => { + await createWorkspace("to-be-edited"); + await app.client.click("[data-test-id=current-workspace]"); + await app.client.keys("edit current workspace"); + await app.client.keys("Enter"); + await app.client.keys("edited-workspace"); + await app.client.keys("Enter"); + await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", "edited-workspace"); + }); + + it("adds cluster in default workspace", async () => { + await switchToWorkspace("default"); + await addMinikubeCluster(app); + await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); + await app.client.waitForExist(`iframe[name="minikube"]`); + await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active"); + }); + + it("adds cluster in test-workspace", async () => { + await switchToWorkspace("test-workspace"); + await addMinikubeCluster(app); + await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); + await app.client.waitForExist(`iframe[name="minikube"]`); + }); + }); +}); diff --git a/integration/helpers/minikube.ts b/integration/helpers/minikube.ts new file mode 100644 index 0000000000..67ef0145d5 --- /dev/null +++ b/integration/helpers/minikube.ts @@ -0,0 +1,59 @@ +import { spawnSync } from "child_process"; +import { Application } from "spectron"; + +export function minikubeReady(testNamespace: string): boolean { + // determine if minikube is running + { + const { status } = spawnSync("minikube status", { shell: true }); + + if (status !== 0) { + console.warn("minikube not running"); + + return false; + } + } + + // Remove TEST_NAMESPACE if it already exists + { + const { status } = spawnSync(`minikube kubectl -- get namespace ${testNamespace}`, { shell: true }); + + if (status === 0) { + console.warn(`Removing existing ${testNamespace} namespace`); + + const { status, stdout, stderr } = spawnSync( + `minikube kubectl -- delete namespace ${testNamespace}`, + { shell: true }, + ); + + if (status !== 0) { + console.warn(`Error removing ${testNamespace} namespace: ${stderr.toString()}`); + + return false; + } + + console.log(stdout.toString()); + } + } + + return true; +} + +export async function addMinikubeCluster(app: Application) { + await app.client.click("div.add-cluster"); + await app.client.waitUntilTextExists("div", "Select kubeconfig file"); + await app.client.click("div.Select__control"); // show the context drop-down list + await app.client.waitUntilTextExists("div", "minikube"); + + if (!await app.client.$("button.primary").isEnabled()) { + await app.client.click("div.minikube"); // select minikube context + } // else the only context, which must be 'minikube', is automatically selected + await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button) + await app.client.click("button.primary"); // add minikube cluster +} + +export async function waitForMinikubeDashboard(app: Application) { + await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); + await app.client.waitForExist(`iframe[name="minikube"]`); + await app.client.frame("minikube"); + await app.client.waitUntilTextExists("span.link-text", "Cluster"); +} diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index 195de2d073..f7fbac5830 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -28,12 +28,29 @@ export function setup(): Application { }); } -type HelmRepository = { - name: string; - url: string; +export const keys = { + backspace: "\uE003" }; + +export async function appStart() { + const app = setup(); + + await app.start(); + // Wait for splash screen to be closed + while (await app.client.getWindowCount() > 1); + await app.client.windowByIndex(0); + await app.client.waitUntilWindowLoaded(); + + return app; +} + +export async function clickWhatsNew(app: Application) { + await app.client.waitUntilTextExists("h1", "What's new?"); + await app.client.click("button.primary"); + await app.client.waitUntilTextExists("h1", "Welcome"); +} + type AsyncPidGetter = () => Promise; -export const promiseExec = util.promisify(exec); export async function tearDown(app: Application) { const pid = await (app.mainProcess.pid as any as AsyncPidGetter)(); @@ -47,6 +64,13 @@ export async function tearDown(app: Application) { } } +export const promiseExec = util.promisify(exec); + +type HelmRepository = { + name: string; + url: string; +}; + export async function listHelmRepositories(retries = 0): Promise{ if (retries < 5) { try { diff --git a/package.json b/package.json index 735a5fa341..2b6e8ca9db 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "build:mac": "yarn run compile && electron-builder --mac --dir -c.productName=Lens", "build:win": "yarn run compile && electron-builder --win --dir -c.productName=Lens", "test": "jest --env=jsdom src $@", - "integration": "jest --coverage integration $@", + "integration": "jest --runInBand integration", "dist": "yarn run compile && electron-builder --publish onTag", "dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32", "dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null", diff --git a/src/common/ipc.ts b/src/common/ipc.ts index 369221815b..c2f8562cf7 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -3,9 +3,12 @@ // https://www.electronjs.org/docs/api/ipc-renderer import { ipcMain, ipcRenderer, webContents, remote } from "electron"; +import { toJS } from "mobx"; import logger from "../main/logger"; import { ClusterFrameInfo, clusterFrameMap } from "./cluster-frames"; +const subFramesChannel = "ipc:get-sub-frames"; + export function handleRequest(channel: string, listener: (...args: any[]) => any) { ipcMain.handle(channel, listener); } @@ -14,38 +17,39 @@ export async function requestMain(channel: string, ...args: any[]) { return ipcRenderer.invoke(channel, ...args); } -async function getSubFrames(): Promise { - const subFrames: ClusterFrameInfo[] = []; - - clusterFrameMap.forEach(frameInfo => { - subFrames.push(frameInfo); - }); - - return subFrames; +function getSubFrames(): ClusterFrameInfo[] { + return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true }); } -export function broadcastMessage(channel: string, ...args: any[]) { +export async function broadcastMessage(channel: string, ...args: any[]) { const views = (webContents || remote?.webContents)?.getAllWebContents(); if (!views) return; - views.forEach(webContent => { - const type = webContent.getType(); - - logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args }); - webContent.send(channel, ...args); - getSubFrames().then((frames) => { - frames.map((frameInfo) => { - webContent.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); - }); - }).catch((e) => e); - }); - if (ipcRenderer) { ipcRenderer.send(channel, ...args); } else { ipcMain.emit(channel, ...args); } + + for (const view of views) { + const type = view.getType(); + + logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${view.id}`, { args }); + view.send(channel, ...args); + + try { + const subFrames: ClusterFrameInfo[] = ipcRenderer + ? await requestMain(subFramesChannel) + : getSubFrames(); + + for (const frameInfo of subFrames) { + view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); + } + } catch (error) { + logger.error("[IPC]: failed to send IPC message", { error }); + } + } } export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) { @@ -73,3 +77,9 @@ export function unsubscribeAllFromBroadcast(channel: string) { ipcMain.removeAllListeners(channel); } } + +export function bindBroadcastHandlers() { + handleRequest(subFramesChannel, () => { + return getSubFrames(); + }); +} diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 25afaa76fe..8b9b132114 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -2,6 +2,7 @@ import type { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPage import type { Cluster } from "../main/cluster"; import { LensExtension } from "./lens-extension"; import { getExtensionPageUrl } from "./registries/page-registry"; +import { CommandRegistration } from "./registries/command-registry"; export class LensRendererExtension extends LensExtension { globalPages: PageRegistration[] = []; @@ -14,6 +15,7 @@ export class LensRendererExtension extends LensExtension { statusBarItems: StatusBarRegistration[] = []; kubeObjectDetailItems: KubeObjectDetailRegistration[] = []; kubeObjectMenuItems: KubeObjectMenuRegistration[] = []; + commands: CommandRegistration[] = []; async navigate

(pageId?: string, params?: P) { const { navigate } = await import("../renderer/navigation"); diff --git a/src/extensions/registries/command-registry.ts b/src/extensions/registries/command-registry.ts new file mode 100644 index 0000000000..0b1fc0252c --- /dev/null +++ b/src/extensions/registries/command-registry.ts @@ -0,0 +1,37 @@ +// Extensions API -> Commands + +import type { Cluster } from "../../main/cluster"; +import type { Workspace } from "../../common/workspace-store"; +import { BaseRegistry } from "./base-registry"; +import { action } from "mobx"; +import { LensExtension } from "../lens-extension"; + +export type CommandContext = { + cluster?: Cluster; + workspace?: Workspace; +}; + +export interface CommandRegistration { + id: string; + title: string; + scope: "cluster" | "global"; + action: (context: CommandContext) => void; + isActive?: (context: CommandContext) => boolean; +} + +export class CommandRegistry extends BaseRegistry { + @action + add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) { + const itemArray = [items].flat(); + + const newIds = itemArray.map((item) => item.id); + const currentIds = this.getItems().map((item) => item.id); + + const filteredIds = newIds.filter((id) => !currentIds.includes(id)); + const filteredItems = itemArray.filter((item) => filteredIds.includes(item.id)); + + return super.add(filteredItems, extension); + } +} + +export const commandRegistry = new CommandRegistry(); diff --git a/src/extensions/renderer-api/components.ts b/src/extensions/renderer-api/components.ts index 49c747da3a..55de99bf80 100644 --- a/src/extensions/renderer-api/components.ts +++ b/src/extensions/renderer-api/components.ts @@ -13,6 +13,9 @@ export * from "../../renderer/components/select"; export * from "../../renderer/components/slider"; export * from "../../renderer/components/input/input"; +// command-overlay +export { CommandOverlay } from "../../renderer/components/command-palette"; + // other components export * from "../../renderer/components/icon"; export * from "../../renderer/components/tooltip"; diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 1b468e3bb6..dfcda98203 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,7 +1,7 @@ import "../common/cluster-ipc"; import type http from "http"; import { ipcMain } from "electron"; -import { autorun } from "mobx"; +import { autorun, reaction } from "mobx"; import { clusterStore, getClusterIdFromHost } from "../common/cluster-store"; import { Cluster } from "./cluster"; import logger from "./logger"; @@ -12,14 +12,14 @@ export class ClusterManager extends Singleton { constructor(public readonly port: number) { super(); // auto-init clusters - autorun(() => { - clusterStore.enabledClustersList.forEach(cluster => { + reaction(() => clusterStore.enabledClustersList, (clusters) => { + clusters.forEach((cluster) => { if (!cluster.initialized && !cluster.initializing) { logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); cluster.init(port); } }); - }); + }, { fireImmediately: true }); // auto-stop removed clusters autorun(() => { diff --git a/src/main/index.ts b/src/main/index.ts index 265c91f6d4..2b7817f093 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -26,6 +26,7 @@ import { InstalledExtension, extensionDiscovery } from "../extensions/extension- import type { LensExtensionId } from "../extensions/lens-extension"; import { installDeveloperTools } from "./developer-tools"; import { filesystemProvisionerStore } from "./extension-filesystem"; +import { bindBroadcastHandlers } from "../common/ipc"; const workingDir = path.join(app.getPath("appData"), appName); let proxyPort: number; @@ -63,6 +64,8 @@ app.on("ready", async () => { logger.info(`🚀 Starting Lens from "${workingDir}"`); await shellSync(); + bindBroadcastHandlers(); + powerMonitor.on("shutdown", () => { app.exit(); }); diff --git a/src/main/menu.ts b/src/main/menu.ts index 2cddbb1b01..57c6ccab5e 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -10,6 +10,7 @@ import { extensionsURL } from "../renderer/components/+extensions/extensions.rou import { menuRegistry } from "../extensions/registries/menu-registry"; import logger from "./logger"; import { exitApp } from "./exit-app"; +import { broadcastMessage } from "../common/ipc"; export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"; @@ -173,6 +174,14 @@ export function buildMenu(windowManager: WindowManager) { const viewMenu: MenuItemConstructorOptions = { label: "View", submenu: [ + { + label: "Command Palette...", + accelerator: "Shift+CmdOrCtrl+P", + click() { + broadcastMessage("command-palette:open"); + } + }, + { type: "separator" }, { label: "Back", accelerator: "CmdOrCtrl+[", diff --git a/src/renderer/components/+apps/apps.command.ts b/src/renderer/components/+apps/apps.command.ts new file mode 100644 index 0000000000..ff6c9d615d --- /dev/null +++ b/src/renderer/components/+apps/apps.command.ts @@ -0,0 +1,18 @@ +import { navigate } from "../../navigation"; +import { commandRegistry } from "../../../extensions/registries/command-registry"; +import { helmChartsURL } from "../+apps-helm-charts"; +import { releaseURL } from "../+apps-releases"; + +commandRegistry.add({ + id: "cluster.viewHelmCharts", + title: "Cluster: View Helm Charts", + scope: "cluster", + action: () => navigate(helmChartsURL()) +}); + +commandRegistry.add({ + id: "cluster.viewHelmReleases", + title: "Cluster: View Helm Releases", + scope: "cluster", + action: () => navigate(releaseURL()) +}); diff --git a/src/renderer/components/+apps/index.ts b/src/renderer/components/+apps/index.ts index 330891b2b1..30fbf65316 100644 --- a/src/renderer/components/+apps/index.ts +++ b/src/renderer/components/+apps/index.ts @@ -1,2 +1,3 @@ export * from "./apps"; export * from "./apps.route"; +export * from "./apps.command"; diff --git a/src/renderer/components/+cluster-settings/cluster-settings.command.ts b/src/renderer/components/+cluster-settings/cluster-settings.command.ts new file mode 100644 index 0000000000..a3b3c8792e --- /dev/null +++ b/src/renderer/components/+cluster-settings/cluster-settings.command.ts @@ -0,0 +1,16 @@ +import { navigate } from "../../navigation"; +import { commandRegistry } from "../../../extensions/registries/command-registry"; +import { clusterSettingsURL } from "./cluster-settings.route"; +import { clusterStore } from "../../../common/cluster-store"; + +commandRegistry.add({ + id: "cluster.viewCurrentClusterSettings", + title: "Cluster: View Settings", + scope: "global", + action: () => navigate(clusterSettingsURL({ + params: { + clusterId: clusterStore.active.id + } + })), + isActive: (context) => !!context.cluster +}); diff --git a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx index ea4ee5a571..fa76dde806 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx @@ -1,7 +1,5 @@ import React from "react"; import { observer } from "mobx-react"; -import { Link } from "react-router-dom"; -import { workspacesURL } from "../../+workspaces"; import { workspaceStore } from "../../../../common/workspace-store"; import { Cluster } from "../../../../main/cluster"; import { Select } from "../../../components/select"; @@ -18,10 +16,7 @@ export class ClusterWorkspaceSetting extends React.Component { <>

- Define cluster{" "} - - workspace - . + Define cluster workspace.

this.onSubmit(v)} + dirty={true} + showValidationLine={true} /> + + Please provide a new workspace name (Press "Enter" to confirm or "Escape" to cancel) + + + ); + } +} + +commandRegistry.add({ + id: "workspace.addWorkspace", + title: "Workspace: Add workspace ...", + scope: "global", + action: () => CommandOverlay.open() +}); diff --git a/src/renderer/components/+workspaces/edit-workspace.tsx b/src/renderer/components/+workspaces/edit-workspace.tsx new file mode 100644 index 0000000000..3ab4b44d5a --- /dev/null +++ b/src/renderer/components/+workspaces/edit-workspace.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { observer } from "mobx-react"; +import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store"; +import { commandRegistry } from "../../../extensions/registries/command-registry"; +import { Input, InputValidator } from "../input"; +import { CommandOverlay } from "../command-palette/command-container"; + +const validateWorkspaceName: InputValidator = { + condition: ({ required }) => required, + message: () => `Workspace with this name already exists`, + validate: (value) => { + const current = workspaceStore.currentWorkspace; + + if (current.name === value.trim()) { + return true; + } + + return !workspaceStore.enabledWorkspacesList.find((workspace) => workspace.name === value); + } +}; + +interface EditWorkspaceState { + name: string; +} + +@observer +export class EditWorkspace extends React.Component<{}, EditWorkspaceState> { + + state: EditWorkspaceState = { + name: "" + }; + + componentDidMount() { + this.setState({name: workspaceStore.currentWorkspace.name}); + } + + onSubmit(name: string) { + if (name.trim() === "") { + return; + } + + workspaceStore.currentWorkspace.name = name; + CommandOverlay.close(); + } + + onChange(name: string) { + this.setState({name}); + } + + get name() { + return this.state.name; + } + + render() { + return ( + <> + this.onChange(v)} + onSubmit={(v) => this.onSubmit(v)} + dirty={true} + value={this.name} + showValidationLine={true} /> + + Please provide a new workspace name (Press "Enter" to confirm or "Escape" to cancel) + + + ); + } +} + +commandRegistry.add({ + id: "workspace.editCurrentWorkspace", + title: "Workspace: Edit current workspace ...", + scope: "global", + action: () => CommandOverlay.open(), + isActive: (context) => context.workspace?.id !== WorkspaceStore.defaultId +}); diff --git a/src/renderer/components/+workspaces/index.ts b/src/renderer/components/+workspaces/index.ts index db23faa3be..5b84fc9b00 100644 --- a/src/renderer/components/+workspaces/index.ts +++ b/src/renderer/components/+workspaces/index.ts @@ -1,2 +1 @@ -export * from "./workspaces.route"; export * from "./workspaces"; diff --git a/src/renderer/components/+workspaces/remove-workspace.tsx b/src/renderer/components/+workspaces/remove-workspace.tsx new file mode 100644 index 0000000000..9f66292447 --- /dev/null +++ b/src/renderer/components/+workspaces/remove-workspace.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { observer } from "mobx-react"; +import { computed} from "mobx"; +import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store"; +import { ConfirmDialog } from "../confirm-dialog"; +import { commandRegistry } from "../../../extensions/registries/command-registry"; +import { Select } from "../select"; +import { CommandOverlay } from "../command-palette/command-container"; + +@observer +export class RemoveWorkspace extends React.Component { + @computed get options() { + return workspaceStore.enabledWorkspacesList.filter((workspace) => workspace.id !== WorkspaceStore.defaultId).map((workspace) => { + return { value: workspace.id, label: workspace.name }; + }); + } + + onChange(id: string) { + const workspace = workspaceStore.enabledWorkspacesList.find((workspace) => workspace.id === id); + + if (!workspace ) { + return; + } + + CommandOverlay.close(); + ConfirmDialog.open({ + okButtonProps: { + label: `Remove Workspace`, + primary: false, + accent: true, + }, + ok: () => { + workspaceStore.removeWorkspace(workspace); + }, + message: ( +
+

+ Are you sure you want remove workspace {workspace.name}? +

+

+ All clusters within workspace will be cleared as well +

+
+ ), + }); + } + + render() { + return ( + editingWorkspace.name = v} - onKeyPress={(e) => this.onInputKeypress(e, workspaceId)} - validators={[isRequired, existenceValidator]} - autoFocus - /> - editingWorkspace.description = v} - onKeyPress={(e) => this.onInputKeypress(e, workspaceId)} - /> - this.saveWorkspace(workspaceId)} - /> - this.clearEditing(workspaceId)} - /> - - )} -
- ); - })} -
-