From 961a38d52f019def748b0f80a14e4c23791d68c1 Mon Sep 17 00:00:00 2001 From: Violetta <38247153+vshakirova@users.noreply.github.com> Date: Thu, 10 Dec 2020 23:18:30 +0400 Subject: [PATCH 01/24] Fix Deployment Scale Button "minus" (#1728) Signed-off-by: vshakirova --- .../deployment-scale-dialog.test.tsx | 48 ++++++++++++------- .../deployment-scale-dialog.tsx | 4 +- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx index ead4a37487..e3d18669f9 100644 --- a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx @@ -128,33 +128,45 @@ describe("", () => { const initReplicas = 1; deploymentApi.getReplicas = jest.fn().mockImplementationOnce(async () => initReplicas); - const { getByTestId } = render(); + const component = render(); DeploymentScaleDialog.open(dummyDeployment); await waitFor(async () => { - const desiredScale = await getByTestId("desired-scale"); - - expect(desiredScale).toHaveTextContent(`${initReplicas}`); + expect(await component.getByTestId("desired-scale")).toHaveTextContent(`${initReplicas}`); + expect(await component.getByTestId("current-scale")).toHaveTextContent(`${initReplicas}`); + expect((await component.baseElement.querySelector("input").value)).toBe(`${initReplicas}`); }); - const up = await getByTestId("desired-replicas-up"); - const down = await getByTestId("desired-replicas-down"); + const up = await component.getByTestId("desired-replicas-up"); + const down = await component.getByTestId("desired-replicas-down"); fireEvent.click(up); - expect(await getByTestId("desired-scale")).toHaveTextContent(`${initReplicas + 1}`); - fireEvent.click(down); - expect(await getByTestId("desired-scale")).toHaveTextContent("1"); - // edge case, desiredScale must > 0 - fireEvent.click(down); - fireEvent.click(down); - expect(await getByTestId("desired-scale")).toHaveTextContent("1"); - const times = 120; + expect(await component.getByTestId("desired-scale")).toHaveTextContent(`${initReplicas + 1}`); + expect(await component.getByTestId("current-scale")).toHaveTextContent(`${initReplicas}`); + expect((await component.baseElement.querySelector("input").value)).toBe(`${initReplicas + 1}`); + + fireEvent.click(down); + expect(await component.getByTestId("desired-scale")).toHaveTextContent(`${initReplicas}`); + expect(await component.getByTestId("current-scale")).toHaveTextContent(`${initReplicas}`); + expect((await component.baseElement.querySelector("input").value)).toBe(`${initReplicas}`); + + // edge case, desiredScale must = 0 + let times = 10; + + for (let i = 0; i < times; i++) { + fireEvent.click(down); + } + expect(await component.getByTestId("desired-scale")).toHaveTextContent("0"); + expect((await component.baseElement.querySelector("input").value)).toBe("0"); + + // edge case, desiredScale must = 100 scaleMax (100) + times = 120; - // edge case, desiredScale must < scaleMax (100) for (let i = 0; i < times; i++) { fireEvent.click(up); } - expect(await getByTestId("desired-scale")).toHaveTextContent("100"); + expect(await component.getByTestId("desired-scale")).toHaveTextContent("100"); + expect((component.baseElement.querySelector("input").value)).toBe("100"); + expect(await component.getByTestId("warning")) + .toHaveTextContent("High number of replicas may cause cluster performance issues"); }); - }); - diff --git a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx index 5fb43368d5..42105aeac7 100644 --- a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx @@ -91,7 +91,7 @@ export class DeploymentScaleDialog extends Component { }; desiredReplicasDown = () => { - this.desiredReplicas > 1 && this.desiredReplicas--; + this.desiredReplicas > 0 && this.desiredReplicas--; }; renderContents() { @@ -124,7 +124,7 @@ export class DeploymentScaleDialog extends Component { {warning && -
+
High number of replicas may cause cluster performance issues
From a61e20965d2458974c261c75a177bb5924d6efe5 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Fri, 11 Dec 2020 08:36:47 +0300 Subject: [PATCH 02/24] ClusterOverview page refactorings (#1696) * ClusterOverview page refactorings Signed-off-by: Alex Andreev * Minor test fix for MainLayoutHeader Signed-off-by: Alex Andreev * Replacing class name in tests Signed-off-by: Alex Andreev * Remove unnecessary parenthesis Signed-off-by: Alex Andreev --- integration/__tests__/app.tests.ts | 2 +- .../components/+cluster/cluster-issues.scss | 7 +- .../+cluster/cluster-metric-switchers.tsx | 8 +- .../components/+cluster/cluster-metrics.tsx | 9 +-- .../{cluster.scss => cluster-overview.scss} | 2 +- ...ter.store.ts => cluster-overview.store.ts} | 33 ++------ .../components/+cluster/cluster-overview.tsx | 79 +++++++++++++++++++ .../+cluster/cluster-pie-charts.tsx | 8 +- src/renderer/components/+cluster/cluster.tsx | 74 ----------------- src/renderer/components/app.tsx | 4 +- .../__test__/main-layout-header.test.tsx | 4 +- 11 files changed, 106 insertions(+), 124 deletions(-) rename src/renderer/components/+cluster/{cluster.scss => cluster-overview.scss} (95%) rename src/renderer/components/+cluster/{cluster.store.ts => cluster-overview.store.ts} (76%) create mode 100644 src/renderer/components/+cluster/cluster-overview.tsx delete mode 100644 src/renderer/components/+cluster/cluster.tsx diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 285af70ceb..410712e4f0 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -226,7 +226,7 @@ describe("Lens integration tests", () => { pages: [{ name: "Cluster", href: "cluster", - expectedSelector: "div.Cluster div.label", + expectedSelector: "div.ClusterOverview div.label", expectedText: "Master" }] }, diff --git a/src/renderer/components/+cluster/cluster-issues.scss b/src/renderer/components/+cluster/cluster-issues.scss index b552cf6877..8886fa40e3 100644 --- a/src/renderer/components/+cluster/cluster-issues.scss +++ b/src/renderer/components/+cluster/cluster-issues.scss @@ -1,17 +1,14 @@ .ClusterIssues { min-height: 350px; position: relative; + grid-column-start: 1; + grid-column-end: 3; @include media("<1024px") { grid-column-start: 1!important; grid-column-end: 1!important; } - &.wide { - grid-column-start: 1; - grid-column-end: 3; - } - .SubHeader { .Icon { font-size: 130%; diff --git a/src/renderer/components/+cluster/cluster-metric-switchers.tsx b/src/renderer/components/+cluster/cluster-metric-switchers.tsx index f2e090cdbb..02ffbd8755 100644 --- a/src/renderer/components/+cluster/cluster-metric-switchers.tsx +++ b/src/renderer/components/+cluster/cluster-metric-switchers.tsx @@ -6,10 +6,10 @@ import { observer } from "mobx-react"; import { nodesStore } from "../+nodes/nodes.store"; import { cssNames } from "../../utils"; import { Radio, RadioGroup } from "../radio"; -import { clusterStore, MetricNodeRole, MetricType } from "./cluster.store"; +import { clusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview.store"; export const ClusterMetricSwitchers = observer(() => { - const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterStore; + const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterOverviewStore; const { masterNodes, workerNodes } = nodesStore; const metricsValues = getMetricsValues(metrics); const disableRoles = !masterNodes.length || !workerNodes.length; @@ -22,7 +22,7 @@ export const ClusterMetricSwitchers = observer(() => { asButtons className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })} value={metricNodeRole} - onChange={(metric: MetricNodeRole) => clusterStore.metricNodeRole = metric} + onChange={(metric: MetricNodeRole) => clusterOverviewStore.metricNodeRole = metric} > Master} value={MetricNodeRole.MASTER}/> Worker} value={MetricNodeRole.WORKER}/> @@ -33,7 +33,7 @@ export const ClusterMetricSwitchers = observer(() => { asButtons className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })} value={metricType} - onChange={(value: MetricType) => clusterStore.metricType = value} + onChange={(value: MetricType) => clusterOverviewStore.metricType = value} > CPU} value={MetricType.CPU}/> Memory} value={MetricType.MEMORY}/> diff --git a/src/renderer/components/+cluster/cluster-metrics.tsx b/src/renderer/components/+cluster/cluster-metrics.tsx index b049cfc2f4..6461bae7f3 100644 --- a/src/renderer/components/+cluster/cluster-metrics.tsx +++ b/src/renderer/components/+cluster/cluster-metrics.tsx @@ -3,7 +3,7 @@ import "./cluster-metrics.scss"; import React from "react"; import { observer } from "mobx-react"; import { ChartOptions, ChartPoint } from "chart.js"; -import { clusterStore, MetricType } from "./cluster.store"; +import { clusterOverviewStore, MetricType } from "./cluster-overview.store"; import { BarChart } from "../chart"; import { bytesToUnits } from "../../utils"; import { Spinner } from "../spinner"; @@ -13,10 +13,9 @@ import { ClusterMetricSwitchers } from "./cluster-metric-switchers"; import { getMetricLastPoints } from "../../api/endpoints/metrics.api"; export const ClusterMetrics = observer(() => { - const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics, liveMetrics } = clusterStore; - const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterStore.metrics); + const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = clusterOverviewStore; + const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterOverviewStore.metrics); const metricValues = getMetricsValues(metrics); - const liveMetricValues = getMetricsValues(liveMetrics); const colors = { cpu: "#3D90CE", memory: "#C93DCE" }; const data = metricValues.map(value => ({ x: value[0], @@ -70,7 +69,7 @@ export const ClusterMetrics = observer(() => { const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions; const renderMetrics = () => { - if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) { + if (!metricValues.length && !metricsLoaded) { return ; } diff --git a/src/renderer/components/+cluster/cluster.scss b/src/renderer/components/+cluster/cluster-overview.scss similarity index 95% rename from src/renderer/components/+cluster/cluster.scss rename to src/renderer/components/+cluster/cluster-overview.scss index 32739c378c..c0534f4fff 100644 --- a/src/renderer/components/+cluster/cluster.scss +++ b/src/renderer/components/+cluster/cluster-overview.scss @@ -1,4 +1,4 @@ -.Cluster { +.ClusterOverview { $gridGap: $margin * 2; position: relative; diff --git a/src/renderer/components/+cluster/cluster.store.ts b/src/renderer/components/+cluster/cluster-overview.store.ts similarity index 76% rename from src/renderer/components/+cluster/cluster.store.ts rename to src/renderer/components/+cluster/cluster-overview.store.ts index 04bc1d8658..64faa2394c 100644 --- a/src/renderer/components/+cluster/cluster.store.ts +++ b/src/renderer/components/+cluster/cluster-overview.store.ts @@ -1,4 +1,4 @@ -import { observable, reaction, when } from "mobx"; +import { action, observable, reaction, when } from "mobx"; import { KubeObjectStore } from "../../kube-object.store"; import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints"; import { autobind, createStorage } from "../../utils"; @@ -17,11 +17,10 @@ export enum MetricNodeRole { } @autobind() -export class ClusterStore extends KubeObjectStore { +export class ClusterOverviewStore extends KubeObjectStore { api = clusterApi; @observable metrics: Partial = {}; - @observable liveMetrics: Partial = {}; @observable metricsLoaded = false; @observable metricType: MetricType; @observable metricNodeRole: MetricNodeRole; @@ -46,9 +45,8 @@ export class ClusterStore extends KubeObjectStore { reaction(() => this.metricNodeRole, () => { if (!this.metricsLoaded) return; this.metrics = {}; - this.liveMetrics = {}; this.metricsLoaded = false; - this.getAllMetrics(); + this.loadMetrics(); }); // check which node type to select @@ -60,33 +58,16 @@ export class ClusterStore extends KubeObjectStore { }); } + @action async loadMetrics(params?: IMetricsReqParams) { await when(() => nodesStore.isLoaded); const { masterNodes, workerNodes } = nodesStore; const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes; - return clusterApi.getMetrics(nodes.map(node => node.getName()), params); - } - - async getAllMetrics() { - await this.getMetrics(); - await this.getLiveMetrics(); + this.metrics = await clusterApi.getMetrics(nodes.map(node => node.getName()), params); this.metricsLoaded = true; } - async getMetrics() { - this.metrics = await this.loadMetrics(); - } - - async getLiveMetrics() { - const step = 3; - const range = 15; - const end = Date.now() / 1000; - const start = end - range; - - this.liveMetrics = await this.loadMetrics({ start, end, step, range }); - } - getMetricsValues(source: Partial): [number, string][] { switch (this.metricType) { case MetricType.CPU: @@ -111,5 +92,5 @@ export class ClusterStore extends KubeObjectStore { } } -export const clusterStore = new ClusterStore(); -apiManager.registerStore(clusterStore); +export const clusterOverviewStore = new ClusterOverviewStore(); +apiManager.registerStore(clusterOverviewStore); diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx new file mode 100644 index 0000000000..104c6fd022 --- /dev/null +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -0,0 +1,79 @@ +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"; +import { ClusterIssues } from "./cluster-issues"; +import { ClusterMetrics } from "./cluster-metrics"; +import { clusterOverviewStore } from "./cluster-overview.store"; +import { ClusterPieCharts } from "./cluster-pie-charts"; + +@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) + ); + + loadMetrics() { + getHostedCluster().available && clusterOverviewStore.loadMetrics(); + } + + async componentDidMount() { + if (isAllowedResource("nodes")) { + this.stores.push(nodesStore); + } + + if (isAllowedResource("pods")) { + this.stores.push(podsStore); + } + + 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(); + } + + componentWillUnmount() { + this.subscribers.forEach(dispose => dispose()); // unsubscribe all + this.metricPoller.stop(); + } + + render() { + const isLoaded = nodesStore.isLoaded && podsStore.isLoaded; + + return ( + +
+ {!isLoaded ? : ( + <> + + + + + )} +
+
+ ); + } +} diff --git a/src/renderer/components/+cluster/cluster-pie-charts.tsx b/src/renderer/components/+cluster/cluster-pie-charts.tsx index 246af6a3ca..684233f8ca 100644 --- a/src/renderer/components/+cluster/cluster-pie-charts.tsx +++ b/src/renderer/components/+cluster/cluster-pie-charts.tsx @@ -4,7 +4,7 @@ import React from "react"; import { observer } from "mobx-react"; import { t, Trans } from "@lingui/macro"; import { useLingui } from "@lingui/react"; -import { clusterStore, MetricNodeRole } from "./cluster.store"; +import { clusterOverviewStore, MetricNodeRole } from "./cluster-overview.store"; import { Spinner } from "../spinner"; import { Icon } from "../icon"; import { nodesStore } from "../+nodes/nodes.store"; @@ -27,7 +27,7 @@ export const ClusterPieCharts = observer(() => { }; const renderCharts = () => { - const data = getMetricLastPoints(clusterStore.metrics); + const data = getMetricLastPoints(clusterOverviewStore.metrics); const { memoryUsage, memoryRequests, memoryCapacity, memoryLimits } = data; const { cpuUsage, cpuRequests, cpuCapacity, cpuLimits } = data; const { podUsage, podCapacity } = data; @@ -173,7 +173,7 @@ export const ClusterPieCharts = observer(() => { const renderContent = () => { const { masterNodes, workerNodes } = nodesStore; - const { metricNodeRole, metricsLoaded } = clusterStore; + const { metricNodeRole, metricsLoaded } = clusterOverviewStore; const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes; if (!nodes.length) { @@ -192,7 +192,7 @@ export const ClusterPieCharts = observer(() => {
); } - const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterStore.metrics); + const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterOverviewStore.metrics); if (!memoryCapacity || !cpuCapacity || !podCapacity) { return ; diff --git a/src/renderer/components/+cluster/cluster.tsx b/src/renderer/components/+cluster/cluster.tsx deleted file mode 100644 index f99f65c479..0000000000 --- a/src/renderer/components/+cluster/cluster.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import "./cluster.scss"; - -import React from "react"; -import { computed, reaction } from "mobx"; -import { disposeOnUnmount, observer } from "mobx-react"; -import { TabLayout } from "../layout/tab-layout"; -import { ClusterIssues } from "./cluster-issues"; -import { Spinner } from "../spinner"; -import { cssNames, interval, isElectron } from "../../utils"; -import { ClusterPieCharts } from "./cluster-pie-charts"; -import { ClusterMetrics } from "./cluster-metrics"; -import { nodesStore } from "../+nodes/nodes.store"; -import { podsStore } from "../+workloads-pods/pods.store"; -import { clusterStore } from "./cluster.store"; -import { eventStore } from "../+events/event.store"; -import { isAllowedResource } from "../../../common/rbac"; -import { getHostedCluster } from "../../../common/cluster-store"; - -@observer -export class Cluster extends React.Component { - private dependentStores = [nodesStore, podsStore]; - - private watchers = [ - interval(60, () => { getHostedCluster().available && clusterStore.getMetrics();}), - interval(20, () => { getHostedCluster().available && eventStore.loadAll();}) - ]; - - @computed get isLoaded() { - return nodesStore.isLoaded && podsStore.isLoaded; - } - - // todo: refactor - async componentDidMount() { - const { dependentStores } = this; - - if (!isAllowedResource("nodes")) { - dependentStores.splice(dependentStores.indexOf(nodesStore), 1); - } - this.watchers.forEach(watcher => watcher.start(true)); - - await Promise.all([ - ...dependentStores.map(store => store.loadAll()), - clusterStore.getAllMetrics() - ]); - - disposeOnUnmount(this, [ - ...dependentStores.map(store => store.subscribe()), - () => this.watchers.forEach(watcher => watcher.stop()), - reaction( - () => clusterStore.metricNodeRole, - () => this.watchers.forEach(watcher => watcher.restart()) - ) - ]); - } - - render() { - const { isLoaded } = this; - - return ( - -
- {!isLoaded && } - {isLoaded && ( - <> - - - - - )} -
-
- ); - } -} diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 1cc5f910b6..e6af7e11d2 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -16,7 +16,7 @@ import { Workloads, workloadsRoute, workloadsURL } from "./+workloads"; import { Namespaces, namespacesRoute } from "./+namespaces"; import { Network, networkRoute } from "./+network"; import { Storage, storageRoute } from "./+storage"; -import { Cluster } from "./+cluster/cluster"; +import { ClusterOverview } from "./+cluster/cluster-overview"; import { Config, configRoute } from "./+config"; import { Events } from "./+events/events"; import { eventRoute } from "./+events"; @@ -181,7 +181,7 @@ export class App extends React.Component { - + diff --git a/src/renderer/components/layout/__test__/main-layout-header.test.tsx b/src/renderer/components/layout/__test__/main-layout-header.test.tsx index 91fcc5dc7f..b2a7bb5d93 100644 --- a/src/renderer/components/layout/__test__/main-layout-header.test.tsx +++ b/src/renderer/components/layout/__test__/main-layout-header.test.tsx @@ -41,9 +41,9 @@ describe("", () => { expect(mockBroadcastIpc).toBeCalledWith("renderer:navigate", "/cluster/foo/settings"); }); - it("renders cluster name", async () => { + it("renders cluster name", () => { const { getByText } = render(); - expect(await getByText("minikube")).toBeTruthy(); + expect(getByText("minikube")).toBeInTheDocument(); }); }); \ No newline at end of file From b33629d24f6ccea58b0d9e55d04ff790dc19a4be Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Fri, 11 Dec 2020 11:21:43 +0300 Subject: [PATCH 03/24] Replace cronjob api version to batch/v1beta1 (#1747) Signed-off-by: Alex Andreev --- .../components/+workloads-cronjobs/cronjob-details.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx index ad24ceede6..fe319c0a7e 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx @@ -91,14 +91,14 @@ export class CronJobDetails extends React.Component { kubeObjectDetailRegistry.add({ kind: "CronJob", - apiVersions: ["batch/v1"], + apiVersions: ["batch/v1beta1"], components: { Details: (props) => } }); kubeObjectDetailRegistry.add({ kind: "CronJob", - apiVersions: ["batch/v1"], + apiVersions: ["batch/v1beta1"], priority: 5, components: { Details: (props) => From ff9e96d8802436a73fcfbbebe751586ca3bd94c9 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 11 Dec 2020 11:21:35 +0200 Subject: [PATCH 04/24] Fix buggy node metrics/chart (#1748) * fix buggy node metrics/chart Signed-off-by: Jari Kolehmainen * fix linter error Signed-off-by: Jari Kolehmainen --- src/renderer/components/+nodes/node-charts.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderer/components/+nodes/node-charts.tsx b/src/renderer/components/+nodes/node-charts.tsx index 5900e574df..52caa37ba7 100644 --- a/src/renderer/components/+nodes/node-charts.tsx +++ b/src/renderer/components/+nodes/node-charts.tsx @@ -26,9 +26,11 @@ export const NodeCharts = observer(() => { const [ memoryUsage, memoryRequests, + _memoryLimits, // eslint-disable-line unused-imports/no-unused-vars-ts memoryCapacity, cpuUsage, cpuRequests, + _cpuLimits, // eslint-disable-line unused-imports/no-unused-vars-ts cpuCapacity, podUsage, podCapacity, From 3b65019dcb3fec73d05db55fc77e0a3bfd8dd253 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 11 Dec 2020 13:13:40 +0200 Subject: [PATCH 05/24] Use correct apiversion for HPA details (#1745) Signed-off-by: Jari Kolehmainen --- src/renderer/components/+config-autoscalers/hpa-details.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/+config-autoscalers/hpa-details.tsx b/src/renderer/components/+config-autoscalers/hpa-details.tsx index 7cf5e3142f..b6fa920b37 100644 --- a/src/renderer/components/+config-autoscalers/hpa-details.tsx +++ b/src/renderer/components/+config-autoscalers/hpa-details.tsx @@ -134,7 +134,7 @@ export class HpaDetails extends React.Component { kubeObjectDetailRegistry.add({ kind: "HorizontalPodAutoscaler", - apiVersions: ["autoscaling/v1"], + apiVersions: ["autoscaling/v2beta1"], components: { Details: (props) => } @@ -142,7 +142,7 @@ kubeObjectDetailRegistry.add({ kubeObjectDetailRegistry.add({ kind: "HorizontalPodAutoscaler", - apiVersions: ["autoscaling/v1"], + apiVersions: ["autoscaling/v2beta1"], priority: 5, components: { Details: (props) => From 1e76e8c06c056f321c8157b0acf7f1566f81a9d5 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Fri, 11 Dec 2020 14:07:07 +0200 Subject: [PATCH 06/24] Ensure telemetry report is sent on extensions start (#1753) Signed-off-by: Lauri Nevala --- extensions/telemetry/src/tracker.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/telemetry/src/tracker.ts b/extensions/telemetry/src/tracker.ts index 398142305c..28595a6a93 100644 --- a/extensions/telemetry/src/tracker.ts +++ b/extensions/telemetry/src/tracker.ts @@ -74,6 +74,7 @@ export class Tracker extends Util.Singleton { } reportPeriodically() { + this.reportData(); this.reportInterval = setInterval(() => { this.reportData(); }, 60 * 60 * 1000); // report every 1h From abd422367579e9b86b3900e6b4152b842ccc4e1c Mon Sep 17 00:00:00 2001 From: Violetta <38247153+vshakirova@users.noreply.github.com> Date: Sun, 13 Dec 2020 21:08:26 +0400 Subject: [PATCH 07/24] Add information about the startupProbe (#1538) Signed-off-by: vshakirova --- locales/en/messages.po | 4 ++ locales/fi/messages.po | 4 ++ locales/ru/messages.po | 4 ++ src/renderer/api/endpoints/deployment.api.ts | 47 +++++++++---------- src/renderer/api/endpoints/pods.api.ts | 5 ++ .../+workloads-pods/pod-details-container.tsx | 10 ++++ 6 files changed, 50 insertions(+), 24 deletions(-) diff --git a/locales/en/messages.po b/locales/en/messages.po index ec9ac00f25..e120570ac4 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -2664,6 +2664,10 @@ msgstr "Status" #~ msgid "Status URI" #~ msgstr "Status URI" +#: src/renderer/components/+workloads-pods/pod-details-container.tsx:140 +msgid "Startup" +msgstr "Startup" + #: src/renderer/components/+storage-volume-claims/volume-claim-details.tsx:56 #: src/renderer/components/layout/sidebar.tsx:85 msgid "Storage" diff --git a/locales/fi/messages.po b/locales/fi/messages.po index 02995c74db..2a8a650953 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -2646,6 +2646,10 @@ msgstr "" #~ msgid "Status URI" #~ msgstr "" +#: src/renderer/components/+workloads-pods/pod-details-container.tsx:140 +msgid "Startup" +msgstr "" + #: src/renderer/components/+storage-volume-claims/volume-claim-details.tsx:56 #: src/renderer/components/layout/sidebar.tsx:85 msgid "Storage" diff --git a/locales/ru/messages.po b/locales/ru/messages.po index 0f5fb2f9e2..6975d03529 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -2664,6 +2664,10 @@ msgstr "Статус" #~ msgid "Status URI" #~ msgstr "Адрес статуса" +#: src/renderer/components/+workloads-pods/pod-details-container.tsx:140 +msgid "Startup" +msgstr "Cтарт" + #: src/renderer/components/+storage-volume-claims/volume-claim-details.tsx:56 #: src/renderer/components/layout/sidebar.tsx:85 msgid "Storage" diff --git a/src/renderer/api/endpoints/deployment.api.ts b/src/renderer/api/endpoints/deployment.api.ts index d876616470..107e970d98 100644 --- a/src/renderer/api/endpoints/deployment.api.ts +++ b/src/renderer/api/endpoints/deployment.api.ts @@ -46,6 +46,26 @@ export class DeploymentApi extends KubeApi { } } +interface IContainerProbe { + httpGet?: { + path?: string; + port: number; + scheme: string; + host?: string; + }; + exec?: { + command: string[]; + }; + tcpSocket?: { + port: number; + }; + initialDelaySeconds?: number; + timeoutSeconds?: number; + periodSeconds?: number; + successThreshold?: number; + failureThreshold?: number; +} + @autobind() export class Deployment extends WorkloadKubeObject { static kind = "Deployment"; @@ -89,30 +109,9 @@ export class Deployment extends WorkloadKubeObject { name: string; mountPath: string; }[]; - livenessProbe?: { - httpGet: { - path: string; - port: number; - scheme: string; - }; - initialDelaySeconds: number; - timeoutSeconds: number; - periodSeconds: number; - successThreshold: number; - failureThreshold: number; - }; - readinessProbe?: { - httpGet: { - path: string; - port: number; - scheme: string; - }; - initialDelaySeconds: number; - timeoutSeconds: number; - periodSeconds: number; - successThreshold: number; - failureThreshold: number; - }; + livenessProbe?: IContainerProbe; + readinessProbe?: IContainerProbe; + startupProbe?: IContainerProbe; terminationMessagePath: string; terminationMessagePolicy: string; imagePullPolicy: string; diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts index 447503558b..eca15251a7 100644 --- a/src/renderer/api/endpoints/pods.api.ts +++ b/src/renderer/api/endpoints/pods.api.ts @@ -112,6 +112,7 @@ export interface IPodContainer { }[]; livenessProbe?: IContainerProbe; readinessProbe?: IContainerProbe; + startupProbe?: IContainerProbe; imagePullPolicy: string; } @@ -429,6 +430,10 @@ export class Pod extends WorkloadKubeObject { return this.getProbe(container.readinessProbe); } + getStartupProbe(container: IPodContainer) { + return this.getProbe(container.startupProbe); + } + getProbe(probeData: IContainerProbe) { if (!probeData) return []; const { diff --git a/src/renderer/components/+workloads-pods/pod-details-container.tsx b/src/renderer/components/+workloads-pods/pod-details-container.tsx index b883835076..cd95e11b43 100644 --- a/src/renderer/components/+workloads-pods/pod-details-container.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-container.tsx @@ -57,6 +57,7 @@ export class PodDetailsContainer extends React.Component { const ready = status ? status.ready : ""; const liveness = pod.getLivenessProbe(container); const readiness = pod.getReadinessProbe(container); + const startup = pod.getStartupProbe(container); const isInitContainer = !!pod.getInitContainers().find(c => c.name == name); const metricTabs = [ CPU, @@ -140,6 +141,15 @@ export class PodDetailsContainer extends React.Component { } } + {startup.length > 0 && + Startup} labelsOnly> + { + startup.map((value, index) => ( + + )) + } + + } {command && Command}> {command.join(" ")} From da6c114b68edfa8a6699a55dad55e4d954ec83aa Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Sun, 13 Dec 2020 19:09:03 +0200 Subject: [PATCH 08/24] Exclude github actions & docs from azure pipeline (#1655) Signed-off-by: Jari Kolehmainen --- .azure-pipelines.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 8db2859598..24bf47c221 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -1,8 +1,15 @@ variables: YARN_CACHE_FOLDER: $(Pipeline.Workspace)/.yarn pr: - - master - - releases/* + branches: + include: + - master + - releases/* + paths: + exclude: + - .github/* + - docs/* + - mkdocs/* trigger: branches: include: @@ -10,6 +17,11 @@ trigger: tags: include: - "*" + paths: + exclude: + - .github/* + - docs/* + - mkdocs/* jobs: - job: Windows pool: From 2a7fc8713481e81de2669b566f83781fc39e0367 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Sun, 13 Dec 2020 19:11:37 +0200 Subject: [PATCH 09/24] Ensure only one app instance (#1742) * force only one app instance Signed-off-by: Jari Kolehmainen * fix Signed-off-by: Jari Kolehmainen --- src/main/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/index.ts b/src/main/index.ts index ea3c53f9b9..1c6b91c2c4 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -45,6 +45,16 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") { process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server"); } +const instanceLock = app.requestSingleInstanceLock(); + +if (!instanceLock) { + app.exit(); +} + +app.on("second-instance", () => { + windowManager?.ensureMainWindow(); +}); + app.on("ready", async () => { logger.info(`🚀 Starting Lens from "${workingDir}"`); await shellSync(); From e408b0c6d9d3207bff7c26ada463fc97b3781042 Mon Sep 17 00:00:00 2001 From: Mario Sarcher Date: Sun, 13 Dec 2020 18:14:02 +0100 Subject: [PATCH 10/24] Export the pod-details-list component so that it can be used in Extensions (#1746) Signed-off-by: Mario Sarcher Co-authored-by: Mario Sarcher --- src/extensions/renderer-api/components.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extensions/renderer-api/components.ts b/src/extensions/renderer-api/components.ts index 242799c749..358dc6c214 100644 --- a/src/extensions/renderer-api/components.ts +++ b/src/extensions/renderer-api/components.ts @@ -27,6 +27,7 @@ export * from "../../renderer/components/menu"; export * from "../../renderer/components/notifications"; export * from "../../renderer/components/spinner"; export * from "../../renderer/components/stepper"; +export * from "../../renderer/components/+workloads-pods/pod-details-list"; // kube helpers export * from "../../renderer/components/kube-object"; From 3300a99a787663cb05d867be6a6b9ad1c11ce559 Mon Sep 17 00:00:00 2001 From: Panu Horsmalahti Date: Mon, 14 Dec 2020 09:23:59 +0200 Subject: [PATCH 11/24] Display error dialog if extensions couldn't be loaded (#1752) * Display error dialog if extensions couldn't be loaded * Reject npm install on failure using the process exit code Signed-off-by: Panu Horsmalahti --- src/extensions/extension-discovery.ts | 20 ++++++++++++++++---- src/extensions/extension-installer.ts | 20 +++++++++++++++----- src/main/index.ts | 26 +++++++++++++++----------- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 2b26d79a9c..73bddd7481 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -266,18 +266,30 @@ export class ExtensionDiscovery { logger.info(`${logModule} loading extensions from ${extensionInstaller.extensionPackagesRoot}`); - if (fs.existsSync(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json"))) { - await fs.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json")); - } + // fs.remove won't throw if path is missing + await fs.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json")); + try { + // Verify write access to static/extensions, which is needed for symlinking await fs.access(this.inTreeFolderPath, fs.constants.W_OK); + + // Set bundled folder path to static/extensions this.bundledFolderPath = this.inTreeFolderPath; } catch { - // we need to copy in-tree extensions so that we can symlink them properly on "npm install" + // If there is error accessing static/extensions, we need to copy in-tree extensions so that we can symlink them properly on "npm install". + // The error can happen if there is read-only rights to static/extensions, which would fail symlinking. + + // Remove e.g. /Users//Library/Application Support/LensDev/extensions await fs.remove(this.inTreeTargetPath); + + // Create folder e.g. /Users//Library/Application Support/LensDev/extensions await fs.ensureDir(this.inTreeTargetPath); + + // Copy static/extensions to e.g. /Users//Library/Application Support/LensDev/extensions await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath); + + // Set bundled folder path to e.g. /Users//Library/Application Support/LensDev/extensions this.bundledFolderPath = this.inTreeTargetPath; } diff --git a/src/extensions/extension-installer.ts b/src/extensions/extension-installer.ts index 2143c62287..75b30d0b9a 100644 --- a/src/extensions/extension-installer.ts +++ b/src/extensions/extension-installer.ts @@ -33,16 +33,26 @@ export class ExtensionInstaller { installDependencies(): Promise { return new Promise((resolve, reject) => { logger.info(`${logModule} installing dependencies at ${extensionPackagesRoot()}`); - const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], { + const child = child_process.fork(this.npmPath, ["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], { cwd: extensionPackagesRoot(), silent: true }); + let stderr = ""; - child.on("close", () => { - resolve(); + child.stderr.on("data", data => { + stderr += String(data); }); - child.on("error", (err) => { - reject(err); + + child.on("close", (code) => { + if (code !== 0) { + reject(new Error(stderr)); + } else { + resolve(); + } + }); + + child.on("error", error => { + reject(error); }); }); } diff --git a/src/main/index.ts b/src/main/index.ts index 1c6b91c2c4..6f682efb6b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -93,8 +93,8 @@ app.on("ready", async () => { // eslint-disable-next-line unused-imports/no-unused-vars-ts proxyServer = LensProxy.create(proxyPort, clusterManager); } catch (error) { - logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`); - dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`); + logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error?.message}`); + dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error?.message || "unknown error"}`); app.exit(); } @@ -104,17 +104,21 @@ app.on("ready", async () => { windowManager = WindowManager.getInstance(proxyPort); // call after windowManager to see splash earlier - const extensions = await extensionDiscovery.load(); + try { + const extensions = await extensionDiscovery.load(); - // Subscribe to extensions that are copied or deleted to/from the extensions folder - extensionDiscovery.events.on("add", (extension: InstalledExtension) => { - extensionLoader.addExtension(extension); - }); - extensionDiscovery.events.on("remove", (lensExtensionId: LensExtensionId) => { - extensionLoader.removeExtension(lensExtensionId); - }); + // Subscribe to extensions that are copied or deleted to/from the extensions folder + extensionDiscovery.events.on("add", (extension: InstalledExtension) => { + extensionLoader.addExtension(extension); + }); + extensionDiscovery.events.on("remove", (lensExtensionId: LensExtensionId) => { + extensionLoader.removeExtension(lensExtensionId); + }); - extensionLoader.initExtensions(extensions); + extensionLoader.initExtensions(extensions); + } catch (error) { + dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`); + } setTimeout(() => { appEventBus.emit({ name: "service", action: "start" }); From c609ea1d967c486b24029c7c363753fdf9a48888 Mon Sep 17 00:00:00 2001 From: pauljwil Date: Mon, 14 Dec 2020 19:55:04 -0800 Subject: [PATCH 12/24] Reworked Extension Guides (#1727) Edited and reworked content in the Extension Guides Overview and the Generator and Main Extension guides. Signed-off-by: Paul Williams Update docs/extensions/guides/main-extension.md Co-authored-by: Panu Horsmalahti Update docs/extensions/guides/generator.md Co-authored-by: chh <1474479+chenhunghan@users.noreply.github.com> Signed-off-by: Paul Williams Co-authored-by: Paul Williams Co-authored-by: chh <1474479+chenhunghan@users.noreply.github.com> --- docs/extensions/guides/README.md | 10 +++---- docs/extensions/guides/generator.md | 36 ++++++++++++++---------- docs/extensions/guides/main-extension.md | 29 +++++++++++++------ 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/docs/extensions/guides/README.md b/docs/extensions/guides/README.md index 0e15268611..8db209dc82 100644 --- a/docs/extensions/guides/README.md +++ b/docs/extensions/guides/README.md @@ -1,14 +1,14 @@ # Extension Guides -The basics of the Lens Extension API are covered in [Your First Extension](../get-started/your-first-extension.md). In this section detailed code guides and samples are used to explain how to use specific Lens Extension APIs. +This section explains how to use specific Lens Extension APIs. It includes detailed guides and code samples. For introductory information about the Lens Extension API, please see [Your First Extension](../get-started/your-first-extension.md). -Each guide or sample will include: +Each guide or code sample includes the following: - Clearly commented source code. - Instructions for running the sample extension. -- Image of the sample extension's appearance and usage. -- Listing of Extension API being used. -- Explanation of Extension API concepts. +- An image showing the sample extension's appearance and usage. +- A listing of the Extension API being used. +- An explanation of the concepts relevant to the Extension. ## Guides diff --git a/docs/extensions/guides/generator.md b/docs/extensions/guides/generator.md index 68f0ad2e7c..46524e97d5 100644 --- a/docs/extensions/guides/generator.md +++ b/docs/extensions/guides/generator.md @@ -1,15 +1,20 @@ -# New Extension Project with Generator +# Lens Extension Generator -The [Lens Extension Generator](https://github.com/lensapp/generator-lens-ext) scaffolds a project ready for development. Install Yeoman and Lens Extension Generator with: +The [Lens Extension Generator](https://github.com/lensapp/generator-lens-ext) creates a directory with the necessary files for developing an extension. + +## Installing and Getting Started with the Generator + +To begin, install Yeoman and the Lens Extension Generator with the following command: ```bash npm install -g yo generator-lens-ext ``` -Run the generator and fill out a few fields for a TypeScript project: +Run the generator by entering the following command: `yo lens-ext`. + +Answer the following questions: ```bash -yo lens-ext # ? What type of extension do you want to create? New Extension (TypeScript) # ? What's the name of your extension? my-first-lens-ext # ? What's the description of your extension? My hello world extension @@ -17,24 +22,25 @@ yo lens-ext # ? Initialize a git repository? Yes # ? Install dependencies after initialization? Yes # ? Which package manager to use? yarn -# ? symlink created extension folder to ~/.k8slens/extensions (mac/linux) or :User -s\\.k8slens\extensions (windows)? Yes +# ? symlink created extension folder to ~/.k8slens/extensions (mac/linux) or :Users\\.k8slens\extensions (windows)? Yes ``` -Start webpack, which watches the `my-first-lens-ext` folder. +Next, you'll need to have webpack watch the `my-first-lens-ext` folder. Start webpack by entering: ```bash cd my-first-lens-ext npm start # start the webpack server in watch mode ``` -Then, open Lens, you should see a Hello World item in the menu: +Open Lens and you will see a **Hello World** item in the left-side menu under **Custom Resources**: ![Hello World](images/hello-world.png) ## Developing the Extension -Try to change `my-first-lens-ext/renderer.tsx` to "Hello Lens!": +Next, you'll try changing the way the new menu item appears in the UI. You'll change it from "Hello World" to "Hello Lens". + +Open `my-first-lens-ext/renderer.tsx` and change the value of `title` from `"Hello World"` to `"Hello Lens"`: ```tsx clusterPageMenus = [ @@ -48,18 +54,18 @@ clusterPageMenus = [ ] ``` -Then, Reload Lens by CMD+R (Mac) / Ctrl+R (Linux/Windows), you should see the menu item text changes: +Reload Lens and you will see that the menu item text has changed to "Hello Lens." To reload Lens, enter `CMD+R` on Mac and `Ctrl+R` on Windows/Linux. ![Hello World](images/hello-lens.png) ## Debugging the Extension -[Testing](../testing-and-publishing/testing.md) +To debug your extension, please see our instructions on [Testing Extensions](../testing-and-publishing/testing.md). -## Next steps +## Next Steps -You can take a closer look at [Common Capabilities](../capabilities/common-capabilities.md) of extension, how to [style](../capabilities/styling.md) the extension. Or the [Extension Anatomy](anatomy.md). +To dive deeper, consider looking at [Common Capabilities](../capabilities/common-capabilities.md), [Styling](../capabilities/styling.md), or [Extension Anatomy](anatomy.md). -You are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues) for Lens Extension Generator, if you find problems, or have feature requests. +If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues). You can find the Lens contribution guidelines [here](../../contributing/README.md). -The source code of the generator is hosted at [Github](https://github.com/lensapp/generator-lens-ext) +The Generator source code is hosted at [Github](https://github.com/lensapp/generator-lens-ext). diff --git a/docs/extensions/guides/main-extension.md b/docs/extensions/guides/main-extension.md index e1249da0d4..42bb9eedc3 100644 --- a/docs/extensions/guides/main-extension.md +++ b/docs/extensions/guides/main-extension.md @@ -1,12 +1,14 @@ # Main Extension -The main extension api is the interface to Lens' main process (Lens runs in main and renderer processes). It allows you to access, configure, and customize Lens data, add custom application menu items, and generally run custom code in Lens' main process. +The Main Extension API is the interface to Lens's main process. Lens runs in both main and renderer processes. The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items, and run custom code in Lens's main process. ## `LensMainExtension` Class +### `onActivate()` and `onDeactivate()` Methods + To create a main extension simply extend the `LensMainExtension` class: -``` typescript +```typescript import { LensMainExtension } from "@k8slens/extensions"; export default class ExampleExtensionMain extends LensMainExtension { @@ -20,11 +22,18 @@ export default class ExampleExtensionMain extends LensMainExtension { } ``` -There are two methods that you can implement to facilitate running your custom code. `onActivate()` is called when your extension has been successfully enabled. By implementing `onActivate()` you can initiate your custom code. `onDeactivate()` is called when the extension is disabled (typically from the [Lens Extensions Page]()) and when implemented gives you a chance to clean up after your extension, if necessary. The example above simply logs messages when the extension is enabled and disabled. Note that to see standard output from the main process there must be a console connected to it. This is typically achieved by starting Lens from the command prompt. +Two methods enable you to run custom code: `onActivate()` and `onDeactivate()`. Enabling your extension calls `onActivate()` and disabling your extension calls `onDeactivate()`. You can initiate custom code by implementing `onActivate()`. Implementing `onDeactivate()` gives you the opportunity to clean up after your extension. -The following example is a little more interesting in that it accesses some Lens state data and periodically logs the name of the currently active cluster in Lens. +Disable extensions from the Lens Extensions page: -``` typescript +1. Navigate to **File** > **Extensions** in the top menu bar. (On Mac, it is **Lens** > **Extensions**.) +2. Click **Disable** on the extension you want to disable. + +The example above logs messages when the extension is enabled and disabled. To see standard output from the main process there must be a console connected to it. Achieve this by starting Lens from the command prompt. + +The following example is a little more interesting. It accesses some Lens state data, and it periodically logs the name of the cluster that is currently active in Lens. + +```typescript import { LensMainExtension, Store } from "@k8slens/extensions"; const clusterStore = Store.clusterStore @@ -51,11 +60,11 @@ export default class ActiveClusterExtensionMain extends LensMainExtension { } ``` -See the [Stores](../stores) guide for more details on accessing Lens state data. +For more details on accessing Lens state data, please see the [Stores](../stores) guide. ### `appMenus` -The only UI feature customizable in the main extension api is the application menu. Custom menu items can be inserted and linked to custom functionality, such as navigating to a specific page. The following example demonstrates adding a menu item to the Help menu. +The Main Extension API allows you to customize the UI application menu. Note that this is the only UI feature that the Main Extension API allows you to customize. The following example demonstrates adding an item to the **Help** menu. ``` typescript import { LensMainExtension } from "@k8slens/extensions"; @@ -73,4 +82,8 @@ export default class SamplePageMainExtension extends LensMainExtension { } ``` -`appMenus` is an array of objects satisfying the `MenuRegistration` interface. `MenuRegistration` extends React's `MenuItemConstructorOptions` interface. `parentId` is the id of the menu to put this menu item under (todo: is this case sensitive and how do we know what the available ids are?), `label` is the text to show on the menu item, and `click()` is called when the menu item is selected. In this example we simply log a message, but typically you would navigate to a specific page or perform some operation. Pages are associated with the [`LensRendererExtension`](renderer-extension.md) class and can be defined when you extend it. \ No newline at end of file +`appMenus` is an array of objects that satisfy the `MenuRegistration` interface. `MenuRegistration` extends React's `MenuItemConstructorOptions` interface. The properties of the appMenus array objects are defined as follows: + +* `parentId` is the name of the menu where your new menu item will be listed. Valid values include: `"file"`, `"edit"`, `"view"`, and `"help"`. `"lens"` is valid on Mac only. +* `label` is the name of your menu item. +* `click()` is called when the menu item is selected. In this example, we simply log a message. However, you would typically have this navigate to a specific page or perform another operation. Note that pages are associated with the [`LensRendererExtension`](renderer-extension.md) class and can be defined in the process of extending it. From a9a27a13ede17b57816cb1e6336289c416ba50e6 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 15 Dec 2020 08:44:48 +0200 Subject: [PATCH 13/24] Switch master branch version to 4.1.0-alpha.0 (#1766) Signed-off-by: Jari Kolehmainen --- package.json | 2 +- static/RELEASE_NOTES.md | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1ceb2883ea..7db7dd4dea 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.0.0-rc.3", + "version": "4.1.0-alpha.0", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index 3cd54f31b7..b030c0d8b5 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,27 @@ 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.0-rc.3 (current version) +## 4.0.2 (current version) + +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: + +- Fix: use correct apiversion for HPA details +- Fix: use correct apiversion fro CronJob details +- Fix: wrong values in node metrics +- Fix: Deployment scale button "minus" +- Fix: remove symlink on extension install and manual runtime uninstall +- Fix: logs autoscroll behaviour +- Performance fixes + +## 4.0.1 + +- Extension install/uninstall fixes +- Fix status brick styles in pod-menu-extension +- MacOS: fix error on app start +- Performance fix: query all objects using single api call if admin and namespace list is not overridden +- Extension API fix: register a cluster page component properly to a route + +## 4.0.0 - Extension API - Improved pod logs @@ -28,6 +48,7 @@ Here you can find description of changes we've built into each release. While we - Replace cluster warning event polling with watches - Detect more Kubernetes distributions - Performance fix when cluster has lots of namespaces +- Store more than largest kube api request amount in the event store - Fix pod usage metrics on Kubernetes >=1.19 - Fix proxy upgrade socket timeouts - Fix UI staleness after network issues From 11b11158713c9b2e1c890b6e5745ed6b89d6a483 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 15 Dec 2020 13:54:43 +0200 Subject: [PATCH 14/24] Metrics-cluster-feature: bundle semver package (#1781) Signed-off-by: Jari Kolehmainen --- extensions/metrics-cluster-feature/package-lock.json | 3 ++- extensions/metrics-cluster-feature/package.json | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/metrics-cluster-feature/package-lock.json b/extensions/metrics-cluster-feature/package-lock.json index e2df4f9b6d..01a2215984 100644 --- a/extensions/metrics-cluster-feature/package-lock.json +++ b/extensions/metrics-cluster-feature/package-lock.json @@ -5362,7 +5362,8 @@ "semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true }, "serialize-javascript": { "version": "4.0.0", diff --git a/extensions/metrics-cluster-feature/package.json b/extensions/metrics-cluster-feature/package.json index 3b098c6bd9..1b1b5850ec 100644 --- a/extensions/metrics-cluster-feature/package.json +++ b/extensions/metrics-cluster-feature/package.json @@ -12,14 +12,13 @@ "dev": "npm run build --watch", "test": "jest --passWithNoTests --env=jsdom src $@" }, - "dependencies": { - "semver": "^7.3.2" - }, + "dependencies": {}, "devDependencies": { "@k8slens/extensions": "file:../../src/extensions/npm/extensions", "jest": "^26.6.3", "mobx": "^5.15.5", "react": "^16.13.1", + "semver": "^7.3.2", "ts-loader": "^8.0.4", "typescript": "^4.0.3", "webpack": "^4.44.2" From ed036d1f993a960069b6486a401107ee4cfc73ec Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 15 Dec 2020 14:15:04 +0200 Subject: [PATCH 15/24] Install in-tree extensions before others (#1782) Signed-off-by: Jari Kolehmainen --- src/extensions/extension-discovery.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 73bddd7481..7d0da112bb 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -299,7 +299,7 @@ export class ExtensionDiscovery { const extensions = await this.loadExtensions(); this.isLoaded = true; - + return extensions; } @@ -355,6 +355,8 @@ export class ExtensionDiscovery { async loadExtensions(): Promise> { const bundledExtensions = await this.loadBundledExtensions(); + + await this.installPackages(); // install in-tree as a separate step const localExtensions = await this.loadFromFolder(this.localFolderPath); await this.installPackages(); From be3aa88fd465d8bb16134f05ba59308827e92f32 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 17 Dec 2020 08:24:25 -0500 Subject: [PATCH 16/24] use remote.app if app is not defined (#1785) Signed-off-by: Sebastian Malton --- src/main/extension-filesystem.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/extension-filesystem.ts b/src/main/extension-filesystem.ts index c4ee622e1d..eddb7b747f 100644 --- a/src/main/extension-filesystem.ts +++ b/src/main/extension-filesystem.ts @@ -1,6 +1,6 @@ import { randomBytes } from "crypto"; import { SHA256 } from "crypto-js"; -import { app } from "electron"; +import { app, remote } from "electron"; import fse from "fs-extra"; import { action, observable, toJS } from "mobx"; import path from "path"; @@ -31,7 +31,7 @@ export class FilesystemProvisionerStore extends BaseStore { if (!this.registeredExtensions.has(extensionName)) { const salt = randomBytes(32).toString("hex"); const hashedName = SHA256(`${extensionName}/${salt}`).toString(); - const dirPath = path.resolve(app.getPath("userData"), "extension_data", hashedName); + const dirPath = path.resolve((app || remote.app).getPath("userData"), "extension_data", hashedName); this.registeredExtensions.set(extensionName, dirPath); } From 733fc8a658ac0da87276c8ce1f16de940930dae4 Mon Sep 17 00:00:00 2001 From: nyako Date: Thu, 17 Dec 2020 21:24:57 +0800 Subject: [PATCH 17/24] fix "not valid as a React child" error while display crds with column which type is object (#1772) Signed-off-by: liuhongyu --- src/renderer/components/+custom-resources/crd-resources.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index a4a52ef867..cca7ac7015 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -93,7 +93,7 @@ export class CrdResources extends React.Component { isNamespaced && crdInstance.getNs(), ...extraColumns.map(column => ({ renderBoolean: true, - children: jsonPath.value(crdInstance, column.jsonPath.slice(1)), + children: JSON.stringify(jsonPath.value(crdInstance, column.jsonPath.slice(1))), })), crdInstance.getAge(), ]} From de7bd32bc2ae90fe811095c7989fe035114bbc97 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 17 Dec 2020 19:54:30 +0200 Subject: [PATCH 18/24] Fix Openshift distribution detector (#1792) Signed-off-by: Jari Kolehmainen --- src/main/cluster-detectors/distribution-detector.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts index be0cadb1bd..317a49e9a0 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster-detectors/distribution-detector.ts @@ -56,12 +56,12 @@ export class DistributionDetector extends BaseClusterDetector { return { value: "docker-desktop", accuracy: 80}; } - if (this.isCustom()) { - return { value: "custom", accuracy: 10}; + if (this.isCustom() && await this.isOpenshift()) { + return { value: "openshift", accuracy: 90}; } - if (await this.isOpenshift()) { - return { value: "openshift", accuracy: 90}; + if (this.isCustom()) { + return { value: "custom", accuracy: 10}; } return { value: "unknown", accuracy: 10}; From 64888652c469d1caaa4ef90514e610ea5b795d79 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Thu, 17 Dec 2020 19:56:24 +0200 Subject: [PATCH 19/24] Fix Azure distribution detection (#1795) Signed-off-by: Lauri Nevala --- src/main/cluster-detectors/distribution-detector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts index 317a49e9a0..041a8b9158 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster-detectors/distribution-detector.ts @@ -88,7 +88,7 @@ export class DistributionDetector extends BaseClusterDetector { } protected isAKS() { - return this.cluster.apiUrl.endsWith("azmk8s.io"); + return this.cluster.apiUrl.includes("azmk8s.io"); } protected isMirantis() { From 804492251d74a546b0908a345ea5bfe63618b924 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 18 Dec 2020 08:17:27 +0200 Subject: [PATCH 20/24] Fix MacOS error on shutdown (#1798) Signed-off-by: Jari Kolehmainen --- src/main/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/index.ts b/src/main/index.ts index 6f682efb6b..8da9be1a01 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -4,7 +4,7 @@ import "../common/system-ca"; import "../common/prometheus-providers"; import * as Mobx from "mobx"; import * as LensExtensions from "../extensions/core-api"; -import { app, dialog } from "electron"; +import { app, dialog, powerMonitor } from "electron"; import { appName } from "../common/vars"; import path from "path"; import { LensProxy } from "./lens-proxy"; @@ -59,6 +59,10 @@ app.on("ready", async () => { logger.info(`🚀 Starting Lens from "${workingDir}"`); await shellSync(); + powerMonitor.on("shutdown", () => { + app.exit(); + }); + const updater = new AppUpdater(); updater.start(); From 2efd17678040d4ed967ed68eb34b8f13b797e050 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 18 Dec 2020 08:18:05 +0200 Subject: [PATCH 21/24] Bundle kubectl v1.17.15 (#1800) Signed-off-by: Jari Kolehmainen --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7db7dd4dea..e24697b48d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,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.11", + "bundledKubectlVersion": "1.17.15", "bundledHelmVersion": "3.3.4" }, "engines": { From 06041e6169e3928b536063f13aa8118c5587f344 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 18 Dec 2020 12:41:17 +0200 Subject: [PATCH 22/24] Test different kube versions (#1806) * test different kube versions Signed-off-by: Jari Kolehmainen * tweak Signed-off-by: Jari Kolehmainen * tweak Signed-off-by: Jari Kolehmainen * tweak Signed-off-by: Jari Kolehmainen --- .azure-pipelines-k8s-matrix.yml | 57 +++++++++++++++++++++++++++++++++ Makefile | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .azure-pipelines-k8s-matrix.yml diff --git a/.azure-pipelines-k8s-matrix.yml b/.azure-pipelines-k8s-matrix.yml new file mode 100644 index 0000000000..77129460a3 --- /dev/null +++ b/.azure-pipelines-k8s-matrix.yml @@ -0,0 +1,57 @@ +variables: + YARN_CACHE_FOLDER: $(Pipeline.Workspace)/.yarn + node_version: 12.x +pr: + branches: + include: + - master + - releases/* + paths: + exclude: + - .github/* + - docs/* + - mkdocs/* +trigger: none +jobs: + - job: Linux + pool: + vmImage: ubuntu-16.04 + strategy: + matrix: + kube_1.16: + kubernetes_version: v1.16.15 + kube_1.17: + kubernetes_version: v1.17.15 + kube_1.18: + kubernetes_version: v1.18.13 + kube_1.19: + kubernetes_version: v1.19.5 + kube_1.20: + kubernetes_version: v1.20.0 + steps: + - task: NodeTool@0 + inputs: + versionSpec: $(node_version) + displayName: Install Node.js + - task: Cache@2 + inputs: + key: 'yarn | "$(Agent.OS)" | yarn.lock' + restoreKeys: | + yarn | "$(Agent.OS)" + path: $(YARN_CACHE_FOLDER) + displayName: Cache Yarn packages + - bash: | + sudo apt-get update + sudo apt-get install libgconf-2-4 conntrack -y + curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 + sudo install minikube-linux-amd64 /usr/local/bin/minikube + sudo minikube start --driver=none --kubernetes-version $(kubernetes_version) + # Although the kube and minikube config files are in placed $HOME they are owned by root + sudo chown -R $USER $HOME/.kube $HOME/.minikube + displayName: Install integration test dependencies + - script: make node_modules + displayName: Install dependencies + - script: make -j2 build + displayName: Run build + - script: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' yarn integration + displayName: Run integration tests for Kubernetes $(kubernetes_version) diff --git a/Makefile b/Makefile index 1f8d4f5392..40eff8445f 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ binaries/client: yarn download-bins node_modules: - yarn install --frozen-lockfile --verbose + yarn install --frozen-lockfile yarn check --verify-tree --integrity static/build/LensDev.html: From 6fe5bfae5c4d7f733b6fe73be29e673244c09297 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 18 Dec 2020 15:03:04 +0200 Subject: [PATCH 23/24] Generate metadata.selfLink if response does not have it (#1804) * generate metadata.selfLink if response does not have it Signed-off-by: Jari Kolehmainen * fix watches Signed-off-by: Jari Kolehmainen * cleanup Signed-off-by: Jari Kolehmainen * fix Signed-off-by: Jari Kolehmainen --- src/renderer/api/api-manager.ts | 4 ++++ src/renderer/api/kube-api.ts | 34 ++++++++++++++++++++++++------ src/renderer/api/kube-watch-api.ts | 8 ++++--- src/renderer/kube-object.store.ts | 5 ++--- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index 68d4773540..01e5ceb228 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -17,6 +17,10 @@ export class ApiManager { return Array.from(this.apis.values()).find(pathOrCallback ?? (() => true)); } + getApiByKind(kind: string, apiVersion: string) { + return Array.from(this.apis.values()).find((api) => api.kind === kind && api.apiVersion === apiVersion); + } + registerApi(apiBase: string, api: KubeApi) { if (!this.apis.has(apiBase)) { this.apis.set(apiBase, api); diff --git a/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts index e7934675c6..8a3a2517c2 100644 --- a/src/renderer/api/kube-api.ts +++ b/src/renderer/api/kube-api.ts @@ -79,6 +79,18 @@ export function forCluster(cluster: IKubeApiCluster, kubeC }); } +export function ensureObjectSelfLink(api: KubeApi, object: KubeJsonApiData) { + if (!object.metadata.selfLink) { + object.metadata.selfLink = createKubeApiURL({ + apiPrefix: api.apiPrefix, + apiVersion: api.apiVersionWithGroup, + resource: api.apiResource, + namespace: api.isNamespaced ? object.metadata.namespace : undefined, + name: object.metadata.name, + }); + } +} + export class KubeApi { static parseApi = parseKubeApi; @@ -260,7 +272,11 @@ export class KubeApi { const KubeObjectConstructor = this.objectConstructor; if (KubeObject.isJsonApiData(data)) { - return new KubeObjectConstructor(data); + const object = new KubeObjectConstructor(data); + + ensureObjectSelfLink(this, object); + + return object; } // process items list response @@ -270,11 +286,17 @@ export class KubeApi { this.setResourceVersion(namespace, metadata.resourceVersion); this.setResourceVersion("", metadata.resourceVersion); - return items.map(item => new KubeObjectConstructor({ - kind: this.kind, - apiVersion, - ...item, - })); + return items.map((item) => { + const object = new KubeObjectConstructor({ + kind: this.kind, + apiVersion, + ...item, + }); + + ensureObjectSelfLink(this, object); + + return object; + }); } // custom apis might return array for list response, e.g. users, groups, etc. diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index 58665a11a1..78ca25256e 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -5,7 +5,7 @@ import { stringify } from "querystring"; import { autobind, EventEmitter } from "../utils"; import { KubeJsonApiData } from "./kube-json-api"; import type { KubeObjectStore } from "../kube-object.store"; -import { KubeApi } from "./kube-api"; +import { ensureObjectSelfLink, KubeApi } from "./kube-api"; import { apiManager } from "./api-manager"; import { apiPrefix, isDevelopment } from "../../common/vars"; import { getHostedCluster } from "../../common/cluster-store"; @@ -158,12 +158,14 @@ export class KubeWatchApi { addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) { const listener = (evt: IKubeWatchEvent) => { - const { selfLink, namespace, resourceVersion } = evt.object.metadata; - const api = apiManager.getApi(selfLink); + 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); } diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index e23adf3566..bb2fffd819 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -195,10 +195,9 @@ export abstract class KubeObjectStore extends ItemSt const items = this.items.toJS(); for (const {type, object} of this.eventsBuffer.clear()) { - const { uid, selfLink } = object.metadata; - const index = items.findIndex(item => item.getId() === uid); + const index = items.findIndex(item => item.getId() === object.metadata?.uid); const item = items[index]; - const api = apiManager.getApi(selfLink); + const api = apiManager.getApiByKind(object.kind, object.apiVersion); switch (type) { case "ADDED": From 42817a6d972781e26a8700164684223235b529d4 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Fri, 18 Dec 2020 16:42:10 +0300 Subject: [PATCH 24/24] Fix: expand/collapse state for CRD sidebar items (#1751) * Moving SidebarNavItem component to its own file Signed-off-by: Alex Andreev * Using id prop to preserve expanding state Signed-off-by: Alex Andreev --- .../components/layout/sidebar-context.ts | 7 ++ .../components/layout/sidebar-nav-item.scss | 76 ++++++++++++ .../components/layout/sidebar-nav-item.tsx | 83 +++++++++++++ src/renderer/components/layout/sidebar.scss | 94 +------------- src/renderer/components/layout/sidebar.tsx | 116 +++--------------- 5 files changed, 190 insertions(+), 186 deletions(-) create mode 100644 src/renderer/components/layout/sidebar-context.ts create mode 100644 src/renderer/components/layout/sidebar-nav-item.scss create mode 100644 src/renderer/components/layout/sidebar-nav-item.tsx diff --git a/src/renderer/components/layout/sidebar-context.ts b/src/renderer/components/layout/sidebar-context.ts new file mode 100644 index 0000000000..fff192cba3 --- /dev/null +++ b/src/renderer/components/layout/sidebar-context.ts @@ -0,0 +1,7 @@ +import React from "react"; + +export const SidebarContext = React.createContext({ pinned: false }); + +export type SidebarContextValue = { + pinned: boolean; +}; \ No newline at end of file diff --git a/src/renderer/components/layout/sidebar-nav-item.scss b/src/renderer/components/layout/sidebar-nav-item.scss new file mode 100644 index 0000000000..24469dbd0f --- /dev/null +++ b/src/renderer/components/layout/sidebar-nav-item.scss @@ -0,0 +1,76 @@ +.SidebarNavItem { + $itemSpacing: floor($unit / 2.6) floor($unit / 1.6); + + width: 100%; + user-select: none; + flex-shrink: 0; + + .nav-item { + cursor: pointer; + width: inherit; + display: flex; + align-items: center; + text-decoration: none; + border: none; + padding: $itemSpacing; + + &.active, &:hover { + background: $lensBlue; + color: $sidebarActiveColor; + } + } + + .expand-icon { + --size: 20px; + } + + .sub-menu { + border-left: 4px solid transparent; + + &.active { + border-left-color: $lensBlue; + } + + a, .SidebarNavItem { + display: block; + border: none; + text-decoration: none; + color: $textColorPrimary; + font-weight: normal; + padding-left: 40px; // parent icon width + overflow: hidden; + text-overflow: ellipsis; + line-height: 0px; // hidden by default + max-height: 0px; + opacity: 0; + transition: 125ms line-height ease-out, 200ms 100ms opacity; + + &.visible { + line-height: 28px; + max-height: 1000px; + opacity: 1; + } + + &.active, &:hover { + color: $sidebarSubmenuActiveColor; + } + } + + .sub-menu-parent { + padding-left: 27px; + font-weight: 500; + + .nav-item { + &:hover { + background: transparent; + } + } + + .sub-menu { + a { + padding-left: $padding * 3; + } + } + } + } +} \ No newline at end of file diff --git a/src/renderer/components/layout/sidebar-nav-item.tsx b/src/renderer/components/layout/sidebar-nav-item.tsx new file mode 100644 index 0000000000..7dfcbb50e6 --- /dev/null +++ b/src/renderer/components/layout/sidebar-nav-item.tsx @@ -0,0 +1,83 @@ +import "./sidebar-nav-item.scss"; + +import React from "react"; +import { computed, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; +import { NavLink } from "react-router-dom"; + +import { createStorage, cssNames } from "../../utils"; +import { Icon } from "../icon"; +import { SidebarContext } from "./sidebar-context"; + +import type { TabLayoutRoute } from "./tab-layout"; +import type { SidebarContextValue } from "./sidebar-context"; + +interface SidebarNavItemProps { + id: string; // Used to save nav item collapse/expand state in local storage + url: string; + text: React.ReactNode | string; + className?: string; + icon?: React.ReactNode; + isHidden?: boolean; + isActive?: boolean; + subMenus?: TabLayoutRoute[]; +} + +const navItemStorage = createStorage<[string, boolean][]>("sidebar_menu_item", []); +const navItemState = observable.map(navItemStorage.get()); + +reaction(() => [...navItemState], (value) => navItemStorage.set(value)); + +@observer +export class SidebarNavItem extends React.Component { + static contextType = SidebarContext; + public context: SidebarContextValue; + + @computed get isExpanded() { + return navItemState.get(this.props.id); + } + + toggleSubMenu = () => { + navItemState.set(this.props.id, !this.isExpanded); + }; + + render() { + const { isHidden, isActive, subMenus = [], icon, text, url, children, className, id } = this.props; + + if (isHidden) { + return null; + } + const extendedView = (subMenus.length > 0 || children) && this.context.pinned; + + if (extendedView) { + return ( +
+
+ {icon} + {text} + +
+
    + {subMenus.map(({ title, url }) => ( + + {title} + + ))} + {React.Children.toArray(children).map((child: React.ReactElement) => { + return React.cloneElement(child, { + className: cssNames(child.props.className, { visible: this.isExpanded }), + }); + })} +
+
+ ); + } + + return ( + isActive}> + {icon} + {text} + + ); + } +} diff --git a/src/renderer/components/layout/sidebar.scss b/src/renderer/components/layout/sidebar.scss index 1c5932b8d9..1379c87d9e 100644 --- a/src/renderer/components/layout/sidebar.scss +++ b/src/renderer/components/layout/sidebar.scss @@ -1,22 +1,7 @@ .Sidebar { $iconSize: 24px; - $activeBgc: $lensBlue; - $activeTextColor: $sidebarActiveColor; $itemSpacing: floor($unit / 2.6) floor($unit / 1.6); - @mixin activeLinkState { - &.active { - background: $activeBgc; - color: $activeTextColor; - } - @media (hover: hover) { // only for devices supported "true" hover (with mouse or similar) - &:hover { - background: $activeBgc; - color: $activeTextColor; - } - } - } - &.pinned { .sidebar-nav { overflow: auto; @@ -77,13 +62,16 @@ } > a { - @include activeLinkState; - display: flex; align-items: center; text-decoration: none; border: none; padding: $itemSpacing; + + &.active, &:hover { + background: $lensBlue; + color: $sidebarActiveColor; + } } hr { @@ -91,78 +79,6 @@ } } - .SidebarNavItem { - width: 100%; - user-select: none; - flex-shrink: 0; - - .nav-item { - @include activeLinkState; - - cursor: pointer; - width: inherit; - display: flex; - align-items: center; - text-decoration: none; - border: none; - padding: $itemSpacing; - } - - .expand-icon { - --size: 20px; - } - - .sub-menu { - border-left: 4px solid transparent; - - &.active { - border-left-color: $activeBgc; - } - - a, .SidebarNavItem { - display: block; - border: none; - text-decoration: none; - color: $textColorPrimary; - font-weight: normal; - padding-left: 40px; // parent icon width - overflow: hidden; - text-overflow: ellipsis; - line-height: 0px; // hidden by default - max-height: 0px; - opacity: 0; - transition: 125ms line-height ease-out, 200ms 100ms opacity; - - &.visible { - line-height: 28px; - max-height: 1000px; - opacity: 1; - } - - &.active, &:hover { - color: $sidebarSubmenuActiveColor; - } - } - - .sub-menu-parent { - padding-left: 27px; - font-weight: 500; - - .nav-item { - &:hover { - background: transparent; - } - } - - .sub-menu { - a { - padding-left: $padding * 3; - } - } - } - } - } - .loading { padding: $padding; text-align: center; diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index f9f8494318..5386d9fb52 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -1,12 +1,11 @@ -import type { TabLayoutRoute } from "./tab-layout"; import "./sidebar.scss"; import React from "react"; -import { computed, observable, reaction } from "mobx"; +import type { TabLayoutRoute } from "./tab-layout"; import { observer } from "mobx-react"; import { NavLink } from "react-router-dom"; import { Trans } from "@lingui/macro"; -import { createStorage, cssNames } from "../../utils"; +import { cssNames } from "../../utils"; import { Icon } from "../icon"; import { workloadsRoute, workloadsURL } from "../+workloads/workloads.route"; import { namespacesRoute, namespacesURL } from "../+namespaces/namespaces.route"; @@ -30,12 +29,8 @@ import { isActiveRoute } from "../../navigation"; import { isAllowedResource } from "../../../common/rbac"; import { Spinner } from "../spinner"; import { ClusterPageMenuRegistration, clusterPageMenuRegistry, clusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries"; - -const SidebarContext = React.createContext({ pinned: false }); - -type SidebarContextValue = { - pinned: boolean; -}; +import { SidebarNavItem } from "./sidebar-nav-item"; +import { SidebarContext } from "./sidebar-context"; interface Props { className?: string; @@ -69,6 +64,7 @@ export class Sidebar extends React.Component { return ( { return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => { const registeredPage = clusterPageRegistry.getByPageMenuTarget(menuItem.target); const tabRoutes = this.getTabLayoutRoutes(menuItem); + const id = `registered-item-${index}`; let pageUrl: string; let isActive = false; @@ -122,7 +119,8 @@ export class Sidebar extends React.Component { return ( } @@ -155,7 +153,7 @@ export class Sidebar extends React.Component {
{ icon={} /> { icon={} /> { icon={} /> { icon={} /> { icon={} /> { text={Storage} /> { text={Namespaces} /> { text={Events} /> { text={Apps} /> { text={Access Control} /> { ); } } - -interface SidebarNavItemProps { - url: string; - text: React.ReactNode | string; - className?: string; - icon?: React.ReactNode; - isHidden?: boolean; - isActive?: boolean; - subMenus?: TabLayoutRoute[]; - testId?: string; // data-test-id="" property for integration tests -} - -const navItemStorage = createStorage<[string, boolean][]>("sidebar_menu_item", []); -const navItemState = observable.map(navItemStorage.get()); - -reaction(() => [...navItemState], (value) => navItemStorage.set(value)); - -@observer -class SidebarNavItem extends React.Component { - static contextType = SidebarContext; - public context: SidebarContextValue; - - get itemId() { - const url = new URL(this.props.url, `${window.location.protocol}//${window.location.host}`); - - return url.pathname; // pathname without get params - } - - @computed get isExpanded() { - return navItemState.get(this.itemId); - } - - toggleSubMenu = () => { - navItemState.set(this.itemId, !this.isExpanded); - }; - - render() { - const { isHidden, isActive, subMenus = [], icon, text, url, children, className, testId } = this.props; - - if (isHidden) { - return null; - } - const extendedView = (subMenus.length > 0 || children) && this.context.pinned; - - if (extendedView) { - return ( -
-
- {icon} - {text} - -
-
    - {subMenus.map(({ title, url }) => ( - - {title} - - ))} - {React.Children.toArray(children).map((child: React.ReactElement) => { - return React.cloneElement(child, { - className: cssNames(child.props.className, { visible: this.isExpanded }), - }); - })} -
-
- ); - } - - return ( - isActive}> - {icon} - {text} - - ); - } -}