123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- import {Fragment, useState} from 'react';
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import type {ModalRenderProps} from 'sentry/actionCreators/modal';
- import type {BulkEditOperation} from 'sentry/actionCreators/monitors';
- import {bulkEditMonitors} from 'sentry/actionCreators/monitors';
- import {Button} from 'sentry/components/button';
- import ButtonBar from 'sentry/components/buttonBar';
- import Checkbox from 'sentry/components/checkbox';
- import Pagination from 'sentry/components/pagination';
- import {PanelTable} from 'sentry/components/panels/panelTable';
- import Placeholder from 'sentry/components/placeholder';
- import SearchBar from 'sentry/components/searchBar';
- import Text from 'sentry/components/text';
- import {t, tct, tn} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
- import useApi from 'sentry/utils/useApi';
- import {useLocation} from 'sentry/utils/useLocation';
- import useOrganization from 'sentry/utils/useOrganization';
- import {
- MonitorSortOption,
- MonitorSortOrder,
- SortSelector,
- } from 'sentry/views/monitors/components/overviewTimeline/sortSelector';
- import type {Monitor} from 'sentry/views/monitors/types';
- import {makeMonitorListQueryKey} from 'sentry/views/monitors/utils';
- import {scheduleAsText} from 'sentry/views/monitors/utils/scheduleAsText';
- interface Props extends ModalRenderProps {}
- const NUM_PLACEHOLDER_ROWS = 5;
- export function BulkEditMonitorsModal({Header, Body, Footer, closeModal}: Props) {
- const organization = useOrganization();
- const location = useLocation();
- const queryClient = useQueryClient();
- const api = useApi();
- const [isUpdating, setIsUpdating] = useState<boolean>(false);
- const [searchQuery, setSearchQuery] = useState<string>('');
- const [cursor, setCursor] = useState<string | undefined>();
- const [sortSelection, setSortSelection] = useState<{
- order: MonitorSortOrder;
- sort: MonitorSortOption;
- }>({sort: MonitorSortOption.STATUS, order: MonitorSortOrder.ASCENDING});
- const queryKey = makeMonitorListQueryKey(organization, {
- ...location.query,
- query: searchQuery,
- cursor,
- sort: sortSelection.sort,
- asc: sortSelection.order,
- });
- const [selectedMonitors, setSelectedMonitors] = useState<Monitor[]>([]);
- const isMonitorSelected = (monitor: Monitor): boolean => {
- return !!selectedMonitors.find(m => m.slug === monitor.slug);
- };
- const handleSearch = (query: string) => {
- setSearchQuery(query);
- setCursor(undefined);
- };
- const handleToggleMonitor = (monitor: Monitor) => {
- const checked = isMonitorSelected(monitor);
- if (!checked) {
- setSelectedMonitors([...selectedMonitors, monitor]);
- } else {
- setSelectedMonitors(selectedMonitors.filter(m => m.slug !== monitor.slug));
- }
- };
- const handleBulkEdit = async (operation: BulkEditOperation) => {
- setIsUpdating(true);
- const resp = await bulkEditMonitors(
- api,
- organization.slug,
- selectedMonitors.map(monitor => monitor.slug),
- operation
- );
- setSelectedMonitors([]);
- if (resp?.updated) {
- setApiQueryData(queryClient, queryKey, (oldMonitorList: Monitor[]) => {
- return oldMonitorList.map(
- monitor =>
- resp.updated.find(newMonitor => newMonitor.slug === monitor.slug) ?? monitor
- );
- });
- }
- setIsUpdating(false);
- };
- const {
- data: monitorList,
- getResponseHeader: monitorListHeaders,
- isLoading,
- } = useApiQuery<Monitor[]>(queryKey, {
- staleTime: 0,
- });
- const monitorPageLinks = monitorListHeaders?.('Link');
- const headers = [t('Monitor'), t('State'), t('Muted'), t('Schedule')];
- const shouldDisable = selectedMonitors.every(monitor => monitor.status !== 'disabled');
- const shouldMute = selectedMonitors.every(monitor => !monitor.isMuted);
- const disableEnableBtnParams = {
- operation: {status: shouldDisable ? 'disabled' : 'active'} as BulkEditOperation,
- actionText: shouldDisable ? t('Disable') : t('Enable'),
- analyticsEventKey: 'crons_bulk_edit_modal.disable_enable_click',
- analyticsEventName: 'Crons Bulk Edit Modal: Disable Enable Click',
- };
- const muteUnmuteBtnParams = {
- operation: {isMuted: shouldMute ? true : false},
- actionText: shouldMute ? t('Mute') : t('Unmute'),
- analyticsEventKey: 'crons_bulk_edit_modal.mute_unmute_click',
- analyticsEventName: 'Crons Bulk Edit Modal: Mute Unmute Click',
- };
- return (
- <Fragment>
- <Header closeButton>
- <h3>{t('Manage Monitors')}</h3>
- </Header>
- <Body>
- <Actions>
- <ActionButtons gap={1}>
- {[disableEnableBtnParams, muteUnmuteBtnParams].map(
- ({operation, actionText, ...analyticsProps}, i) => (
- <Button
- key={i}
- size="sm"
- onClick={() => handleBulkEdit(operation)}
- disabled={isUpdating || selectedMonitors.length === 0}
- title={
- selectedMonitors.length === 0 &&
- tct('Please select monitors to [actionText]', {actionText})
- }
- aria-label={actionText}
- {...analyticsProps}
- >
- {selectedMonitors.length > 0
- ? `${actionText} ${tn(
- '%s monitor',
- '%s monitors',
- selectedMonitors.length
- )}`
- : actionText}
- </Button>
- )
- )}
- </ActionButtons>
- <SearchBar
- size="sm"
- placeholder={t('Search Monitors')}
- query={searchQuery}
- onSearch={handleSearch}
- />
- <SortSelector
- size="sm"
- onChangeOrder={({value: order}) =>
- setSortSelection({...sortSelection, order})
- }
- onChangeSort={({value: sort}) => setSortSelection({...sortSelection, sort})}
- {...sortSelection}
- />
- </Actions>
- <StyledPanelTable
- headers={headers}
- stickyHeaders
- isEmpty={monitorList?.length === 0}
- emptyMessage={t('No monitors found')}
- >
- {isLoading || !monitorList
- ? [...new Array(NUM_PLACEHOLDER_ROWS)].map((_, i) => (
- <RowPlaceholder key={i}>
- <Placeholder height="2rem" />
- </RowPlaceholder>
- ))
- : monitorList.map(monitor => (
- <Fragment key={monitor.slug}>
- <MonitorSlug>
- <Checkbox
- checked={isMonitorSelected(monitor)}
- onChange={() => {
- handleToggleMonitor(monitor);
- }}
- />
- <Text>{monitor.slug}</Text>
- </MonitorSlug>
- <Text>{monitor.status === 'active' ? t('Active') : t('Disabled')}</Text>
- <Text>{monitor.isMuted ? t('Yes') : t('No')}</Text>
- <Text>{scheduleAsText(monitor.config)}</Text>
- </Fragment>
- ))}
- </StyledPanelTable>
- {monitorPageLinks && (
- <Pagination pageLinks={monitorListHeaders?.('Link')} onCursor={setCursor} />
- )}
- </Body>
- <Footer>
- <Button priority="primary" onClick={closeModal} aria-label={t('Done')}>
- {t('Done')}
- </Button>
- </Footer>
- </Fragment>
- );
- }
- export const modalCss = css`
- width: 100%;
- max-width: 900px;
- `;
- const Actions = styled('div')`
- display: grid;
- grid-template-columns: 1fr max-content max-content;
- gap: ${space(1)};
- margin-bottom: ${space(2)};
- `;
- const ActionButtons = styled(ButtonBar)`
- margin-right: auto;
- `;
- const StyledPanelTable = styled(PanelTable)`
- overflow: scroll;
- max-height: 425px;
- `;
- const RowPlaceholder = styled('div')`
- grid-column: 1 / -1;
- padding: ${space(1)};
- `;
- const MonitorSlug = styled('div')`
- display: grid;
- grid-template-columns: max-content 1fr;
- gap: ${space(1)};
- align-items: center;
- `;
- export default BulkEditMonitorsModal;
|