mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 21:35:59 +08:00
feat: uplot graph is added and some re-rendering is reduced (#3771)
* feat: uplot graph is added and some re-rendering is reduced * chore: uplot is updated * feat: changes for the graph is updated * refactor: added y-axis unit in uplot graph (#3818) * refactor: added y-axis unit in uplot graph * refactor: removed the ticks stroke from both access * feat: create tooltip plugin for uplot charts (#3823) * feat: create tooltip plugin for uplot charts * feat: show labels in legends section --------- Co-authored-by: Yunus M <myounis.ar@live.com> * feat: uplot points is handled (#3817) * chore: resize is updated * chore: uplot chart dark mode is updated * chore: widget is updated * chore: options is updated * chore: value panel is updated * feat: uplot chart is updated * feat: onDrag is updated * feat: data for graph is updated * feat: alert section is fixed * feat: not found is updated * feat: fix dashboard title section and other uplot parity issues (#3839) * feat: fix dashboard title section and other uplot parity issues * feat: update scrollbar style for legend container * chore: initial width is updated * feat: onlcick is updated * feat: widget full view fixes (#3847) Co-authored-by: Palash Gupta <palashgdev@gmail.com> * feat: show labels in tooltip overlay (#3867) * chore: memo is added * feat: toggle is updated * fix: Tooltip values is now fixed (#3894) * chore: tooltip is updated * chore: avoided the compute based on show * chore: tooltip data is updated * feat: resize graph based on the y axis max label length (#3895) * chore: build is in progress to fix * [Feat]: Full View (#3896) * fix: initial setup for full view done * refactor: done with the graph manager logic * refactor: done with the toggle issue in full view * refactor: done with toggle of data * refactor: done with legend to table mapping * refactor: ts error * chore: utils is updated * refactor: updated types * fix: option type fix --------- Co-authored-by: Palash Gupta <palashgdev@gmail.com> * feat: use spline renderer to plot curved line graphs, full view impor… (#3919) * feat: use spline renderer to plot curved line graphs, full view imporvements * feat: increase min height for panel * chore: move code to utils and plugins in uplot folder * chore: update tooltip styles * fix: add panel issue in dashboard (#3920) * fix: update on click plugin opts import path * feat: replace time series graph in logs explorer and trace explorer with uplot (#3925) * feat: alert threshold is added (#3931) * feat: uplot styles are fixed (#3941) * Fix/app dex aligment (#3944) * feat: uplot styles are fixed * fix: app dex aligment * fix: full view after saving is fixed * feat: css is updated (#3948) * feat: on click handler position - factor in the padding on top and left * fix: timestamp for start and end is updated for view trace (#3966) * fix: timestamp for start and end is updated for view trace * chore: timestamp is added * fix: loading over flow is fixed (#3969) --------- Co-authored-by: Rajat Dabade <rajat@signoz.io> Co-authored-by: Yunus M <myounis.ar@live.com>
This commit is contained in:
parent
a99d7f09a1
commit
f2f89eb38b
@ -84,7 +84,6 @@
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-helmet-async": "1.3.0",
|
||||
"react-i18next": "^11.16.1",
|
||||
"react-intersection-observer": "9.4.1",
|
||||
"react-markdown": "8.0.7",
|
||||
"react-query": "^3.34.19",
|
||||
"react-redux": "^7.2.2",
|
||||
@ -102,6 +101,7 @@
|
||||
"ts-node": "^10.2.1",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"typescript": "^4.0.5",
|
||||
"uplot": "1.6.26",
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^0.2.4",
|
||||
"webpack": "5.88.2",
|
||||
|
@ -35,7 +35,7 @@ export type GraphOnClickHandler = (
|
||||
) => void;
|
||||
|
||||
export type ToggleGraphProps = {
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void;
|
||||
toggleGraph(graphIndex: number, isVisible: boolean, reference?: string): void;
|
||||
};
|
||||
|
||||
export type CustomChartOptions = ChartOptions & {
|
||||
|
@ -46,7 +46,7 @@ export const getYAxisFormattedValue = (
|
||||
return `${parseFloat(value)}`;
|
||||
};
|
||||
|
||||
export const getToolTipValue = (value: string, format: string): string => {
|
||||
export const getToolTipValue = (value: string, format?: string): string => {
|
||||
try {
|
||||
return formattedValueToString(
|
||||
getValueFormat(format)(parseFloat(value), undefined, undefined, undefined),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown } from 'antd';
|
||||
import TimeItems, {
|
||||
timePreferance,
|
||||
@ -33,7 +34,9 @@ function TimePreference({
|
||||
return (
|
||||
<TextContainer noButtonMargin>
|
||||
<Dropdown menu={menu}>
|
||||
<Button>{selectedTime.name}</Button>
|
||||
<Button>
|
||||
{selectedTime.name} <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</TextContainer>
|
||||
);
|
||||
|
141
frontend/src/components/Uplot/Uplot.tsx
Normal file
141
frontend/src/components/Uplot/Uplot.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './uplot.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import UPlot from 'uplot';
|
||||
|
||||
import { dataMatch, optionsUpdateState } from './utils';
|
||||
|
||||
export interface UplotProps {
|
||||
options: uPlot.Options;
|
||||
data: uPlot.AlignedData;
|
||||
onDelete?: (chart: uPlot) => void;
|
||||
onCreate?: (chart: uPlot) => void;
|
||||
resetScales?: boolean;
|
||||
}
|
||||
|
||||
const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||
(
|
||||
{ options, data, onDelete, onCreate, resetScales = true },
|
||||
ref,
|
||||
): JSX.Element | null => {
|
||||
const chartRef = useRef<uPlot | null>(null);
|
||||
const propOptionsRef = useRef(options);
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
const propDataRef = useRef(data);
|
||||
const onCreateRef = useRef(onCreate);
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
(): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
chartRef.current?.setSeries(graphIndex, { show: isVisible });
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onCreateRef.current = onCreate;
|
||||
onDeleteRef.current = onDelete;
|
||||
});
|
||||
|
||||
const destroy = useCallback((chart: uPlot | null) => {
|
||||
if (chart) {
|
||||
onDeleteRef.current?.(chart);
|
||||
chart.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const create = useCallback(() => {
|
||||
if (targetRef.current === null) return;
|
||||
|
||||
// If data is empty, hide cursor
|
||||
if (data && data[0] && data[0]?.length === 0) {
|
||||
propOptionsRef.current = {
|
||||
...propOptionsRef.current,
|
||||
cursor: { show: false },
|
||||
};
|
||||
}
|
||||
|
||||
const newChart = new UPlot(
|
||||
propOptionsRef.current,
|
||||
propDataRef.current,
|
||||
targetRef.current,
|
||||
);
|
||||
|
||||
chartRef.current = newChart;
|
||||
onCreateRef.current?.(newChart);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
create();
|
||||
return (): void => {
|
||||
destroy(chartRef.current);
|
||||
};
|
||||
}, [create, destroy]);
|
||||
|
||||
useEffect(() => {
|
||||
if (propOptionsRef.current !== options) {
|
||||
const optionsState = optionsUpdateState(propOptionsRef.current, options);
|
||||
propOptionsRef.current = options;
|
||||
if (!chartRef.current || optionsState === 'create') {
|
||||
destroy(chartRef.current);
|
||||
create();
|
||||
} else if (optionsState === 'update') {
|
||||
chartRef.current.setSize({
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [options, create, destroy]);
|
||||
|
||||
useEffect(() => {
|
||||
if (propDataRef.current !== data) {
|
||||
if (!chartRef.current) {
|
||||
propDataRef.current = data;
|
||||
create();
|
||||
} else if (!dataMatch(propDataRef.current, data)) {
|
||||
if (resetScales) {
|
||||
chartRef.current.setData(data, true);
|
||||
} else {
|
||||
chartRef.current.setData(data, false);
|
||||
chartRef.current.redraw();
|
||||
}
|
||||
}
|
||||
propDataRef.current = data;
|
||||
}
|
||||
}, [data, resetScales, create]);
|
||||
|
||||
return (
|
||||
<div className="uplot-graph-container" ref={targetRef}>
|
||||
{data && data[0] && data[0]?.length === 0 ? (
|
||||
<div className="not-found">
|
||||
<Typography>No Data</Typography>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Uplot.displayName = 'Uplot';
|
||||
|
||||
Uplot.defaultProps = {
|
||||
onDelete: undefined,
|
||||
onCreate: undefined,
|
||||
resetScales: true,
|
||||
};
|
||||
|
||||
export default memo(Uplot);
|
3
frontend/src/components/Uplot/index.ts
Normal file
3
frontend/src/components/Uplot/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import Uplot from './Uplot';
|
||||
|
||||
export default Uplot;
|
15
frontend/src/components/Uplot/uplot.scss
Normal file
15
frontend/src/components/Uplot/uplot.scss
Normal file
@ -0,0 +1,15 @@
|
||||
.not-found {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 0;
|
||||
height: 85%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.uplot-graph-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
48
frontend/src/components/Uplot/utils.ts
Normal file
48
frontend/src/components/Uplot/utils.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import uPlot from 'uplot';
|
||||
|
||||
type OptionsUpdateState = 'keep' | 'update' | 'create';
|
||||
|
||||
export const optionsUpdateState = (
|
||||
_lhs: uPlot.Options,
|
||||
_rhs: uPlot.Options,
|
||||
): OptionsUpdateState => {
|
||||
const { width: lhsWidth, height: lhsHeight, ...lhs } = _lhs;
|
||||
const { width: rhsWidth, height: rhsHeight, ...rhs } = _rhs;
|
||||
|
||||
let state: OptionsUpdateState = 'keep';
|
||||
|
||||
if (lhsHeight !== rhsHeight || lhsWidth !== rhsWidth) {
|
||||
state = 'update';
|
||||
}
|
||||
if (Object.keys(lhs).length !== Object.keys(rhs).length) {
|
||||
return 'create';
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const k of Object.keys(lhs)) {
|
||||
if (!Object.is((lhs as any)[k], (rhs as any)[k])) {
|
||||
state = 'create';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const dataMatch = (
|
||||
lhs: uPlot.AlignedData,
|
||||
rhs: uPlot.AlignedData,
|
||||
): boolean => {
|
||||
if (lhs.length !== rhs.length) {
|
||||
return false;
|
||||
}
|
||||
return lhs.every((lhsOneSeries, seriesIdx) => {
|
||||
const rhsOneSeries = rhs[seriesIdx];
|
||||
if (lhsOneSeries.length !== rhsOneSeries.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// compare each value in the series
|
||||
return (lhsOneSeries as number[])?.every(
|
||||
(value, valueIdx) => value === rhsOneSeries[valueIdx],
|
||||
);
|
||||
});
|
||||
};
|
@ -1,11 +1,11 @@
|
||||
import Graph from 'components/Graph';
|
||||
import Uplot from 'components/Uplot';
|
||||
import GridTableComponent from 'container/GridTableComponent';
|
||||
import GridValueComponent from 'container/GridValueComponent';
|
||||
|
||||
import { PANEL_TYPES } from './queryBuilder';
|
||||
|
||||
export const PANEL_TYPES_COMPONENT_MAP = {
|
||||
[PANEL_TYPES.TIME_SERIES]: Graph,
|
||||
[PANEL_TYPES.TIME_SERIES]: Uplot,
|
||||
[PANEL_TYPES.VALUE]: GridValueComponent,
|
||||
[PANEL_TYPES.TABLE]: GridTableComponent,
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
|
@ -9,7 +9,6 @@ const themeColors = {
|
||||
silver: '#BDBDBD',
|
||||
outrageousOrange: '#FF6633',
|
||||
roseBud: '#FFB399',
|
||||
magentaPink: '#FF33FF',
|
||||
canary: '#FFFF99',
|
||||
deepSkyBlue: '#00B3E6',
|
||||
goldTips: '#E6B333',
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { StaticLineProps } from 'components/Graph/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { useMemo } from 'react';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartData';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getChartData';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -54,20 +56,6 @@ function ChartPreview({
|
||||
targetUnit: query?.unit,
|
||||
});
|
||||
|
||||
const staticLine: StaticLineProps | undefined =
|
||||
threshold !== undefined
|
||||
? {
|
||||
yMin: thresholdValue,
|
||||
yMax: thresholdValue,
|
||||
borderColor: '#f14',
|
||||
borderWidth: 1,
|
||||
lineText: `${t('preview_chart_threshold_label')} (y=${thresholdValue} ${
|
||||
query?.unit || ''
|
||||
})`,
|
||||
textColor: '#f14',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const canQuery = useMemo((): boolean => {
|
||||
if (!query || query == null) {
|
||||
return false;
|
||||
@ -114,15 +102,36 @@ function ChartPreview({
|
||||
},
|
||||
);
|
||||
|
||||
const chartDataSet = queryResponse.isError
|
||||
? null
|
||||
: getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: queryResponse?.data?.payload?.data?.result ?? [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
id: 'alert_legend_widget',
|
||||
yAxisUnit: query?.unit,
|
||||
apiResponse: queryResponse?.data?.payload,
|
||||
dimensions: containerDimensions,
|
||||
isDarkMode,
|
||||
thresholdText: `${t(
|
||||
'preview_chart_threshold_label',
|
||||
)} (y=${thresholdValue} ${query?.unit || ''})`,
|
||||
thresholdValue,
|
||||
}),
|
||||
[
|
||||
query?.unit,
|
||||
queryResponse?.data?.payload,
|
||||
containerDimensions,
|
||||
isDarkMode,
|
||||
t,
|
||||
thresholdValue,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<ChartContainer>
|
||||
@ -136,18 +145,18 @@ function ChartPreview({
|
||||
{queryResponse.isLoading && (
|
||||
<Spinner size="large" tip="Loading..." height="70vh" />
|
||||
)}
|
||||
{chartDataSet && !queryResponse.isError && (
|
||||
<GridPanelSwitch
|
||||
panelType={graphType}
|
||||
title={name}
|
||||
data={chartDataSet.data}
|
||||
isStacked
|
||||
name={name || 'Chart Preview'}
|
||||
staticLine={staticLine}
|
||||
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
|
||||
query={query || initialQueriesMap.metrics}
|
||||
yAxisUnit={query?.unit}
|
||||
/>
|
||||
{chartData && !queryResponse.isError && (
|
||||
<div ref={graphRef} style={{ height: '100%' }}>
|
||||
<GridPanelSwitch
|
||||
options={options}
|
||||
panelType={graphType}
|
||||
data={chartData}
|
||||
name={name || 'Chart Preview'}
|
||||
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
|
||||
query={query || initialQueriesMap.metrics}
|
||||
yAxisUnit={query?.unit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ChartContainer>
|
||||
);
|
||||
|
@ -1,21 +0,0 @@
|
||||
.graph-manager-container {
|
||||
margin-top: 1.25rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
overflow-x: scroll;
|
||||
|
||||
.filter-table-container {
|
||||
flex-basis: 80%;
|
||||
}
|
||||
|
||||
.save-cancel-container {
|
||||
flex-basis: 20%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.save-cancel-button {
|
||||
margin: 0 0.313rem;
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import './GraphManager.styles.scss';
|
||||
import './WidgetFullView.styles.scss';
|
||||
|
||||
import { Button, Input } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
@ -19,12 +19,13 @@ function GraphManager({
|
||||
yAxisUnit,
|
||||
onToggleModelHandler,
|
||||
setGraphsVisibilityStates,
|
||||
graphsVisibilityStates = [],
|
||||
graphsVisibilityStates = [], // not trimed
|
||||
lineChartRef,
|
||||
parentChartRef,
|
||||
options,
|
||||
}: GraphManagerProps): JSX.Element {
|
||||
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(
|
||||
getDefaultTableDataSet(data),
|
||||
getDefaultTableDataSet(options, data),
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
@ -32,21 +33,22 @@ function GraphManager({
|
||||
const checkBoxOnChangeHandler = useCallback(
|
||||
(e: CheckboxChangeEvent, index: number): void => {
|
||||
const newStates = [...graphsVisibilityStates];
|
||||
|
||||
newStates[index] = e.target.checked;
|
||||
|
||||
lineChartRef?.current?.toggleGraph(index, e.target.checked);
|
||||
|
||||
parentChartRef?.current?.toggleGraph(index, e.target.checked);
|
||||
setGraphsVisibilityStates([...newStates]);
|
||||
},
|
||||
[graphsVisibilityStates, setGraphsVisibilityStates, lineChartRef],
|
||||
[
|
||||
graphsVisibilityStates,
|
||||
lineChartRef,
|
||||
parentChartRef,
|
||||
setGraphsVisibilityStates,
|
||||
],
|
||||
);
|
||||
|
||||
const labelClickedHandler = useCallback(
|
||||
(labelIndex: number): void => {
|
||||
const newGraphVisibilityStates = Array<boolean>(data.datasets.length).fill(
|
||||
false,
|
||||
);
|
||||
const newGraphVisibilityStates = Array<boolean>(data.length).fill(false);
|
||||
newGraphVisibilityStates[labelIndex] = true;
|
||||
|
||||
newGraphVisibilityStates.forEach((state, index) => {
|
||||
@ -55,18 +57,13 @@ function GraphManager({
|
||||
});
|
||||
setGraphsVisibilityStates(newGraphVisibilityStates);
|
||||
},
|
||||
[
|
||||
data.datasets.length,
|
||||
setGraphsVisibilityStates,
|
||||
lineChartRef,
|
||||
parentChartRef,
|
||||
],
|
||||
[data.length, lineChartRef, parentChartRef, setGraphsVisibilityStates],
|
||||
);
|
||||
|
||||
const columns = getGraphManagerTableColumns({
|
||||
data,
|
||||
tableDataSet,
|
||||
checkBoxOnChangeHandler,
|
||||
graphVisibilityState: graphsVisibilityStates || [],
|
||||
graphVisibilityState: graphsVisibilityStates,
|
||||
labelClickedHandler,
|
||||
yAxisUnit,
|
||||
});
|
||||
@ -87,7 +84,7 @@ function GraphManager({
|
||||
|
||||
const saveHandler = useCallback((): void => {
|
||||
saveLegendEntriesToLocalStorage({
|
||||
data,
|
||||
options,
|
||||
graphVisibilityState: graphsVisibilityStates || [],
|
||||
name,
|
||||
});
|
||||
@ -97,34 +94,49 @@ function GraphManager({
|
||||
if (onToggleModelHandler) {
|
||||
onToggleModelHandler();
|
||||
}
|
||||
}, [data, graphsVisibilityStates, name, notifications, onToggleModelHandler]);
|
||||
}, [
|
||||
graphsVisibilityStates,
|
||||
name,
|
||||
notifications,
|
||||
onToggleModelHandler,
|
||||
options,
|
||||
]);
|
||||
|
||||
const dataSource = tableDataSet.filter((item) => item.show);
|
||||
const dataSource = tableDataSet.filter(
|
||||
(item, index) => index !== 0 && item.show,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="graph-manager-container">
|
||||
<div className="filter-table-container">
|
||||
<div className="graph-manager-header">
|
||||
<Input onChange={filterHandler} placeholder="Filter Series" />
|
||||
<div className="save-cancel-container">
|
||||
<span className="save-cancel-button">
|
||||
<Button type="default" onClick={onToggleModelHandler}>
|
||||
Cancel
|
||||
</Button>
|
||||
</span>
|
||||
<span className="save-cancel-button">
|
||||
<Button type="primary" onClick={saveHandler}>
|
||||
Save
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="legends-list-container">
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
rowKey="index"
|
||||
pagination={false}
|
||||
scroll={{ y: 240 }}
|
||||
style={{
|
||||
maxHeight: 200,
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="save-cancel-container">
|
||||
<span className="save-cancel-button">
|
||||
<Button type="default" onClick={onToggleModelHandler}>
|
||||
Cancel
|
||||
</Button>
|
||||
</span>
|
||||
<span className="save-cancel-button">
|
||||
<Button onClick={saveHandler} type="primary">
|
||||
Save
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -10,13 +10,11 @@ function CustomCheckBox({
|
||||
graphVisibilityState = [],
|
||||
checkBoxOnChangeHandler,
|
||||
}: CheckBoxProps): JSX.Element {
|
||||
const { datasets } = data;
|
||||
|
||||
const onChangeHandler = (e: CheckboxChangeEvent): void => {
|
||||
checkBoxOnChangeHandler(e, index);
|
||||
};
|
||||
|
||||
const color = datasets[index]?.borderColor?.toString() || grey[0];
|
||||
const color = data[index]?.stroke?.toString() || grey[0];
|
||||
|
||||
const isChecked = graphVisibilityState[index] || false;
|
||||
|
||||
|
@ -6,7 +6,7 @@ import Label from './Label';
|
||||
export const getLabel = (
|
||||
labelClickedHandler: (labelIndex: number) => void,
|
||||
): ColumnType<DataSetProps> => ({
|
||||
render: (label, record): JSX.Element => (
|
||||
render: (label: string, record): JSX.Element => (
|
||||
<Label
|
||||
label={label}
|
||||
labelIndex={record.index}
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import { ChartData } from 'chart.js';
|
||||
|
||||
import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants';
|
||||
import { DataSetProps } from '../types';
|
||||
import { DataSetProps, ExtendedChartDataset } from '../types';
|
||||
import { getGraphManagerTableHeaderTitle } from '../utils';
|
||||
import CustomCheckBox from './CustomCheckBox';
|
||||
import { getLabel } from './GetLabel';
|
||||
|
||||
export const getGraphManagerTableColumns = ({
|
||||
data,
|
||||
tableDataSet,
|
||||
checkBoxOnChangeHandler,
|
||||
graphVisibilityState,
|
||||
labelClickedHandler,
|
||||
@ -22,7 +21,7 @@ export const getGraphManagerTableColumns = ({
|
||||
key: ColumnsKeyAndDataIndex.Index,
|
||||
render: (_: string, record: DataSetProps): JSX.Element => (
|
||||
<CustomCheckBox
|
||||
data={data}
|
||||
data={tableDataSet}
|
||||
index={record.index}
|
||||
checkBoxOnChangeHandler={checkBoxOnChangeHandler}
|
||||
graphVisibilityState={graphVisibilityState}
|
||||
@ -75,7 +74,7 @@ export const getGraphManagerTableColumns = ({
|
||||
];
|
||||
|
||||
interface GetGraphManagerTableColumnsProps {
|
||||
data: ChartData;
|
||||
tableDataSet: ExtendedChartDataset[];
|
||||
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||
labelClickedHandler: (labelIndex: number) => void;
|
||||
graphVisibilityState: boolean[];
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import { LabelContainer } from '../styles';
|
||||
import { LabelProps } from '../types';
|
||||
import { getAbbreviatedLabel } from '../utils';
|
||||
@ -7,12 +9,18 @@ function Label({
|
||||
labelIndex,
|
||||
label,
|
||||
}: LabelProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const onClickHandler = (): void => {
|
||||
labelClickedHandler(labelIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<LabelContainer type="button" onClick={onClickHandler}>
|
||||
<LabelContainer
|
||||
isDarkMode={isDarkMode}
|
||||
type="button"
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
{getAbbreviatedLabel(label)}
|
||||
</LabelContainer>
|
||||
);
|
||||
|
@ -0,0 +1,45 @@
|
||||
.full-view-container {
|
||||
height: 80vh;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
.full-view-header-container {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
height: calc(60% - 40px);
|
||||
min-height: 300px;
|
||||
border: 1px solid #333;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
margin: 16px 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.graph-manager-container {
|
||||
height: calc(40% - 40px);
|
||||
|
||||
.graph-manager-header {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.legends-list-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.save-cancel-container {
|
||||
flex-basis: 20%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.save-cancel-button {
|
||||
margin: 0 0.313rem;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
import './WidgetFullView.styles.scss';
|
||||
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
@ -10,16 +13,20 @@ import {
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useChartMutable } from 'hooks/useChartMutable';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartData';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getChartData';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||
import GraphManager from './GraphManager';
|
||||
// import GraphManager from './GraphManager';
|
||||
import { GraphContainer, TimeContainer } from './styles';
|
||||
import { FullViewProps } from './types';
|
||||
|
||||
@ -33,14 +40,18 @@ function FullView({
|
||||
isDependedDataLoaded = false,
|
||||
graphsVisibilityStates,
|
||||
onToggleModelHandler,
|
||||
setGraphsVisibilityStates,
|
||||
parentChartRef,
|
||||
setGraphsVisibilityStates,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const fullViewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [chartOptions, setChartOptions] = useState<uPlot.Options>();
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const getSelectedTime = useCallback(
|
||||
@ -49,7 +60,7 @@ function FullView({
|
||||
[widget],
|
||||
);
|
||||
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
const fullViewChartRef = useRef<ToggleGraphProps>();
|
||||
|
||||
const [selectedTime, setSelectedTime] = useState<timePreferance>({
|
||||
name: getSelectedTime()?.name || '',
|
||||
@ -77,80 +88,108 @@ function FullView({
|
||||
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||
});
|
||||
|
||||
const chartDataSet = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: response?.data?.payload?.data?.result || [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[response],
|
||||
);
|
||||
const chartData = getUPlotChartData(response?.data?.payload);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
useEffect(() => {
|
||||
if (!response.isFetching && lineChartRef.current) {
|
||||
graphsVisibilityStates?.forEach((e, i) => {
|
||||
lineChartRef?.current?.toggleGraph(i, e);
|
||||
parentChartRef?.current?.toggleGraph(i, e);
|
||||
if (!response.isFetching && fullViewRef.current) {
|
||||
const width = fullViewRef.current?.clientWidth
|
||||
? fullViewRef.current.clientWidth - 45
|
||||
: 700;
|
||||
|
||||
const height = fullViewRef.current?.clientWidth
|
||||
? fullViewRef.current.clientHeight
|
||||
: 300;
|
||||
|
||||
const newChartOptions = getUPlotChartOptions({
|
||||
yAxisUnit: yAxisUnit || '',
|
||||
apiResponse: response.data?.payload,
|
||||
dimensions: {
|
||||
height,
|
||||
width,
|
||||
},
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
graphsVisibilityStates,
|
||||
setGraphsVisibilityStates,
|
||||
});
|
||||
|
||||
setChartOptions(newChartOptions);
|
||||
}
|
||||
}, [graphsVisibilityStates, parentChartRef, response.isFetching]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [response.isFetching, graphsVisibilityStates, fullViewRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
graphsVisibilityStates?.forEach((e, i) => {
|
||||
fullViewChartRef?.current?.toggleGraph(i, e);
|
||||
parentChartRef?.current?.toggleGraph(i, e);
|
||||
});
|
||||
}, [graphsVisibilityStates, parentChartRef]);
|
||||
|
||||
if (response.isFetching) {
|
||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{fullViewOptions && (
|
||||
<TimeContainer $panelType={widget.panelTypes}>
|
||||
<TimePreference
|
||||
selectedTime={selectedTime}
|
||||
setSelectedTime={setSelectedTime}
|
||||
/>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
response.refetch();
|
||||
}}
|
||||
type="primary"
|
||||
<div className="full-view-container">
|
||||
<div className="full-view-header-container">
|
||||
{fullViewOptions && (
|
||||
<TimeContainer $panelType={widget.panelTypes}>
|
||||
<TimePreference
|
||||
selectedTime={selectedTime}
|
||||
setSelectedTime={setSelectedTime}
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
}}
|
||||
onClick={(): void => {
|
||||
response.refetch();
|
||||
}}
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
/>
|
||||
</TimeContainer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="graph-container" ref={fullViewRef}>
|
||||
{chartOptions && (
|
||||
<GraphContainer
|
||||
style={{ height: '90%' }}
|
||||
isGraphLegendToggleAvailable={canModifyChart}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</TimeContainer>
|
||||
)}
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={chartData}
|
||||
options={chartOptions}
|
||||
onClickHandler={onClickHandler}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
panelData={response.data?.payload.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
ref={fullViewChartRef}
|
||||
/>
|
||||
</GraphContainer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<GraphContainer isGraphLegendToggleAvailable={canModifyChart}>
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={chartDataSet.data}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={widget.title}
|
||||
onClickHandler={onClickHandler}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
panelData={response.data?.payload.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
ref={lineChartRef}
|
||||
/>
|
||||
</GraphContainer>
|
||||
|
||||
{canModifyChart && (
|
||||
{canModifyChart && chartOptions && (
|
||||
<GraphManager
|
||||
data={chartDataSet.data}
|
||||
data={chartData}
|
||||
name={name}
|
||||
options={chartOptions}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
setGraphsVisibilityStates={setGraphsVisibilityStates}
|
||||
graphsVisibilityStates={graphsVisibilityStates}
|
||||
lineChartRef={lineChartRef}
|
||||
lineChartRef={fullViewChartRef}
|
||||
parentChartRef={parentChartRef}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -31,10 +31,11 @@ export const GraphContainer = styled.div<GraphContainerProps>`
|
||||
isGraphLegendToggleAvailable ? '50%' : '100%'};
|
||||
`;
|
||||
|
||||
export const LabelContainer = styled.button`
|
||||
export const LabelContainer = styled.button<{ isDarkMode?: boolean }>`
|
||||
max-width: 18.75rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: ${themeColors.white};
|
||||
color: ${(props): string =>
|
||||
props.isDarkMode ? themeColors.white : themeColors.black};
|
||||
`;
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ChartData, ChartDataset } from 'chart.js';
|
||||
import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { UplotProps } from 'components/Uplot/Uplot';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { MutableRefObject } from 'react';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
export interface DataSetProps {
|
||||
index: number;
|
||||
@ -22,12 +24,13 @@ export interface LegendEntryProps {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export type ExtendedChartDataset = ChartDataset & {
|
||||
export type ExtendedChartDataset = uPlot.Series & {
|
||||
show: boolean;
|
||||
sum: number;
|
||||
avg: number;
|
||||
min: number;
|
||||
max: number;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type PanelTypeAndGraphManagerVisibilityProps = Record<
|
||||
@ -44,22 +47,22 @@ export interface LabelProps {
|
||||
export interface FullViewProps {
|
||||
widget: Widgets;
|
||||
fullViewOptions?: boolean;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
isDependedDataLoaded?: boolean;
|
||||
graphsVisibilityStates?: boolean[];
|
||||
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||
setGraphsVisibilityStates: (graphsVisibilityStates: boolean[]) => void;
|
||||
setGraphsVisibilityStates: Dispatch<SetStateAction<boolean[]>>;
|
||||
parentChartRef: GraphManagerProps['lineChartRef'];
|
||||
}
|
||||
|
||||
export interface GraphManagerProps {
|
||||
data: ChartData;
|
||||
export interface GraphManagerProps extends UplotProps {
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
onToggleModelHandler?: () => void;
|
||||
options: uPlot.Options;
|
||||
setGraphsVisibilityStates: FullViewProps['setGraphsVisibilityStates'];
|
||||
graphsVisibilityStates: FullViewProps['graphsVisibilityStates'];
|
||||
lineChartRef?: MutableRefObject<ToggleGraphProps | undefined>;
|
||||
@ -67,14 +70,14 @@ export interface GraphManagerProps {
|
||||
}
|
||||
|
||||
export interface CheckBoxProps {
|
||||
data: ChartData;
|
||||
data: ExtendedChartDataset[];
|
||||
index: number;
|
||||
graphVisibilityState: boolean[];
|
||||
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||
}
|
||||
|
||||
export interface SaveLegendEntriesToLocalStoreProps {
|
||||
data: ChartData;
|
||||
options: uPlot.Options;
|
||||
graphVisibilityState: boolean[];
|
||||
name: string;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ChartData, ChartDataset } from 'chart.js';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import {
|
||||
ExtendedChartDataset,
|
||||
@ -21,33 +21,23 @@ function convertToTwoDecimalsOrZero(value: number): number {
|
||||
}
|
||||
|
||||
export const getDefaultTableDataSet = (
|
||||
data: ChartData,
|
||||
options: uPlot.Options,
|
||||
data: uPlot.AlignedData,
|
||||
): ExtendedChartDataset[] =>
|
||||
data.datasets.map(
|
||||
(item: ChartDataset): ExtendedChartDataset => {
|
||||
if (item.data.length === 0) {
|
||||
return {
|
||||
...item,
|
||||
show: true,
|
||||
sum: 0,
|
||||
avg: 0,
|
||||
max: 0,
|
||||
min: 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
show: true,
|
||||
sum: convertToTwoDecimalsOrZero(
|
||||
(item.data as number[]).reduce((a, b) => a + b, 0),
|
||||
),
|
||||
avg: convertToTwoDecimalsOrZero(
|
||||
(item.data as number[]).reduce((a, b) => a + b, 0) / item.data.length,
|
||||
),
|
||||
max: convertToTwoDecimalsOrZero(Math.max(...(item.data as number[]))),
|
||||
min: convertToTwoDecimalsOrZero(Math.min(...(item.data as number[]))),
|
||||
};
|
||||
},
|
||||
options.series.map(
|
||||
(item: uPlot.Series, index: number): ExtendedChartDataset => ({
|
||||
...item,
|
||||
index,
|
||||
show: true,
|
||||
sum: convertToTwoDecimalsOrZero(
|
||||
(data[index] as number[]).reduce((a, b) => a + b, 0),
|
||||
),
|
||||
avg: convertToTwoDecimalsOrZero(
|
||||
(data[index] as number[]).reduce((a, b) => a + b, 0) / data[index].length,
|
||||
),
|
||||
max: convertToTwoDecimalsOrZero(Math.max(...(data[index] as number[]))),
|
||||
min: convertToTwoDecimalsOrZero(Math.min(...(data[index] as number[]))),
|
||||
}),
|
||||
);
|
||||
|
||||
export const getAbbreviatedLabel = (label: string): string => {
|
||||
@ -58,22 +48,24 @@ export const getAbbreviatedLabel = (label: string): string => {
|
||||
return newLabel;
|
||||
};
|
||||
|
||||
export const showAllDataSet = (data: ChartData): LegendEntryProps[] =>
|
||||
data.datasets.map(
|
||||
(item): LegendEntryProps => ({
|
||||
label: item.label || '',
|
||||
show: true,
|
||||
}),
|
||||
);
|
||||
export const showAllDataSet = (options: uPlot.Options): LegendEntryProps[] =>
|
||||
options.series
|
||||
.map(
|
||||
(item): LegendEntryProps => ({
|
||||
label: item.label || '',
|
||||
show: true,
|
||||
}),
|
||||
)
|
||||
.filter((_, index) => index !== 0);
|
||||
|
||||
export const saveLegendEntriesToLocalStorage = ({
|
||||
data,
|
||||
options,
|
||||
graphVisibilityState,
|
||||
name,
|
||||
}: SaveLegendEntriesToLocalStoreProps): void => {
|
||||
const newLegendEntry = {
|
||||
name,
|
||||
dataIndex: data.datasets.map(
|
||||
dataIndex: options.series.map(
|
||||
(item, index): LegendEntryProps => ({
|
||||
label: item.label || '',
|
||||
show: graphVisibilityState[index],
|
||||
|
@ -1,62 +0,0 @@
|
||||
import { mockTestData } from './__mock__/mockChartData';
|
||||
import { mocklegendEntryResult } from './__mock__/mockLegendEntryData';
|
||||
import { showAllDataSet } from './FullView/utils';
|
||||
import { getGraphVisibilityStateOnDataChange } from './utils';
|
||||
|
||||
describe('getGraphVisibilityStateOnDataChange', () => {
|
||||
beforeEach(() => {
|
||||
const localStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
};
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||
});
|
||||
|
||||
it('should return the correct visibility state and legend entry', () => {
|
||||
// Mock the localStorage behavior
|
||||
const mockLocalStorageData = [
|
||||
{
|
||||
name: 'exampleexpanded',
|
||||
dataIndex: [
|
||||
{ label: 'customer', show: true },
|
||||
{ label: 'demo-app', show: false },
|
||||
],
|
||||
},
|
||||
];
|
||||
jest
|
||||
.spyOn(window.localStorage, 'getItem')
|
||||
.mockReturnValue(JSON.stringify(mockLocalStorageData));
|
||||
|
||||
const result1 = getGraphVisibilityStateOnDataChange({
|
||||
data: mockTestData,
|
||||
isExpandedName: true,
|
||||
name: 'example',
|
||||
});
|
||||
expect(result1.graphVisibilityStates).toEqual([true, false]);
|
||||
expect(result1.legendEntry).toEqual(mocklegendEntryResult);
|
||||
|
||||
const result2 = getGraphVisibilityStateOnDataChange({
|
||||
data: mockTestData,
|
||||
isExpandedName: false,
|
||||
name: 'example',
|
||||
});
|
||||
expect(result2.graphVisibilityStates).toEqual(
|
||||
Array(mockTestData.datasets.length).fill(true),
|
||||
);
|
||||
expect(result2.legendEntry).toEqual(showAllDataSet(mockTestData));
|
||||
});
|
||||
|
||||
it('should return default values if localStorage data is not available', () => {
|
||||
// Mock the localStorage behavior to return null
|
||||
jest.spyOn(window.localStorage, 'getItem').mockReturnValue(null);
|
||||
|
||||
const result = getGraphVisibilityStateOnDataChange({
|
||||
data: mockTestData,
|
||||
isExpandedName: true,
|
||||
name: 'example',
|
||||
});
|
||||
expect(result.graphVisibilityStates).toEqual(
|
||||
Array(mockTestData.datasets.length).fill(true),
|
||||
);
|
||||
expect(result.legendEntry).toEqual(showAllDataSet(mockTestData));
|
||||
});
|
||||
});
|
@ -25,21 +25,22 @@ import { v4 } from 'uuid';
|
||||
|
||||
import WidgetHeader from '../WidgetHeader';
|
||||
import FullView from './FullView';
|
||||
import { FullViewContainer, Modal } from './styles';
|
||||
import { Modal } from './styles';
|
||||
import { WidgetGraphComponentProps } from './types';
|
||||
import { getGraphVisibilityStateOnDataChange } from './utils';
|
||||
|
||||
function WidgetGraphComponent({
|
||||
data,
|
||||
widget,
|
||||
queryResponse,
|
||||
errorMessage,
|
||||
name,
|
||||
onDragSelect,
|
||||
onClickHandler,
|
||||
threshold,
|
||||
headerMenuList,
|
||||
isWarning,
|
||||
data,
|
||||
options,
|
||||
onDragSelect,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const [modal, setModal] = useState<boolean>(false);
|
||||
@ -48,15 +49,16 @@ function WidgetGraphComponent({
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
|
||||
() =>
|
||||
getGraphVisibilityStateOnDataChange({
|
||||
data,
|
||||
options,
|
||||
isExpandedName: true,
|
||||
name,
|
||||
}),
|
||||
[data, name],
|
||||
[options, name],
|
||||
);
|
||||
|
||||
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
|
||||
@ -64,6 +66,7 @@ function WidgetGraphComponent({
|
||||
>(localStoredVisibilityStates);
|
||||
|
||||
useEffect(() => {
|
||||
setGraphsVisibilityStates(localStoredVisibilityStates);
|
||||
if (!lineChartRef.current) return;
|
||||
|
||||
localStoredVisibilityStates.forEach((state, index) => {
|
||||
@ -74,9 +77,10 @@ function WidgetGraphComponent({
|
||||
|
||||
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
const featureResponse = useSelector<AppState, AppReducer['featureResponse']>(
|
||||
(state) => state.app.featureResponse,
|
||||
);
|
||||
|
||||
const onToggleModal = useCallback(
|
||||
(func: Dispatch<SetStateAction<boolean>>) => {
|
||||
func((value) => !value);
|
||||
@ -133,7 +137,7 @@ function WidgetGraphComponent({
|
||||
i: uuid,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 2,
|
||||
h: 3,
|
||||
y: 0,
|
||||
},
|
||||
];
|
||||
@ -186,8 +190,22 @@ function WidgetGraphComponent({
|
||||
onToggleModal(setModal);
|
||||
};
|
||||
|
||||
if (queryResponse.isLoading || queryResponse.status === 'idle') {
|
||||
return (
|
||||
<Skeleton
|
||||
style={{
|
||||
height: '100%',
|
||||
padding: '16px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
}}
|
||||
onMouseOver={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
@ -200,6 +218,7 @@ function WidgetGraphComponent({
|
||||
onBlur={(): void => {
|
||||
setHovered(false);
|
||||
}}
|
||||
id={name}
|
||||
>
|
||||
<Modal
|
||||
destroyOnClose
|
||||
@ -214,7 +233,7 @@ function WidgetGraphComponent({
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="View"
|
||||
title={widget?.title || 'View'}
|
||||
footer={[]}
|
||||
centered
|
||||
open={modal}
|
||||
@ -222,17 +241,16 @@ function WidgetGraphComponent({
|
||||
width="85%"
|
||||
destroyOnClose
|
||||
>
|
||||
<FullViewContainer>
|
||||
<FullView
|
||||
name={`${name}expanded`}
|
||||
widget={widget}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
graphsVisibilityStates={graphsVisibilityStates}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
setGraphsVisibilityStates={setGraphsVisibilityStates}
|
||||
parentChartRef={lineChartRef}
|
||||
/>
|
||||
</FullViewContainer>
|
||||
<FullView
|
||||
name={`${name}expanded`}
|
||||
widget={widget}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
parentChartRef={lineChartRef}
|
||||
onDragSelect={onDragSelect}
|
||||
setGraphsVisibilityStates={setGraphsVisibilityStates}
|
||||
graphsVisibilityStates={graphsVisibilityStates}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<div className="drag-handle">
|
||||
@ -252,29 +270,27 @@ function WidgetGraphComponent({
|
||||
</div>
|
||||
{queryResponse.isLoading && <Skeleton />}
|
||||
{queryResponse.isSuccess && (
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={data}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '}
|
||||
name={name}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
onClickHandler={onClickHandler}
|
||||
onDragSelect={onDragSelect}
|
||||
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
ref={lineChartRef}
|
||||
/>
|
||||
<div style={{ height: '90%' }} ref={graphRef}>
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={data}
|
||||
name={name}
|
||||
ref={lineChartRef}
|
||||
options={options}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
onClickHandler={onClickHandler}
|
||||
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
WidgetGraphComponent.defaultProps = {
|
||||
yAxisUnit: undefined,
|
||||
setLayout: undefined,
|
||||
onDragSelect: undefined,
|
||||
onClickHandler: undefined,
|
||||
};
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartData';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getChartData';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import _noop from 'lodash-es/noop';
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -20,30 +23,30 @@ import WidgetGraphComponent from './WidgetGraphComponent';
|
||||
function GridCardGraph({
|
||||
widget,
|
||||
name,
|
||||
onClickHandler,
|
||||
onClickHandler = _noop,
|
||||
headerMenuList = [MenuItemKeys.View],
|
||||
isQueryEnabled,
|
||||
threshold,
|
||||
variables,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const onDragSelect = (start: number, end: number): void => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
};
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const { ref: graphRef, inView: isGraphVisible } = useInView({
|
||||
threshold: 0,
|
||||
triggerOnce: true,
|
||||
initialInView: false,
|
||||
});
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const isVisible = useIntersectionObserver(graphRef, undefined, true);
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
@ -61,20 +64,20 @@ function GridCardGraph({
|
||||
graphType: widget?.panelTypes,
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
variables: getDashboardVariables(variables),
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
maxTime,
|
||||
minTime,
|
||||
globalSelectedInterval,
|
||||
selectedDashboard?.data?.variables,
|
||||
variables,
|
||||
widget?.query,
|
||||
widget?.panelTypes,
|
||||
widget.timePreferance,
|
||||
],
|
||||
keepPreviousData: true,
|
||||
enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled,
|
||||
enabled: isVisible && !isEmptyWidget && isQueryEnabled,
|
||||
refetchOnMount: false,
|
||||
onError: (error) => {
|
||||
setErrorMessage(error.message);
|
||||
@ -82,44 +85,61 @@ function GridCardGraph({
|
||||
},
|
||||
);
|
||||
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: queryResponse?.data?.payload?.data?.result || [],
|
||||
},
|
||||
],
|
||||
createDataset: undefined,
|
||||
isWarningLimit: widget.panelTypes === PANEL_TYPES.TIME_SERIES,
|
||||
}),
|
||||
[queryResponse, widget?.panelTypes],
|
||||
);
|
||||
|
||||
const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET;
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const menuList =
|
||||
widget.panelTypes === PANEL_TYPES.TABLE
|
||||
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
|
||||
: headerMenuList;
|
||||
|
||||
return (
|
||||
<span ref={graphRef}>
|
||||
<WidgetGraphComponent
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
data={chartData.data}
|
||||
isWarning={chartData.isWarning}
|
||||
name={name}
|
||||
onDragSelect={onDragSelect}
|
||||
threshold={threshold}
|
||||
headerMenuList={menuList}
|
||||
onClickHandler={onClickHandler}
|
||||
/>
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
id: widget?.id,
|
||||
apiResponse: queryResponse.data?.payload,
|
||||
dimensions: containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
yAxisUnit: widget?.yAxisUnit,
|
||||
onClickHandler,
|
||||
}),
|
||||
[
|
||||
widget?.id,
|
||||
widget?.yAxisUnit,
|
||||
queryResponse.data?.payload,
|
||||
containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
onClickHandler,
|
||||
],
|
||||
);
|
||||
|
||||
{isEmptyLayout && <EmptyWidget />}
|
||||
</span>
|
||||
return (
|
||||
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
||||
{isEmptyLayout ? (
|
||||
<EmptyWidget />
|
||||
) : (
|
||||
<WidgetGraphComponent
|
||||
data={chartData}
|
||||
options={options}
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
isWarning={false}
|
||||
name={name}
|
||||
onDragSelect={onDragSelect}
|
||||
threshold={threshold}
|
||||
headerMenuList={menuList}
|
||||
onClickHandler={onClickHandler}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { ChartData } from 'chart.js';
|
||||
import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { UplotProps } from 'components/Uplot/Uplot';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { MutableRefObject, ReactNode } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||
import { LegendEntryProps } from './FullView/types';
|
||||
@ -14,16 +16,15 @@ export interface GraphVisibilityLegendEntryProps {
|
||||
legendEntry: LegendEntryProps[];
|
||||
}
|
||||
|
||||
export interface WidgetGraphComponentProps {
|
||||
export interface WidgetGraphComponentProps extends UplotProps {
|
||||
widget: Widgets;
|
||||
queryResponse: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
||||
>;
|
||||
errorMessage: string | undefined;
|
||||
data: ChartData;
|
||||
name: string;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
threshold?: ReactNode;
|
||||
headerMenuList: MenuItemKeys[];
|
||||
isWarning: boolean;
|
||||
@ -33,14 +34,15 @@ export interface GridCardGraphProps {
|
||||
widget: Widgets;
|
||||
name: string;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
threshold?: ReactNode;
|
||||
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
|
||||
isQueryEnabled: boolean;
|
||||
variables?: Dashboard['data']['variables'];
|
||||
}
|
||||
|
||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||
data: ChartData;
|
||||
options: uPlot.Options;
|
||||
isExpandedName: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
import { LegendEntryProps } from './FullView/types';
|
||||
@ -9,13 +10,13 @@ import {
|
||||
} from './types';
|
||||
|
||||
export const getGraphVisibilityStateOnDataChange = ({
|
||||
data,
|
||||
options,
|
||||
isExpandedName,
|
||||
name,
|
||||
}: GetGraphVisibilityStateOnLegendClickProps): GraphVisibilityLegendEntryProps => {
|
||||
const visibilityStateAndLegendEntry: GraphVisibilityLegendEntryProps = {
|
||||
graphVisibilityStates: Array(data.datasets.length).fill(true),
|
||||
legendEntry: showAllDataSet(data),
|
||||
graphVisibilityStates: Array(options.series.length).fill(true),
|
||||
legendEntry: showAllDataSet(options),
|
||||
};
|
||||
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
|
||||
const legendGraphFromLocalStore = localStorage.getItem(
|
||||
@ -35,17 +36,19 @@ export const getGraphVisibilityStateOnDataChange = ({
|
||||
);
|
||||
}
|
||||
|
||||
const newGraphVisibilityStates = Array(data.datasets.length).fill(true);
|
||||
const newGraphVisibilityStates = Array(options.series.length).fill(true);
|
||||
legendFromLocalStore.forEach((item) => {
|
||||
const newName = isExpandedName ? `${name}expanded` : name;
|
||||
if (item.name === newName) {
|
||||
visibilityStateAndLegendEntry.legendEntry = item.dataIndex;
|
||||
data.datasets.forEach((datasets, i) => {
|
||||
const index = item.dataIndex.findIndex(
|
||||
(dataKey) => dataKey.label === datasets.label,
|
||||
);
|
||||
if (index !== -1) {
|
||||
newGraphVisibilityStates[i] = item.dataIndex[index].show;
|
||||
options.series.forEach((datasets, i) => {
|
||||
if (i !== 0) {
|
||||
const index = item.dataIndex.findIndex(
|
||||
(dataKey) => dataKey.label === datasets.label,
|
||||
);
|
||||
if (index !== -1) {
|
||||
newGraphVisibilityStates[i] = item.dataIndex[index].show;
|
||||
}
|
||||
}
|
||||
});
|
||||
visibilityStateAndLegendEntry.graphVisibilityStates = newGraphVisibilityStates;
|
||||
|
@ -25,10 +25,7 @@ import {
|
||||
} from './styles';
|
||||
import { GraphLayoutProps } from './types';
|
||||
|
||||
function GraphLayout({
|
||||
onAddPanelHandler,
|
||||
widgets,
|
||||
}: GraphLayoutProps): JSX.Element {
|
||||
function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
const {
|
||||
selectedDashboard,
|
||||
layouts,
|
||||
@ -36,6 +33,10 @@ function GraphLayout({
|
||||
setSelectedDashboard,
|
||||
isDashboardLocked,
|
||||
} = useDashboard();
|
||||
const { data } = selectedDashboard || {};
|
||||
|
||||
const { widgets, variables } = data || {};
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
|
||||
const { featureResponse, role, user } = useSelector<AppState, AppReducer>(
|
||||
@ -129,6 +130,7 @@ function GraphLayout({
|
||||
rowHeight={100}
|
||||
autoSize
|
||||
width={100}
|
||||
useCSSTransforms
|
||||
isDraggable={!isDashboardLocked && addPanelPermission}
|
||||
isDroppable={!isDashboardLocked && addPanelPermission}
|
||||
isResizable={!isDashboardLocked && addPanelPermission}
|
||||
@ -156,6 +158,7 @@ function GraphLayout({
|
||||
widget={currentWidget || ({ id, query: {} } as Widgets)}
|
||||
name={currentWidget?.id || ''}
|
||||
headerMenuList={widgetActions}
|
||||
variables={variables}
|
||||
/>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
|
@ -5,10 +5,8 @@ import styled from 'styled-components';
|
||||
export const HeaderContainer = styled.div<{ hover: boolean }>`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background: ${({ hover }): string => (hover ? `${grey[0]}66` : 'inherit')};
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.8rem;
|
||||
cursor: all-scroll;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -20,12 +18,6 @@ export const HeaderContentContainer = styled.span`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const ArrowContainer = styled.span<{ hover: boolean }>`
|
||||
visibility: ${({ hover }): string => (hover ? 'visible' : 'hidden')};
|
||||
position: absolute;
|
||||
right: -1rem;
|
||||
`;
|
||||
|
||||
export const ThesholdContainer = styled.span`
|
||||
margin-top: -0.3rem;
|
||||
`;
|
||||
@ -39,8 +31,18 @@ export const DisplayThresholdContainer = styled.div`
|
||||
|
||||
export const WidgetHeaderContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
`;
|
||||
|
||||
export const ArrowContainer = styled.span<{ hover: boolean }>`
|
||||
visibility: ${({ hover }): string => (hover ? 'visible' : 'hidden')};
|
||||
position: absolute;
|
||||
right: -1rem;
|
||||
`;
|
||||
|
||||
export const Typography = styled(TypographyComponent)`
|
||||
|
@ -16,6 +16,6 @@ export const EMPTY_WIDGET_LAYOUT = {
|
||||
i: PANEL_TYPES.EMPTY_WIDGET,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 2,
|
||||
h: 3,
|
||||
y: 0,
|
||||
};
|
||||
|
@ -6,14 +6,7 @@ import { EMPTY_WIDGET_LAYOUT } from './config';
|
||||
import GraphLayoutContainer from './GridCardLayout';
|
||||
|
||||
function GridGraph(): JSX.Element {
|
||||
const {
|
||||
selectedDashboard,
|
||||
setLayouts,
|
||||
handleToggleDashboardSlider,
|
||||
} = useDashboard();
|
||||
|
||||
const { data } = selectedDashboard || {};
|
||||
const { widgets } = data || {};
|
||||
const { handleToggleDashboardSlider, setLayouts } = useDashboard();
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(() => {
|
||||
handleToggleDashboardSlider(true);
|
||||
@ -24,12 +17,7 @@ function GridGraph(): JSX.Element {
|
||||
]);
|
||||
}, [handleToggleDashboardSlider, setLayouts]);
|
||||
|
||||
return (
|
||||
<GraphLayoutContainer
|
||||
onAddPanelHandler={onEmptyWidgetHandler}
|
||||
widgets={widgets}
|
||||
/>
|
||||
);
|
||||
return <GraphLayoutContainer onAddPanelHandler={onEmptyWidgetHandler} />;
|
||||
}
|
||||
|
||||
export default GridGraph;
|
||||
|
@ -13,10 +13,11 @@ interface CardProps {
|
||||
export const Card = styled(CardComponent)<CardProps>`
|
||||
&&& {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: 95%;
|
||||
height: 90%;
|
||||
padding: 0;
|
||||
${({ $panelType }): FlattenSimpleInterpolation =>
|
||||
$panelType === PANEL_TYPES.TABLE
|
||||
|
@ -1,6 +1,3 @@
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface GraphLayoutProps {
|
||||
onAddPanelHandler: VoidFunction;
|
||||
widgets?: Widgets[];
|
||||
}
|
||||
|
@ -11,37 +11,17 @@ const GridPanelSwitch = forwardRef<
|
||||
GridPanelSwitchProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
panelType,
|
||||
data,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
panelData,
|
||||
query,
|
||||
},
|
||||
{ panelType, data, yAxisUnit, panelData, query, options },
|
||||
ref,
|
||||
): JSX.Element | null => {
|
||||
const currentProps: PropsTypePropsMap = useMemo(() => {
|
||||
const result: PropsTypePropsMap = {
|
||||
[PANEL_TYPES.TIME_SERIES]: {
|
||||
type: 'line',
|
||||
data,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
options,
|
||||
ref,
|
||||
},
|
||||
[PANEL_TYPES.VALUE]: {
|
||||
title,
|
||||
data,
|
||||
yAxisUnit,
|
||||
},
|
||||
@ -52,19 +32,7 @@ const GridPanelSwitch = forwardRef<
|
||||
};
|
||||
|
||||
return result;
|
||||
}, [
|
||||
data,
|
||||
isStacked,
|
||||
name,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
staticLine,
|
||||
title,
|
||||
yAxisUnit,
|
||||
panelData,
|
||||
query,
|
||||
ref,
|
||||
]);
|
||||
}, [data, options, ref, yAxisUnit, panelData, query]);
|
||||
|
||||
const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC<
|
||||
PropsTypePropsMap[typeof panelType]
|
||||
|
@ -1,24 +1,20 @@
|
||||
import { ChartData } from 'chart.js';
|
||||
import {
|
||||
GraphOnClickHandler,
|
||||
GraphProps,
|
||||
StaticLineProps,
|
||||
} from 'components/Graph/types';
|
||||
import { StaticLineProps, ToggleGraphProps } from 'components/Graph/types';
|
||||
import { UplotProps } from 'components/Uplot/Uplot';
|
||||
import { GridTableComponentProps } from 'container/GridTableComponent/types';
|
||||
import { GridValueComponentProps } from 'container/GridValueComponent/types';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { ForwardedRef } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { PANEL_TYPES } from '../../constants/queryBuilder';
|
||||
|
||||
export type GridPanelSwitchProps = {
|
||||
panelType: PANEL_TYPES;
|
||||
data: ChartData;
|
||||
title?: Widgets['title'];
|
||||
opacity?: string;
|
||||
isStacked?: boolean;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
data: uPlot.AlignedData;
|
||||
options: uPlot.Options;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
staticLine?: StaticLineProps;
|
||||
@ -28,7 +24,9 @@ export type GridPanelSwitchProps = {
|
||||
};
|
||||
|
||||
export type PropsTypePropsMap = {
|
||||
[PANEL_TYPES.TIME_SERIES]: GraphProps;
|
||||
[PANEL_TYPES.TIME_SERIES]: UplotProps & {
|
||||
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
||||
};
|
||||
[PANEL_TYPES.VALUE]: GridValueComponentProps;
|
||||
[PANEL_TYPES.TABLE]: GridTableComponentProps;
|
||||
[PANEL_TYPES.TRACE]: null;
|
||||
|
@ -13,16 +13,16 @@ function GridValueComponent({
|
||||
title,
|
||||
yAxisUnit,
|
||||
}: GridValueComponentProps): JSX.Element {
|
||||
const value = (((data.datasets[0] || []).data || [])[0] || 0) as number;
|
||||
const value = ((data[1] || [])[0] || 0) as number;
|
||||
|
||||
const location = useLocation();
|
||||
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
|
||||
|
||||
const isDashboardPage = location.pathname.split('/').length === 3;
|
||||
|
||||
if (data.datasets.length === 0) {
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<ValueContainer isDashboardPage={isDashboardPage}>
|
||||
<ValueContainer>
|
||||
<Typography>No Data</Typography>
|
||||
</ValueContainer>
|
||||
);
|
||||
@ -33,7 +33,7 @@ function GridValueComponent({
|
||||
<TitleContainer isDashboardPage={isDashboardPage}>
|
||||
<Typography>{gridTitle}</Typography>
|
||||
</TitleContainer>
|
||||
<ValueContainer isDashboardPage={isDashboardPage}>
|
||||
<ValueContainer>
|
||||
<ValueGraph
|
||||
value={
|
||||
yAxisUnit
|
||||
|
@ -3,9 +3,9 @@ import styled from 'styled-components';
|
||||
interface Props {
|
||||
isDashboardPage: boolean;
|
||||
}
|
||||
export const ValueContainer = styled.div<Props>`
|
||||
height: ${({ isDashboardPage }): string =>
|
||||
isDashboardPage ? '100%' : '55vh'};
|
||||
|
||||
export const ValueContainer = styled.div`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ChartData } from 'chart.js';
|
||||
import { ReactNode } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
export type GridValueComponentProps = {
|
||||
data: ChartData;
|
||||
title?: ReactNode;
|
||||
data: uPlot.AlignedData;
|
||||
options?: uPlot.Options;
|
||||
title?: React.ReactNode;
|
||||
yAxisUnit?: string;
|
||||
};
|
||||
|
@ -109,13 +109,12 @@ function DBCall(): JSX.Element {
|
||||
<Graph
|
||||
name="database_call_rps"
|
||||
widget={databaseCallsRPSWidget}
|
||||
headerMenuList={MENU_ITEMS}
|
||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||
onGraphClickHandler(setSelectedTimeStamp)(
|
||||
ChartEvent,
|
||||
activeElements,
|
||||
chart,
|
||||
data,
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
'database_call_rps',
|
||||
);
|
||||
}}
|
||||
@ -144,12 +143,12 @@ function DBCall(): JSX.Element {
|
||||
name="database_call_avg_duration"
|
||||
widget={databaseCallsAverageDurationWidget}
|
||||
headerMenuList={MENU_ITEMS}
|
||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||
onGraphClickHandler(setSelectedTimeStamp)(
|
||||
ChartEvent,
|
||||
activeElements,
|
||||
chart,
|
||||
data,
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
'database_call_avg_duration',
|
||||
);
|
||||
}}
|
||||
|
@ -151,12 +151,12 @@ function External(): JSX.Element {
|
||||
headerMenuList={MENU_ITEMS}
|
||||
name="external_call_error_percentage"
|
||||
widget={externalCallErrorWidget}
|
||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||
onGraphClickHandler(setSelectedTimeStamp)(
|
||||
ChartEvent,
|
||||
activeElements,
|
||||
chart,
|
||||
data,
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
'external_call_error_percentage',
|
||||
);
|
||||
}}
|
||||
@ -186,12 +186,12 @@ function External(): JSX.Element {
|
||||
name="external_call_duration"
|
||||
headerMenuList={MENU_ITEMS}
|
||||
widget={externalCallDurationWidget}
|
||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||
onGraphClickHandler(setSelectedTimeStamp)(
|
||||
ChartEvent,
|
||||
activeElements,
|
||||
chart,
|
||||
data,
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
'external_call_duration',
|
||||
);
|
||||
}}
|
||||
@ -222,15 +222,15 @@ function External(): JSX.Element {
|
||||
name="external_call_rps_by_address"
|
||||
widget={externalCallRPSWidget}
|
||||
headerMenuList={MENU_ITEMS}
|
||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): Promise<void> =>
|
||||
onGraphClickHandler(setSelectedTimeStamp)(
|
||||
ChartEvent,
|
||||
activeElements,
|
||||
chart,
|
||||
data,
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
'external_call_rps_by_address',
|
||||
);
|
||||
}}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@ -257,12 +257,12 @@ function External(): JSX.Element {
|
||||
name="external_call_duration_by_address"
|
||||
widget={externalCallDurationAddressWidget}
|
||||
headerMenuList={MENU_ITEMS}
|
||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||
onGraphClickHandler(setSelectedTimeStamp)(
|
||||
ChartEvent,
|
||||
activeElements,
|
||||
chart,
|
||||
data,
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
'external_call_duration_by_address',
|
||||
);
|
||||
}}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import getTopLevelOperations, {
|
||||
ServiceDataProps,
|
||||
} from 'api/metrics/getTopLevelOperations';
|
||||
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@ -15,6 +14,7 @@ import {
|
||||
resourceAttributesToTagFilterItems,
|
||||
} from 'hooks/useResourceAttribute/utils';
|
||||
import history from 'lib/history';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@ -73,20 +73,19 @@ function Application(): JSX.Element {
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const handleGraphClick = useCallback(
|
||||
(type: string): ClickHandlerType => (
|
||||
ChartEvent: ChartEvent,
|
||||
activeElements: ActiveElement[],
|
||||
chart: Chart,
|
||||
data: ChartData,
|
||||
): void => {
|
||||
(type: string): OnClickPluginOpts['onClick'] => (
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
): Promise<void> =>
|
||||
onGraphClickHandler(handleSetTimeStamp)(
|
||||
ChartEvent,
|
||||
activeElements,
|
||||
chart,
|
||||
data,
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
type,
|
||||
);
|
||||
},
|
||||
),
|
||||
[handleSetTimeStamp],
|
||||
);
|
||||
|
||||
@ -283,12 +282,6 @@ function Application(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export type ClickHandlerType = (
|
||||
ChartEvent: ChartEvent,
|
||||
activeElements: ActiveElement[],
|
||||
chart: Chart,
|
||||
data: ChartData,
|
||||
type?: string,
|
||||
) => void;
|
||||
export type ClickHandlerType = () => void;
|
||||
|
||||
export default Application;
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { ClickHandlerType } from '../../Overview';
|
||||
|
||||
export interface ApDexApplicationProps {
|
||||
handleGraphClick: (type: string) => ClickHandlerType;
|
||||
handleGraphClick: (type: string) => OnClickPluginOpts['onClick'];
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
topLevelOperationsRoute: string[];
|
||||
tagFilterItems: TagFilterItem[];
|
||||
|
@ -8,12 +8,12 @@ import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
||||
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { resourceAttributesToTagFilterItems } from 'hooks/useResourceAttribute/utils';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { ClickHandlerType } from '../Overview';
|
||||
import { Button } from '../styles';
|
||||
import { IServiceName } from '../types';
|
||||
import { handleNonInQueryRange, onViewTracePopupClick } from '../util';
|
||||
@ -99,7 +99,7 @@ interface ServiceOverviewProps {
|
||||
selectedTimeStamp: number;
|
||||
selectedTraceTags: string;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
handleGraphClick: (type: string) => ClickHandlerType;
|
||||
handleGraphClick: (type: string) => OnClickPluginOpts['onClick'];
|
||||
topLevelOperationsRoute: string[];
|
||||
topLevelOperationsIsLoading: boolean;
|
||||
}
|
||||
|
@ -3,10 +3,9 @@ import axios from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { ClickHandlerType } from '../Overview';
|
||||
|
||||
function TopLevelOperation({
|
||||
name,
|
||||
opName,
|
||||
@ -46,7 +45,7 @@ interface TopLevelOperationProps {
|
||||
topLevelOperationsIsError: boolean;
|
||||
topLevelOperationsError: unknown;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
handleGraphClick: (type: string) => ClickHandlerType;
|
||||
handleGraphClick: (type: string) => OnClickPluginOpts['onClick'];
|
||||
widget: Widgets;
|
||||
topLevelOperationsIsLoading: boolean;
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { routeConfig } from 'container/SideNav/config';
|
||||
@ -32,7 +31,8 @@ export function onViewTracePopupClick({
|
||||
}: OnViewTracePopupClickProps): VoidFunction {
|
||||
return (): void => {
|
||||
const currentTime = timestamp;
|
||||
const tPlusOne = timestamp + 60 * 1000;
|
||||
|
||||
const tPlusOne = timestamp + 60;
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.set(QueryParams.startTime, currentTime.toString());
|
||||
@ -54,37 +54,25 @@ export function onGraphClickHandler(
|
||||
setSelectedTimeStamp: (n: number) => void | Dispatch<SetStateAction<number>>,
|
||||
) {
|
||||
return async (
|
||||
event: ChartEvent,
|
||||
elements: ActiveElement[],
|
||||
chart: Chart,
|
||||
data: ChartData,
|
||||
from: string,
|
||||
xValue: number,
|
||||
yValue: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
type: string,
|
||||
): Promise<void> => {
|
||||
if (event.native) {
|
||||
const points = chart.getElementsAtEventForMode(
|
||||
event.native,
|
||||
'nearest',
|
||||
{ intersect: false },
|
||||
true,
|
||||
);
|
||||
const id = `${from}_button`;
|
||||
const buttonElement = document.getElementById(id);
|
||||
const id = `${type}_button`;
|
||||
|
||||
if (points.length !== 0) {
|
||||
const firstPoint = points[0];
|
||||
const buttonElement = document.getElementById(id);
|
||||
|
||||
if (data.labels) {
|
||||
const time = data?.labels[firstPoint.index] as Date;
|
||||
if (buttonElement) {
|
||||
buttonElement.style.display = 'block';
|
||||
buttonElement.style.left = `${firstPoint.element.x}px`;
|
||||
buttonElement.style.top = `${firstPoint.element.y}px`;
|
||||
setSelectedTimeStamp(time.getTime());
|
||||
}
|
||||
}
|
||||
} else if (buttonElement && buttonElement.style.display === 'block') {
|
||||
buttonElement.style.display = 'none';
|
||||
if (xValue) {
|
||||
if (buttonElement) {
|
||||
buttonElement.style.display = 'block';
|
||||
buttonElement.style.left = `${mouseX}px`;
|
||||
buttonElement.style.top = `${mouseY}px`;
|
||||
setSelectedTimeStamp(xValue);
|
||||
}
|
||||
} else if (buttonElement && buttonElement.style.display === 'block') {
|
||||
buttonElement.style.display = 'none';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ function DashboardGraphSlider(): JSX.Element {
|
||||
i: id,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 2,
|
||||
h: 3,
|
||||
y: 0,
|
||||
},
|
||||
...(layouts.filter((layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET) ||
|
||||
|
@ -0,0 +1,7 @@
|
||||
.dashboard-description {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2; /* Show up to 2 lines */
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
@ -5,7 +5,7 @@ import { useState } from 'react';
|
||||
import DashboardSettingsContent from '../DashboardSettings';
|
||||
import { DrawerContainer } from './styles';
|
||||
|
||||
function SettingsDrawer(): JSX.Element {
|
||||
function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
|
||||
const showDrawer = (): void => {
|
||||
@ -22,8 +22,9 @@ function SettingsDrawer(): JSX.Element {
|
||||
<SettingOutlined /> Configure
|
||||
</Button>
|
||||
<DrawerContainer
|
||||
title={drawerTitle}
|
||||
placement="right"
|
||||
width="70%"
|
||||
width="50%"
|
||||
onClose={onClose}
|
||||
open={visible}
|
||||
>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Button, Modal, Typography } from 'antd';
|
||||
import { CopyFilled, DownloadOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal } from 'antd';
|
||||
import Editor from 'components/Editor';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@ -6,7 +7,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { downloadObjectAsJson } from './util';
|
||||
import { downloadObjectAsJson } from './utils';
|
||||
|
||||
function ShareModal({
|
||||
isJSONModalVisible,
|
||||
@ -16,7 +17,6 @@ function ShareModal({
|
||||
const getParsedValue = (): string => JSON.stringify(selectedData, null, 2);
|
||||
|
||||
const [jsonValue, setJSONValue] = useState<string>(getParsedValue());
|
||||
const [isViewJSON, setIsViewJSON] = useState<boolean>(false);
|
||||
const { t } = useTranslation(['dashboard', 'common']);
|
||||
const [state, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
@ -39,44 +39,41 @@ function ShareModal({
|
||||
}
|
||||
}, [state.error, state.value, t, notifications]);
|
||||
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
const GetFooterComponent = useMemo(() => {
|
||||
if (!isViewJSON) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
setIsViewJSON(true);
|
||||
}}
|
||||
>
|
||||
{t('view_json')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(): void => {
|
||||
downloadObjectAsJson(selectedData, selectedData.title);
|
||||
}}
|
||||
>
|
||||
{t('download_json')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button onClick={(): void => setCopy(jsonValue)} type="primary">
|
||||
{t('copy_to_clipboard')}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
style={{
|
||||
marginTop: '16px',
|
||||
}}
|
||||
onClick={(): void => setCopy(jsonValue)}
|
||||
type="primary"
|
||||
size="small"
|
||||
>
|
||||
<CopyFilled /> {t('copy_to_clipboard')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={(): void => {
|
||||
downloadObjectAsJson(selectedData, selectedData.title);
|
||||
}}
|
||||
>
|
||||
<DownloadOutlined /> {t('download_json')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}, [isViewJSON, jsonValue, selectedData, setCopy, t]);
|
||||
}, [jsonValue, selectedData, setCopy, t]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isJSONModalVisible}
|
||||
onCancel={(): void => {
|
||||
onToggleHandler();
|
||||
setIsViewJSON(false);
|
||||
}}
|
||||
width="70vw"
|
||||
width="80vw"
|
||||
centered
|
||||
title={t('share', {
|
||||
ns: 'common',
|
||||
@ -86,11 +83,11 @@ function ShareModal({
|
||||
destroyOnClose
|
||||
footer={GetFooterComponent}
|
||||
>
|
||||
{!isViewJSON ? (
|
||||
<Typography>{t('export_dashboard')}</Typography>
|
||||
) : (
|
||||
<Editor onChange={(value): void => setJSONValue(value)} value={jsonValue} />
|
||||
)}
|
||||
<Editor
|
||||
height="70vh"
|
||||
onChange={(value): void => setJSONValue(value)}
|
||||
value={jsonValue}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import './Description.styles.scss';
|
||||
|
||||
import { LockFilled, ShareAltOutlined, UnlockFilled } from '@ant-design/icons';
|
||||
import { Button, Card, Col, Row, Space, Tag, Tooltip, Typography } from 'antd';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
@ -6,6 +8,7 @@ import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
@ -20,10 +23,11 @@ function DashboardDescription(): JSX.Element {
|
||||
handleDashboardLockToggle,
|
||||
} = useDashboard();
|
||||
|
||||
const selectedData = selectedDashboard?.data;
|
||||
const { title, tags, description } = selectedData || {};
|
||||
const selectedData = selectedDashboard?.data || ({} as DashboardData);
|
||||
|
||||
const [isJSONModalVisible, isIsJSONModalVisible] = useState<boolean>(false);
|
||||
const { title = '', tags, description } = selectedData || {};
|
||||
|
||||
const [openDashboardJSON, setOpenDashboardJSON] = useState<boolean>(false);
|
||||
|
||||
const { t } = useTranslation('common');
|
||||
const { user, role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
@ -36,7 +40,7 @@ function DashboardDescription(): JSX.Element {
|
||||
}
|
||||
|
||||
const onToggleHandler = (): void => {
|
||||
isIsJSONModalVisible((state) => !state);
|
||||
setOpenDashboardJSON((state) => !state);
|
||||
};
|
||||
|
||||
const handleLockDashboardToggle = (): void => {
|
||||
@ -45,8 +49,8 @@ function DashboardDescription(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Row>
|
||||
<Col flex={1}>
|
||||
<Row gutter={16}>
|
||||
<Col flex={1} span={12}>
|
||||
<Typography.Title level={4} style={{ padding: 0, margin: 0 }}>
|
||||
{isDashboardLocked && (
|
||||
<Tooltip title="Dashboard Locked" placement="top">
|
||||
@ -55,27 +59,36 @@ function DashboardDescription(): JSX.Element {
|
||||
)}
|
||||
{title}
|
||||
</Typography.Title>
|
||||
<Typography>{description}</Typography>
|
||||
{description && (
|
||||
<Typography className="dashboard-description">{description}</Typography>
|
||||
)}
|
||||
|
||||
<div style={{ margin: '0.5rem 0' }}>
|
||||
{tags?.map((tag) => (
|
||||
<Tag key={tag}>{tag}</Tag>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DashboardVariableSelection />
|
||||
{tags && (
|
||||
<div style={{ margin: '0.5rem 0' }}>
|
||||
{tags?.map((tag) => (
|
||||
<Tag key={tag}>{tag}</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col>
|
||||
<Col span={8}>
|
||||
<Row justify="end">
|
||||
<DashboardVariableSelection />
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={4} style={{ textAlign: 'right' }}>
|
||||
{selectedData && (
|
||||
<ShareModal
|
||||
isJSONModalVisible={isJSONModalVisible}
|
||||
isJSONModalVisible={openDashboardJSON}
|
||||
onToggleHandler={onToggleHandler}
|
||||
selectedData={selectedData}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Space direction="vertical">
|
||||
{!isDashboardLocked && editDashboard && <SettingsDrawer />}
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
<SettingsDrawer drawerTitle={title} />
|
||||
)}
|
||||
<Button
|
||||
style={{ width: '100%' }}
|
||||
type="dashed"
|
||||
|
@ -14,7 +14,11 @@ export const Button = styled(ButtonComponent)`
|
||||
|
||||
export const DrawerContainer = styled(Drawer)`
|
||||
.ant-drawer-header {
|
||||
padding: 0;
|
||||
padding: 16px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding-top: 0;
|
||||
}
|
||||
`;
|
||||
|
@ -104,7 +104,13 @@ function AddTags({ tags, setTags }: AddTagsProps): JSX.Element {
|
||||
|
||||
{!inputVisible && (
|
||||
<NewTagContainer icon={<PlusOutlined />} onClick={showInput}>
|
||||
<Typography>New Tag</Typography>
|
||||
<Typography
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
New Tag
|
||||
</Typography>
|
||||
</NewTagContainer>
|
||||
)}
|
||||
</TagsContainer>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { SaveOutlined } from '@ant-design/icons';
|
||||
import { Col, Divider, Input, Space, Typography } from 'antd';
|
||||
import { Col, Input, Space, Typography } from 'antd';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
@ -71,6 +71,7 @@ function GeneralDashboardSettings(): JSX.Element {
|
||||
<div>
|
||||
<Typography style={{ marginBottom: '0.5rem' }}>Description</Typography>
|
||||
<Input.TextArea
|
||||
rows={5}
|
||||
value={updatedDescription}
|
||||
onChange={(e): void => setUpdatedDescription(e.target.value)}
|
||||
/>
|
||||
@ -80,8 +81,10 @@ function GeneralDashboardSettings(): JSX.Element {
|
||||
<AddTags tags={updatedTags} setTags={setUpdatedTags} />
|
||||
</div>
|
||||
<div>
|
||||
<Divider />
|
||||
<Button
|
||||
style={{
|
||||
margin: '16px 0',
|
||||
}}
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
icon={<SaveOutlined />}
|
||||
|
@ -3,7 +3,7 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { map, sortBy } from 'lodash-es';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useState } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
@ -114,4 +114,4 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardVariableSelection;
|
||||
export default memo(DashboardVariableSelection);
|
||||
|
@ -1,99 +0,0 @@
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Card, Tooltip, Typography } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
errorTooltipPosition,
|
||||
tooltipStyles,
|
||||
WARNING_MESSAGE,
|
||||
} from 'container/GridCardLayout/WidgetHeader/config';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { WidgetGraphProps } from 'container/NewWidget/types';
|
||||
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { NotFoundContainer } from './styles';
|
||||
|
||||
function WidgetGraph({
|
||||
selectedGraph,
|
||||
yAxisUnit,
|
||||
selectedTime,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const { widgets = [] } = selectedDashboard?.data || {};
|
||||
const { search } = useLocation();
|
||||
|
||||
const params = new URLSearchParams(search);
|
||||
const widgetId = params.get('widgetId');
|
||||
|
||||
const selectedWidget = widgets.find((e) => e.id === widgetId);
|
||||
|
||||
const getWidgetQueryRange = useGetWidgetQueryRange({
|
||||
graphType: selectedGraph,
|
||||
selectedTime: selectedTime.enum,
|
||||
});
|
||||
|
||||
if (selectedWidget === undefined) {
|
||||
return <Card>Invalid widget</Card>;
|
||||
}
|
||||
|
||||
const { title, opacity, isStacked, query } = selectedWidget;
|
||||
|
||||
if (getWidgetQueryRange.error) {
|
||||
return (
|
||||
<NotFoundContainer>
|
||||
<Typography>{getWidgetQueryRange.error.message}</Typography>
|
||||
</NotFoundContainer>
|
||||
);
|
||||
}
|
||||
if (getWidgetQueryRange.isLoading) {
|
||||
return <Spinner size="large" tip="Loading..." />;
|
||||
}
|
||||
if (getWidgetQueryRange.data?.payload.data.result.length === 0) {
|
||||
return (
|
||||
<NotFoundContainer>
|
||||
<Typography>No Data</Typography>
|
||||
</NotFoundContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const chartDataSet = getChartData({
|
||||
queryData: [
|
||||
{ queryData: getWidgetQueryRange.data?.payload.data.result ?? [] },
|
||||
],
|
||||
createDataset: undefined,
|
||||
isWarningLimit: selectedWidget.panelTypes === PANEL_TYPES.TIME_SERIES,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{chartDataSet.isWarning && (
|
||||
<Tooltip title={WARNING_MESSAGE} placement={errorTooltipPosition}>
|
||||
<WarningOutlined style={tooltipStyles} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<GridPanelSwitch
|
||||
title={title}
|
||||
isStacked={isStacked}
|
||||
opacity={opacity}
|
||||
data={chartDataSet.data}
|
||||
panelType={selectedGraph}
|
||||
name={widgetId || 'legend_widget'}
|
||||
yAxisUnit={yAxisUnit}
|
||||
panelData={
|
||||
getWidgetQueryRange.data?.payload.data.newResult.data.result || []
|
||||
}
|
||||
query={stagedQuery || query}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default WidgetGraph;
|
@ -0,0 +1,62 @@
|
||||
import { Card, Typography } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { WidgetGraphProps } from 'container/NewWidget/types';
|
||||
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
|
||||
import { NotFoundContainer } from './styles';
|
||||
import WidgetGraph from './WidgetGraphs';
|
||||
|
||||
function WidgetGraphContainer({
|
||||
selectedGraph,
|
||||
yAxisUnit,
|
||||
selectedTime,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const { widgets = [] } = selectedDashboard?.data || {};
|
||||
|
||||
const params = useUrlQuery();
|
||||
|
||||
const widgetId = params.get('widgetId');
|
||||
|
||||
const selectedWidget = widgets.find((e) => e.id === widgetId);
|
||||
|
||||
const getWidgetQueryRange = useGetWidgetQueryRange({
|
||||
graphType: selectedGraph,
|
||||
selectedTime: selectedTime.enum,
|
||||
});
|
||||
|
||||
if (selectedWidget === undefined) {
|
||||
return <Card>Invalid widget</Card>;
|
||||
}
|
||||
|
||||
if (getWidgetQueryRange.error) {
|
||||
return (
|
||||
<NotFoundContainer>
|
||||
<Typography>{getWidgetQueryRange.error.message}</Typography>
|
||||
</NotFoundContainer>
|
||||
);
|
||||
}
|
||||
if (getWidgetQueryRange.isLoading) {
|
||||
return <Spinner size="large" tip="Loading..." />;
|
||||
}
|
||||
if (getWidgetQueryRange.data?.payload.data.result.length === 0) {
|
||||
return (
|
||||
<NotFoundContainer>
|
||||
<Typography>No Data</Typography>
|
||||
</NotFoundContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WidgetGraph
|
||||
yAxisUnit={yAxisUnit || ''}
|
||||
getWidgetQueryRange={getWidgetQueryRange}
|
||||
selectedWidget={selectedWidget}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default WidgetGraphContainer;
|
@ -0,0 +1,95 @@
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartData';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getChartData';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
function WidgetGraph({
|
||||
getWidgetQueryRange,
|
||||
selectedWidget,
|
||||
yAxisUnit,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const chartData = getUPlotChartData(getWidgetQueryRange?.data?.payload);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const params = useUrlQuery();
|
||||
|
||||
const widgetId = params.get('widgetId');
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
id: widgetId || 'legend_widget',
|
||||
yAxisUnit,
|
||||
apiResponse: getWidgetQueryRange?.data?.payload,
|
||||
dimensions: containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
}),
|
||||
[
|
||||
widgetId,
|
||||
yAxisUnit,
|
||||
getWidgetQueryRange?.data?.payload,
|
||||
containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={graphRef} style={{ height: '100%' }}>
|
||||
<GridPanelSwitch
|
||||
data={chartData}
|
||||
options={options}
|
||||
panelType={selectedWidget.panelTypes}
|
||||
name={widgetId || 'legend_widget'}
|
||||
yAxisUnit={yAxisUnit}
|
||||
panelData={
|
||||
getWidgetQueryRange.data?.payload.data.newResult.data.result || []
|
||||
}
|
||||
query={stagedQuery || selectedWidget.query}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface WidgetGraphProps {
|
||||
yAxisUnit: string;
|
||||
selectedWidget: Widgets;
|
||||
getWidgetQueryRange: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
}
|
||||
|
||||
export default WidgetGraph;
|
@ -2,14 +2,14 @@ import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { WidgetGraphProps } from '../../types';
|
||||
import PlotTag from './PlotTag';
|
||||
import { AlertIconContainer, Container } from './styles';
|
||||
import WidgetGraphComponent from './WidgetGraph';
|
||||
import WidgetGraphComponent from './WidgetGraphContainer';
|
||||
|
||||
function WidgetGraph({
|
||||
selectedGraph,
|
||||
@ -19,11 +19,10 @@ function WidgetGraph({
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const { search } = useLocation();
|
||||
|
||||
const { widgets = [] } = selectedDashboard?.data || {};
|
||||
|
||||
const params = new URLSearchParams(search);
|
||||
const params = useUrlQuery();
|
||||
|
||||
const widgetId = params.get('widgetId');
|
||||
|
||||
const selectedWidget = widgets.find((e) => e.id === widgetId);
|
||||
|
@ -1,7 +1,9 @@
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { useMemo } from 'react';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartData';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getChartData';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
@ -13,31 +15,45 @@ function TimeSeriesView({
|
||||
isError,
|
||||
yAxisUnit,
|
||||
}: TimeSeriesViewProps): JSX.Element {
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: data?.payload?.data?.result || [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[data?.payload?.data?.result],
|
||||
);
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const chartData = useMemo(() => getUPlotChartData(data?.payload), [
|
||||
data?.payload,
|
||||
]);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const width = graphRef.current?.clientWidth
|
||||
? graphRef.current.clientWidth
|
||||
: 700;
|
||||
|
||||
const height = graphRef.current?.clientWidth
|
||||
? graphRef.current.clientHeight
|
||||
: 300;
|
||||
|
||||
const chartOptions = getUPlotChartOptions({
|
||||
yAxisUnit: yAxisUnit || '',
|
||||
apiResponse: data?.payload,
|
||||
dimensions: {
|
||||
width,
|
||||
height,
|
||||
},
|
||||
isDarkMode,
|
||||
});
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{isLoading && <Spinner height="50vh" size="small" tip="Loading..." />}
|
||||
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
|
||||
{!isLoading && !isError && (
|
||||
<Graph
|
||||
animate={false}
|
||||
data={chartData.data}
|
||||
yAxisUnit={yAxisUnit}
|
||||
name="tracesExplorerGraph"
|
||||
type="line"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="graph-container"
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
ref={graphRef}
|
||||
>
|
||||
{!isLoading && !isError && chartData && chartOptions && (
|
||||
<Uplot data={chartData} options={chartOptions} />
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
const GlobalStyles = createGlobalStyle`
|
||||
#root,
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
export default GlobalStyles;
|
@ -15,7 +15,7 @@ export const addEmptyWidgetInDashboardJSONWithQuery = (
|
||||
i: 'empty',
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 2,
|
||||
h: 3,
|
||||
y: 0,
|
||||
},
|
||||
...(dashboard?.data?.layout || []),
|
||||
|
39
frontend/src/hooks/useDimensions.ts
Normal file
39
frontend/src/hooks/useDimensions.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export type Dimensions = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export function useResizeObserver<T extends HTMLElement>(
|
||||
ref: React.RefObject<T>,
|
||||
debounceTime = 300,
|
||||
): Dimensions {
|
||||
const [size, setSize] = useState<Dimensions>({
|
||||
width: ref.current?.clientWidth || 0,
|
||||
height: ref.current?.clientHeight || 0,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const handleResize = debounce((entries: ResizeObserverEntry[]) => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
const { width, height } = entry.contentRect;
|
||||
setSize({ width, height });
|
||||
}
|
||||
}, debounceTime);
|
||||
|
||||
const ro = new ResizeObserver(handleResize);
|
||||
ro.observe(ref.current);
|
||||
|
||||
return (): void => {
|
||||
ro.disconnect();
|
||||
};
|
||||
}
|
||||
}, [ref, debounceTime]);
|
||||
|
||||
return size;
|
||||
}
|
38
frontend/src/hooks/useIntersectionObserver.ts
Normal file
38
frontend/src/hooks/useIntersectionObserver.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { RefObject, useEffect, useState } from 'react';
|
||||
|
||||
export function useIntersectionObserver<T extends HTMLElement>(
|
||||
ref: RefObject<T>,
|
||||
options?: IntersectionObserverInit,
|
||||
isObserverOnce?: boolean,
|
||||
): boolean {
|
||||
const [isIntersecting, setIntersecting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const currentReference = ref?.current;
|
||||
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIntersecting(true);
|
||||
|
||||
if (isObserverOnce) {
|
||||
// Optionally: Once it becomes visible, we don't need to observe it anymore
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
} else {
|
||||
setIntersecting(false);
|
||||
}
|
||||
}, options);
|
||||
|
||||
if (currentReference) {
|
||||
observer.observe(currentReference);
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
if (currentReference) {
|
||||
observer.unobserve(currentReference);
|
||||
}
|
||||
};
|
||||
}, [ref, options, isObserverOnce]);
|
||||
|
||||
return isIntersecting;
|
||||
}
|
@ -56,6 +56,11 @@
|
||||
href="https://fonts.googleapis.com/css?family=Fira+Code"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/uplot@1.6.26/dist/uPlot.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import './ReactI18';
|
||||
import 'styles.scss';
|
||||
|
||||
import AppRoutes from 'AppRoutes';
|
||||
import GlobalStyles from 'globalStyles';
|
||||
import { ThemeProvider } from 'hooks/useDarkMode';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
@ -28,7 +28,6 @@ if (container) {
|
||||
<ThemeProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<GlobalStyles />
|
||||
<AppRoutes />
|
||||
</Provider>
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
import { Span } from 'types/api/trace/getTraceItem';
|
||||
|
||||
import { themeColors } from '../constants/theme';
|
||||
@ -13,6 +14,33 @@ const getRandomColor = (): string => {
|
||||
return colors[index];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
|
||||
export function hexToRgba(hex: string, alpha: number = 1): string {
|
||||
// Create a new local variable to work with
|
||||
let hexColor = hex;
|
||||
|
||||
// Ensure the hex string has a "#" at the start
|
||||
if (hexColor.charAt(0) === '#') {
|
||||
hexColor = hexColor.slice(1);
|
||||
}
|
||||
|
||||
// Check if it's a shorthand hex code (e.g., #FFF)
|
||||
if (hexColor.length === 3) {
|
||||
const r = hexColor.charAt(0);
|
||||
const g = hexColor.charAt(1);
|
||||
const b = hexColor.charAt(2);
|
||||
hexColor = r + r + g + g + b + b;
|
||||
}
|
||||
|
||||
// Parse the r, g, b values
|
||||
const bigint = parseInt(hexColor, 16);
|
||||
const r = (bigint >> 16) & 255;
|
||||
const g = (bigint >> 8) & 255;
|
||||
const b = bigint & 255;
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
export const SIGNOZ_UI_COLOR_HEX = 'signoz_ui_color_hex';
|
||||
|
||||
export const spanServiceNameToColorMapping = (
|
||||
|
164
frontend/src/lib/uPlotLib/getUplotChartData.ts
Normal file
164
frontend/src/lib/uPlotLib/getUplotChartData.ts
Normal file
@ -0,0 +1,164 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './uPlotLib.styles.scss';
|
||||
|
||||
import { FullViewProps } from 'container/GridCardLayout/GridCard/FullView/types';
|
||||
import { Dimensions } from 'hooks/useDimensions';
|
||||
import _noop from 'lodash-es/noop';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import onClickPlugin, { OnClickPluginOpts } from './plugins/onClickPlugin';
|
||||
import tooltipPlugin from './plugins/tooltipPlugin';
|
||||
import getAxes from './utils/getAxes';
|
||||
import getSeries from './utils/getSeriesData';
|
||||
|
||||
interface GetUPlotChartOptions {
|
||||
id?: string;
|
||||
apiResponse?: MetricRangePayloadProps;
|
||||
dimensions: Dimensions;
|
||||
isDarkMode: boolean;
|
||||
onDragSelect?: (startTime: number, endTime: number) => void;
|
||||
yAxisUnit?: string;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
graphsVisibilityStates?: boolean[];
|
||||
setGraphsVisibilityStates?: FullViewProps['setGraphsVisibilityStates'];
|
||||
thresholdValue?: number;
|
||||
thresholdText?: string;
|
||||
}
|
||||
|
||||
export const getUPlotChartOptions = ({
|
||||
id,
|
||||
dimensions,
|
||||
isDarkMode,
|
||||
apiResponse,
|
||||
onDragSelect,
|
||||
yAxisUnit,
|
||||
onClickHandler = _noop,
|
||||
graphsVisibilityStates,
|
||||
setGraphsVisibilityStates,
|
||||
thresholdValue,
|
||||
thresholdText,
|
||||
}: GetUPlotChartOptions): uPlot.Options => ({
|
||||
id,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height - 45,
|
||||
// tzDate: (ts) => uPlot.tzDate(new Date(ts * 1e3), ''), // Pass timezone for 2nd param
|
||||
legend: {
|
||||
show: true,
|
||||
live: false,
|
||||
},
|
||||
focus: {
|
||||
alpha: 0.3,
|
||||
},
|
||||
cursor: {
|
||||
focus: {
|
||||
prox: 1e6,
|
||||
bias: 1,
|
||||
},
|
||||
points: {
|
||||
size: (u, seriesIdx): number => u.series[seriesIdx].points.size * 2.5,
|
||||
width: (u, seriesIdx, size): number => size / 4,
|
||||
stroke: (u, seriesIdx): string =>
|
||||
`${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`,
|
||||
fill: (): string => '#fff',
|
||||
},
|
||||
},
|
||||
padding: [16, 16, 16, 16],
|
||||
scales: {
|
||||
x: {
|
||||
time: true,
|
||||
auto: true, // Automatically adjust scale range
|
||||
},
|
||||
y: {
|
||||
auto: true,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
tooltipPlugin(apiResponse, yAxisUnit),
|
||||
onClickPlugin({
|
||||
onClick: onClickHandler,
|
||||
}),
|
||||
],
|
||||
hooks: {
|
||||
draw: [
|
||||
(u): void => {
|
||||
if (thresholdValue) {
|
||||
const { ctx } = u;
|
||||
ctx.save();
|
||||
|
||||
const yPos = u.valToPos(thresholdValue, 'y', true);
|
||||
|
||||
ctx.strokeStyle = 'red';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([10, 5]);
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
const plotLeft = u.bbox.left; // left edge of the plot area
|
||||
const plotRight = plotLeft + u.bbox.width; // right edge of the plot area
|
||||
|
||||
ctx.moveTo(plotLeft, yPos);
|
||||
ctx.lineTo(plotRight, yPos);
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
// Text configuration
|
||||
if (thresholdText) {
|
||||
const text = thresholdText;
|
||||
const textX = plotRight - ctx.measureText(text).width - 20;
|
||||
const textY = yPos - 15;
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText(text, textX, textY);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
},
|
||||
],
|
||||
setSelect: [
|
||||
(self): void => {
|
||||
const selection = self.select;
|
||||
if (selection) {
|
||||
const startTime = self.posToVal(selection.left, 'x');
|
||||
const endTime = self.posToVal(selection.left + selection.width, 'x');
|
||||
|
||||
const diff = endTime - startTime;
|
||||
|
||||
if (typeof onDragSelect === 'function' && diff > 0) {
|
||||
onDragSelect(startTime * 1000, endTime * 1000);
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
ready: [
|
||||
(self): void => {
|
||||
const legend = self.root.querySelector('.u-legend');
|
||||
if (legend) {
|
||||
const seriesEls = legend.querySelectorAll('.u-label');
|
||||
const seriesArray = Array.from(seriesEls);
|
||||
seriesArray.forEach((seriesEl, index) => {
|
||||
seriesEl.addEventListener('click', () => {
|
||||
if (graphsVisibilityStates) {
|
||||
setGraphsVisibilityStates?.((prev) => {
|
||||
const newGraphVisibilityStates = [...prev];
|
||||
newGraphVisibilityStates[index + 1] = !newGraphVisibilityStates[
|
||||
index + 1
|
||||
];
|
||||
return newGraphVisibilityStates;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
series: getSeries(
|
||||
apiResponse,
|
||||
apiResponse?.data.result,
|
||||
graphsVisibilityStates,
|
||||
),
|
||||
axes: getAxes(isDarkMode, yAxisUnit),
|
||||
});
|
114
frontend/src/lib/uPlotLib/placement.ts
Normal file
114
frontend/src/lib/uPlotLib/placement.ts
Normal file
@ -0,0 +1,114 @@
|
||||
/* eslint-disable radix */
|
||||
/* eslint-disable guard-for-in */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable vars-on-top */
|
||||
/* eslint-disable func-style */
|
||||
/* eslint-disable no-void */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable func-names */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable no-sequences */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
|
||||
// https://tobyzerner.github.io/placement.js/dist/index.js
|
||||
|
||||
export const placement = (function () {
|
||||
const e = {
|
||||
size: ['height', 'width'],
|
||||
clientSize: ['clientHeight', 'clientWidth'],
|
||||
offsetSize: ['offsetHeight', 'offsetWidth'],
|
||||
maxSize: ['maxHeight', 'maxWidth'],
|
||||
before: ['top', 'left'],
|
||||
marginBefore: ['marginTop', 'marginLeft'],
|
||||
after: ['bottom', 'right'],
|
||||
marginAfter: ['marginBottom', 'marginRight'],
|
||||
scrollOffset: ['pageYOffset', 'pageXOffset'],
|
||||
};
|
||||
function t(e) {
|
||||
return { top: e.top, bottom: e.bottom, left: e.left, right: e.right };
|
||||
}
|
||||
return function (o, r, f, a, i) {
|
||||
void 0 === f && (f = 'bottom'),
|
||||
void 0 === a && (a = 'center'),
|
||||
void 0 === i && (i = {}),
|
||||
(r instanceof Element || r instanceof Range) &&
|
||||
(r = t(r.getBoundingClientRect()));
|
||||
const n = {
|
||||
top: r.bottom,
|
||||
bottom: r.top,
|
||||
left: r.right,
|
||||
right: r.left,
|
||||
...r,
|
||||
};
|
||||
const s = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: window.innerHeight,
|
||||
right: window.innerWidth,
|
||||
};
|
||||
i.bound &&
|
||||
((i.bound instanceof Element || i.bound instanceof Range) &&
|
||||
(i.bound = t(i.bound.getBoundingClientRect())),
|
||||
Object.assign(s, i.bound));
|
||||
const l = getComputedStyle(o);
|
||||
const m = {};
|
||||
const b = {};
|
||||
for (const g in e)
|
||||
(m[g] = e[g][f === 'top' || f === 'bottom' ? 0 : 1]),
|
||||
(b[g] = e[g][f === 'top' || f === 'bottom' ? 1 : 0]);
|
||||
(o.style.position = 'absolute'),
|
||||
(o.style.maxWidth = ''),
|
||||
(o.style.maxHeight = '');
|
||||
const d = parseInt(l[b.marginBefore]);
|
||||
const c = parseInt(l[b.marginAfter]);
|
||||
const u = d + c;
|
||||
const p = s[b.after] - s[b.before] - u;
|
||||
const h = parseInt(l[b.maxSize]);
|
||||
(!h || p < h) && (o.style[b.maxSize] = `${p}px`);
|
||||
const x = parseInt(l[m.marginBefore]) + parseInt(l[m.marginAfter]);
|
||||
const y = n[m.before] - s[m.before] - x;
|
||||
const z = s[m.after] - n[m.after] - x;
|
||||
((f === m.before && o[m.offsetSize] > y) ||
|
||||
(f === m.after && o[m.offsetSize] > z)) &&
|
||||
(f = y > z ? m.before : m.after);
|
||||
const S = f === m.before ? y : z;
|
||||
const v = parseInt(l[m.maxSize]);
|
||||
(!v || S < v) && (o.style[m.maxSize] = `${S}px`);
|
||||
const w = window[m.scrollOffset];
|
||||
const O = function (e) {
|
||||
return Math.max(s[m.before], Math.min(e, s[m.after] - o[m.offsetSize] - x));
|
||||
};
|
||||
f === m.before
|
||||
? ((o.style[m.before] = `${w + O(n[m.before] - o[m.offsetSize] - x)}px`),
|
||||
(o.style[m.after] = 'auto'))
|
||||
: ((o.style[m.before] = `${w + O(n[m.after])}px`),
|
||||
(o.style[m.after] = 'auto'));
|
||||
const B = window[b.scrollOffset];
|
||||
const I = function (e) {
|
||||
return Math.max(s[b.before], Math.min(e, s[b.after] - o[b.offsetSize] - u));
|
||||
};
|
||||
switch (a) {
|
||||
case 'start':
|
||||
(o.style[b.before] = `${B + I(n[b.before] - d)}px`),
|
||||
(o.style[b.after] = 'auto');
|
||||
break;
|
||||
case 'end':
|
||||
(o.style[b.before] = 'auto'),
|
||||
(o.style[b.after] = `${
|
||||
B + I(document.documentElement[b.clientSize] - n[b.after] - c)
|
||||
}px`);
|
||||
break;
|
||||
default:
|
||||
var H = n[b.after] - n[b.before];
|
||||
(o.style[b.before] = `${
|
||||
B + I(n[b.before] + H / 2 - o[b.offsetSize] / 2 - d)
|
||||
}px`),
|
||||
(o.style[b.after] = 'auto');
|
||||
}
|
||||
(o.dataset.side = f), (o.dataset.align = a);
|
||||
};
|
||||
})();
|
39
frontend/src/lib/uPlotLib/plugins/onClickPlugin.ts
Normal file
39
frontend/src/lib/uPlotLib/plugins/onClickPlugin.ts
Normal file
@ -0,0 +1,39 @@
|
||||
export interface OnClickPluginOpts {
|
||||
onClick: (
|
||||
xValue: number,
|
||||
yValue: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
) => void;
|
||||
}
|
||||
|
||||
function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin {
|
||||
let handleClick: (event: MouseEvent) => void;
|
||||
|
||||
const hooks: uPlot.Plugin['hooks'] = {
|
||||
init: (u: uPlot) => {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
handleClick = function (event: MouseEvent) {
|
||||
const mouseX = event.offsetX + 40;
|
||||
const mouseY = event.offsetY + 40;
|
||||
|
||||
// Convert pixel positions to data values
|
||||
const xValue = u.posToVal(mouseX, 'x');
|
||||
const yValue = u.posToVal(mouseY, 'y');
|
||||
|
||||
opts.onClick(xValue, yValue, mouseX, mouseY);
|
||||
};
|
||||
|
||||
u.over.addEventListener('click', handleClick);
|
||||
},
|
||||
destroy: (u: uPlot) => {
|
||||
u.over.removeEventListener('click', handleClick);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
hooks,
|
||||
};
|
||||
}
|
||||
|
||||
export default onClickPlugin;
|
145
frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts
Normal file
145
frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
import dayjs from 'dayjs';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { colors } from '../../getRandomColor';
|
||||
import { placement } from '../placement';
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
const createDivsFromArray = (
|
||||
seriesList: any[],
|
||||
data: any[],
|
||||
idx: number,
|
||||
yAxisUnit?: string,
|
||||
series?: uPlot.Options['series'],
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): HTMLElement => {
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('tooltip-container');
|
||||
|
||||
if (Array.isArray(series) && series.length > 0) {
|
||||
series.forEach((item, index) => {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('tooltip-content-row');
|
||||
|
||||
if (index === 0) {
|
||||
const formattedDate = dayjs(data[0][idx] * 1000).format(
|
||||
'MMM DD YYYY HH:mm:ss',
|
||||
);
|
||||
|
||||
div.textContent = formattedDate;
|
||||
div.classList.add('tooltip-content-header');
|
||||
} else if (item.show && data[index][idx]) {
|
||||
div.classList.add('tooltip-content');
|
||||
const color = colors[(index - 1) % colors.length];
|
||||
|
||||
const squareBox = document.createElement('div');
|
||||
squareBox.classList.add('pointSquare');
|
||||
|
||||
squareBox.style.borderColor = color;
|
||||
|
||||
const text = document.createElement('div');
|
||||
text.classList.add('tooltip-data-point');
|
||||
|
||||
const { metric = {}, queryName = '', legend = '' } =
|
||||
seriesList[index - 1] || {};
|
||||
|
||||
const label = getLabelName(
|
||||
metric,
|
||||
queryName || '', // query
|
||||
legend || '',
|
||||
);
|
||||
|
||||
const tooltipValue = getToolTipValue(data[index][idx], yAxisUnit);
|
||||
|
||||
text.textContent = `${label} : ${tooltipValue}`;
|
||||
text.style.color = color;
|
||||
|
||||
div.appendChild(squareBox);
|
||||
div.appendChild(text);
|
||||
}
|
||||
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
const tooltipPlugin = (
|
||||
apiResponse: MetricRangePayloadProps | undefined,
|
||||
yAxisUnit?: string,
|
||||
): any => {
|
||||
let over: HTMLElement;
|
||||
let bound: HTMLElement;
|
||||
let bLeft: any;
|
||||
let bTop: any;
|
||||
|
||||
const syncBounds = (): void => {
|
||||
const bbox = over.getBoundingClientRect();
|
||||
bLeft = bbox.left;
|
||||
bTop = bbox.top;
|
||||
};
|
||||
|
||||
let overlay = document.getElementById('overlay');
|
||||
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.id = 'overlay';
|
||||
overlay.style.display = 'none';
|
||||
overlay.style.position = 'absolute';
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
const apiResult = apiResponse?.data?.result || [];
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
init: (u: any): void => {
|
||||
over = u?.over;
|
||||
bound = over;
|
||||
over.onmouseenter = (): void => {
|
||||
if (overlay) {
|
||||
overlay.style.display = 'block';
|
||||
}
|
||||
};
|
||||
over.onmouseleave = (): void => {
|
||||
if (overlay) {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
};
|
||||
},
|
||||
setSize: (): void => {
|
||||
syncBounds();
|
||||
},
|
||||
setCursor: (u: {
|
||||
cursor: { left: any; top: any; idx: any };
|
||||
data: any[];
|
||||
series: uPlot.Options['series'];
|
||||
}): void => {
|
||||
if (overlay) {
|
||||
overlay.textContent = '';
|
||||
const { left, top, idx } = u.cursor;
|
||||
|
||||
if (idx) {
|
||||
const anchor = { left: left + bLeft, top: top + bTop };
|
||||
const content = createDivsFromArray(
|
||||
apiResult,
|
||||
u.data,
|
||||
idx,
|
||||
yAxisUnit,
|
||||
u.series,
|
||||
);
|
||||
overlay.appendChild(content);
|
||||
placement(overlay, anchor, 'right', 'start', { bound });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default tooltipPlugin;
|
29
frontend/src/lib/uPlotLib/uPlotLib.styles.scss
Normal file
29
frontend/src/lib/uPlotLib/uPlotLib.styles.scss
Normal file
@ -0,0 +1,29 @@
|
||||
.pointSquare {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: transparent;
|
||||
border: 2px solid white;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.tooltip-content-header {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tooltip-data-point {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.pointSquare,
|
||||
.tooltip-data-point {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
67
frontend/src/lib/uPlotLib/utils/getAxes.ts
Normal file
67
frontend/src/lib/uPlotLib/utils/getAxes.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
|
||||
import getGridColor from './getGridColor';
|
||||
|
||||
const getAxes = (isDarkMode: boolean, yAxisUnit?: string): any => [
|
||||
{
|
||||
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
||||
grid: {
|
||||
stroke: getGridColor(isDarkMode), // Color of the grid lines
|
||||
dash: [10, 10], // Dash pattern for grid lines,
|
||||
width: 0.5, // Width of the grid lines,
|
||||
show: true,
|
||||
},
|
||||
ticks: {
|
||||
// stroke: isDarkMode ? 'white' : 'black', // Color of the tick lines
|
||||
width: 0.3, // Width of the tick lines,
|
||||
show: true,
|
||||
},
|
||||
gap: 5,
|
||||
},
|
||||
{
|
||||
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
||||
grid: {
|
||||
stroke: getGridColor(isDarkMode), // Color of the grid lines
|
||||
dash: [10, 10], // Dash pattern for grid lines,
|
||||
width: 0.3, // Width of the grid lines
|
||||
},
|
||||
ticks: {
|
||||
// stroke: isDarkMode ? 'white' : 'black', // Color of the tick lines
|
||||
width: 0.3, // Width of the tick lines
|
||||
show: true,
|
||||
},
|
||||
values: (_, t): string[] =>
|
||||
t.map((v) => {
|
||||
const value = getToolTipValue(v.toString(), yAxisUnit);
|
||||
|
||||
return `${value}`;
|
||||
}),
|
||||
gap: 5,
|
||||
size: (self, values, axisIdx, cycleNum): number => {
|
||||
const axis = self.axes[axisIdx];
|
||||
|
||||
// bail out, force convergence
|
||||
if (cycleNum > 1) return axis._size;
|
||||
|
||||
let axisSize = axis.ticks.size + axis.gap;
|
||||
|
||||
// find longest value
|
||||
const longestVal = (values ?? []).reduce(
|
||||
(acc, val) => (val.length > acc.length ? val : acc),
|
||||
'',
|
||||
);
|
||||
|
||||
if (longestVal !== '' && self) {
|
||||
// eslint-disable-next-line prefer-destructuring, no-param-reassign
|
||||
self.ctx.font = axis.font[0];
|
||||
axisSize += self.ctx.measureText(longestVal).width / devicePixelRatio;
|
||||
}
|
||||
|
||||
return Math.ceil(axisSize);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default getAxes;
|
25
frontend/src/lib/uPlotLib/utils/getChartData.ts
Normal file
25
frontend/src/lib/uPlotLib/utils/getChartData.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
export const getUPlotChartData = (
|
||||
apiResponse?: MetricRangePayloadProps,
|
||||
): uPlot.AlignedData => {
|
||||
const seriesList = apiResponse?.data?.result || [];
|
||||
const uPlotData: uPlot.AlignedData = [];
|
||||
|
||||
// sort seriesList
|
||||
for (let index = 0; index < seriesList.length; index += 1) {
|
||||
seriesList[index]?.values?.sort((a, b) => a[0] - b[0]);
|
||||
}
|
||||
|
||||
// timestamp
|
||||
uPlotData.push(new Float64Array(seriesList[0]?.values?.map((v) => v[0])));
|
||||
|
||||
// for each series, push the values
|
||||
seriesList.forEach((series) => {
|
||||
const seriesData = series?.values?.map((v) => parseFloat(v[1])) || [];
|
||||
|
||||
uPlotData.push(new Float64Array(seriesData));
|
||||
});
|
||||
|
||||
return uPlotData;
|
||||
};
|
8
frontend/src/lib/uPlotLib/utils/getGridColor.ts
Normal file
8
frontend/src/lib/uPlotLib/utils/getGridColor.ts
Normal file
@ -0,0 +1,8 @@
|
||||
const getGridColor = (isDarkMode: boolean): string => {
|
||||
if (isDarkMode) {
|
||||
return 'rgba(231,233,237,0.2)';
|
||||
}
|
||||
return 'rgba(231,233,237,0.8)';
|
||||
};
|
||||
|
||||
export default getGridColor;
|
31
frontend/src/lib/uPlotLib/utils/getRenderer.ts
Normal file
31
frontend/src/lib/uPlotLib/utils/getRenderer.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import uPlot from 'uplot';
|
||||
|
||||
// Define type annotations for style and interp
|
||||
export const drawStyles = {
|
||||
line: 'line',
|
||||
bars: 'bars',
|
||||
barsLeft: 'barsLeft',
|
||||
barsRight: 'barsRight',
|
||||
points: 'points',
|
||||
};
|
||||
|
||||
export const lineInterpolations = {
|
||||
linear: 'linear',
|
||||
stepAfter: 'stepAfter',
|
||||
stepBefore: 'stepBefore',
|
||||
spline: 'spline',
|
||||
};
|
||||
|
||||
const { spline: splinePath } = uPlot.paths;
|
||||
|
||||
const spline = splinePath && splinePath();
|
||||
|
||||
const getRenderer = (style: any, interp: any): any => {
|
||||
if (style === drawStyles.line && interp === lineInterpolations.spline) {
|
||||
return spline;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default getRenderer;
|
69
frontend/src/lib/uPlotLib/utils/getSeriesData.ts
Normal file
69
frontend/src/lib/uPlotLib/utils/getSeriesData.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
import getRenderer, { drawStyles, lineInterpolations } from './getRenderer';
|
||||
|
||||
const paths = (
|
||||
u: any,
|
||||
seriesIdx: number,
|
||||
idx0: number,
|
||||
idx1: number,
|
||||
extendGap: boolean,
|
||||
buildClip: boolean,
|
||||
): any => {
|
||||
const s = u.series[seriesIdx];
|
||||
const style = s.drawStyle;
|
||||
const interp = s.lineInterpolation;
|
||||
|
||||
const renderer = getRenderer(style, interp);
|
||||
|
||||
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
|
||||
};
|
||||
|
||||
const getSeries = (
|
||||
apiResponse?: MetricRangePayloadProps,
|
||||
widgetMetaData: QueryData[] = [],
|
||||
graphsVisibilityStates?: boolean[],
|
||||
): uPlot.Options['series'] => {
|
||||
const configurations: uPlot.Series[] = [
|
||||
{ label: 'Timestamp', stroke: 'purple' },
|
||||
];
|
||||
|
||||
const seriesList = apiResponse?.data.result || [];
|
||||
|
||||
const newGraphVisibilityStates = graphsVisibilityStates?.slice(1);
|
||||
|
||||
for (let i = 0; i < seriesList?.length; i += 1) {
|
||||
const color = colors[i % colors.length]; // Use modulo to loop through colors if there are more series than colors
|
||||
|
||||
const { metric = {}, queryName = '', legend = '' } = widgetMetaData[i] || {};
|
||||
|
||||
const label = getLabelName(
|
||||
metric,
|
||||
queryName || '', // query
|
||||
legend || '',
|
||||
);
|
||||
|
||||
const seriesObj: any = {
|
||||
width: 1.4,
|
||||
paths,
|
||||
drawStyle: drawStyles.line,
|
||||
lineInterpolation: lineInterpolations.spline,
|
||||
show: newGraphVisibilityStates ? newGraphVisibilityStates[i] : true,
|
||||
label,
|
||||
stroke: color,
|
||||
spanGaps: true,
|
||||
points: {
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
|
||||
configurations.push(seriesObj);
|
||||
}
|
||||
|
||||
return configurations;
|
||||
};
|
||||
|
||||
export default getSeries;
|
@ -1,4 +1,4 @@
|
||||
import { Modal } from 'antd';
|
||||
import Modal from 'antd/es/modal';
|
||||
import get from 'api/dashboard/get';
|
||||
import lockDashboardApi from 'api/dashboard/lockDashboard';
|
||||
import unlockDashboardApi from 'api/dashboard/unlockDashboard';
|
||||
@ -9,6 +9,9 @@ import dayjs, { Dayjs } from 'dayjs';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import isUndefined from 'lodash-es/isUndefined';
|
||||
import omitBy from 'lodash-es/omitBy';
|
||||
import {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
@ -164,9 +167,18 @@ export function DashboardProvider({
|
||||
|
||||
dashboardRef.current = data;
|
||||
|
||||
setSelectedDashboard(data);
|
||||
if (!isEqual(selectedDashboard, data)) {
|
||||
setSelectedDashboard(data);
|
||||
}
|
||||
|
||||
setLayouts(getUpdatedLayout(data.data.layout));
|
||||
if (
|
||||
!isEqual(
|
||||
[omitBy(layouts, (value): boolean => isUndefined(value))[0]],
|
||||
data.data.layout,
|
||||
)
|
||||
) {
|
||||
setLayouts(getUpdatedLayout(data.data.layout));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
125
frontend/src/styles.scss
Normal file
125
frontend/src/styles.scss
Normal file
@ -0,0 +1,125 @@
|
||||
#root,
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.u-legend {
|
||||
max-height: 30px; // slicing the height of the widget Header height ;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgb(136, 136, 136);
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
tr.u-series {
|
||||
th {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
.u-marker {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Style the selected background */
|
||||
.u-select {
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
margin: 0.5rem;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
color: #fff;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
overflow: auto;
|
||||
max-height: 600px !important;
|
||||
border-radius: 5px;
|
||||
|
||||
.tooltip-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgb(136, 136, 136);
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-content-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.uplot {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 1rem;
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar:horizontal {
|
||||
height: 0.5rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
--tw-border-opacity: 1;
|
||||
background-color: rgba(217, 217, 227, 0.8);
|
||||
border-color: rgba(255, 255, 255, var(--tw-border-opacity));
|
||||
border-radius: 9999px;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(236, 236, 241, var(--tw-bg-opacity));
|
||||
}
|
@ -12801,11 +12801,6 @@ react-i18next@^11.16.1:
|
||||
"@babel/runtime" "^7.14.5"
|
||||
html-parse-stringify "^3.0.1"
|
||||
|
||||
react-intersection-observer@9.4.1:
|
||||
version "9.4.1"
|
||||
resolved "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.1.tgz"
|
||||
integrity sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==
|
||||
|
||||
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||
@ -14967,6 +14962,11 @@ uplot@1.6.24:
|
||||
resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.24.tgz#dfa213fa7da92763261920ea972ed1a5f9f6af12"
|
||||
integrity sha512-WpH2BsrFrqxkMu+4XBvc0eCDsRBhzoq9crttYeSI0bfxpzR5YoSVzZXOKFVWcVC7sp/aDXrdDPbDZGCtck2PVg==
|
||||
|
||||
uplot@1.6.26:
|
||||
version "1.6.26"
|
||||
resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.26.tgz#a6012fd141ad4a71741c75af0c71283d0ade45a7"
|
||||
integrity sha512-qN0mveL6UsP40TnHzHAJkUQvpfA3y8zSLXtXKVlJo/sLfj2+vjan/Z3g81MCZjy/hEDUFNtnLftPmETDA4s7Rg==
|
||||
|
||||
uri-js@^4.2.2:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"
|
||||
|
Loading…
x
Reference in New Issue
Block a user