-
}
diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx
index 561af111ae..439ab98a67 100644
--- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx
+++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx
@@ -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);
diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.tsx
deleted file mode 100644
index fd46163807..0000000000
--- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.tsx
+++ /dev/null
@@ -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
Invalid widget;
- }
-
- const { title, opacity, isStacked, query } = selectedWidget;
-
- if (getWidgetQueryRange.error) {
- return (
-
- {getWidgetQueryRange.error.message}
-
- );
- }
- if (getWidgetQueryRange.isLoading) {
- return
;
- }
- if (getWidgetQueryRange.data?.payload.data.result.length === 0) {
- return (
-
- No Data
-
- );
- }
-
- const chartDataSet = getChartData({
- queryData: [
- { queryData: getWidgetQueryRange.data?.payload.data.result ?? [] },
- ],
- createDataset: undefined,
- isWarningLimit: selectedWidget.panelTypes === PANEL_TYPES.TIME_SERIES,
- });
-
- return (
- <>
- {chartDataSet.isWarning && (
-
-
-
- )}
-
-
- >
- );
-}
-
-export default WidgetGraph;
diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx
new file mode 100644
index 0000000000..49bdd14117
--- /dev/null
+++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx
@@ -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
Invalid widget;
+ }
+
+ if (getWidgetQueryRange.error) {
+ return (
+
+ {getWidgetQueryRange.error.message}
+
+ );
+ }
+ if (getWidgetQueryRange.isLoading) {
+ return
;
+ }
+ if (getWidgetQueryRange.data?.payload.data.result.length === 0) {
+ return (
+
+ No Data
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default WidgetGraphContainer;
diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx
new file mode 100644
index 0000000000..e8d413cc17
--- /dev/null
+++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx
@@ -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
(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 (
+
+
+
+ );
+}
+
+interface WidgetGraphProps {
+ yAxisUnit: string;
+ selectedWidget: Widgets;
+ getWidgetQueryRange: UseQueryResult<
+ SuccessResponse,
+ Error
+ >;
+}
+
+export default WidgetGraph;
diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx
index 8bc4f57d1a..2bdb4576c7 100644
--- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx
+++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx
@@ -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);
diff --git a/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx b/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx
index 8594ccd90c..057aec82c4 100644
--- a/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx
+++ b/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx
@@ -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(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 (
{isLoading && }
{isError && {data?.error || 'Something went wrong'}}
- {!isLoading && !isError && (
-
- )}
+
+ {!isLoading && !isError && chartData && chartOptions && (
+
+ )}
+
);
}
diff --git a/frontend/src/globalStyles.ts b/frontend/src/globalStyles.ts
deleted file mode 100644
index 86dc8258ed..0000000000
--- a/frontend/src/globalStyles.ts
+++ /dev/null
@@ -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;
diff --git a/frontend/src/hooks/dashboard/utils.ts b/frontend/src/hooks/dashboard/utils.ts
index 84fa59332d..ba02cea7a4 100644
--- a/frontend/src/hooks/dashboard/utils.ts
+++ b/frontend/src/hooks/dashboard/utils.ts
@@ -15,7 +15,7 @@ export const addEmptyWidgetInDashboardJSONWithQuery = (
i: 'empty',
w: 6,
x: 0,
- h: 2,
+ h: 3,
y: 0,
},
...(dashboard?.data?.layout || []),
diff --git a/frontend/src/hooks/useDimensions.ts b/frontend/src/hooks/useDimensions.ts
new file mode 100644
index 0000000000..da11ea7681
--- /dev/null
+++ b/frontend/src/hooks/useDimensions.ts
@@ -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(
+ ref: React.RefObject,
+ debounceTime = 300,
+): Dimensions {
+ const [size, setSize] = useState({
+ 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;
+}
diff --git a/frontend/src/hooks/useIntersectionObserver.ts b/frontend/src/hooks/useIntersectionObserver.ts
new file mode 100644
index 0000000000..50a23608e9
--- /dev/null
+++ b/frontend/src/hooks/useIntersectionObserver.ts
@@ -0,0 +1,38 @@
+import { RefObject, useEffect, useState } from 'react';
+
+export function useIntersectionObserver(
+ ref: RefObject,
+ 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;
+}
diff --git a/frontend/src/index.html.ejs b/frontend/src/index.html.ejs
index 227506006b..2bbd3ed880 100644
--- a/frontend/src/index.html.ejs
+++ b/frontend/src/index.html.ejs
@@ -56,6 +56,11 @@
href="https://fonts.googleapis.com/css?family=Fira+Code"
rel="stylesheet"
/>
+
+
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 712fb4cc58..7ef3d47bb9 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -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) {
-
{process.env.NODE_ENV === 'development' && (
diff --git a/frontend/src/lib/getRandomColor.ts b/frontend/src/lib/getRandomColor.ts
index 6cca527b32..c24106fb96 100644
--- a/frontend/src/lib/getRandomColor.ts
+++ b/frontend/src/lib/getRandomColor.ts
@@ -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 = (
diff --git a/frontend/src/lib/uPlotLib/getUplotChartData.ts b/frontend/src/lib/uPlotLib/getUplotChartData.ts
new file mode 100644
index 0000000000..a3893f7325
--- /dev/null
+++ b/frontend/src/lib/uPlotLib/getUplotChartData.ts
@@ -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),
+});
diff --git a/frontend/src/lib/uPlotLib/placement.ts b/frontend/src/lib/uPlotLib/placement.ts
new file mode 100644
index 0000000000..629e2dbcb2
--- /dev/null
+++ b/frontend/src/lib/uPlotLib/placement.ts
@@ -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);
+ };
+})();
diff --git a/frontend/src/lib/uPlotLib/plugins/onClickPlugin.ts b/frontend/src/lib/uPlotLib/plugins/onClickPlugin.ts
new file mode 100644
index 0000000000..56a6f1e333
--- /dev/null
+++ b/frontend/src/lib/uPlotLib/plugins/onClickPlugin.ts
@@ -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;
diff --git a/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts
new file mode 100644
index 0000000000..fabb5ea971
--- /dev/null
+++ b/frontend/src/lib/uPlotLib/plugins/tooltipPlugin.ts
@@ -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;
diff --git a/frontend/src/lib/uPlotLib/uPlotLib.styles.scss b/frontend/src/lib/uPlotLib/uPlotLib.styles.scss
new file mode 100644
index 0000000000..42bb247772
--- /dev/null
+++ b/frontend/src/lib/uPlotLib/uPlotLib.styles.scss
@@ -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;
+ }
+}
diff --git a/frontend/src/lib/uPlotLib/utils/getAxes.ts b/frontend/src/lib/uPlotLib/utils/getAxes.ts
new file mode 100644
index 0000000000..8c4a926ccd
--- /dev/null
+++ b/frontend/src/lib/uPlotLib/utils/getAxes.ts
@@ -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;
diff --git a/frontend/src/lib/uPlotLib/utils/getChartData.ts b/frontend/src/lib/uPlotLib/utils/getChartData.ts
new file mode 100644
index 0000000000..8850dd315e
--- /dev/null
+++ b/frontend/src/lib/uPlotLib/utils/getChartData.ts
@@ -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;
+};
diff --git a/frontend/src/lib/uPlotLib/utils/getGridColor.ts b/frontend/src/lib/uPlotLib/utils/getGridColor.ts
new file mode 100644
index 0000000000..72557e192c
--- /dev/null
+++ b/frontend/src/lib/uPlotLib/utils/getGridColor.ts
@@ -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;
diff --git a/frontend/src/lib/uPlotLib/utils/getRenderer.ts b/frontend/src/lib/uPlotLib/utils/getRenderer.ts
new file mode 100644
index 0000000000..564a4532b0
--- /dev/null
+++ b/frontend/src/lib/uPlotLib/utils/getRenderer.ts
@@ -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;
diff --git a/frontend/src/lib/uPlotLib/utils/getSeriesData.ts b/frontend/src/lib/uPlotLib/utils/getSeriesData.ts
new file mode 100644
index 0000000000..45d6796fbc
--- /dev/null
+++ b/frontend/src/lib/uPlotLib/utils/getSeriesData.ts
@@ -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;
diff --git a/frontend/src/providers/Dashboard/Dashboard.tsx b/frontend/src/providers/Dashboard/Dashboard.tsx
index 48190fd3c4..c5678c18f0 100644
--- a/frontend/src/providers/Dashboard/Dashboard.tsx
+++ b/frontend/src/providers/Dashboard/Dashboard.tsx
@@ -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);
-
- setLayouts(getUpdatedLayout(data.data.layout));
+ if (!isEqual(selectedDashboard, data)) {
+ setSelectedDashboard(data);
+ }
+
+ if (
+ !isEqual(
+ [omitBy(layouts, (value): boolean => isUndefined(value))[0]],
+ data.data.layout,
+ )
+ ) {
+ setLayouts(getUpdatedLayout(data.data.layout));
+ }
}
},
},
diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss
new file mode 100644
index 0000000000..10525a1f02
--- /dev/null
+++ b/frontend/src/styles.scss
@@ -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));
+}
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index c0da947052..920498669d 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -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"