mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-23 10:10:33 +01:00
Compare commits
7 Commits
boot-setti
...
chore/aler
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42e7f158ed | ||
|
|
35b2f5e216 | ||
|
|
ec7a10ecf9 | ||
|
|
eec3cbefc6 | ||
|
|
fcd0376125 | ||
|
|
b0c797b507 | ||
|
|
fe586228d3 |
@@ -94,15 +94,12 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
window.signozBootData = { settings: [[.BootSettings]] };
|
||||
</script>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script>
|
||||
var pylonAppId = (window.signozBootData?.settings?.pylon || {}).appId || '';
|
||||
if (pylonAppId) {
|
||||
var PYLON_APP_ID = '<%- PYLON_APP_ID %>';
|
||||
if (PYLON_APP_ID) {
|
||||
(function () {
|
||||
var e = window;
|
||||
var t = document;
|
||||
@@ -118,7 +115,10 @@
|
||||
var e = t.createElement('script');
|
||||
e.setAttribute('type', 'text/javascript');
|
||||
e.setAttribute('async', 'true');
|
||||
e.setAttribute('src', 'https://widget.usepylon.com/widget/' + pylonAppId);
|
||||
e.setAttribute(
|
||||
'src',
|
||||
'https://widget.usepylon.com/widget/' + PYLON_APP_ID,
|
||||
);
|
||||
var n = t.getElementsByTagName('script')[0];
|
||||
n.parentNode.insertBefore(e, n);
|
||||
};
|
||||
@@ -130,15 +130,16 @@
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
window.AppcuesSettings = { enableURLDetection: true };
|
||||
</script>
|
||||
<script>
|
||||
var appcuesAppId =
|
||||
(window.signozBootData?.settings?.appcues || {}).appId || '';
|
||||
if (appcuesAppId) {
|
||||
window.AppcuesSettings = { enableURLDetection: true };
|
||||
var APPCUES_APP_ID = '<%- APPCUES_APP_ID %>';
|
||||
if (APPCUES_APP_ID) {
|
||||
(function (d, t) {
|
||||
var a = d.createElement(t);
|
||||
a.async = 1;
|
||||
a.src = '//fast.appcues.com/' + appcuesAppId + '.js';
|
||||
a.src = '//fast.appcues.com/' + APPCUES_APP_ID + '.js';
|
||||
var s = d.getElementsByTagName(t)[0];
|
||||
s.parentNode.insertBefore(a, s);
|
||||
})(document, 'script');
|
||||
|
||||
@@ -35,7 +35,6 @@ import { PreferenceContextProvider } from 'providers/preferences/context/Prefere
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { extractDomain } from 'utils/app';
|
||||
import { bootSettings } from 'utils/bootData';
|
||||
|
||||
import { Home } from './pageComponents';
|
||||
import PrivateRoute from './Private';
|
||||
@@ -294,7 +293,7 @@ function App(): JSX.Element {
|
||||
(isCloudUser || isEnterpriseSelfHostedUser)
|
||||
) {
|
||||
const email = user.email || '';
|
||||
const secret = bootSettings.pylon.identSecret ?? '';
|
||||
const secret = process.env.PYLON_IDENTITY_SECRET || '';
|
||||
let emailHash = '';
|
||||
|
||||
if (email && secret) {
|
||||
@@ -303,7 +302,7 @@ function App(): JSX.Element {
|
||||
|
||||
window.pylon = {
|
||||
chat_settings: {
|
||||
app_id: bootSettings.pylon.appId,
|
||||
app_id: process.env.PYLON_APP_ID,
|
||||
email: user.email,
|
||||
name: user.displayName || user.email,
|
||||
email_hash: emailHash,
|
||||
@@ -333,8 +332,8 @@ function App(): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
if (isCloudUser || isEnterpriseSelfHostedUser) {
|
||||
if (bootSettings.posthog.key) {
|
||||
posthog.init(bootSettings.posthog.key, {
|
||||
if (process.env.POSTHOG_KEY) {
|
||||
posthog.init(process.env.POSTHOG_KEY, {
|
||||
api_host: 'https://us.i.posthog.com',
|
||||
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
|
||||
});
|
||||
@@ -342,8 +341,8 @@ function App(): JSX.Element {
|
||||
|
||||
if (!isSentryInitialized) {
|
||||
Sentry.init({
|
||||
dsn: bootSettings.sentry.dsn,
|
||||
tunnel: bootSettings.sentry.tunnelUrl,
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
tunnel: process.env.TUNNEL_URL,
|
||||
environment: 'production',
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
|
||||
@@ -91,7 +91,6 @@ function ChartPreview({
|
||||
const renderQBChartPreview = (): JSX.Element => (
|
||||
<ChartPreviewComponent
|
||||
headline={headline}
|
||||
name=""
|
||||
query={stagedQuery}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
alertDef={alertDef}
|
||||
@@ -107,7 +106,6 @@ function ChartPreview({
|
||||
const renderPromAndChQueryChartPreview = (): JSX.Element => (
|
||||
<ChartPreviewComponent
|
||||
headline={headline}
|
||||
name="Chart Preview"
|
||||
query={stagedQuery}
|
||||
alertDef={alertDef}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
|
||||
@@ -17,7 +17,6 @@ import { CreateAlertProvider } from '../../context';
|
||||
import ChartPreview from '../ChartPreview/ChartPreview';
|
||||
|
||||
const REQUESTS_PER_SEC = 'requests/sec';
|
||||
const CHART_PREVIEW_NAME = 'Chart Preview';
|
||||
const QUERY_TYPE_TEST_ID = 'query-type';
|
||||
const GRAPH_TYPE_TEST_ID = 'graph-type';
|
||||
const CHART_PREVIEW_COMPONENT_TEST_ID = 'chart-preview-component';
|
||||
@@ -34,7 +33,6 @@ jest.mock(
|
||||
return (
|
||||
<div data-testid={CHART_PREVIEW_COMPONENT_TEST_ID}>
|
||||
<div data-testid="headline">{props.headline}</div>
|
||||
<div data-testid="name">{props.name}</div>
|
||||
<div data-testid={QUERY_TYPE_TEST_ID}>{props.query?.queryType}</div>
|
||||
<div data-testid="selected-interval">
|
||||
{props.selectedInterval?.startTime}
|
||||
@@ -175,12 +173,6 @@ describe('ChartPreview', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders QueryBuilder chart preview with empty name when query type is QUERY_BUILDER', () => {
|
||||
renderChartPreview();
|
||||
|
||||
expect(screen.getByTestId('name')).toHaveTextContent('');
|
||||
});
|
||||
|
||||
it('renders QueryBuilder chart preview with correct props', () => {
|
||||
renderChartPreview();
|
||||
|
||||
@@ -191,7 +183,6 @@ describe('ChartPreview', () => {
|
||||
expect(screen.getByTestId(GRAPH_TYPE_TEST_ID)).toHaveTextContent(
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
);
|
||||
expect(screen.getByTestId('name')).toHaveTextContent('');
|
||||
expect(screen.getByTestId('headline')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('selected-interval')).toBeInTheDocument();
|
||||
});
|
||||
@@ -214,7 +205,6 @@ describe('ChartPreview', () => {
|
||||
expect(
|
||||
screen.getByTestId(CHART_PREVIEW_COMPONENT_TEST_ID),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('name')).toHaveTextContent(CHART_PREVIEW_NAME);
|
||||
expect(screen.getByTestId(QUERY_TYPE_TEST_ID)).toHaveTextContent(
|
||||
EQueryType.PROM,
|
||||
);
|
||||
@@ -238,7 +228,6 @@ describe('ChartPreview', () => {
|
||||
expect(
|
||||
screen.getByTestId(CHART_PREVIEW_COMPONENT_TEST_ID),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('name')).toHaveTextContent(CHART_PREVIEW_NAME);
|
||||
expect(screen.getByTestId(QUERY_TYPE_TEST_ID)).toHaveTextContent(
|
||||
EQueryType.CLICKHOUSE,
|
||||
);
|
||||
|
||||
@@ -17,10 +17,11 @@ import { getTimeRange } from 'utils/getTimeRange';
|
||||
import BarChart from '../../charts/BarChart/BarChart';
|
||||
import ChartManager from '../../components/ChartManager/ChartManager';
|
||||
import { usePanelContextMenu } from '../../hooks/usePanelContextMenu';
|
||||
import { prepareBarPanelConfig, prepareBarPanelData } from './utils';
|
||||
import { prepareBarPanelConfig } from './utils';
|
||||
|
||||
import '../Panel.styles.scss';
|
||||
import TooltipFooter from '../components/TooltipFooter';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const {
|
||||
@@ -99,7 +100,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
if (!queryResponse?.data?.payload) {
|
||||
return [];
|
||||
}
|
||||
return prepareBarPanelData(queryResponse?.data?.payload);
|
||||
return prepareChartData(queryResponse?.data?.payload);
|
||||
}, [queryResponse?.data?.payload]);
|
||||
|
||||
const layoutChildren = useMemo(() => {
|
||||
|
||||
@@ -11,21 +11,10 @@ import { get } from 'lodash-es';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { AlignedData } from 'uplot';
|
||||
|
||||
import { PanelMode } from '../types';
|
||||
import { fillMissingXAxisTimestamps, getXAxisTimestamps } from '../utils';
|
||||
import { buildBaseConfig } from '../utils/baseConfigBuilder';
|
||||
|
||||
export function prepareBarPanelData(
|
||||
apiResponse: MetricRangePayloadProps,
|
||||
): AlignedData {
|
||||
const seriesList = apiResponse?.data?.result || [];
|
||||
const timestampArr = getXAxisTimestamps(seriesList);
|
||||
const yAxisValuesArr = fillMissingXAxisTimestamps(timestampArr, seriesList);
|
||||
return [timestampArr, ...yAxisValuesArr];
|
||||
}
|
||||
|
||||
export function prepareBarPanelConfig({
|
||||
widget,
|
||||
isDarkMode,
|
||||
|
||||
@@ -17,10 +17,11 @@ import { useTimezone } from 'providers/Timezone';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { prepareChartData, prepareUPlotConfig } from '../TimeSeriesPanel/utils';
|
||||
import { prepareUPlotConfig } from '../TimeSeriesPanel/utils';
|
||||
|
||||
import '../Panel.styles.scss';
|
||||
import TooltipFooter from '../components/TooltipFooter';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const {
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { PanelMode } from '../../types';
|
||||
import { prepareChartData, prepareUPlotConfig } from '../utils';
|
||||
import { prepareUPlotConfig } from '../utils';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
fillMissingXAxisTimestamps,
|
||||
getXAxisTimestamps,
|
||||
} from 'container/DashboardContainer/visualization/panels/utils';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
@@ -15,42 +11,15 @@ import {
|
||||
LineStyle,
|
||||
} from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { isInvalidPlotValue } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import { hasSingleVisiblePoint } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import get from 'lodash-es/get';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
import { PanelMode } from '../types';
|
||||
import { buildBaseConfig } from '../utils/baseConfigBuilder';
|
||||
|
||||
export const prepareChartData = (
|
||||
apiResponse: MetricRangePayloadProps,
|
||||
): uPlot.AlignedData => {
|
||||
const seriesList = apiResponse?.data?.result || [];
|
||||
const timestampArr = getXAxisTimestamps(seriesList);
|
||||
const yAxisValuesArr = fillMissingXAxisTimestamps(timestampArr, seriesList);
|
||||
|
||||
return [timestampArr, ...yAxisValuesArr];
|
||||
};
|
||||
|
||||
function hasSingleVisiblePointForSeries(series: QueryData): boolean {
|
||||
const rawValues = series.values ?? [];
|
||||
let validPointCount = 0;
|
||||
|
||||
for (const [, rawValue] of rawValues) {
|
||||
if (!isInvalidPlotValue(rawValue)) {
|
||||
validPointCount += 1;
|
||||
if (validPointCount > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const prepareUPlotConfig = ({
|
||||
widget,
|
||||
isDarkMode,
|
||||
@@ -107,7 +76,7 @@ export const prepareUPlotConfig = ({
|
||||
}
|
||||
|
||||
apiResponse.data.result.forEach((series) => {
|
||||
const hasSingleValidPoint = hasSingleVisiblePointForSeries(series);
|
||||
const hasSingleValidPoint = hasSingleVisiblePoint(series.values);
|
||||
const baseLabelName = getLabelName(
|
||||
series.metric,
|
||||
series.queryName || '', // query
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import {
|
||||
AlertChartPanelType,
|
||||
buildAlertChartConfig,
|
||||
buildChartId,
|
||||
} from './utils';
|
||||
|
||||
// Panel types that render through the UPlotConfigBuilder pipeline.
|
||||
// To support a new modern-chart panel type, add an entry here and extend
|
||||
// `AlertChartPanelType` / `buildAlertChartConfig` to handle its series setup.
|
||||
const SUPPORTED_CHARTS: Record<
|
||||
AlertChartPanelType,
|
||||
typeof TimeSeries | typeof BarChart
|
||||
> = {
|
||||
[PANEL_TYPES.TIME_SERIES]: TimeSeries,
|
||||
[PANEL_TYPES.BAR]: BarChart,
|
||||
};
|
||||
|
||||
const isSupportedPanelType = (
|
||||
panelType: PANEL_TYPES,
|
||||
): panelType is AlertChartPanelType => panelType in SUPPORTED_CHARTS;
|
||||
|
||||
export interface ChartContentProps {
|
||||
panelType: PANEL_TYPES;
|
||||
alertId?: string;
|
||||
query: Query;
|
||||
apiResponse?: MetricRangePayloadProps;
|
||||
data: uPlot.AlignedData;
|
||||
thresholds: ThresholdProps[];
|
||||
yAxisUnit: string;
|
||||
legendPosition: LegendPosition;
|
||||
isDarkMode: boolean;
|
||||
timezone: Timezone;
|
||||
width: number;
|
||||
height: number;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
}
|
||||
|
||||
export default function ChartContent({
|
||||
panelType,
|
||||
alertId,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
data,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
width,
|
||||
height,
|
||||
legendPosition,
|
||||
}: ChartContentProps): JSX.Element | null {
|
||||
const supported = isSupportedPanelType(panelType);
|
||||
|
||||
const config = useMemo(
|
||||
() =>
|
||||
buildAlertChartConfig({
|
||||
id: buildChartId(alertId),
|
||||
panelType: panelType as AlertChartPanelType,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
}),
|
||||
[
|
||||
alertId,
|
||||
panelType,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
],
|
||||
);
|
||||
|
||||
if (!supported) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Component = SUPPORTED_CHARTS[panelType];
|
||||
|
||||
return (
|
||||
<Component
|
||||
config={config}
|
||||
data={data}
|
||||
width={width}
|
||||
height={height}
|
||||
legendConfig={{ position: legendPosition }}
|
||||
canPinTooltip
|
||||
yAxisUnit={yAxisUnit}
|
||||
timezone={timezone}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -15,8 +15,6 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||
import { INITIAL_CRITICAL_THRESHOLD } from 'container/CreateAlertV2/context/constants';
|
||||
import { Threshold } from 'container/CreateAlertV2/context/types';
|
||||
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
@@ -32,8 +30,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import history from 'lib/history';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -41,24 +38,27 @@ import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
import { LegendPosition } from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import uPlot from 'uplot';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { AlertDetectionTypes } from '..';
|
||||
import ChartContent from './ChartContent';
|
||||
import { ChartContainer } from './styles';
|
||||
import { getThresholds } from './utils';
|
||||
|
||||
import './ChartPreview.styles.scss';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
// Height reserved for the `.chart-preview-header` strip rendered above the chart.
|
||||
const CHART_PREVIEW_HEADER_HEIGHT = 48;
|
||||
const CHART_PREVIEW_CONTAINER_PADDING = 16;
|
||||
|
||||
export interface ChartPreviewProps {
|
||||
name: string;
|
||||
query: Query | null;
|
||||
graphType?: PANEL_TYPES;
|
||||
selectedTime?: timePreferenceType;
|
||||
@@ -77,7 +77,6 @@ export interface ChartPreviewProps {
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function ChartPreview({
|
||||
name,
|
||||
query,
|
||||
graphType = PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime = 'GLOBAL_TIME',
|
||||
@@ -113,14 +112,6 @@ function ChartPreview({
|
||||
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
|
||||
const legendScrollPositionRef = useRef<{
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}>({
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
});
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const {
|
||||
@@ -219,18 +210,6 @@ function ChartPreview({
|
||||
setMaxTimeScale(endTime);
|
||||
}, [maxTime, minTime, globalSelectedInterval, queryResponse, setQueryStatus]);
|
||||
|
||||
// Initialize graph visibility from localStorage
|
||||
useEffect(() => {
|
||||
if (queryResponse?.data?.payload?.data?.result) {
|
||||
const { graphVisibilityStates: localStoredVisibilityState } =
|
||||
getLocalStorageGraphVisibilityState({
|
||||
apiResponse: queryResponse.data.payload.data.result,
|
||||
name: 'alert-chart-preview',
|
||||
});
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
}, [queryResponse?.data?.payload?.data?.result]);
|
||||
|
||||
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
|
||||
const sortedSeriesData = getSortedSeriesData(
|
||||
queryResponse.data?.payload.data.result,
|
||||
@@ -288,62 +267,17 @@ function ChartPreview({
|
||||
return LegendPosition.RIGHT;
|
||||
}, [queryResponse?.data?.payload?.data?.result?.length, showSideLegend]);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
id: 'alert_legend_widget',
|
||||
yAxisUnit,
|
||||
apiResponse: queryResponse?.data?.payload,
|
||||
dimensions: {
|
||||
height: containerDimensions?.height ? containerDimensions.height - 48 : 0,
|
||||
width: containerDimensions?.width,
|
||||
},
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
thresholds: getThresholds(thresholds, t, optionName, yAxisUnit),
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
panelType: graphType,
|
||||
tzDate: (timestamp: number) =>
|
||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||
timezone: timezone.value,
|
||||
currentQuery,
|
||||
query: query || currentQuery,
|
||||
graphsVisibilityStates: graphVisibility,
|
||||
setGraphsVisibilityStates: setGraphVisibility,
|
||||
enhancedLegend: true,
|
||||
legendPosition,
|
||||
legendScrollPosition: legendScrollPositionRef.current,
|
||||
setLegendScrollPosition: (position: {
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}) => {
|
||||
legendScrollPositionRef.current = position;
|
||||
},
|
||||
}),
|
||||
[
|
||||
yAxisUnit,
|
||||
queryResponse?.data?.payload,
|
||||
containerDimensions,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
thresholds,
|
||||
t,
|
||||
optionName,
|
||||
graphType,
|
||||
timezone.value,
|
||||
currentQuery,
|
||||
query,
|
||||
graphVisibility,
|
||||
legendPosition,
|
||||
],
|
||||
const resolvedThresholds = useMemo(
|
||||
() => getThresholds(thresholds, t, optionName, yAxisUnit),
|
||||
[thresholds, t, optionName, yAxisUnit],
|
||||
);
|
||||
|
||||
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||
const chartData = useMemo(() => {
|
||||
if (!queryResponse?.data?.payload) {
|
||||
return [];
|
||||
}
|
||||
return prepareChartData(queryResponse?.data?.payload);
|
||||
}, [queryResponse?.data?.payload]);
|
||||
|
||||
const hasResultData = !!queryResponse?.data?.payload?.data?.result?.length;
|
||||
|
||||
@@ -361,6 +295,14 @@ function ChartPreview({
|
||||
?.active || false;
|
||||
|
||||
const isWarning = !isEmpty(queryResponse.data?.warning);
|
||||
|
||||
const chartWidth = containerDimensions?.width
|
||||
? containerDimensions.width - CHART_PREVIEW_CONTAINER_PADDING
|
||||
: 0;
|
||||
const chartHeight = containerDimensions?.height
|
||||
? containerDimensions.height - CHART_PREVIEW_HEADER_HEIGHT
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="alert-chart-container" ref={graphRef}>
|
||||
<ChartContainer>
|
||||
@@ -384,16 +326,22 @@ function ChartPreview({
|
||||
)}
|
||||
|
||||
{chartDataAvailable && !isAnomalyDetectionAlert && (
|
||||
<GridPanelSwitch
|
||||
options={options}
|
||||
<ChartContent
|
||||
panelType={graphType}
|
||||
alertId={alertDef?.id}
|
||||
query={query || currentQuery}
|
||||
apiResponse={queryResponse.data?.payload}
|
||||
data={chartData}
|
||||
name={name || 'Chart Preview'}
|
||||
panelData={
|
||||
queryResponse.data?.payload?.data?.newResult?.data?.result || []
|
||||
}
|
||||
query={query || initialQueriesMap.metrics}
|
||||
thresholds={resolvedThresholds}
|
||||
yAxisUnit={yAxisUnit}
|
||||
legendPosition={legendPosition}
|
||||
isDarkMode={isDarkMode}
|
||||
timezone={timezone}
|
||||
width={chartWidth}
|
||||
height={chartHeight}
|
||||
minTimeScale={minTimeScale}
|
||||
maxTimeScale={maxTimeScale}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Threshold } from 'container/CreateAlertV2/context/types';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import {
|
||||
BooleanFormats,
|
||||
@@ -11,6 +15,20 @@ import {
|
||||
TimeFormats,
|
||||
} from 'container/NewWidget/RightContainer/types';
|
||||
import { TFunction } from 'i18next';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import {
|
||||
DrawStyle,
|
||||
FillMode,
|
||||
LineInterpolation,
|
||||
LineStyle,
|
||||
} from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { hasSingleVisiblePoint } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import { get } from 'lodash-es';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
dataFormatConfig,
|
||||
@@ -20,6 +38,8 @@ import {
|
||||
timeUnitsConfig,
|
||||
} from './config';
|
||||
|
||||
const CHART_ID_PREFIX = 'alert_legend_widget';
|
||||
|
||||
export function covertIntoDataFormats({
|
||||
value,
|
||||
sourceUnit,
|
||||
@@ -142,3 +162,110 @@ export const getThresholds = (
|
||||
});
|
||||
return thresholdsToReturn;
|
||||
};
|
||||
|
||||
export type AlertChartPanelType = PANEL_TYPES.TIME_SERIES | PANEL_TYPES.BAR;
|
||||
|
||||
export interface BuildAlertChartConfigParams {
|
||||
id: string;
|
||||
panelType: AlertChartPanelType;
|
||||
query: Query;
|
||||
thresholds: ThresholdProps[];
|
||||
apiResponse?: MetricRangePayloadProps;
|
||||
yAxisUnit?: string;
|
||||
isDarkMode: boolean;
|
||||
timezone: Timezone;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
onDragSelect: (startTime: number, endTime: number) => void;
|
||||
onClick?: OnClickPluginOpts['onClick'];
|
||||
}
|
||||
|
||||
export const buildAlertChartConfig = ({
|
||||
id,
|
||||
panelType,
|
||||
query,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
onDragSelect,
|
||||
onClick,
|
||||
}: BuildAlertChartConfigParams): UPlotConfigBuilder => {
|
||||
const stepIntervals: ExecStats['stepIntervals'] = get(
|
||||
apiResponse,
|
||||
'data.newResult.meta.stepIntervals',
|
||||
{},
|
||||
);
|
||||
const stepIntervalValues = Object.values(stepIntervals);
|
||||
const minStepInterval = stepIntervalValues.length
|
||||
? Math.min(...stepIntervalValues)
|
||||
: undefined;
|
||||
|
||||
const builder = buildBaseConfig({
|
||||
id,
|
||||
panelType,
|
||||
panelMode: PanelMode.DASHBOARD_VIEW,
|
||||
thresholds,
|
||||
apiResponse,
|
||||
yAxisUnit,
|
||||
isDarkMode,
|
||||
timezone,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
stepInterval: minStepInterval,
|
||||
onDragSelect,
|
||||
onClick,
|
||||
});
|
||||
|
||||
const seriesList = apiResponse?.data?.result;
|
||||
if (!seriesList?.length) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
const isBar = panelType === PANEL_TYPES.BAR;
|
||||
|
||||
seriesList.forEach((series) => {
|
||||
const baseLabelName = getLabelName(
|
||||
series.metric,
|
||||
series.queryName || '',
|
||||
series.legend || '',
|
||||
);
|
||||
const label = query ? getLegend(series, query, baseLabelName) : baseLabelName;
|
||||
|
||||
if (isBar) {
|
||||
builder.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: DrawStyle.Bar,
|
||||
label,
|
||||
colorMapping: {},
|
||||
isDarkMode,
|
||||
stepInterval: get(stepIntervals, series.queryName, undefined),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const hasSingleValidPoint = hasSingleVisiblePoint(series.values);
|
||||
builder.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: hasSingleValidPoint ? DrawStyle.Points : DrawStyle.Line,
|
||||
label,
|
||||
colorMapping: {},
|
||||
spanGaps: true,
|
||||
lineStyle: LineStyle.Solid,
|
||||
lineInterpolation: LineInterpolation.Spline,
|
||||
showPoints: hasSingleValidPoint,
|
||||
pointSize: 5,
|
||||
fillMode: FillMode.None,
|
||||
isDarkMode,
|
||||
metric: series.metric,
|
||||
});
|
||||
});
|
||||
|
||||
return builder;
|
||||
};
|
||||
|
||||
export const buildChartId = (id?: string): string =>
|
||||
id ? `${CHART_ID_PREFIX}_${id}` : CHART_ID_PREFIX;
|
||||
|
||||
@@ -719,7 +719,6 @@ function FormAlertRules({
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
/>
|
||||
}
|
||||
name=""
|
||||
query={stagedQuery}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
alertDef={alertDef}
|
||||
@@ -739,7 +738,6 @@ function FormAlertRules({
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
/>
|
||||
}
|
||||
name="Chart Preview"
|
||||
query={stagedQuery}
|
||||
alertDef={alertDef}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onSelect,
|
||||
}: {
|
||||
onSelect: (value: string) => void;
|
||||
}): JSX.Element => (
|
||||
<div data-testid="custom-time-picker">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-15m"
|
||||
onClick={(): void => onSelect('15m')}
|
||||
>
|
||||
15m
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-1h"
|
||||
onClick={(): void => onSelect('1h')}
|
||||
>
|
||||
1h
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-6h"
|
||||
onClick={(): void => onSelect('6h')}
|
||||
>
|
||||
6h
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-custom"
|
||||
onClick={(): void => onSelect('custom')}
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('DateTimeSelectionV2 - Edge Cases', () => {
|
||||
let currentSearchParams: URLSearchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSafeNavigate.mockClear();
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
describe('Fresh Params at Navigation Time (Core Fix)', () => {
|
||||
it('should read params at navigation time, not render time', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams="relativeTime=30m"
|
||||
onUrlUpdate={(event): void => {
|
||||
currentSearchParams = event.searchParams;
|
||||
}}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'relativeTime=30m&externalParam=addedLater',
|
||||
);
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=1h');
|
||||
expect(navigatedUrl).toContain('externalParam=addedLater');
|
||||
});
|
||||
|
||||
it('should preserve multiple externally added params', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'relativeTime=30m&yAxisUnit=bytes&groupBy=host&view=table',
|
||||
);
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-6h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=6h');
|
||||
expect(navigatedUrl).toContain('yAxisUnit=bytes');
|
||||
expect(navigatedUrl).toContain('groupBy=host');
|
||||
expect(navigatedUrl).toContain('view=table');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Empty and Special Values', () => {
|
||||
it('should handle empty URL params gracefully', async () => {
|
||||
currentSearchParams = new URLSearchParams('');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-15m'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=15m');
|
||||
});
|
||||
|
||||
it('should handle special characters in preserved params', async () => {
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'relativeTime=30m&filter=name%3D%22test%22',
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m&filter=name%3D%22test%22">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=1h');
|
||||
expect(navigatedUrl).toContain('filter=');
|
||||
});
|
||||
|
||||
it('should not navigate when selecting custom (opens picker instead)', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-custom'));
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const customNavigationCalls = mockSafeNavigate.mock.calls.filter((call) => {
|
||||
const url = call[0] as string;
|
||||
return url.includes('startTime=') || url.includes('endTime=');
|
||||
});
|
||||
|
||||
expect(customNavigationCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,180 +0,0 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
let mockOnCustomDateHandler: ((range: [unknown, unknown]) => void) | null =
|
||||
null;
|
||||
let mockOnValidCustomDateChange: ((data: { timeStr: string }) => void) | null =
|
||||
null;
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onSelect,
|
||||
onCustomDateHandler,
|
||||
onValidCustomDateChange,
|
||||
}: {
|
||||
onSelect: (value: string) => void;
|
||||
onCustomDateHandler?: (range: [unknown, unknown]) => void;
|
||||
onValidCustomDateChange?: (data: { timeStr: string }) => void;
|
||||
}): JSX.Element => {
|
||||
mockOnCustomDateHandler = onCustomDateHandler || null;
|
||||
mockOnValidCustomDateChange = onValidCustomDateChange || null;
|
||||
|
||||
return (
|
||||
<div data-testid="custom-time-picker">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-1h"
|
||||
onClick={(): void => onSelect('1h')}
|
||||
>
|
||||
1h
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DateTimeSelectionV2 - Modal Mode', () => {
|
||||
let currentSearchParams: URLSearchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSafeNavigate.mockClear();
|
||||
queryClient.clear();
|
||||
mockOnCustomDateHandler = null;
|
||||
mockOnValidCustomDateChange = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
it('should call onTimeChange instead of navigating for relative time', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
const mockOnTimeChange = jest.fn();
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m">
|
||||
<DateTimeSelection
|
||||
showAutoRefresh
|
||||
isModalTimeSelection
|
||||
onTimeChange={mockOnTimeChange}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnTimeChange).toHaveBeenCalledWith('1h');
|
||||
});
|
||||
|
||||
expect(mockSafeNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onTimeChange with custom and timestamps for custom date', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
const mockOnTimeChange = jest.fn();
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m">
|
||||
<DateTimeSelection
|
||||
showAutoRefresh
|
||||
isModalTimeSelection
|
||||
onTimeChange={mockOnTimeChange}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const startMoment = { toDate: (): Date => new Date(1700000000000) };
|
||||
const endMoment = { toDate: (): Date => new Date(1700003600000) };
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockOnCustomDateHandler?.([startMoment, endMoment]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnTimeChange).toHaveBeenCalledWith(
|
||||
'custom',
|
||||
[1700000000000, 1700003600000],
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockSafeNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onTimeChange for valid custom date string in modal', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
const mockOnTimeChange = jest.fn();
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m">
|
||||
<DateTimeSelection
|
||||
showAutoRefresh
|
||||
isModalTimeSelection
|
||||
onTimeChange={mockOnTimeChange}
|
||||
/>
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockOnValidCustomDateChange?.({ timeStr: '4h' });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnTimeChange).toHaveBeenCalledWith('4h');
|
||||
});
|
||||
|
||||
expect(mockSafeNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,207 +0,0 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { AppContext } from 'providers/App/App';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import store from 'store';
|
||||
import { getAppContextMock } from 'tests/test-utils';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
|
||||
import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { refetchOnWindowFocus: false, retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onSelect,
|
||||
}: {
|
||||
onSelect: (value: string) => void;
|
||||
}): JSX.Element => (
|
||||
<div data-testid="custom-time-picker">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-1h"
|
||||
onClick={(): void => onSelect('1h')}
|
||||
>
|
||||
1h
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
function NuqsParamSetter({ paramValue }: { paramValue: string }): JSX.Element {
|
||||
const [, setYAxisUnit] = useQueryState(
|
||||
'yAxisUnit',
|
||||
parseAsString.withDefault(''),
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="set-nuqs-param"
|
||||
onClick={(): void => {
|
||||
setYAxisUnit(paramValue);
|
||||
}}
|
||||
>
|
||||
Set yAxisUnit
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface WrapperProps {
|
||||
children: React.ReactNode;
|
||||
initialSearchParams?: string;
|
||||
initialPath?: string;
|
||||
onUrlUpdate?: (event: { searchParams: URLSearchParams }) => void;
|
||||
}
|
||||
|
||||
function TestWrapper({
|
||||
children,
|
||||
initialSearchParams = '',
|
||||
initialPath = '/services',
|
||||
onUrlUpdate,
|
||||
}: WrapperProps): JSX.Element {
|
||||
const initialEntry = initialSearchParams
|
||||
? `${initialPath}?${initialSearchParams}`
|
||||
: initialPath;
|
||||
|
||||
const mockedStore = mockStore({
|
||||
...store.getState(),
|
||||
app: {
|
||||
...store.getState().app,
|
||||
role: 'ADMIN',
|
||||
user: {
|
||||
userId: 'test-user-id',
|
||||
email: 'test@signoz.io',
|
||||
name: 'TestUser',
|
||||
profilePictureURL: '',
|
||||
accessJwt: '',
|
||||
refreshJwt: '',
|
||||
},
|
||||
isLoggedIn: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter initialEntries={[initialEntry]}>
|
||||
<CompatRouter>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={initialSearchParams}
|
||||
onUrlUpdate={onUrlUpdate}
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={mockedStore}>
|
||||
<AppContext.Provider value={getAppContextMock('ADMIN')}>
|
||||
<TimezoneProvider>
|
||||
<QueryBuilderProvider>
|
||||
<Route path="*">{children}</Route>
|
||||
</QueryBuilderProvider>
|
||||
</TimezoneProvider>
|
||||
</AppContext.Provider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</CompatRouter>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
describe('REGRESSION: DateTimeSelectionV2 preserves nuqs query params on time change', () => {
|
||||
let currentSearchParams: URLSearchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSafeNavigate.mockClear();
|
||||
queryClient.clear();
|
||||
|
||||
// Initialize with test's initial search params
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
it('should preserve yAxisUnit param set via nuqs when changing time selection', async () => {
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams="relativeTime=30m"
|
||||
onUrlUpdate={(event): void => {
|
||||
// Sync nuqs URL updates to our mock getter
|
||||
// This simulates how window.location.search would be updated in real browser
|
||||
currentSearchParams = event.searchParams;
|
||||
}}
|
||||
>
|
||||
<NuqsParamSetter paramValue="bytes" />
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('set-nuqs-param'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(currentSearchParams.get('yAxisUnit')).toBe('bytes');
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=1h');
|
||||
expect(navigatedUrl).toContain('yAxisUnit=bytes');
|
||||
});
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="custom-time-picker" />,
|
||||
}));
|
||||
|
||||
describe('DateTimeSelectionV2 - Route-Specific Behavior', () => {
|
||||
let currentSearchParams: URLSearchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSafeNavigate.mockClear();
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
describe('Alert Pages', () => {
|
||||
it('should set default time for alert overview when no time params', async () => {
|
||||
currentSearchParams = new URLSearchParams('otherParam=value');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams="otherParam=value"
|
||||
initialPath={ROUTES.ALERT_OVERVIEW}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh defaultRelativeTime="6h" />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[0][0] as string;
|
||||
expect(navigatedUrl).toContain('relativeTime=6h');
|
||||
expect(navigatedUrl).toContain('otherParam=value');
|
||||
});
|
||||
|
||||
it('should set default time for alert history when no time params', async () => {
|
||||
currentSearchParams = new URLSearchParams('filter=active');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams="filter=active"
|
||||
initialPath={ROUTES.ALERT_HISTORY}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh defaultRelativeTime="6h" />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[0][0] as string;
|
||||
expect(navigatedUrl).toContain('relativeTime=6h');
|
||||
});
|
||||
|
||||
it('should NOT override existing time params on alert pages', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=1h&filter=active');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams="relativeTime=1h&filter=active"
|
||||
initialPath={ROUTES.ALERT_OVERVIEW}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh defaultRelativeTime="6h" />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const calls = mockSafeNavigate.mock.calls;
|
||||
if (calls.length > 0) {
|
||||
const lastUrl = calls[calls.length - 1][0] as string;
|
||||
expect(lastUrl).toContain('relativeTime=1h');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableUrlSync Behavior', () => {
|
||||
it('should not sync URL on mount when disableUrlSync is true', async () => {
|
||||
currentSearchParams = new URLSearchParams('');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="" initialPath="/services">
|
||||
<DateTimeSelection showAutoRefresh disableUrlSync />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const syncCalls = mockSafeNavigate.mock.calls.filter((call) => {
|
||||
const url = call[0] as string;
|
||||
return url.includes('relativeTime=') && !url.includes('services?');
|
||||
});
|
||||
|
||||
expect(syncCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,353 +0,0 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import DateTimeSelection from '../index';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from '../utils/getUnstableCurrentSearchParams';
|
||||
import { queryClient, TestWrapper, createMockMoment } from './testUtils';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/NewExplorerCTA', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
|
||||
let mockOnCustomDateHandler: ((range: [unknown, unknown]) => void) | null =
|
||||
null;
|
||||
let mockOnValidCustomDateChange: ((data: { timeStr: string }) => void) | null =
|
||||
null;
|
||||
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onSelect,
|
||||
onCustomDateHandler,
|
||||
onValidCustomDateChange,
|
||||
}: {
|
||||
onSelect: (value: string) => void;
|
||||
onCustomDateHandler?: (range: [unknown, unknown]) => void;
|
||||
onValidCustomDateChange?: (data: { timeStr: string }) => void;
|
||||
}): JSX.Element => {
|
||||
mockOnCustomDateHandler = onCustomDateHandler || null;
|
||||
mockOnValidCustomDateChange = onValidCustomDateChange || null;
|
||||
|
||||
return (
|
||||
<div data-testid="custom-time-picker">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-15m"
|
||||
onClick={(): void => onSelect('15m')}
|
||||
>
|
||||
15m
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-1h"
|
||||
onClick={(): void => onSelect('1h')}
|
||||
>
|
||||
1h
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="select-6h"
|
||||
onClick={(): void => onSelect('6h')}
|
||||
>
|
||||
6h
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DateTimeSelectionV2 - Time Selection', () => {
|
||||
let currentSearchParams: URLSearchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSafeNavigate.mockClear();
|
||||
queryClient.clear();
|
||||
mockOnCustomDateHandler = null;
|
||||
mockOnValidCustomDateChange = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
describe('Relative Time', () => {
|
||||
it('should update relativeTime and remove startTime/endTime', async () => {
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'startTime=1000&endTime=2000&otherParam=keep',
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="startTime=1000&endTime=2000&otherParam=keep">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-15m'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=15m');
|
||||
expect(navigatedUrl).not.toContain('startTime=');
|
||||
expect(navigatedUrl).not.toContain('endTime=');
|
||||
expect(navigatedUrl).toContain('otherParam=keep');
|
||||
});
|
||||
|
||||
it('should remove activeLogId param on time change', async () => {
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'relativeTime=30m&activeLogId=log123',
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m&activeLogId=log123">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=1h');
|
||||
expect(navigatedUrl).not.toContain('activeLogId');
|
||||
});
|
||||
|
||||
it('should update compositeQuery with new ID when present', async () => {
|
||||
const compositeQuery = encodeURIComponent(
|
||||
JSON.stringify({ id: 'old-id', builder: { queryData: [] } }),
|
||||
);
|
||||
currentSearchParams = new URLSearchParams(
|
||||
`relativeTime=30m&compositeQuery=${compositeQuery}`,
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams={`relativeTime=30m&compositeQuery=${compositeQuery}`}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-6h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('compositeQuery=');
|
||||
expect(navigatedUrl).not.toContain('old-id');
|
||||
});
|
||||
|
||||
it('should preserve all non-time URL params', async () => {
|
||||
currentSearchParams = new URLSearchParams(
|
||||
'relativeTime=30m¶m1=a¶m2=b¶m3=c',
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m¶m1=a¶m2=b¶m3=c">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByTestId('select-1h'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=1h');
|
||||
expect(navigatedUrl).toContain('param1=a');
|
||||
expect(navigatedUrl).toContain('param2=b');
|
||||
expect(navigatedUrl).toContain('param3=c');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Date Range', () => {
|
||||
it('should set startTime/endTime and remove relativeTime', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m&keepThis=yes');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m&keepThis=yes">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const startMoment = createMockMoment(1700000000000);
|
||||
const endMoment = createMockMoment(1700003600000);
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockOnCustomDateHandler?.([startMoment, endMoment]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('startTime=1700000000000');
|
||||
expect(navigatedUrl).toContain('endTime=1700003600000');
|
||||
expect(navigatedUrl).not.toContain('relativeTime=');
|
||||
expect(navigatedUrl).toContain('keepThis=yes');
|
||||
});
|
||||
|
||||
it('should update compositeQuery when present for custom date', async () => {
|
||||
const compositeQuery = encodeURIComponent(
|
||||
JSON.stringify({ id: 'old-id', builder: { queryData: [] } }),
|
||||
);
|
||||
currentSearchParams = new URLSearchParams(
|
||||
`relativeTime=30m&compositeQuery=${compositeQuery}`,
|
||||
);
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
initialSearchParams={`relativeTime=30m&compositeQuery=${compositeQuery}`}
|
||||
>
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const startMoment = createMockMoment(1700000000000);
|
||||
const endMoment = createMockMoment(1700003600000);
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockOnCustomDateHandler?.([startMoment, endMoment]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('compositeQuery=');
|
||||
expect(navigatedUrl).not.toContain('old-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Valid Custom Date String', () => {
|
||||
it('should handle shorthand date format and preserve params', async () => {
|
||||
currentSearchParams = new URLSearchParams('relativeTime=30m&filter=active');
|
||||
__setSearchParamsGetterForTest(() => currentSearchParams);
|
||||
|
||||
render(
|
||||
<TestWrapper initialSearchParams="relativeTime=30m&filter=active">
|
||||
<DateTimeSelection showAutoRefresh />
|
||||
</TestWrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('custom-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
mockSafeNavigate.mockClear();
|
||||
|
||||
act(() => {
|
||||
mockOnValidCustomDateChange?.({ timeStr: '2h' });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const navigatedUrl = mockSafeNavigate.mock.calls[
|
||||
mockSafeNavigate.mock.calls.length - 1
|
||||
][0] as string;
|
||||
|
||||
expect(navigatedUrl).toContain('relativeTime=2h');
|
||||
expect(navigatedUrl).toContain('filter=active');
|
||||
expect(navigatedUrl).not.toContain('startTime=');
|
||||
expect(navigatedUrl).not.toContain('endTime=');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { AppContext } from 'providers/App/App';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import store from 'store';
|
||||
import { getAppContextMock } from 'tests/test-utils';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { refetchOnWindowFocus: false, retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
export const mockStore = configureStore([thunk]);
|
||||
|
||||
interface WrapperProps {
|
||||
children: React.ReactNode;
|
||||
initialSearchParams?: string;
|
||||
initialPath?: string;
|
||||
onUrlUpdate?: (event: { searchParams: URLSearchParams }) => void;
|
||||
}
|
||||
|
||||
export function TestWrapper({
|
||||
children,
|
||||
initialSearchParams = '',
|
||||
initialPath = '/services',
|
||||
onUrlUpdate,
|
||||
}: WrapperProps): JSX.Element {
|
||||
const initialEntry = initialSearchParams
|
||||
? `${initialPath}?${initialSearchParams}`
|
||||
: initialPath;
|
||||
|
||||
const mockedStore = mockStore({
|
||||
...store.getState(),
|
||||
app: {
|
||||
...store.getState().app,
|
||||
role: 'ADMIN',
|
||||
user: {
|
||||
userId: 'test-user-id',
|
||||
email: 'test@signoz.io',
|
||||
name: 'TestUser',
|
||||
profilePictureURL: '',
|
||||
accessJwt: '',
|
||||
refreshJwt: '',
|
||||
},
|
||||
isLoggedIn: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter initialEntries={[initialEntry]}>
|
||||
<CompatRouter>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={initialSearchParams}
|
||||
onUrlUpdate={onUrlUpdate}
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={mockedStore}>
|
||||
<AppContext.Provider value={getAppContextMock('ADMIN')}>
|
||||
<TimezoneProvider>
|
||||
<QueryBuilderProvider>
|
||||
<Route path="*">{children}</Route>
|
||||
</QueryBuilderProvider>
|
||||
</TimezoneProvider>
|
||||
</AppContext.Provider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</CompatRouter>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export function createMockMoment(timestamp: number): {
|
||||
toDate: () => Date;
|
||||
toISOString: () => string;
|
||||
format: () => string;
|
||||
toString: () => string;
|
||||
} {
|
||||
const date = new Date(timestamp);
|
||||
return {
|
||||
toDate: (): Date => date,
|
||||
toISOString: (): string => date.toISOString(),
|
||||
format: (): string => date.toISOString(),
|
||||
toString: (): string => date.toString(),
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { useNavigationType } from 'react-router-dom-v5-compat';
|
||||
import { useNavigationType, useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { RefreshCw, Undo } from '@signozhq/icons';
|
||||
import { Button } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from 'store/globalTime';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import { cloneDeep, isObject } from 'lodash-es';
|
||||
@@ -53,7 +54,6 @@ import {
|
||||
Time,
|
||||
TimeRange,
|
||||
} from './types';
|
||||
import { getUnstableCurrentSearchParams } from './utils/getUnstableCurrentSearchParams';
|
||||
|
||||
import './DateTimeSelectionV2.styles.scss';
|
||||
|
||||
@@ -90,12 +90,10 @@ function DateTimeSelection({
|
||||
const [hasSelectedTimeError, setHasSelectedTimeError] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const currentSearchParams = getUnstableCurrentSearchParams();
|
||||
const searchStartTime = currentSearchParams.get(QueryParams.startTime);
|
||||
const searchEndTime = currentSearchParams.get(QueryParams.endTime);
|
||||
const relativeTimeFromUrl = currentSearchParams.get(QueryParams.relativeTime);
|
||||
const hasTimeParamsInUrl =
|
||||
(searchStartTime && searchEndTime) || relativeTimeFromUrl;
|
||||
const urlQuery = useUrlQuery();
|
||||
const searchStartTime = urlQuery.get('startTime');
|
||||
const searchEndTime = urlQuery.get('endTime');
|
||||
const relativeTimeFromUrl = urlQuery.get(QueryParams.relativeTime);
|
||||
|
||||
// Prioritize props for initial modal time, fallback to URL params
|
||||
let initialModalStartTime = 0;
|
||||
@@ -117,6 +115,8 @@ function DateTimeSelection({
|
||||
);
|
||||
const [modalEndTime, setModalEndTime] = useState<number>(initialModalEndTime);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Effect to update modal time state when props change
|
||||
useEffect(() => {
|
||||
if (modalInitialStartTime !== undefined) {
|
||||
@@ -323,15 +323,14 @@ function DateTimeSelection({
|
||||
return;
|
||||
}
|
||||
|
||||
const urlQuery = getUnstableCurrentSearchParams();
|
||||
urlQuery.delete(QueryParams.startTime);
|
||||
urlQuery.delete(QueryParams.endTime);
|
||||
urlQuery.delete('startTime');
|
||||
urlQuery.delete('endTime');
|
||||
|
||||
urlQuery.set(QueryParams.relativeTime, value);
|
||||
// Remove Hidden Filters from URL query parameters on time change
|
||||
urlQuery.delete(QueryParams.activeLogId);
|
||||
|
||||
if (urlQuery.has(QueryParams.compositeQuery)) {
|
||||
if (searchParams.has(QueryParams.compositeQuery)) {
|
||||
const updatedCompositeQuery = getUpdatedCompositeQuery();
|
||||
urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery);
|
||||
}
|
||||
@@ -350,6 +349,8 @@ function DateTimeSelection({
|
||||
getUpdatedCompositeQuery,
|
||||
updateLocalStorageForRoutes,
|
||||
updateTimeInterval,
|
||||
urlQuery,
|
||||
searchParams,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -413,7 +414,6 @@ function DateTimeSelection({
|
||||
|
||||
updateLocalStorageForRoutes(JSON.stringify({ startTime, endTime }));
|
||||
|
||||
const urlQuery = getUnstableCurrentSearchParams();
|
||||
urlQuery.set(
|
||||
QueryParams.startTime,
|
||||
startTime?.toDate().getTime().toString(),
|
||||
@@ -421,7 +421,7 @@ function DateTimeSelection({
|
||||
urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString());
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
|
||||
if (urlQuery.has(QueryParams.compositeQuery)) {
|
||||
if (searchParams.has(QueryParams.compositeQuery)) {
|
||||
const updatedCompositeQuery = getUpdatedCompositeQuery();
|
||||
urlQuery.set(QueryParams.compositeQuery, updatedCompositeQuery);
|
||||
}
|
||||
@@ -441,9 +441,8 @@ function DateTimeSelection({
|
||||
updateTimeInterval(dateTimeStr);
|
||||
updateLocalStorageForRoutes(dateTimeStr);
|
||||
|
||||
const urlQuery = getUnstableCurrentSearchParams();
|
||||
urlQuery.delete(QueryParams.startTime);
|
||||
urlQuery.delete(QueryParams.endTime);
|
||||
urlQuery.delete('startTime');
|
||||
urlQuery.delete('endTime');
|
||||
|
||||
urlQuery.set(QueryParams.relativeTime, dateTimeStr);
|
||||
|
||||
@@ -596,12 +595,13 @@ function DateTimeSelection({
|
||||
|
||||
// set the default relative time for alert history and overview pages if relative time is not specified
|
||||
if (
|
||||
!hasTimeParamsInUrl &&
|
||||
(!urlQuery.has(QueryParams.startTime) ||
|
||||
!urlQuery.has(QueryParams.endTime)) &&
|
||||
!urlQuery.has(QueryParams.relativeTime) &&
|
||||
(currentRoute === ROUTES.ALERT_OVERVIEW ||
|
||||
currentRoute === ROUTES.ALERT_HISTORY)
|
||||
) {
|
||||
updateTimeInterval(defaultRelativeTime);
|
||||
const urlQuery = getUnstableCurrentSearchParams();
|
||||
urlQuery.set(QueryParams.relativeTime, defaultRelativeTime);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
@@ -625,10 +625,9 @@ function DateTimeSelection({
|
||||
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
|
||||
}
|
||||
|
||||
const urlQuery = getUnstableCurrentSearchParams();
|
||||
if (updatedTime !== 'custom') {
|
||||
urlQuery.delete(QueryParams.startTime);
|
||||
urlQuery.delete(QueryParams.endTime);
|
||||
urlQuery.delete('startTime');
|
||||
urlQuery.delete('endTime');
|
||||
urlQuery.set(QueryParams.relativeTime, updatedTime);
|
||||
} else {
|
||||
const startTime = preStartTime.toString();
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* This was introduced to fix a sync bug between Nuqs and react-router-dom
|
||||
*
|
||||
* We are using the wrong adapter for nuqs because the correct one only supports v6/v7,
|
||||
* and we are at version v5. This causes the nuqs/react-router-dom to be out of sync.
|
||||
*
|
||||
* We can revert this commit once we migrate react-router-dom to v6, or once we migrate
|
||||
* to DateTimeSelectionV3
|
||||
*/
|
||||
|
||||
/**
|
||||
* This was created to help testing the regression introduced between nuqs/react-router-dom
|
||||
*/
|
||||
type SearchParamsGetter = () => URLSearchParams;
|
||||
let getter: SearchParamsGetter = (): URLSearchParams =>
|
||||
new URLSearchParams(window.location.search);
|
||||
|
||||
/**
|
||||
* This function will return a fresh instance of URLSearchParams every time it's called.
|
||||
*
|
||||
* DO NOT USE IT FOR useEffect/useCallback dependencies, use Nuqs instead.
|
||||
*/
|
||||
export function getUnstableCurrentSearchParams(): URLSearchParams {
|
||||
return getter();
|
||||
}
|
||||
|
||||
// Testing helpers
|
||||
export function __setSearchParamsGetterForTest(fn: SearchParamsGetter): void {
|
||||
getter = fn;
|
||||
}
|
||||
|
||||
export function __resetSearchParamsGetter(): void {
|
||||
getter = (): URLSearchParams => new URLSearchParams(window.location.search);
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
fillMissingXAxisTimestamps,
|
||||
getXAxisTimestamps,
|
||||
} from 'container/DashboardContainer/visualization/panels/utils';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
/**
|
||||
* Checks if a value is invalid for plotting
|
||||
*
|
||||
@@ -52,6 +58,28 @@ export function normalizePlotValue(
|
||||
return value as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if at most one entry in `values` is a valid plot value.
|
||||
*
|
||||
* Used to decide whether a series should render as a single point (drawStyle:
|
||||
* Points) vs a line — a continuous line with only one visible sample is
|
||||
* invisible to the user.
|
||||
*/
|
||||
export function hasSingleVisiblePoint(
|
||||
values: ReadonlyArray<readonly [unknown, unknown]> | undefined,
|
||||
): boolean {
|
||||
let validPointCount = 0;
|
||||
for (const [, rawValue] of values ?? []) {
|
||||
if (!isInvalidPlotValue(rawValue)) {
|
||||
validPointCount += 1;
|
||||
if (validPointCount > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface SeriesSpanGapsOption {
|
||||
spanGaps?: boolean | number;
|
||||
}
|
||||
@@ -226,3 +254,21 @@ export function applySpanGapsToAlignedData(
|
||||
|
||||
return [newX, ...transformedSeries] as uPlot.AlignedData;
|
||||
}
|
||||
|
||||
/** * Transforms raw API response into aligned data format expected by uPlot.
|
||||
*
|
||||
* The API response contains multiple series of time-value pairs, each with its
|
||||
* own set of timestamps. uPlot requires a single shared x-axis (timestamps)
|
||||
* and separate y-value arrays for each series, aligned by index. This function
|
||||
* extracts the unique sorted timestamps across all series and fills in missing
|
||||
* values with null to maintain alignment.
|
||||
*/
|
||||
export const prepareChartData = (
|
||||
apiResponse: MetricRangePayloadProps,
|
||||
): uPlot.AlignedData => {
|
||||
const seriesList = apiResponse?.data?.result || [];
|
||||
const timestampArr = getXAxisTimestamps(seriesList);
|
||||
const yAxisValuesArr = fillMissingXAxisTimestamps(timestampArr, seriesList);
|
||||
|
||||
return [timestampArr, ...yAxisValuesArr];
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { compose, Store } from 'redux';
|
||||
import type { SignozBootSettings } from 'utils/bootData';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -8,7 +7,6 @@ declare global {
|
||||
pylon: any;
|
||||
Appcues: Record<string, any>;
|
||||
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose;
|
||||
signozBootData?: { settings?: Partial<SignozBootSettings> };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
export {};
|
||||
|
||||
type BootData = typeof import('../bootData');
|
||||
|
||||
function loadModule(settings?: object): BootData {
|
||||
(window as any).signozBootData =
|
||||
settings !== undefined ? { settings } : undefined;
|
||||
let mod!: BootData;
|
||||
jest.isolateModules(() => {
|
||||
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
|
||||
mod = require('../bootData');
|
||||
});
|
||||
return mod;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
delete (window as any).signozBootData;
|
||||
});
|
||||
|
||||
describe('when window.signozBootData is absent', () => {
|
||||
it('all sub-objects are defined and empty', () => {
|
||||
const { bootSettings } = loadModule();
|
||||
expect(bootSettings.sentry).toStrictEqual({});
|
||||
expect(bootSettings.posthog).toStrictEqual({});
|
||||
expect(bootSettings.pylon).toStrictEqual({});
|
||||
expect(bootSettings.appcues).toStrictEqual({});
|
||||
expect(bootSettings.roles).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('optional fields are undefined', () => {
|
||||
const { bootSettings } = loadModule();
|
||||
expect(bootSettings.sentry.dsn).toBeUndefined();
|
||||
expect(bootSettings.sentry.tunnelUrl).toBeUndefined();
|
||||
expect(bootSettings.posthog.key).toBeUndefined();
|
||||
expect(bootSettings.pylon.appId).toBeUndefined();
|
||||
expect(bootSettings.pylon.identSecret).toBeUndefined();
|
||||
expect(bootSettings.appcues.appId).toBeUndefined();
|
||||
expect(bootSettings.roles.isRolesDetailEnabled).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when window.signozBootData.settings is populated', () => {
|
||||
it('reads sentry config', () => {
|
||||
const { bootSettings } = loadModule({
|
||||
sentry: { dsn: 'https://abc@sentry.io/1', tunnelUrl: '/tunnel' },
|
||||
});
|
||||
expect(bootSettings.sentry.dsn).toBe('https://abc@sentry.io/1');
|
||||
expect(bootSettings.sentry.tunnelUrl).toBe('/tunnel');
|
||||
});
|
||||
|
||||
it('reads posthog config', () => {
|
||||
const { bootSettings } = loadModule({ posthog: { key: 'phk_xxx' } });
|
||||
expect(bootSettings.posthog.key).toBe('phk_xxx');
|
||||
});
|
||||
|
||||
it('reads pylon config', () => {
|
||||
const { bootSettings } = loadModule({
|
||||
pylon: { appId: 'pylon-abc', identSecret: 'secret-xyz' },
|
||||
});
|
||||
expect(bootSettings.pylon.appId).toBe('pylon-abc');
|
||||
expect(bootSettings.pylon.identSecret).toBe('secret-xyz');
|
||||
});
|
||||
|
||||
it('reads appcues config', () => {
|
||||
const { bootSettings } = loadModule({ appcues: { appId: 'appcues-123' } });
|
||||
expect(bootSettings.appcues.appId).toBe('appcues-123');
|
||||
});
|
||||
|
||||
it('reads roles config', () => {
|
||||
const { bootSettings } = loadModule({
|
||||
roles: { isRolesDetailEnabled: true },
|
||||
});
|
||||
expect(bootSettings.roles.isRolesDetailEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('missing sub-namespaces fall back to empty objects', () => {
|
||||
const { bootSettings } = loadModule({
|
||||
sentry: { dsn: 'https://abc@sentry.io/1' },
|
||||
});
|
||||
expect(bootSettings.posthog).toStrictEqual({});
|
||||
expect(bootSettings.posthog.key).toBeUndefined();
|
||||
expect(bootSettings.pylon).toStrictEqual({});
|
||||
expect(bootSettings.appcues).toStrictEqual({});
|
||||
expect(bootSettings.roles).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when window.signozBootData exists but settings is undefined', () => {
|
||||
it('all sub-objects are empty', () => {
|
||||
(window as any).signozBootData = {};
|
||||
let mod!: BootData;
|
||||
jest.isolateModules(() => {
|
||||
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
|
||||
mod = require('../bootData');
|
||||
});
|
||||
expect(mod.bootSettings.sentry).toStrictEqual({});
|
||||
expect(mod.bootSettings.posthog).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
export interface SentryConfig {
|
||||
dsn?: string;
|
||||
tunnelUrl?: string;
|
||||
}
|
||||
export interface PosthogConfig {
|
||||
key?: string;
|
||||
}
|
||||
export interface PylonConfig {
|
||||
appId?: string;
|
||||
identSecret?: string;
|
||||
}
|
||||
export interface AppcuesConfig {
|
||||
appId?: string;
|
||||
}
|
||||
export interface RolesConfig {
|
||||
isRolesDetailEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface SignozBootSettings {
|
||||
sentry: SentryConfig;
|
||||
posthog: PosthogConfig;
|
||||
pylon: PylonConfig;
|
||||
appcues: AppcuesConfig;
|
||||
roles: RolesConfig;
|
||||
}
|
||||
|
||||
const raw = window.signozBootData?.settings;
|
||||
|
||||
export const bootSettings: Readonly<SignozBootSettings> = {
|
||||
sentry: raw?.sentry ?? {},
|
||||
posthog: raw?.posthog ?? {},
|
||||
pylon: raw?.pylon ?? {},
|
||||
appcues: raw?.appcues ?? {},
|
||||
roles: raw?.roles ?? {},
|
||||
};
|
||||
7
frontend/src/vite-env.d.ts
vendored
7
frontend/src/vite-env.d.ts
vendored
@@ -13,9 +13,16 @@ declare module '*.md?raw' {
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_FRONTEND_API_ENDPOINT: string;
|
||||
readonly VITE_WEBSOCKET_API_ENDPOINT: string;
|
||||
readonly VITE_PYLON_APP_ID: string;
|
||||
readonly VITE_PYLON_IDENTITY_SECRET: string;
|
||||
readonly VITE_APPCUES_APP_ID: string;
|
||||
readonly VITE_POSTHOG_KEY: string;
|
||||
readonly VITE_SENTRY_AUTH_TOKEN: string;
|
||||
readonly VITE_SENTRY_ORG: string;
|
||||
readonly VITE_SENTRY_PROJECT_ID: string;
|
||||
readonly VITE_SENTRY_DSN: string;
|
||||
readonly VITE_TUNNEL_URL: string;
|
||||
readonly VITE_TUNNEL_DOMAIN: string;
|
||||
readonly VITE_DOCS_BASE_URL: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Plugin, TransformResult, UserConfig } from 'vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import vitePluginChecker from 'vite-plugin-checker';
|
||||
import viteCompression from 'vite-plugin-compression';
|
||||
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
@@ -22,29 +23,6 @@ function devBasePathPlugin(basePath: string): Plugin {
|
||||
};
|
||||
}
|
||||
|
||||
function devBootDataPlugin(env: Record<string, string>): Plugin {
|
||||
return {
|
||||
name: 'dev-boot-data',
|
||||
apply: 'serve',
|
||||
transformIndexHtml(html): string {
|
||||
const bootSettings = {
|
||||
sentry: {
|
||||
dsn: env.VITE_SENTRY_DSN || undefined,
|
||||
tunnelUrl: env.VITE_TUNNEL_URL || undefined,
|
||||
},
|
||||
posthog: { key: env.VITE_POSTHOG_KEY || undefined },
|
||||
pylon: {
|
||||
appId: env.VITE_PYLON_APP_ID || undefined,
|
||||
identSecret: env.VITE_PYLON_IDENTITY_SECRET || undefined,
|
||||
},
|
||||
appcues: { appId: env.VITE_APPCUES_APP_ID || undefined },
|
||||
roles: {},
|
||||
};
|
||||
return html.replaceAll('[[.BootSettings]]', JSON.stringify(bootSettings));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rawMarkdownPlugin(): Plugin {
|
||||
return {
|
||||
name: 'raw-markdown',
|
||||
@@ -69,8 +47,15 @@ export default defineConfig(({ mode }): UserConfig => {
|
||||
tsconfigPaths(),
|
||||
rawMarkdownPlugin(),
|
||||
devBasePathPlugin(basePath),
|
||||
devBootDataPlugin(env),
|
||||
react(),
|
||||
createHtmlPlugin({
|
||||
inject: {
|
||||
data: {
|
||||
PYLON_APP_ID: env.VITE_PYLON_APP_ID || '',
|
||||
APPCUES_APP_ID: env.VITE_APPCUES_APP_ID || '',
|
||||
},
|
||||
},
|
||||
}),
|
||||
vitePluginChecker({
|
||||
typescript: true,
|
||||
// this doubles the build tim
|
||||
@@ -141,8 +126,17 @@ export default defineConfig(({ mode }): UserConfig => {
|
||||
'process.env.WEBSOCKET_API_ENDPOINT': JSON.stringify(
|
||||
env.VITE_WEBSOCKET_API_ENDPOINT,
|
||||
),
|
||||
'process.env.PYLON_APP_ID': JSON.stringify(env.VITE_PYLON_APP_ID),
|
||||
'process.env.PYLON_IDENTITY_SECRET': JSON.stringify(
|
||||
env.VITE_PYLON_IDENTITY_SECRET,
|
||||
),
|
||||
'process.env.APPCUES_APP_ID': JSON.stringify(env.VITE_APPCUES_APP_ID),
|
||||
'process.env.POSTHOG_KEY': JSON.stringify(env.VITE_POSTHOG_KEY),
|
||||
'process.env.SENTRY_ORG': JSON.stringify(env.VITE_SENTRY_ORG),
|
||||
'process.env.SENTRY_PROJECT_ID': JSON.stringify(env.VITE_SENTRY_PROJECT_ID),
|
||||
'process.env.SENTRY_DSN': JSON.stringify(env.VITE_SENTRY_DSN),
|
||||
'process.env.TUNNEL_URL': JSON.stringify(env.VITE_TUNNEL_URL),
|
||||
'process.env.TUNNEL_DOMAIN': JSON.stringify(env.VITE_TUNNEL_DOMAIN),
|
||||
'process.env.DOCS_BASE_URL': JSON.stringify(env.VITE_DOCS_BASE_URL),
|
||||
},
|
||||
// In production, use relative paths so assets work with any base path injected by the backend.
|
||||
|
||||
Reference in New Issue
Block a user