Browse Source

feat(crons): Allow for individual deletion of monitor environments (#57041)

Tinkered with this a lot and in the end decided to just get this minimal
version of deleting individual monitor environments.

On hover, an ellipsis dropdown menu will appear with an option to delete
the individual environment.

<img width="861" alt="image"
src="https://github.com/getsentry/sentry/assets/9372512/c45c1999-7719-4f22-b4de-dfa827f329a0">

Currently it supports deleting from the listing page, but will add
functionality for the monitor details page to have similar behavior.
David Wang 1 year ago
parent
commit
b002410d17

+ 3 - 1
static/app/actionCreators/monitors.tsx

@@ -26,7 +26,7 @@ export async function deleteMonitorEnvironment(
   orgId: string,
   monitorSlug: string,
   environment: string
-) {
+): Promise<boolean> {
   addLoadingMessage(t('Deleting Environment...'));
 
   try {
@@ -37,9 +37,11 @@ export async function deleteMonitorEnvironment(
       },
     });
     clearIndicators();
+    return true;
   } catch {
     addErrorMessage(t('Unable to remove environment from monitor.'));
   }
+  return false;
 }
 
 export async function updateMonitor(

+ 41 - 1
static/app/views/monitors/components/overviewTimeline/index.tsx

@@ -1,10 +1,12 @@
 import {useRef} from 'react';
 import styled from '@emotion/styled';
 
+import {deleteMonitorEnvironment} from 'sentry/actionCreators/monitors';
 import Panel from 'sentry/components/panels/panel';
 import {Sticky} from 'sentry/components/sticky';
 import {space} from 'sentry/styles/space';
-import {useApiQuery} from 'sentry/utils/queryClient';
+import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
+import useApi from 'sentry/utils/useApi';
 import {useDimensions} from 'sentry/utils/useDimensions';
 import useOrganization from 'sentry/utils/useOrganization';
 import useRouter from 'sentry/utils/useRouter';
@@ -12,6 +14,7 @@ import {
   GridLineOverlay,
   GridLineTimeLabels,
 } from 'sentry/views/monitors/components/overviewTimeline/gridLines';
+import {makeMonitorListQueryKey} from 'sentry/views/monitors/utils';
 
 import {Monitor} from '../../types';
 
@@ -27,6 +30,9 @@ interface Props {
 export function OverviewTimeline({monitorList}: Props) {
   const {location} = useRouter();
   const organization = useOrganization();
+  const api = useApi();
+  const queryClient = useQueryClient();
+  const router = useRouter();
 
   const timeWindow: TimeWindow = location.query?.timeWindow ?? '24h';
   const nowRef = useRef<Date>(new Date());
@@ -56,6 +62,39 @@ export function OverviewTimeline({monitorList}: Props) {
     }
   );
 
+  const handleDeleteEnvironment = async (monitor: Monitor, env: string) => {
+    const success = await deleteMonitorEnvironment(
+      api,
+      organization.slug,
+      monitor.slug,
+      env
+    );
+    if (!success) {
+      return;
+    }
+
+    const queryKey = makeMonitorListQueryKey(organization, router.location);
+    setApiQueryData(queryClient, queryKey, (oldMonitorList: Monitor[]) => {
+      const oldMonitorIdx = oldMonitorList.findIndex(m => m.slug === monitor.slug);
+      if (oldMonitorIdx < 0) {
+        return oldMonitorList;
+      }
+
+      const oldMonitor = oldMonitorList[oldMonitorIdx];
+      const newEnvList = oldMonitor.environments.filter(e => e.name !== env);
+      const newMonitor = {
+        ...oldMonitor,
+        environments: newEnvList,
+      };
+
+      return [
+        ...oldMonitorList.slice(0, oldMonitorIdx),
+        newMonitor,
+        ...oldMonitorList.slice(oldMonitorIdx + 1),
+      ];
+    });
+  };
+
   return (
     <MonitorListPanel>
       <TimelineWidthTracker ref={elementRef} />
@@ -88,6 +127,7 @@ export function OverviewTimeline({monitorList}: Props) {
           bucketedData={monitorStats?.[monitor.slug]}
           end={nowRef.current}
           width={timelineWidth}
+          onDeleteEnvironment={env => handleDeleteEnvironment(monitor, env)}
         />
       ))}
     </MonitorListPanel>

+ 40 - 4
static/app/views/monitors/components/overviewTimeline/timelineTableRow.tsx

@@ -4,7 +4,9 @@ import {css} from '@emotion/react';
 import styled from '@emotion/styled';
 
 import {Button} from 'sentry/components/button';
-import {tct} from 'sentry/locale';
+import {DropdownMenu} from 'sentry/components/dropdownMenu';
+import {IconEllipsis} from 'sentry/icons';
+import {t, tct} from 'sentry/locale';
 import {fadeIn} from 'sentry/styles/animations';
 import {space} from 'sentry/styles/space';
 import useOrganization from 'sentry/utils/useOrganization';
@@ -19,6 +21,7 @@ import {MonitorBucket} from './types';
 interface Props extends Omit<CheckInTimelineProps, 'bucketedData' | 'environment'> {
   monitor: Monitor;
   bucketedData?: MonitorBucket[];
+  onDeleteEnvironment?: (env: string) => void;
   /**
    * Whether only one monitor is being rendered in a larger view with this component
    * turns off things like zebra striping, hover effect, and showing monitor name
@@ -32,6 +35,7 @@ export function TimelineTableRow({
   monitor,
   bucketedData,
   singleMonitorView,
+  onDeleteEnvironment,
   ...timelineProps
 }: Props) {
   const [isExpanded, setExpanded] = useState(
@@ -51,6 +55,28 @@ export function TimelineTableRow({
             monitor.status === MonitorStatus.DISABLED ? MonitorStatus.DISABLED : status;
           return (
             <EnvWithStatus key={name}>
+              {onDeleteEnvironment && (
+                <DropdownMenu
+                  size="sm"
+                  trigger={triggerProps => (
+                    <EnvActionButton
+                      {...triggerProps}
+                      aria-label={t('Monitor environment actions')}
+                      size="zero"
+                      icon={<IconEllipsis size="sm" />}
+                    />
+                  )}
+                  items={[
+                    {
+                      label: t('Delete Environment'),
+                      key: 'delete',
+                      onAction: () => {
+                        onDeleteEnvironment(name);
+                      },
+                    },
+                  ]}
+                />
+              )}
               <MonitorEnvLabel status={envStatus}>{name}</MonitorEnvLabel>
               {statusIconColorMap[envStatus].icon}
             </EnvWithStatus>
@@ -140,17 +166,25 @@ const Schedule = styled('small')`
 
 const MonitorEnvContainer = styled('div')`
   display: flex;
-  padding: ${space(3)} ${space(2)};
+  padding: 0 ${space(2)};
   flex-direction: column;
-  gap: ${space(4)};
   border-right: 1px solid ${p => p.theme.innerBorder};
   text-align: right;
 `;
 
+const EnvActionButton = styled(Button)`
+  padding: ${space(0.5)} ${space(1)};
+  display: none;
+`;
+
 const EnvWithStatus = styled('div')`
   display: flex;
-  gap: ${space(1)};
+  gap: ${space(0.5)};
   align-items: center;
+
+  &:hover ${EnvActionButton} {
+    display: block;
+  }
 `;
 
 const MonitorEnvLabel = styled('div')<{status: MonitorStatus}>`
@@ -158,6 +192,8 @@ const MonitorEnvLabel = styled('div')<{status: MonitorStatus}>`
   overflow: hidden;
   white-space: nowrap;
   flex: 1;
+
+  padding: ${space(2)} 0;
   color: ${p => p.theme[statusIconColorMap[p.status].color]};
 `;