From 227a1497825afa362d543d5393d075efb7d11ae1 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 27 Jan 2021 20:04:50 +0200 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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" ],