diff --git a/package.json b/package.json index 4004ef119d..ae69de1fdd 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "5.4.5", + "version": "5.4.6", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT", diff --git a/src/common/utils/__tests__/convert-memory.test.ts b/src/common/utils/__tests__/convert-memory.test.ts new file mode 100644 index 0000000000..5acc10e5ab --- /dev/null +++ b/src/common/utils/__tests__/convert-memory.test.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { bytesToUnits, unitsToBytes } from "../convertMemory"; + +describe("unitsToBytes", () => { + it("without any units, just parse as float", () => { + expect(unitsToBytes("1234")).toBe(1234); + }); + + it("given unrelated data, return NaN", () => { + expect(unitsToBytes("I am not a number")).toBeNaN(); + }); + + it("given unrelated data, but has number, return that", () => { + expect(unitsToBytes("I am not a number, but this is 0.1")).toBe(0.1); + }); +}); + +describe("bytesToUnits", () => { + it("should return N/A for invalid bytes", () => { + expect(bytesToUnits(-1)).toBe("N/A"); + expect(bytesToUnits(Infinity)).toBe("N/A"); + expect(bytesToUnits(NaN)).toBe("N/A"); + }); + + it("given a number within the magnitude of 0..124, format with B", () => { + expect(bytesToUnits(100)).toBe("100.0B"); + }); + + it("given a number within the magnitude of 1024..1024^2, format with KiB", () => { + expect(bytesToUnits(1024)).toBe("1.0KiB"); + expect(bytesToUnits(2048)).toBe("2.0KiB"); + expect(bytesToUnits(1900)).toBe("1.9KiB"); + expect(bytesToUnits(50*1024 + 1)).toBe("50.0KiB"); + }); + + it("given a number within the magnitude of 1024^2..1024^3, format with MiB", () => { + expect(bytesToUnits(1024**2)).toBe("1.0MiB"); + expect(bytesToUnits(2048**2)).toBe("4.0MiB"); + expect(bytesToUnits(1900 * 1024)).toBe("1.9MiB"); + expect(bytesToUnits(50*(1024 ** 2) + 1)).toBe("50.0MiB"); + }); + + it("given a number within the magnitude of 1024^3..1024^4, format with GiB", () => { + expect(bytesToUnits(1024**3)).toBe("1.0GiB"); + expect(bytesToUnits(2048**3)).toBe("8.0GiB"); + expect(bytesToUnits(1900 * 1024 ** 2)).toBe("1.9GiB"); + expect(bytesToUnits(50*(1024 ** 3) + 1)).toBe("50.0GiB"); + }); + + it("given a number within the magnitude of 1024^4..1024^5, format with TiB", () => { + expect(bytesToUnits(1024**4)).toBe("1.0TiB"); + expect(bytesToUnits(2048**4)).toBe("16.0TiB"); + expect(bytesToUnits(1900 * 1024 ** 3)).toBe("1.9TiB"); + expect(bytesToUnits(50*(1024 ** 4) + 1)).toBe("50.0TiB"); + }); + + it("given a number within the magnitude of 1024^5..1024^6, format with PiB", () => { + expect(bytesToUnits(1024**5)).toBe("1.0PiB"); + expect(bytesToUnits(2048**5)).toBe("32.0PiB"); + expect(bytesToUnits(1900 * 1024 ** 4)).toBe("1.9PiB"); + expect(bytesToUnits(50*(1024 ** 5) + 1)).toBe("50.0PiB"); + }); + + it("given a number within the magnitude of 1024^6.., format with EiB", () => { + expect(bytesToUnits(1024**6)).toBe("1.0EiB"); + expect(bytesToUnits(2048**6)).toBe("64.0EiB"); + expect(bytesToUnits(1900 * 1024 ** 5)).toBe("1.9EiB"); + expect(bytesToUnits(50*(1024 ** 6) + 1)).toBe("50.0EiB"); + expect(bytesToUnits(1024**8)).toBe("1048576.0EiB"); + }); +}); + +describe("bytesToUnits -> unitsToBytes", () => { + it("given an input, round trip to the same value, given enough precision", () => { + expect(unitsToBytes(bytesToUnits(123))).toBe(123); + expect(unitsToBytes(bytesToUnits(1024**0 + 1, { precision: 2 }))).toBe(1024**0 + 1); + expect(unitsToBytes(bytesToUnits(1024**1 + 2, { precision: 3 }))).toBe(1024**1 + 2); + expect(unitsToBytes(bytesToUnits(1024**2 + 3, { precision: 6 }))).toBe(1024**2 + 3); + expect(unitsToBytes(bytesToUnits(1024**3 + 4, { precision: 9 }))).toBe(1024**3 + 4); + expect(unitsToBytes(bytesToUnits(1024**4 + 5, { precision: 12 }))).toBe(1024**4 + 5); + expect(unitsToBytes(bytesToUnits(1024**5 + 6, { precision: 16 }))).toBe(1024**5 + 6); + expect(unitsToBytes(bytesToUnits(1024**6 + 7, { precision: 20 }))).toBe(1024**6 + 7); + }); + + it("given an invalid input, round trips to NaN", () => { + expect(unitsToBytes(bytesToUnits(-1))).toBeNaN(); + }); +}); diff --git a/src/common/utils/convertMemory.ts b/src/common/utils/convertMemory.ts index 7fed4502b6..d06b33d64a 100644 --- a/src/common/utils/convertMemory.ts +++ b/src/common/utils/convertMemory.ts @@ -3,35 +3,59 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +import assert from "assert"; +import * as iter from "./iter"; + // Helper to convert memory from units Ki, Mi, Gi, Ti, Pi to bytes and vise versa -const base = 1024; -const suffixes = ["K", "M", "G", "T", "P", "E"]; // Equivalents: Ki, Mi, Gi, Ti, Pi, Ei +const baseMagnitude = 1024; +const maxMagnitude = ["EiB", baseMagnitude ** 6] as const; +const magnitudes = new Map([ + ["B", 1] as const, + ["KiB", baseMagnitude ** 1] as const, + ["MiB", baseMagnitude ** 2] as const, + ["GiB", baseMagnitude ** 3] as const, + ["TiB", baseMagnitude ** 4] as const, + ["PiB", baseMagnitude ** 5] as const, + maxMagnitude, +]); +const unitRegex = /(?[0-9]+(\.[0-9]*)?)(?(B|[KMGTPE]iB))?/; -export function unitsToBytes(value: string) { - if (!suffixes.some(suffix => value.includes(suffix))) { - return parseFloat(value); +export function unitsToBytes(value: string): number { + const unitsMatch = value.match(unitRegex); + + if (!unitsMatch?.groups) { + return NaN; } - - const suffix = value.replace(/[0-9]|i|\./g, ""); - const index = suffixes.indexOf(suffix); - return parseInt( - (parseFloat(value) * Math.pow(base, index + 1)).toFixed(1), - ); + const parsedValue = parseFloat(unitsMatch.groups.value); + + if (!unitsMatch.groups?.suffix) { + return parsedValue; + } + + const magnitude = magnitudes.get(unitsMatch.groups.suffix as never); + + assert(magnitude, "UnitRegex is wrong some how"); + + return parseInt((parsedValue * magnitude).toFixed(1)); } -export function bytesToUnits(bytes: number, precision = 1) { - const sizes = ["B", ...suffixes]; - const index = Math.floor(Math.log(bytes) / Math.log(base)); +export interface BytesToUnitesOptions { + /** + * The number of decimal places. MUST be an integer. MUST be in the range [0, 20]. + * @default 1 + */ + precision?: number; +} - if (!bytes) { +export function bytesToUnits(bytes: number, { precision = 1 }: BytesToUnitesOptions = {}): string { + if (bytes <= 0 || isNaN(bytes) || !isFinite(bytes)) { return "N/A"; } - if (index === 0) { - return `${bytes}${sizes[index]}`; - } + const index = Math.floor(Math.log(bytes) / Math.log(baseMagnitude)); + const [suffix, magnitude] = iter.nth(magnitudes.entries(), index) ?? maxMagnitude; - return `${(bytes / (1024 ** index)).toFixed(precision)}${sizes[index]}i`; + return `${(bytes / magnitude).toFixed(precision)}${suffix}`; } diff --git a/src/common/utils/iter.ts b/src/common/utils/iter.ts index 9a26162d8f..e1a9f41724 100644 --- a/src/common/utils/iter.ts +++ b/src/common/utils/iter.ts @@ -171,6 +171,23 @@ export function join(src: Iterable, connector = ","): string { return reduce(src, (acc, cur) => `${acc}${connector}${cur}`, ""); } +/** + * Returns the next value after iterating over the iterable `index` times. + * + * For example: `nth(["a", "b"], 0)` will return `"a"` + * For example: `nth(["a", "b"], 1)` will return `"b"` + * For example: `nth(["a", "b"], 2)` will return `undefined` + */ +export function nth(src: Iterable, index: number): T | undefined { + const iteree = src[Symbol.iterator](); + + while (index-- > 0) { + iteree.next(); + } + + return iteree.next().value; +} + /** * Iterate through `src` and return `true` if `fn` returns a thruthy value for every yielded value. * Otherwise, return `false`. This function short circuits. diff --git a/src/common/vars.ts b/src/common/vars.ts index abbf09e877..3daec7d21f 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -60,6 +60,13 @@ export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as stri export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ" as string; export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string; +export const lensWebsiteWeblinkId = "lens-website-link"; +export const lensDocumentationWeblinkId = "lens-documentation-link"; +export const lensSlackWeblinkId = "lens-slack-link"; +export const lensTwitterWeblinkId = "lens-twitter-link"; +export const lensBlogWeblinkId = "lens-blog-link"; +export const kubernetesDocumentationWeblinkId = "kubernetes-documentation-link"; + export const appSemVer = new SemVer(packageInfo.version); export const docsUrl = "https://docs.k8slens.dev/main/" as string; diff --git a/src/common/weblink-store.ts b/src/common/weblink-store.ts index 7606d894c2..a8430666e5 100644 --- a/src/common/weblink-store.ts +++ b/src/common/weblink-store.ts @@ -54,12 +54,11 @@ export class WeblinkStore extends BaseStore { name, url, } = data; + const weblink: WeblinkData = { id, name, url }; - const weblink = { id, name, url }; + this.weblinks.push(weblink); - this.weblinks.push(weblink as WeblinkData); - - return weblink as WeblinkData; + return weblink; } @action diff --git a/src/migrations/helpers.ts b/src/migrations/helpers.ts index 4e97f7f681..6b35a86004 100644 --- a/src/migrations/helpers.ts +++ b/src/migrations/helpers.ts @@ -20,7 +20,7 @@ export interface MigrationDeclaration { } export function joinMigrations(...declarations: MigrationDeclaration[]): Migrations { - const migrations = new Map) => void)[]>(); + const migrations = new Map(); for (const decl of declarations) { getOrInsert(migrations, decl.version, []).push(decl.run); diff --git a/src/migrations/weblinks-store/5.1.4.ts b/src/migrations/weblinks-store/5.1.4.ts index d36016e7fa..59c47be8b0 100644 --- a/src/migrations/weblinks-store/5.1.4.ts +++ b/src/migrations/weblinks-store/5.1.4.ts @@ -7,6 +7,13 @@ import { docsUrl, slackUrl } from "../../common/vars"; import type { WeblinkData } from "../../common/weblink-store"; import type { MigrationDeclaration } from "../helpers"; +export const lensWebsiteLinkName = "Lens Website"; +export const lensDocumentationWeblinkName = "Lens Documentation"; +export const lensSlackWeblinkName = "Lens Community Slack"; +export const lensTwitterWeblinkName = "Lens on Twitter"; +export const lensBlogWeblinkName = "Lens Official Blog"; +export const kubernetesDocumentationWeblinkName = "Kubernetes Documentation"; + export default { version: "5.1.4", run(store) { @@ -14,12 +21,12 @@ export default { const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[]; weblinks.push( - { id: "https://k8slens.dev", name: "Lens Website", url: "https://k8slens.dev" }, - { id: docsUrl, name: "Lens Documentation", url: docsUrl }, - { id: slackUrl, name: "Lens Community Slack", url: slackUrl }, - { id: "https://kubernetes.io/docs/home/", name: "Kubernetes Documentation", url: "https://kubernetes.io/docs/home/" }, - { id: "https://twitter.com/k8slens", name: "Lens on Twitter", url: "https://twitter.com/k8slens" }, - { id: "https://medium.com/k8slens", name: "Lens Official Blog", url: "https://medium.com/k8slens" }, + { id: "https://k8slens.dev", name: lensWebsiteLinkName, url: "https://k8slens.dev" }, + { id: docsUrl, name: lensDocumentationWeblinkName, url: docsUrl }, + { id: slackUrl, name: lensSlackWeblinkName, url: slackUrl }, + { id: "https://twitter.com/k8slens", name: lensTwitterWeblinkName, url: "https://twitter.com/k8slens" }, + { id: "https://medium.com/k8slens", name: lensBlogWeblinkName, url: "https://medium.com/k8slens" }, + { id: "https://kubernetes.io/docs/home/", name: kubernetesDocumentationWeblinkName, url: "https://kubernetes.io/docs/home/" }, ); store.set("weblinks", weblinks); diff --git a/src/migrations/weblinks-store/5.4.5-beta.1.ts b/src/migrations/weblinks-store/5.4.5-beta.1.ts new file mode 100644 index 0000000000..57d6f5c4ba --- /dev/null +++ b/src/migrations/weblinks-store/5.4.5-beta.1.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { kubernetesDocumentationWeblinkId, lensBlogWeblinkId, lensDocumentationWeblinkId, lensSlackWeblinkId, lensTwitterWeblinkId, lensWebsiteWeblinkId } from "../../common/vars"; +import type { WeblinkData } from "../../common/weblink-store"; +import type { MigrationDeclaration } from "../helpers"; +import { kubernetesDocumentationWeblinkName, lensBlogWeblinkName, lensDocumentationWeblinkName, lensSlackWeblinkName, lensTwitterWeblinkName, lensWebsiteLinkName } from "./5.1.4"; + +export default { + version: "5.4.5-beta.1 || >=5.5.0-alpha.0", + run(store) { + const weblinksRaw: any = store.get("weblinks"); + const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[]; + + const lensWebsiteLink = weblinks.find(weblink => weblink.name === lensWebsiteLinkName); + + if (lensWebsiteLink) { + lensWebsiteLink.id = lensWebsiteWeblinkId; + } + + const lensDocumentationWeblinkLink = weblinks.find(weblink => weblink.name === lensDocumentationWeblinkName); + + if (lensDocumentationWeblinkLink) { + lensDocumentationWeblinkLink.id = lensDocumentationWeblinkId; + } + + const lensSlackWeblinkLink = weblinks.find(weblink => weblink.name === lensSlackWeblinkName); + + if (lensSlackWeblinkLink) { + lensSlackWeblinkLink.id = lensSlackWeblinkId; + } + + const lensTwitterWeblinkLink = weblinks.find(weblink => weblink.name === lensTwitterWeblinkName); + + if (lensTwitterWeblinkLink) { + lensTwitterWeblinkLink.id = lensTwitterWeblinkId; + } + + const lensBlogWeblinkLink = weblinks.find(weblink => weblink.name === lensBlogWeblinkName); + + if (lensBlogWeblinkLink) { + lensBlogWeblinkLink.id = lensBlogWeblinkId; + } + + const kubernetesDocumentationWeblinkLink = weblinks.find(weblink => weblink.name === kubernetesDocumentationWeblinkName); + + if (kubernetesDocumentationWeblinkLink) { + kubernetesDocumentationWeblinkLink.id = kubernetesDocumentationWeblinkId; + } + + store.set("weblinks", weblinks); + }, +} as MigrationDeclaration; diff --git a/src/migrations/weblinks-store/currentVersion.ts b/src/migrations/weblinks-store/currentVersion.ts new file mode 100644 index 0000000000..6a319bc286 --- /dev/null +++ b/src/migrations/weblinks-store/currentVersion.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getAppVersion } from "../../common/utils"; +import { lensSlackWeblinkId, slackUrl } from "../../common/vars"; +import type { WeblinkData } from "../../common/weblink-store"; +import type { MigrationDeclaration } from "../helpers"; + +export default { + version: getAppVersion(), // Run always after upgrade + run(store) { + const weblinksRaw: any = store.get("weblinks"); + const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[]; + const slackWeblink = weblinks.find(weblink => weblink.id === lensSlackWeblinkId); + + if (slackWeblink) { + slackWeblink.url = slackUrl; + } + + store.set("weblinks", weblinks); + }, +} as MigrationDeclaration; diff --git a/src/migrations/weblinks-store/index.ts b/src/migrations/weblinks-store/index.ts index 6b42e44c53..a6e6ea1eea 100644 --- a/src/migrations/weblinks-store/index.ts +++ b/src/migrations/weblinks-store/index.ts @@ -6,7 +6,11 @@ import { joinMigrations } from "../helpers"; import version514 from "./5.1.4"; +import version545Beta1 from "./5.4.5-beta.1"; +import currentVersion from "./currentVersion"; export default joinMigrations( version514, + version545Beta1, + currentVersion, ); diff --git a/src/renderer/components/+cluster/cluster-metrics.tsx b/src/renderer/components/+cluster/cluster-metrics.tsx index 11cbb68084..2d6ad36367 100644 --- a/src/renderer/components/+cluster/cluster-metrics.tsx +++ b/src/renderer/components/+cluster/cluster-metrics.tsx @@ -72,7 +72,7 @@ const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType label: ({ index }, data) => { const value = data.datasets[0].data[index] as ChartPoint; - return bytesToUnits(parseInt(value.y as string), 3); + return bytesToUnits(parseInt(value.y as string), { precision: 3 }); }, }, }, diff --git a/src/renderer/components/+cluster/cluster-pie-charts.tsx b/src/renderer/components/+cluster/cluster-pie-charts.tsx index f955ece37b..775d3bacf0 100644 --- a/src/renderer/components/+cluster/cluster-pie-charts.tsx +++ b/src/renderer/components/+cluster/cluster-pie-charts.tsx @@ -11,7 +11,8 @@ import { ClusterOverviewStore, MetricNodeRole } from "./cluster-overview-store/c import { Spinner } from "../spinner"; import { Icon } from "../icon"; import { nodesStore } from "../+nodes/nodes.store"; -import { ChartData, PieChart } from "../chart"; +import type { PieChartData } from "../chart"; +import { PieChart } from "../chart"; import { ClusterNoMetrics } from "./cluster-no-metrics"; import { bytesToUnits, cssNames } from "../../utils"; import { ThemeStore } from "../../theme.store"; @@ -47,7 +48,7 @@ const NonInjectedClusterPieCharts = observer(({ clusterOverviewStore }: Dependen const defaultColor = ThemeStore.getInstance().activeTheme.colors.pieChartDefaultColor; if (!memoryCapacity || !cpuCapacity || !podCapacity || !memoryAllocatableCapacity || !cpuAllocatableCapacity || !podAllocatableCapacity) return null; - const cpuData: ChartData = { + const cpuData: PieChartData = { datasets: [ { data: [ @@ -94,7 +95,7 @@ const NonInjectedClusterPieCharts = observer(({ clusterOverviewStore }: Dependen ["Capacity", cpuCapacity], ]), }; - const memoryData: ChartData = { + const memoryData: PieChartData = { datasets: [ { data: [ @@ -141,7 +142,7 @@ const NonInjectedClusterPieCharts = observer(({ clusterOverviewStore }: Dependen `Capacity: ${bytesToUnits(memoryCapacity)}`, ], }; - const podsData: ChartData = { + const podsData: PieChartData = { datasets: [ { data: [ @@ -154,6 +155,10 @@ const NonInjectedClusterPieCharts = observer(({ clusterOverviewStore }: Dependen ], id: "podUsage", label: "Usage", + tooltipLabels: [ + (percent) => `Usage: ${percent}`, + (percent) => `Available: ${percent}`, + ], }, ], labels: [ diff --git a/src/renderer/components/+nodes/route.tsx b/src/renderer/components/+nodes/route.tsx index 8801e6e832..5e92ca9e62 100644 --- a/src/renderer/components/+nodes/route.tsx +++ b/src/renderer/components/+nodes/route.tsx @@ -130,7 +130,7 @@ export class NodesRoute extends React.Component { metricNames: ["workloadMemoryUsage", "memoryAllocatableCapacity"], formatters: [ ([usage, capacity]) => `${(usage * 100 / capacity).toFixed(2)}%`, - ([usage]) => bytesToUnits(usage, 3), + ([usage]) => bytesToUnits(usage, { precision: 3 }), ], }); } @@ -142,7 +142,7 @@ export class NodesRoute extends React.Component { metricNames: ["fsUsage", "fsSize"], formatters: [ ([usage, capacity]) => `${(usage * 100 / capacity).toFixed(2)}%`, - ([usage]) => bytesToUnits(usage, 3), + ([usage]) => bytesToUnits(usage, { precision: 3 }), ], }); } diff --git a/src/renderer/components/+workloads-overview/overview-workload-status.scss b/src/renderer/components/+workloads-overview/overview-workload-status.scss index 087cd25888..f8b2aadfd5 100644 --- a/src/renderer/components/+workloads-overview/overview-workload-status.scss +++ b/src/renderer/components/+workloads-overview/overview-workload-status.scss @@ -13,10 +13,4 @@ --workload-status-failed: #{$pod-status-failed-color}; --workload-status-terminated: #{$pod-status-terminated-color}; --workload-status-unknown: #{$pod-status-unknown-color}; - - .PieChart { - .chart-container { - width: 110px - } - } } diff --git a/src/renderer/components/+workloads-overview/overview-workload-status.tsx b/src/renderer/components/+workloads-overview/overview-workload-status.tsx index d042ba1597..5a1751f39c 100644 --- a/src/renderer/components/+workloads-overview/overview-workload-status.tsx +++ b/src/renderer/components/+workloads-overview/overview-workload-status.tsx @@ -8,9 +8,9 @@ import "./overview-workload-status.scss"; import React from "react"; import capitalize from "lodash/capitalize"; import { observer } from "mobx-react"; +import type { DatasetTooltipLabel, PieChartData } from "../chart"; import { PieChart } from "../chart"; import { cssVar } from "../../utils"; -import type { ChartData } from "chart.js"; import { ThemeStore } from "../../theme.store"; interface Props { @@ -27,7 +27,7 @@ export class OverviewWorkloadStatus extends React.Component { } const cssVars = cssVar(this.elem); - const chartData: Required = { + const chartData: Required = { labels: [], datasets: [], }; @@ -43,10 +43,12 @@ export class OverviewWorkloadStatus extends React.Component { } else { const data: number[] = []; const backgroundColor: string[] = []; + const tooltipLabels: DatasetTooltipLabel[] = []; for (const [status, value] of statuses) { data.push(value); backgroundColor.push(cssVars.get(`--workload-status-${status.toLowerCase()}`).toString()); + tooltipLabels.push(percent => `${capitalize(status)}: ${percent}`); chartData.labels.push(`${capitalize(status)}: ${value}`); } @@ -54,6 +56,7 @@ export class OverviewWorkloadStatus extends React.Component { data, backgroundColor, label: "Status", + tooltipLabels, }); } diff --git a/src/renderer/components/+workloads-pods/pod-details-list.tsx b/src/renderer/components/+workloads-pods/pod-details-list.tsx index 52a2ede8f7..d82d193e2d 100644 --- a/src/renderer/components/+workloads-pods/pod-details-list.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-list.tsx @@ -78,7 +78,7 @@ export class PodDetailsList extends React.Component { renderMemoryUsage(id: string, usage: number) { const { maxMemory } = this.props; const tooltip = ( -

Memory: {Math.ceil(usage * 100 / maxMemory)}%
{bytesToUnits(usage, 3)}

+

Memory: {Math.ceil(usage * 100 / maxMemory)}%
{bytesToUnits(usage, { precision: 3 })}

); if (!maxMemory) return usage ? bytesToUnits(usage) : 0; diff --git a/src/renderer/components/animate/animate.tsx b/src/renderer/components/animate/animate.tsx index f8bbd9b137..df160883fb 100644 --- a/src/renderer/components/animate/animate.tsx +++ b/src/renderer/components/animate/animate.tsx @@ -83,19 +83,23 @@ export class Animate extends React.Component { } render() { + if (!this.isVisible) { + return null; + } + const { name, enterDuration, leaveDuration } = this.props; const contentElem = this.contentElem; - const durations = { + const cssVarsForAnimation = { "--enter-duration": `${enterDuration}ms`, "--leave-duration": `${leaveDuration}ms`, } as React.CSSProperties; return React.cloneElement(contentElem, { className: cssNames("Animate", name, contentElem.props.className, this.statusClassName), - children: this.isVisible ? contentElem.props.children : null, + children: contentElem.props.children, style: { ...contentElem.props.style, - ...durations, + ...cssVarsForAnimation, }, }); } diff --git a/src/renderer/components/chart/bar-chart.tsx b/src/renderer/components/chart/bar-chart.tsx index d906b7b1c9..431ee9ce4f 100644 --- a/src/renderer/components/chart/bar-chart.tsx +++ b/src/renderer/components/chart/bar-chart.tsx @@ -194,7 +194,7 @@ export const memoryOptions: ChartOptions = { const { label, data } = datasets[datasetIndex]; const value = data[index] as ChartPoint; - return `${label}: ${bytesToUnits(parseInt(value.y.toString()), 3)}`; + return `${label}: ${bytesToUnits(parseInt(value.y.toString()), { precision: 3 })}`; }, }, }, diff --git a/src/renderer/components/chart/pie-chart.tsx b/src/renderer/components/chart/pie-chart.tsx index 3c5f0a9e19..23eb4dd9c6 100644 --- a/src/renderer/components/chart/pie-chart.tsx +++ b/src/renderer/components/chart/pie-chart.tsx @@ -14,6 +14,17 @@ import { ThemeStore } from "../../theme.store"; interface Props extends ChartProps { } +export interface PieChartData extends ChartJS.ChartData { + datasets?: PieChartDataSets[]; +} + +export type DatasetTooltipLabel = (percent: string) => string | string; + +interface PieChartDataSets extends ChartJS.ChartDataSets { + id?: string; + tooltipLabels?: DatasetTooltipLabel[]; +} + @observer export class PieChart extends React.Component { render() { @@ -26,15 +37,24 @@ export class PieChart extends React.Component { mode: "index", callbacks: { title: () => "", - label: (tooltipItem, data) => { - const dataset: any = data["datasets"][tooltipItem.datasetIndex]; - const metaData = Object.values<{ total: number }>(dataset["_meta"])[0]; - const percent = Math.round((dataset["data"][tooltipItem["index"]] / metaData.total) * 100); - const label = dataset["label"]; + label: (tooltipItem, data: PieChartData) => { + const dataset = data.datasets[tooltipItem.datasetIndex]; + const datasetData = dataset.data as number[]; + const total = datasetData.reduce((acc, cur) => acc + cur, 0); + const percent = Math.round((datasetData[tooltipItem.index] as number / total) * 100); + const percentLabel = isNaN(percent) ? "N/A" : `${percent}%`; + const tooltipLabel = dataset.tooltipLabels?.[tooltipItem.index]; + let tooltip = `${dataset.label}: ${percentLabel}`; - if (isNaN(percent)) return `${label}: N/A`; + if (tooltipLabel) { + if (typeof tooltipLabel === "function") { + tooltip = tooltipLabel(percentLabel); + } else { + tooltip = tooltipLabel; + } + } - return `${label}: ${percent}%`; + return tooltip; }, }, filter: ({ datasetIndex, index }, { datasets }) => { diff --git a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap index 0d1d591668..b0200f73d5 100644 --- a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap +++ b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap @@ -37,10 +37,6 @@ exports[`kube-object-menu given kube object renders 1`] = ` -