controls.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import Feature from 'sentry/components/acl/feature';
  4. import FeatureDisabled from 'sentry/components/acl/featureDisabled';
  5. import {Button} from 'sentry/components/button';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import Confirm from 'sentry/components/confirm';
  8. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  9. import {Hovercard} from 'sentry/components/hovercard';
  10. import {Tooltip} from 'sentry/components/tooltip';
  11. import {IconAdd, IconDownload, IconEdit} from 'sentry/icons';
  12. import {t, tct} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import type {Organization} from 'sentry/types/organization';
  15. import {trackAnalytics} from 'sentry/utils/analytics';
  16. import {hasCustomMetrics} from 'sentry/utils/metrics/features';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import {useUser} from 'sentry/utils/useUser';
  19. import {AddWidgetButton} from 'sentry/views/dashboards/addWidget';
  20. import EditAccessSelector from 'sentry/views/dashboards/editAccessSelector';
  21. import {DataSet} from 'sentry/views/dashboards/widgetBuilder/utils';
  22. import {checkUserHasEditAccess, UNSAVED_FILTERS_MESSAGE} from './detail';
  23. import exportDashboard from './exportDashboard';
  24. import type {DashboardDetails, DashboardListItem, DashboardPermissions} from './types';
  25. import {DashboardState, MAX_WIDGETS} from './types';
  26. type Props = {
  27. dashboard: DashboardDetails;
  28. dashboardState: DashboardState;
  29. dashboards: DashboardListItem[];
  30. onAddWidget: (dataset: DataSet) => void;
  31. onCancel: () => void;
  32. onCommit: () => void;
  33. onDelete: () => void;
  34. onEdit: () => void;
  35. organization: Organization;
  36. widgetLimitReached: boolean;
  37. hasUnsavedFilters?: boolean;
  38. onChangeEditAccess?: (newDashboardPermissions: DashboardPermissions) => void;
  39. };
  40. function Controls({
  41. dashboardState,
  42. dashboard,
  43. dashboards,
  44. hasUnsavedFilters,
  45. widgetLimitReached,
  46. onChangeEditAccess,
  47. onEdit,
  48. onCommit,
  49. onDelete,
  50. onCancel,
  51. onAddWidget,
  52. }: Props) {
  53. function renderCancelButton(label = t('Cancel')) {
  54. return (
  55. <Button
  56. data-test-id="dashboard-cancel"
  57. size="sm"
  58. onClick={e => {
  59. e.preventDefault();
  60. onCancel();
  61. }}
  62. >
  63. {label}
  64. </Button>
  65. );
  66. }
  67. const organization = useOrganization();
  68. const currentUser = useUser();
  69. if ([DashboardState.EDIT, DashboardState.PENDING_DELETE].includes(dashboardState)) {
  70. return (
  71. <StyledButtonBar gap={1} key="edit-controls">
  72. {renderCancelButton()}
  73. <Confirm
  74. priority="danger"
  75. message={t('Are you sure you want to delete this dashboard?')}
  76. onConfirm={onDelete}
  77. disabled={dashboards.length <= 1}
  78. >
  79. <Button size="sm" data-test-id="dashboard-delete" priority="danger">
  80. {t('Delete')}
  81. </Button>
  82. </Confirm>
  83. <Button
  84. data-test-id="dashboard-commit"
  85. size="sm"
  86. onClick={e => {
  87. e.preventDefault();
  88. onCommit();
  89. }}
  90. priority="primary"
  91. >
  92. {t('Save and Finish')}
  93. </Button>
  94. </StyledButtonBar>
  95. );
  96. }
  97. if (dashboardState === DashboardState.CREATE) {
  98. return (
  99. <StyledButtonBar gap={1} key="create-controls">
  100. {renderCancelButton()}
  101. <Button
  102. data-test-id="dashboard-commit"
  103. size="sm"
  104. onClick={e => {
  105. e.preventDefault();
  106. onCommit();
  107. }}
  108. priority="primary"
  109. >
  110. {t('Save and Finish')}
  111. </Button>
  112. </StyledButtonBar>
  113. );
  114. }
  115. if (dashboardState === DashboardState.PREVIEW) {
  116. return (
  117. <StyledButtonBar gap={1} key="preview-controls">
  118. {renderCancelButton(t('Go Back'))}
  119. <Button
  120. data-test-id="dashboard-commit"
  121. size="sm"
  122. onClick={e => {
  123. e.preventDefault();
  124. onCommit();
  125. }}
  126. priority="primary"
  127. >
  128. {t('Add Dashboard')}
  129. </Button>
  130. </StyledButtonBar>
  131. );
  132. }
  133. const defaultDataset = organization.features.includes(
  134. 'performance-discover-dataset-selector'
  135. )
  136. ? DataSet.ERRORS
  137. : DataSet.EVENTS;
  138. let hasEditAccess = true;
  139. if (organization.features.includes('dashboards-edit-access')) {
  140. hasEditAccess = checkUserHasEditAccess(dashboard, currentUser, organization);
  141. }
  142. return (
  143. <StyledButtonBar gap={1} key="controls">
  144. <DashboardEditFeature>
  145. {hasFeature => (
  146. <Fragment>
  147. <FeedbackWidgetButton />
  148. <Feature features="dashboards-import">
  149. <Button
  150. data-test-id="dashboard-export"
  151. onClick={e => {
  152. e.preventDefault();
  153. exportDashboard();
  154. }}
  155. icon={<IconDownload />}
  156. priority="default"
  157. size="sm"
  158. >
  159. {t('Export Dashboard')}
  160. </Button>
  161. </Feature>
  162. <Feature features="dashboards-edit-access">
  163. <EditAccessSelector
  164. dashboard={dashboard}
  165. onChangeEditAccess={onChangeEditAccess}
  166. />
  167. </Feature>
  168. <Button
  169. data-test-id="dashboard-edit"
  170. onClick={e => {
  171. e.preventDefault();
  172. onEdit();
  173. }}
  174. icon={<IconEdit />}
  175. disabled={!hasFeature || hasUnsavedFilters || !hasEditAccess}
  176. title={hasUnsavedFilters && UNSAVED_FILTERS_MESSAGE}
  177. priority="default"
  178. size="sm"
  179. >
  180. {t('Edit Dashboard')}
  181. </Button>
  182. {hasFeature ? (
  183. <Tooltip
  184. title={tct('Max widgets ([maxWidgets]) per dashboard reached.', {
  185. maxWidgets: MAX_WIDGETS,
  186. })}
  187. disabled={!widgetLimitReached}
  188. >
  189. {hasCustomMetrics(organization) ? (
  190. <AddWidgetButton
  191. onAddWidget={onAddWidget}
  192. aria-label={t('Add Widget')}
  193. priority="primary"
  194. data-test-id="add-widget-library"
  195. disabled={widgetLimitReached}
  196. />
  197. ) : (
  198. <Button
  199. data-test-id="add-widget-library"
  200. priority="primary"
  201. size="sm"
  202. disabled={widgetLimitReached || !hasEditAccess}
  203. icon={<IconAdd isCircled />}
  204. onClick={() => {
  205. trackAnalytics('dashboards_views.widget_library.opened', {
  206. organization,
  207. });
  208. onAddWidget(defaultDataset);
  209. }}
  210. >
  211. {t('Add Widget')}
  212. </Button>
  213. )}
  214. </Tooltip>
  215. ) : null}
  216. </Fragment>
  217. )}
  218. </DashboardEditFeature>
  219. </StyledButtonBar>
  220. );
  221. }
  222. function DashboardEditFeature({
  223. children,
  224. }: {
  225. children: (hasFeature: boolean) => React.ReactNode;
  226. }) {
  227. const renderDisabled = p => (
  228. <Hovercard
  229. body={
  230. <FeatureDisabled
  231. features={p.features}
  232. hideHelpToggle
  233. featureName={t('Dashboard Editing')}
  234. />
  235. }
  236. >
  237. {p.children(p)}
  238. </Hovercard>
  239. );
  240. return (
  241. <Feature
  242. hookName="feature-disabled:dashboards-edit"
  243. features="organizations:dashboards-edit"
  244. renderDisabled={renderDisabled}
  245. >
  246. {({hasFeature}) => children(hasFeature)}
  247. </Feature>
  248. );
  249. }
  250. const StyledButtonBar = styled(ButtonBar)`
  251. @media (max-width: ${p => p.theme.breakpoints.small}) {
  252. grid-auto-flow: row;
  253. grid-row-gap: ${space(1)};
  254. width: 100%;
  255. }
  256. `;
  257. export default Controls;