Browse Source

feat(ddm): Remove scratchpads (#62825)

Set `transaction.duration` as default MRI.
Remove scratchpads.

- closes https://github.com/getsentry/sentry/issues/62734
- closes https://github.com/getsentry/sentry/issues/62738
ArthurKnaus 1 year ago
parent
commit
5ff9728041

+ 2 - 2
static/app/views/ddm/context.tsx

@@ -61,8 +61,8 @@ export function useDDMContext() {
 }
 
 const emptyWidget: MetricWidgetQueryParams = {
-  mri: '' as MRI,
-  op: undefined,
+  mri: 'd:transactions/duration@millisecond' satisfies MRI,
+  op: 'count',
   query: '',
   groupBy: [],
   sort: DEFAULT_SORT_STATE,

+ 5 - 17
static/app/views/ddm/ddm.tsx

@@ -8,16 +8,6 @@ import {trackAnalytics} from 'sentry/utils/analytics';
 import useOrganization from 'sentry/utils/useOrganization';
 import {DDMContextProvider} from 'sentry/views/ddm/context';
 import {DDMLayout} from 'sentry/views/ddm/layout';
-import {ScratchpadsProvider, useScratchpads} from 'sentry/views/ddm/scratchpadContext';
-
-function WrappedPageFiltersContainer({children}: {children: React.ReactNode}) {
-  const {selected} = useScratchpads();
-  return (
-    <PageFiltersContainer disablePersistence={!!selected}>
-      {children}
-    </PageFiltersContainer>
-  );
-}
 
 function DDM() {
   const organization = useOrganization();
@@ -32,13 +22,11 @@ function DDM() {
 
   return (
     <SentryDocumentTitle title={t('Metrics')} orgSlug={organization.slug}>
-      <ScratchpadsProvider>
-        <WrappedPageFiltersContainer>
-          <DDMContextProvider>
-            <DDMLayout />
-          </DDMContextProvider>
-        </WrappedPageFiltersContainer>
-      </ScratchpadsProvider>
+      <PageFiltersContainer>
+        <DDMContextProvider>
+          <DDMLayout />
+        </DDMContextProvider>
+      </PageFiltersContainer>
     </SentryDocumentTitle>
   );
 }

+ 1 - 5
static/app/views/ddm/layout.tsx

@@ -27,15 +27,12 @@ import {useDDMContext} from 'sentry/views/ddm/context';
 import {useDashboardImport} from 'sentry/views/ddm/dashboardImportModal';
 import {useMetricsOnboardingSidebar} from 'sentry/views/ddm/ddmOnboarding/useMetricsOnboardingSidebar';
 import {MetricScratchpad} from 'sentry/views/ddm/scratchpad';
-import {useScratchpads} from 'sentry/views/ddm/scratchpadContext';
-import {ScratchpadSelector} from 'sentry/views/ddm/scratchpadSelector';
 import ShareButton from 'sentry/views/ddm/shareButton';
 import {WidgetDetails} from 'sentry/views/ddm/widgetDetails';
 
 export const DDMLayout = memo(() => {
   const organization = useOrganization();
   const {metricsMeta, isLoading} = useDDMContext();
-  const {isLoading: isLoadingScratchpads} = useScratchpads();
   const hasMetrics = !isLoading && metricsMeta.length > 0;
   const {activateSidebar} = useMetricsOnboardingSidebar();
 
@@ -103,9 +100,8 @@ export const DDMLayout = memo(() => {
               <EnvironmentPageFilter />
               <DatePageFilter />
             </PageFilterBar>
-            <ScratchpadSelector />
           </PaddedContainer>
-          {isLoading || isLoadingScratchpads ? (
+          {isLoading ? (
             <LoadingIndicator />
           ) : hasMetrics ? (
             <Fragment>

+ 0 - 219
static/app/views/ddm/scratchpadContext.tsx

@@ -1,219 +0,0 @@
-import {
-  createContext,
-  useCallback,
-  useContext,
-  useEffect,
-  useMemo,
-  useState,
-} from 'react';
-import {uuid4} from '@sentry/utils';
-import isEmpty from 'lodash/isEmpty';
-import isEqual from 'lodash/isEqual';
-
-import {useClearQuery, useInstantRef, useUpdateQuery} from 'sentry/utils/metrics';
-import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
-import useOrganization from 'sentry/utils/useOrganization';
-import usePageFilters from 'sentry/utils/usePageFilters';
-import usePrevious from 'sentry/utils/usePrevious';
-import useRouter from 'sentry/utils/useRouter';
-
-type Scratchpad = {
-  id: string;
-  name: string;
-  query: Record<string, unknown>;
-};
-
-type ScratchpadState = {
-  default: string | null;
-  scratchpads: Record<string, Scratchpad>;
-};
-
-function makeLocalStorageKey(orgSlug: string) {
-  return `ddm-scratchpads:${orgSlug}`;
-}
-
-const EMPTY_QUERY = {};
-
-const mapProjectQueryParam = (project: any) => {
-  if (typeof project === 'string') {
-    return [Number(project)];
-  }
-  if (Array.isArray(project)) {
-    return project.map(Number);
-  }
-  return [];
-};
-
-function useScratchpadUrlSync() {
-  const {slug} = useOrganization();
-  const router = useRouter();
-  const updateQuery = useUpdateQuery();
-  const clearQuery = useClearQuery();
-  const {projects} = usePageFilters().selection;
-
-  const [state, setState] = useLocalStorageState<ScratchpadState>(
-    makeLocalStorageKey(slug),
-    {
-      default: null,
-      scratchpads: {},
-    }
-  );
-  const stateRef = useInstantRef(state);
-  const routerQuery = router.location.query ?? EMPTY_QUERY;
-  const routerQueryRef = useInstantRef(routerQuery);
-
-  const [selected, setSelected] = useState<string | null | undefined>(() => {
-    if (
-      (state.default && !routerQuery.widgets) ||
-      (state.default && isEqual(state.scratchpads[state.default].query, routerQuery))
-    ) {
-      return state.default;
-    }
-    return undefined;
-  });
-
-  const savedProjects = selected && state.scratchpads[selected].query.project;
-  // The scratchpad is "loading" while the project selection state is different from the saved state
-  const isLoading = !!selected && !isEqual(mapProjectQueryParam(savedProjects), projects);
-
-  const toggleSelected = useCallback(
-    (id: string | null) => {
-      if (id === selected) {
-        setSelected(null);
-      } else {
-        setSelected(id);
-      }
-    },
-    [setSelected, selected]
-  );
-
-  const setDefault = useCallback(
-    (id: string | null) => {
-      setState({...state, default: id});
-    },
-    [state, setState]
-  );
-
-  const add = useCallback(
-    (name: string) => {
-      const currentState = stateRef.current;
-      const id = uuid4();
-      const newScratchpads = {
-        ...currentState.scratchpads,
-        [id]: {
-          name,
-          id,
-          query: {environment: null, statsPeriod: null, ...routerQueryRef.current},
-        },
-      };
-      setState({...currentState, scratchpads: newScratchpads});
-      toggleSelected(id);
-    },
-    [stateRef, routerQueryRef, setState, toggleSelected]
-  );
-
-  const update = useCallback(
-    (id: string, query: Scratchpad['query']) => {
-      const currentState = stateRef.current;
-      const oldScratchpad = currentState.scratchpads[id];
-      const newQuery = {
-        ...query,
-      };
-      const newScratchpads = {
-        ...currentState.scratchpads,
-        [id]: {...oldScratchpad, query: newQuery},
-      };
-      setState({...currentState, scratchpads: newScratchpads});
-    },
-    [setState, stateRef]
-  );
-
-  const remove = useCallback(
-    (id: string) => {
-      const currentState = stateRef.current;
-      const newScratchpads = {...currentState.scratchpads};
-      delete newScratchpads[id];
-      if (currentState.default === id) {
-        setState({...currentState, default: null, scratchpads: newScratchpads});
-      } else {
-        setState({...currentState, scratchpads: newScratchpads});
-      }
-      if (selected === id) {
-        toggleSelected(null);
-      }
-    },
-    [stateRef, selected, setState, toggleSelected]
-  );
-
-  // Changes the query when a scratchpad is selected, clears it when none is selected
-  useEffect(() => {
-    const selectedQuery = selected && stateRef.current.scratchpads[selected].query;
-    if (selectedQuery && !isEqual(selectedQuery, routerQueryRef.current)) {
-      const queryCopy: Record<string, any> = {
-        project: undefined, // make sure that project will be removed if not present in the stored query
-        ...selectedQuery,
-      };
-      // If the selected scratchpad has a start and end date, remove the statsPeriod
-      if (selectedQuery.start && selectedQuery.end) {
-        delete queryCopy.statsPeriod;
-      }
-      updateQuery(queryCopy);
-    } else if (selectedQuery === null) {
-      clearQuery();
-    }
-  }, [clearQuery, updateQuery, selected, routerQueryRef, stateRef]);
-
-  const previousSelected = usePrevious(selected);
-  // Saves all URL changes to the selected scratchpad to local storage
-  useEffect(() => {
-    const selectedQuery = selected && stateRef.current.scratchpads[selected].query;
-    // normal update path
-    if (selected && !isEmpty(routerQuery) && !isLoading) {
-      update(selected, routerQuery);
-      // project selection changes should ignore loading state
-    } else if (
-      selectedQuery &&
-      isLoading &&
-      selected === previousSelected &&
-      routerQuery.project !== selectedQuery.project
-    ) {
-      update(selected, routerQuery);
-    }
-  }, [routerQuery, projects, selected, update, isLoading, stateRef, previousSelected]);
-
-  return useMemo(
-    () => ({
-      all: state.scratchpads,
-      default: state.default,
-      selected,
-      isLoading,
-      add,
-      update,
-      remove,
-      toggleSelected,
-      setDefault,
-    }),
-    [state, selected, isLoading, add, update, remove, toggleSelected, setDefault]
-  );
-}
-
-const Context = createContext<ReturnType<typeof useScratchpadUrlSync>>({
-  all: {},
-  default: null,
-  selected: null,
-  isLoading: false,
-  add: () => {},
-  update: () => {},
-  remove: () => {},
-  toggleSelected: () => {},
-  setDefault: () => {},
-});
-
-export const useScratchpads = () => {
-  return useContext(Context);
-};
-
-export function ScratchpadsProvider({children}: {children: React.ReactNode}) {
-  const contextValue = useScratchpadUrlSync();
-  return <Context.Provider value={contextValue}>{children}</Context.Provider>;
-}

+ 0 - 242
static/app/views/ddm/scratchpadSelector.tsx

@@ -1,242 +0,0 @@
-import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
-import {useTheme} from '@emotion/react';
-import styled from '@emotion/styled';
-import {FocusScope} from '@react-aria/focus';
-import * as Sentry from '@sentry/react';
-import {AnimatePresence} from 'framer-motion';
-
-import GuideAnchor from 'sentry/components/assistant/guideAnchor';
-import {Button} from 'sentry/components/button';
-import {CompactSelect} from 'sentry/components/compactSelect';
-import {openConfirmModal} from 'sentry/components/confirm';
-import InputControl from 'sentry/components/input';
-import {Overlay, PositionWrapper} from 'sentry/components/overlay';
-import {Tooltip} from 'sentry/components/tooltip';
-import {IconBookmark, IconDashboard, IconDelete, IconStar} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
-import {trackAnalytics} from 'sentry/utils/analytics';
-import useKeyPress from 'sentry/utils/useKeyPress';
-import useOrganization from 'sentry/utils/useOrganization';
-import useOverlay from 'sentry/utils/useOverlay';
-import {useScratchpads} from 'sentry/views/ddm/scratchpadContext';
-
-import {useCreateDashboard} from './useCreateDashboard';
-
-export function ScratchpadSelector() {
-  const scratchpads = useScratchpads();
-  const organization = useOrganization();
-  const createDashboard = useCreateDashboard();
-
-  const isDefault = useCallback(
-    scratchpad => scratchpads.default === scratchpad.id,
-    [scratchpads.default]
-  );
-
-  const scratchpadOptions = useMemo(
-    () =>
-      Object.values(scratchpads.all).map(scratchpad => ({
-        value: scratchpad.id,
-        label: scratchpad.name,
-        trailingItems: (
-          <Fragment>
-            <Tooltip
-              title={
-                isDefault(scratchpad)
-                  ? t('Remove as default scratchpad')
-                  : t('Set as default scratchpad')
-              }
-            >
-              <Button
-                size="zero"
-                borderless
-                onPointerDown={e => e.stopPropagation()}
-                onClick={() => {
-                  trackAnalytics('ddm.scratchpad.set-default', {
-                    organization,
-                  });
-                  Sentry.metrics.increment('ddm.scratchpad.set_default');
-
-                  if (isDefault(scratchpad)) {
-                    scratchpads.setDefault(null);
-                  } else {
-                    scratchpads.setDefault(scratchpad.id ?? null);
-                  }
-                }}
-              >
-                <StyledDropdownIcon>
-                  <IconBookmark isSolid={isDefault(scratchpad)} />
-                </StyledDropdownIcon>
-              </Button>
-            </Tooltip>
-            <Tooltip title={t('Remove scratchpad')}>
-              <Button
-                size="zero"
-                borderless
-                onPointerDown={e => e.stopPropagation()}
-                onClick={() => {
-                  openConfirmModal({
-                    onConfirm: () => {
-                      trackAnalytics('ddm.scratchpad.remove', {
-                        organization,
-                      });
-                      Sentry.metrics.increment('ddm.scratchpad.remove');
-
-                      scratchpads.remove(scratchpad.id);
-                    },
-                    message: t('Are you sure you want to delete this scratchpad?'),
-                    confirmText: t('Delete'),
-                  });
-                }}
-              >
-                <StyledDropdownIcon danger>
-                  <IconDelete size="sm" />
-                </StyledDropdownIcon>
-              </Button>
-            </Tooltip>
-          </Fragment>
-        ),
-      })),
-    [scratchpads, isDefault, organization]
-  );
-
-  const selectedScratchpad = scratchpads.selected
-    ? scratchpads.all[scratchpads.selected]
-    : undefined;
-
-  return (
-    <ScratchpadGroup>
-      <Button
-        icon={<IconDashboard />}
-        onClick={() => {
-          Sentry.metrics.increment('ddm.scratchpad.dashboard');
-          createDashboard(selectedScratchpad);
-        }}
-      >
-        {t('Add to Dashboard')}
-      </Button>
-      <SaveAsDropdown
-        onSave={name => {
-          scratchpads.add(name);
-        }}
-        mode={scratchpads.selected ? 'fork' : 'save'}
-      />
-      <CompactSelect
-        grid
-        options={scratchpadOptions}
-        value={scratchpads.selected ?? `None`}
-        closeOnSelect={false}
-        onChange={option => {
-          scratchpads.toggleSelected(option.value);
-        }}
-        triggerProps={{prefix: t('Scratchpad')}}
-        emptyMessage="No scratchpads yet."
-        disabled={false}
-      />
-    </ScratchpadGroup>
-  );
-}
-
-function SaveAsDropdown({
-  onSave,
-  mode,
-}: {
-  mode: 'save' | 'fork';
-  onSave: (name: string) => void;
-}) {
-  const {
-    isOpen,
-    triggerProps,
-    overlayProps,
-    arrowProps,
-    state: {setOpen},
-  } = useOverlay({});
-  const theme = useTheme();
-  const organization = useOrganization();
-  const [name, setName] = useState('');
-
-  const save = useCallback(() => {
-    trackAnalytics('ddm.scratchpad.save', {
-      organization,
-    });
-    Sentry.metrics.increment('ddm.scratchpad.save');
-
-    onSave(name);
-    setOpen(false);
-    setName('');
-  }, [name, onSave, setOpen, organization]);
-
-  const enterKeyPressed = useKeyPress('Enter');
-
-  useEffect(() => {
-    if (isOpen && enterKeyPressed && name) {
-      save();
-    }
-  }, [enterKeyPressed, isOpen, name, save]);
-
-  const isFork = mode === 'fork';
-
-  return (
-    <div>
-      <Button icon={isFork ? null : <IconStar isSolid={isFork} />} {...triggerProps}>
-        {isFork ? `${t('Duplicate as')}\u2026` : `${t('Save as')}\u2026`}
-      </Button>
-      <AnimatePresence>
-        {isOpen && (
-          <FocusScope contain restoreFocus autoFocus>
-            <PositionWrapper zIndex={theme.zIndex.dropdown} {...overlayProps}>
-              <StyledOverlay arrowProps={arrowProps} animated>
-                <SaveAsInput
-                  type="txt"
-                  name="scratchpad-name"
-                  placeholder={t('Scratchpad name')}
-                  value={name}
-                  size="sm"
-                  onChange={({target}) => setName(target.value)}
-                />
-                <GuideAnchor target="create_scratchpad" position="bottom">
-                  <SaveAsButton
-                    priority="primary"
-                    disabled={!name}
-                    onClick={() => {
-                      save();
-                    }}
-                  >
-                    {mode === 'fork' ? t('Duplicate') : t('Save')}
-                  </SaveAsButton>
-                </GuideAnchor>
-              </StyledOverlay>
-            </PositionWrapper>
-          </FocusScope>
-        )}
-      </AnimatePresence>
-    </div>
-  );
-}
-
-const ScratchpadGroup = styled('div')`
-  display: flex;
-  gap: ${space(1)};
-`;
-
-const StyledOverlay = styled(Overlay)`
-  padding: ${space(1)};
-`;
-
-const SaveAsButton = styled(Button)`
-  width: 100%;
-`;
-
-const SaveAsInput = styled(InputControl)`
-  margin-bottom: ${space(1)};
-`;
-
-const StyledDropdownIcon = styled('span')<{danger?: boolean}>`
-  padding: ${space(0.5)} ${space(0.5)} 0 ${space(0.5)};
-  opacity: 0.5;
-
-  :hover {
-    opacity: 0.9;
-    color: ${p => (p.danger ? p.theme.red300 : p.theme.gray300)};
-  }
-`;