test_organization_dashboards.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942
  1. import pytest
  2. from selenium.webdriver.common.action_chains import ActionChains
  3. from selenium.webdriver.common.by import By
  4. from selenium.webdriver.common.keys import Keys
  5. from selenium.webdriver.support import expected_conditions as EC
  6. from selenium.webdriver.support.wait import WebDriverWait
  7. from fixtures.page_objects.dashboard_detail import (
  8. EDIT_WIDGET_BUTTON,
  9. WIDGET_DRAG_HANDLE,
  10. WIDGET_RESIZE_HANDLE,
  11. WIDGET_TITLE_FIELD,
  12. DashboardDetailPage,
  13. )
  14. from sentry.models import (
  15. Dashboard,
  16. DashboardWidget,
  17. DashboardWidgetDisplayTypes,
  18. DashboardWidgetQuery,
  19. DashboardWidgetTypes,
  20. )
  21. from sentry.testutils import AcceptanceTestCase
  22. from sentry.testutils.helpers.datetime import before_now, iso_format
  23. from sentry.testutils.silo import region_silo_test
  24. FEATURE_NAMES = [
  25. "organizations:discover-basic",
  26. "organizations:discover-query",
  27. "organizations:dashboards-basic",
  28. ]
  29. EDIT_FEATURE = ["organizations:dashboards-edit"]
  30. GRID_LAYOUT_FEATURE = ["organizations:dashboard-grid-layout"]
  31. WIDGET_LIBRARY_FEATURE = ["organizations:widget-library"]
  32. @region_silo_test
  33. class OrganizationDashboardsAcceptanceTest(AcceptanceTestCase):
  34. def setUp(self):
  35. super().setUp()
  36. min_ago = iso_format(before_now(minutes=1))
  37. self.store_event(
  38. data={"event_id": "a" * 32, "message": "oh no", "timestamp": min_ago},
  39. project_id=self.project.id,
  40. )
  41. self.dashboard = Dashboard.objects.create(
  42. title="Dashboard 1", created_by=self.user, organization=self.organization
  43. )
  44. self.existing_widget = DashboardWidget.objects.create(
  45. dashboard=self.dashboard,
  46. order=0,
  47. title="Existing Widget",
  48. display_type=DashboardWidgetDisplayTypes.LINE_CHART,
  49. widget_type=DashboardWidgetTypes.DISCOVER,
  50. interval="1d",
  51. )
  52. DashboardWidgetQuery.objects.create(
  53. widget=self.existing_widget,
  54. fields=["count()"],
  55. columns=[],
  56. aggregates=["count()"],
  57. order=0,
  58. )
  59. self.page = DashboardDetailPage(
  60. self.browser, self.client, organization=self.organization, dashboard=self.dashboard
  61. )
  62. self.login_as(self.user)
  63. def test_view_dashboard(self):
  64. with self.feature(FEATURE_NAMES):
  65. self.page.visit_default_overview()
  66. self.browser.snapshot("dashboards - default overview")
  67. def test_view_dashboard_with_manager(self):
  68. with self.feature(FEATURE_NAMES + EDIT_FEATURE):
  69. self.page.visit_default_overview()
  70. self.browser.snapshot("dashboards - default overview manager")
  71. def test_edit_dashboard(self):
  72. with self.feature(FEATURE_NAMES + EDIT_FEATURE):
  73. self.page.visit_default_overview()
  74. self.page.enter_edit_state()
  75. self.browser.snapshot("dashboards - edit state")
  76. def test_add_widget(self):
  77. with self.feature(FEATURE_NAMES + EDIT_FEATURE):
  78. self.page.visit_default_overview()
  79. self.page.enter_edit_state()
  80. # Add a widget
  81. self.page.click_dashboard_add_widget_button()
  82. self.browser.snapshot("dashboards - add widget")
  83. def test_edit_widget(self):
  84. with self.feature(FEATURE_NAMES + EDIT_FEATURE):
  85. self.page.visit_default_overview()
  86. self.page.enter_edit_state()
  87. # Edit the first widget.
  88. button = self.browser.element(EDIT_WIDGET_BUTTON)
  89. button.click()
  90. self.browser.snapshot("dashboards - edit widget")
  91. def test_widget_library(self):
  92. with self.feature(FEATURE_NAMES + EDIT_FEATURE + WIDGET_LIBRARY_FEATURE):
  93. self.page.visit_default_overview()
  94. # Open widget library
  95. self.page.click_dashboard_header_add_widget_button()
  96. self.browser.element('[data-test-id="library-tab"]').click()
  97. # Select/deselect widget library cards
  98. self.browser.element('[data-test-id="widget-library-card-0"]').click()
  99. self.browser.element('[data-test-id="widget-library-card-2"]').click()
  100. self.browser.element('[data-test-id="widget-library-card-3"]').click()
  101. self.browser.element('[data-test-id="widget-library-card-2"]').click()
  102. self.browser.snapshot("dashboards - widget library")
  103. def test_duplicate_widget_in_view_mode(self):
  104. with self.feature(FEATURE_NAMES + EDIT_FEATURE):
  105. self.page.visit_dashboard_detail()
  106. self.browser.element('[aria-label="Widget actions"]').click()
  107. self.browser.element('[data-test-id="duplicate-widget"]').click()
  108. self.page.wait_until_loaded()
  109. self.browser.element('[aria-label="Widget actions"]').click()
  110. self.browser.element('[data-test-id="duplicate-widget"]').click()
  111. self.page.wait_until_loaded()
  112. self.browser.snapshot("dashboard widget - duplicate")
  113. def test_delete_widget_in_view_mode(self):
  114. with self.feature(FEATURE_NAMES + EDIT_FEATURE):
  115. self.page.visit_dashboard_detail()
  116. self.browser.element('[aria-label="Widget actions"]').click()
  117. self.browser.element('[data-test-id="delete-widget"]').click()
  118. self.browser.element('[data-test-id="confirm-button"]').click()
  119. self.page.wait_until_loaded()
  120. self.browser.snapshot("dashboard widget - delete")
  121. @region_silo_test
  122. class OrganizationDashboardLayoutAcceptanceTest(AcceptanceTestCase):
  123. def setUp(self):
  124. super().setUp()
  125. min_ago = iso_format(before_now(minutes=1))
  126. self.store_event(
  127. data={"event_id": "a" * 32, "message": "oh no", "timestamp": min_ago},
  128. project_id=self.project.id,
  129. )
  130. self.dashboard = Dashboard.objects.create(
  131. title="Dashboard 1", created_by=self.user, organization=self.organization
  132. )
  133. self.page = DashboardDetailPage(
  134. self.browser, self.client, organization=self.organization, dashboard=self.dashboard
  135. )
  136. self.login_as(self.user)
  137. def capture_screenshots(self, screenshot_name):
  138. """
  139. Captures screenshots in both a pre and post refresh state.
  140. Necessary for verifying that the layout persists after saving.
  141. """
  142. self.page.wait_until_loaded()
  143. self.browser.snapshot(screenshot_name)
  144. self.browser.refresh()
  145. self.page.wait_until_loaded()
  146. self.browser.snapshot(f"{screenshot_name} (refresh)")
  147. def test_default_overview_dashboard_layout(self):
  148. with self.feature(FEATURE_NAMES + GRID_LAYOUT_FEATURE):
  149. self.page.visit_default_overview()
  150. self.browser.snapshot("dashboards - default overview layout")
  151. def test_add_and_move_new_widget_on_existing_dashboard(self):
  152. with self.feature(FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE):
  153. self.page.visit_dashboard_detail()
  154. self.page.enter_edit_state()
  155. self.page.add_widget_through_dashboard("New Widget")
  156. # Drag to the right
  157. dragHandle = self.browser.element(WIDGET_DRAG_HANDLE)
  158. action = ActionChains(self.browser.driver)
  159. action.drag_and_drop_by_offset(dragHandle, 1000, 0).perform()
  160. self.page.save_dashboard()
  161. self.capture_screenshots("dashboards - save new widget layout in custom dashboard")
  162. def test_create_new_dashboard_with_modified_widget_layout(self):
  163. with self.feature(FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE):
  164. # Create a new dashboard
  165. self.page.visit_create_dashboard()
  166. self.page.add_widget_through_dashboard("New Widget")
  167. # Drag to the right
  168. dragHandle = self.browser.element(WIDGET_DRAG_HANDLE)
  169. action = ActionChains(self.browser.driver)
  170. action.drag_and_drop_by_offset(dragHandle, 1000, 0).perform()
  171. self.page.save_dashboard()
  172. # Wait for page redirect, or else loading check passes too early
  173. wait = WebDriverWait(self.browser.driver, 10)
  174. wait.until(
  175. lambda driver: (
  176. f"/organizations/{self.organization.slug}/dashboards/new/"
  177. not in driver.current_url
  178. )
  179. )
  180. self.capture_screenshots("dashboards - save widget layout in new custom dashboard")
  181. def test_move_existing_widget_on_existing_dashboard(self):
  182. existing_widget = DashboardWidget.objects.create(
  183. dashboard=self.dashboard,
  184. order=0,
  185. title="Existing Widget",
  186. display_type=DashboardWidgetDisplayTypes.LINE_CHART,
  187. widget_type=DashboardWidgetTypes.DISCOVER,
  188. interval="1d",
  189. )
  190. DashboardWidgetQuery.objects.create(
  191. widget=existing_widget, fields=["count()"], columns=[], aggregates=["count()"], order=0
  192. )
  193. with self.feature(FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE):
  194. self.page.visit_dashboard_detail()
  195. self.page.enter_edit_state()
  196. # Drag to the right
  197. dragHandle = self.browser.element(WIDGET_DRAG_HANDLE)
  198. action = ActionChains(self.browser.driver)
  199. action.drag_and_drop_by_offset(dragHandle, 1000, 0).perform()
  200. self.page.save_dashboard()
  201. self.capture_screenshots("dashboards - move existing widget on existing dashboard")
  202. def test_add_by_widget_library_do_not_overlap(self):
  203. with self.feature(
  204. FEATURE_NAMES + EDIT_FEATURE + WIDGET_LIBRARY_FEATURE + GRID_LAYOUT_FEATURE
  205. ):
  206. self.page.visit_dashboard_detail()
  207. self.page.click_dashboard_header_add_widget_button()
  208. self.browser.element('[data-test-id="library-tab"]').click()
  209. # Add library widgets
  210. self.browser.element('[data-test-id="widget-library-card-0"]').click()
  211. self.browser.element('[data-test-id="widget-library-card-2"]').click()
  212. self.browser.element('[data-test-id="widget-library-card-3"]').click()
  213. self.browser.element('[data-test-id="widget-library-card-2"]').click()
  214. self.browser.element('[data-test-id="confirm-widgets"]').click()
  215. self.capture_screenshots(
  216. "dashboards - widgets from widget library do not overlap when added"
  217. )
  218. def test_widget_edit_keeps_same_layout_after_modification(self):
  219. existing_widget = DashboardWidget.objects.create(
  220. dashboard=self.dashboard,
  221. order=0,
  222. title="Existing Widget",
  223. display_type=DashboardWidgetDisplayTypes.LINE_CHART,
  224. widget_type=DashboardWidgetTypes.DISCOVER,
  225. interval="1d",
  226. )
  227. DashboardWidgetQuery.objects.create(
  228. widget=existing_widget, fields=["count()"], columns=[], aggregates=["count()"], order=0
  229. )
  230. with self.feature(FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE):
  231. self.page.visit_dashboard_detail()
  232. self.page.enter_edit_state()
  233. # Drag existing widget to the right
  234. dragHandle = self.browser.element(WIDGET_DRAG_HANDLE)
  235. action = ActionChains(self.browser.driver)
  236. action.drag_and_drop_by_offset(dragHandle, 1000, 0).perform()
  237. # Edit the existing widget
  238. button = self.browser.element(EDIT_WIDGET_BUTTON)
  239. button.click()
  240. title_input = self.browser.element(WIDGET_TITLE_FIELD)
  241. title_input.clear()
  242. title_input.send_keys(Keys.END, "Existing WidgetUPDATED!!")
  243. button = self.browser.element('[data-test-id="add-widget"]')
  244. button.click()
  245. # Add and drag new widget to the right
  246. self.page.add_widget_through_dashboard("New Widget")
  247. dragHandle = self.browser.element(
  248. f".react-grid-item:nth-of-type(2) {WIDGET_DRAG_HANDLE}"
  249. )
  250. action = ActionChains(self.browser.driver)
  251. action.drag_and_drop_by_offset(dragHandle, 1000, 0)
  252. action.perform()
  253. # Edit the new widget
  254. button = self.browser.element(f".react-grid-item:nth-of-type(2) {EDIT_WIDGET_BUTTON}")
  255. button.click()
  256. title_input = self.browser.element(WIDGET_TITLE_FIELD)
  257. title_input.clear()
  258. title_input.send_keys(Keys.END, "New WidgetUPDATED!!")
  259. button = self.browser.element('[data-test-id="add-widget"]')
  260. button.click()
  261. self.page.save_dashboard()
  262. self.capture_screenshots(
  263. "dashboards - edit widgets after layout change does not reset layout"
  264. )
  265. def test_add_issue_widgets_do_not_overlap(self):
  266. def add_issue_widget(widget_title):
  267. self.browser.wait_until_clickable('[data-test-id="widget-add"]')
  268. self.page.click_dashboard_add_widget_button()
  269. title_input = self.browser.element(WIDGET_TITLE_FIELD)
  270. title_input.send_keys(widget_title)
  271. self.browser.element(
  272. '[aria-label="Select Issues (States, Assignment, Time, etc.)"]'
  273. ).click()
  274. button = self.browser.element('[data-test-id="add-widget"]')
  275. button.click()
  276. with self.feature(FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE):
  277. self.page.visit_dashboard_detail()
  278. self.page.enter_edit_state()
  279. add_issue_widget("Issue Widget 1")
  280. add_issue_widget("Issue Widget 2")
  281. self.page.save_dashboard()
  282. self.capture_screenshots("dashboards - issue widgets do not overlap")
  283. def test_resize_new_and_existing_widgets(self):
  284. existing_widget = DashboardWidget.objects.create(
  285. dashboard=self.dashboard,
  286. order=0,
  287. title="Existing Widget",
  288. display_type=DashboardWidgetDisplayTypes.LINE_CHART,
  289. widget_type=DashboardWidgetTypes.DISCOVER,
  290. interval="1d",
  291. )
  292. DashboardWidgetQuery.objects.create(
  293. widget=existing_widget, fields=["count()"], columns=[], aggregates=["count()"], order=0
  294. )
  295. with self.feature(FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE):
  296. self.page.visit_dashboard_detail()
  297. self.page.enter_edit_state()
  298. # Resize existing widget
  299. resizeHandle = self.browser.element(WIDGET_RESIZE_HANDLE)
  300. action = ActionChains(self.browser.driver)
  301. action.drag_and_drop_by_offset(resizeHandle, 500, 0).perform()
  302. self.page.add_widget_through_dashboard("New Widget")
  303. # Drag it to the left for consistency
  304. dragHandle = self.browser.element(
  305. f".react-grid-item:nth-of-type(2) {WIDGET_DRAG_HANDLE}"
  306. )
  307. action = ActionChains(self.browser.driver)
  308. action.drag_and_drop_by_offset(dragHandle, -1000, 0).perform()
  309. # Resize new widget, get the 2nd element instead of the "last" because the "last" is
  310. # the add widget button
  311. resizeHandle = self.browser.element(
  312. f".react-grid-item:nth-of-type(2) {WIDGET_RESIZE_HANDLE}"
  313. )
  314. action = ActionChains(self.browser.driver)
  315. action.drag_and_drop_by_offset(resizeHandle, 500, 0).perform()
  316. self.page.save_dashboard()
  317. self.capture_screenshots("dashboards - resize new and existing widgets")
  318. def test_delete_existing_widget_does_not_trigger_new_widget_layout_reset(self):
  319. existing_widget = DashboardWidget.objects.create(
  320. dashboard=self.dashboard,
  321. order=0,
  322. title="Existing Widget",
  323. display_type=DashboardWidgetDisplayTypes.LINE_CHART,
  324. widget_type=DashboardWidgetTypes.DISCOVER,
  325. interval="1d",
  326. )
  327. DashboardWidgetQuery.objects.create(
  328. widget=existing_widget, fields=["count()"], columns=[], aggregates=["count()"], order=0
  329. )
  330. with self.feature(FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE):
  331. self.page.visit_dashboard_detail()
  332. self.page.enter_edit_state()
  333. self.page.add_widget_through_dashboard("New Widget")
  334. # Drag it to the bottom left
  335. dragHandle = self.browser.element(
  336. f".react-grid-item:nth-of-type(2) {WIDGET_DRAG_HANDLE}"
  337. )
  338. action = ActionChains(self.browser.driver)
  339. action.drag_and_drop_by_offset(dragHandle, -500, 500).perform()
  340. # Resize new widget, get the 2nd element instead of the "last" because the "last" is
  341. # the add widget button
  342. resizeHandle = self.browser.element(
  343. f".react-grid-item:nth-of-type(2) {WIDGET_RESIZE_HANDLE}"
  344. )
  345. action = ActionChains(self.browser.driver)
  346. action.drag_and_drop_by_offset(resizeHandle, 500, 0).perform()
  347. # Delete first existing widget
  348. delete_widget_button = self.browser.element(
  349. '.react-grid-item:first-of-type [data-test-id="widget-delete"]'
  350. )
  351. delete_widget_button.click()
  352. self.page.save_dashboard()
  353. self.capture_screenshots(
  354. "dashboards - delete existing widget does not reset new widget layout"
  355. )
  356. def test_resize_big_number_widget(self):
  357. existing_widget = DashboardWidget.objects.create(
  358. dashboard=self.dashboard,
  359. order=0,
  360. title="Big Number Widget",
  361. display_type=DashboardWidgetDisplayTypes.BIG_NUMBER,
  362. widget_type=DashboardWidgetTypes.DISCOVER,
  363. interval="1d",
  364. )
  365. DashboardWidgetQuery.objects.create(
  366. widget=existing_widget,
  367. fields=["count_unique(issue)"],
  368. columns=[],
  369. aggregates=["count_unique(issue)"],
  370. order=0,
  371. )
  372. with self.feature(FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE):
  373. self.page.visit_dashboard_detail()
  374. self.page.enter_edit_state()
  375. # Resize existing widget
  376. resizeHandle = self.browser.element(WIDGET_RESIZE_HANDLE)
  377. action = ActionChains(self.browser.driver)
  378. action.drag_and_drop_by_offset(resizeHandle, 200, 200).perform()
  379. self.page.save_dashboard()
  380. self.capture_screenshots("dashboards - resize big number widget")
  381. def test_default_layout_when_widgets_do_not_have_layout_set(self):
  382. existing_widgets = DashboardWidget.objects.bulk_create(
  383. [
  384. DashboardWidget(
  385. dashboard=self.dashboard,
  386. order=i,
  387. title=f"Existing Widget {i}",
  388. display_type=DashboardWidgetDisplayTypes.LINE_CHART,
  389. widget_type=DashboardWidgetTypes.DISCOVER,
  390. interval="1d",
  391. )
  392. for i in range(4)
  393. ]
  394. )
  395. DashboardWidgetQuery.objects.bulk_create(
  396. [
  397. DashboardWidgetQuery(
  398. widget=existing_widget,
  399. fields=["count()"],
  400. columns=[],
  401. aggregates=["count()"],
  402. order=0,
  403. )
  404. for existing_widget in existing_widgets
  405. ]
  406. )
  407. with self.feature(FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE):
  408. self.page.visit_dashboard_detail()
  409. self.page.wait_until_loaded()
  410. self.browser.snapshot("dashboards - default layout when widgets do not have layout set")
  411. def test_duplicate_widget_in_view_mode(self):
  412. existing_widget = DashboardWidget.objects.create(
  413. dashboard=self.dashboard,
  414. order=0,
  415. title="Big Number Widget",
  416. display_type=DashboardWidgetDisplayTypes.BIG_NUMBER,
  417. widget_type=DashboardWidgetTypes.DISCOVER,
  418. interval="1d",
  419. )
  420. DashboardWidgetQuery.objects.create(
  421. widget=existing_widget,
  422. fields=["count_unique(issue)"],
  423. columns=[],
  424. aggregates=["count_unique(issue)"],
  425. order=0,
  426. )
  427. with self.feature(FEATURE_NAMES + EDIT_FEATURE):
  428. self.page.visit_dashboard_detail()
  429. self.browser.element('[aria-label="Widget actions"]').click()
  430. self.browser.element('[data-test-id="duplicate-widget"]').click()
  431. self.page.wait_until_loaded()
  432. self.browser.element('[aria-label="Widget actions"]').click()
  433. self.browser.element('[data-test-id="duplicate-widget"]').click()
  434. self.page.wait_until_loaded()
  435. # Should not trigger alert
  436. self.page.enter_edit_state()
  437. self.page.click_cancel_button()
  438. wait = WebDriverWait(self.browser.driver, 5)
  439. wait.until_not(EC.alert_is_present())
  440. self.browser.snapshot("dashboard widget - duplicate with grid")
  441. def test_delete_widget_in_view_mode(self):
  442. existing_widget = DashboardWidget.objects.create(
  443. dashboard=self.dashboard,
  444. order=0,
  445. title="Big Number Widget",
  446. display_type=DashboardWidgetDisplayTypes.BIG_NUMBER,
  447. widget_type=DashboardWidgetTypes.DISCOVER,
  448. interval="1d",
  449. )
  450. DashboardWidgetQuery.objects.create(
  451. widget=existing_widget,
  452. fields=["count_unique(issue)"],
  453. columns=[],
  454. aggregates=["count_unique(issue)"],
  455. order=0,
  456. )
  457. with self.feature(FEATURE_NAMES + EDIT_FEATURE):
  458. self.page.visit_dashboard_detail()
  459. self.browser.element('[aria-label="Widget actions"]').click()
  460. self.browser.element('[data-test-id="delete-widget"]').click()
  461. self.browser.element('[data-test-id="confirm-button"]').click()
  462. self.page.wait_until_loaded()
  463. self.browser.snapshot("dashboard widget - delete with grid")
  464. def test_cancel_without_changes_does_not_trigger_confirm_with_widget_library_through_header(
  465. self,
  466. ):
  467. with self.feature(
  468. FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE + WIDGET_LIBRARY_FEATURE
  469. ):
  470. self.page.visit_dashboard_detail()
  471. # Open widget library
  472. self.page.click_dashboard_header_add_widget_button()
  473. self.browser.element('[data-test-id="library-tab"]').click()
  474. # Select/deselect widget library cards
  475. self.browser.element('[data-test-id="widget-library-card-0"]').click()
  476. self.browser.element('[data-test-id="widget-library-card-2"]').click()
  477. # Save widget library selections
  478. button = self.browser.element('[data-test-id="confirm-widgets"]')
  479. button.click()
  480. self.page.wait_until_loaded()
  481. # Should not trigger alert
  482. self.page.enter_edit_state()
  483. self.page.click_cancel_button()
  484. wait = WebDriverWait(self.browser.driver, 5)
  485. wait.until_not(EC.alert_is_present())
  486. def test_cancel_without_changes_does_not_trigger_confirm_with_custom_widget_through_header(
  487. self,
  488. ):
  489. with self.feature(
  490. FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE + WIDGET_LIBRARY_FEATURE
  491. ):
  492. self.page.visit_dashboard_detail()
  493. self.page.click_dashboard_header_add_widget_button()
  494. title_input = self.browser.element(WIDGET_TITLE_FIELD)
  495. title_input.send_keys("New custom widget")
  496. button = self.browser.element('[data-test-id="add-widget"]')
  497. button.click()
  498. self.page.wait_until_loaded()
  499. # Should not trigger confirm dialog
  500. self.page.enter_edit_state()
  501. self.page.click_cancel_button()
  502. wait = WebDriverWait(self.browser.driver, 5)
  503. wait.until_not(EC.alert_is_present())
  504. def test_position_when_adding_multiple_widgets_through_add_widget_tile_in_edit(
  505. self,
  506. ):
  507. with self.feature(
  508. FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE + WIDGET_LIBRARY_FEATURE
  509. ):
  510. self.page.visit_dashboard_detail()
  511. self.page.enter_edit_state()
  512. # Widgets should take up the whole first row and the first spot in second row
  513. self.page.add_widget_through_dashboard("A")
  514. self.page.add_widget_through_dashboard("B")
  515. self.page.add_widget_through_dashboard("C")
  516. self.page.add_widget_through_dashboard("D")
  517. self.page.wait_until_loaded()
  518. self.page.save_dashboard()
  519. self.capture_screenshots(
  520. "dashboards - position when adding multiple widgets through Add Widget tile in edit"
  521. )
  522. def test_position_when_adding_multiple_widgets_through_add_widget_tile_in_create(
  523. self,
  524. ):
  525. with self.feature(
  526. FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE + WIDGET_LIBRARY_FEATURE
  527. ):
  528. self.page.visit_create_dashboard()
  529. # Widgets should take up the whole first row and the first spot in second row
  530. self.page.add_widget_through_dashboard("A")
  531. self.page.add_widget_through_dashboard("B")
  532. self.page.add_widget_through_dashboard("C")
  533. self.page.add_widget_through_dashboard("D")
  534. self.page.wait_until_loaded()
  535. self.page.save_dashboard()
  536. # Wait for page redirect, or else loading check passes too early
  537. wait = WebDriverWait(self.browser.driver, 10)
  538. wait.until(
  539. lambda driver: (
  540. f"/organizations/{self.organization.slug}/dashboards/new/"
  541. not in driver.current_url
  542. )
  543. )
  544. self.capture_screenshots(
  545. "dashboards - position when adding multiple widgets through Add Widget tile in create"
  546. )
  547. def test_deleting_stacked_widgets_by_context_menu_does_not_trigger_confirm_on_edit_cancel(
  548. self,
  549. ):
  550. layouts = [
  551. {"x": 0, "y": 0, "w": 2, "h": 2, "minH": 2},
  552. {"x": 0, "y": 2, "w": 2, "h": 2, "minH": 2},
  553. ]
  554. existing_widgets = DashboardWidget.objects.bulk_create(
  555. [
  556. DashboardWidget(
  557. dashboard=self.dashboard,
  558. order=i,
  559. title=f"Existing Widget {i}",
  560. display_type=DashboardWidgetDisplayTypes.LINE_CHART,
  561. widget_type=DashboardWidgetTypes.DISCOVER,
  562. interval="1d",
  563. detail={"layout": layout},
  564. )
  565. for i, layout in enumerate(layouts)
  566. ]
  567. )
  568. DashboardWidgetQuery.objects.bulk_create(
  569. DashboardWidgetQuery(
  570. widget=widget, fields=["count()"], columns=[], aggregates=["count()"], order=0
  571. )
  572. for widget in existing_widgets
  573. )
  574. with self.feature(
  575. FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE + WIDGET_LIBRARY_FEATURE
  576. ):
  577. self.page.visit_dashboard_detail()
  578. dropdown_trigger = self.browser.element('[aria-label="Widget actions"]')
  579. dropdown_trigger.click()
  580. delete_widget_menu_item = self.browser.element('[data-test-id="delete-widget"]')
  581. delete_widget_menu_item.click()
  582. confirm_button = self.browser.element('[data-test-id="confirm-button"]')
  583. confirm_button.click()
  584. wait = WebDriverWait(self.browser.driver, 5)
  585. wait.until(
  586. EC.presence_of_element_located(
  587. (By.XPATH, "//*[contains(text(),'Dashboard updated')]")
  588. )
  589. )
  590. # Should not trigger confirm dialog
  591. self.page.enter_edit_state()
  592. self.page.click_cancel_button()
  593. wait.until_not(EC.alert_is_present())
  594. def test_changing_number_widget_to_area_updates_widget_height(
  595. self,
  596. ):
  597. layouts = [
  598. (DashboardWidgetDisplayTypes.BIG_NUMBER, {"x": 0, "y": 0, "w": 2, "h": 1, "minH": 1}),
  599. (DashboardWidgetDisplayTypes.LINE_CHART, {"x": 0, "y": 1, "w": 2, "h": 2, "minH": 2}),
  600. ]
  601. existing_widgets = DashboardWidget.objects.bulk_create(
  602. [
  603. DashboardWidget(
  604. dashboard=self.dashboard,
  605. order=i,
  606. title=f"Widget {i}",
  607. display_type=display_type,
  608. widget_type=DashboardWidgetTypes.DISCOVER,
  609. interval="1d",
  610. detail={"layout": layout},
  611. )
  612. for i, (display_type, layout) in enumerate(layouts)
  613. ]
  614. )
  615. DashboardWidgetQuery.objects.bulk_create(
  616. DashboardWidgetQuery(
  617. widget=widget, fields=["count()"], columns=[], aggregates=["count()"], order=0
  618. )
  619. for widget in existing_widgets
  620. )
  621. with self.feature(
  622. FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE + WIDGET_LIBRARY_FEATURE
  623. ):
  624. self.page.visit_dashboard_detail()
  625. # Open edit modal for first widget
  626. dropdown_trigger = self.browser.element('[aria-label="Widget actions"]')
  627. dropdown_trigger.click()
  628. edit_widget_menu_item = self.browser.element('[data-test-id="edit-widget"]')
  629. edit_widget_menu_item.click()
  630. # Change the chart type to the first visualization option - Area chart
  631. chart_type_input = self.browser.element("#react-select-2-input")
  632. chart_type_input.send_keys("Area", Keys.ENTER)
  633. button = self.browser.element('[data-test-id="add-widget"]')
  634. button.click()
  635. # No confirm dialog because of shifting lower element
  636. self.page.enter_edit_state()
  637. self.page.click_cancel_button()
  638. wait = WebDriverWait(self.browser.driver, 5)
  639. wait.until_not(EC.alert_is_present())
  640. # Try to decrease height to 1 row, should stay at 2 rows
  641. self.page.enter_edit_state()
  642. resizeHandle = self.browser.element(WIDGET_RESIZE_HANDLE)
  643. action = ActionChains(self.browser.driver)
  644. action.drag_and_drop_by_offset(resizeHandle, 0, -100).perform()
  645. self.page.save_dashboard()
  646. self.browser.snapshot(
  647. "dashboards - change from big number to area chart increases widget to min height"
  648. )
  649. @pytest.mark.skip(reason="flaky behaviour due to loading spinner")
  650. def test_changing_number_widget_larger_than_min_height_for_area_chart_keeps_height(
  651. self,
  652. ):
  653. existing_widget = DashboardWidget.objects.create(
  654. dashboard=self.dashboard,
  655. order=0,
  656. title="Originally Big Number - 3 rows",
  657. display_type=DashboardWidgetDisplayTypes.BIG_NUMBER,
  658. widget_type=DashboardWidgetTypes.DISCOVER,
  659. interval="1d",
  660. detail={"layout": {"x": 0, "y": 0, "w": 2, "h": 3, "minH": 1}},
  661. )
  662. DashboardWidgetQuery.objects.create(
  663. widget=existing_widget, fields=["count()"], columns=[], aggregates=["count()"], order=0
  664. )
  665. with self.feature(
  666. FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE + WIDGET_LIBRARY_FEATURE
  667. ):
  668. self.page.visit_dashboard_detail()
  669. # Open edit modal for first widget
  670. dropdown_trigger = self.browser.element('[aria-label="Widget actions"]')
  671. dropdown_trigger.click()
  672. edit_widget_menu_item = self.browser.element('[data-test-id="edit-widget"]')
  673. edit_widget_menu_item.click()
  674. # Change the chart type to the first visualization option - Area chart
  675. chart_type_input = self.browser.element("#react-select-2-input")
  676. chart_type_input.send_keys("Area", Keys.ENTER)
  677. button = self.browser.element('[data-test-id="add-widget"]')
  678. button.click()
  679. self.page.wait_until_loaded()
  680. # This snapshot is flaky due to the loading spinner
  681. self.browser.snapshot(
  682. "dashboards - change from big number to other chart of more than 2 rows keeps height"
  683. )
  684. # Try to decrease height by >1 row, should be at 2 rows
  685. self.page.enter_edit_state()
  686. resizeHandle = self.browser.element(WIDGET_RESIZE_HANDLE)
  687. action = ActionChains(self.browser.driver)
  688. action.drag_and_drop_by_offset(resizeHandle, 0, -400).perform()
  689. self.page.save_dashboard()
  690. self.browser.snapshot(
  691. "dashboards - change from big number to other chart enforces min height of 2"
  692. )
  693. def test_changing_area_widget_larger_than_min_height_for_number_chart_keeps_height(
  694. self,
  695. ):
  696. existing_widget = DashboardWidget.objects.create(
  697. dashboard=self.dashboard,
  698. order=0,
  699. title="Originally Area Chart - 3 rows",
  700. display_type=DashboardWidgetDisplayTypes.AREA_CHART,
  701. widget_type=DashboardWidgetTypes.DISCOVER,
  702. interval="1d",
  703. detail={"layout": {"x": 0, "y": 0, "w": 2, "h": 3, "minH": 2}},
  704. )
  705. DashboardWidgetQuery.objects.create(
  706. widget=existing_widget, fields=["count()"], columns=[], aggregates=["count()"], order=0
  707. )
  708. with self.feature(
  709. FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE + WIDGET_LIBRARY_FEATURE
  710. ):
  711. self.page.visit_dashboard_detail()
  712. # Open edit modal for first widget
  713. dropdown_trigger = self.browser.element('[aria-label="Widget actions"]')
  714. dropdown_trigger.click()
  715. edit_widget_menu_item = self.browser.element('[data-test-id="edit-widget"]')
  716. edit_widget_menu_item.click()
  717. # Change the chart type to big number
  718. chart_type_input = self.browser.element("#react-select-2-input")
  719. chart_type_input.send_keys("Big Number", Keys.ENTER)
  720. button = self.browser.element('[data-test-id="add-widget"]')
  721. button.click()
  722. self.page.wait_until_loaded()
  723. self.browser.snapshot("dashboards - change from area chart to big number keeps height")
  724. # Decrease height by >1 row, should stop at 1 row
  725. self.page.enter_edit_state()
  726. resizeHandle = self.browser.element(WIDGET_RESIZE_HANDLE)
  727. action = ActionChains(self.browser.driver)
  728. action.drag_and_drop_by_offset(resizeHandle, 0, -400).perform()
  729. self.page.save_dashboard()
  730. self.browser.snapshot(
  731. "dashboards - change from area chart to big number allows min height of 1"
  732. )
  733. @region_silo_test
  734. class OrganizationDashboardsManageAcceptanceTest(AcceptanceTestCase):
  735. def setUp(self):
  736. super().setUp()
  737. self.team = self.create_team(organization=self.organization, name="Mariachi Band")
  738. self.project = self.create_project(
  739. organization=self.organization, teams=[self.team], name="Bengal"
  740. )
  741. self.dashboard = Dashboard.objects.create(
  742. title="Dashboard 1", created_by=self.user, organization=self.organization
  743. )
  744. self.widget_1 = DashboardWidget.objects.create(
  745. dashboard=self.dashboard,
  746. order=0,
  747. title="Widget 1",
  748. display_type=DashboardWidgetDisplayTypes.LINE_CHART,
  749. widget_type=DashboardWidgetTypes.DISCOVER,
  750. interval="1d",
  751. )
  752. self.widget_2 = DashboardWidget.objects.create(
  753. dashboard=self.dashboard,
  754. order=1,
  755. title="Widget 2",
  756. display_type=DashboardWidgetDisplayTypes.TABLE,
  757. widget_type=DashboardWidgetTypes.DISCOVER,
  758. interval="1d",
  759. )
  760. self.login_as(self.user)
  761. self.default_path = f"/organizations/{self.organization.slug}/dashboards/"
  762. def wait_until_loaded(self):
  763. self.browser.wait_until_not('[data-test-id="loading-indicator"]')
  764. self.browser.wait_until_not('[data-test-id="loading-placeholder"]')
  765. def test_dashboard_manager(self):
  766. with self.feature(FEATURE_NAMES + EDIT_FEATURE):
  767. self.browser.get(self.default_path)
  768. self.wait_until_loaded()
  769. self.browser.snapshot("dashboards - manage overview")
  770. def test_dashboard_manager_with_unset_layouts_and_defined_layouts(self):
  771. dashboard_with_layouts = Dashboard.objects.create(
  772. title="Dashboard with some defined layouts",
  773. created_by=self.user,
  774. organization=self.organization,
  775. )
  776. DashboardWidget.objects.create(
  777. dashboard=dashboard_with_layouts,
  778. order=0,
  779. title="Widget 1",
  780. display_type=DashboardWidgetDisplayTypes.BAR_CHART,
  781. widget_type=DashboardWidgetTypes.DISCOVER,
  782. interval="1d",
  783. detail={"layout": {"x": 1, "y": 0, "w": 3, "h": 3, "minH": 2}},
  784. )
  785. # This widget has no layout, but should position itself at
  786. # x: 4, y: 0, w: 2, h: 2
  787. DashboardWidget.objects.create(
  788. dashboard=dashboard_with_layouts,
  789. order=1,
  790. title="Widget 2",
  791. display_type=DashboardWidgetDisplayTypes.TABLE,
  792. widget_type=DashboardWidgetTypes.DISCOVER,
  793. interval="1d",
  794. )
  795. with self.feature(FEATURE_NAMES + EDIT_FEATURE + GRID_LAYOUT_FEATURE):
  796. self.browser.get(self.default_path)
  797. self.wait_until_loaded()
  798. self.browser.snapshot("dashboards - manage overview with grid layout")