mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 20:38:59 +08:00
Uplot time range (#4144)
* feat: show range bound chart based on the selected time range * feat: handle no data * feat: show bigger point if only data point exists * feat: show bigger point if only data point exists * feat: widget ui fixes * feat: no data - full view fix * fix: show closed point on hover * feat: handle widget time preference in case of dashboard, edit view, full view and chart preview
This commit is contained in:
parent
7efe907757
commit
418ab67d50
@ -86,6 +86,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
|
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
|
||||||
|
'no-plusplus': 'off',
|
||||||
'jsx-a11y/label-has-associated-control': [
|
'jsx-a11y/label-has-associated-control': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
@ -109,7 +110,6 @@ module.exports = {
|
|||||||
// eslint rules need to remove
|
// eslint rules need to remove
|
||||||
'@typescript-eslint/no-shadow': 'off',
|
'@typescript-eslint/no-shadow': 'off',
|
||||||
'import/no-cycle': 'off',
|
'import/no-cycle': 'off',
|
||||||
|
|
||||||
'prettier/prettier': [
|
'prettier/prettier': [
|
||||||
'error',
|
'error',
|
||||||
{},
|
{},
|
||||||
|
@ -13,3 +13,11 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uplot-no-data {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import './uplot.scss';
|
import './Uplot.styles.scss';
|
||||||
|
|
||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
|
import { LineChart } from 'lucide-react';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
@ -127,6 +128,16 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
|||||||
}
|
}
|
||||||
}, [data, resetScales, create]);
|
}, [data, resetScales, create]);
|
||||||
|
|
||||||
|
if (data && data[0] && data[0]?.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="uplot-no-data not-found">
|
||||||
|
<LineChart size={48} strokeWidth={0.5} />
|
||||||
|
|
||||||
|
<Typography>No Data</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||||
<div className="uplot-graph-container" ref={targetRef}>
|
<div className="uplot-graph-container" ref={targetRef}>
|
||||||
|
@ -10,7 +10,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
|||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -18,6 +18,7 @@ import { AlertDef } from 'types/api/alerts/def';
|
|||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
|
|
||||||
import { ChartContainer, FailedMessageContainer } from './styles';
|
import { ChartContainer, FailedMessageContainer } from './styles';
|
||||||
import { getThresholdLabel } from './utils';
|
import { getThresholdLabel } from './utils';
|
||||||
@ -49,9 +50,13 @@ function ChartPreview({
|
|||||||
}: ChartPreviewProps): JSX.Element | null {
|
}: ChartPreviewProps): JSX.Element | null {
|
||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
const threshold = alertDef?.condition.target || 0;
|
const threshold = alertDef?.condition.target || 0;
|
||||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||||
(state) => state.globalTime,
|
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||||
);
|
|
||||||
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
|
AppState,
|
||||||
|
GlobalReducer
|
||||||
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
const canQuery = useMemo((): boolean => {
|
const canQuery = useMemo((): boolean => {
|
||||||
if (!query || query == null) {
|
if (!query || query == null) {
|
||||||
@ -101,6 +106,13 @@ function ChartPreview({
|
|||||||
|
|
||||||
const graphRef = useRef<HTMLDivElement>(null);
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect((): void => {
|
||||||
|
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||||
|
|
||||||
|
setMinTimeScale(startTime);
|
||||||
|
setMaxTimeScale(endTime);
|
||||||
|
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
|
||||||
|
|
||||||
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||||
|
|
||||||
const containerDimensions = useResizeObserver(graphRef);
|
const containerDimensions = useResizeObserver(graphRef);
|
||||||
@ -117,6 +129,8 @@ function ChartPreview({
|
|||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
apiResponse: queryResponse?.data?.payload,
|
apiResponse: queryResponse?.data?.payload,
|
||||||
dimensions: containerDimensions,
|
dimensions: containerDimensions,
|
||||||
|
minTimeScale,
|
||||||
|
maxTimeScale,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
thresholds: [
|
thresholds: [
|
||||||
{
|
{
|
||||||
@ -141,6 +155,8 @@ function ChartPreview({
|
|||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
queryResponse?.data?.payload,
|
queryResponse?.data?.payload,
|
||||||
containerDimensions,
|
containerDimensions,
|
||||||
|
minTimeScale,
|
||||||
|
maxTimeScale,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
threshold,
|
threshold,
|
||||||
t,
|
t,
|
||||||
|
@ -23,6 +23,7 @@ import { useSelector } from 'react-redux';
|
|||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
|
|
||||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||||
import GraphManager from './GraphManager';
|
import GraphManager from './GraphManager';
|
||||||
@ -92,6 +93,21 @@ function FullView({
|
|||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||||
|
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||||
|
|
||||||
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
|
AppState,
|
||||||
|
GlobalReducer
|
||||||
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
|
useEffect((): void => {
|
||||||
|
const { startTime, endTime } = getTimeRange(response);
|
||||||
|
|
||||||
|
setMinTimeScale(startTime);
|
||||||
|
setMaxTimeScale(endTime);
|
||||||
|
}, [maxTime, minTime, globalSelectedInterval, response]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!response.isFetching && fullViewRef.current) {
|
if (!response.isFetching && fullViewRef.current) {
|
||||||
const width = fullViewRef.current?.clientWidth
|
const width = fullViewRef.current?.clientWidth
|
||||||
@ -114,6 +130,8 @@ function FullView({
|
|||||||
graphsVisibilityStates,
|
graphsVisibilityStates,
|
||||||
setGraphsVisibilityStates,
|
setGraphsVisibilityStates,
|
||||||
thresholds: widget.thresholds,
|
thresholds: widget.thresholds,
|
||||||
|
minTimeScale,
|
||||||
|
maxTimeScale,
|
||||||
});
|
});
|
||||||
|
|
||||||
setChartOptions(newChartOptions);
|
setChartOptions(newChartOptions);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Skeleton, Typography } from 'antd';
|
import { Skeleton, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
@ -298,7 +299,10 @@ function WidgetGraphComponent({
|
|||||||
</div>
|
</div>
|
||||||
{queryResponse.isLoading && <Skeleton />}
|
{queryResponse.isLoading && <Skeleton />}
|
||||||
{queryResponse.isSuccess && (
|
{queryResponse.isSuccess && (
|
||||||
<div style={{ height: '90%' }} ref={graphRef}>
|
<div
|
||||||
|
className={cx('widget-graph-container', widget.panelTypes)}
|
||||||
|
ref={graphRef}
|
||||||
|
>
|
||||||
<GridPanelSwitch
|
<GridPanelSwitch
|
||||||
panelType={widget.panelTypes}
|
panelType={widget.panelTypes}
|
||||||
data={data}
|
data={data}
|
||||||
|
@ -15,6 +15,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
|
|
||||||
import EmptyWidget from '../EmptyWidget';
|
import EmptyWidget from '../EmptyWidget';
|
||||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||||
@ -34,6 +35,8 @@ function GridCardGraph({
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||||
|
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||||
|
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||||
|
|
||||||
const onDragSelect = useCallback(
|
const onDragSelect = useCallback(
|
||||||
(start: number, end: number): void => {
|
(start: number, end: number): void => {
|
||||||
@ -62,16 +65,16 @@ function GridCardGraph({
|
|||||||
}
|
}
|
||||||
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
|
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
|
||||||
|
|
||||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
|
||||||
AppState,
|
|
||||||
GlobalReducer
|
|
||||||
>((state) => state.globalTime);
|
|
||||||
|
|
||||||
const updatedQuery = useStepInterval(widget?.query);
|
const updatedQuery = useStepInterval(widget?.query);
|
||||||
|
|
||||||
const isEmptyWidget =
|
const isEmptyWidget =
|
||||||
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
|
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
|
||||||
|
|
||||||
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
|
AppState,
|
||||||
|
GlobalReducer
|
||||||
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
const queryResponse = useGetQueryRange(
|
const queryResponse = useGetQueryRange(
|
||||||
{
|
{
|
||||||
selectedTime: widget?.timePreferance,
|
selectedTime: widget?.timePreferance,
|
||||||
@ -103,6 +106,13 @@ function GridCardGraph({
|
|||||||
|
|
||||||
const containerDimensions = useResizeObserver(graphRef);
|
const containerDimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
|
useEffect((): void => {
|
||||||
|
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||||
|
|
||||||
|
setMinTimeScale(startTime);
|
||||||
|
setMaxTimeScale(endTime);
|
||||||
|
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
|
||||||
|
|
||||||
const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans);
|
const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans);
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
@ -123,6 +133,8 @@ function GridCardGraph({
|
|||||||
yAxisUnit: widget?.yAxisUnit,
|
yAxisUnit: widget?.yAxisUnit,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
thresholds: widget.thresholds,
|
thresholds: widget.thresholds,
|
||||||
|
minTimeScale,
|
||||||
|
maxTimeScale,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
widget?.id,
|
widget?.id,
|
||||||
@ -133,6 +145,8 @@ function GridCardGraph({
|
|||||||
isDarkMode,
|
isDarkMode,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
|
minTimeScale,
|
||||||
|
maxTimeScale,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -5,3 +5,11 @@
|
|||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.widget-graph-container {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&.graph {
|
||||||
|
height: calc(100% - 30px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 30px;
|
height: 40px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-header-title {
|
.widget-header-title {
|
||||||
@ -19,6 +22,10 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-header-hover {
|
.widget-header-hover {
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
WarningOutlined,
|
WarningOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Button, Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@ -199,9 +199,7 @@ function WidgetHeader({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
||||||
<Button
|
<MoreOutlined
|
||||||
type="default"
|
|
||||||
icon={<MoreOutlined />}
|
|
||||||
className={`widget-header-more-options ${
|
className={`widget-header-more-options ${
|
||||||
parentHover ? 'widget-header-hover' : ''
|
parentHover ? 'widget-header-hover' : ''
|
||||||
}`}
|
}`}
|
||||||
|
@ -2,7 +2,7 @@ import { Button as ButtonComponent, Card as CardComponent, Space } from 'antd';
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { StyledCSS } from 'container/GantChart/Trace/styles';
|
import { StyledCSS } from 'container/GantChart/Trace/styles';
|
||||||
import RGL, { WidthProvider } from 'react-grid-layout';
|
import RGL, { WidthProvider } from 'react-grid-layout';
|
||||||
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
const ReactGridLayoutComponent = WidthProvider(RGL);
|
const ReactGridLayoutComponent = WidthProvider(RGL);
|
||||||
|
|
||||||
@ -17,14 +17,8 @@ export const Card = styled(CardComponent)<CardProps>`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
height: 90%;
|
height: calc(100% - 40px);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
${({ $panelType }): FlattenSimpleInterpolation =>
|
|
||||||
$panelType === PANEL_TYPES.TABLE
|
|
||||||
? css`
|
|
||||||
padding-top: 1.8rem;
|
|
||||||
`
|
|
||||||
: css``}
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -6,13 +6,16 @@ import { useResizeObserver } from 'hooks/useDimensions';
|
|||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
import { useCallback, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
|
|
||||||
function WidgetGraph({
|
function WidgetGraph({
|
||||||
getWidgetQueryRange,
|
getWidgetQueryRange,
|
||||||
@ -23,6 +26,21 @@ function WidgetGraph({
|
|||||||
}: WidgetGraphProps): JSX.Element {
|
}: WidgetGraphProps): JSX.Element {
|
||||||
const { stagedQuery } = useQueryBuilder();
|
const { stagedQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
|
AppState,
|
||||||
|
GlobalReducer
|
||||||
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
|
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||||
|
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||||
|
|
||||||
|
useEffect((): void => {
|
||||||
|
const { startTime, endTime } = getTimeRange(getWidgetQueryRange);
|
||||||
|
|
||||||
|
setMinTimeScale(startTime);
|
||||||
|
setMaxTimeScale(endTime);
|
||||||
|
}, [getWidgetQueryRange, maxTime, minTime, globalSelectedInterval]);
|
||||||
|
|
||||||
const graphRef = useRef<HTMLDivElement>(null);
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const containerDimensions = useResizeObserver(graphRef);
|
const containerDimensions = useResizeObserver(graphRef);
|
||||||
@ -63,6 +81,8 @@ function WidgetGraph({
|
|||||||
onDragSelect,
|
onDragSelect,
|
||||||
thresholds,
|
thresholds,
|
||||||
fillSpans,
|
fillSpans,
|
||||||
|
minTimeScale,
|
||||||
|
maxTimeScale,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
widgetId,
|
widgetId,
|
||||||
@ -73,6 +93,8 @@ function WidgetGraph({
|
|||||||
onDragSelect,
|
onDragSelect,
|
||||||
thresholds,
|
thresholds,
|
||||||
fillSpans,
|
fillSpans,
|
||||||
|
minTimeScale,
|
||||||
|
maxTimeScale,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -12,8 +12,7 @@ export const Container = styled(Card)<Props>`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
padding: ${({ $panelType }): string =>
|
padding: 8px;
|
||||||
$panelType === PANEL_TYPES.TABLE ? '0 0' : '1.5rem 0'};
|
|
||||||
height: 57vh;
|
height: 57vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -3,9 +3,13 @@ import Uplot from 'components/Uplot';
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
|
|
||||||
import { Container, ErrorText } from './styles';
|
import { Container, ErrorText } from './styles';
|
||||||
|
|
||||||
@ -31,6 +35,21 @@ function TimeSeriesView({
|
|||||||
? graphRef.current.clientHeight
|
? graphRef.current.clientHeight
|
||||||
: 300;
|
: 300;
|
||||||
|
|
||||||
|
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||||
|
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||||
|
|
||||||
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
|
AppState,
|
||||||
|
GlobalReducer
|
||||||
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
|
useEffect((): void => {
|
||||||
|
const { startTime, endTime } = getTimeRange();
|
||||||
|
|
||||||
|
setMinTimeScale(startTime);
|
||||||
|
setMaxTimeScale(endTime);
|
||||||
|
}, [maxTime, minTime, globalSelectedInterval, data]);
|
||||||
|
|
||||||
const chartOptions = getUPlotChartOptions({
|
const chartOptions = getUPlotChartOptions({
|
||||||
yAxisUnit: yAxisUnit || '',
|
yAxisUnit: yAxisUnit || '',
|
||||||
apiResponse: data?.payload,
|
apiResponse: data?.payload,
|
||||||
@ -39,6 +58,8 @@ function TimeSeriesView({
|
|||||||
height,
|
height,
|
||||||
},
|
},
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
|
minTimeScale,
|
||||||
|
maxTimeScale,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
@ -15,6 +16,7 @@ import onClickPlugin, { OnClickPluginOpts } from './plugins/onClickPlugin';
|
|||||||
import tooltipPlugin from './plugins/tooltipPlugin';
|
import tooltipPlugin from './plugins/tooltipPlugin';
|
||||||
import getAxes from './utils/getAxes';
|
import getAxes from './utils/getAxes';
|
||||||
import getSeries from './utils/getSeriesData';
|
import getSeries from './utils/getSeriesData';
|
||||||
|
import { getXAxisScale } from './utils/getXAxisScale';
|
||||||
import { getYAxisScale } from './utils/getYAxisScale';
|
import { getYAxisScale } from './utils/getYAxisScale';
|
||||||
|
|
||||||
interface GetUPlotChartOptions {
|
interface GetUPlotChartOptions {
|
||||||
@ -31,6 +33,8 @@ interface GetUPlotChartOptions {
|
|||||||
thresholdValue?: number;
|
thresholdValue?: number;
|
||||||
thresholdText?: string;
|
thresholdText?: string;
|
||||||
fillSpans?: boolean;
|
fillSpans?: boolean;
|
||||||
|
minTimeScale?: number;
|
||||||
|
maxTimeScale?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUPlotChartOptions = ({
|
export const getUPlotChartOptions = ({
|
||||||
@ -40,18 +44,20 @@ export const getUPlotChartOptions = ({
|
|||||||
apiResponse,
|
apiResponse,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
|
minTimeScale,
|
||||||
|
maxTimeScale,
|
||||||
onClickHandler = _noop,
|
onClickHandler = _noop,
|
||||||
graphsVisibilityStates,
|
graphsVisibilityStates,
|
||||||
setGraphsVisibilityStates,
|
setGraphsVisibilityStates,
|
||||||
thresholds,
|
thresholds,
|
||||||
fillSpans,
|
fillSpans,
|
||||||
}: GetUPlotChartOptions): uPlot.Options => {
|
}: GetUPlotChartOptions): uPlot.Options => {
|
||||||
// eslint-disable-next-line sonarjs/prefer-immediate-return
|
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
||||||
const chartOptions = {
|
|
||||||
|
return {
|
||||||
id,
|
id,
|
||||||
width: dimensions.width,
|
width: dimensions.width,
|
||||||
height: dimensions.height - 45,
|
height: dimensions.height - 30,
|
||||||
// tzDate: (ts) => uPlot.tzDate(new Date(ts * 1e3), ''), // Pass timezone for 2nd param
|
|
||||||
legend: {
|
legend: {
|
||||||
show: true,
|
show: true,
|
||||||
live: false,
|
live: false,
|
||||||
@ -67,18 +73,18 @@ export const getUPlotChartOptions = ({
|
|||||||
bias: 1,
|
bias: 1,
|
||||||
},
|
},
|
||||||
points: {
|
points: {
|
||||||
size: (u, seriesIdx): number => u.series[seriesIdx].points.size * 2.5,
|
size: (u, seriesIdx): number => u.series[seriesIdx].points.size * 3,
|
||||||
width: (u, seriesIdx, size): number => size / 4,
|
width: (u, seriesIdx, size): number => size / 4,
|
||||||
stroke: (u, seriesIdx): string =>
|
stroke: (u, seriesIdx): string =>
|
||||||
`${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`,
|
`${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`,
|
||||||
fill: (): string => '#fff',
|
fill: (): string => '#fff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
padding: [16, 16, 16, 16],
|
padding: [16, 16, 8, 8],
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
time: true,
|
spanGaps: true,
|
||||||
auto: true, // Automatically adjust scale range
|
...timeScaleProps,
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
...getYAxisScale(
|
...getYAxisScale(
|
||||||
@ -194,6 +200,4 @@ export const getUPlotChartOptions = ({
|
|||||||
),
|
),
|
||||||
axes: getAxes(isDarkMode, yAxisUnit),
|
axes: getAxes(isDarkMode, yAxisUnit),
|
||||||
};
|
};
|
||||||
|
|
||||||
return chartOptions;
|
|
||||||
};
|
};
|
||||||
|
@ -54,7 +54,7 @@ const generateTooltipContent = (
|
|||||||
const value = data[index][idx];
|
const value = data[index][idx];
|
||||||
const label = getLabelName(metric, queryName || '', legend || '');
|
const label = getLabelName(metric, queryName || '', legend || '');
|
||||||
|
|
||||||
if (value) {
|
if (Number.isFinite(value)) {
|
||||||
const tooltipValue = getToolTipValue(value, yAxisUnit);
|
const tooltipValue = getToolTipValue(value, yAxisUnit);
|
||||||
|
|
||||||
const dataObj = {
|
const dataObj = {
|
||||||
@ -191,7 +191,8 @@ const tooltipPlugin = (
|
|||||||
if (overlay) {
|
if (overlay) {
|
||||||
overlay.textContent = '';
|
overlay.textContent = '';
|
||||||
const { left, top, idx } = u.cursor;
|
const { left, top, idx } = u.cursor;
|
||||||
if (idx) {
|
|
||||||
|
if (Number.isInteger(idx)) {
|
||||||
const anchor = { left: left + bLeft, top: top + bTop };
|
const anchor = { left: left + bLeft, top: top + bTop };
|
||||||
const content = generateTooltipContent(
|
const content = generateTooltipContent(
|
||||||
apiResult,
|
apiResult,
|
||||||
|
@ -9,8 +9,7 @@ const getAxes = (isDarkMode: boolean, yAxisUnit?: string): any => [
|
|||||||
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
||||||
grid: {
|
grid: {
|
||||||
stroke: getGridColor(isDarkMode), // Color of the grid lines
|
stroke: getGridColor(isDarkMode), // Color of the grid lines
|
||||||
dash: [10, 10], // Dash pattern for grid lines,
|
width: 0.2, // Width of the grid lines,
|
||||||
width: 0.5, // Width of the grid lines,
|
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
@ -24,8 +23,7 @@ const getAxes = (isDarkMode: boolean, yAxisUnit?: string): any => [
|
|||||||
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
||||||
grid: {
|
grid: {
|
||||||
stroke: getGridColor(isDarkMode), // Color of the grid lines
|
stroke: getGridColor(isDarkMode), // Color of the grid lines
|
||||||
dash: [10, 10], // Dash pattern for grid lines,
|
width: 0.2, // Width of the grid lines
|
||||||
width: 0.3, // Width of the grid lines
|
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
// stroke: isDarkMode ? 'white' : 'black', // Color of the tick lines
|
// stroke: isDarkMode ? 'white' : 'black', // Color of the tick lines
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
const getGridColor = (isDarkMode: boolean): string => {
|
const getGridColor = (isDarkMode: boolean): string => {
|
||||||
if (isDarkMode) {
|
if (isDarkMode) {
|
||||||
return 'rgba(231,233,237,0.2)';
|
return 'rgba(231,233,237,0.3)';
|
||||||
}
|
}
|
||||||
return 'rgba(231,233,237,0.8)';
|
return 'rgba(0,0,0,0.5)';
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getGridColor;
|
export default getGridColor;
|
||||||
|
@ -46,17 +46,22 @@ const getSeries = (
|
|||||||
legend || '',
|
legend || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pointSize = seriesList[i].values.length > 1 ? 5 : 10;
|
||||||
|
const showPoints = !(seriesList[i].values.length > 1);
|
||||||
|
|
||||||
const seriesObj: any = {
|
const seriesObj: any = {
|
||||||
width: 1.4,
|
|
||||||
paths,
|
paths,
|
||||||
drawStyle: drawStyles.line,
|
drawStyle: drawStyles.line,
|
||||||
lineInterpolation: lineInterpolations.spline,
|
lineInterpolation: lineInterpolations.spline,
|
||||||
show: newGraphVisibilityStates ? newGraphVisibilityStates[i] : true,
|
show: newGraphVisibilityStates ? newGraphVisibilityStates[i] : true,
|
||||||
label,
|
label,
|
||||||
stroke: color,
|
stroke: color,
|
||||||
|
width: 2,
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
points: {
|
points: {
|
||||||
show: false,
|
size: pointSize,
|
||||||
|
show: showPoints,
|
||||||
|
stroke: color,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
40
frontend/src/lib/uPlotLib/utils/getXAxisScale.ts
Normal file
40
frontend/src/lib/uPlotLib/utils/getXAxisScale.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
function getFallbackMinMaxTimeStamp(): {
|
||||||
|
fallbackMin: number;
|
||||||
|
fallbackMax: number;
|
||||||
|
} {
|
||||||
|
const currentDate = new Date();
|
||||||
|
// Get the Unix timestamp (milliseconds since January 1, 1970)
|
||||||
|
const currentTime = currentDate.getTime();
|
||||||
|
const currentUnixTimestamp = Math.floor(currentTime / 1000);
|
||||||
|
|
||||||
|
// Calculate the date and time one day ago
|
||||||
|
const oneDayAgoUnixTimestamp = Math.floor(
|
||||||
|
(currentDate.getTime() - 86400000) / 1000,
|
||||||
|
); // 86400000 milliseconds in a day
|
||||||
|
|
||||||
|
return {
|
||||||
|
fallbackMin: oneDayAgoUnixTimestamp,
|
||||||
|
fallbackMax: currentUnixTimestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getXAxisScale = (
|
||||||
|
minTimeScale: number,
|
||||||
|
maxTimeScale: number,
|
||||||
|
): {
|
||||||
|
time: boolean;
|
||||||
|
auto: boolean;
|
||||||
|
range?: [number, number];
|
||||||
|
} => {
|
||||||
|
let minTime = minTimeScale;
|
||||||
|
let maxTime = maxTimeScale;
|
||||||
|
|
||||||
|
if (!minTimeScale || !maxTimeScale) {
|
||||||
|
const { fallbackMin, fallbackMax } = getFallbackMinMaxTimeStamp();
|
||||||
|
|
||||||
|
minTime = fallbackMin;
|
||||||
|
maxTime = fallbackMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { time: true, auto: false, range: [minTime, maxTime] };
|
||||||
|
};
|
@ -54,7 +54,12 @@ function getRange(
|
|||||||
const [minSeriesValue, maxSeriesValue] = findMinMaxValues(series);
|
const [minSeriesValue, maxSeriesValue] = findMinMaxValues(series);
|
||||||
|
|
||||||
const min = Math.min(minThresholdValue, minSeriesValue);
|
const min = Math.min(minThresholdValue, minSeriesValue);
|
||||||
const max = Math.max(maxThresholdValue, maxSeriesValue);
|
let max = Math.max(maxThresholdValue, maxSeriesValue);
|
||||||
|
|
||||||
|
// this is a temp change, we need to figure out a generic way to better handle ranges based on yAxisUnit
|
||||||
|
if (yAxisUnit === 'percentunit' && max < 1) {
|
||||||
|
max = 1;
|
||||||
|
}
|
||||||
|
|
||||||
return [min, max];
|
return [min, max];
|
||||||
}
|
}
|
||||||
|
36
frontend/src/utils/getTimeRange.ts
Normal file
36
frontend/src/utils/getTimeRange.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import store from 'store';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
MetricRangePayloadProps,
|
||||||
|
QueryRangePayload,
|
||||||
|
} from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
|
export const getTimeRange = (
|
||||||
|
widgetQueryRange?: UseQueryResult<
|
||||||
|
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||||
|
Error
|
||||||
|
>,
|
||||||
|
): Record<string, number> => {
|
||||||
|
const widgetParams =
|
||||||
|
(widgetQueryRange?.data?.params as QueryRangePayload) || null;
|
||||||
|
|
||||||
|
if (widgetParams && widgetParams?.start && widgetParams?.end) {
|
||||||
|
return {
|
||||||
|
startTime: widgetParams.start / 1000,
|
||||||
|
endTime: widgetParams.end / 1000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { globalTime } = store.getState();
|
||||||
|
|
||||||
|
const { start: globalStartTime, end: globalEndTime } = getStartEndRangeTime({
|
||||||
|
type: 'GLOBAL_TIME',
|
||||||
|
interval: globalTime.selectedTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
startTime: (parseInt(globalStartTime, 10) * 1e3) / 1000,
|
||||||
|
endTime: (parseInt(globalEndTime, 10) * 1e3) / 1000,
|
||||||
|
};
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user