bulkEditMonitorsModal.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import {Fragment, useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  5. import type {BulkEditOperation} from 'sentry/actionCreators/monitors';
  6. import {bulkEditMonitors} from 'sentry/actionCreators/monitors';
  7. import {Button} from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import Checkbox from 'sentry/components/checkbox';
  10. import Pagination from 'sentry/components/pagination';
  11. import {PanelTable} from 'sentry/components/panels/panelTable';
  12. import Placeholder from 'sentry/components/placeholder';
  13. import SearchBar from 'sentry/components/searchBar';
  14. import Text from 'sentry/components/text';
  15. import {t, tct, tn} from 'sentry/locale';
  16. import {space} from 'sentry/styles/space';
  17. import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
  18. import useApi from 'sentry/utils/useApi';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import {
  22. MonitorSortOption,
  23. MonitorSortOrder,
  24. SortSelector,
  25. } from 'sentry/views/monitors/components/overviewTimeline/sortSelector';
  26. import type {Monitor} from 'sentry/views/monitors/types';
  27. import {makeMonitorListQueryKey} from 'sentry/views/monitors/utils';
  28. import {scheduleAsText} from 'sentry/views/monitors/utils/scheduleAsText';
  29. interface Props extends ModalRenderProps {}
  30. const NUM_PLACEHOLDER_ROWS = 5;
  31. export function BulkEditMonitorsModal({Header, Body, Footer, closeModal}: Props) {
  32. const organization = useOrganization();
  33. const location = useLocation();
  34. const queryClient = useQueryClient();
  35. const api = useApi();
  36. const [isUpdating, setIsUpdating] = useState<boolean>(false);
  37. const [searchQuery, setSearchQuery] = useState<string>('');
  38. const [cursor, setCursor] = useState<string | undefined>();
  39. const [sortSelection, setSortSelection] = useState<{
  40. order: MonitorSortOrder;
  41. sort: MonitorSortOption;
  42. }>({sort: MonitorSortOption.STATUS, order: MonitorSortOrder.ASCENDING});
  43. const queryKey = makeMonitorListQueryKey(organization, {
  44. ...location.query,
  45. query: searchQuery,
  46. cursor,
  47. sort: sortSelection.sort,
  48. asc: sortSelection.order,
  49. });
  50. const [selectedMonitors, setSelectedMonitors] = useState<Monitor[]>([]);
  51. const isMonitorSelected = (monitor: Monitor): boolean => {
  52. return !!selectedMonitors.find(m => m.slug === monitor.slug);
  53. };
  54. const handleSearch = (query: string) => {
  55. setSearchQuery(query);
  56. setCursor(undefined);
  57. };
  58. const handleToggleMonitor = (monitor: Monitor) => {
  59. const checked = isMonitorSelected(monitor);
  60. if (!checked) {
  61. setSelectedMonitors([...selectedMonitors, monitor]);
  62. } else {
  63. setSelectedMonitors(selectedMonitors.filter(m => m.slug !== monitor.slug));
  64. }
  65. };
  66. const handleBulkEdit = async (operation: BulkEditOperation) => {
  67. setIsUpdating(true);
  68. const resp = await bulkEditMonitors(
  69. api,
  70. organization.slug,
  71. selectedMonitors.map(monitor => monitor.slug),
  72. operation
  73. );
  74. setSelectedMonitors([]);
  75. if (resp?.updated) {
  76. setApiQueryData(queryClient, queryKey, (oldMonitorList: Monitor[]) => {
  77. return oldMonitorList.map(
  78. monitor =>
  79. resp.updated.find(newMonitor => newMonitor.slug === monitor.slug) ?? monitor
  80. );
  81. });
  82. }
  83. setIsUpdating(false);
  84. };
  85. const {
  86. data: monitorList,
  87. getResponseHeader: monitorListHeaders,
  88. isLoading,
  89. } = useApiQuery<Monitor[]>(queryKey, {
  90. staleTime: 0,
  91. });
  92. const monitorPageLinks = monitorListHeaders?.('Link');
  93. const headers = [t('Monitor'), t('State'), t('Muted'), t('Schedule')];
  94. const shouldDisable = selectedMonitors.every(monitor => monitor.status !== 'disabled');
  95. const shouldMute = selectedMonitors.every(monitor => !monitor.isMuted);
  96. const disableEnableBtnParams = {
  97. operation: {status: shouldDisable ? 'disabled' : 'active'} as BulkEditOperation,
  98. actionText: shouldDisable ? t('Disable') : t('Enable'),
  99. analyticsEventKey: 'crons_bulk_edit_modal.disable_enable_click',
  100. analyticsEventName: 'Crons Bulk Edit Modal: Disable Enable Click',
  101. };
  102. const muteUnmuteBtnParams = {
  103. operation: {isMuted: shouldMute ? true : false},
  104. actionText: shouldMute ? t('Mute') : t('Unmute'),
  105. analyticsEventKey: 'crons_bulk_edit_modal.mute_unmute_click',
  106. analyticsEventName: 'Crons Bulk Edit Modal: Mute Unmute Click',
  107. };
  108. return (
  109. <Fragment>
  110. <Header closeButton>
  111. <h3>{t('Manage Monitors')}</h3>
  112. </Header>
  113. <Body>
  114. <Actions>
  115. <ActionButtons gap={1}>
  116. {[disableEnableBtnParams, muteUnmuteBtnParams].map(
  117. ({operation, actionText, ...analyticsProps}, i) => (
  118. <Button
  119. key={i}
  120. size="sm"
  121. onClick={() => handleBulkEdit(operation)}
  122. disabled={isUpdating || selectedMonitors.length === 0}
  123. title={
  124. selectedMonitors.length === 0 &&
  125. tct('Please select monitors to [actionText]', {actionText})
  126. }
  127. aria-label={actionText}
  128. {...analyticsProps}
  129. >
  130. {selectedMonitors.length > 0
  131. ? `${actionText} ${tn(
  132. '%s monitor',
  133. '%s monitors',
  134. selectedMonitors.length
  135. )}`
  136. : actionText}
  137. </Button>
  138. )
  139. )}
  140. </ActionButtons>
  141. <SearchBar
  142. size="sm"
  143. placeholder={t('Search Monitors')}
  144. query={searchQuery}
  145. onSearch={handleSearch}
  146. />
  147. <SortSelector
  148. size="sm"
  149. onChangeOrder={({value: order}) =>
  150. setSortSelection({...sortSelection, order})
  151. }
  152. onChangeSort={({value: sort}) => setSortSelection({...sortSelection, sort})}
  153. {...sortSelection}
  154. />
  155. </Actions>
  156. <StyledPanelTable
  157. headers={headers}
  158. stickyHeaders
  159. isEmpty={monitorList?.length === 0}
  160. emptyMessage={t('No monitors found')}
  161. >
  162. {isLoading || !monitorList
  163. ? [...new Array(NUM_PLACEHOLDER_ROWS)].map((_, i) => (
  164. <RowPlaceholder key={i}>
  165. <Placeholder height="2rem" />
  166. </RowPlaceholder>
  167. ))
  168. : monitorList.map(monitor => (
  169. <Fragment key={monitor.slug}>
  170. <MonitorSlug>
  171. <Checkbox
  172. checked={isMonitorSelected(monitor)}
  173. onChange={() => {
  174. handleToggleMonitor(monitor);
  175. }}
  176. />
  177. <Text>{monitor.slug}</Text>
  178. </MonitorSlug>
  179. <Text>{monitor.status === 'active' ? t('Active') : t('Disabled')}</Text>
  180. <Text>{monitor.isMuted ? t('Yes') : t('No')}</Text>
  181. <Text>{scheduleAsText(monitor.config)}</Text>
  182. </Fragment>
  183. ))}
  184. </StyledPanelTable>
  185. {monitorPageLinks && (
  186. <Pagination pageLinks={monitorListHeaders?.('Link')} onCursor={setCursor} />
  187. )}
  188. </Body>
  189. <Footer>
  190. <Button priority="primary" onClick={closeModal} aria-label={t('Done')}>
  191. {t('Done')}
  192. </Button>
  193. </Footer>
  194. </Fragment>
  195. );
  196. }
  197. export const modalCss = css`
  198. width: 100%;
  199. max-width: 900px;
  200. `;
  201. const Actions = styled('div')`
  202. display: grid;
  203. grid-template-columns: 1fr max-content max-content;
  204. gap: ${space(1)};
  205. margin-bottom: ${space(2)};
  206. `;
  207. const ActionButtons = styled(ButtonBar)`
  208. margin-right: auto;
  209. `;
  210. const StyledPanelTable = styled(PanelTable)`
  211. overflow: scroll;
  212. max-height: 425px;
  213. `;
  214. const RowPlaceholder = styled('div')`
  215. grid-column: 1 / -1;
  216. padding: ${space(1)};
  217. `;
  218. const MonitorSlug = styled('div')`
  219. display: grid;
  220. grid-template-columns: max-content 1fr;
  221. gap: ${space(1)};
  222. align-items: center;
  223. `;
  224. export default BulkEditMonitorsModal;