diff --git a/frontend/package.json b/frontend/package.json index 3e0a7f3981..ff11513b49 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/Graph/types.ts b/frontend/src/components/Graph/types.ts index b005e24c80..4dd1d5bde4 100644 --- a/frontend/src/components/Graph/types.ts +++ b/frontend/src/components/Graph/types.ts @@ -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 & { diff --git a/frontend/src/components/Graph/yAxisConfig.ts b/frontend/src/components/Graph/yAxisConfig.ts index 5d1eeb5da7..a5eca12926 100644 --- a/frontend/src/components/Graph/yAxisConfig.ts +++ b/frontend/src/components/Graph/yAxisConfig.ts @@ -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), diff --git a/frontend/src/components/TimePreferenceDropDown/index.tsx b/frontend/src/components/TimePreferenceDropDown/index.tsx index b24c07b3f2..572593af7c 100644 --- a/frontend/src/components/TimePreferenceDropDown/index.tsx +++ b/frontend/src/components/TimePreferenceDropDown/index.tsx @@ -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 ( - + ); diff --git a/frontend/src/components/Uplot/Uplot.tsx b/frontend/src/components/Uplot/Uplot.tsx new file mode 100644 index 0000000000..c3df529752 --- /dev/null +++ b/frontend/src/components/Uplot/Uplot.tsx @@ -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( + ( + { options, data, onDelete, onCreate, resetScales = true }, + ref, + ): JSX.Element | null => { + const chartRef = useRef(null); + const propOptionsRef = useRef(options); + const targetRef = useRef(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 ( +
+ {data && data[0] && data[0]?.length === 0 ? ( +
+ No Data +
+ ) : null} +
+ ); + }, +); + +Uplot.displayName = 'Uplot'; + +Uplot.defaultProps = { + onDelete: undefined, + onCreate: undefined, + resetScales: true, +}; + +export default memo(Uplot); diff --git a/frontend/src/components/Uplot/index.ts b/frontend/src/components/Uplot/index.ts new file mode 100644 index 0000000000..ac91e5612a --- /dev/null +++ b/frontend/src/components/Uplot/index.ts @@ -0,0 +1,3 @@ +import Uplot from './Uplot'; + +export default Uplot; diff --git a/frontend/src/components/Uplot/uplot.scss b/frontend/src/components/Uplot/uplot.scss new file mode 100644 index 0000000000..55e681cb70 --- /dev/null +++ b/frontend/src/components/Uplot/uplot.scss @@ -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%; +} diff --git a/frontend/src/components/Uplot/utils.ts b/frontend/src/components/Uplot/utils.ts new file mode 100644 index 0000000000..4680354d77 --- /dev/null +++ b/frontend/src/components/Uplot/utils.ts @@ -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], + ); + }); +}; diff --git a/frontend/src/constants/panelTypes.ts b/frontend/src/constants/panelTypes.ts index 93d66e5019..87cc62eae3 100644 --- a/frontend/src/constants/panelTypes.ts +++ b/frontend/src/constants/panelTypes.ts @@ -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, diff --git a/frontend/src/constants/theme.ts b/frontend/src/constants/theme.ts index be46d0d342..cff5c3d036 100644 --- a/frontend/src/constants/theme.ts +++ b/frontend/src/constants/theme.ts @@ -9,7 +9,6 @@ const themeColors = { silver: '#BDBDBD', outrageousOrange: '#FF6633', roseBud: '#FFB399', - magentaPink: '#FF33FF', canary: '#FFFF99', deepSkyBlue: '#00B3E6', goldTips: '#E6B333', diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index 9fdc30d835..f2ef361bdc 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -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(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 ( @@ -136,18 +145,18 @@ function ChartPreview({ {queryResponse.isLoading && ( )} - {chartDataSet && !queryResponse.isError && ( - + {chartData && !queryResponse.isError && ( +
+ +
)}
); diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.styles.scss b/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.styles.scss deleted file mode 100644 index 2d594aa8a9..0000000000 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.styles.scss +++ /dev/null @@ -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; - } - -} \ No newline at end of file diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.tsx index e6c6e26c0b..b139af7e0e 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/GraphManager.tsx @@ -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( - 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(data.datasets.length).fill( - false, - ); + const newGraphVisibilityStates = Array(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]); - - const dataSource = tableDataSet.filter((item) => item.show); + }, [ + graphsVisibilityStates, + name, + notifications, + onToggleModelHandler, + options, + ]); + + const dataSource = tableDataSet.filter( + (item, index) => index !== 0 && item.show, + ); return (
-
+
+
+ + + + + + +
+
+ +
-
- - - - - - -
); } diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx index eda971c1e4..922510eaea 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx @@ -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; diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx index 4ce97d8af2..1cafd49bf5 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx @@ -6,7 +6,7 @@ import Label from './Label'; export const getLabel = ( labelClickedHandler: (labelIndex: number) => void, ): ColumnType => ({ - render: (label, record): JSX.Element => ( + render: (label: string, record): JSX.Element => (
+ +
+ {chartOptions && ( + - Refresh - - - )} - - - - - - {canModifyChart && ( + + + )} +
+ + {canModifyChart && chartOptions && ( )} - + ); } diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts index b73a2e9112..09ea5448b8 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts @@ -31,10 +31,11 @@ export const GraphContainer = styled.div` 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}; `; diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts index ae686496e5..66d6382675 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts @@ -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>; 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; @@ -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; } diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts index b1ffb3a032..400394d26e 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts @@ -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], diff --git a/frontend/src/container/GridCardLayout/GridCard/Graph.test.tsx b/frontend/src/container/GridCardLayout/GridCard/Graph.test.tsx deleted file mode 100644 index a8ff540cc5..0000000000 --- a/frontend/src/container/GridCardLayout/GridCard/Graph.test.tsx +++ /dev/null @@ -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)); - }); -}); diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index 6ab2bb32a2..9eafb0cf6d 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -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(false); @@ -48,15 +49,16 @@ function WidgetGraphComponent({ const { pathname } = useLocation(); const lineChartRef = useRef(); + const graphRef = useRef(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( - (state) => state.app, + const featureResponse = useSelector( + (state) => state.app.featureResponse, ); + const onToggleModal = useCallback( (func: Dispatch>) => { 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 ( + + ); + } + return ( - { setHovered(true); }} @@ -200,6 +218,7 @@ function WidgetGraphComponent({ onBlur={(): void => { setHovered(false); }} + id={name} > - - - +
@@ -252,29 +270,27 @@ function WidgetGraphComponent({
{queryResponse.isLoading && } {queryResponse.isSuccess && ( - +
+ +
)} -
+ ); } WidgetGraphComponent.defaultProps = { yAxisUnit: undefined, setLayout: undefined, - onDragSelect: undefined, onClickHandler: undefined, }; diff --git a/frontend/src/container/GridCardLayout/GridCard/index.tsx b/frontend/src/container/GridCardLayout/GridCard/index.tsx index 18ac5775b5..db265a9f01 100644 --- a/frontend/src/container/GridCardLayout/GridCard/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/index.tsx @@ -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(); - 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(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; + 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, + ], + ); + return ( - - - - {isEmptyLayout && } - +
+ {isEmptyLayout ? ( + + ) : ( + + )} +
); } diff --git a/frontend/src/container/GridCardLayout/GridCard/types.ts b/frontend/src/container/GridCardLayout/GridCard/types.ts index fccf488dc8..ca68ea8540 100644 --- a/frontend/src/container/GridCardLayout/GridCard/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/types.ts @@ -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 | 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; } diff --git a/frontend/src/container/GridCardLayout/GridCard/utils.ts b/frontend/src/container/GridCardLayout/GridCard/utils.ts index a97c1fa403..ebf9fa612e 100644 --- a/frontend/src/container/GridCardLayout/GridCard/utils.ts +++ b/frontend/src/container/GridCardLayout/GridCard/utils.ts @@ -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; diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.tsx b/frontend/src/container/GridCardLayout/GridCardLayout.tsx index a293d1395e..a6b1186880 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.tsx +++ b/frontend/src/container/GridCardLayout/GridCardLayout.tsx @@ -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( @@ -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} /> diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/styles.ts b/frontend/src/container/GridCardLayout/WidgetHeader/styles.ts index dd08ea8eb2..71d01fe6e5 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/styles.ts +++ b/frontend/src/container/GridCardLayout/WidgetHeader/styles.ts @@ -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)` diff --git a/frontend/src/container/GridCardLayout/config.ts b/frontend/src/container/GridCardLayout/config.ts index e3e6aac6f9..2801913df8 100644 --- a/frontend/src/container/GridCardLayout/config.ts +++ b/frontend/src/container/GridCardLayout/config.ts @@ -16,6 +16,6 @@ export const EMPTY_WIDGET_LAYOUT = { i: PANEL_TYPES.EMPTY_WIDGET, w: 6, x: 0, - h: 2, + h: 3, y: 0, }; diff --git a/frontend/src/container/GridCardLayout/index.tsx b/frontend/src/container/GridCardLayout/index.tsx index e715d7d539..a54daa313c 100644 --- a/frontend/src/container/GridCardLayout/index.tsx +++ b/frontend/src/container/GridCardLayout/index.tsx @@ -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 ( - - ); + return ; } export default GridGraph; diff --git a/frontend/src/container/GridCardLayout/styles.ts b/frontend/src/container/GridCardLayout/styles.ts index 9416df6674..79b7525b34 100644 --- a/frontend/src/container/GridCardLayout/styles.ts +++ b/frontend/src/container/GridCardLayout/styles.ts @@ -13,10 +13,11 @@ interface CardProps { export const Card = styled(CardComponent)` &&& { height: 100%; + overflow: hidden; } .ant-card-body { - height: 95%; + height: 90%; padding: 0; ${({ $panelType }): FlattenSimpleInterpolation => $panelType === PANEL_TYPES.TABLE diff --git a/frontend/src/container/GridCardLayout/types.ts b/frontend/src/container/GridCardLayout/types.ts index 0d2b678af6..a0e8e9aa13 100644 --- a/frontend/src/container/GridCardLayout/types.ts +++ b/frontend/src/container/GridCardLayout/types.ts @@ -1,6 +1,3 @@ -import { Widgets } from 'types/api/dashboard/getAll'; - export interface GraphLayoutProps { onAddPanelHandler: VoidFunction; - widgets?: Widgets[]; } diff --git a/frontend/src/container/GridPanelSwitch/index.tsx b/frontend/src/container/GridPanelSwitch/index.tsx index 60f0a618fa..eab39a2baf 100644 --- a/frontend/src/container/GridPanelSwitch/index.tsx +++ b/frontend/src/container/GridPanelSwitch/index.tsx @@ -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] diff --git a/frontend/src/container/GridPanelSwitch/types.ts b/frontend/src/container/GridPanelSwitch/types.ts index c7703aee56..0612fbae67 100644 --- a/frontend/src/container/GridPanelSwitch/types.ts +++ b/frontend/src/container/GridPanelSwitch/types.ts @@ -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; + }; [PANEL_TYPES.VALUE]: GridValueComponentProps; [PANEL_TYPES.TABLE]: GridTableComponentProps; [PANEL_TYPES.TRACE]: null; diff --git a/frontend/src/container/GridValueComponent/index.tsx b/frontend/src/container/GridValueComponent/index.tsx index be9a3890ba..14294893c6 100644 --- a/frontend/src/container/GridValueComponent/index.tsx +++ b/frontend/src/container/GridValueComponent/index.tsx @@ -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 ( - + No Data ); @@ -33,7 +33,7 @@ function GridValueComponent({ {gridTitle} - + ` - height: ${({ isDashboardPage }): string => - isDashboardPage ? '100%' : '55vh'}; + +export const ValueContainer = styled.div` + height: 100%; display: flex; justify-content: center; align-items: center; diff --git a/frontend/src/container/GridValueComponent/types.ts b/frontend/src/container/GridValueComponent/types.ts index 94c3c04d4d..feb32f6d89 100644 --- a/frontend/src/container/GridValueComponent/types.ts +++ b/frontend/src/container/GridValueComponent/types.ts @@ -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; }; diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index 7b92dca752..13d954de4b 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -109,13 +109,12 @@ function DBCall(): JSX.Element { { + 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', ); }} diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index 55d9c86bea..c02f4f1ce7 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -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 => onGraphClickHandler(setSelectedTimeStamp)( - ChartEvent, - activeElements, - chart, - data, + xValue, + yValue, + mouseX, + mouseY, 'external_call_rps_by_address', - ); - }} + ) + } /> @@ -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', ); }} diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index b91d1b1175..ff6460e3d7 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -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 => 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; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/types.ts b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/types.ts index e3046261f0..79b4d3463f 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/types.ts +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/types.ts @@ -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[]; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx index d785f21be1..b97a631b55 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx @@ -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; } diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx index bd37ebec9b..1dbbf2ee98 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/TopLevelOperations.tsx @@ -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; } diff --git a/frontend/src/container/MetricsApplication/Tabs/util.ts b/frontend/src/container/MetricsApplication/Tabs/util.ts index b33bbf0767..c44e7462f4 100644 --- a/frontend/src/container/MetricsApplication/Tabs/util.ts +++ b/frontend/src/container/MetricsApplication/Tabs/util.ts @@ -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>, ) { return async ( - event: ChartEvent, - elements: ActiveElement[], - chart: Chart, - data: ChartData, - from: string, + xValue: number, + yValue: number, + mouseX: number, + mouseY: number, + type: string, ): Promise => { - 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'; } }; } diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index c9c1c6dde1..1a111cf4fe 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -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) || diff --git a/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss b/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss new file mode 100644 index 0000000000..a04f57f3f8 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardDescription/Description.styles.scss @@ -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; +} diff --git a/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx b/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx index fb512e999a..44b697cc67 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx @@ -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(false); const showDrawer = (): void => { @@ -22,8 +22,9 @@ function SettingsDrawer(): JSX.Element { Configure diff --git a/frontend/src/container/NewDashboard/DashboardDescription/ShareModal.tsx b/frontend/src/container/NewDashboard/DashboardDescription/ShareModal.tsx index d7ccd451e4..8826eccddc 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/ShareModal.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/ShareModal.tsx @@ -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(getParsedValue()); - const [isViewJSON, setIsViewJSON] = useState(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 ( - <> - - - - - ); - } return ( - + <> + + + + ); - }, [isViewJSON, jsonValue, selectedData, setCopy, t]); + }, [jsonValue, selectedData, setCopy, t]); return ( { onToggleHandler(); - setIsViewJSON(false); }} - width="70vw" + width="80vw" centered title={t('share', { ns: 'common', @@ -86,11 +83,11 @@ function ShareModal({ destroyOnClose footer={GetFooterComponent} > - {!isViewJSON ? ( - {t('export_dashboard')} - ) : ( - setJSONValue(value)} value={jsonValue} /> - )} + setJSONValue(value)} + value={jsonValue} + /> ); } diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index 679ba2b25e..6eae3ff416 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -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 { title = '', tags, description } = selectedData || {}; - const [isJSONModalVisible, isIsJSONModalVisible] = useState(false); + const [openDashboardJSON, setOpenDashboardJSON] = useState(false); const { t } = useTranslation('common'); const { user, role } = useSelector((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 ( - - + + {isDashboardLocked && ( @@ -55,27 +59,36 @@ function DashboardDescription(): JSX.Element { )} {title} - {description} - -
- {tags?.map((tag) => ( - {tag} - ))} -
+ {description && ( + {description} + )} - + {tags && ( +
+ {tags?.map((tag) => ( + {tag} + ))} +
+ )} + + + + + - + {selectedData && ( )} - {!isDashboardLocked && editDashboard && } + {!isDashboardLocked && editDashboard && ( + + )}