mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
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:
1 parent
381d77c633
commit
dfcb7c3330
1062 files changed
+26512
-28689
No files matched your search
@@ -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),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user