controls.tsx 8.0 KB

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