1
0
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:
Jim Ehrismann 2022-04-21 18:16:31 -04:00 committed by GitHub
parent 5dd3896716
commit a5c26e8e24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 339 additions and 75 deletions

View File

@ -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",

View 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();
});
});

View File

@ -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}`;
}

View File

@ -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.

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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);

View 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;

View 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;

View File

@ -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,
);

View File

@ -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 });
},
},
},

View File

@ -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: [

View File

@ -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 }),
],
});
}

View File

@ -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
}
}
}

View File

@ -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,
});
}

View File

@ -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;

View File

@ -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,
},
});
}

View File

@ -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 })}`;
},
},
},

View File

@ -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 }) => {

View File

@ -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>
`;

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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>
);