test_organization_dashboards.py 36 KB

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