controls.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {updateDashboardFavorite} from 'sentry/actionCreators/dashboards';
  4. import Feature from 'sentry/components/acl/feature';
  5. import FeatureDisabled from 'sentry/components/acl/featureDisabled';
  6. import {Button} from 'sentry/components/button';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import Confirm from 'sentry/components/confirm';
  9. import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
  10. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  11. import {Hovercard} from 'sentry/components/hovercard';
  12. import {Tooltip} from 'sentry/components/tooltip';
  13. import {IconAdd, IconDownload, IconEdit, IconStar} from 'sentry/icons';
  14. import {t, tct} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {Organization} from 'sentry/types/organization';
  17. import {trackAnalytics} from 'sentry/utils/analytics';
  18. import useApi from 'sentry/utils/useApi';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import {useUser} from 'sentry/utils/useUser';
  21. import {useUserTeams} from 'sentry/utils/useUserTeams';
  22. import EditAccessSelector from 'sentry/views/dashboards/editAccessSelector';
  23. import {DataSet} from 'sentry/views/dashboards/widgetBuilder/utils';
  24. import {checkUserHasEditAccess, UNSAVED_FILTERS_MESSAGE} from './detail';
  25. import exportDashboard from './exportDashboard';
  26. import type {DashboardDetails, DashboardListItem, DashboardPermissions} from './types';
  27. import {DashboardState, MAX_WIDGETS} from './types';
  28. type Props = {
  29. dashboard: DashboardDetails;
  30. dashboardState: DashboardState;
  31. dashboards: DashboardListItem[];
  32. onAddWidget: (dataset: DataSet) => void;
  33. onAddWidgetFromNewWidgetBuilder: (
  34. dataset: DataSet,
  35. openWidgetTemplates: boolean
  36. ) => void;
  37. onCancel: () => void;
  38. onCommit: () => void;
  39. onDelete: () => void;
  40. onEdit: () => void;
  41. organization: Organization;
  42. widgetLimitReached: boolean;
  43. hasUnsavedFilters?: boolean;
  44. onChangeEditAccess?: (newDashboardPermissions: DashboardPermissions) => void;
  45. };
  46. function Controls({
  47. dashboardState,
  48. dashboard,
  49. dashboards,
  50. hasUnsavedFilters,
  51. widgetLimitReached,
  52. onChangeEditAccess,
  53. onEdit,
  54. onCommit,
  55. onDelete,
  56. onCancel,
  57. onAddWidget,
  58. onAddWidgetFromNewWidgetBuilder,
  59. }: Props) {
  60. const [isFavorited, setIsFavorited] = useState(dashboard.isFavorited);
  61. function renderCancelButton(label = t('Cancel')) {
  62. return (
  63. <Button
  64. data-test-id="dashboard-cancel"
  65. size="sm"
  66. onClick={e => {
  67. e.preventDefault();
  68. onCancel();
  69. }}
  70. >
  71. {label}
  72. </Button>
  73. );
  74. }
  75. const organization = useOrganization();
  76. const currentUser = useUser();
  77. const {teams: userTeams} = useUserTeams();
  78. const api = useApi();
  79. if ([DashboardState.EDIT, DashboardState.PENDING_DELETE].includes(dashboardState)) {
  80. return (
  81. <StyledButtonBar gap={1} key="edit-controls">
  82. {renderCancelButton()}
  83. <Confirm
  84. priority="danger"
  85. message={t('Are you sure you want to delete this dashboard?')}
  86. onConfirm={onDelete}
  87. disabled={dashboards.length <= 1}
  88. >
  89. <Button size="sm" data-test-id="dashboard-delete" priority="danger">
  90. {t('Delete')}
  91. </Button>
  92. </Confirm>
  93. <Button
  94. data-test-id="dashboard-commit"
  95. size="sm"
  96. onClick={e => {
  97. e.preventDefault();
  98. onCommit();
  99. }}
  100. priority="primary"
  101. >
  102. {t('Save and Finish')}
  103. </Button>
  104. </StyledButtonBar>
  105. );
  106. }
  107. if (dashboardState === DashboardState.CREATE) {
  108. return (
  109. <StyledButtonBar gap={1} key="create-controls">
  110. {renderCancelButton()}
  111. <Button
  112. data-test-id="dashboard-commit"
  113. size="sm"
  114. onClick={e => {
  115. e.preventDefault();
  116. onCommit();
  117. }}
  118. priority="primary"
  119. >
  120. {t('Save and Finish')}
  121. </Button>
  122. </StyledButtonBar>
  123. );
  124. }
  125. if (dashboardState === DashboardState.PREVIEW) {
  126. return (
  127. <StyledButtonBar gap={1} key="preview-controls">
  128. {renderCancelButton(t('Go Back'))}
  129. <Button
  130. data-test-id="dashboard-commit"
  131. size="sm"
  132. onClick={e => {
  133. e.preventDefault();
  134. onCommit();
  135. }}
  136. priority="primary"
  137. >
  138. {t('Add Dashboard')}
  139. </Button>
  140. </StyledButtonBar>
  141. );
  142. }
  143. const defaultDataset = organization.features.includes(
  144. 'performance-discover-dataset-selector'
  145. )
  146. ? DataSet.ERRORS
  147. : DataSet.EVENTS;
  148. const hasEditAccess = checkUserHasEditAccess(
  149. currentUser,
  150. userTeams,
  151. organization,
  152. dashboard.permissions,
  153. dashboard.createdBy
  154. );
  155. const addWidgetDropdownItems: MenuItemProps[] = [
  156. {
  157. key: 'create-custom-widget',
  158. label: t('Create Custom Widget'),
  159. onAction: () => onAddWidgetFromNewWidgetBuilder(defaultDataset, false),
  160. },
  161. {
  162. key: 'from-widget-library',
  163. label: t('From Widget Library'),
  164. onAction: () => onAddWidgetFromNewWidgetBuilder(defaultDataset, true),
  165. },
  166. ];
  167. return (
  168. <StyledButtonBar gap={1} key="controls">
  169. <FeedbackWidgetButton />
  170. <DashboardEditFeature>
  171. {hasFeature => (
  172. <Fragment>
  173. <Feature features="dashboards-import">
  174. <Button
  175. data-test-id="dashboard-export"
  176. onClick={e => {
  177. e.preventDefault();
  178. exportDashboard();
  179. }}
  180. icon={<IconDownload />}
  181. priority="default"
  182. size="sm"
  183. >
  184. {t('Export Dashboard')}
  185. </Button>
  186. </Feature>
  187. {dashboard.id !== 'default-overview' && (
  188. <Feature features="dashboards-favourite">
  189. <Button
  190. size="sm"
  191. aria-label={'dashboards-favourite'}
  192. icon={
  193. <IconStar
  194. color={isFavorited ? 'yellow300' : 'gray300'}
  195. isSolid={isFavorited}
  196. aria-label={isFavorited ? t('UnFavorite') : t('Favorite')}
  197. data-test-id={isFavorited ? 'yellow-star' : 'empty-star'}
  198. />
  199. }
  200. onClick={async () => {
  201. try {
  202. setIsFavorited(!isFavorited);
  203. await updateDashboardFavorite(
  204. api,
  205. organization.slug,
  206. dashboard.id,
  207. !isFavorited
  208. );
  209. trackAnalytics('dashboards_manage.toggle_favorite', {
  210. organization,
  211. dashboard_id: dashboard.id,
  212. favorited: !isFavorited,
  213. });
  214. } catch (error) {
  215. // If the api call fails, revert the state
  216. setIsFavorited(isFavorited);
  217. }
  218. }}
  219. />
  220. </Feature>
  221. )}
  222. {dashboard.id !== 'default-overview' && (
  223. <EditAccessSelector
  224. dashboard={dashboard}
  225. onChangeEditAccess={onChangeEditAccess}
  226. />
  227. )}
  228. <Button
  229. data-test-id="dashboard-edit"
  230. onClick={e => {
  231. e.preventDefault();
  232. onEdit();
  233. }}
  234. icon={<IconEdit />}
  235. disabled={!hasFeature || hasUnsavedFilters || !hasEditAccess}
  236. title={
  237. !hasEditAccess
  238. ? t('You do not have permission to edit this dashboard')
  239. : hasUnsavedFilters && UNSAVED_FILTERS_MESSAGE
  240. }
  241. priority="default"
  242. size="sm"
  243. >
  244. {t('Edit Dashboard')}
  245. </Button>
  246. {hasFeature ? (
  247. <Tooltip
  248. title={tct('Max widgets ([maxWidgets]) per dashboard reached.', {
  249. maxWidgets: MAX_WIDGETS,
  250. })}
  251. disabled={!widgetLimitReached}
  252. >
  253. {organization.features.includes('dashboards-widget-builder-redesign') ? (
  254. <DropdownMenu
  255. items={addWidgetDropdownItems}
  256. isDisabled={widgetLimitReached || !hasEditAccess}
  257. triggerLabel={t('Add Widget')}
  258. triggerProps={{
  259. 'aria-label': t('Add Widget'),
  260. size: 'sm',
  261. showChevron: true,
  262. icon: <IconAdd isCircled size="sm" />,
  263. priority: 'primary',
  264. }}
  265. position="bottom-end"
  266. />
  267. ) : (
  268. <Button
  269. data-test-id="add-widget-library"
  270. priority="primary"
  271. size="sm"
  272. disabled={widgetLimitReached || !hasEditAccess}
  273. icon={<IconAdd isCircled />}
  274. onClick={() => {
  275. trackAnalytics('dashboards_views.widget_library.opened', {
  276. organization,
  277. });
  278. onAddWidget(defaultDataset);
  279. }}
  280. title={
  281. !hasEditAccess &&
  282. t('You do not have permission to edit this dashboard')
  283. }
  284. >
  285. {t('Add Widget')}
  286. </Button>
  287. )}
  288. </Tooltip>
  289. ) : null}
  290. </Fragment>
  291. )}
  292. </DashboardEditFeature>
  293. </StyledButtonBar>
  294. );
  295. }
  296. function DashboardEditFeature({
  297. children,
  298. }: {
  299. children: (hasFeature: boolean) => React.ReactNode;
  300. }) {
  301. const renderDisabled = (p: any) => (
  302. <Hovercard
  303. body={
  304. <FeatureDisabled
  305. features={p.features}
  306. hideHelpToggle
  307. featureName={t('Dashboard Editing')}
  308. />
  309. }
  310. >
  311. {p.children(p)}
  312. </Hovercard>
  313. );
  314. return (
  315. <Feature
  316. hookName="feature-disabled:dashboards-edit"
  317. features="organizations:dashboards-edit"
  318. renderDisabled={renderDisabled}
  319. >
  320. {({hasFeature}) => children(hasFeature)}
  321. </Feature>
  322. );
  323. }
  324. const StyledButtonBar = styled(ButtonBar)`
  325. @media (max-width: ${p => p.theme.breakpoints.small}) {
  326. grid-auto-flow: row;
  327. grid-row-gap: ${space(1)};
  328. width: 100%;
  329. }
  330. `;
  331. export default Controls;