controls.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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. currentUser,
  144. userTeams,
  145. organization,
  146. dashboard.permissions,
  147. dashboard.createdBy
  148. );
  149. }
  150. return (
  151. <StyledButtonBar gap={1} key="controls">
  152. <DashboardEditFeature>
  153. {hasFeature => (
  154. <Fragment>
  155. <FeedbackWidgetButton />
  156. <Feature features="dashboards-import">
  157. <Button
  158. data-test-id="dashboard-export"
  159. onClick={e => {
  160. e.preventDefault();
  161. exportDashboard();
  162. }}
  163. icon={<IconDownload />}
  164. priority="default"
  165. size="sm"
  166. >
  167. {t('Export Dashboard')}
  168. </Button>
  169. </Feature>
  170. <Feature features="dashboards-edit-access">
  171. <EditAccessSelector
  172. dashboard={dashboard}
  173. onChangeEditAccess={onChangeEditAccess}
  174. />
  175. </Feature>
  176. <Button
  177. data-test-id="dashboard-edit"
  178. onClick={e => {
  179. e.preventDefault();
  180. onEdit();
  181. }}
  182. icon={<IconEdit />}
  183. disabled={!hasFeature || hasUnsavedFilters || !hasEditAccess}
  184. title={
  185. !hasEditAccess
  186. ? t('You do not have permission to edit this dashboard')
  187. : hasUnsavedFilters && UNSAVED_FILTERS_MESSAGE
  188. }
  189. priority="default"
  190. size="sm"
  191. >
  192. {t('Edit Dashboard')}
  193. </Button>
  194. {hasFeature ? (
  195. <Tooltip
  196. title={tct('Max widgets ([maxWidgets]) per dashboard reached.', {
  197. maxWidgets: MAX_WIDGETS,
  198. })}
  199. disabled={!widgetLimitReached}
  200. >
  201. {hasCustomMetrics(organization) ? (
  202. <AddWidgetButton
  203. onAddWidget={onAddWidget}
  204. aria-label={t('Add Widget')}
  205. priority="primary"
  206. data-test-id="add-widget-library"
  207. disabled={widgetLimitReached}
  208. />
  209. ) : (
  210. <Button
  211. data-test-id="add-widget-library"
  212. priority="primary"
  213. size="sm"
  214. disabled={widgetLimitReached || !hasEditAccess}
  215. icon={<IconAdd isCircled />}
  216. onClick={() => {
  217. trackAnalytics('dashboards_views.widget_library.opened', {
  218. organization,
  219. });
  220. onAddWidget(defaultDataset);
  221. }}
  222. title={
  223. !hasEditAccess &&
  224. t('You do not have permission to edit this dashboard')
  225. }
  226. >
  227. {t('Add Widget')}
  228. </Button>
  229. )}
  230. </Tooltip>
  231. ) : null}
  232. </Fragment>
  233. )}
  234. </DashboardEditFeature>
  235. </StyledButtonBar>
  236. );
  237. }
  238. function DashboardEditFeature({
  239. children,
  240. }: {
  241. children: (hasFeature: boolean) => React.ReactNode;
  242. }) {
  243. const renderDisabled = p => (
  244. <Hovercard
  245. body={
  246. <FeatureDisabled
  247. features={p.features}
  248. hideHelpToggle
  249. featureName={t('Dashboard Editing')}
  250. />
  251. }
  252. >
  253. {p.children(p)}
  254. </Hovercard>
  255. );
  256. return (
  257. <Feature
  258. hookName="feature-disabled:dashboards-edit"
  259. features="organizations:dashboards-edit"
  260. renderDisabled={renderDisabled}
  261. >
  262. {({hasFeature}) => children(hasFeature)}
  263. </Feature>
  264. );
  265. }
  266. const StyledButtonBar = styled(ButtonBar)`
  267. @media (max-width: ${p => p.theme.breakpoints.small}) {
  268. grid-auto-flow: row;
  269. grid-row-gap: ${space(1)};
  270. width: 100%;
  271. }
  272. `;
  273. export default Controls;