Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions static/app/utils/url/replaceUrlWithoutNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as qs from 'query-string';

/**
* Replaces the current browser URL without going through the router.
*
* Updating the URL via `navigate()` notifies the router, which re-renders
* every `useLocation` subscriber (and their subtrees) even when the change is
* only meant to persist state in the URL. This helper writes the URL with
* `history.replaceState` so the address bar, refreshes, and copied links stay
* accurate without triggering any React re-renders.
*
* Trade-off: the router's in-memory location is NOT updated, so
* `useLocation().query` will not reflect params written this way. Only use
* this for params that are exclusively read back via component state (e.g. a
* store hydrated from the URL on mount), never for params other components
* read from the router location.
*/
export function replaceUrlWithoutNavigation(to: {
pathname: string;
query: Record<string, any>;
}) {
const search = qs.stringify(to.query);
const url = search ? `${to.pathname}?${search}` : to.pathname;

// Preserve the existing history state so the router's back/forward
// bookkeeping (e.g. react-router's `idx`) stays intact.
window.history.replaceState(window.history.state, '', url);
}
4 changes: 2 additions & 2 deletions static/app/views/dashboards/datasetConfig/traceMetrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
import {formatTraceMetricsFunction} from 'sentry/views/dashboards/datasetConfig/formatTraceMetricsFunction';
import {combineBaseFieldsWithTags} from 'sentry/views/dashboards/datasetConfig/utils/combineBaseFieldsWithEapTags';
import {DisplayType, type WidgetQuery} from 'sentry/views/dashboards/types';
import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {useWidgetBuilderStateSlice} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {useTraceMetricMultiMetricSelection} from 'sentry/views/dashboards/widgetBuilder/hooks/useTraceMetricMultiMetricSelection';
import {
extractTraceMetricFromColumn,
Expand Down Expand Up @@ -192,7 +192,7 @@ function useTraceMetricsSearchBarDataProvider(
}

function useTraceMetricsSearchScope() {
const {state: widgetBuilderState} = useWidgetBuilderContext();
const widgetBuilderState = useWidgetBuilderStateSlice('displayType', 'fields', 'yAxis');
const hasMultiMetricSelection = useTraceMetricMultiMetricSelection();

const aggregateSource = getTraceMetricAggregateSource(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import {Flex} from '@sentry/scraps/layout';

import {t} from 'sentry/locale';
import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {
useWidgetBuilderDispatch,
useWidgetBuilderStateSlice,
} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';

export function AxisRangeSection() {
const theme = useTheme();
const {state, dispatch} = useWidgetBuilderContext();
const state = useWidgetBuilderStateSlice('axisRange', 'dataset');
const dispatch = useWidgetBuilderDispatch();
const datasetConfig = getDatasetConfig(state.dataset);
const value = state.axisRange ?? datasetConfig.axisRange ?? 'auto';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,16 @@ import {OrganizationFixture} from 'sentry-fixture/organization';

import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';

import {useNavigate} from 'sentry/utils/useNavigate';
import {replaceUrlWithoutNavigation} from 'sentry/utils/url/replaceUrlWithoutNavigation';
import {WidgetBuilderDatasetSelector as DatasetSelector} from 'sentry/views/dashboards/widgetBuilder/components/datasetSelector';
import {WidgetBuilderProvider} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';

jest.mock('sentry/utils/useNavigate', () => ({
useNavigate: jest.fn(),
}));
jest.mock('sentry/utils/url/replaceUrlWithoutNavigation');

const mockUseNavigate = jest.mocked(useNavigate);
const mockReplaceUrl = jest.mocked(replaceUrlWithoutNavigation);

describe('DatasetSelector', () => {
it('changes the dataset', async () => {
const mockNavigate = jest.fn();
mockUseNavigate.mockReturnValue(mockNavigate);

render(
<WidgetBuilderProvider>
<DatasetSelector />
Expand All @@ -27,11 +22,10 @@ describe('DatasetSelector', () => {

await userEvent.click(await screen.findByRole('option', {name: 'Issues'}));

expect(mockNavigate).toHaveBeenCalledWith(
expect(mockReplaceUrl).toHaveBeenCalledWith(
expect.objectContaining({
query: expect.objectContaining({dataset: 'issue'}),
}),
expect.anything()
})
);
});

Expand Down Expand Up @@ -61,9 +55,6 @@ describe('DatasetSelector', () => {
});

it('allows selection of transactions dataset when discover-saved-queries-deprecation feature is disabled', async () => {
const mockNavigate = jest.fn();
mockUseNavigate.mockReturnValue(mockNavigate);

const organizationWithoutDeprecation = OrganizationFixture({
features: [], // No discover-saved-queries-deprecation feature
});
Expand All @@ -87,11 +78,10 @@ describe('DatasetSelector', () => {

await userEvent.click(transactionsOption);

expect(mockNavigate).toHaveBeenCalledWith(
expect(mockReplaceUrl).toHaveBeenCalledWith(
expect.objectContaining({
query: expect.objectContaining({dataset: 'transaction-like'}),
}),
expect.anything()
})
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEv
import {useOrganization} from 'sentry/utils/useOrganization';
import {WidgetType} from 'sentry/views/dashboards/types';
import {SectionHeader} from 'sentry/views/dashboards/widgetBuilder/components/common/sectionHeader';
import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {useWidgetBuilderStateSlice} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {useCacheBuilderState} from 'sentry/views/dashboards/widgetBuilder/hooks/useCacheBuilderState';
import {useDashboardWidgetSource} from 'sentry/views/dashboards/widgetBuilder/hooks/useDashboardWidgetSource';
import {useIsEditingWidget} from 'sentry/views/dashboards/widgetBuilder/hooks/useIsEditingWidget';
import {isLogsEnabled} from 'sentry/views/explore/logs/isLogsEnabled';

export function WidgetBuilderDatasetSelector() {
const organization = useOrganization();
const {state} = useWidgetBuilderContext();
const state = useWidgetBuilderStateSlice('dataset');
const source = useDashboardWidgetSource();
const isEditing = useIsEditingWidget();
const {cacheBuilderState, restoreOrSetBuilderState} = useCacheBuilderState();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {trackAnalytics} from 'sentry/utils/analytics';
import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents';
import {useOrganization} from 'sentry/utils/useOrganization';
import {DisplayType} from 'sentry/views/dashboards/types';
import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {
useWidgetBuilderDispatch,
useWidgetBuilderStateSlice,
} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {useDashboardWidgetSource} from 'sentry/views/dashboards/widgetBuilder/hooks/useDashboardWidgetSource';
import {useIsEditingWidget} from 'sentry/views/dashboards/widgetBuilder/hooks/useIsEditingWidget';
import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';
Expand All @@ -22,7 +25,13 @@ export function WidgetBuilderDescriptionField({
autosize = true,
}: WidgetBuilderDescriptionFieldProps) {
const organization = useOrganization();
const {state, dispatch} = useWidgetBuilderContext();
const state = useWidgetBuilderStateSlice(
'dataset',
'description',
'displayType',
'textContent'
);
const dispatch = useWidgetBuilderDispatch();
const isEditing = useIsEditingWidget();
const source = useDashboardWidgetSource();
const isTextWidget = state.displayType === DisplayType.TEXT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
import {WidgetType, type ValidateWidgetResponse} from 'sentry/views/dashboards/types';
import {GroupBySelector} from 'sentry/views/dashboards/widgetBuilder/buildSteps/groupByStep/groupBySelector';
import {SectionHeader} from 'sentry/views/dashboards/widgetBuilder/components/common/sectionHeader';
import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {
useWidgetBuilderDispatch,
useWidgetBuilderStateSlice,
} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {useDisableTransactionWidget} from 'sentry/views/dashboards/widgetBuilder/hooks/useDisableTransactionWidget';
import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';
import {useWidgetBuilderTraceItemConfig} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderTraceItemConfig';
Expand All @@ -25,7 +28,8 @@ interface WidgetBuilderGroupBySelectorProps {
export function WidgetBuilderGroupBySelector({
validatedWidgetResponse,
}: WidgetBuilderGroupBySelectorProps) {
const {state, dispatch} = useWidgetBuilderContext();
const state = useWidgetBuilderStateSlice('dataset', 'fields');
const dispatch = useWidgetBuilderDispatch();
const disableTransactionWidget = useDisableTransactionWidget();

const organization = useOrganization();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import {Flex} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';

import {t} from 'sentry/locale';
import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {
useWidgetBuilderDispatch,
useWidgetBuilderStateSlice,
} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';

export function LegendTypeSelector() {
const {state, dispatch} = useWidgetBuilderContext();
const state = useWidgetBuilderStateSlice('legendType');
const dispatch = useWidgetBuilderDispatch();

return (
<Flex as="label" align="center" gap="sm" cursor="pointer">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('WidgetBuilder', () => {
it('edits name and description', async () => {
setWindowLocation('http://localhost/organizations/org-slug/dashboard/1/?project=-1');

const {router} = render(
render(
<WidgetBuilderProvider>
<WidgetBuilderNameAndDescription />
</WidgetBuilderProvider>
Expand All @@ -18,11 +18,9 @@ describe('WidgetBuilder', () => {

// trigger blur
await userEvent.tab();
expect(router.location).toEqual(
expect.objectContaining({
query: expect.objectContaining({title: 'some name'}),
})
);
// The widget builder writes its params to the URL without notifying the
// router, so assert against the browser URL
expect(window.location.search).toContain('title=some%20name');

await userEvent.click(await screen.findByTestId('add-description'));

Expand All @@ -33,11 +31,7 @@ describe('WidgetBuilder', () => {

// trigger blur
await userEvent.tab();
expect(router.location).toEqual(
expect.objectContaining({
query: expect.objectContaining({description: 'some description'}),
})
);
expect(window.location.search).toContain('description=some%20description');
});

it('displays error', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {useOrganization} from 'sentry/utils/useOrganization';
import {DisplayType} from 'sentry/views/dashboards/types';
import {SectionHeader} from 'sentry/views/dashboards/widgetBuilder/components/common/sectionHeader';
import {WidgetBuilderDescriptionField} from 'sentry/views/dashboards/widgetBuilder/components/descriptionField';
import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {
useWidgetBuilderDispatch,
useWidgetBuilderStateSlice,
} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {useDashboardWidgetSource} from 'sentry/views/dashboards/widgetBuilder/hooks/useDashboardWidgetSource';
import {useIsEditingWidget} from 'sentry/views/dashboards/widgetBuilder/hooks/useIsEditingWidget';
import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';
Expand All @@ -26,7 +29,13 @@ export function WidgetBuilderNameAndDescription({
setError,
}: WidgetBuilderNameAndDescriptionProps) {
const organization = useOrganization();
const {state, dispatch} = useWidgetBuilderContext();
const state = useWidgetBuilderStateSlice(
'title',
'description',
'displayType',
'dataset'
);
const dispatch = useWidgetBuilderDispatch();
const [isDescSelected, setIsDescSelected] = useState(state.description ? true : false);
const isEditing = useIsEditingWidget();
const source = useDashboardWidgetSource();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import {closestCorners, DndContext, useDraggable, useDroppable} from '@dnd-kit/c
import {css, Global, useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import {AnimatePresence, motion, type MotionNodeAnimationOptions} from 'framer-motion';
import omit from 'lodash/omit';

import {Backdrop} from '@sentry/scraps/backdrop';
import {Flex} from '@sentry/scraps/layout';

import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {t} from 'sentry/locale';
import {CustomMeasurementsProvider} from 'sentry/utils/customMeasurements/customMeasurementsProvider';
import {defined} from 'sentry/utils/defined';
import {EventView} from 'sentry/utils/discover/eventView';
import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
Expand Down Expand Up @@ -46,7 +46,8 @@ import {WidgetBuilderFilterBar} from 'sentry/views/dashboards/widgetBuilder/comp
import {WidgetBuilderSlideout} from 'sentry/views/dashboards/widgetBuilder/components/widgetBuilderSlideout';
import {WidgetPreview} from 'sentry/views/dashboards/widgetBuilder/components/widgetPreview';
import {
useWidgetBuilderContext,
useWidgetBuilderStateSlice,
useWidgetBuilderUrlParams,
WidgetBuilderProvider,
} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import type {OnDataFetchedParams} from 'sentry/views/dashboards/widgetCard';
Expand Down Expand Up @@ -248,7 +249,8 @@ export function WidgetPreviewContainer({
onDataFetched?: (results: OnDataFetchedParams) => void;
openWidgetTemplates?: boolean;
}) {
const {state} = useWidgetBuilderContext();
const state = useWidgetBuilderStateSlice('displayType');
const builderUrlParams = useWidgetBuilderUrlParams();
const organization = useOrganization();
const location = useLocation();
const theme = useTheme();
Expand Down Expand Up @@ -283,18 +285,14 @@ export function WidgetPreviewContainer({
position: isDragEnabled ? 'fixed' : undefined,
};

// check if the state is in the url because the state variable has default values
const hasUrlParams =
Object.keys(
omit(location.query, [
'environment',
'project',
'release',
'start',
'end',
'statsPeriod',
])
).length > 0;
// check if the state is in the url because the state variable has default
// values. The store writes its params to the URL without notifying the
// router, so merge them over the router's (possibly stale) query.
const hasUrlParams = Object.entries({...location.query, ...builderUrlParams}).some(
([key, value]) =>
defined(value) &&
!['environment', 'project', 'release', 'start', 'end', 'statsPeriod'].includes(key)
);

const getPreviewHeight = () => {
if (isDragEnabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import {
} from 'sentry/views/dashboards/types';
import {SectionHeader} from 'sentry/views/dashboards/widgetBuilder/components/common/sectionHeader';
import {WidgetOnDemandQueryWarning} from 'sentry/views/dashboards/widgetBuilder/components/widgetOnDemandQueryWarning';
import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {
useWidgetBuilderDispatch,
useWidgetBuilderQueryState,
} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {useDashboardWidgetSource} from 'sentry/views/dashboards/widgetBuilder/hooks/useDashboardWidgetSource';
import {useDisableTransactionWidget} from 'sentry/views/dashboards/widgetBuilder/hooks/useDisableTransactionWidget';
import {useIsEditingWidget} from 'sentry/views/dashboards/widgetBuilder/hooks/useIsEditingWidget';
Expand All @@ -42,7 +45,8 @@ export function WidgetBuilderQueryFilterBuilder({
onQueryConditionChange,
validatedWidgetResponse,
}: WidgetBuilderQueryFilterBuilderProps) {
const {state, dispatch} = useWidgetBuilderContext();
const state = useWidgetBuilderQueryState();
const dispatch = useWidgetBuilderDispatch();
const {selection} = usePageFilters();
const organization = useOrganization();
const [queryConditionValidity, setQueryConditionValidity] = useState<boolean[]>(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {useOrganization} from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
import {type Widget, WidgetType} from 'sentry/views/dashboards/types';
import {flattenErrors} from 'sentry/views/dashboards/utils';
import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {useWidgetBuilderStore} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
import {useDisableTransactionWidget} from 'sentry/views/dashboards/widgetBuilder/hooks/useDisableTransactionWidget';
import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget';

Expand All @@ -25,14 +25,17 @@ export interface SaveButtonProps {
}

export function SaveButton({isEditing, onSave, setError}: SaveButtonProps) {
const {state} = useWidgetBuilderContext();
// The state is only needed when the button is clicked, so read it from the
// store on demand instead of subscribing to every state change
const store = useWidgetBuilderStore();
const {widgetIndex} = useParams();
const api = useApi();
const organization = useOrganization();
const [isSaving, setIsSaving] = useState(false);
const disableTransactionWidget = useDisableTransactionWidget();

const handleSave = async () => {
const state = store.getState();
trackAnalytics('dashboards_views.widget_builder.save', {
builder_version: WidgetBuilderVersion.SLIDEOUT,
data_set: state.dataset ?? '',
Expand Down
Loading
Loading