widgetBuilder.spec.tsx 82 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711
  1. import {urlEncode} from '@sentry/utils';
  2. import {DashboardFixture} from 'sentry-fixture/dashboard';
  3. import {LocationFixture} from 'sentry-fixture/locationFixture';
  4. import {MetricsFieldFixture} from 'sentry-fixture/metrics';
  5. import {ReleaseFixture} from 'sentry-fixture/release';
  6. import {SessionsFieldFixture} from 'sentry-fixture/sessions';
  7. import {TagsFixture} from 'sentry-fixture/tags';
  8. import {WidgetFixture} from 'sentry-fixture/widget';
  9. import {initializeOrg} from 'sentry-test/initializeOrg';
  10. import {
  11. act,
  12. render,
  13. renderGlobalModal,
  14. screen,
  15. userEvent,
  16. waitFor,
  17. } from 'sentry-test/reactTestingLibrary';
  18. import selectEvent from 'sentry-test/selectEvent';
  19. import * as modals from 'sentry/actionCreators/modal';
  20. import ProjectsStore from 'sentry/stores/projectsStore';
  21. import TagStore from 'sentry/stores/tagStore';
  22. import {DatasetSource, TOP_N} from 'sentry/utils/discover/types';
  23. import type {DashboardDetails, Widget} from 'sentry/views/dashboards/types';
  24. import {
  25. DashboardWidgetSource,
  26. DisplayType,
  27. WidgetType,
  28. } from 'sentry/views/dashboards/types';
  29. import type {WidgetBuilderProps} from 'sentry/views/dashboards/widgetBuilder';
  30. import WidgetBuilder from 'sentry/views/dashboards/widgetBuilder';
  31. import WidgetLegendSelectionState from '../widgetLegendSelectionState';
  32. const defaultOrgFeatures = [
  33. 'performance-view',
  34. 'dashboards-edit',
  35. 'global-views',
  36. 'dashboards-mep',
  37. ];
  38. function mockDashboard(dashboard: Partial<DashboardDetails>): DashboardDetails {
  39. return {
  40. id: '1',
  41. title: 'Dashboard',
  42. createdBy: undefined,
  43. dateCreated: '2020-01-01T00:00:00.000Z',
  44. widgets: [],
  45. projects: [],
  46. filters: {},
  47. ...dashboard,
  48. };
  49. }
  50. function renderTestComponent({
  51. dashboard,
  52. query,
  53. orgFeatures,
  54. onSave,
  55. params,
  56. updateDashboardSplitDecision,
  57. }: {
  58. dashboard?: WidgetBuilderProps['dashboard'];
  59. onSave?: WidgetBuilderProps['onSave'];
  60. orgFeatures?: string[];
  61. params?: Partial<WidgetBuilderProps['params']>;
  62. query?: Record<string, any>;
  63. updateDashboardSplitDecision?: WidgetBuilderProps['updateDashboardSplitDecision'];
  64. } = {}) {
  65. const {organization, projects, router} = initializeOrg({
  66. organization: {
  67. features: orgFeatures ?? defaultOrgFeatures,
  68. },
  69. router: {
  70. location: {
  71. query: {
  72. source: DashboardWidgetSource.DASHBOARDS,
  73. ...query,
  74. },
  75. },
  76. },
  77. });
  78. ProjectsStore.loadInitialData(projects);
  79. const widgetLegendState = new WidgetLegendSelectionState({
  80. location: LocationFixture(),
  81. dashboard: DashboardFixture([], {id: 'new', title: 'Dashboard', ...dashboard}),
  82. organization,
  83. router,
  84. });
  85. render(
  86. <WidgetBuilder
  87. route={{}}
  88. router={router}
  89. routes={router.routes}
  90. routeParams={router.params}
  91. location={router.location}
  92. dashboard={{
  93. id: 'new',
  94. title: 'Dashboard',
  95. createdBy: undefined,
  96. dateCreated: '2020-01-01T00:00:00.000Z',
  97. widgets: [],
  98. projects: [],
  99. filters: {},
  100. ...dashboard,
  101. }}
  102. onSave={onSave ?? jest.fn()}
  103. params={{
  104. orgId: organization.slug,
  105. dashboardId: dashboard?.id ?? 'new',
  106. ...params,
  107. }}
  108. updateDashboardSplitDecision={updateDashboardSplitDecision}
  109. widgetLegendState={widgetLegendState}
  110. />,
  111. {
  112. router,
  113. organization,
  114. }
  115. );
  116. return {router};
  117. }
  118. /**
  119. * This test suite contains tests that test the generic interactions
  120. * between most components in the WidgetBuilder. Tests for the
  121. * SortBy step can be found in (and should be added to)
  122. * ./widgetBuilderSortBy.spec.tsx and tests for specific dataset
  123. * behaviour can be found (and should be added to) ./widgetBuilderDataset.spec.tsx.
  124. * The test files are broken up to allow better parallelization
  125. * in CI (we currently parallelize files).
  126. */
  127. describe('WidgetBuilder', function () {
  128. const untitledDashboard: DashboardDetails = {
  129. id: '1',
  130. title: 'Untitled Dashboard',
  131. createdBy: undefined,
  132. dateCreated: '2020-01-01T00:00:00.000Z',
  133. widgets: [],
  134. projects: [],
  135. filters: {},
  136. };
  137. const testDashboard: DashboardDetails = {
  138. id: '2',
  139. title: 'Test Dashboard',
  140. createdBy: undefined,
  141. dateCreated: '2020-01-01T00:00:00.000Z',
  142. widgets: [],
  143. projects: [],
  144. filters: {},
  145. };
  146. let eventsStatsMock: jest.Mock | undefined;
  147. let eventsMock: jest.Mock | undefined;
  148. let tagsMock: jest.Mock | undefined;
  149. beforeEach(function () {
  150. MockApiClient.addMockResponse({
  151. url: '/organizations/org-slug/dashboards/',
  152. body: [
  153. {...untitledDashboard, widgetDisplay: [DisplayType.TABLE]},
  154. {...testDashboard, widgetDisplay: [DisplayType.AREA]},
  155. ],
  156. });
  157. MockApiClient.addMockResponse({
  158. url: '/organizations/org-slug/dashboards/widgets/',
  159. method: 'POST',
  160. statusCode: 200,
  161. body: [],
  162. });
  163. eventsMock = MockApiClient.addMockResponse({
  164. url: '/organizations/org-slug/events/',
  165. method: 'GET',
  166. statusCode: 200,
  167. body: {
  168. meta: {fields: {}},
  169. data: [],
  170. },
  171. });
  172. MockApiClient.addMockResponse({
  173. url: '/organizations/org-slug/projects/',
  174. method: 'GET',
  175. body: [],
  176. });
  177. MockApiClient.addMockResponse({
  178. url: '/organizations/org-slug/recent-searches/',
  179. method: 'GET',
  180. body: [],
  181. });
  182. MockApiClient.addMockResponse({
  183. url: '/organizations/org-slug/recent-searches/',
  184. method: 'POST',
  185. body: [],
  186. });
  187. MockApiClient.addMockResponse({
  188. url: '/organizations/org-slug/issues/',
  189. method: 'GET',
  190. body: [],
  191. });
  192. eventsStatsMock = MockApiClient.addMockResponse({
  193. url: '/organizations/org-slug/events-stats/',
  194. body: [],
  195. });
  196. MockApiClient.addMockResponse({
  197. url: '/organizations/org-slug/tags/event.type/values/',
  198. body: [{count: 2, name: 'Nvidia 1080ti'}],
  199. });
  200. MockApiClient.addMockResponse({
  201. url: '/organizations/org-slug/users/',
  202. body: [],
  203. });
  204. MockApiClient.addMockResponse({
  205. method: 'GET',
  206. url: '/organizations/org-slug/sessions/',
  207. body: SessionsFieldFixture(`sum(session)`),
  208. });
  209. MockApiClient.addMockResponse({
  210. method: 'GET',
  211. url: '/organizations/org-slug/metrics/data/',
  212. body: MetricsFieldFixture('session.all'),
  213. });
  214. tagsMock = MockApiClient.addMockResponse({
  215. url: '/organizations/org-slug/tags/',
  216. method: 'GET',
  217. body: TagsFixture(),
  218. });
  219. MockApiClient.addMockResponse({
  220. url: '/organizations/org-slug/measurements-meta/',
  221. method: 'GET',
  222. body: {},
  223. });
  224. MockApiClient.addMockResponse({
  225. url: '/organizations/org-slug/tags/is/values/',
  226. method: 'GET',
  227. body: [],
  228. });
  229. MockApiClient.addMockResponse({
  230. url: '/organizations/org-slug/tags/transaction.duration/values/',
  231. method: 'GET',
  232. body: [],
  233. });
  234. MockApiClient.addMockResponse({
  235. url: '/organizations/org-slug/releases/',
  236. body: [],
  237. });
  238. MockApiClient.addMockResponse({
  239. url: `/organizations/org-slug/spans/fields/`,
  240. body: [],
  241. });
  242. TagStore.reset();
  243. });
  244. afterEach(function () {
  245. MockApiClient.clearMockResponses();
  246. jest.clearAllMocks();
  247. });
  248. it('no feature access', function () {
  249. renderTestComponent({orgFeatures: []});
  250. expect(screen.getByText("You don't have access to this feature")).toBeInTheDocument();
  251. });
  252. it('widget not found', async function () {
  253. const widget: Widget = {
  254. displayType: DisplayType.AREA,
  255. interval: '1d',
  256. queries: [
  257. {
  258. name: 'Known Users',
  259. fields: [],
  260. columns: [],
  261. aggregates: [],
  262. conditions: '',
  263. orderby: '-time',
  264. },
  265. {
  266. name: 'Anonymous Users',
  267. fields: [],
  268. columns: [],
  269. aggregates: [],
  270. conditions: '',
  271. orderby: '-time',
  272. },
  273. ],
  274. title: 'Transactions',
  275. id: '1',
  276. };
  277. const dashboard = mockDashboard({widgets: [widget]});
  278. renderTestComponent({
  279. dashboard,
  280. orgFeatures: ['dashboards-edit'],
  281. params: {
  282. widgetIndex: '2', // Out of bounds, only one widget
  283. },
  284. });
  285. expect(
  286. await screen.findByText('The widget you want to edit was not found.')
  287. ).toBeInTheDocument();
  288. });
  289. it('renders a widget not found message if the widget index url is not an integer', async function () {
  290. const widget: Widget = {
  291. displayType: DisplayType.AREA,
  292. interval: '1d',
  293. queries: [
  294. {
  295. name: 'Known Users',
  296. fields: [],
  297. columns: [],
  298. aggregates: [],
  299. conditions: '',
  300. orderby: '-time',
  301. },
  302. ],
  303. title: 'Transactions',
  304. id: '1',
  305. };
  306. const dashboard = mockDashboard({widgets: [widget]});
  307. renderTestComponent({
  308. dashboard,
  309. orgFeatures: ['dashboards-edit'],
  310. params: {
  311. widgetIndex: '0.5', // Invalid index
  312. },
  313. });
  314. expect(
  315. await screen.findByText('The widget you want to edit was not found.')
  316. ).toBeInTheDocument();
  317. });
  318. it('renders', async function () {
  319. renderTestComponent();
  320. // Header - Breadcrumbs
  321. expect(await screen.findByRole('link', {name: 'Dashboards'})).toHaveAttribute(
  322. 'href',
  323. '/organizations/org-slug/dashboards/'
  324. );
  325. expect(screen.getByRole('link', {name: 'Dashboard'})).toHaveAttribute(
  326. 'href',
  327. '/organizations/org-slug/dashboards/new/'
  328. );
  329. expect(screen.getByText('Widget Builder')).toBeInTheDocument();
  330. // Header - Widget Title
  331. expect(screen.getByText('Custom Widget')).toBeInTheDocument();
  332. // Footer - Actions
  333. expect(screen.getByLabelText('Cancel')).toBeInTheDocument();
  334. expect(screen.getByLabelText('Add Widget')).toBeInTheDocument();
  335. // Content - Step 1
  336. expect(
  337. screen.getByRole('heading', {name: 'Choose your dataset'})
  338. ).toBeInTheDocument();
  339. expect(screen.getByLabelText('Errors and Transactions')).toBeChecked();
  340. // Content - Step 2
  341. expect(
  342. screen.getByRole('heading', {name: 'Choose your visualization'})
  343. ).toBeInTheDocument();
  344. // Content - Step 3
  345. expect(
  346. screen.getByRole('heading', {name: 'Choose your columns'})
  347. ).toBeInTheDocument();
  348. // Content - Step 4
  349. expect(
  350. screen.getByRole('heading', {name: 'Filter your results'})
  351. ).toBeInTheDocument();
  352. // Content - Step 5
  353. expect(screen.getByRole('heading', {name: 'Sort by a column'})).toBeInTheDocument();
  354. });
  355. it('has links back to the new dashboard if creating', async function () {
  356. // Dashboard has undefined dashboardId when creating from a new dashboard
  357. // because of route setup
  358. renderTestComponent({params: {dashboardId: undefined}});
  359. expect(await screen.findByLabelText('Cancel')).toHaveAttribute(
  360. 'href',
  361. '/organizations/org-slug/dashboards/new/'
  362. );
  363. });
  364. it('renders new design', async function () {
  365. renderTestComponent({
  366. orgFeatures: [...defaultOrgFeatures],
  367. });
  368. // Switch to line chart for time series
  369. await userEvent.click(screen.getByText('Table'));
  370. await userEvent.click(screen.getByText('Line Chart'));
  371. // Header - Breadcrumbs
  372. expect(await screen.findByRole('link', {name: 'Dashboards'})).toHaveAttribute(
  373. 'href',
  374. '/organizations/org-slug/dashboards/'
  375. );
  376. expect(screen.getByRole('link', {name: 'Dashboard'})).toHaveAttribute(
  377. 'href',
  378. '/organizations/org-slug/dashboards/new/'
  379. );
  380. expect(screen.getByText('Widget Builder')).toBeInTheDocument();
  381. // Header - Widget Title
  382. expect(screen.getByText('Custom Widget')).toBeInTheDocument();
  383. // Footer - Actions
  384. expect(screen.getByLabelText('Cancel')).toBeInTheDocument();
  385. expect(screen.getByLabelText('Add Widget')).toBeInTheDocument();
  386. // Content - Step 1
  387. expect(
  388. screen.getByRole('heading', {name: 'Choose your dataset'})
  389. ).toBeInTheDocument();
  390. expect(screen.getByLabelText('Errors and Transactions')).toBeChecked();
  391. // Content - Step 2
  392. expect(
  393. screen.getByRole('heading', {name: 'Choose your visualization'})
  394. ).toBeInTheDocument();
  395. // Content - Step 3
  396. expect(
  397. screen.getByRole('heading', {name: 'Choose what to plot in the y-axis'})
  398. ).toBeInTheDocument();
  399. // Content - Step 4
  400. expect(
  401. screen.getByRole('heading', {name: 'Filter your results'})
  402. ).toBeInTheDocument();
  403. // Content - Step 5
  404. expect(screen.getByRole('heading', {name: 'Group your results'})).toBeInTheDocument();
  405. });
  406. it('can update the title', async function () {
  407. renderTestComponent({
  408. query: {source: DashboardWidgetSource.DISCOVERV2},
  409. });
  410. const customWidgetLabels = await screen.findByText('Custom Widget');
  411. // EditableText and chart title
  412. expect(customWidgetLabels).toBeInTheDocument();
  413. await userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  414. await userEvent.click(screen.getByRole('textbox', {name: 'Widget title'}));
  415. await userEvent.paste('Unique Users');
  416. await userEvent.keyboard('{enter}');
  417. expect(screen.queryByText('Custom Widget')).not.toBeInTheDocument();
  418. expect(screen.getByText('Unique Users')).toBeInTheDocument();
  419. });
  420. it('can add query conditions', async function () {
  421. const {router} = renderTestComponent({
  422. query: {source: DashboardWidgetSource.DISCOVERV2},
  423. dashboard: testDashboard,
  424. });
  425. await userEvent.click(
  426. await screen.findByRole('combobox', {name: 'Add a search term'})
  427. );
  428. await userEvent.paste('color:blue');
  429. await userEvent.keyboard('{enter}');
  430. await userEvent.click(screen.getByText('Add Widget'));
  431. await waitFor(() => {
  432. expect(router.push).toHaveBeenCalledWith(
  433. expect.objectContaining({
  434. pathname: '/organizations/org-slug/dashboard/2/',
  435. query: {
  436. displayType: 'table',
  437. interval: '5m',
  438. title: 'Custom Widget',
  439. queryNames: [''],
  440. queryConditions: ['color:blue'],
  441. queryFields: ['count()'],
  442. queryOrderby: '-count()',
  443. start: null,
  444. end: null,
  445. statsPeriod: '24h',
  446. utc: null,
  447. project: [],
  448. environment: [],
  449. widgetType: 'discover',
  450. },
  451. })
  452. );
  453. });
  454. });
  455. it('can choose a field', async function () {
  456. const {router} = renderTestComponent({
  457. query: {source: DashboardWidgetSource.DISCOVERV2},
  458. dashboard: testDashboard,
  459. });
  460. expect(await screen.findByText('Custom Widget')).toBeInTheDocument();
  461. // No delete button as there is only one query.
  462. expect(screen.queryByLabelText('Remove query')).not.toBeInTheDocument();
  463. // 1 in the table header, 1 in the column selector, 1 in the sort field
  464. const countFields = screen.getAllByText('count()');
  465. expect(countFields).toHaveLength(3);
  466. await selectEvent.select(countFields[1], ['last_seen()']);
  467. await userEvent.click(screen.getByText('Add Widget'));
  468. await waitFor(() => {
  469. expect(router.push).toHaveBeenCalledWith(
  470. expect.objectContaining({
  471. pathname: '/organizations/org-slug/dashboard/2/',
  472. query: {
  473. displayType: 'table',
  474. interval: '5m',
  475. title: 'Custom Widget',
  476. queryNames: [''],
  477. queryConditions: [''],
  478. queryFields: ['last_seen()'],
  479. queryOrderby: '-last_seen()',
  480. start: null,
  481. end: null,
  482. statsPeriod: '24h',
  483. utc: null,
  484. project: [],
  485. environment: [],
  486. widgetType: 'discover',
  487. },
  488. })
  489. );
  490. });
  491. });
  492. it('can add additional fields', async function () {
  493. const handleSave = jest.fn();
  494. renderTestComponent({onSave: handleSave});
  495. await userEvent.click(await screen.findByText('Table'));
  496. // Select line chart display
  497. await userEvent.click(screen.getByText('Line Chart'));
  498. // Click the Add Series button
  499. await userEvent.click(screen.getByLabelText('Add Series'));
  500. await selectEvent.select(screen.getByText('(Required)'), ['count_unique(…)']);
  501. await userEvent.click(screen.getByLabelText('Add Widget'));
  502. await waitFor(() => {
  503. expect(handleSave).toHaveBeenCalledWith([
  504. expect.objectContaining({
  505. title: 'Custom Widget',
  506. displayType: DisplayType.LINE,
  507. interval: '5m',
  508. widgetType: WidgetType.DISCOVER,
  509. queries: [
  510. {
  511. conditions: '',
  512. fields: ['count()', 'count_unique(user)'],
  513. aggregates: ['count()', 'count_unique(user)'],
  514. fieldAliases: [],
  515. columns: [],
  516. orderby: '',
  517. name: '',
  518. },
  519. ],
  520. }),
  521. ]);
  522. });
  523. expect(handleSave).toHaveBeenCalledTimes(1);
  524. });
  525. it('can add additional fields and equation for Big Number with selection', async function () {
  526. renderTestComponent({
  527. query: {
  528. displayType: DisplayType.BIG_NUMBER,
  529. },
  530. orgFeatures: [...defaultOrgFeatures, 'dashboards-bignumber-equations'],
  531. });
  532. // Add new field
  533. await userEvent.click(screen.getByLabelText('Add Field'));
  534. expect(screen.getByText('(Required)')).toBeInTheDocument();
  535. await selectEvent.select(screen.getByText('(Required)'), ['count_unique(…)']);
  536. expect(screen.getByRole('radio', {name: 'field1'})).toBeChecked();
  537. // Add another new field
  538. await userEvent.click(screen.getByLabelText('Add Field'));
  539. expect(screen.getByText('(Required)')).toBeInTheDocument();
  540. await selectEvent.select(screen.getByText('(Required)'), ['eps()']);
  541. expect(screen.getByRole('radio', {name: 'field2'})).toBeChecked();
  542. // Add an equation
  543. await userEvent.click(screen.getByLabelText('Add an Equation'));
  544. expect(screen.getByPlaceholderText('Equation')).toBeInTheDocument();
  545. expect(screen.getByRole('radio', {name: 'field3'})).toBeChecked();
  546. await userEvent.click(screen.getByPlaceholderText('Equation'));
  547. await userEvent.paste('eps() + 100');
  548. // Check if right value is displayed from equation
  549. await userEvent.click(screen.getByPlaceholderText('Equation'));
  550. await userEvent.paste('2 * 100');
  551. expect(screen.getByText('200')).toBeInTheDocument();
  552. });
  553. it('can add equation fields', async function () {
  554. const handleSave = jest.fn();
  555. renderTestComponent({onSave: handleSave});
  556. await userEvent.click(await screen.findByText('Table'));
  557. // Select line chart display
  558. await userEvent.click(screen.getByText('Line Chart'));
  559. // Click the add an equation button
  560. await userEvent.click(screen.getByLabelText('Add an Equation'));
  561. expect(screen.getByPlaceholderText('Equation')).toBeInTheDocument();
  562. await userEvent.click(screen.getByPlaceholderText('Equation'));
  563. await userEvent.paste('count() + 100');
  564. await userEvent.click(screen.getByLabelText('Add Widget'));
  565. await waitFor(() => {
  566. expect(handleSave).toHaveBeenCalledWith([
  567. expect.objectContaining({
  568. title: 'Custom Widget',
  569. displayType: DisplayType.LINE,
  570. interval: '5m',
  571. widgetType: WidgetType.DISCOVER,
  572. queries: [
  573. {
  574. name: '',
  575. fields: ['count()', 'equation|count() + 100'],
  576. aggregates: ['count()', 'equation|count() + 100'],
  577. columns: [],
  578. fieldAliases: [],
  579. conditions: '',
  580. orderby: '',
  581. },
  582. ],
  583. }),
  584. ]);
  585. });
  586. expect(handleSave).toHaveBeenCalledTimes(1);
  587. });
  588. it('can respond to validation feedback', async function () {
  589. MockApiClient.addMockResponse({
  590. url: '/organizations/org-slug/dashboards/widgets/',
  591. method: 'POST',
  592. statusCode: 400,
  593. body: {
  594. title: ['This field may not be blank.'],
  595. },
  596. });
  597. renderTestComponent();
  598. await userEvent.click(await screen.findByText('Table'));
  599. const customWidgetLabels = await screen.findByText('Custom Widget');
  600. // EditableText and chart title
  601. expect(customWidgetLabels).toBeInTheDocument();
  602. await userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  603. await userEvent.click(screen.getByText('Add Widget'));
  604. await screen.findByText('This field may not be blank.');
  605. });
  606. it('sets up widget data in edit correctly', async function () {
  607. const widget: Widget = {
  608. id: '1',
  609. title: 'Errors over time',
  610. interval: '5m',
  611. displayType: DisplayType.LINE,
  612. queries: [
  613. {
  614. name: 'errors',
  615. conditions: 'event.type:error',
  616. fields: ['count()', 'count_unique(id)'],
  617. aggregates: ['count()', 'count_unique(id)'],
  618. columns: [],
  619. orderby: '',
  620. },
  621. {
  622. name: 'csp',
  623. conditions: 'event.type:csp',
  624. fields: ['count()', 'count_unique(id)'],
  625. aggregates: ['count()', 'count_unique(id)'],
  626. columns: [],
  627. orderby: '',
  628. },
  629. ],
  630. };
  631. const dashboard = mockDashboard({widgets: [widget]});
  632. renderTestComponent({dashboard, params: {widgetIndex: '0'}});
  633. await screen.findByText('Line Chart');
  634. // Should be in edit 'mode'
  635. expect(await screen.findByText('Update Widget')).toBeInTheDocument();
  636. // Should set widget data up.
  637. expect(screen.getByText('Update Widget')).toBeInTheDocument();
  638. // Filters
  639. expect(
  640. await screen.findAllByRole('grid', {name: 'Create a search query'})
  641. ).toHaveLength(2);
  642. expect(screen.getByRole('row', {name: 'event.type:csp'})).toBeInTheDocument();
  643. expect(screen.getByRole('row', {name: 'event.type:error'})).toBeInTheDocument();
  644. // Y-axis
  645. expect(screen.getAllByRole('button', {name: 'Remove query'})).toHaveLength(2);
  646. expect(screen.getByText('count()')).toBeInTheDocument();
  647. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  648. expect(screen.getByText('id')).toBeInTheDocument();
  649. // Expect events-stats endpoint to be called for each search conditions with
  650. // the same y-axis parameters
  651. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  652. 1,
  653. '/organizations/org-slug/events-stats/',
  654. expect.objectContaining({
  655. query: expect.objectContaining({
  656. query: 'event.type:error',
  657. yAxis: ['count()', 'count_unique(id)'],
  658. }),
  659. })
  660. );
  661. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  662. 2,
  663. '/organizations/org-slug/events-stats/',
  664. expect.objectContaining({
  665. query: expect.objectContaining({
  666. query: 'event.type:csp',
  667. yAxis: ['count()', 'count_unique(id)'],
  668. }),
  669. })
  670. );
  671. });
  672. it('can edit a widget', async function () {
  673. const widget: Widget = {
  674. id: '1',
  675. title: 'Errors over time',
  676. interval: '5m',
  677. displayType: DisplayType.LINE,
  678. queries: [
  679. {
  680. name: 'errors',
  681. conditions: 'event.type:error',
  682. fields: ['count()', 'count_unique(id)'],
  683. aggregates: ['count()', 'count_unique(id)'],
  684. columns: [],
  685. orderby: '',
  686. },
  687. {
  688. name: 'csp',
  689. conditions: 'event.type:csp',
  690. fields: ['count()', 'count_unique(id)'],
  691. aggregates: ['count()', 'count_unique(id)'],
  692. columns: [],
  693. orderby: '',
  694. },
  695. ],
  696. };
  697. const dashboard = mockDashboard({widgets: [widget]});
  698. const handleSave = jest.fn();
  699. renderTestComponent({onSave: handleSave, dashboard, params: {widgetIndex: '0'}});
  700. await screen.findByText('Line Chart');
  701. // Should be in edit 'mode'
  702. expect(screen.getByText('Update Widget')).toBeInTheDocument();
  703. const customWidgetLabels = screen.getByText(widget.title);
  704. // EditableText and chart title
  705. expect(customWidgetLabels).toBeInTheDocument();
  706. await userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  707. await userEvent.click(screen.getByRole('textbox', {name: 'Widget title'}));
  708. await userEvent.paste('New Title');
  709. await userEvent.click(screen.getByRole('button', {name: 'Update Widget'}));
  710. await waitFor(() => {
  711. expect(handleSave).toHaveBeenCalledWith([
  712. expect.objectContaining({
  713. ...widget,
  714. title: 'New Title',
  715. }),
  716. ]);
  717. });
  718. expect(handleSave).toHaveBeenCalledTimes(1);
  719. });
  720. it('renders column inputs for table widgets', async function () {
  721. const widget: Widget = {
  722. id: '0',
  723. title: 'sdk usage',
  724. interval: '5m',
  725. displayType: DisplayType.TABLE,
  726. queries: [
  727. {
  728. name: 'errors',
  729. conditions: 'event.type:error',
  730. fields: ['sdk.name', 'count()'],
  731. columns: ['sdk.name'],
  732. aggregates: ['count()'],
  733. orderby: '',
  734. },
  735. ],
  736. };
  737. const dashboard = mockDashboard({widgets: [widget]});
  738. renderTestComponent({dashboard, params: {widgetIndex: '0'}});
  739. // Should be in edit 'mode'
  740. expect(await screen.findByText('Update Widget')).toBeInTheDocument();
  741. // Should set widget data up.
  742. expect(screen.getByText(widget.title)).toBeInTheDocument();
  743. expect(screen.getByText('Table')).toBeInTheDocument();
  744. await screen.findByRole('grid', {name: 'Create a search query'});
  745. // Should have an orderby select
  746. expect(screen.getByText('Sort by a column')).toBeInTheDocument();
  747. // Add a column, and choose a value,
  748. expect(screen.getByLabelText('Add a Column')).toBeInTheDocument();
  749. });
  750. it('can save table widgets', async function () {
  751. const widget: Widget = {
  752. id: '0',
  753. title: 'sdk usage',
  754. interval: '5m',
  755. displayType: DisplayType.TABLE,
  756. queries: [
  757. {
  758. name: 'errors',
  759. conditions: 'event.type:error',
  760. fields: ['sdk.name', 'count()'],
  761. columns: ['sdk.name'],
  762. aggregates: ['count()'],
  763. orderby: '-count()',
  764. },
  765. ],
  766. };
  767. const dashboard = mockDashboard({widgets: [widget]});
  768. const handleSave = jest.fn();
  769. renderTestComponent({dashboard, onSave: handleSave, params: {widgetIndex: '0'}});
  770. // Should be in edit 'mode'
  771. expect(await screen.findByText('Update Widget')).toBeInTheDocument();
  772. // Add a column, and choose a value,
  773. await userEvent.click(screen.getByLabelText('Add a Column'));
  774. await selectEvent.select(screen.getByText('(Required)'), 'trace');
  775. // Save widget
  776. await userEvent.click(screen.getByLabelText('Update Widget'));
  777. await waitFor(() => {
  778. expect(handleSave).toHaveBeenCalledWith([
  779. expect.objectContaining({
  780. id: '0',
  781. title: 'sdk usage',
  782. displayType: DisplayType.TABLE,
  783. interval: '5m',
  784. queries: [
  785. {
  786. name: 'errors',
  787. conditions: 'event.type:error',
  788. fields: ['sdk.name', 'count()', 'trace'],
  789. aggregates: ['count()'],
  790. columns: ['sdk.name', 'trace'],
  791. orderby: '-count()',
  792. fieldAliases: ['', '', ''],
  793. },
  794. ],
  795. widgetType: WidgetType.DISCOVER,
  796. }),
  797. ]);
  798. });
  799. expect(handleSave).toHaveBeenCalledTimes(1);
  800. });
  801. it('should properly query for table fields', async function () {
  802. const defaultWidgetQuery = {
  803. name: '',
  804. fields: ['title', 'count()'],
  805. columns: ['title'],
  806. aggregates: ['count()'],
  807. conditions: '',
  808. orderby: '',
  809. };
  810. const defaultTableColumns = ['title', 'count()', 'count_unique(user)', 'epm()'];
  811. renderTestComponent({
  812. query: {
  813. source: DashboardWidgetSource.DISCOVERV2,
  814. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  815. displayType: DisplayType.LINE,
  816. defaultTableColumns,
  817. },
  818. });
  819. expect(await screen.findByText('Line Chart')).toBeInTheDocument();
  820. await userEvent.click(screen.getByText('Line Chart'));
  821. await userEvent.click(screen.getByText('Table'));
  822. await waitFor(() => {
  823. expect(eventsMock).toHaveBeenLastCalledWith(
  824. '/organizations/org-slug/events/',
  825. expect.objectContaining({
  826. query: expect.objectContaining({
  827. field: defaultTableColumns,
  828. }),
  829. })
  830. );
  831. });
  832. });
  833. it('should use defaultWidgetQuery Y-Axis and Conditions if given a defaultWidgetQuery', async function () {
  834. const defaultWidgetQuery = {
  835. name: '',
  836. fields: ['count()', 'failure_count()', 'count_unique(user)'],
  837. columns: [],
  838. aggregates: ['count()', 'failure_count()', 'count_unique(user)'],
  839. conditions: 'tag:value',
  840. orderby: '',
  841. };
  842. renderTestComponent({
  843. query: {
  844. source: DashboardWidgetSource.DISCOVERV2,
  845. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  846. },
  847. });
  848. expect(await screen.findByRole('row', {name: 'tag:value'})).toBeInTheDocument();
  849. // Table display, column, and sort field
  850. await waitFor(() => {
  851. expect(screen.getAllByText('count()')).toHaveLength(3);
  852. });
  853. // Table display and column
  854. expect(screen.getAllByText('failure_count()')).toHaveLength(2);
  855. // Table display
  856. expect(screen.getByText('count_unique(user)')).toBeInTheDocument();
  857. // Column
  858. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  859. // Column
  860. expect(screen.getByText('user')).toBeInTheDocument();
  861. });
  862. it('uses displayType if given a displayType', async function () {
  863. renderTestComponent({
  864. query: {
  865. displayType: DisplayType.BAR,
  866. },
  867. });
  868. expect(await screen.findByText('Bar Chart')).toBeInTheDocument();
  869. });
  870. it('deletes the widget when the modal is confirmed', async () => {
  871. const handleSave = jest.fn();
  872. const widget: Widget = {
  873. id: '1',
  874. title: 'Errors over time',
  875. interval: '5m',
  876. displayType: DisplayType.LINE,
  877. queries: [
  878. {
  879. name: 'errors',
  880. conditions: 'event.type:error',
  881. fields: ['count()', 'count_unique(id)'],
  882. aggregates: ['count()', 'count_unique(id)'],
  883. columns: [],
  884. orderby: '',
  885. },
  886. {
  887. name: 'csp',
  888. conditions: 'event.type:csp',
  889. fields: ['count()', 'count_unique(id)'],
  890. aggregates: ['count()', 'count_unique(id)'],
  891. columns: [],
  892. orderby: '',
  893. },
  894. ],
  895. };
  896. const dashboard = mockDashboard({widgets: [widget]});
  897. renderTestComponent({onSave: handleSave, dashboard, params: {widgetIndex: '0'}});
  898. await userEvent.click(await screen.findByText('Delete'));
  899. renderGlobalModal();
  900. await userEvent.click(await screen.findByText('Confirm'));
  901. await waitFor(() => {
  902. // The only widget was deleted
  903. expect(handleSave).toHaveBeenCalledWith([]);
  904. });
  905. expect(handleSave).toHaveBeenCalledTimes(1);
  906. });
  907. it('persists the page filter period when updating a widget', async () => {
  908. const widget: Widget = {
  909. id: '1',
  910. title: 'Errors over time',
  911. interval: '5m',
  912. displayType: DisplayType.LINE,
  913. queries: [
  914. {
  915. name: 'errors',
  916. conditions: 'event.type:error',
  917. fields: ['count()', 'count_unique(id)'],
  918. aggregates: ['count()', 'count_unique(id)'],
  919. columns: [],
  920. orderby: '',
  921. },
  922. ],
  923. };
  924. const dashboard = mockDashboard({widgets: [widget]});
  925. const {router} = renderTestComponent({
  926. dashboard,
  927. params: {orgId: 'org-slug', widgetIndex: '0'},
  928. query: {statsPeriod: '90d'},
  929. });
  930. await userEvent.click(screen.getByText('Update Widget'));
  931. await waitFor(() => {
  932. expect(router.push).toHaveBeenLastCalledWith(
  933. expect.objectContaining({
  934. pathname: '/organizations/org-slug/dashboard/1/',
  935. query: expect.objectContaining({
  936. statsPeriod: '90d',
  937. }),
  938. })
  939. );
  940. });
  941. });
  942. it('renders page filters in the filter step', async () => {
  943. const mockReleases = MockApiClient.addMockResponse({
  944. url: '/organizations/org-slug/releases/',
  945. body: [ReleaseFixture()],
  946. });
  947. renderTestComponent({
  948. params: {orgId: 'org-slug'},
  949. query: {statsPeriod: '90d'},
  950. orgFeatures: defaultOrgFeatures,
  951. });
  952. expect(await screen.findByTestId('page-filter-timerange-selector')).toBeDisabled();
  953. expect(screen.getByTestId('page-filter-environment-selector')).toBeDisabled();
  954. expect(screen.getByTestId('page-filter-project-selector')).toBeDisabled();
  955. expect(mockReleases).toHaveBeenCalled();
  956. expect(screen.getByRole('button', {name: /all releases/i})).toBeDisabled();
  957. });
  958. it('appends dashboard filters to widget builder fetch data request', async () => {
  959. MockApiClient.addMockResponse({
  960. url: '/organizations/org-slug/releases/',
  961. body: [ReleaseFixture()],
  962. });
  963. const mock = MockApiClient.addMockResponse({
  964. url: '/organizations/org-slug/events/',
  965. body: [],
  966. });
  967. renderTestComponent({
  968. dashboard: {
  969. id: 'new',
  970. title: 'Dashboard',
  971. createdBy: undefined,
  972. dateCreated: '2020-01-01T00:00:00.000Z',
  973. widgets: [],
  974. projects: [],
  975. filters: {release: ['abc@1.2.0']},
  976. },
  977. params: {orgId: 'org-slug'},
  978. query: {statsPeriod: '90d'},
  979. orgFeatures: defaultOrgFeatures,
  980. });
  981. await waitFor(() => {
  982. expect(mock).toHaveBeenCalledWith(
  983. '/organizations/org-slug/events/',
  984. expect.objectContaining({
  985. query: expect.objectContaining({
  986. query: ' release:"abc@1.2.0" ',
  987. }),
  988. })
  989. );
  990. });
  991. });
  992. it('does not error when query conditions field is blurred', async function () {
  993. const widget: Widget = {
  994. id: '0',
  995. title: 'sdk usage',
  996. interval: '5m',
  997. displayType: DisplayType.BAR,
  998. queries: [
  999. {
  1000. name: 'filled in',
  1001. conditions: 'event.type:error',
  1002. fields: ['count()', 'count_unique(id)'],
  1003. aggregates: ['count()', 'count_unique(id)'],
  1004. columns: [],
  1005. orderby: '-count()',
  1006. },
  1007. ],
  1008. };
  1009. const dashboard = mockDashboard({widgets: [widget]});
  1010. const handleSave = jest.fn();
  1011. renderTestComponent({dashboard, onSave: handleSave, params: {widgetIndex: '0'}});
  1012. await userEvent.click(await screen.findByLabelText('Add Query'), {delay: null});
  1013. // Triggering the onBlur of the new field should not error
  1014. await userEvent.click(
  1015. screen.getAllByPlaceholderText('Search for events, users, tags, and more')[1],
  1016. {delay: null}
  1017. );
  1018. await userEvent.keyboard('{Escape}', {delay: null});
  1019. // Run all timers because the handleBlur contains a setTimeout
  1020. await act(tick);
  1021. });
  1022. it('does not wipe column changes when filters are modified', async function () {
  1023. // widgetIndex: undefined means creating a new widget
  1024. renderTestComponent({params: {widgetIndex: undefined}});
  1025. await userEvent.click(await screen.findByLabelText('Add a Column'), {delay: null});
  1026. await selectEvent.select(screen.getByText('(Required)'), /project/);
  1027. // Triggering the onBlur of the filter should not error
  1028. await userEvent.click(
  1029. screen.getByPlaceholderText('Search for events, users, tags, and more'),
  1030. {delay: null}
  1031. );
  1032. await userEvent.keyboard('{enter}', {delay: null});
  1033. expect(await screen.findAllByText('project')).toHaveLength(2);
  1034. });
  1035. it('renders fields with commas properly', async () => {
  1036. const defaultWidgetQuery = {
  1037. conditions: '',
  1038. fields: ['equation|count_if(transaction.duration,equals,300)*2'],
  1039. aggregates: ['equation|count_if(transaction.duration,equals,300)*2'],
  1040. columns: [],
  1041. orderby: '',
  1042. name: '',
  1043. };
  1044. const defaultTableColumns = [
  1045. 'count_if(transaction.duration,equals,300)',
  1046. 'equation|count_if(transaction.duration,equals,300)*2',
  1047. ];
  1048. renderTestComponent({
  1049. query: {
  1050. source: DashboardWidgetSource.DISCOVERV2,
  1051. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  1052. defaultTableColumns,
  1053. yAxis: ['equation|count_if(transaction.duration,equals,300)*2'],
  1054. },
  1055. });
  1056. expect(
  1057. await screen.findByText('count_if(transaction.duration,equals,300)*2')
  1058. ).toBeInTheDocument();
  1059. });
  1060. it('sets the correct fields for a top n widget', async () => {
  1061. renderTestComponent({
  1062. orgFeatures: [...defaultOrgFeatures, 'performance-view'],
  1063. query: {
  1064. displayType: DisplayType.TOP_N,
  1065. },
  1066. });
  1067. // Top N now opens as Area Chart
  1068. expect(await screen.findByText('Area Chart')).toBeInTheDocument();
  1069. // Add a group by
  1070. await userEvent.click(screen.getByText('Add Series'));
  1071. await selectEvent.select(screen.getByText('Select group'), /project/);
  1072. // Change the y-axis
  1073. await selectEvent.select(screen.getAllByText('count()')[0], 'eps()');
  1074. await waitFor(() => {
  1075. expect(eventsStatsMock).toHaveBeenLastCalledWith(
  1076. '/organizations/org-slug/events-stats/',
  1077. expect.objectContaining({
  1078. query: expect.objectContaining({
  1079. query: '',
  1080. yAxis: ['eps()'],
  1081. field: ['project', 'eps()'],
  1082. topEvents: TOP_N,
  1083. orderby: '-eps()',
  1084. }),
  1085. })
  1086. );
  1087. });
  1088. });
  1089. it('fetches tags when tag store is empty', async function () {
  1090. renderTestComponent();
  1091. await waitFor(() => {
  1092. expect(tagsMock).toHaveBeenCalled();
  1093. });
  1094. });
  1095. it('does not fetch tags when tag store is not empty', async function () {
  1096. await act(async () => {
  1097. TagStore.loadTagsSuccess(TagsFixture());
  1098. renderTestComponent();
  1099. await tick();
  1100. });
  1101. expect(tagsMock).not.toHaveBeenCalled();
  1102. });
  1103. it('excludes the Other series when grouping and using multiple y-axes', async function () {
  1104. renderTestComponent({
  1105. orgFeatures: [...defaultOrgFeatures],
  1106. query: {
  1107. displayType: DisplayType.LINE,
  1108. },
  1109. });
  1110. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1111. await userEvent.click(screen.getByText('Add Series'));
  1112. await selectEvent.select(screen.getByText('(Required)'), /count_unique/);
  1113. await waitFor(() => {
  1114. expect(eventsStatsMock).toHaveBeenCalledWith(
  1115. '/organizations/org-slug/events-stats/',
  1116. expect.objectContaining({
  1117. query: expect.objectContaining({excludeOther: '1'}),
  1118. })
  1119. );
  1120. });
  1121. });
  1122. it('excludes the Other series when grouping and using multiple queries', async function () {
  1123. renderTestComponent({
  1124. orgFeatures: [...defaultOrgFeatures],
  1125. query: {
  1126. displayType: DisplayType.LINE,
  1127. },
  1128. });
  1129. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1130. await userEvent.click(screen.getByText('Add Query'));
  1131. await waitFor(() => {
  1132. expect(eventsStatsMock).toHaveBeenCalledWith(
  1133. '/organizations/org-slug/events-stats/',
  1134. expect.objectContaining({
  1135. query: expect.objectContaining({excludeOther: '1'}),
  1136. })
  1137. );
  1138. });
  1139. });
  1140. it('includes Other series when there is only one query and one y-axis', async function () {
  1141. renderTestComponent({
  1142. orgFeatures: [...defaultOrgFeatures],
  1143. query: {
  1144. displayType: DisplayType.LINE,
  1145. },
  1146. });
  1147. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1148. await waitFor(() => {
  1149. expect(eventsStatsMock).toHaveBeenCalledWith(
  1150. '/organizations/org-slug/events-stats/',
  1151. expect.objectContaining({
  1152. query: expect.not.objectContaining({excludeOther: '1'}),
  1153. })
  1154. );
  1155. });
  1156. });
  1157. it('decreases the limit when more y-axes and queries are added', async function () {
  1158. renderTestComponent({
  1159. orgFeatures: [...defaultOrgFeatures],
  1160. query: {
  1161. displayType: DisplayType.LINE,
  1162. },
  1163. });
  1164. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1165. screen.getByText('Limit to 5 results');
  1166. await userEvent.click(screen.getByText('Add Query'));
  1167. await userEvent.click(screen.getByText('Add Series'));
  1168. expect(screen.getByText('Limit to 2 results')).toBeInTheDocument();
  1169. });
  1170. it('alerts the user if there are unsaved title changes', async function () {
  1171. renderTestComponent();
  1172. window.confirm = jest.fn();
  1173. const customWidgetLabels = await screen.findByText('Custom Widget');
  1174. // EditableText and chart title
  1175. expect(customWidgetLabels).toBeInTheDocument();
  1176. // Change title text
  1177. await userEvent.clear(screen.getByRole('textbox', {name: 'Widget title'}));
  1178. await userEvent.click(screen.getByRole('textbox', {name: 'Widget title'}));
  1179. await userEvent.paste('Unique Users');
  1180. await userEvent.keyboard('{Enter}');
  1181. // Click Cancel
  1182. await userEvent.click(screen.getByText('Cancel'));
  1183. // Assert an alert was triggered
  1184. expect(window.confirm).toHaveBeenCalled();
  1185. });
  1186. it('alerts the user if there are unsaved description changes', async function () {
  1187. renderTestComponent();
  1188. window.confirm = jest.fn();
  1189. const descriptionTextArea = await screen.findByRole('textbox', {
  1190. name: 'Widget Description',
  1191. });
  1192. expect(descriptionTextArea).toBeInTheDocument();
  1193. expect(descriptionTextArea).toHaveAttribute(
  1194. 'placeholder',
  1195. 'Enter description (Optional)'
  1196. );
  1197. // Change description text
  1198. await userEvent.clear(descriptionTextArea);
  1199. await userEvent.click(descriptionTextArea);
  1200. await userEvent.paste('This is a description');
  1201. await userEvent.keyboard('{Enter}');
  1202. // Click Cancel
  1203. await userEvent.click(screen.getByText('Cancel'));
  1204. // Assert an alert was triggered
  1205. expect(window.confirm).toHaveBeenCalled();
  1206. });
  1207. it('does not trigger alert dialog if no changes', async function () {
  1208. renderTestComponent();
  1209. const alertMock = jest.spyOn(window, 'confirm');
  1210. await userEvent.click(await screen.findByText('Cancel'));
  1211. expect(alertMock).not.toHaveBeenCalled();
  1212. });
  1213. describe('Widget creation coming from other verticals', function () {
  1214. it('redirects correctly when creating a new dashboard', async function () {
  1215. const {router} = renderTestComponent({
  1216. query: {source: DashboardWidgetSource.DISCOVERV2},
  1217. });
  1218. await userEvent.click(await screen.findByText('Add Widget'));
  1219. await waitFor(() => {
  1220. expect(router.push).toHaveBeenCalledWith(
  1221. expect.objectContaining({
  1222. pathname: '/organizations/org-slug/dashboards/new/',
  1223. query: {
  1224. displayType: 'table',
  1225. interval: '5m',
  1226. title: 'Custom Widget',
  1227. queryNames: [''],
  1228. queryConditions: [''],
  1229. queryFields: ['count()'],
  1230. queryOrderby: '-count()',
  1231. start: null,
  1232. end: null,
  1233. statsPeriod: '24h',
  1234. utc: null,
  1235. project: [],
  1236. environment: [],
  1237. widgetType: 'discover',
  1238. },
  1239. })
  1240. );
  1241. });
  1242. });
  1243. it('redirects correctly when choosing an existing dashboard', async function () {
  1244. const {router} = renderTestComponent({
  1245. query: {source: DashboardWidgetSource.DISCOVERV2},
  1246. dashboard: testDashboard,
  1247. });
  1248. await userEvent.click(await screen.findByText('Add Widget'));
  1249. await waitFor(() => {
  1250. expect(router.push).toHaveBeenCalledWith(
  1251. expect.objectContaining({
  1252. pathname: '/organizations/org-slug/dashboard/2/',
  1253. query: {
  1254. displayType: 'table',
  1255. interval: '5m',
  1256. title: 'Custom Widget',
  1257. queryNames: [''],
  1258. queryConditions: [''],
  1259. queryFields: ['count()'],
  1260. queryOrderby: '-count()',
  1261. start: null,
  1262. end: null,
  1263. statsPeriod: '24h',
  1264. utc: null,
  1265. project: [],
  1266. environment: [],
  1267. widgetType: 'discover',
  1268. },
  1269. })
  1270. );
  1271. });
  1272. });
  1273. it('shows the correct orderby when switching from a line chart to table', async function () {
  1274. const defaultWidgetQuery = {
  1275. name: '',
  1276. fields: ['count_unique(user)'],
  1277. columns: [],
  1278. aggregates: ['count_unique(user)'],
  1279. conditions: '',
  1280. orderby: 'count_unique(user)',
  1281. };
  1282. const defaultTableColumns = ['title', 'count_unique(user)'];
  1283. renderTestComponent({
  1284. orgFeatures: [...defaultOrgFeatures],
  1285. query: {
  1286. source: DashboardWidgetSource.DISCOVERV2,
  1287. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  1288. displayType: DisplayType.LINE,
  1289. defaultTableColumns,
  1290. },
  1291. });
  1292. await userEvent.click(await screen.findByText('Line Chart'));
  1293. await userEvent.click(screen.getByText('Table'));
  1294. expect(screen.getAllByText('count_unique(user)')[0]).toBeInTheDocument();
  1295. await waitFor(() => {
  1296. expect(eventsMock).toHaveBeenLastCalledWith(
  1297. '/organizations/org-slug/events/',
  1298. expect.objectContaining({
  1299. query: expect.objectContaining({
  1300. field: defaultTableColumns,
  1301. sort: ['count_unique(user)'],
  1302. }),
  1303. })
  1304. );
  1305. });
  1306. });
  1307. it('does not send request with orderby if a timeseries chart without grouping', async function () {
  1308. const defaultWidgetQuery = {
  1309. name: '',
  1310. fields: ['count_unique(user)'],
  1311. columns: [],
  1312. aggregates: ['count_unique(user)'],
  1313. conditions: '',
  1314. orderby: 'count_unique(user)',
  1315. };
  1316. const defaultTableColumns = ['title', 'count_unique(user)'];
  1317. renderTestComponent({
  1318. orgFeatures: [...defaultOrgFeatures],
  1319. query: {
  1320. source: DashboardWidgetSource.DISCOVERV2,
  1321. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  1322. displayType: DisplayType.LINE,
  1323. defaultTableColumns,
  1324. },
  1325. });
  1326. await waitFor(() => {
  1327. expect(eventsStatsMock).toHaveBeenLastCalledWith(
  1328. '/organizations/org-slug/events-stats/',
  1329. expect.objectContaining({
  1330. query: expect.objectContaining({
  1331. orderby: '',
  1332. }),
  1333. })
  1334. );
  1335. });
  1336. });
  1337. });
  1338. it('opens top-N widgets as area display', async function () {
  1339. const widget: Widget = {
  1340. id: '1',
  1341. title: 'Errors over time',
  1342. interval: '5m',
  1343. displayType: DisplayType.TOP_N,
  1344. queries: [
  1345. {
  1346. name: '',
  1347. conditions: '',
  1348. fields: ['count()', 'count_unique(id)'],
  1349. aggregates: ['count()', 'count_unique(id)'],
  1350. columns: [],
  1351. orderby: '-count()',
  1352. },
  1353. ],
  1354. };
  1355. const dashboard = mockDashboard({widgets: [widget]});
  1356. renderTestComponent({
  1357. orgFeatures: [...defaultOrgFeatures],
  1358. dashboard,
  1359. params: {
  1360. widgetIndex: '0',
  1361. },
  1362. });
  1363. expect(await screen.findByText('Area Chart')).toBeInTheDocument();
  1364. });
  1365. it('Update table header values (field alias)', async function () {
  1366. const handleSave = jest.fn();
  1367. renderTestComponent({
  1368. onSave: handleSave,
  1369. orgFeatures: [...defaultOrgFeatures],
  1370. });
  1371. await userEvent.click(screen.getByPlaceholderText('Alias'));
  1372. await userEvent.paste('First Alias');
  1373. await userEvent.click(screen.getByLabelText('Add a Column'));
  1374. await userEvent.click(screen.getAllByPlaceholderText('Alias')[1]);
  1375. await userEvent.paste('Second Alias');
  1376. await userEvent.click(screen.getByText('Add Widget'));
  1377. await waitFor(() => {
  1378. expect(handleSave).toHaveBeenCalledWith([
  1379. expect.objectContaining({
  1380. queries: [
  1381. expect.objectContaining({fieldAliases: ['First Alias', 'Second Alias']}),
  1382. ],
  1383. }),
  1384. ]);
  1385. });
  1386. });
  1387. it('does not wipe equation aliases when a column alias is updated', async function () {
  1388. renderTestComponent({
  1389. orgFeatures: [...defaultOrgFeatures],
  1390. });
  1391. await userEvent.click(screen.getByText('Add an Equation'));
  1392. await userEvent.click(screen.getAllByPlaceholderText('Alias')[1]);
  1393. await userEvent.paste('This should persist');
  1394. await userEvent.type(screen.getAllByPlaceholderText('Alias')[0], 'A');
  1395. expect(await screen.findByText('This should persist')).toBeInTheDocument();
  1396. });
  1397. it('does not wipe equation aliases when a column selection is made', async function () {
  1398. renderTestComponent({
  1399. orgFeatures: [...defaultOrgFeatures],
  1400. });
  1401. await userEvent.click(screen.getByText('Add an Equation'));
  1402. await userEvent.click(screen.getAllByPlaceholderText('Alias')[1]);
  1403. await userEvent.paste('This should persist');
  1404. // 1 for the table, 1 for the column selector, 1 for the sort
  1405. await waitFor(() => expect(screen.getAllByText('count()')).toHaveLength(3));
  1406. await selectEvent.select(screen.getAllByText('count()')[1], /count_unique/);
  1407. expect(screen.getByText('This should persist')).toBeInTheDocument();
  1408. });
  1409. it('copies over the orderby from the previous query if adding another', async function () {
  1410. renderTestComponent({
  1411. orgFeatures: [...defaultOrgFeatures],
  1412. });
  1413. await userEvent.click(await screen.findByText('Table'));
  1414. await userEvent.click(screen.getByText('Line Chart'));
  1415. await selectEvent.select(screen.getByText('Select group'), 'project');
  1416. await selectEvent.select(screen.getAllByText('count()')[1], 'count_unique(…)');
  1417. MockApiClient.clearMockResponses();
  1418. eventsStatsMock = MockApiClient.addMockResponse({
  1419. url: '/organizations/org-slug/events-stats/',
  1420. body: [],
  1421. });
  1422. await userEvent.click(screen.getByText('Add Query'));
  1423. // Assert on two calls, one for each query
  1424. const expectedArgs = expect.objectContaining({
  1425. query: expect.objectContaining({
  1426. orderby: '-count_unique(user)',
  1427. }),
  1428. });
  1429. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1430. 1,
  1431. '/organizations/org-slug/events-stats/',
  1432. expectedArgs
  1433. );
  1434. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  1435. 2,
  1436. '/organizations/org-slug/events-stats/',
  1437. expectedArgs
  1438. );
  1439. });
  1440. it('disables add widget button and prevents widget previewing from firing widget query if widget query condition is invalid', async function () {
  1441. renderTestComponent({
  1442. orgFeatures: [...defaultOrgFeatures],
  1443. });
  1444. await userEvent.click(await screen.findByText('Table'));
  1445. await userEvent.click(screen.getByText('Line Chart'));
  1446. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  1447. await userEvent.click(
  1448. await screen.findByRole('combobox', {name: 'Add a search term'})
  1449. );
  1450. await userEvent.paste('transaction.duration:123a');
  1451. // Unfocus input
  1452. await userEvent.click(screen.getByText('Filter your results'));
  1453. expect(screen.getByText('Add Widget').closest('button')).toBeDisabled();
  1454. expect(screen.getByText('Widget query condition is invalid.')).toBeInTheDocument();
  1455. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  1456. });
  1457. describe('discover dataset split', function () {
  1458. let widget, dashboard;
  1459. describe('events', function () {
  1460. beforeEach(function () {
  1461. widget = {
  1462. displayType: DisplayType.TABLE,
  1463. interval: '1d',
  1464. queries: [
  1465. {
  1466. name: 'Test Widget',
  1467. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  1468. columns: ['project'],
  1469. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  1470. conditions: '',
  1471. orderby: '',
  1472. },
  1473. ],
  1474. title: 'Transactions',
  1475. id: '1',
  1476. };
  1477. dashboard = mockDashboard({widgets: [widget]});
  1478. });
  1479. it('selects the error discover split type as the dataset when the events request completes', async function () {
  1480. eventsMock = MockApiClient.addMockResponse({
  1481. url: '/organizations/org-slug/events/',
  1482. method: 'GET',
  1483. statusCode: 200,
  1484. body: {
  1485. meta: {discoverSplitDecision: WidgetType.ERRORS},
  1486. data: [],
  1487. },
  1488. });
  1489. const mockUpdateDashboardSplitDecision = jest.fn();
  1490. renderTestComponent({
  1491. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1492. dashboard,
  1493. params: {
  1494. widgetIndex: '0',
  1495. },
  1496. updateDashboardSplitDecision: mockUpdateDashboardSplitDecision,
  1497. });
  1498. await waitFor(() => {
  1499. expect(eventsMock).toHaveBeenCalled();
  1500. });
  1501. expect(screen.getByRole('radio', {name: /errors/i})).toBeChecked();
  1502. expect(mockUpdateDashboardSplitDecision).toHaveBeenCalledWith(
  1503. '1',
  1504. WidgetType.ERRORS
  1505. );
  1506. expect(
  1507. await screen.findByText(
  1508. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Errors. Edit as you see fit."
  1509. )
  1510. ).toBeInTheDocument();
  1511. });
  1512. it('selects the transaction discover split type as the dataset when the events request completes', async function () {
  1513. eventsMock = MockApiClient.addMockResponse({
  1514. url: '/organizations/org-slug/events/',
  1515. method: 'GET',
  1516. statusCode: 200,
  1517. body: {
  1518. meta: {discoverSplitDecision: WidgetType.TRANSACTIONS},
  1519. data: [],
  1520. },
  1521. });
  1522. const mockUpdateDashboardSplitDecision = jest.fn();
  1523. renderTestComponent({
  1524. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1525. dashboard,
  1526. params: {
  1527. widgetIndex: '0',
  1528. },
  1529. updateDashboardSplitDecision: mockUpdateDashboardSplitDecision,
  1530. });
  1531. await waitFor(() => {
  1532. expect(eventsMock).toHaveBeenCalled();
  1533. });
  1534. expect(screen.getByRole('radio', {name: /transactions/i})).toBeChecked();
  1535. expect(mockUpdateDashboardSplitDecision).toHaveBeenCalledWith(
  1536. '1',
  1537. WidgetType.TRANSACTIONS
  1538. );
  1539. expect(
  1540. await screen.findByText(
  1541. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Transactions. Edit as you see fit."
  1542. )
  1543. ).toBeInTheDocument();
  1544. });
  1545. it('persists the query state for tables when switching between errors and transactions', async function () {
  1546. dashboard = mockDashboard({
  1547. widgets: [
  1548. WidgetFixture({
  1549. displayType: DisplayType.TABLE,
  1550. widgetType: WidgetType.TRANSACTIONS,
  1551. queries: [
  1552. {
  1553. name: 'Test Widget',
  1554. fields: ['p99(transaction.duration)'],
  1555. columns: [],
  1556. aggregates: ['p99(transaction.duration)'],
  1557. conditions: 'testFilter:value',
  1558. orderby: '',
  1559. },
  1560. ],
  1561. }),
  1562. ],
  1563. });
  1564. renderTestComponent({
  1565. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1566. dashboard,
  1567. params: {
  1568. widgetIndex: '0',
  1569. },
  1570. });
  1571. expect(await screen.findByText(/p99\(…\)/i)).toBeInTheDocument();
  1572. expect(screen.getByText('transaction.duration')).toBeInTheDocument();
  1573. expect(screen.getByRole('row', {name: 'testFilter:value'})).toBeInTheDocument();
  1574. expect(screen.getByRole('radio', {name: /transactions/i})).toBeChecked();
  1575. // Switch to errors
  1576. await userEvent.click(screen.getByRole('radio', {name: /errors/i}));
  1577. expect(screen.getByRole('radio', {name: /transactions/i})).not.toBeChecked();
  1578. expect(screen.getByRole('radio', {name: /errors/i})).toBeChecked();
  1579. // The state is still the same
  1580. expect(await screen.findByText(/p99\(…\)/i)).toBeInTheDocument();
  1581. expect(screen.getByText('transaction.duration')).toBeInTheDocument();
  1582. expect(screen.getByRole('row', {name: 'testFilter:value'})).toBeInTheDocument();
  1583. });
  1584. it('sets the correct default count_if parameters for the errors dataset', async function () {
  1585. dashboard = mockDashboard({
  1586. widgets: [
  1587. WidgetFixture({
  1588. displayType: DisplayType.TABLE,
  1589. widgetType: WidgetType.ERRORS,
  1590. queries: [
  1591. {
  1592. name: 'Test Widget',
  1593. fields: ['count()'],
  1594. columns: [],
  1595. aggregates: ['count()'],
  1596. conditions: '',
  1597. orderby: '',
  1598. },
  1599. ],
  1600. }),
  1601. ],
  1602. });
  1603. renderTestComponent({
  1604. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1605. dashboard,
  1606. params: {
  1607. widgetIndex: '0',
  1608. },
  1609. });
  1610. await userEvent.click(await screen.findByTestId('label'));
  1611. await userEvent.click(screen.getByText(/count_if/));
  1612. const fieldLabels = screen.getAllByTestId('label');
  1613. expect(fieldLabels[0]).toHaveTextContent(/count_if/);
  1614. expect(fieldLabels[1]).toHaveTextContent('event.type');
  1615. expect(screen.getByDisplayValue('error')).toBeInTheDocument();
  1616. });
  1617. it('sets the correct default count_if parameters for the transactions dataset', async function () {
  1618. dashboard = mockDashboard({
  1619. widgets: [
  1620. WidgetFixture({
  1621. displayType: DisplayType.TABLE,
  1622. widgetType: WidgetType.TRANSACTIONS,
  1623. queries: [
  1624. {
  1625. name: 'Test Widget',
  1626. fields: ['count()'],
  1627. columns: [],
  1628. aggregates: ['count()'],
  1629. conditions: '',
  1630. orderby: '',
  1631. },
  1632. ],
  1633. }),
  1634. ],
  1635. });
  1636. renderTestComponent({
  1637. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1638. dashboard,
  1639. params: {
  1640. widgetIndex: '0',
  1641. },
  1642. });
  1643. await userEvent.click(await screen.findByTestId('label'));
  1644. await userEvent.click(screen.getByText(/count_if/));
  1645. const fieldLabels = screen.getAllByTestId('label');
  1646. expect(fieldLabels[0]).toHaveTextContent(/count_if/);
  1647. expect(fieldLabels[1]).toHaveTextContent('transaction.duration');
  1648. expect(screen.getByDisplayValue('300')).toBeInTheDocument();
  1649. });
  1650. });
  1651. describe('events-stats', function () {
  1652. beforeEach(function () {
  1653. widget = {
  1654. displayType: DisplayType.LINE,
  1655. interval: '1d',
  1656. queries: [
  1657. {
  1658. name: 'Test Widget',
  1659. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  1660. columns: ['project'],
  1661. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  1662. conditions: '',
  1663. orderby: '',
  1664. },
  1665. ],
  1666. title: 'Transactions',
  1667. id: '1',
  1668. };
  1669. dashboard = mockDashboard({widgets: [widget]});
  1670. });
  1671. it('selects the error discover split type as the dataset when the request completes', async function () {
  1672. eventsStatsMock = MockApiClient.addMockResponse({
  1673. url: '/organizations/org-slug/events-stats/',
  1674. method: 'GET',
  1675. statusCode: 200,
  1676. body: {
  1677. meta: {discoverSplitDecision: WidgetType.ERRORS},
  1678. data: [],
  1679. },
  1680. });
  1681. const mockUpdateDashboardSplitDecision = jest.fn();
  1682. renderTestComponent({
  1683. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1684. dashboard,
  1685. params: {
  1686. widgetIndex: '0',
  1687. },
  1688. updateDashboardSplitDecision: mockUpdateDashboardSplitDecision,
  1689. });
  1690. await waitFor(() => {
  1691. expect(eventsStatsMock).toHaveBeenCalled();
  1692. });
  1693. expect(screen.getByRole('radio', {name: /errors/i})).toBeChecked();
  1694. expect(mockUpdateDashboardSplitDecision).toHaveBeenCalledWith(
  1695. '1',
  1696. WidgetType.ERRORS
  1697. );
  1698. expect(
  1699. await screen.findByText(
  1700. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Errors. Edit as you see fit."
  1701. )
  1702. ).toBeInTheDocument();
  1703. });
  1704. it('selects the transaction discover split type as the dataset when the request completes', async function () {
  1705. eventsStatsMock = MockApiClient.addMockResponse({
  1706. url: '/organizations/org-slug/events-stats/',
  1707. method: 'GET',
  1708. statusCode: 200,
  1709. body: {
  1710. meta: {discoverSplitDecision: WidgetType.TRANSACTIONS},
  1711. data: [],
  1712. },
  1713. });
  1714. const mockUpdateDashboardSplitDecision = jest.fn();
  1715. renderTestComponent({
  1716. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1717. dashboard,
  1718. params: {
  1719. widgetIndex: '0',
  1720. },
  1721. updateDashboardSplitDecision: mockUpdateDashboardSplitDecision,
  1722. });
  1723. await waitFor(() => {
  1724. expect(eventsStatsMock).toHaveBeenCalled();
  1725. });
  1726. expect(screen.getByRole('radio', {name: /transactions/i})).toBeChecked();
  1727. expect(mockUpdateDashboardSplitDecision).toHaveBeenCalledWith(
  1728. '1',
  1729. WidgetType.TRANSACTIONS
  1730. );
  1731. expect(
  1732. await screen.findByText(
  1733. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Transactions. Edit as you see fit."
  1734. )
  1735. ).toBeInTheDocument();
  1736. });
  1737. it('persists the query state for timeseries when switching between errors and transactions', async function () {
  1738. dashboard = mockDashboard({
  1739. widgets: [
  1740. WidgetFixture({
  1741. displayType: DisplayType.LINE,
  1742. widgetType: WidgetType.TRANSACTIONS,
  1743. queries: [
  1744. {
  1745. name: 'Test Widget',
  1746. fields: ['p99(transaction.duration)'],
  1747. columns: [],
  1748. aggregates: ['p99(transaction.duration)'],
  1749. conditions: 'testFilter:value',
  1750. orderby: '',
  1751. },
  1752. ],
  1753. }),
  1754. ],
  1755. });
  1756. renderTestComponent({
  1757. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1758. dashboard,
  1759. params: {
  1760. widgetIndex: '0',
  1761. },
  1762. });
  1763. expect(await screen.findByText(/p99\(…\)/i)).toBeInTheDocument();
  1764. expect(screen.getByText('transaction.duration')).toBeInTheDocument();
  1765. expect(screen.getByRole('row', {name: 'testFilter:value'})).toBeInTheDocument(); // Check for query builder token
  1766. expect(screen.getByRole('radio', {name: /transactions/i})).toBeChecked();
  1767. // Switch to errors
  1768. await userEvent.click(screen.getByRole('radio', {name: /errors/i}));
  1769. expect(screen.getByRole('radio', {name: /transactions/i})).not.toBeChecked();
  1770. expect(screen.getByRole('radio', {name: /errors/i})).toBeChecked();
  1771. // The state is still the same
  1772. expect(await screen.findByText(/p99\(…\)/i)).toBeInTheDocument();
  1773. expect(screen.getByText('transaction.duration')).toBeInTheDocument();
  1774. expect(screen.getByRole('row', {name: 'testFilter:value'})).toBeInTheDocument();
  1775. });
  1776. it('sets the correct default count_if parameters for the errors dataset', async function () {
  1777. dashboard = mockDashboard({
  1778. widgets: [
  1779. WidgetFixture({
  1780. displayType: DisplayType.LINE,
  1781. widgetType: WidgetType.ERRORS,
  1782. queries: [
  1783. {
  1784. name: 'Test Widget',
  1785. fields: ['count()'],
  1786. columns: [],
  1787. aggregates: ['count()'],
  1788. conditions: '',
  1789. orderby: '',
  1790. },
  1791. ],
  1792. }),
  1793. ],
  1794. });
  1795. renderTestComponent({
  1796. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1797. dashboard,
  1798. params: {
  1799. widgetIndex: '0',
  1800. },
  1801. });
  1802. await userEvent.click(await screen.findByTestId('label'));
  1803. await userEvent.click(screen.getByText(/count_if/));
  1804. const fieldLabels = screen.getAllByTestId('label');
  1805. expect(fieldLabels[0]).toHaveTextContent(/count_if/);
  1806. expect(fieldLabels[1]).toHaveTextContent('event.type');
  1807. expect(screen.getByDisplayValue('error')).toBeInTheDocument();
  1808. });
  1809. it('sets the correct default count_if parameters for the transactions dataset', async function () {
  1810. dashboard = mockDashboard({
  1811. widgets: [
  1812. WidgetFixture({
  1813. displayType: DisplayType.LINE,
  1814. widgetType: WidgetType.TRANSACTIONS,
  1815. queries: [
  1816. {
  1817. name: 'Test Widget',
  1818. fields: ['count()'],
  1819. columns: [],
  1820. aggregates: ['count()'],
  1821. conditions: '',
  1822. orderby: '',
  1823. },
  1824. ],
  1825. }),
  1826. ],
  1827. });
  1828. renderTestComponent({
  1829. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1830. dashboard,
  1831. params: {
  1832. widgetIndex: '0',
  1833. },
  1834. });
  1835. await userEvent.click(await screen.findByTestId('label'));
  1836. await userEvent.click(screen.getByText(/count_if/));
  1837. const fieldLabels = screen.getAllByTestId('label');
  1838. expect(fieldLabels[0]).toHaveTextContent(/count_if/);
  1839. expect(fieldLabels[1]).toHaveTextContent('transaction.duration');
  1840. expect(screen.getByDisplayValue('300')).toBeInTheDocument();
  1841. });
  1842. });
  1843. describe('discover split warning', function () {
  1844. it('does not show the alert if the widget type is already split', async function () {
  1845. dashboard = mockDashboard({
  1846. widgets: [WidgetFixture({widgetType: WidgetType.TRANSACTIONS})],
  1847. });
  1848. eventsStatsMock = MockApiClient.addMockResponse({
  1849. url: '/organizations/org-slug/events-stats/',
  1850. method: 'GET',
  1851. statusCode: 200,
  1852. body: {
  1853. meta: {},
  1854. data: [],
  1855. },
  1856. });
  1857. renderTestComponent({
  1858. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1859. dashboard,
  1860. params: {
  1861. widgetIndex: '0',
  1862. },
  1863. });
  1864. await waitFor(() => {
  1865. expect(screen.getByRole('radio', {name: /transactions/i})).toBeChecked();
  1866. });
  1867. expect(
  1868. screen.queryByText(/we're splitting our datasets/i)
  1869. ).not.toBeInTheDocument();
  1870. });
  1871. it('shows the alert if the widget is split but the decision is forced', async function () {
  1872. dashboard = mockDashboard({
  1873. widgets: [
  1874. WidgetFixture({
  1875. widgetType: WidgetType.ERRORS,
  1876. datasetSource: DatasetSource.FORCED,
  1877. }),
  1878. ],
  1879. });
  1880. eventsStatsMock = MockApiClient.addMockResponse({
  1881. url: '/organizations/org-slug/events-stats/',
  1882. method: 'GET',
  1883. statusCode: 200,
  1884. body: {
  1885. meta: {},
  1886. data: [],
  1887. },
  1888. });
  1889. renderTestComponent({
  1890. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1891. dashboard,
  1892. params: {
  1893. widgetIndex: '0',
  1894. },
  1895. });
  1896. await waitFor(() => {
  1897. expect(screen.getByRole('radio', {name: /errors/i})).toBeChecked();
  1898. });
  1899. expect(
  1900. await screen.findByText(
  1901. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Errors. Edit as you see fit."
  1902. )
  1903. ).toBeInTheDocument();
  1904. });
  1905. it('is dismissable', async function () {
  1906. dashboard = mockDashboard({
  1907. widgets: [
  1908. WidgetFixture({
  1909. widgetType: WidgetType.ERRORS,
  1910. datasetSource: DatasetSource.FORCED,
  1911. }),
  1912. ],
  1913. });
  1914. eventsStatsMock = MockApiClient.addMockResponse({
  1915. url: '/organizations/org-slug/events-stats/',
  1916. method: 'GET',
  1917. statusCode: 200,
  1918. body: {
  1919. meta: {},
  1920. data: [],
  1921. },
  1922. });
  1923. renderTestComponent({
  1924. orgFeatures: [...defaultOrgFeatures, 'performance-discover-dataset-selector'],
  1925. dashboard,
  1926. params: {
  1927. widgetIndex: '0',
  1928. },
  1929. });
  1930. expect(
  1931. await screen.findByText(
  1932. "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Errors. Edit as you see fit."
  1933. )
  1934. ).toBeInTheDocument();
  1935. await userEvent.click(screen.getByRole('button', {name: 'Close'}));
  1936. expect(
  1937. screen.queryByText(/we're splitting our datasets/i)
  1938. ).not.toBeInTheDocument();
  1939. });
  1940. });
  1941. });
  1942. describe('Widget Library', function () {
  1943. it('only opens the modal when the query data is changed', async function () {
  1944. const mockModal = jest.spyOn(modals, 'openWidgetBuilderOverwriteModal');
  1945. renderTestComponent();
  1946. await screen.findByText('Widget Library');
  1947. await userEvent.click(screen.getByText('Duration Distribution'));
  1948. // Widget Library, Builder title, and Chart title
  1949. expect(screen.getAllByText('Duration Distribution')).toHaveLength(2);
  1950. // Confirm modal doesn't open because no changes were made
  1951. expect(mockModal).not.toHaveBeenCalled();
  1952. await userEvent.click(screen.getAllByLabelText('Remove this Y-Axis')[0]);
  1953. await userEvent.click(screen.getByText('High Throughput Transactions'));
  1954. // Should not have overwritten widget data, and confirm modal should open
  1955. expect(screen.getAllByText('Duration Distribution')).toHaveLength(2);
  1956. expect(mockModal).toHaveBeenCalled();
  1957. });
  1958. });
  1959. describe('group by field', function () {
  1960. it('does not contain functions as options', async function () {
  1961. renderTestComponent({
  1962. query: {displayType: 'line'},
  1963. orgFeatures: [...defaultOrgFeatures],
  1964. });
  1965. expect(await screen.findByText('Select group')).toBeInTheDocument();
  1966. await userEvent.click(screen.getByText('Select group'));
  1967. // Only one f(x) field set in the y-axis selector
  1968. expect(screen.getByText('f(x)')).toBeInTheDocument();
  1969. });
  1970. it('adds more fields when Add Group is clicked', async function () {
  1971. renderTestComponent({
  1972. query: {displayType: 'line'},
  1973. orgFeatures: [...defaultOrgFeatures],
  1974. });
  1975. await userEvent.click(await screen.findByText('Add Group'));
  1976. expect(await screen.findAllByText('Select group')).toHaveLength(2);
  1977. });
  1978. it("doesn't reset group by when changing y-axis", async function () {
  1979. renderTestComponent({
  1980. query: {displayType: 'line'},
  1981. orgFeatures: [...defaultOrgFeatures],
  1982. });
  1983. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1984. await userEvent.click(screen.getAllByText('count()')[0], {
  1985. skipHover: true,
  1986. });
  1987. await userEvent.click(screen.getByText(/count_unique/), {
  1988. skipHover: true,
  1989. });
  1990. expect(await screen.findByText('project')).toBeInTheDocument();
  1991. });
  1992. it("doesn't erase the selection when switching to another time series", async function () {
  1993. renderTestComponent({
  1994. query: {displayType: 'line'},
  1995. orgFeatures: [...defaultOrgFeatures],
  1996. });
  1997. await selectEvent.select(await screen.findByText('Select group'), 'project');
  1998. await userEvent.click(screen.getByText('Line Chart'));
  1999. await userEvent.click(screen.getByText('Area Chart'));
  2000. expect(await screen.findByText('project')).toBeInTheDocument();
  2001. });
  2002. it('sends a top N request when a grouping is selected', async function () {
  2003. renderTestComponent({
  2004. query: {displayType: 'line'},
  2005. orgFeatures: [...defaultOrgFeatures],
  2006. });
  2007. await userEvent.click(await screen.findByText('Group your results'));
  2008. await userEvent.type(screen.getByText('Select group'), 'project{enter}');
  2009. await waitFor(() =>
  2010. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  2011. 2,
  2012. '/organizations/org-slug/events-stats/',
  2013. expect.objectContaining({
  2014. query: expect.objectContaining({
  2015. query: '',
  2016. yAxis: ['count()'],
  2017. field: ['project', 'count()'],
  2018. topEvents: TOP_N,
  2019. orderby: '-count()',
  2020. }),
  2021. })
  2022. )
  2023. );
  2024. });
  2025. it('allows deleting groups until there is one left', async function () {
  2026. renderTestComponent({
  2027. query: {displayType: 'line'},
  2028. orgFeatures: [...defaultOrgFeatures],
  2029. });
  2030. await userEvent.click(await screen.findByText('Add Group'));
  2031. expect(screen.getAllByLabelText('Remove group')).toHaveLength(2);
  2032. await userEvent.click(screen.getAllByLabelText('Remove group')[1]);
  2033. await waitFor(() =>
  2034. expect(screen.queryByLabelText('Remove group')).not.toBeInTheDocument()
  2035. );
  2036. });
  2037. it("display 'remove' and 'drag to reorder' buttons", async function () {
  2038. renderTestComponent({
  2039. query: {displayType: 'line'},
  2040. orgFeatures: [...defaultOrgFeatures],
  2041. });
  2042. expect(screen.queryByLabelText('Remove group')).not.toBeInTheDocument();
  2043. await selectEvent.select(screen.getByText('Select group'), 'project');
  2044. expect(screen.getByLabelText('Remove group')).toBeInTheDocument();
  2045. expect(screen.queryByLabelText('Drag to reorder')).not.toBeInTheDocument();
  2046. await userEvent.click(screen.getByText('Add Group'));
  2047. expect(screen.getAllByLabelText('Remove group')).toHaveLength(2);
  2048. expect(screen.getAllByLabelText('Drag to reorder')).toHaveLength(2);
  2049. });
  2050. it.todo(
  2051. 'Since simulate drag and drop with RTL is not recommended because of browser layout, remember to create acceptance test for this'
  2052. );
  2053. });
  2054. describe('limit field', function () {
  2055. it('renders if groupBy value is present', async function () {
  2056. const handleSave = jest.fn();
  2057. renderTestComponent({
  2058. query: {displayType: 'line'},
  2059. orgFeatures: [...defaultOrgFeatures],
  2060. onSave: handleSave,
  2061. });
  2062. await selectEvent.select(await screen.findByText('Select group'), 'project');
  2063. expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();
  2064. await userEvent.click(screen.getByText('Add Widget'));
  2065. await waitFor(() =>
  2066. expect(handleSave).toHaveBeenCalledWith([
  2067. expect.objectContaining({
  2068. limit: 5,
  2069. }),
  2070. ])
  2071. );
  2072. });
  2073. it('update value', async function () {
  2074. renderTestComponent({
  2075. query: {displayType: 'line'},
  2076. orgFeatures: [...defaultOrgFeatures],
  2077. });
  2078. await selectEvent.select(await screen.findByText('Select group'), 'project');
  2079. await userEvent.click(screen.getByText('Limit to 5 results'));
  2080. await userEvent.click(screen.getByText('Limit to 2 results'));
  2081. await waitFor(() =>
  2082. expect(eventsStatsMock).toHaveBeenCalledWith(
  2083. '/organizations/org-slug/events-stats/',
  2084. expect.objectContaining({
  2085. query: expect.objectContaining({
  2086. query: '',
  2087. yAxis: ['count()'],
  2088. field: ['project', 'count()'],
  2089. topEvents: 2,
  2090. orderby: '-count()',
  2091. }),
  2092. })
  2093. )
  2094. );
  2095. });
  2096. it('gets removed if no groupBy value', async function () {
  2097. renderTestComponent({
  2098. query: {displayType: 'line'},
  2099. orgFeatures: [...defaultOrgFeatures],
  2100. });
  2101. await selectEvent.select(await screen.findByText('Select group'), 'project');
  2102. expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();
  2103. await userEvent.click(screen.getByLabelText('Remove group'));
  2104. await waitFor(() =>
  2105. expect(screen.queryByText('Limit to 5 results')).not.toBeInTheDocument()
  2106. );
  2107. });
  2108. it('applies a limit when switching from a table to timeseries chart with grouping', async function () {
  2109. const widget: Widget = {
  2110. displayType: DisplayType.TABLE,
  2111. interval: '1d',
  2112. queries: [
  2113. {
  2114. name: 'Test Widget',
  2115. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  2116. columns: ['project'],
  2117. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  2118. conditions: '',
  2119. orderby: '',
  2120. },
  2121. ],
  2122. title: 'Transactions',
  2123. id: '1',
  2124. };
  2125. const dashboard = mockDashboard({widgets: [widget]});
  2126. renderTestComponent({
  2127. dashboard,
  2128. orgFeatures: [...defaultOrgFeatures],
  2129. params: {
  2130. widgetIndex: '0',
  2131. },
  2132. });
  2133. await userEvent.click(await screen.findByText('Table'));
  2134. await userEvent.click(screen.getByText('Line Chart'));
  2135. expect(screen.getByText('Limit to 3 results')).toBeInTheDocument();
  2136. expect(eventsStatsMock).toHaveBeenCalledWith(
  2137. '/organizations/org-slug/events-stats/',
  2138. expect.objectContaining({
  2139. query: expect.objectContaining({
  2140. topEvents: 3,
  2141. }),
  2142. })
  2143. );
  2144. });
  2145. it('persists the limit when switching between timeseries charts', async function () {
  2146. const widget: Widget = {
  2147. displayType: DisplayType.AREA,
  2148. interval: '1d',
  2149. queries: [
  2150. {
  2151. name: 'Test Widget',
  2152. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  2153. columns: ['project'],
  2154. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  2155. conditions: '',
  2156. orderby: '',
  2157. },
  2158. ],
  2159. title: 'Transactions',
  2160. id: '1',
  2161. limit: 1,
  2162. };
  2163. const dashboard = mockDashboard({widgets: [widget]});
  2164. renderTestComponent({
  2165. dashboard,
  2166. orgFeatures: [...defaultOrgFeatures],
  2167. params: {
  2168. widgetIndex: '0',
  2169. },
  2170. });
  2171. await userEvent.click(await screen.findByText('Area Chart'));
  2172. await userEvent.click(screen.getByText('Line Chart'));
  2173. expect(screen.getByText('Limit to 1 result')).toBeInTheDocument();
  2174. expect(eventsStatsMock).toHaveBeenCalledWith(
  2175. '/organizations/org-slug/events-stats/',
  2176. expect.objectContaining({
  2177. query: expect.objectContaining({
  2178. topEvents: 1,
  2179. }),
  2180. })
  2181. );
  2182. });
  2183. it('unsets the limit when going from timeseries to table', async function () {
  2184. const widget: Widget = {
  2185. displayType: DisplayType.AREA,
  2186. interval: '1d',
  2187. queries: [
  2188. {
  2189. name: 'Test Widget',
  2190. fields: ['count()', 'count_unique(user)', 'epm()', 'project'],
  2191. columns: ['project'],
  2192. aggregates: ['count()', 'count_unique(user)', 'epm()'],
  2193. conditions: '',
  2194. orderby: '',
  2195. },
  2196. ],
  2197. title: 'Transactions',
  2198. id: '1',
  2199. limit: 1,
  2200. };
  2201. const dashboard = mockDashboard({widgets: [widget]});
  2202. renderTestComponent({
  2203. dashboard,
  2204. orgFeatures: [...defaultOrgFeatures],
  2205. params: {
  2206. widgetIndex: '0',
  2207. },
  2208. });
  2209. await userEvent.click(await screen.findByText('Area Chart'));
  2210. await userEvent.click(screen.getByText('Table'));
  2211. expect(screen.queryByText('Limit to 1 result')).not.toBeInTheDocument();
  2212. expect(eventsMock).toHaveBeenCalledWith(
  2213. '/organizations/org-slug/events/',
  2214. expect.objectContaining({
  2215. query: expect.objectContaining({
  2216. topEvents: undefined,
  2217. }),
  2218. })
  2219. );
  2220. });
  2221. });
  2222. describe('Spans Dataset', () => {
  2223. it('queries for span tags and returns the correct data', async () => {
  2224. MockApiClient.addMockResponse({
  2225. url: `/organizations/org-slug/spans/fields/`,
  2226. body: [
  2227. {
  2228. key: 'plan',
  2229. name: 'plan',
  2230. },
  2231. ],
  2232. match: [
  2233. function (_url: string, options: Record<string, any>) {
  2234. return options.query.type === 'string';
  2235. },
  2236. ],
  2237. });
  2238. MockApiClient.addMockResponse({
  2239. url: `/organizations/org-slug/spans/fields/`,
  2240. body: [
  2241. {
  2242. key: 'tags[lcp.size,number]',
  2243. name: 'lcp.size',
  2244. },
  2245. {
  2246. key: 'tags[something.else,number]',
  2247. name: 'something.else',
  2248. },
  2249. ],
  2250. match: [
  2251. function (_url: string, options: Record<string, any>) {
  2252. return options.query.type === 'number';
  2253. },
  2254. ],
  2255. });
  2256. const dashboard = mockDashboard({
  2257. widgets: [
  2258. WidgetFixture({
  2259. widgetType: WidgetType.SPANS,
  2260. displayType: DisplayType.TABLE,
  2261. queries: [
  2262. {
  2263. name: 'Test Widget',
  2264. fields: ['count(tags[lcp.size,number])'],
  2265. columns: [],
  2266. aggregates: ['count(tags[lcp.size,number])'],
  2267. conditions: '',
  2268. orderby: '',
  2269. },
  2270. ],
  2271. }),
  2272. ],
  2273. });
  2274. renderTestComponent({
  2275. dashboard,
  2276. orgFeatures: [...defaultOrgFeatures, 'dashboards-eap'],
  2277. params: {
  2278. widgetIndex: '0',
  2279. },
  2280. });
  2281. // Click the argument to the count() function
  2282. expect(await screen.findByText('lcp.size')).toBeInTheDocument();
  2283. await userEvent.click(screen.getByText('lcp.size'));
  2284. // The option now appears in the aggregate property dropdown
  2285. expect(screen.queryAllByText('lcp.size')).toHaveLength(2);
  2286. expect(screen.getByText('something.else')).toBeInTheDocument();
  2287. // Click count() to verify the string tag is in the dropdown
  2288. expect(screen.queryByText('plan')).not.toBeInTheDocument();
  2289. await userEvent.click(screen.getByText(`count(…)`));
  2290. expect(screen.getByText('plan')).toBeInTheDocument();
  2291. });
  2292. it('does not show the Add Query button', async function () {
  2293. const dashboard = mockDashboard({
  2294. widgets: [
  2295. WidgetFixture({
  2296. widgetType: WidgetType.SPANS,
  2297. // Add Query is only available for timeseries charts
  2298. displayType: DisplayType.LINE,
  2299. queries: [
  2300. {
  2301. name: 'Test Widget',
  2302. fields: ['count(span.duration)'],
  2303. columns: [],
  2304. conditions: '',
  2305. orderby: '',
  2306. aggregates: ['count(span.duration)'],
  2307. },
  2308. ],
  2309. }),
  2310. ],
  2311. });
  2312. renderTestComponent({
  2313. dashboard,
  2314. orgFeatures: [...defaultOrgFeatures],
  2315. params: {
  2316. widgetIndex: '0',
  2317. },
  2318. });
  2319. await screen.findByText('Line Chart');
  2320. expect(screen.queryByText('Add Query')).not.toBeInTheDocument();
  2321. });
  2322. });
  2323. });