controls.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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 GuideAnchor from 'sentry/components/assistant/guideAnchor';
  6. import Button from 'sentry/components/button';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import Confirm from 'sentry/components/confirm';
  9. import {Hovercard} from 'sentry/components/hovercard';
  10. import Tooltip from 'sentry/components/tooltip';
  11. import {IconAdd, IconEdit} from 'sentry/icons';
  12. import {t, tct} from 'sentry/locale';
  13. import space from 'sentry/styles/space';
  14. import {Organization} from 'sentry/types';
  15. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  16. import {DashboardListItem, DashboardState, MAX_WIDGETS} from './types';
  17. type Props = {
  18. dashboardState: DashboardState;
  19. dashboards: DashboardListItem[];
  20. onAddWidget: () => void;
  21. onCancel: () => void;
  22. onCommit: () => void;
  23. onDelete: () => void;
  24. onEdit: () => void;
  25. organization: Organization;
  26. widgetLimitReached: boolean;
  27. };
  28. function Controls({
  29. organization,
  30. dashboardState,
  31. dashboards,
  32. widgetLimitReached,
  33. onEdit,
  34. onCommit,
  35. onDelete,
  36. onCancel,
  37. onAddWidget,
  38. }: Props) {
  39. function renderCancelButton(label = t('Cancel')) {
  40. return (
  41. <Button
  42. data-test-id="dashboard-cancel"
  43. onClick={e => {
  44. e.preventDefault();
  45. onCancel();
  46. }}
  47. >
  48. {label}
  49. </Button>
  50. );
  51. }
  52. if ([DashboardState.EDIT, DashboardState.PENDING_DELETE].includes(dashboardState)) {
  53. return (
  54. <StyledButtonBar gap={1} key="edit-controls">
  55. {renderCancelButton()}
  56. <Confirm
  57. priority="danger"
  58. message={t('Are you sure you want to delete this dashboard?')}
  59. onConfirm={onDelete}
  60. disabled={dashboards.length <= 1}
  61. >
  62. <Button data-test-id="dashboard-delete" priority="danger">
  63. {t('Delete')}
  64. </Button>
  65. </Confirm>
  66. <Button
  67. data-test-id="dashboard-commit"
  68. onClick={e => {
  69. e.preventDefault();
  70. onCommit();
  71. }}
  72. priority="primary"
  73. >
  74. {t('Save and Finish')}
  75. </Button>
  76. </StyledButtonBar>
  77. );
  78. }
  79. if (dashboardState === DashboardState.CREATE) {
  80. return (
  81. <StyledButtonBar gap={1} key="create-controls">
  82. {renderCancelButton()}
  83. <Button
  84. data-test-id="dashboard-commit"
  85. onClick={e => {
  86. e.preventDefault();
  87. onCommit();
  88. }}
  89. priority="primary"
  90. >
  91. {t('Save and Finish')}
  92. </Button>
  93. </StyledButtonBar>
  94. );
  95. }
  96. if (dashboardState === DashboardState.PREVIEW) {
  97. return (
  98. <StyledButtonBar gap={1} key="preview-controls">
  99. {renderCancelButton(t('Go Back'))}
  100. <Button
  101. data-test-id="dashboard-commit"
  102. onClick={e => {
  103. e.preventDefault();
  104. onCommit();
  105. }}
  106. priority="primary"
  107. >
  108. {t('Add Dashboard')}
  109. </Button>
  110. </StyledButtonBar>
  111. );
  112. }
  113. return (
  114. <StyledButtonBar gap={1} key="controls">
  115. <DashboardEditFeature>
  116. {hasFeature => (
  117. <Fragment>
  118. <Button
  119. data-test-id="dashboard-edit"
  120. onClick={e => {
  121. e.preventDefault();
  122. onEdit();
  123. }}
  124. icon={<IconEdit />}
  125. disabled={!hasFeature}
  126. priority="default"
  127. >
  128. {t('Edit Dashboard')}
  129. </Button>
  130. {hasFeature ? (
  131. <Tooltip
  132. title={tct('Max widgets ([maxWidgets]) per dashboard reached.', {
  133. maxWidgets: MAX_WIDGETS,
  134. })}
  135. disabled={!!!widgetLimitReached}
  136. >
  137. <GuideAnchor
  138. disabled={!!!organization.features.includes('dashboards-releases')}
  139. target="releases_widget"
  140. >
  141. <Button
  142. data-test-id="add-widget-library"
  143. priority="primary"
  144. disabled={widgetLimitReached}
  145. icon={<IconAdd isCircled />}
  146. onClick={() => {
  147. trackAdvancedAnalyticsEvent(
  148. 'dashboards_views.widget_library.opened',
  149. {
  150. organization,
  151. }
  152. );
  153. onAddWidget();
  154. }}
  155. >
  156. {t('Add Widget')}
  157. </Button>
  158. </GuideAnchor>
  159. </Tooltip>
  160. ) : null}
  161. </Fragment>
  162. )}
  163. </DashboardEditFeature>
  164. </StyledButtonBar>
  165. );
  166. }
  167. const DashboardEditFeature = ({
  168. children,
  169. }: {
  170. children: (hasFeature: boolean) => React.ReactNode;
  171. }) => {
  172. const renderDisabled = p => (
  173. <Hovercard
  174. body={
  175. <FeatureDisabled
  176. features={p.features}
  177. hideHelpToggle
  178. featureName={t('Dashboard Editing')}
  179. />
  180. }
  181. >
  182. {p.children(p)}
  183. </Hovercard>
  184. );
  185. return (
  186. <Feature
  187. hookName="feature-disabled:dashboards-edit"
  188. features={['organizations:dashboards-edit']}
  189. renderDisabled={renderDisabled}
  190. >
  191. {({hasFeature}) => children(hasFeature)}
  192. </Feature>
  193. );
  194. };
  195. const StyledButtonBar = styled(ButtonBar)`
  196. @media (max-width: ${p => p.theme.breakpoints.small}) {
  197. grid-auto-flow: row;
  198. grid-row-gap: ${space(1)};
  199. width: 100%;
  200. }
  201. `;
  202. export default Controls;