Turn on strict mode in tsconfig.json, some helpful lints, and required cleanup where strictness necessitates it (#5195)

* Turn on strict mode in tsconfig.json

- Add route, clusterRoute, and payloadValidatedClusterRoute helper
  functions to improve types with backend routes

- Turn on the following new lints:
  - react/jsx-first-prop-new-line
  - react/jsx-wrap-multilines
  - react/jsx-one-expression-per-line
  - react/jsx-max-props-per-line
  - react/jsx-indent
  - react/jsx-indent-props

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix build

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace KubeObject scope strings with enum

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Revert package.json version changes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* revert move hostedCluster(Id)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* change some type param names to be not single letters

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove copy-extension-themes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* add new make clean action

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix build to not use webpack for generating types

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube-object-menu.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix select.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* revert move fileNameMigration to index

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix ref logic error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix log-resource-selector.test.tsx tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-store.test.ts test by overriding createStorage to not touch file system

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cluster.test.ts tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube=api.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fixed hotbar-store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kubeconfig-manager.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cluster-role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix pods.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix delete-cluster-dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix daemonset.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix replicaset.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix statefulsets/dialog/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix replicasets/scale-dialog/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix deployments.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix deployments/scale/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cronjob.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix stateful-set.api.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix deployment.api.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix api-manager.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix statefulset.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix job.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix pods.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix scroll-spy.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix hotbar-remove-command.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog-entity-registry.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix welcome.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix verify-that-all-routes-have-route-component.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix pod-tolerations.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* better fix for previous 3 fixes, plus also select.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube-object-menu.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix app-paths.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-tabs.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix isReactNode typing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix sub-title.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix drawer.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix list-layout.tsx and header.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix error-boundary.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix upgrade-chart/store.ts and dock-tab.store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix install-chart/store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix edit-resource/store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix create-resource/store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix namespace-select.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix namespace-select-filter.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix crd-list.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix wrong types for extensions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix circular dependency

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix circular dependency on catalogCategoryRegistry

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix api-kube

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix type errors, most <Select /> errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fixing more type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* some more fixing type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* convert all KubeApis to injectable with legacy global backups

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* factor out into a common file all the exports

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* convert all KubeObjectStores to injectable with legacy global backups

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix lint

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove unused legacy KubeApi globals

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix bad previous commit

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* more crash fixing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* try and fix behavioural tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix sidebar-and-tab-navigation-for-core.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix sidebar-and-tab-navigation-for-extensions.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-using-application-menu.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make ThemeStore non-singleton and fix navigation-to-terminal-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* extensions.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog-entity-registry.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-using-application-menu.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix log-resource-selector.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-tabs.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix delete-cluster-dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-kubernetes-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-editor-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-proxy-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-using-application-menu.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-application-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix select.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix hotbar-remove-command.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cluster-role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-extension-specific-preferences.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-telemetry-preferences.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix closing-preferences.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-editor-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-proxy-preferences.test.ts

- Fix other type errors too

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* final tweaks

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add more tsconfig files, fix bug in <Catalog>

- Make all of history, navigation injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert all of kube-details-params/ and navigate/ to injectable

- This fixes a runtime error that was encountered during testing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix runtime errors on renderer

- remove all static uses of `createPageParam` (and then removed the
  legacy global)
- Made LensRendererExtension and LensMainExtension just used
  dependencies and not the getLegacyDi
- Fixed circular dep in extension-loader

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move registerStore calls to after injectMany

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* replace all the rest of the legacy uses of apiManager

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix stack overflow and cycles in DI

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix NamespaceSelectFilter not opening

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix WizardStep and AddNamespaceDialog

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix KubeApi's not being registered

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* cleanup WindowManager

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Proper fix for Wizard, fix NamespaceStore.subscribe

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Rewrite withTooltip to be more type correct

- Fixes mobx related "too many recursive actions" error

- Change all the uses of withTooltips to be functional components

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add e2e test to cover kube api registration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* cleanup internal-commands

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove cast in <Animate>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix command-palette e2e test

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error after rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test name

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix lint

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix code to help CodeQL scanner

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* update intree extension lock files

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix build-extensions picking wrong @types/react

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix tests from rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix type error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make KubeconfigSyncManager more injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix crash in test mode for Dialog

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* make Select snapshots deterministic

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix new type error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube-object.store.test.ts typing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix merge build issues

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix snapshots after merge

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix lint after merge

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* reexport BaseKubeJsonApiObjectMetadata

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix typo in terminalSpawningPool

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove duplicate license header

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix typo to waitUntilDefined

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove iter use from getLegacyGlobalDiForExtensionApi

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove complex createStorage override

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* override logger with mocks only when needed for tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove specialized overrideStore flags for getDiForUnitTesting

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove unnecessary | undefined types from the exactOptionalFieldTypes experiment

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* use more descriptive name for local test mocks

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove unnecessary addition to 'make clean' target

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove oddity of KubeObjectStore.getById(undefined) being allowed

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* rename KubeObject.getDescriptor in favour of name without fundemental JS meaning

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Simplify legacyRegisterApi when working in behaviour unit tests

- Don't emit within main environment as there should be no auto
  registering there

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* change confusing variable name in ReactiveDuration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* make visitor pattern more explicit for Entity contextMenuOpen

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* toggleDetails -> toggleKubeDetailsPane is more specific

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove outdated comment

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix bug where LensExtension dependencies are not set

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests from the revert of react 18

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix more tests from merge

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix typings with new is-compatible-extension tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* more type fixing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Revert in-tree extension versions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Improve name of guarding injectable for stores and apis

- New name better implies that it is just a guard state and does not do
  anything

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add helper for <Select>.isMulti for storing in a Set<Value>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix is-compatible-extension.test.ts types

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2022-05-16 07:17:57 -04:00
1 parent 381d77c633
commit dfcb7c3330
1062 files changed
+26512 -28689

No files matched your search

+165 -150
View File
@@ -8,171 +8,196 @@ import merge from "lodash/merge";
import moment from "moment";
import Color from "color";
import { observer } from "mobx-react";
import type { ChartData, ChartOptions, ChartPoint, ChartTooltipItem, Scriptable } from "chart.js";
import type { ChartOptions, ChartTooltipCallback, ChartTooltipItem, Scriptable } from "chart.js";
import type { ChartProps } from "./chart";
import { Chart, ChartKind } from "./chart";
import { bytesToUnits, cssNames } from "../../utils";
import { ZebraStripes } from "./zebra-stripes.plugin";
import { ThemeStore } from "../../theme.store";
import { bytesToUnits, cssNames, isObject } from "../../utils";
import { ZebraStripesPlugin } from "./zebra-stripes.plugin";
import type { ThemeStore } from "../../themes/store";
import { NoMetrics } from "../resource-metrics/no-metrics";
import assert from "assert";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
export interface BarChartProps extends ChartProps {
name?: string;
timeLabelStep?: number; // Minute labels appearance step
}
const defaultProps: Partial<BarChartProps> = {
timeLabelStep: 10,
plugins: [ZebraStripes],
};
const getBarColor: Scriptable<string> = ({ dataset }) => Color(dataset?.borderColor).alpha(0.2).string();
@observer
export class BarChart extends React.Component<BarChartProps> {
static defaultProps = defaultProps as object;
interface Dependencies {
themeStore: ThemeStore;
}
render() {
const { name, data, className, timeLabelStep, plugins, options: customOptions, ...settings } = this.props;
const { textColorPrimary, borderFaintColor, chartStripesColor } = ThemeStore.getInstance().activeTheme.colors;
const NonInjectedBarChart = observer(({
themeStore,
name,
data,
className,
timeLabelStep = 10,
plugins,
options: customOptions,
...settings
}: Dependencies & BarChartProps) => {
const { textColorPrimary, borderFaintColor, chartStripesColor } = themeStore.activeTheme.colors;
const { datasets: rawDatasets = [], ...rest } = data;
const datasets = rawDatasets
.filter(set => set.data?.length)
.map(item => ({
type: ChartKind.BAR,
borderWidth: { top: 3 },
barPercentage: 1,
categoryPercentage: 1,
...item,
}));
const getBarColor: Scriptable<string> = ({ dataset }) => {
const color = dataset.borderColor;
plugins ??= [new ZebraStripesPlugin({
stripeColor: chartStripesColor,
interval: datasets[0]?.data?.length,
})];
return Color(color).alpha(0.2).string();
};
if (datasets.length === 0) {
return <NoMetrics/>;
}
// Remove empty sets and insert default data
const chartData: ChartData = {
...data,
datasets: data.datasets
.filter(set => set.data.length)
.map(item => {
return {
type: ChartKind.BAR,
borderWidth: { top: 3 },
barPercentage: 1,
categoryPercentage: 1,
...item,
};
}),
};
const formatTimeLabels = (timestamp: string, index: number) => {
const label = moment(parseInt(timestamp)).format("HH:mm");
const offset = " ";
if (chartData.datasets.length == 0) {
return <NoMetrics/>;
if (index == 0) return offset + label;
if (index == 60) return label + offset;
return index % timeLabelStep == 0 ? label : "";
};
const barOptions: ChartOptions = {
maintainAspectRatio: false,
responsive: true,
scales: {
xAxes: [{
type: "time",
offset: true,
gridLines: {
display: false,
},
stacked: true,
ticks: {
callback: formatTimeLabels,
autoSkip: false,
source: "data",
backdropColor: "white",
fontColor: textColorPrimary,
fontSize: 11,
maxRotation: 0,
minRotation: 0,
},
bounds: "data",
time: {
unit: "minute",
displayFormats: {
minute: "x",
},
parser: timestamp => moment.unix(parseInt(timestamp)),
},
}],
yAxes: [{
position: "right",
gridLines: {
color: borderFaintColor,
drawBorder: false,
tickMarkLength: 0,
zeroLineWidth: 0,
},
ticks: {
maxTicksLimit: 6,
fontColor: textColorPrimary,
fontSize: 11,
padding: 8,
min: 0,
},
}],
},
tooltips: {
mode: "index",
position: "cursor",
callbacks: {
title([tooltip]: ChartTooltipItem[]) {
const xLabel = tooltip?.xLabel;
const skipLabel = xLabel == null || new Date(xLabel).getTime() > Date.now();
if (skipLabel) return "";
return String(xLabel);
},
labelColor: ({ datasetIndex }) => (
typeof datasetIndex === "number"
? {
borderColor: "darkgray",
backgroundColor: datasets[datasetIndex].borderColor as string,
}
: {
borderColor: "darkgray",
backgroundColor: "gray",
}
),
},
},
animation: {
duration: 0,
},
elements: {
rectangle: {
backgroundColor: getBarColor.bind(null),
},
},
};
return (
<Chart
className={cssNames("BarChart flex box grow column", className)}
type={ChartKind.BAR}
data={{ datasets, ...rest }}
options={merge(barOptions, customOptions)}
plugins={plugins}
{...settings}
/>
);
});
export const BarChart = withInjectables<Dependencies, BarChartProps>(NonInjectedBarChart, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
}),
});
const tooltipCallbackWith = (precision: number): ChartTooltipCallback["label"] => (
({ datasetIndex, index }, { datasets = [] }) => {
if (typeof datasetIndex !== "number" || typeof index !== "number") {
return "";
}
const formatTimeLabels = (timestamp: string, index: number) => {
const label = moment(parseInt(timestamp)).format("HH:mm");
const offset = " ";
const { label, data } = datasets[datasetIndex];
if (index == 0) return offset + label;
if (index == 60) return label + offset;
if (!label || !data) {
return "<unknown>";
}
return index % timeLabelStep == 0 ? label : "";
};
const value = data[index];
const barOptions: ChartOptions = {
maintainAspectRatio: false,
responsive: true,
scales: {
xAxes: [{
type: "time",
offset: true,
gridLines: {
display: false,
},
stacked: true,
ticks: {
callback: formatTimeLabels,
autoSkip: false,
source: "data",
backdropColor: "white",
fontColor: textColorPrimary,
fontSize: 11,
maxRotation: 0,
minRotation: 0,
},
bounds: "data",
time: {
unit: "minute",
displayFormats: {
minute: "x",
},
parser: timestamp => moment.unix(parseInt(timestamp)),
},
}],
yAxes: [{
position: "right",
gridLines: {
color: borderFaintColor,
drawBorder: false,
tickMarkLength: 0,
zeroLineWidth: 0,
},
ticks: {
maxTicksLimit: 6,
fontColor: textColorPrimary,
fontSize: 11,
padding: 8,
min: 0,
},
}],
},
tooltips: {
mode: "index",
position: "cursor",
callbacks: {
title([tooltip]: ChartTooltipItem[]) {
const xLabel = tooltip?.xLabel;
const skipLabel = xLabel == null || new Date(xLabel).getTime() > Date.now();
assert(isObject(value) && !Array.isArray(value) && typeof value.y === "number");
if (skipLabel) return "";
return String(xLabel);
},
labelColor: ({ datasetIndex }) => {
return {
borderColor: "darkgray",
backgroundColor: chartData.datasets[datasetIndex].borderColor as string,
};
},
},
},
animation: {
duration: 0,
},
elements: {
rectangle: {
backgroundColor: getBarColor.bind(null),
},
},
plugins: {
ZebraStripes: {
stripeColor: chartStripesColor,
interval: chartData.datasets[0].data.length,
},
},
};
const options = merge(barOptions, customOptions);
return (
<Chart
className={cssNames("BarChart flex box grow column", className)}
type={ChartKind.BAR}
data={chartData}
options={options}
plugins={plugins}
{...settings}
/>
);
return `${label}: ${bytesToUnits(parseInt(value.y.toString()), { precision })}`;
}
}
);
// Default options for all charts containing memory units (network, disk, memory, etc)
export const memoryOptions: ChartOptions = {
scales: {
yAxes: [{
ticks: {
callback: (value: number | string): string => {
callback: (value) => {
if (typeof value == "string") {
const float = parseFloat(value);
@@ -191,12 +216,7 @@ export const memoryOptions: ChartOptions = {
},
tooltips: {
callbacks: {
label: ({ datasetIndex, index }, { datasets }) => {
const { label, data } = datasets[datasetIndex];
const value = data[index] as ChartPoint;
return `${label}: ${bytesToUnits(parseInt(value.y.toString()), { precision: 3 })}`;
},
label: tooltipCallbackWith(3),
},
},
};
@@ -206,7 +226,7 @@ export const cpuOptions: ChartOptions = {
scales: {
yAxes: [{
ticks: {
callback: (value: number | string): string => {
callback: (value) => {
const float = parseFloat(`${value}`);
if (float == 0) return "0";
@@ -220,12 +240,7 @@ export const cpuOptions: ChartOptions = {
},
tooltips: {
callbacks: {
label: ({ datasetIndex, index }, { datasets }) => {
const { label, data } = datasets[datasetIndex];
const value = data[index] as ChartPoint;
return `${label}: ${parseFloat(value.y as string).toPrecision(2)}`;
},
label: tooltipCallbackWith(2),
},
},
};
+55 -41
View File
@@ -4,7 +4,9 @@
*/
import "./chart.scss";
import type { CSSProperties } from "react";
import React from "react";
import type { PluginServiceRegistrationOptions } from "chart.js";
import ChartJS from "chart.js";
import { remove } from "lodash";
import { cssNames } from "../../utils";
@@ -30,7 +32,7 @@ export interface ChartProps {
showLegend?: boolean;
legendPosition?: "bottom";
legendColors?: string[]; // Hex colors for each of the labels in data object
plugins?: any[];
plugins?: PluginServiceRegistrationOptions[];
redraw?: boolean; // If true - recreate chart instance with no animation
title?: string;
className?: string;
@@ -57,31 +59,31 @@ export class Chart extends React.Component<ChartProps> {
static defaultProps = defaultProps as object;
private canvas = React.createRef<HTMLCanvasElement>();
private chart: ChartJS;
private chart: ChartJS | null = null;
// ChartJS adds _meta field to any data object passed to it.
// We clone new data prop into currentChartData to compare props and prevProps
private currentChartData: ChartData;
private currentChartData?: ChartData;
componentDidMount() {
const { showChart } = this.props;
if (!showChart) return;
this.renderChart();
if (showChart) {
this.renderChart();
}
}
componentDidUpdate() {
const { showChart, redraw } = this.props;
if (redraw) {
this.chart.destroy();
this.chart?.destroy();
this.renderChart();
return;
}
if (showChart) {
if (!this.chart) this.renderChart();
else this.updateChart();
} else if (showChart) {
if (!this.chart) {
this.renderChart();
} else {
this.updateChart();
}
}
}
@@ -107,8 +109,8 @@ export class Chart extends React.Component<ChartProps> {
this.memoizeDataProps();
const datasets: ChartDataSets[] = this.chart.config.data.datasets;
const nextDatasets: ChartDataSets[] = this.currentChartData.datasets || [];
const datasets: ChartDataSets[] = (this.chart.config.data ??= {}).datasets ??= [];
const nextDatasets: ChartDataSets[] = (this.currentChartData ??= {}).datasets ??= [];
// Remove stale datasets if they're not available in nextDatasets
if (datasets.length > nextDatasets.length) {
@@ -126,14 +128,17 @@ export class Chart extends React.Component<ChartProps> {
const index = datasets.findIndex(set => set.id === next.id);
if (index !== -1) {
datasets[index].data = datasets[index].data.slice(); // "Clean" mobx observables data to use in ChartJS
datasets[index].data.splice(next.data.length);
next.data.forEach((point: any, dataIndex: number) => {
datasets[index].data[dataIndex] = next.data[dataIndex];
});
const data = datasets[index].data = (datasets[index].data ?? []).slice(); // "Clean" mobx observables data to use in ChartJS
const nextData = next.data ??= [];
data.splice(next.data.length);
for (let dataIndex = 0; dataIndex < nextData.length; dataIndex += 1) {
data[dataIndex] = nextData[dataIndex];
}
// Merge other fields
const { data, ...props } = next;
const { data: _data, ...props } = next;
datasets[index] = {
...datasets[index],
@@ -150,7 +155,7 @@ export class Chart extends React.Component<ChartProps> {
if (!this.props.showLegend) return null;
const { data, legendColors } = this.props;
const { labels, datasets } = data;
const labelElem = (title: string, color: string, tooltip?: string) => (
const labelElem = (title: string | undefined, color: string | CSSProperties["backgroundColor"], tooltip?: string) => (
<Badge
key={title}
className="LegendBadge flex gaps align-center"
@@ -167,24 +172,33 @@ export class Chart extends React.Component<ChartProps> {
return (
<div className="legend flex wrap">
{labels && labels.map((label: string, index) => {
const { backgroundColor } = datasets[0] as any;
const color = legendColors ? legendColors[index] : backgroundColor[index];
{
labels
? labels.map((label, index) => {
const { backgroundColor = [] } = datasets?.[0] ?? {};
const color = legendColors ? legendColors[index] : (backgroundColor as string[])[index];
return labelElem(label, color);
})}
{!labels && datasets.map(({ borderColor, label, tooltip }) =>
labelElem(label, borderColor as any, tooltip),
)}
return labelElem(label as string, color);
})
: datasets?.map(({ borderColor, label, tooltip }) =>
labelElem(label, borderColor as string, tooltip),
)
}
</div>
);
}
renderChart() {
const { type, options, plugins } = this.props;
const canvas = this.canvas.current;
if (!canvas) {
return;
}
this.memoizeDataProps();
this.chart = new ChartJS(this.canvas.current, {
this.chart = new ChartJS(canvas, {
type,
plugins,
options: {
@@ -204,16 +218,16 @@ export class Chart extends React.Component<ChartProps> {
<>
<div className={cssNames("Chart", className)}>
{title && <div className="chart-title">{title}</div>}
{showChart &&
<div className="chart-container">
<canvas
ref={this.canvas}
width={width}
height={height}
/>
<div className="chartjs-tooltip flex column"></div>
</div>
}
{showChart && (
<div className="chart-container">
<canvas
ref={this.canvas}
width={width}
height={height}
/>
<div className="chartjs-tooltip flex column"></div>
</div>
)}
{this.renderLegend()}
</div>
</>
+150
View File
@@ -0,0 +1,150 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ChartOptions, ChartPoint } from "chart.js";
import { bytesToUnits, isDefined } from "../../utils";
export type MetricsTab = "CPU" | "Memory" | "Disk" | "Pods" | "Network" | "Filesystem" | "Duration";
const memoryLikeOptions: ChartOptions = {
scales: {
yAxes: [{
ticks: {
callback: (value: number | string): string => {
if (typeof value == "string") {
const float = parseFloat(value);
if (float < 1) {
return float.toFixed(3);
}
return bytesToUnits(parseInt(value));
}
return bytesToUnits(value);
},
stepSize: 1,
},
}],
},
tooltips: {
callbacks: {
label: ({ datasetIndex, index }, { datasets }) => {
if (!isDefined(datasetIndex) || !isDefined(index) || !isDefined(datasets)) {
return "";
}
const { label, data } = datasets[datasetIndex];
if (!data) {
return label ?? "";
}
const value = data[index] as { y: number };
return `${label}: ${bytesToUnits(parseInt(value.y.toString()), { precision: 3 })}`;
},
},
},
};
export const metricTabOptions: Record<MetricsTab, ChartOptions> = {
Memory: memoryLikeOptions,
Disk: memoryLikeOptions,
Network: memoryLikeOptions,
Filesystem: memoryLikeOptions,
CPU: {
scales: {
yAxes: [{
ticks: {
callback: (value: number | string): string => {
const float = parseFloat(`${value}`);
if (float == 0) return "0";
if (float < 10) return float.toFixed(3);
if (float < 100) return float.toFixed(2);
return float.toFixed(1);
},
},
}],
},
tooltips: {
callbacks: {
label: ({ datasetIndex, index }, { datasets }) => {
if (!isDefined(datasetIndex) || !isDefined(index) || !isDefined(datasets)) {
return "";
}
const { label, data } = datasets[datasetIndex];
if (!data) {
return label ?? "";
}
const value = data[index] as ChartPoint;
return `${label}: ${parseFloat(value.y as string).toPrecision(2)}`;
},
},
},
},
Pods: {
scales: {
yAxes: [{
ticks: {
callback: value => value,
},
}],
},
tooltips: {
callbacks: {
label: ({ datasetIndex, index }, { datasets }) => {
if (!isDefined(datasetIndex) || !isDefined(index) || !isDefined(datasets)) {
return "";
}
const { label, data } = datasets[datasetIndex];
if (!data) {
return label ?? "";
}
const value = data[index] as ChartPoint;
return `${label}: ${value.y}`;
},
},
},
},
Duration: {
scales: {
yAxes: [{
ticks: {
callback: value => value,
},
}],
},
tooltips: {
callbacks: {
label: ({ datasetIndex, index }, { datasets }) => {
if (!isDefined(datasetIndex) || !isDefined(index) || !isDefined(datasets)) {
return "";
}
const { label, data } = datasets[datasetIndex];
if (!data) {
return label ?? "";
}
const value = data[index] as { y: string };
return `${label}: ${parseFloat(value.y).toFixed(3)} sec`;
},
},
},
},
};
+95 -61
View File
@@ -11,7 +11,9 @@ import ChartJS from "chart.js";
import type { ChartProps } from "./chart";
import { Chart } from "./chart";
import { cssNames } from "../../utils";
import { ThemeStore } from "../../theme.store";
import type { ThemeStore } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
export interface PieChartProps extends ChartProps {
}
@@ -27,69 +29,101 @@ interface PieChartDataSets extends ChartJS.ChartDataSets {
tooltipLabels?: DatasetTooltipLabel[];
}
@observer
export class PieChart extends React.Component<PieChartProps> {
render() {
const { data, className, options, ...chartProps } = this.props;
const { contentColor } = ThemeStore.getInstance().activeTheme.colors;
const cutouts = [88, 76, 63];
const opts: ChartOptions = this.props.showChart === false ? {} : {
maintainAspectRatio: false,
tooltips: {
mode: "index",
callbacks: {
title: () => "",
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 (tooltipLabel) {
if (typeof tooltipLabel === "function") {
tooltip = tooltipLabel(percentLabel);
} else {
tooltip = tooltipLabel;
}
}
return tooltip;
},
},
filter: ({ datasetIndex, index }, { datasets }) => {
const { data } = datasets[datasetIndex];
if (datasets.length === 1) return true;
return index !== data.length - 1;
},
position: "cursor",
},
elements: {
arc: {
borderWidth: 1,
borderColor: contentColor,
},
},
cutoutPercentage: cutouts[data.datasets.length - 1] || 50,
responsive: true,
...options,
};
return (
<Chart
className={cssNames("PieChart flex column align-center", className)}
data={data}
options={opts}
{...chartProps}
/>
);
function getCutout(length: number | undefined): number {
switch (length) {
case 0:
case 1:
return 88;
case 2:
return 76;
case 3:
return 63;
default:
return 50;
}
}
interface Dependencies {
themeStore: ThemeStore;
}
const NonInjectedPieChart = observer(({
themeStore,
data,
className,
options,
showChart,
...chartProps
}: Dependencies & PieChartProps) => {
const { contentColor } = themeStore.activeTheme.colors;
const opts: ChartOptions = {
maintainAspectRatio: false,
tooltips: {
mode: "index",
callbacks: {
title: () => "",
label: (tooltipItem: { datasetIndex: number; index: number }, 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 (tooltipLabel) {
if (typeof tooltipLabel === "function") {
tooltip = tooltipLabel(percentLabel);
} else {
tooltip = tooltipLabel;
}
}
return tooltip;
},
},
filter: ({ datasetIndex, index }, { datasets = [] }) => {
if (datasetIndex === undefined) {
return false;
}
const { data = [] } = datasets[datasetIndex];
if (datasets.length === 1) return true;
return index !== data.length - 1;
},
position: "cursor",
},
elements: {
arc: {
borderWidth: 1,
borderColor: contentColor,
},
},
cutoutPercentage: getCutout(data.datasets?.length),
responsive: true,
...options,
};
return (
<Chart
className={cssNames("PieChart flex column align-center", className)}
data={data}
options={showChart ? {} : opts}
showChart={showChart}
{...chartProps}
/>
);
});
export const PieChart = withInjectables<Dependencies, PieChartProps>(NonInjectedPieChart, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
}),
});
ChartJS.Tooltip.positioners.cursor = function (elements: any, position: { x: number; y: number }) {
return position;
};
@@ -9,50 +9,61 @@
import type ChartJS from "chart.js";
import type { Moment } from "moment";
import moment from "moment";
import get from "lodash/get";
import type { PluginServiceRegistrationOptions } from "chart.js";
const defaultOptions = {
stripeColor: "#ffffff08",
interval: 10,
};
export const ZebraStripes = {
updated: null as Moment, // timestamp which all stripe movements based on
options: {},
export interface ZebraStripesOptions {
stripeColor: string;
interval: number;
}
getOptions(chart: ChartJS) {
return get(chart, "options.plugins.ZebraStripes");
},
export class ZebraStripesPlugin implements PluginServiceRegistrationOptions {
updated: Moment | null = null;
options: ZebraStripesOptions;
constructor(options?: Partial<ZebraStripesOptions>) {
this.options = Object.assign({}, defaultOptions, options);
}
getOptions(chart: ChartJS): ZebraStripesOptions | undefined {
return chart.options.plugins?.ZebraStripes;
}
getLastUpdate(chart: ChartJS) {
const data = chart.data.datasets[0].data[0] as ChartJS.ChartPoint;
const data = chart.data.datasets?.[0]?.data?.[0] as ChartJS.ChartPoint;
return moment.unix(parseInt(data.x as string));
},
}
getStripesElem(chart: ChartJS) {
return chart.canvas.parentElement.querySelector(".zebra-cover");
},
return chart.canvas?.parentElement?.querySelector<HTMLElement>(".zebra-cover");
}
removeStripesElem(chart: ChartJS) {
const elem = this.getStripesElem(chart);
if (!elem) return;
chart.canvas.parentElement.removeChild(elem);
},
if (elem) {
chart.canvas?.parentElement?.removeChild(elem);
}
}
updateOptions(chart: ChartJS) {
this.options = {
...defaultOptions,
...this.getOptions(chart),
};
},
}
getStripeMinutes() {
return this.options.interval < 10 ? 0 : 10;
},
}
renderStripes(chart: ChartJS) {
if (!chart.data.datasets.length) return;
if (!chart.data.datasets?.length) return;
const { interval, stripeColor } = this.options;
const { top, left, bottom, right } = chart.chartArea;
const step = (right - left) / interval;
@@ -71,26 +82,26 @@ export const ZebraStripes = {
repeating-linear-gradient(to right, ${stripeColor} 0px, ${stripeColor} ${stripeWidth}px,
transparent ${stripeWidth}px, transparent ${stripeWidth * 2 + step}px)
`;
chart.canvas.parentElement.appendChild(cover);
},
chart.canvas?.parentElement?.appendChild(cover);
}
afterInit(chart: ChartJS) {
if (!chart.data.datasets.length) return;
if (!chart.data.datasets?.length) return;
this.updateOptions(chart);
this.updated = this.getLastUpdate(chart);
},
}
afterUpdate(chart: ChartJS) {
this.updateOptions(chart);
this.renderStripes(chart);
},
}
resize(chart: ChartJS) {
this.removeStripesElem(chart);
},
}
afterDatasetUpdate(chart: ChartJS): void {
if (!this.updated) this.updated = this.getLastUpdate(chart);
this.updated ??= this.getLastUpdate(chart);
const { interval } = this.options;
const { left, right } = chart.chartArea;
@@ -105,7 +116,9 @@ export const ZebraStripes = {
// Move position regarding to difference in time
const cover = this.getStripesElem(chart);
cover.style.backgroundPositionX = `${-step * minutes}px`;
if (cover) {
cover.style.backgroundPositionX = `${-step * minutes}px`;
}
}
},
};
}
}