Skip to content
Merged
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
83 changes: 82 additions & 1 deletion static/app/views/explore/logs/useSaveAsItems.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe('useSaveAsItems', () => {
});
});

it('should open save query modal when save as query is clicked', () => {
it('should open save query modal when save as new query is clicked', () => {
const {result} = renderHook(
() =>
useSaveAsItems({
Expand Down Expand Up @@ -132,6 +132,87 @@ describe('useSaveAsItems', () => {
});
});

it('should show both existing and new query options when saved query exists', async () => {
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/explore/saved/test-query-id/`,
body: {
id: 'test-query-id',
name: 'Test Query',
isPrebuilt: false,
query: [{}],
dateAdded: '2024-01-01T00:00:00.000Z',
dateUpdated: '2024-01-01T00:00:00.000Z',
interval: '5m',
lastVisited: '2024-01-01T00:00:00.000Z',
position: null,
projects: [1],
dataset: 'logs',
starred: false,
},
});

mockedUseLocation.mockReturnValue(
LocationFixture({
query: {
id: 'test-query-id',
logsFields: ['timestamp', 'message'],
logsQuery: 'message:"test"',
mode: 'aggregate',
},
})
);

const {result} = renderHook(
() =>
useSaveAsItems({
visualizes: [new VisualizeFunction('count()')],
groupBys: ['message.template'],
interval: '5m',
mode: Mode.AGGREGATE,
search: new MutableSearch('message:"test"'),
sortBys: [{field: 'timestamp', kind: 'desc'}],
}),
{wrapper: createWrapper()}
);

await waitFor(() => {
expect(result.current.some(item => item.key === 'update-query')).toBe(true);
});

const saveAsItems = result.current;
expect(saveAsItems.some(item => item.key === 'save-query')).toBe(true);
});

it('should show only new query option when no saved query exists', () => {
mockedUseLocation.mockReturnValue(
LocationFixture({
query: {
logsFields: ['timestamp', 'message'],
logsQuery: 'message:"test"',
mode: 'aggregate',
},
})
);

const {result} = renderHook(
() =>
useSaveAsItems({
visualizes: [new VisualizeFunction('count()')],
groupBys: ['message.template'],
interval: '5m',
mode: Mode.AGGREGATE,
search: new MutableSearch('message:"test"'),
sortBys: [{field: 'timestamp', kind: 'desc'}],
}),
{wrapper: createWrapper()}
);

const saveAsItems = result.current;

expect(saveAsItems.some(item => item.key === 'update-query')).toBe(false);
expect(saveAsItems.some(item => item.key === 'save-query')).toBe(true);
});

it('should call saveQuery with correct parameters when modal saves', async () => {
const {result} = renderHook(
() =>
Expand Down
15 changes: 9 additions & 6 deletions static/app/views/explore/logs/useSaveAsItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ export function useSaveAsItems({
);

const saveAsQuery = useMemo(() => {
// Show "Existing Query" if we have a non-prebuilt saved query, otherwise "A New Query"
const items = [];

if (defined(id) && savedQuery?.isPrebuilt === false) {
return {
items.push({
key: 'update-query',
textValue: t('Existing Query'),
label: <span>{t('Existing Query')}</span>,
Expand All @@ -98,10 +99,10 @@ export function useSaveAsItems({
Sentry.captureException(error);
}
},
};
});
}

return {
items.push({
key: 'save-query',
label: <span>{t('A New Query')}</span>,
textValue: t('A New Query'),
Expand All @@ -119,7 +120,9 @@ export function useSaveAsItems({
traceItemDataset: TraceItemDataset.LOGS,
});
},
};
});

return items;
}, [id, savedQuery?.isPrebuilt, updateQuery, saveQuery, organization]);

const saveAsAlert = useMemo(() => {
Expand Down Expand Up @@ -235,7 +238,7 @@ export function useSaveAsItems({
return useMemo(() => {
const saveAs = [];
if (isLogsEnabled(organization)) {
saveAs.push(saveAsQuery);
saveAs.push(...saveAsQuery);
saveAs.push(saveAsAlert);
saveAs.push(saveAsDashboard);
}
Expand Down
183 changes: 183 additions & 0 deletions static/app/views/explore/metrics/useSaveAsMetricItems.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import {LocationFixture} from 'sentry-fixture/locationFixture';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';

import {makeTestQueryClient} from 'sentry-test/queryClient';
import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary';

import * as modal from 'sentry/actionCreators/modal';
import ProjectsStore from 'sentry/stores/projectsStore';
import {QueryClientProvider} from 'sentry/utils/queryClient';
import {useLocation} from 'sentry/utils/useLocation';
import {useNavigate} from 'sentry/utils/useNavigate';
import {MockMetricQueryParamsContext} from 'sentry/views/explore/metrics/hooks/testUtils';
import {useSaveAsMetricItems} from 'sentry/views/explore/metrics/useSaveAsMetricItems';
import {OrganizationContext} from 'sentry/views/organizationContext';

jest.mock('sentry/utils/useLocation');
jest.mock('sentry/utils/useNavigate');
jest.mock('sentry/actionCreators/modal');

const mockedUseLocation = jest.mocked(useLocation);
const mockUseNavigate = jest.mocked(useNavigate);
const mockOpenSaveQueryModal = jest.mocked(modal.openSaveQueryModal);

describe('useSaveAsMetricItems', () => {
const organization = OrganizationFixture({
features: ['tracemetrics-enabled', 'tracemetrics-saved-queries'],
});
const project = ProjectFixture({id: '1'});
const queryClient = makeTestQueryClient();
ProjectsStore.loadInitialData([project]);

function createWrapper() {
return function ({children}: {children?: React.ReactNode}) {
return (
<OrganizationContext.Provider value={organization}>
<QueryClientProvider client={queryClient}>
<MockMetricQueryParamsContext>{children}</MockMetricQueryParamsContext>
</QueryClientProvider>
</OrganizationContext.Provider>
);
};
}

beforeEach(() => {
jest.resetAllMocks();
MockApiClient.clearMockResponses();
queryClient.clear();

mockedUseLocation.mockReturnValue(
LocationFixture({
query: {
interval: '5m',
},
})
);
mockUseNavigate.mockReturnValue(jest.fn());

MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/explore/saved/`,
method: 'POST',
body: {id: 'new-query-id', name: 'Test Query'},
});
});

it('should open save query modal when save as new query is clicked', () => {
const {result} = renderHook(
() =>
useSaveAsMetricItems({
interval: '5m',
}),
{wrapper: createWrapper()}
);

const saveAsItems = result.current;
const saveAsQuery = saveAsItems.find(item => item.key === 'save-query') as {
onAction: () => void;
};

saveAsQuery?.onAction?.();

expect(mockOpenSaveQueryModal).toHaveBeenCalledWith({
organization,
saveQuery: expect.any(Function),
source: 'table',
traceItemDataset: 'tracemetrics',
});
});

it('should show both existing and new query options when saved query exists', async () => {
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/explore/saved/test-query-id/`,
body: {
id: 'test-query-id',
name: 'Test Metrics Query',
isPrebuilt: false,
query: [{}],
dateAdded: '2024-01-01T00:00:00.000Z',
dateUpdated: '2024-01-01T00:00:00.000Z',
interval: '5m',
lastVisited: '2024-01-01T00:00:00.000Z',
position: null,
projects: [1],
dataset: 'tracemetrics',
starred: false,
},
});

mockedUseLocation.mockReturnValue(
LocationFixture({
query: {
id: 'test-query-id',
interval: '5m',
},
})
);

const {result} = renderHook(
() =>
useSaveAsMetricItems({
interval: '5m',
}),
{wrapper: createWrapper()}
);

await waitFor(() => {
expect(result.current.some(item => item.key === 'update-query')).toBe(true);
});

const saveAsItems = result.current;
expect(saveAsItems.some(item => item.key === 'save-query')).toBe(true);
});

it('should show only new query option when no saved query exists', () => {
mockedUseLocation.mockReturnValue(
LocationFixture({
query: {
interval: '5m',
},
})
);

const {result} = renderHook(
() =>
useSaveAsMetricItems({
interval: '5m',
}),
{wrapper: createWrapper()}
);

const saveAsItems = result.current;

expect(saveAsItems.some(item => item.key === 'update-query')).toBe(false);
expect(saveAsItems.some(item => item.key === 'save-query')).toBe(true);
});

it('should return empty array when metrics saved queries UI is not enabled', () => {
const orgWithoutFeature = OrganizationFixture({
features: [],
});

const {result} = renderHook(
() =>
useSaveAsMetricItems({
interval: '5m',
}),
{
wrapper: function ({children}: {children?: React.ReactNode}) {
return (
<OrganizationContext.Provider value={orgWithoutFeature}>
<QueryClientProvider client={queryClient}>
<MockMetricQueryParamsContext>{children}</MockMetricQueryParamsContext>
</QueryClientProvider>
</OrganizationContext.Provider>
);
},
}
);

const saveAsItems = result.current;
expect(saveAsItems).toEqual([]);
});
});
22 changes: 13 additions & 9 deletions static/app/views/explore/metrics/useSaveAsMetricItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
addSuccessMessage,
} from 'sentry/actionCreators/indicator';
import {openSaveQueryModal} from 'sentry/actionCreators/modal';
import type {MenuItemProps} from 'sentry/components/dropdownMenu';
import {t} from 'sentry/locale';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
Expand All @@ -31,12 +30,15 @@ export function useSaveAsMetricItems(_options: UseSaveAsMetricItemsOptions) {
const id = getIdFromLocation(location);
const {data: savedQuery} = useGetSavedQuery(id);

const saveAsQuery = useMemo(() => {
const saveAsItems = useMemo(() => {
if (!canUseMetricsSavedQueriesUI(organization)) {
return null;
return [];
}

const items = [];

if (defined(id) && savedQuery?.isPrebuilt === false) {
return {
items.push({
key: 'update-query',
textValue: t('Existing Query'),
label: <span>{t('Existing Query')}</span>,
Expand All @@ -55,10 +57,10 @@ export function useSaveAsMetricItems(_options: UseSaveAsMetricItemsOptions) {
Sentry.captureException(error);
}
},
};
});
}

return {
items.push({
key: 'save-query',
label: <span>{t('A New Query')}</span>,
textValue: t('A New Query'),
Expand All @@ -76,14 +78,16 @@ export function useSaveAsMetricItems(_options: UseSaveAsMetricItemsOptions) {
traceItemDataset: TraceItemDataset.TRACEMETRICS,
});
},
};
});

return items;
}, [id, savedQuery?.isPrebuilt, updateQuery, saveQuery, organization]);

// TODO: Implement alert functionality when organizations:tracemetrics-alerts flag is enabled

// TODO: Implement dashboard functionality when organizations:tracemetrics-dashboards flag is enabled

return useMemo(() => {
return [saveAsQuery].filter(Boolean) as MenuItemProps[];
}, [saveAsQuery]);
return saveAsItems;
}, [saveAsItems]);
}
Loading