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 diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 8672076226..14b55a3c74 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/package.json b/package.json index 3b5a340052..ebc045dc4d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.0.1", + "version": "4.0.2", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/src/extensions/__tests__/extension-discovery.test.ts b/src/extensions/__tests__/extension-discovery.test.ts index 0e72cf16fb..0317319329 100644 --- a/src/extensions/__tests__/extension-discovery.test.ts +++ b/src/extensions/__tests__/extension-discovery.test.ts @@ -18,7 +18,7 @@ const mockedWatch = watch as jest.MockedFunction; describe("ExtensionDiscovery", () => { it("emits add for added extension", async done => { - globalThis.__non_webpack_require__.mockImplementationOnce(() => ({ + globalThis.__non_webpack_require__.mockImplementation(() => ({ name: "my-extension" })); let addHandler: (filePath: string) => void; @@ -61,9 +61,6 @@ describe("ExtensionDiscovery", () => { }); it("doesn't emit add for added file under extension", async done => { - globalThis.__non_webpack_require__.mockImplementationOnce(() => ({ - name: "my-extension" - })); let addHandler: (filePath: string) => void; const mockWatchInstance: any = { diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 287c232be9..2b26d79a9c 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -155,23 +155,26 @@ export class ExtensionDiscovery { .on("unlinkDir", this.handleWatchUnlinkDir); } - handleWatchFileAdd = async (filePath: string) => { + handleWatchFileAdd = async (manifestPath: string) => { // e.g. "foo/package.json" - const relativePath = path.relative(this.localFolderPath, filePath); + const relativePath = path.relative(this.localFolderPath, manifestPath); // Converts "foo/package.json" to ["foo", "package.json"], where length of 2 implies // that the added file is in a folder under local folder path. // This safeguards against a file watch being triggered under a sub-directory which is not an extension. const isUnderLocalFolderPath = relativePath.split(path.sep).length === 2; - if (path.basename(filePath) === manifestFilename && isUnderLocalFolderPath) { + if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) { try { - const absPath = path.dirname(filePath); + const absPath = path.dirname(manifestPath); // this.loadExtensionFromPath updates this.packagesJson - const extension = await this.loadExtensionFromPath(absPath); + const extension = await this.loadExtensionFromFolder(absPath); if (extension) { + // Remove a broken symlink left by a previous installation if it exists. + await this.removeSymlinkByManifestPath(manifestPath); + // Install dependencies for the new extension await this.installPackages(); @@ -199,6 +202,9 @@ export class ExtensionDiscovery { .find(([, extensionFolder]) => filePath === extensionFolder)?.[0]; if (extensionName !== undefined) { + // If the extension is deleted manually while the application is running, also remove the symlink + await this.removeSymlinkByPackageName(extensionName); + delete this.packagesJson.dependencies[extensionName]; // Reinstall dependencies to remove the extension from package.json @@ -216,6 +222,26 @@ export class ExtensionDiscovery { } }; + /** + * Remove the symlink under node_modules if exists. + * If we don't remove the symlink, the uninstall would leave a non-working symlink, + * which wouldn't be fixed if the extension was reinstalled, causing the extension not to work. + * @param name e.g. "@mirantis/lens-extension-cc" + */ + removeSymlinkByPackageName(name: string) { + return fs.remove(this.getInstalledPath(name)); + } + + /** + * Remove the symlink under node_modules if it exists. + * @param manifestPath Path to package.json + */ + removeSymlinkByManifestPath(manifestPath: string) { + const manifestJson = __non_webpack_require__(manifestPath); + + return this.removeSymlinkByPackageName(manifestJson.name); + } + /** * Uninstalls extension. * The application will detect the folder unlink and remove the extension from the UI automatically. @@ -224,16 +250,7 @@ export class ExtensionDiscovery { async uninstallExtension({ absolutePath, manifest }: InstalledExtension) { logger.info(`${logModule} Uninstalling ${manifest.name}`); - // remove the symlink under node_modules. - // If we don't remove the symlink, the uninstall would leave a non-working symlink, - // which wouldn't be fixed if the extension was reinstalled, causing the extension not to work. - await fs.remove(this.getInstalledPath(manifest.name)); - - const exists = await fs.pathExists(absolutePath); - - if (!exists) { - throw new Error(`Extension path ${absolutePath} doesn't exist`); - } + await this.removeSymlinkByPackageName(manifest.name); // fs.remove does nothing if the path doesn't exist anymore await fs.remove(absolutePath); @@ -290,6 +307,10 @@ export class ExtensionDiscovery { return path.join(this.getInstalledPath(name), manifestFilename); } + /** + * Returns InstalledExtension from path to package.json file. + * Also updates this.packagesJson. + */ protected async getByManifest(manifestPath: string, { isBundled = false }: { isBundled?: boolean; } = {}): Promise { @@ -349,7 +370,7 @@ export class ExtensionDiscovery { } const absPath = path.resolve(folderPath, fileName); - const extension = await this.loadExtensionFromPath(absPath, { isBundled: true }); + const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true }); if (extension) { extensions.push(extension); @@ -384,7 +405,7 @@ export class ExtensionDiscovery { continue; } - const extension = await this.loadExtensionFromPath(absPath); + const extension = await this.loadExtensionFromFolder(absPath); if (extension) { extensions.push(extension); @@ -398,8 +419,9 @@ export class ExtensionDiscovery { /** * Loads extension from absolute path, updates this.packagesJson to include it and returns the extension. + * @param absPath Folder path to extension */ - async loadExtensionFromPath(absPath: string, { isBundled = false }: { + async loadExtensionFromFolder(absPath: string, { isBundled = false }: { isBundled?: boolean; } = {}): Promise { const manifestPath = path.resolve(absPath, manifestFilename); 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/+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) => 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, 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) => 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
diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 1fb57fc0b9..30b1be8ab2 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"; @@ -180,7 +180,7 @@ export class App extends React.Component { - + diff --git a/src/renderer/components/dock/pod-log-list.tsx b/src/renderer/components/dock/pod-log-list.tsx index 392e0eefc0..a3ab172a16 100644 --- a/src/renderer/components/dock/pod-log-list.tsx +++ b/src/renderer/components/dock/pod-log-list.tsx @@ -95,15 +95,9 @@ export class PodLogList extends React.Component { @action setLastLineVisibility = (props: ListOnScrollProps) => { const { scrollHeight, clientHeight } = this.virtualListDiv.current; - const { scrollOffset, scrollDirection } = props; + const { scrollOffset } = props; - if (scrollDirection == "backward") { - this.isLastLineVisible = false; - } else { - if (clientHeight + scrollOffset === scrollHeight) { - this.isLastLineVisible = true; - } - } + this.isLastLineVisible = (clientHeight + scrollOffset) === scrollHeight; }; /** diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index e914b6ad01..b030c0d8b5 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,19 @@ 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.1 (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