123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802 |
- import {initializeOrg} from 'sentry-test/initializeOrg';
- import {
- render,
- renderGlobalModal,
- screen,
- userEvent,
- waitFor,
- } from 'sentry-test/reactTestingLibrary';
- import * as modal from 'sentry/actionCreators/modal';
- import * as LineChart from 'sentry/components/charts/lineChart';
- import SimpleTableChart from 'sentry/components/charts/simpleTableChart';
- import {MINUTE, SECOND} from 'sentry/utils/formatters';
- import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
- import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboards/types';
- import WidgetCard from 'sentry/views/dashboards/widgetCard';
- import ReleaseWidgetQueries from 'sentry/views/dashboards/widgetCard/releaseWidgetQueries';
- jest.mock('sentry/components/charts/simpleTableChart');
- jest.mock('sentry/views/dashboards/widgetCard/releaseWidgetQueries');
- describe('Dashboards > WidgetCard', function () {
- const {router, organization, routerContext} = initializeOrg({
- organization: TestStubs.Organization({
- features: ['dashboards-edit', 'discover-basic'],
- projects: [TestStubs.Project()],
- }),
- router: {orgId: 'orgId'},
- } as Parameters<typeof initializeOrg>[0]);
- const renderWithProviders = component =>
- render(
- <MEPSettingProvider forceTransactions={false}>{component}</MEPSettingProvider>,
- {organization, router, context: routerContext}
- );
- const multipleQueryWidget: Widget = {
- title: 'Errors',
- description: 'Valid widget description',
- interval: '5m',
- displayType: DisplayType.LINE,
- widgetType: WidgetType.DISCOVER,
- queries: [
- {
- conditions: 'event.type:error',
- fields: ['count()', 'failure_count()'],
- aggregates: ['count()', 'failure_count()'],
- columns: [],
- name: 'errors',
- orderby: '',
- },
- {
- conditions: 'event.type:default',
- fields: ['count()', 'failure_count()'],
- aggregates: ['count()', 'failure_count()'],
- columns: [],
- name: 'default',
- orderby: '',
- },
- ],
- };
- const selection = {
- projects: [1],
- environments: ['prod'],
- datetime: {
- period: '14d',
- start: null,
- end: null,
- utc: false,
- },
- };
- const api = new MockApiClient();
- let eventsMock;
- beforeEach(function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-stats/',
- body: {meta: {isMetricsData: false}},
- });
- eventsMock = MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events/',
- body: {
- meta: {fields: {title: 'string'}},
- data: [{title: 'title'}],
- },
- });
- });
- afterEach(function () {
- MockApiClient.clearMockResponses();
- });
- it('renders with Open in Discover button and opens the Query Selector Modal when clicked', async function () {
- const spy = jest.spyOn(modal, 'openDashboardWidgetQuerySelectorModal');
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={multipleQueryWidget}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- await userEvent.click(await screen.findByLabelText('Widget actions'));
- expect(screen.getByText('Open in Discover')).toBeInTheDocument();
- await userEvent.click(screen.getByText('Open in Discover'));
- expect(spy).toHaveBeenCalledWith({
- isMetricsData: false,
- organization,
- widget: multipleQueryWidget,
- });
- });
- it('renders with Open in Discover button and opens in Discover when clicked', async function () {
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{...multipleQueryWidget, queries: [multipleQueryWidget.queries[0]]}}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- await userEvent.click(await screen.findByLabelText('Widget actions'));
- expect(screen.getByText('Open in Discover')).toBeInTheDocument();
- await userEvent.click(screen.getByText('Open in Discover'));
- expect(router.push).toHaveBeenCalledWith(
- '/organizations/org-slug/discover/results/?environment=prod&field=count%28%29&field=failure_count%28%29&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=count%28%29&yAxis=failure_count%28%29'
- );
- });
- it('renders widget description in dashboard', async function () {
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={multipleQueryWidget}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- expect(await screen.findByText('Valid widget description')).toBeInTheDocument();
- });
- it('Opens in Discover with prepended fields pulled from equations', async function () {
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{
- ...multipleQueryWidget,
- queries: [
- {
- ...multipleQueryWidget.queries[0],
- fields: [
- 'equation|(count() + failure_count()) / count_if(transaction.duration,equals,300)',
- ],
- columns: [],
- aggregates: [
- 'equation|(count() + failure_count()) / count_if(transaction.duration,equals,300)',
- ],
- },
- ],
- }}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- await userEvent.click(await screen.findByLabelText('Widget actions'));
- expect(screen.getByText('Open in Discover')).toBeInTheDocument();
- await userEvent.click(screen.getByText('Open in Discover'));
- expect(router.push).toHaveBeenCalledWith(
- '/organizations/org-slug/discover/results/?environment=prod&field=count_if%28transaction.duration%2Cequals%2C300%29&field=failure_count%28%29&field=count%28%29&field=equation%7C%28count%28%29%20%2B%20failure_count%28%29%29%20%2F%20count_if%28transaction.duration%2Cequals%2C300%29&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=equation%7C%28count%28%29%20%2B%20failure_count%28%29%29%20%2F%20count_if%28transaction.duration%2Cequals%2C300%29'
- );
- });
- it('Opens in Discover with Top N', async function () {
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{
- ...multipleQueryWidget,
- displayType: DisplayType.TOP_N,
- queries: [
- {
- ...multipleQueryWidget.queries[0],
- fields: ['transaction', 'count()'],
- columns: ['transaction'],
- aggregates: ['count()'],
- },
- ],
- }}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- await userEvent.click(await screen.findByLabelText('Widget actions'));
- expect(screen.getByText('Open in Discover')).toBeInTheDocument();
- await userEvent.click(screen.getByText('Open in Discover'));
- expect(router.push).toHaveBeenCalledWith(
- '/organizations/org-slug/discover/results/?display=top5&environment=prod&field=transaction&field=count%28%29&name=Errors&project=1&query=event.type%3Aerror&statsPeriod=14d&yAxis=count%28%29'
- );
- });
- it('allows Open in Discover when the widget contains custom measurements', async function () {
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{
- ...multipleQueryWidget,
- displayType: DisplayType.LINE,
- queries: [
- {
- ...multipleQueryWidget.queries[0],
- conditions: '',
- fields: [],
- columns: [],
- aggregates: ['p99(measurements.custom.measurement)'],
- },
- ],
- }}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- await userEvent.click(await screen.findByLabelText('Widget actions'));
- expect(screen.getByText('Open in Discover')).toBeInTheDocument();
- await userEvent.click(screen.getByText('Open in Discover'));
- expect(router.push).toHaveBeenCalledWith(
- '/organizations/org-slug/discover/results/?environment=prod&field=p99%28measurements.custom.measurement%29&name=Errors&project=1&query=&statsPeriod=14d&yAxis=p99%28measurements.custom.measurement%29'
- );
- });
- it('calls onDuplicate when Duplicate Widget is clicked', async function () {
- const mock = jest.fn();
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{
- ...multipleQueryWidget,
- displayType: DisplayType.AREA,
- queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
- }}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={mock}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- await userEvent.click(await screen.findByLabelText('Widget actions'));
- expect(screen.getByText('Duplicate Widget')).toBeInTheDocument();
- await userEvent.click(screen.getByText('Duplicate Widget'));
- expect(mock).toHaveBeenCalledTimes(1);
- });
- it('does not add duplicate widgets if max widget is reached', async function () {
- const mock = jest.fn();
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{
- ...multipleQueryWidget,
- displayType: DisplayType.AREA,
- queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
- }}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={mock}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached
- />
- );
- await userEvent.click(await screen.findByLabelText('Widget actions'));
- expect(screen.getByText('Duplicate Widget')).toBeInTheDocument();
- await userEvent.click(screen.getByText('Duplicate Widget'));
- expect(mock).toHaveBeenCalledTimes(0);
- });
- it('calls onEdit when Edit Widget is clicked', async function () {
- const mock = jest.fn();
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{
- ...multipleQueryWidget,
- displayType: DisplayType.AREA,
- queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
- }}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={mock}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- await userEvent.click(await screen.findByLabelText('Widget actions'));
- expect(screen.getByText('Edit Widget')).toBeInTheDocument();
- await userEvent.click(screen.getByText('Edit Widget'));
- expect(mock).toHaveBeenCalledTimes(1);
- });
- it('renders delete widget option', async function () {
- const mock = jest.fn();
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{
- ...multipleQueryWidget,
- displayType: DisplayType.AREA,
- queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
- }}
- selection={selection}
- isEditing={false}
- onDelete={mock}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- await userEvent.click(await screen.findByLabelText('Widget actions'));
- expect(screen.getByText('Delete Widget')).toBeInTheDocument();
- await userEvent.click(screen.getByText('Delete Widget'));
- // Confirm Modal
- renderGlobalModal();
- await screen.findByRole('dialog');
- await userEvent.click(screen.getByTestId('confirm-button'));
- expect(mock).toHaveBeenCalled();
- });
- it('calls events with a limit of 20 items', async function () {
- const mock = jest.fn();
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{
- ...multipleQueryWidget,
- displayType: DisplayType.TABLE,
- queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
- }}
- selection={selection}
- isEditing={false}
- onDelete={mock}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- tableItemLimit={20}
- />
- );
- await waitFor(() => {
- expect(eventsMock).toHaveBeenCalledWith(
- '/organizations/org-slug/events/',
- expect.objectContaining({
- query: expect.objectContaining({
- per_page: 20,
- }),
- })
- );
- });
- });
- it('calls events with a default limit of 5 items', async function () {
- const mock = jest.fn();
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{
- ...multipleQueryWidget,
- displayType: DisplayType.TABLE,
- queries: [{...multipleQueryWidget.queries[0], fields: ['count()']}],
- }}
- selection={selection}
- isEditing={false}
- onDelete={mock}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- await waitFor(() => {
- expect(eventsMock).toHaveBeenCalledWith(
- '/organizations/org-slug/events/',
- expect.objectContaining({
- query: expect.objectContaining({
- per_page: 5,
- }),
- })
- );
- });
- });
- it('has sticky table headers', async function () {
- const tableWidget: Widget = {
- title: 'Table Widget',
- interval: '5m',
- displayType: DisplayType.TABLE,
- widgetType: WidgetType.DISCOVER,
- queries: [
- {
- conditions: '',
- fields: ['transaction', 'count()'],
- columns: ['transaction'],
- aggregates: ['count()'],
- name: 'Table',
- orderby: '',
- },
- ],
- };
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={tableWidget}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- tableItemLimit={20}
- />
- );
- await waitFor(() => expect(eventsMock).toHaveBeenCalled());
- expect(SimpleTableChart).toHaveBeenCalledWith(
- expect.objectContaining({stickyHeaders: true}),
- expect.anything()
- );
- });
- it('calls release queries', function () {
- const widget: Widget = {
- title: 'Release Widget',
- interval: '5m',
- displayType: DisplayType.LINE,
- widgetType: WidgetType.RELEASE,
- queries: [],
- };
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={widget}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- tableItemLimit={20}
- />
- );
- expect(ReleaseWidgetQueries).toHaveBeenCalledTimes(1);
- });
- it('opens the widget viewer modal when a widget has no id', async () => {
- const widget: Widget = {
- title: 'Widget',
- interval: '5m',
- displayType: DisplayType.LINE,
- widgetType: WidgetType.DISCOVER,
- queries: [],
- };
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={widget}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- index="10"
- isPreview
- />
- );
- await userEvent.click(await screen.findByLabelText('Open Widget Viewer'));
- expect(router.push).toHaveBeenCalledWith(
- expect.objectContaining({pathname: '/mock-pathname/widget/10/'})
- );
- });
- it('renders stored data disclaimer', async function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events/',
- body: {
- meta: {title: 'string', isMetricsData: false},
- data: [{title: 'title'}],
- },
- });
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={{
- ...organization,
- features: [...organization.features, 'dashboards-mep'],
- }}
- widget={{
- ...multipleQueryWidget,
- displayType: DisplayType.TABLE,
- queries: [{...multipleQueryWidget.queries[0]}],
- }}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- showStoredAlert
- />
- );
- // Badge in the widget header
- expect(await screen.findByText('Indexed')).toBeInTheDocument();
- expect(
- // Alert below the widget
- await screen.findByText(/we've automatically adjusted your results/i)
- ).toBeInTheDocument();
- });
- it('renders chart using axis and tooltip formatters from custom measurement meta', async function () {
- const spy = jest.spyOn(LineChart, 'LineChart');
- const eventsStatsMock = MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-stats/',
- body: {
- data: [
- [
- 1658262600,
- [
- {
- count: 24,
- },
- ],
- ],
- ],
- meta: {
- fields: {
- time: 'date',
- p95_measurements_custom: 'duration',
- },
- units: {
- time: null,
- p95_measurements_custom: 'millisecond',
- },
- isMetricsData: true,
- tips: {},
- },
- },
- });
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{
- title: '',
- interval: '5m',
- widgetType: WidgetType.DISCOVER,
- displayType: DisplayType.LINE,
- queries: [
- {
- conditions: '',
- name: '',
- fields: [],
- columns: [],
- aggregates: ['p95(measurements.custom)'],
- orderby: '',
- },
- ],
- }}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- await waitFor(function () {
- expect(eventsStatsMock).toHaveBeenCalled();
- });
- const {tooltip, yAxis} = spy.mock.calls.pop()?.[0] ?? {};
- expect(tooltip).toBeDefined();
- expect(yAxis).toBeDefined();
- // @ts-expect-error
- expect(tooltip.valueFormatter(24, 'p95(measurements.custom)')).toEqual('24.00ms');
- // @ts-expect-error
- expect(yAxis.axisLabel.formatter(24, 'p95(measurements.custom)')).toEqual('24ms');
- });
- it('renders label in seconds when there is a transition from seconds to minutes in the y axis', async function () {
- const spy = jest.spyOn(LineChart, 'LineChart');
- const eventsStatsMock = MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-stats/',
- body: {
- data: [
- [
- 1658262600,
- [
- {
- count: 40 * SECOND,
- },
- ],
- ],
- [
- 1658262601,
- [
- {
- count: 50 * SECOND,
- },
- ],
- ],
- [
- 1658262602,
- [
- {
- count: MINUTE,
- },
- ],
- ],
- [
- 1658262603,
- [
- {
- count: 1.3 * MINUTE,
- },
- ],
- ],
- ],
- meta: {
- fields: {
- time: 'date',
- p50_transaction_duration: 'duration',
- },
- units: {
- time: null,
- p50_transaction_duration: 'millisecond',
- },
- isMetricsData: false,
- tips: {},
- },
- },
- });
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={organization}
- widget={{
- title: '',
- interval: '5m',
- widgetType: WidgetType.DISCOVER,
- displayType: DisplayType.LINE,
- queries: [
- {
- conditions: '',
- name: '',
- fields: [],
- columns: [],
- aggregates: ['p50(transaction.duration)'],
- orderby: '',
- },
- ],
- }}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- />
- );
- await waitFor(function () {
- expect(eventsStatsMock).toHaveBeenCalled();
- });
- const {yAxis} = spy.mock.calls.pop()?.[0] ?? {};
- expect(yAxis).toBeDefined();
- // @ts-expect-error
- expect(yAxis.axisLabel.formatter(60000, 'p50(transaction.duration)')).toEqual('60s');
- expect((yAxis as any).minInterval).toEqual(SECOND);
- });
- it('displays indexed badge in preview mode', async function () {
- renderWithProviders(
- <WidgetCard
- api={api}
- organization={{
- ...organization,
- features: [...organization.features, 'dashboards-mep'],
- }}
- widget={multipleQueryWidget}
- selection={selection}
- isEditing={false}
- onDelete={() => undefined}
- onEdit={() => undefined}
- onDuplicate={() => undefined}
- renderErrorMessage={() => undefined}
- showContextMenu
- widgetLimitReached={false}
- isPreview
- />
- );
- expect(await screen.findByText('Indexed')).toBeInTheDocument();
- });
- });
|