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:
Palash Gupta 2023-11-15 15:33:45 +05:30 committed by GitHub
parent a99d7f09a1
commit f2f89eb38b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 2016 additions and 785 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,3 @@
import Uplot from './Uplot';
export default Uplot;

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

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

View File

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

View File

@ -9,7 +9,6 @@ const themeColors = {
silver: '#BDBDBD',
outrageousOrange: '#FF6633',
roseBud: '#FFB399',
magentaPink: '#FF33FF',
canary: '#FFFF99',
deepSkyBlue: '#00B3E6',
goldTips: '#E6B333',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,6 @@ export const EMPTY_WIDGET_LAYOUT = {
i: PANEL_TYPES.EMPTY_WIDGET,
w: 6,
x: 0,
h: 2,
h: 3,
y: 0,
};

View File

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

View File

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

View File

@ -1,6 +1,3 @@
import { Widgets } from 'types/api/dashboard/getAll';
export interface GraphLayoutProps {
onAddPanelHandler: VoidFunction;
widgets?: Widgets[];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ export const addEmptyWidgetInDashboardJSONWithQuery = (
i: 'empty',
w: 6,
x: 0,
h: 2,
h: 3,
y: 0,
},
...(dashboard?.data?.layout || []),

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

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

View File

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

View File

@ -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' && (

View File

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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