Compare commits

...

3 Commits

Author SHA1 Message Date
Ashwin Bhatkal
e392bfbc6c fix(dashboard-v2): show progress while the blank dashboard is created
The submit button was only disabled for the duration of the create request,
so the wait read as a stuck modal. The import panel already passes loading.
2026-08-21 19:17:42 +05:30
Ashwin Bhatkal
5345d95f59 fix(dashboard-v2): close the new dashboard modal before the page mounts
Same batching as the panel menu: onClose and the navigation to the created
dashboard landed in one commit, so the modal sat over the dashboard page
while it mounted.
2026-08-21 19:17:30 +05:30
Ashwin Bhatkal
3a3f707af8 fix(dashboard-v2): close the panel menu before the editor and view render
Opening the panel editor or the View modal calls resetQuery on the app-wide
query builder and swaps the route, tearing down every grid panel. Those
updates were batched with the dropdown's own close, so the menu stayed
painted for the whole render. Deferring them leaves the close on the urgent
lane, so it paints first.
2026-08-21 19:17:18 +05:30
4 changed files with 53 additions and 29 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { startTransition, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import logEvent from 'api/common/logEvent';
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
@@ -57,12 +57,16 @@ export function useViewPanel(): UseViewPanelApi {
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(query)),
);
// The provider applies the URL in an effect, a tick after the builder's fields have
// mounted and read the query they keep. `resetQuery` — not `initQueryBuilderData`:
// swapping one staged id for another re-anchors global time and refetches the grid.
resetQuery(query);
void logEvent(DashboardDetailEvents.PanelViewed, { panelId });
safeNavigate(`${pathname}?${next.toString()}`);
// Off the urgent lane: mounting the modal re-renders the app-wide query builder
// and every grid panel, so let the menu this ran from close and paint first.
startTransition(() => {
// The provider applies the URL in an effect, a tick after the builder's fields have
// mounted and read the query they keep. `resetQuery` — not `initQueryBuilderData`:
// swapping one staged id for another re-anchors global time and refetches the grid.
resetQuery(query);
safeNavigate(`${pathname}?${next.toString()}`);
});
},
[pathname, safeNavigate, urlQuery, resetQuery],
);
@@ -74,17 +78,19 @@ export function useViewPanel(): UseViewPanelApi {
next.set(QueryParams.graphType, panelType);
// A grid drilldown opens on the saved panel, never a stale editor handoff.
clearViewPanelHandoff();
// As in `openView`. Clearing the staged query matters twice over here: the URL
// below carries this query's own id, and a staged query with a matching id
// makes the provider skip the hydration that normalises legacy filter fields.
resetQuery(query);
// Same encoding the query builder uses (see `useGetCompositeQueryParam`): the URL
// value is `encodeURIComponent(JSON.stringify(query))`, decoded once on read.
next.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(query)),
);
safeNavigate(`${pathname}?${next.toString()}`);
startTransition(() => {
// As in `openView`. Clearing the staged query matters twice over here: the URL
// below carries this query's own id, and a staged query with a matching id
// makes the provider skip the hydration that normalises legacy filter fields.
resetQuery(query);
safeNavigate(`${pathname}?${next.toString()}`);
});
},
[pathname, safeNavigate, urlQuery, resetQuery],
);

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { startTransition, useCallback } from 'react';
import { generatePath } from 'react-router-dom';
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
@@ -46,23 +46,32 @@ export function useOpenPanelEditor(): (
new URLSearchParams(timeSearch).forEach((value, key) => {
params.set(key, value);
});
if (options?.panel) {
const query = getPanelBuilderQuery(options.panel);
const query = options?.panel
? getPanelBuilderQuery(options.panel)
: undefined;
if (query) {
// Single-encoded: `useGetCompositeQueryParam` decodes once on top of the decode
// `URLSearchParams` already does.
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(query)),
);
// The provider applies the URL in an effect, a tick after the builder's fields
// have mounted and read the query they keep (PromQL inputs, add-on rows).
resetQuery(query);
}
const search = params.toString();
safeNavigate(
search ? `${path}?${search}` : path,
options?.handoffState ? { state: options.handoffState } : undefined,
);
// Leaving the dashboard tears down every panel and re-renders the app-wide
// query builder — off the urgent lane so the menu this ran from closes and
// paints before that work starts.
startTransition(() => {
if (query) {
// The provider applies the URL in an effect, a tick after the builder's fields
// have mounted and read the query they keep (PromQL inputs, add-on rows).
resetQuery(query);
}
safeNavigate(
search ? `${path}?${search}` : path,
options?.handoffState ? { state: options.handoffState } : undefined,
);
});
},
[safeNavigate, dashboardId, timeSearch, resetQuery],
);

View File

@@ -1,4 +1,4 @@
import { type ChangeEvent, useState } from 'react';
import { type ChangeEvent, startTransition, useState } from 'react';
// eslint-disable-next-line signoz/no-antd-components -- no @signozhq/ui multiline TextArea yet
import { Input as AntInput } from 'antd';
import { Button } from '@signozhq/ui/button';
@@ -71,9 +71,13 @@ function BlankDashboardPanel({ onClose }: Props): JSX.Element {
hasImage: Boolean(image),
});
onClose();
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: created.data.id }),
);
// Off the urgent lane so the modal's close paints before the dashboard page
// mounts, instead of the modal sitting over it while that render runs.
startTransition(() => {
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: created.data.id }),
);
});
} catch (e) {
showErrorModal(e as APIError);
toast.error((e as AxiosError).toString() || 'Failed to create dashboard');
@@ -154,6 +158,7 @@ function BlankDashboardPanel({ onClose }: Props): JSX.Element {
color="primary"
size="md"
disabled={!canSubmit}
loading={submitting}
testId="create-dashboard-submit"
onClick={(): void => {
void handleCreate();

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { startTransition, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { red } from '@ant-design/colors';
@@ -81,9 +81,13 @@ function ImportJsonPanel({ onClose }: Props): JSX.Element {
const response = await createDashboardV2(payload);
void logEvent(DashboardListEvents.DashboardCreated, { method: 'import' });
onClose();
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
);
// As in the blank panel: let the modal's close paint before the dashboard
// page mounts.
startTransition(() => {
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
);
});
} catch (error) {
showErrorModal(error as APIError);
setIsCreateError(true);