mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Release/v5.4.6 (#5265)
* Do not render Tooltip and Menu elements until needed (#5168) * Clean up Menu DOM elements Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Clean up Tooltip DOM Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Do not render Animate when not in need Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Update snapshots Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * clean up <Animate/> and <Tooltip/> Signed-off-by: Roman <ixrock@gmail.com> Co-authored-by: Roman <ixrock@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Add B to bytesToUnits to make clear that they are bytes (#5170) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix slackLink in catalog becoming out of date (#5108) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix PieChart tooltips (#5223) * Add tooltipLabels field to ChartData Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Use tooltipLabels in ClusterPieCharts for pods Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Check for tooltipLabels field to assign tooltip text Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Use tooltipLabels inside overview charts Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Expand workload overview charts to fit tooltips Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Move tooltipLabels into chart datasets Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Move tooltipLabels prop to PieCharts Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Little clean up Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Getting back id field to PieChartData interface Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Id fix Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * More clean up Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Not using paddings for empty top bar items (#5249) Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * release v5.4.6 Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com> Co-authored-by: Roman <ixrock@gmail.com> Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
5dd3896716
commit
a5c26e8e24
@ -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",
|
||||
|
||||
92
src/common/utils/__tests__/convert-memory.test.ts
Normal file
92
src/common/utils/__tests__/convert-memory.test.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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 = /(?<value>[0-9]+(\.[0-9]*)?)(?<suffix>(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);
|
||||
const parsedValue = parseFloat(unitsMatch.groups.value);
|
||||
|
||||
return parseInt(
|
||||
(parseFloat(value) * Math.pow(base, index + 1)).toFixed(1),
|
||||
);
|
||||
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}`;
|
||||
}
|
||||
|
||||
@ -171,6 +171,23 @@ export function join(src: Iterable<string>, 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<T>(src: Iterable<T>, 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.
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -54,12 +54,11 @@ export class WeblinkStore extends BaseStore<WeblinkStoreModel> {
|
||||
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
|
||||
|
||||
@ -20,7 +20,7 @@ export interface MigrationDeclaration {
|
||||
}
|
||||
|
||||
export function joinMigrations(...declarations: MigrationDeclaration[]): Migrations<any> {
|
||||
const migrations = new Map<string, ((store: Conf<any>) => void)[]>();
|
||||
const migrations = new Map<string, MigrationDeclaration["run"][]>();
|
||||
|
||||
for (const decl of declarations) {
|
||||
getOrInsert(migrations, decl.version, []).push(decl.run);
|
||||
|
||||
@ -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);
|
||||
|
||||
55
src/migrations/weblinks-store/5.4.5-beta.1.ts
Normal file
55
src/migrations/weblinks-store/5.4.5-beta.1.ts
Normal file
@ -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;
|
||||
24
src/migrations/weblinks-store/currentVersion.ts
Normal file
24
src/migrations/weblinks-store/currentVersion.ts
Normal file
@ -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;
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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 });
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -130,7 +130,7 @@ export class NodesRoute extends React.Component<Props> {
|
||||
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<Props> {
|
||||
metricNames: ["fsUsage", "fsSize"],
|
||||
formatters: [
|
||||
([usage, capacity]) => `${(usage * 100 / capacity).toFixed(2)}%`,
|
||||
([usage]) => bytesToUnits(usage, 3),
|
||||
([usage]) => bytesToUnits(usage, { precision: 3 }),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Props> {
|
||||
}
|
||||
|
||||
const cssVars = cssVar(this.elem);
|
||||
const chartData: Required<ChartData> = {
|
||||
const chartData: Required<PieChartData> = {
|
||||
labels: [],
|
||||
datasets: [],
|
||||
};
|
||||
@ -43,10 +43,12 @@ export class OverviewWorkloadStatus extends React.Component<Props> {
|
||||
} 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<Props> {
|
||||
data,
|
||||
backgroundColor,
|
||||
label: "Status",
|
||||
tooltipLabels,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -78,7 +78,7 @@ export class PodDetailsList extends React.Component<Props> {
|
||||
renderMemoryUsage(id: string, usage: number) {
|
||||
const { maxMemory } = this.props;
|
||||
const tooltip = (
|
||||
<p>Memory: {Math.ceil(usage * 100 / maxMemory)}%<br/>{bytesToUnits(usage, 3)}</p>
|
||||
<p>Memory: {Math.ceil(usage * 100 / maxMemory)}%<br/>{bytesToUnits(usage, { precision: 3 })}</p>
|
||||
);
|
||||
|
||||
if (!maxMemory) return usage ? bytesToUnits(usage) : 0;
|
||||
|
||||
@ -83,19 +83,23 @@ export class Animate extends React.Component<AnimateProps> {
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -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 })}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -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<Props> {
|
||||
render() {
|
||||
@ -26,15 +37,24 @@ export class PieChart extends React.Component<Props> {
|
||||
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 }) => {
|
||||
|
||||
@ -37,10 +37,6 @@ exports[`kube-object-menu given kube object renders 1`] = `
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Animate opacity-scale Dialog flex center ConfirmDialog modal"
|
||||
style="--enter-duration: 100ms; --leave-duration: 100ms;"
|
||||
/>
|
||||
</body>
|
||||
`;
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
&:first-of-type {
|
||||
padding-left: 1.5rem;
|
||||
|
||||
& > * {
|
||||
& > *:not(:empty) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,7 @@
|
||||
&:last-of-type {
|
||||
padding-right: 1.5rem;
|
||||
|
||||
& > * {
|
||||
& > *:not(:empty) {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,7 +93,6 @@ export class Menu extends React.Component<MenuProps, State> {
|
||||
this.opener.addEventListener(this.props.toggleEvent, this.toggle);
|
||||
this.opener.addEventListener("keydown", this.onKeyDown);
|
||||
}
|
||||
this.elem.addEventListener("keydown", this.onKeyDown);
|
||||
window.addEventListener("resize", this.onWindowResize);
|
||||
window.addEventListener("click", this.onClickOutside, true);
|
||||
window.addEventListener("scroll", this.onScrollOutside, true);
|
||||
@ -106,7 +105,6 @@ export class Menu extends React.Component<MenuProps, State> {
|
||||
this.opener.removeEventListener(this.props.toggleEvent, this.toggle);
|
||||
this.opener.removeEventListener("keydown", this.onKeyDown);
|
||||
}
|
||||
this.elem.removeEventListener("keydown", this.onKeyDown);
|
||||
window.removeEventListener("resize", this.onWindowResize);
|
||||
window.removeEventListener("click", this.onClickOutside, true);
|
||||
window.removeEventListener("scroll", this.onScrollOutside, true);
|
||||
@ -218,7 +216,7 @@ export class Menu extends React.Component<MenuProps, State> {
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDown(evt: KeyboardEvent) {
|
||||
onKeyDown(evt: React.KeyboardEvent | KeyboardEvent) {
|
||||
if (!this.isOpen) return;
|
||||
|
||||
switch (evt.code) {
|
||||
@ -330,6 +328,7 @@ export class Menu extends React.Component<MenuProps, State> {
|
||||
left: this.state?.menuStyle?.left,
|
||||
top: this.state?.menuStyle?.top,
|
||||
}}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{menuItems}
|
||||
</ul>
|
||||
|
||||
@ -9,7 +9,7 @@ import React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { observer } from "mobx-react";
|
||||
import { boundMethod, cssNames, IClassName } from "../../utils";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { observable, makeObservable, action } from "mobx";
|
||||
|
||||
export enum TooltipPosition {
|
||||
TOP = "top",
|
||||
@ -54,7 +54,8 @@ export class Tooltip extends React.Component<TooltipProps> {
|
||||
|
||||
@observable.ref elem: HTMLElement;
|
||||
@observable activePosition: TooltipPosition;
|
||||
@observable isVisible = false;
|
||||
@observable isVisible = this.props.visible ?? false;
|
||||
@observable isContentVisible = false; // animation manager
|
||||
|
||||
constructor(props: TooltipProps) {
|
||||
super(props);
|
||||
@ -87,15 +88,16 @@ export class Tooltip extends React.Component<TooltipProps> {
|
||||
this.hoverTarget.removeEventListener("mouseleave", this.onLeaveTarget);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@action.bound
|
||||
protected onEnterTarget() {
|
||||
this.isVisible = true;
|
||||
this.refreshPosition();
|
||||
requestAnimationFrame(action(() => this.isContentVisible = true));
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@action.bound
|
||||
protected onLeaveTarget() {
|
||||
this.isVisible = false;
|
||||
this.isContentVisible = false;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@ -103,6 +105,10 @@ export class Tooltip extends React.Component<TooltipProps> {
|
||||
const { preferredPositions } = this.props;
|
||||
const { elem, targetElem } = this;
|
||||
|
||||
if (!elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
let positions = new Set<TooltipPosition>([
|
||||
TooltipPosition.RIGHT,
|
||||
TooltipPosition.BOTTOM,
|
||||
@ -150,6 +156,10 @@ export class Tooltip extends React.Component<TooltipProps> {
|
||||
}
|
||||
|
||||
protected setPosition(pos: { left: number; top: number }) {
|
||||
if (!this.elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elemStyle = this.elem.style;
|
||||
|
||||
elemStyle.left = `${pos.left}px`;
|
||||
@ -214,13 +224,17 @@ export class Tooltip extends React.Component<TooltipProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { style, formatters, usePortal, children, visible } = this.props;
|
||||
if (!this.isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { style, formatters, usePortal, children } = this.props;
|
||||
const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, {
|
||||
visible: visible ?? this.isVisible,
|
||||
visible: this.isContentVisible,
|
||||
formatter: !!formatters,
|
||||
});
|
||||
const tooltip = (
|
||||
<div className={className} style={style} ref={this.bindRef}>
|
||||
<div className={className} style={style} ref={this.bindRef} role="tooltip">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user