dashboardTable.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import styled from '@emotion/styled';
  2. import type {Location} from 'history';
  3. import {
  4. createDashboard,
  5. deleteDashboard,
  6. fetchDashboard,
  7. } from 'sentry/actionCreators/dashboards';
  8. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  9. import type {Client} from 'sentry/api';
  10. import {ActivityAvatar} from 'sentry/components/activity/item/avatar';
  11. import {Button} from 'sentry/components/button';
  12. import {openConfirmModal} from 'sentry/components/confirm';
  13. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  14. import GridEditable, {
  15. COL_WIDTH_UNDEFINED,
  16. type GridColumnOrder,
  17. } from 'sentry/components/gridEditable';
  18. import Link from 'sentry/components/links/link';
  19. import TimeSince from 'sentry/components/timeSince';
  20. import {IconCopy, IconDelete} from 'sentry/icons';
  21. import {t} from 'sentry/locale';
  22. import {space} from 'sentry/styles/space';
  23. import type {Organization} from 'sentry/types/organization';
  24. import {trackAnalytics} from 'sentry/utils/analytics';
  25. import withApi from 'sentry/utils/withApi';
  26. import type {DashboardListItem} from 'sentry/views/dashboards/types';
  27. import {cloneDashboard} from '../utils';
  28. type Props = {
  29. api: Client;
  30. dashboards: DashboardListItem[] | undefined;
  31. location: Location;
  32. onDashboardsChange: () => void;
  33. organization: Organization;
  34. isLoading?: boolean;
  35. };
  36. enum ResponseKeys {
  37. NAME = 'title',
  38. WIDGETS = 'widgetDisplay',
  39. OWNER = 'createdBy',
  40. CREATED = 'dateCreated',
  41. }
  42. function DashboardTable({
  43. api,
  44. organization,
  45. location,
  46. dashboards,
  47. onDashboardsChange,
  48. isLoading,
  49. }: Props) {
  50. const columnOrder = [
  51. {key: ResponseKeys.NAME, name: t('Name'), width: COL_WIDTH_UNDEFINED},
  52. {key: ResponseKeys.WIDGETS, name: t('Widgets'), width: COL_WIDTH_UNDEFINED},
  53. {key: ResponseKeys.OWNER, name: t('Owner'), width: COL_WIDTH_UNDEFINED},
  54. {key: ResponseKeys.CREATED, name: t('Created'), width: COL_WIDTH_UNDEFINED},
  55. ];
  56. function handleDelete(dashboard: DashboardListItem) {
  57. deleteDashboard(api, organization.slug, dashboard.id)
  58. .then(() => {
  59. trackAnalytics('dashboards_manage.delete', {
  60. organization,
  61. dashboard_id: parseInt(dashboard.id, 10),
  62. });
  63. onDashboardsChange();
  64. addSuccessMessage(t('Dashboard deleted'));
  65. })
  66. .catch(() => {
  67. addErrorMessage(t('Error deleting Dashboard'));
  68. });
  69. }
  70. async function handleDuplicate(dashboard: DashboardListItem) {
  71. try {
  72. const dashboardDetail = await fetchDashboard(api, organization.slug, dashboard.id);
  73. const newDashboard = cloneDashboard(dashboardDetail);
  74. newDashboard.widgets.map(widget => (widget.id = undefined));
  75. await createDashboard(api, organization.slug, newDashboard, true);
  76. trackAnalytics('dashboards_manage.duplicate', {
  77. organization,
  78. dashboard_id: parseInt(dashboard.id, 10),
  79. });
  80. onDashboardsChange();
  81. addSuccessMessage(t('Dashboard duplicated'));
  82. } catch (e) {
  83. addErrorMessage(t('Error duplicating Dashboard'));
  84. }
  85. }
  86. // TODO(__SENTRY_USING_REACT_ROUTER_SIX): We can remove this later, react
  87. // router 6 handles empty query objects without appending a trailing ?
  88. const queryLocation = {
  89. ...(location.query && Object.keys(location.query).length > 0
  90. ? {query: location.query}
  91. : {}),
  92. };
  93. const renderBodyCell = (
  94. column: GridColumnOrder<string>,
  95. dataRow: DashboardListItem
  96. ) => {
  97. if (column.key === ResponseKeys.NAME) {
  98. return (
  99. <Link
  100. to={{
  101. pathname: `/organizations/${organization.slug}/dashboard/${dataRow.id}/`,
  102. ...queryLocation,
  103. }}
  104. >
  105. {dataRow[ResponseKeys.NAME]}
  106. </Link>
  107. );
  108. }
  109. if (column.key === ResponseKeys.WIDGETS) {
  110. return dataRow[ResponseKeys.WIDGETS].length;
  111. }
  112. if (column.key === ResponseKeys.OWNER) {
  113. return dataRow[ResponseKeys.OWNER] ? (
  114. <ActivityAvatar type="user" user={dataRow[ResponseKeys.OWNER]} size={26} />
  115. ) : (
  116. <ActivityAvatar type="system" size={26} />
  117. );
  118. }
  119. if (column.key === ResponseKeys.CREATED) {
  120. return (
  121. <DateActionsContainer>
  122. <DateSelected>
  123. {dataRow[ResponseKeys.CREATED] ? (
  124. <DateStatus>
  125. <TimeSince date={dataRow[ResponseKeys.CREATED]} />
  126. </DateStatus>
  127. ) : (
  128. <DateStatus />
  129. )}
  130. </DateSelected>
  131. <ActionsIconWrapper>
  132. <StyledButton
  133. onClick={e => {
  134. e.stopPropagation();
  135. handleDuplicate(dataRow);
  136. }}
  137. aria-label={t('Duplicate Dashboard')}
  138. data-test-id={'dashboard-duplicate'}
  139. icon={<IconCopy />}
  140. size="sm"
  141. />
  142. <StyledButton
  143. onClick={e => {
  144. e.stopPropagation();
  145. openConfirmModal({
  146. message: t('Are you sure you want to delete this dashboard?'),
  147. priority: 'danger',
  148. onConfirm: () => handleDelete(dataRow),
  149. });
  150. }}
  151. aria-label={t('Delete Dashboard')}
  152. data-test-id={'dashboard-delete'}
  153. icon={<IconDelete />}
  154. size="sm"
  155. disabled={dashboards && dashboards.length <= 1}
  156. />
  157. </ActionsIconWrapper>
  158. </DateActionsContainer>
  159. );
  160. }
  161. return <span>{dataRow[column.key]}</span>;
  162. };
  163. return (
  164. <GridEditable
  165. data={dashboards ?? []}
  166. columnOrder={columnOrder}
  167. columnSortBy={[]}
  168. grid={{
  169. renderBodyCell,
  170. }}
  171. isLoading={isLoading}
  172. emptyMessage={
  173. <EmptyStateWarning>
  174. <p>{t('Sorry, no Dashboards match your filters.')}</p>
  175. </EmptyStateWarning>
  176. }
  177. />
  178. );
  179. }
  180. export default withApi(DashboardTable);
  181. const DateSelected = styled('div')`
  182. font-size: ${p => p.theme.fontSizeMedium};
  183. display: grid;
  184. grid-column-gap: ${space(1)};
  185. color: ${p => p.theme.textColor};
  186. ${p => p.theme.overflowEllipsis};
  187. `;
  188. const DateStatus = styled('span')`
  189. color: ${p => p.theme.textColor};
  190. padding-left: ${space(1)};
  191. `;
  192. const DateActionsContainer = styled('div')`
  193. display: flex;
  194. gap: ${space(4)};
  195. justify-content: space-between;
  196. align-items: center;
  197. `;
  198. const ActionsIconWrapper = styled('div')`
  199. display: flex;
  200. `;
  201. const StyledButton = styled(Button)`
  202. border: none;
  203. box-shadow: none;
  204. `;