123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535 |
- import type {Location} from 'history';
- import {LocationFixture} from 'sentry-fixture/locationFixture';
- import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
- import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
- import EventView from 'sentry/utils/discover/eventView';
- import {MutableSearch} from 'sentry/utils/tokenizeSearch';
- import CellAction, {Actions, updateQuery} from 'sentry/views/discover/table/cellAction';
- import type {TableColumn} from 'sentry/views/discover/table/types';
- const defaultData: TableDataRow = {
- transaction: 'best-transaction',
- count: 19,
- timestamp: '2020-06-09T01:46:25+00:00',
- release: 'F2520C43515BD1F0E8A6BD46233324641A370BF6',
- 'measurements.fcp': 1234,
- 'percentile(measurements.fcp, 0.5)': 1234,
- // TODO: Fix this type
- // @ts-ignore
- 'error.handled': [null],
- // TODO: Fix this type
- // @ts-ignore
- 'error.type': [
- 'ServerException',
- 'ClickhouseError',
- 'QueryException',
- 'QueryException',
- ],
- id: '42',
- };
- function renderComponent({
- eventView,
- handleCellAction = jest.fn(),
- columnIndex = 0,
- data = defaultData,
- }: {
- eventView: EventView;
- columnIndex?: number;
- data?: TableDataRow;
- handleCellAction?: (
- action: Actions,
- value: React.ReactText | null[] | string[] | null
- ) => void;
- }) {
- return render(
- <CellAction
- dataRow={data}
- column={eventView.getColumns()[columnIndex]}
- handleCellAction={handleCellAction}
- >
- <strong>some content</strong>
- </CellAction>
- );
- }
- describe('Discover -> CellAction', function () {
- const location: Location = LocationFixture({
- query: {
- id: '42',
- name: 'best query',
- field: [
- 'transaction',
- 'count()',
- 'timestamp',
- 'release',
- 'nullValue',
- 'measurements.fcp',
- 'percentile(measurements.fcp, 0.5)',
- 'error.handled',
- 'error.type',
- ],
- widths: ['437', '647', '416', '905'],
- sort: ['title'],
- query: 'event.type:transaction',
- project: ['123'],
- start: '2019-10-01T00:00:00',
- end: '2019-10-02T00:00:00',
- statsPeriod: '14d',
- environment: ['staging'],
- yAxis: 'p95',
- },
- });
- const view = EventView.fromLocation(location);
- async function openMenu() {
- await userEvent.click(screen.getByRole('button', {name: 'Actions'}));
- }
- describe('hover menu button', function () {
- it('shows no menu by default', function () {
- renderComponent({eventView: view});
- expect(screen.queryByRole('button', {name: 'Actions'})).toBeInTheDocument();
- });
- });
- describe('opening the menu', function () {
- it('toggles the menu on click', async function () {
- renderComponent({eventView: view});
- await openMenu();
- expect(
- screen.getByRole('menuitemradio', {name: 'Add to filter'})
- ).toBeInTheDocument();
- });
- });
- describe('per cell actions', function () {
- let handleCellAction;
- beforeEach(function () {
- handleCellAction = jest.fn();
- });
- it('add button appends condition', async function () {
- renderComponent({eventView: view, handleCellAction});
- await openMenu();
- await userEvent.click(screen.getByRole('menuitemradio', {name: 'Add to filter'}));
- expect(handleCellAction).toHaveBeenCalledWith('add', 'best-transaction');
- });
- it('exclude button adds condition', async function () {
- renderComponent({eventView: view, handleCellAction});
- await openMenu();
- await userEvent.click(
- screen.getByRole('menuitemradio', {name: 'Exclude from filter'})
- );
- expect(handleCellAction).toHaveBeenCalledWith('exclude', 'best-transaction');
- });
- it('exclude button appends exclusions', async function () {
- const excludeView = EventView.fromLocation(
- LocationFixture({
- query: {...location.query, query: '!transaction:nope'},
- })
- );
- renderComponent({eventView: excludeView, handleCellAction});
- await openMenu();
- await userEvent.click(
- screen.getByRole('menuitemradio', {name: 'Exclude from filter'})
- );
- expect(handleCellAction).toHaveBeenCalledWith('exclude', 'best-transaction');
- });
- it('go to release button goes to release health page', async function () {
- renderComponent({eventView: view, handleCellAction, columnIndex: 3});
- await openMenu();
- await userEvent.click(screen.getByRole('menuitemradio', {name: 'Go to release'}));
- expect(handleCellAction).toHaveBeenCalledWith(
- 'release',
- 'F2520C43515BD1F0E8A6BD46233324641A370BF6'
- );
- });
- it('greater than button adds condition', async function () {
- renderComponent({eventView: view, handleCellAction, columnIndex: 2});
- await openMenu();
- await userEvent.click(
- screen.getByRole('menuitemradio', {name: 'Show values greater than'})
- );
- expect(handleCellAction).toHaveBeenCalledWith(
- 'show_greater_than',
- '2020-06-09T01:46:25+00:00'
- );
- });
- it('less than button adds condition', async function () {
- renderComponent({eventView: view, handleCellAction, columnIndex: 2});
- await openMenu();
- await userEvent.click(
- screen.getByRole('menuitemradio', {name: 'Show values less than'})
- );
- expect(handleCellAction).toHaveBeenCalledWith(
- 'show_less_than',
- '2020-06-09T01:46:25+00:00'
- );
- });
- it('error.handled with null adds condition', async function () {
- renderComponent({
- eventView: view,
- handleCellAction,
- columnIndex: 7,
- data: defaultData,
- });
- await openMenu();
- await userEvent.click(screen.getByRole('menuitemradio', {name: 'Add to filter'}));
- expect(handleCellAction).toHaveBeenCalledWith('add', 1);
- });
- it('error.type with array values adds condition', async function () {
- renderComponent({
- eventView: view,
- handleCellAction,
- columnIndex: 8,
- data: defaultData,
- });
- await openMenu();
- await userEvent.click(screen.getByRole('menuitemradio', {name: 'Add to filter'}));
- expect(handleCellAction).toHaveBeenCalledWith('add', [
- 'ServerException',
- 'ClickhouseError',
- 'QueryException',
- 'QueryException',
- ]);
- });
- it('error.handled with 0 adds condition', async function () {
- renderComponent({
- eventView: view,
- handleCellAction,
- columnIndex: 7,
- data: {
- ...defaultData,
- // TODO: Fix this type
- // @ts-ignore
- 'error.handled': ['0'],
- },
- });
- await openMenu();
- await userEvent.click(screen.getByRole('menuitemradio', {name: 'Add to filter'}));
- expect(handleCellAction).toHaveBeenCalledWith('add', ['0']);
- });
- it('show appropriate actions for string cells', async function () {
- renderComponent({eventView: view, handleCellAction, columnIndex: 0});
- await openMenu();
- expect(
- screen.getByRole('menuitemradio', {name: 'Add to filter'})
- ).toBeInTheDocument();
- expect(
- screen.getByRole('menuitemradio', {name: 'Exclude from filter'})
- ).toBeInTheDocument();
- expect(
- screen.queryByRole('menuitemradio', {name: 'Show values greater than'})
- ).not.toBeInTheDocument();
- expect(
- screen.queryByRole('menuitemradio', {name: 'Show values less than'})
- ).not.toBeInTheDocument();
- });
- it('show appropriate actions for string cells with null values', async function () {
- renderComponent({eventView: view, handleCellAction, columnIndex: 4});
- await openMenu();
- expect(
- screen.getByRole('menuitemradio', {name: 'Add to filter'})
- ).toBeInTheDocument();
- expect(
- screen.getByRole('menuitemradio', {name: 'Exclude from filter'})
- ).toBeInTheDocument();
- });
- it('show appropriate actions for number cells', async function () {
- renderComponent({eventView: view, handleCellAction, columnIndex: 1});
- await openMenu();
- expect(
- screen.queryByRole('menuitemradio', {name: 'Add to filter'})
- ).not.toBeInTheDocument();
- expect(
- screen.queryByRole('menuitemradio', {name: 'Exclude from filter'})
- ).not.toBeInTheDocument();
- expect(
- screen.getByRole('menuitemradio', {name: 'Show values greater than'})
- ).toBeInTheDocument();
- expect(
- screen.getByRole('menuitemradio', {name: 'Show values less than'})
- ).toBeInTheDocument();
- });
- it('show appropriate actions for date cells', async function () {
- renderComponent({eventView: view, handleCellAction, columnIndex: 2});
- await openMenu();
- expect(
- screen.getByRole('menuitemradio', {name: 'Add to filter'})
- ).toBeInTheDocument();
- expect(
- screen.queryByRole('menuitemradio', {name: 'Exclude from filter'})
- ).not.toBeInTheDocument();
- expect(
- screen.getByRole('menuitemradio', {name: 'Show values greater than'})
- ).toBeInTheDocument();
- expect(
- screen.getByRole('menuitemradio', {name: 'Show values less than'})
- ).toBeInTheDocument();
- });
- it('show appropriate actions for release cells', async function () {
- renderComponent({eventView: view, handleCellAction, columnIndex: 3});
- await openMenu();
- expect(
- screen.getByRole('menuitemradio', {name: 'Go to release'})
- ).toBeInTheDocument();
- });
- it('show appropriate actions for empty release cells', async function () {
- renderComponent({
- eventView: view,
- handleCellAction,
- columnIndex: 3,
- // TODO: Fix this type
- // @ts-ignore
- data: {...defaultData, release: null},
- });
- await openMenu();
- expect(
- screen.queryByRole('menuitemradio', {name: 'Go to release'})
- ).not.toBeInTheDocument();
- });
- it('show appropriate actions for measurement cells', async function () {
- renderComponent({eventView: view, handleCellAction, columnIndex: 5});
- await openMenu();
- expect(
- screen.queryByRole('menuitemradio', {name: 'Add to filter'})
- ).not.toBeInTheDocument();
- expect(
- screen.queryByRole('menuitemradio', {name: 'Exclude from filter'})
- ).not.toBeInTheDocument();
- expect(
- screen.getByRole('menuitemradio', {name: 'Show values greater than'})
- ).toBeInTheDocument();
- expect(
- screen.getByRole('menuitemradio', {name: 'Show values less than'})
- ).toBeInTheDocument();
- });
- it('show appropriate actions for empty measurement cells', async function () {
- renderComponent({
- eventView: view,
- handleCellAction,
- columnIndex: 5,
- data: {
- ...defaultData,
- // TODO: Fix this type
- // @ts-ignore
- 'measurements.fcp': null,
- },
- });
- await openMenu();
- expect(
- screen.getByRole('menuitemradio', {name: 'Add to filter'})
- ).toBeInTheDocument();
- expect(
- screen.getByRole('menuitemradio', {name: 'Exclude from filter'})
- ).toBeInTheDocument();
- expect(
- screen.queryByRole('menuitemradio', {name: 'Show values greater than'})
- ).not.toBeInTheDocument();
- expect(
- screen.queryByRole('menuitemradio', {name: 'Show values less than'})
- ).not.toBeInTheDocument();
- });
- it('show appropriate actions for numeric function cells', async function () {
- renderComponent({eventView: view, handleCellAction, columnIndex: 6});
- await openMenu();
- expect(
- screen.getByRole('menuitemradio', {name: 'Show values greater than'})
- ).toBeInTheDocument();
- expect(
- screen.getByRole('menuitemradio', {name: 'Show values less than'})
- ).toBeInTheDocument();
- });
- it('show appropriate actions for empty numeric function cells', function () {
- renderComponent({
- eventView: view,
- handleCellAction,
- columnIndex: 6,
- data: {
- ...defaultData,
- // TODO: Fix this type
- // @ts-ignore
- 'percentile(measurements.fcp, 0.5)': null,
- },
- });
- expect(screen.queryByRole('button', {name: 'Actions'})).not.toBeInTheDocument();
- });
- });
- });
- describe('updateQuery()', function () {
- const columnA: TableColumn<keyof TableDataRow> = {
- key: 'a',
- name: 'a',
- type: 'number',
- isSortable: false,
- column: {
- kind: 'field',
- field: 'a',
- },
- width: -1,
- };
- const columnB: TableColumn<keyof TableDataRow> = {
- key: 'b',
- name: 'b',
- type: 'number',
- isSortable: false,
- column: {
- kind: 'field',
- field: 'b',
- },
- width: -1,
- };
- it('modifies the query with has/!has', function () {
- let results = new MutableSearch([]);
- // TODO: Fix this type
- // @ts-ignore
- updateQuery(results, Actions.ADD, columnA, null);
- expect(results.formatString()).toEqual('!has:a');
- // TODO: Fix this type
- // @ts-ignore
- updateQuery(results, Actions.EXCLUDE, columnA, null);
- expect(results.formatString()).toEqual('has:a');
- // TODO: Fix this type
- // @ts-ignore
- updateQuery(results, Actions.ADD, columnA, null);
- expect(results.formatString()).toEqual('!has:a');
- results = new MutableSearch([]);
- // TODO: Fix this type
- // @ts-ignore
- updateQuery(results, Actions.ADD, columnA, [null]);
- expect(results.formatString()).toEqual('!has:a');
- });
- it('modifies the query with additions', function () {
- const results = new MutableSearch([]);
- updateQuery(results, Actions.ADD, columnA, '1');
- expect(results.formatString()).toEqual('a:1');
- updateQuery(results, Actions.ADD, columnB, '1');
- expect(results.formatString()).toEqual('a:1 b:1');
- updateQuery(results, Actions.ADD, columnA, '2');
- expect(results.formatString()).toEqual('b:1 a:2');
- updateQuery(results, Actions.ADD, columnA, ['1', '2', '3']);
- expect(results.formatString()).toEqual('b:1 a:2 a:1 a:3');
- });
- it('modifies the query with exclusions', function () {
- const results = new MutableSearch([]);
- updateQuery(results, Actions.EXCLUDE, columnA, '1');
- expect(results.formatString()).toEqual('!a:1');
- updateQuery(results, Actions.EXCLUDE, columnB, '1');
- expect(results.formatString()).toEqual('!a:1 !b:1');
- updateQuery(results, Actions.EXCLUDE, columnA, '2');
- expect(results.formatString()).toEqual('!b:1 !a:1 !a:2');
- updateQuery(results, Actions.EXCLUDE, columnA, ['1', '2', '3']);
- expect(results.formatString()).toEqual('!b:1 !a:1 !a:2 !a:3');
- });
- it('modifies the query with a mix of additions and exclusions', function () {
- const results = new MutableSearch([]);
- updateQuery(results, Actions.ADD, columnA, '1');
- expect(results.formatString()).toEqual('a:1');
- updateQuery(results, Actions.ADD, columnB, '2');
- expect(results.formatString()).toEqual('a:1 b:2');
- updateQuery(results, Actions.EXCLUDE, columnA, '3');
- expect(results.formatString()).toEqual('b:2 !a:3');
- updateQuery(results, Actions.EXCLUDE, columnB, '4');
- expect(results.formatString()).toEqual('!a:3 !b:4');
- results.addFilterValues('!a', ['*dontescapeme*'], false);
- expect(results.formatString()).toEqual('!a:3 !b:4 !a:*dontescapeme*');
- updateQuery(results, Actions.EXCLUDE, columnA, '*escapeme*');
- expect(results.formatString()).toEqual(
- '!b:4 !a:3 !a:*dontescapeme* !a:"\\*escapeme\\*"'
- );
- updateQuery(results, Actions.ADD, columnA, '5');
- expect(results.formatString()).toEqual('!b:4 a:5');
- updateQuery(results, Actions.ADD, columnB, '6');
- expect(results.formatString()).toEqual('a:5 b:6');
- });
- it('modifies the query with greater/less than', function () {
- const results = new MutableSearch([]);
- updateQuery(results, Actions.SHOW_GREATER_THAN, columnA, 1);
- expect(results.formatString()).toEqual('a:>1');
- updateQuery(results, Actions.SHOW_GREATER_THAN, columnA, 2);
- expect(results.formatString()).toEqual('a:>2');
- updateQuery(results, Actions.SHOW_LESS_THAN, columnA, 3);
- expect(results.formatString()).toEqual('a:<3');
- updateQuery(results, Actions.SHOW_LESS_THAN, columnA, 4);
- expect(results.formatString()).toEqual('a:<4');
- });
- it('modifies the query with greater/less than on duration fields', function () {
- const columnADuration: TableColumn<keyof TableDataRow> = {
- ...columnA,
- type: 'duration',
- };
- const results = new MutableSearch([]);
- updateQuery(results, Actions.SHOW_GREATER_THAN, columnADuration, 1);
- expect(results.formatString()).toEqual('a:>1.00ms');
- updateQuery(results, Actions.SHOW_GREATER_THAN, columnADuration, 2);
- expect(results.formatString()).toEqual('a:>2.00ms');
- updateQuery(results, Actions.SHOW_LESS_THAN, columnADuration, 3);
- expect(results.formatString()).toEqual('a:<3.00ms');
- updateQuery(results, Actions.SHOW_LESS_THAN, columnADuration, 4.1234);
- expect(results.formatString()).toEqual('a:<4.12ms');
- });
- it('does not error for special actions', function () {
- const results = new MutableSearch([]);
- updateQuery(results, Actions.RELEASE, columnA, '');
- updateQuery(results, Actions.DRILLDOWN, columnA, '');
- });
- it('errors for unknown actions', function () {
- const results = new MutableSearch([]);
- // TODO: Fix this type
- // @ts-ignore
- expect(() => updateQuery(results, 'unknown', columnA, '')).toThrow();
- });
- });
|