mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 13:46:00 +08:00
feat: research on data limit for signoz dashboard perf
This commit is contained in:
parent
f9cb9f10be
commit
520a3f93c6
@ -0,0 +1,85 @@
|
||||
.custom-data-controls {
|
||||
margin: 8px 0;
|
||||
|
||||
.custom-data-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.custom-data-inputs {
|
||||
margin-top: 8px;
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.input-label {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.global-custom-data-controls {
|
||||
background-color: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.ant-card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&.dark-mode {
|
||||
background-color: #141414;
|
||||
border: 1px solid #303030;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&.inline-layout {
|
||||
.custom-data-inline-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
.title-switch-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 180px;
|
||||
|
||||
.ant-typography {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.inputs-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
|
||||
.input-label {
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce vertical padding for the card
|
||||
.ant-card-body {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import './CustomDataControls.styles.scss';
|
||||
|
||||
import { Card, InputNumber, Switch, Typography } from 'antd';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
interface CustomDataControlsProps {
|
||||
widget: Widgets;
|
||||
onUpdate: (updatedWidget: Partial<Widgets>) => void;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function CustomDataControls({
|
||||
widget,
|
||||
onUpdate,
|
||||
}: CustomDataControlsProps): JSX.Element {
|
||||
const handleCustomDataModeChange = (checked: boolean): void => {
|
||||
onUpdate({
|
||||
customDataMode: checked,
|
||||
customXData: checked ? widget.customXData || 15 : undefined,
|
||||
customYData: checked ? widget.customYData || 4 : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDataPointsChange = (value: number | null): void => {
|
||||
if (value !== null && value > 0) {
|
||||
onUpdate({ customXData: value });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSeriesCountChange = (value: number | null): void => {
|
||||
if (value !== null && value > 0) {
|
||||
onUpdate({ customYData: value });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="custom-data-controls" size="small">
|
||||
<div className="custom-data-header">
|
||||
<Text strong>Custom Data Generator</Text>
|
||||
<Switch
|
||||
checked={widget.customDataMode || false}
|
||||
onChange={handleCustomDataModeChange}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{widget.customDataMode && (
|
||||
<div className="custom-data-inputs">
|
||||
<div className="input-group">
|
||||
<Text className="input-label">Data Points (X):</Text>
|
||||
<InputNumber
|
||||
value={widget.customXData || 15}
|
||||
onChange={handleDataPointsChange}
|
||||
size="small"
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<Text className="input-label">Series Count (Y):</Text>
|
||||
<InputNumber
|
||||
value={widget.customYData || 4}
|
||||
onChange={handleSeriesCountChange}
|
||||
size="small"
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomDataControls;
|
@ -0,0 +1,91 @@
|
||||
import './CustomDataControls.styles.scss';
|
||||
|
||||
import { Card, InputNumber, Switch, Typography } from 'antd';
|
||||
|
||||
interface GlobalCustomDataControlsProps {
|
||||
customDataMode: boolean;
|
||||
setCustomDataMode: (value: boolean) => void;
|
||||
customXData: number;
|
||||
setCustomXData: (value: number) => void;
|
||||
customYData: number;
|
||||
setCustomYData: (value: number) => void;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function GlobalCustomDataControls({
|
||||
customDataMode,
|
||||
setCustomDataMode,
|
||||
customXData,
|
||||
setCustomXData,
|
||||
customYData,
|
||||
setCustomYData,
|
||||
}: GlobalCustomDataControlsProps): JSX.Element {
|
||||
const handleCustomDataModeChange = (checked: boolean): void => {
|
||||
setCustomDataMode(checked);
|
||||
|
||||
// Set default values if not already set
|
||||
if (checked) {
|
||||
if (!customXData) setCustomXData(15);
|
||||
if (!customYData) setCustomYData(4);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDataPointsChange = (value: number | null): void => {
|
||||
if (value !== null && value > 0) {
|
||||
setCustomXData(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSeriesCountChange = (value: number | null): void => {
|
||||
if (value !== null && value > 0) {
|
||||
setCustomYData(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="custom-data-controls global-custom-data-controls"
|
||||
size="small"
|
||||
>
|
||||
<div className="custom-data-header">
|
||||
<Text strong>Global Custom Data Generator</Text>
|
||||
<Switch
|
||||
checked={customDataMode}
|
||||
onChange={handleCustomDataModeChange}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{customDataMode && (
|
||||
<div className="custom-data-inputs">
|
||||
<div className="input-group">
|
||||
<Text className="input-label">Data Points (X):</Text>
|
||||
<InputNumber
|
||||
value={customXData || 15}
|
||||
onChange={handleDataPointsChange}
|
||||
size="small"
|
||||
style={{ width: 80 }}
|
||||
min={1}
|
||||
max={1000}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<Text className="input-label">Series Count (Y):</Text>
|
||||
<InputNumber
|
||||
value={customYData || 4}
|
||||
onChange={handleSeriesCountChange}
|
||||
size="small"
|
||||
style={{ width: 80 }}
|
||||
min={1}
|
||||
max={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default GlobalCustomDataControls;
|
@ -0,0 +1,88 @@
|
||||
import './CustomDataControls.styles.scss';
|
||||
|
||||
import { Card, InputNumber, Switch, Typography } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function GlobalCustomDataControlsHeader(): JSX.Element {
|
||||
const {
|
||||
globalCustomDataMode,
|
||||
setGlobalCustomDataMode,
|
||||
globalCustomXData,
|
||||
setGlobalCustomXData,
|
||||
globalCustomYData,
|
||||
setGlobalCustomYData,
|
||||
} = useDashboard();
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const handleCustomDataModeChange = (checked: boolean): void => {
|
||||
setGlobalCustomDataMode(checked);
|
||||
|
||||
// Set default values if not already set
|
||||
if (checked) {
|
||||
if (!globalCustomXData) setGlobalCustomXData(15);
|
||||
if (!globalCustomYData) setGlobalCustomYData(4);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDataPointsChange = (value: number | null): void => {
|
||||
if (value !== null && value > 0) {
|
||||
setGlobalCustomXData(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSeriesCountChange = (value: number | null): void => {
|
||||
if (value !== null && value > 0) {
|
||||
setGlobalCustomYData(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`custom-data-controls global-custom-data-controls inline-layout ${
|
||||
isDarkMode ? 'dark-mode' : ''
|
||||
}`}
|
||||
size="small"
|
||||
>
|
||||
<div className="custom-data-inline-container">
|
||||
<div className="title-switch-group">
|
||||
<Text strong>Custom Data Generator</Text>
|
||||
<Switch
|
||||
checked={globalCustomDataMode}
|
||||
onChange={handleCustomDataModeChange}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{globalCustomDataMode && (
|
||||
<div className="inputs-container">
|
||||
<div className="input-group">
|
||||
<Text className="input-label">Points (X):</Text>
|
||||
<InputNumber
|
||||
value={globalCustomXData || 15}
|
||||
onChange={handleDataPointsChange}
|
||||
size="small"
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<Text className="input-label">Series (Y):</Text>
|
||||
<InputNumber
|
||||
value={globalCustomYData || 4}
|
||||
onChange={handleSeriesCountChange}
|
||||
size="small"
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default GlobalCustomDataControlsHeader;
|
@ -121,6 +121,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.global-custom-data-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dashboard-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import GlobalCustomDataControlsHeader from 'components/CustomDataControls/GlobalCustomDataControlsHeader';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@ -478,6 +479,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<section className="global-custom-data-section">
|
||||
<GlobalCustomDataControlsHeader />
|
||||
</section>
|
||||
{(tags?.length || 0) > 0 && (
|
||||
<div className="dashboard-tags">
|
||||
{tags?.map((tag) => (
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
Switch,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import CustomDataControls from 'components/CustomDataControls/CustomDataControls';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
||||
import GraphTypes, {
|
||||
@ -113,6 +114,12 @@ function RightContainer({
|
||||
customLegendColors,
|
||||
setCustomLegendColors,
|
||||
queryResponse,
|
||||
customDataMode,
|
||||
setCustomDataMode,
|
||||
customXData,
|
||||
setCustomXData,
|
||||
customYData,
|
||||
setCustomYData,
|
||||
}: RightContainerProps): JSX.Element {
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const [inputValue, setInputValue] = useState(title);
|
||||
@ -280,6 +287,30 @@ function RightContainer({
|
||||
rootClassName="description-input"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* Custom Data Controls */}
|
||||
{selectedWidget && (
|
||||
<CustomDataControls
|
||||
widget={{
|
||||
...selectedWidget,
|
||||
customDataMode,
|
||||
customXData,
|
||||
customYData,
|
||||
}}
|
||||
onUpdate={(updatedWidget: Partial<Widgets>): void => {
|
||||
if (updatedWidget.customDataMode !== undefined) {
|
||||
setCustomDataMode(updatedWidget.customDataMode);
|
||||
}
|
||||
if (updatedWidget.customXData !== undefined) {
|
||||
setCustomXData(updatedWidget.customXData);
|
||||
}
|
||||
if (updatedWidget.customYData !== undefined) {
|
||||
setCustomYData(updatedWidget.customYData);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<section className="panel-config">
|
||||
<Typography.Text className="typography">Panel Type</Typography.Text>
|
||||
<Select
|
||||
@ -554,6 +585,12 @@ interface RightContainerProps {
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
customDataMode: boolean;
|
||||
setCustomDataMode: Dispatch<SetStateAction<boolean>>;
|
||||
customXData: number;
|
||||
setCustomXData: Dispatch<SetStateAction<number>>;
|
||||
customYData: number;
|
||||
setCustomYData: Dispatch<SetStateAction<number>>;
|
||||
}
|
||||
|
||||
RightContainer.defaultProps = {
|
||||
|
@ -239,6 +239,17 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedWidget?.columnUnits || {},
|
||||
);
|
||||
|
||||
// Custom data generator state
|
||||
const [customDataMode, setCustomDataMode] = useState<boolean>(
|
||||
selectedWidget?.customDataMode || false,
|
||||
);
|
||||
const [customXData, setCustomXData] = useState<number>(
|
||||
selectedWidget?.customXData || 15,
|
||||
);
|
||||
const [customYData, setCustomYData] = useState<number>(
|
||||
selectedWidget?.customYData || 4,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedWidget((prev) => {
|
||||
if (!prev) {
|
||||
@ -268,6 +279,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
legendPosition,
|
||||
customLegendColors,
|
||||
columnWidths: columnWidths?.[selectedWidget?.id],
|
||||
customDataMode,
|
||||
customXData,
|
||||
customYData,
|
||||
};
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -294,6 +308,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
legendPosition,
|
||||
customLegendColors,
|
||||
columnWidths,
|
||||
customDataMode,
|
||||
customXData,
|
||||
customYData,
|
||||
]);
|
||||
|
||||
const closeModal = (): void => {
|
||||
@ -503,6 +520,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
||||
legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
||||
customLegendColors: selectedWidget?.customLegendColors || {},
|
||||
customDataMode: selectedWidget?.customDataMode || false,
|
||||
customXData: selectedWidget?.customXData || 15,
|
||||
customYData: selectedWidget?.customYData || 4,
|
||||
},
|
||||
]
|
||||
: [
|
||||
@ -532,6 +552,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
||||
legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
||||
customLegendColors: selectedWidget?.customLegendColors || {},
|
||||
customDataMode: selectedWidget?.customDataMode || false,
|
||||
customXData: selectedWidget?.customXData || 15,
|
||||
customYData: selectedWidget?.customYData || 4,
|
||||
},
|
||||
...afterWidgets,
|
||||
],
|
||||
@ -796,6 +819,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
setSoftMin={setSoftMin}
|
||||
softMax={softMax}
|
||||
setSoftMax={setSoftMax}
|
||||
customDataMode={customDataMode}
|
||||
setCustomDataMode={setCustomDataMode}
|
||||
customXData={customXData}
|
||||
setCustomXData={setCustomXData}
|
||||
customYData={customYData}
|
||||
setCustomYData={setCustomYData}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</RightContainerWrapper>
|
||||
|
@ -10,12 +10,19 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import {
|
||||
getCustomApiResponse,
|
||||
getCustomChartData,
|
||||
} from 'lib/uPlotLib/utils/getCustomChartData';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { cloneDeep, isEqual, isUndefined } from 'lodash-es';
|
||||
import _noop from 'lodash-es/noop';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import uPlot from 'uplot';
|
||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
@ -35,7 +42,14 @@ function UplotPanelWrapper({
|
||||
customTooltipElement,
|
||||
customSeries,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const {
|
||||
toScrollWidgetId,
|
||||
setToScrollWidgetId,
|
||||
globalCustomDataMode,
|
||||
globalCustomXData,
|
||||
globalCustomYData,
|
||||
} = useDashboard();
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
@ -43,6 +57,11 @@ function UplotPanelWrapper({
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
// Get global time for custom data generation
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [hiddenGraph, setHiddenGraph] = useState<{ [key: string]: boolean }>();
|
||||
|
||||
useEffect(() => {
|
||||
@ -56,12 +75,64 @@ function UplotPanelWrapper({
|
||||
}
|
||||
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
|
||||
|
||||
// Create custom response when custom data mode is enabled (either via widget or global setting)
|
||||
const effectiveQueryResponse = useMemo(() => {
|
||||
// If global custom data mode is enabled, it should override widget settings
|
||||
const isCustomDataEnabled = globalCustomDataMode || widget.customDataMode;
|
||||
|
||||
// When global custom data is enabled, use global values regardless of widget settings
|
||||
let xData = null;
|
||||
let yData = null;
|
||||
|
||||
if (globalCustomDataMode) {
|
||||
// Global settings override widget settings
|
||||
xData = globalCustomXData;
|
||||
yData = globalCustomYData;
|
||||
} else if (widget.customDataMode) {
|
||||
// Only use widget settings if global is not enabled
|
||||
xData = widget.customXData;
|
||||
yData = widget.customYData;
|
||||
}
|
||||
|
||||
if (isCustomDataEnabled && xData && yData) {
|
||||
// Convert nanoseconds to seconds for custom data generation
|
||||
const startTimeSeconds = Math.floor(minTime / 1000000000);
|
||||
const endTimeSeconds = Math.floor(maxTime / 1000000000);
|
||||
|
||||
const customResponse = getCustomApiResponse(
|
||||
xData,
|
||||
yData,
|
||||
startTimeSeconds,
|
||||
endTimeSeconds,
|
||||
);
|
||||
// Return a properly structured response that matches the expected type
|
||||
return {
|
||||
...queryResponse,
|
||||
data: customResponse,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
} as typeof queryResponse;
|
||||
}
|
||||
return queryResponse;
|
||||
}, [
|
||||
queryResponse,
|
||||
widget.customDataMode,
|
||||
widget.customXData,
|
||||
widget.customYData,
|
||||
globalCustomDataMode,
|
||||
globalCustomXData,
|
||||
globalCustomYData,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
const { startTime, endTime } = getTimeRange(effectiveQueryResponse);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [queryResponse]);
|
||||
}, [effectiveQueryResponse]);
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
@ -69,28 +140,71 @@ function UplotPanelWrapper({
|
||||
const {
|
||||
graphVisibilityStates: localStoredVisibilityState,
|
||||
} = getLocalStorageGraphVisibilityState({
|
||||
apiResponse: queryResponse.data?.payload.data.result || [],
|
||||
apiResponse: effectiveQueryResponse.data?.payload.data.result || [],
|
||||
name: widget.id,
|
||||
});
|
||||
if (setGraphVisibility) {
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
}, [queryResponse.data?.payload.data.result, setGraphVisibility, widget.id]);
|
||||
}, [
|
||||
effectiveQueryResponse.data?.payload.data.result,
|
||||
setGraphVisibility,
|
||||
widget.id,
|
||||
]);
|
||||
|
||||
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
|
||||
if (effectiveQueryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
|
||||
const sortedSeriesData = getSortedSeriesData(
|
||||
queryResponse.data?.payload.data.result,
|
||||
effectiveQueryResponse.data?.payload.data.result,
|
||||
);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||
effectiveQueryResponse.data.payload.data.result = sortedSeriesData;
|
||||
}
|
||||
|
||||
const chartData = getUPlotChartData(
|
||||
queryResponse?.data?.payload,
|
||||
const chartData = useMemo(() => {
|
||||
// If global custom data mode is enabled, it should override widget settings
|
||||
const isCustomDataEnabled = globalCustomDataMode || widget.customDataMode;
|
||||
|
||||
// When global custom data is enabled, use global values regardless of widget settings
|
||||
let xData = null;
|
||||
let yData = null;
|
||||
|
||||
if (globalCustomDataMode) {
|
||||
// Global settings override widget settings
|
||||
xData = globalCustomXData;
|
||||
yData = globalCustomYData;
|
||||
} else if (widget.customDataMode) {
|
||||
// Only use widget settings if global is not enabled
|
||||
xData = widget.customXData;
|
||||
yData = widget.customYData;
|
||||
}
|
||||
|
||||
if (isCustomDataEnabled && xData && yData) {
|
||||
// Convert nanoseconds to seconds for custom data generation
|
||||
const startTimeSeconds = Math.floor(minTime / 1000000000);
|
||||
const endTimeSeconds = Math.floor(maxTime / 1000000000);
|
||||
|
||||
return getCustomChartData(xData, yData, startTimeSeconds, endTimeSeconds);
|
||||
}
|
||||
return getUPlotChartData(
|
||||
effectiveQueryResponse?.data?.payload,
|
||||
widget.fillSpans,
|
||||
widget?.stackedBarChart,
|
||||
hiddenGraph,
|
||||
);
|
||||
}, [
|
||||
widget.customDataMode,
|
||||
widget.customXData,
|
||||
widget.customYData,
|
||||
globalCustomDataMode,
|
||||
globalCustomXData,
|
||||
globalCustomYData,
|
||||
minTime,
|
||||
maxTime,
|
||||
effectiveQueryResponse?.data?.payload,
|
||||
widget.fillSpans,
|
||||
widget?.stackedBarChart,
|
||||
hiddenGraph,
|
||||
);
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (widget.panelTypes === PANEL_TYPES.BAR && widget?.stackedBarChart) {
|
||||
@ -110,16 +224,22 @@ function UplotPanelWrapper({
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
// Standard click handler without performance monitoring
|
||||
const enhancedClickHandler = useMemo(() => {
|
||||
if (!onClickHandler) return _noop;
|
||||
return onClickHandler;
|
||||
}, [onClickHandler]);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
id: widget?.id,
|
||||
apiResponse: queryResponse.data?.payload,
|
||||
apiResponse: effectiveQueryResponse.data?.payload,
|
||||
dimensions: containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
yAxisUnit: widget?.yAxisUnit,
|
||||
onClickHandler: onClickHandler || _noop,
|
||||
onClickHandler: enhancedClickHandler,
|
||||
thresholds: widget.thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
@ -150,11 +270,11 @@ function UplotPanelWrapper({
|
||||
widget.softMin,
|
||||
widget.panelTypes,
|
||||
widget?.stackedBarChart,
|
||||
queryResponse.data?.payload,
|
||||
effectiveQueryResponse.data?.payload,
|
||||
containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
onClickHandler,
|
||||
enhancedClickHandler,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
graphVisibility,
|
||||
@ -171,6 +291,14 @@ function UplotPanelWrapper({
|
||||
],
|
||||
);
|
||||
|
||||
console.log(
|
||||
'chartData',
|
||||
'x-axis',
|
||||
chartData[0].length,
|
||||
'y-axis',
|
||||
chartData.length,
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
||||
<Uplot options={options} data={chartData} ref={lineChartRef} />
|
||||
@ -183,7 +311,10 @@ function UplotPanelWrapper({
|
||||
)}
|
||||
{isFullViewMode && setGraphVisibility && !widget?.stackedBarChart && (
|
||||
<GraphManager
|
||||
data={getUPlotChartData(queryResponse?.data?.payload, widget.fillSpans)}
|
||||
data={getUPlotChartData(
|
||||
effectiveQueryResponse?.data?.payload,
|
||||
widget.fillSpans,
|
||||
)}
|
||||
name={widget.id}
|
||||
options={options}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
|
@ -431,9 +431,10 @@ export const getUPlotChartOptions = ({
|
||||
// Add single global cleanup listener for this chart
|
||||
const globalCleanupHandler = (e: MouseEvent): void => {
|
||||
const target = e.target as HTMLElement;
|
||||
console.log('target', target);
|
||||
if (
|
||||
!target.closest('.u-legend') &&
|
||||
!target.classList.contains('legend-tooltip')
|
||||
!target?.closest?.('.u-legend') &&
|
||||
!target?.classList?.contains('legend-tooltip')
|
||||
) {
|
||||
cleanupAllTooltips();
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQuery, UseQueryResult } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { Dispatch } from 'redux';
|
||||
import { Dispatch as ReduxDispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
@ -51,7 +51,7 @@ const DashboardContext = createContext<IDashboardContext>({
|
||||
isDashboardSliderOpen: false,
|
||||
isDashboardLocked: false,
|
||||
handleToggleDashboardSlider: () => {},
|
||||
handleDashboardLockToggle: () => {},
|
||||
handleDashboardLockToggle: async () => {},
|
||||
dashboardResponse: {} as UseQueryResult<Dashboard, unknown>,
|
||||
selectedDashboard: {} as Dashboard,
|
||||
dashboardId: '',
|
||||
@ -80,6 +80,13 @@ const DashboardContext = createContext<IDashboardContext>({
|
||||
isDashboardFetching: false,
|
||||
columnWidths: {},
|
||||
setColumnWidths: () => {},
|
||||
// Global custom data state
|
||||
globalCustomDataMode: false,
|
||||
setGlobalCustomDataMode: () => {},
|
||||
globalCustomXData: 15,
|
||||
setGlobalCustomXData: () => {},
|
||||
globalCustomYData: 4,
|
||||
setGlobalCustomYData: () => {},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
@ -146,7 +153,7 @@ export function DashboardProvider({
|
||||
|
||||
function setListSortOrder(sortOrder: DashboardSortOrder): void {
|
||||
if (!isEqual(sortOrder, listSortOrder)) {
|
||||
setListOrder(sortOrder);
|
||||
setListOrder(sortOrder as any);
|
||||
}
|
||||
params.set('columnKey', sortOrder.columnKey as string);
|
||||
params.set('order', sortOrder.order as string);
|
||||
@ -155,7 +162,7 @@ export function DashboardProvider({
|
||||
safeNavigate({ search: params.toString() });
|
||||
}
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const dispatch = useDispatch<ReduxDispatch<AppActions>>();
|
||||
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
@ -416,6 +423,13 @@ export function DashboardProvider({
|
||||
|
||||
const [columnWidths, setColumnWidths] = useState<WidgetColumnWidths>({});
|
||||
|
||||
// Global custom data state
|
||||
const [globalCustomDataMode, setGlobalCustomDataMode] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const [globalCustomXData, setGlobalCustomXData] = useState<number>(15);
|
||||
const [globalCustomYData, setGlobalCustomYData] = useState<number>(4);
|
||||
|
||||
const value: IDashboardContext = useMemo(
|
||||
() => ({
|
||||
toScrollWidgetId,
|
||||
@ -424,7 +438,7 @@ export function DashboardProvider({
|
||||
handleToggleDashboardSlider,
|
||||
handleDashboardLockToggle,
|
||||
dashboardResponse,
|
||||
selectedDashboard,
|
||||
selectedDashboard: selectedDashboard as Dashboard,
|
||||
dashboardId,
|
||||
layouts,
|
||||
listSortOrder,
|
||||
@ -445,6 +459,13 @@ export function DashboardProvider({
|
||||
isDashboardFetching,
|
||||
columnWidths,
|
||||
setColumnWidths,
|
||||
// Global custom data state
|
||||
globalCustomDataMode,
|
||||
setGlobalCustomDataMode,
|
||||
globalCustomXData,
|
||||
setGlobalCustomXData,
|
||||
globalCustomYData,
|
||||
setGlobalCustomYData,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
@ -469,6 +490,10 @@ export function DashboardProvider({
|
||||
isDashboardFetching,
|
||||
columnWidths,
|
||||
setColumnWidths,
|
||||
// Global custom data state
|
||||
globalCustomDataMode,
|
||||
globalCustomXData,
|
||||
globalCustomYData,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -1,39 +1,40 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface DashboardSortOrder {
|
||||
columnKey: string;
|
||||
order: string;
|
||||
pagination: string;
|
||||
search: string;
|
||||
columnKey?: string | null;
|
||||
order?: string | null;
|
||||
pagination?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export type WidgetColumnWidths = {
|
||||
[widgetId: string]: Record<string, number>;
|
||||
};
|
||||
export interface WidgetColumnWidths {
|
||||
[key: string]: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface IDashboardContext {
|
||||
isDashboardSliderOpen: boolean;
|
||||
isDashboardLocked: boolean;
|
||||
handleToggleDashboardSlider: (value: boolean) => void;
|
||||
handleDashboardLockToggle: (value: boolean) => void;
|
||||
handleDashboardLockToggle: (value: boolean) => Promise<void>;
|
||||
dashboardResponse: UseQueryResult<Dashboard, unknown>;
|
||||
selectedDashboard: Dashboard | undefined;
|
||||
selectedDashboard: Dashboard;
|
||||
dashboardId: string;
|
||||
layouts: Layout[];
|
||||
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
|
||||
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||
listSortOrder: DashboardSortOrder;
|
||||
setListSortOrder: (sortOrder: DashboardSortOrder) => void;
|
||||
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
|
||||
setSelectedDashboard: React.Dispatch<
|
||||
React.SetStateAction<Dashboard | undefined>
|
||||
setPanelMap: Dispatch<
|
||||
SetStateAction<Record<string, { widgets: Layout[]; collapsed: boolean }>>
|
||||
>;
|
||||
updatedTimeRef: React.MutableRefObject<dayjs.Dayjs | null>;
|
||||
listSortOrder: DashboardSortOrder;
|
||||
setListSortOrder: (value: DashboardSortOrder) => void;
|
||||
setLayouts: Dispatch<SetStateAction<Layout[]>>;
|
||||
setSelectedDashboard: Dispatch<SetStateAction<Dashboard | undefined>>;
|
||||
updatedTimeRef: React.MutableRefObject<Dayjs | null>;
|
||||
toScrollWidgetId: string;
|
||||
setToScrollWidgetId: React.Dispatch<React.SetStateAction<string>>;
|
||||
setToScrollWidgetId: Dispatch<SetStateAction<string>>;
|
||||
updateLocalStorageDashboardVariables: (
|
||||
id: string,
|
||||
selectedValue:
|
||||
@ -46,12 +47,19 @@ export interface IDashboardContext {
|
||||
allSelected: boolean,
|
||||
) => void;
|
||||
variablesToGetUpdated: string[];
|
||||
setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setVariablesToGetUpdated: Dispatch<SetStateAction<string[]>>;
|
||||
dashboardQueryRangeCalled: boolean;
|
||||
setDashboardQueryRangeCalled: (value: boolean) => void;
|
||||
setDashboardQueryRangeCalled: Dispatch<SetStateAction<boolean>>;
|
||||
selectedRowWidgetId: string | null;
|
||||
setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setSelectedRowWidgetId: Dispatch<SetStateAction<string | null>>;
|
||||
isDashboardFetching: boolean;
|
||||
columnWidths: WidgetColumnWidths;
|
||||
setColumnWidths: React.Dispatch<React.SetStateAction<WidgetColumnWidths>>;
|
||||
setColumnWidths: Dispatch<SetStateAction<WidgetColumnWidths>>;
|
||||
// Global custom data state
|
||||
globalCustomDataMode: boolean;
|
||||
setGlobalCustomDataMode: Dispatch<SetStateAction<boolean>>;
|
||||
globalCustomXData: number;
|
||||
setGlobalCustomXData: Dispatch<SetStateAction<number>>;
|
||||
globalCustomYData: number;
|
||||
setGlobalCustomYData: Dispatch<SetStateAction<number>>;
|
||||
}
|
||||
|
@ -118,6 +118,9 @@ export interface IBaseWidget {
|
||||
columnWidths?: Record<string, number>;
|
||||
legendPosition?: LegendPosition;
|
||||
customLegendColors?: Record<string, string>;
|
||||
customDataMode?: boolean;
|
||||
customXData?: number; // number of data points
|
||||
customYData?: number; // number of series
|
||||
}
|
||||
export interface Widgets extends IBaseWidget {
|
||||
query: Query;
|
||||
|
Loading…
x
Reference in New Issue
Block a user