Browse Source

feat(ddm): New main actions (#62999)

- closes https://github.com/getsentry/sentry/issues/62737
ArthurKnaus 1 year ago
parent
commit
8ff7deaafd

+ 50 - 13
static/app/views/ddm/context.tsx

@@ -7,6 +7,7 @@ import {
   useState,
 } from 'react';
 import * as Sentry from '@sentry/react';
+import isEqual from 'lodash/isEqual';
 
 import {MRI} from 'sentry/types';
 import {
@@ -19,6 +20,7 @@ import {
 } from 'sentry/utils/metrics';
 import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
 import {decodeList} from 'sentry/utils/queryString';
+import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
 import usePageFilters from 'sentry/utils/usePageFilters';
 import useRouter from 'sentry/utils/useRouter';
 import {FocusArea} from 'sentry/views/ddm/chartBrush';
@@ -31,30 +33,34 @@ interface DDMContextValue {
   addWidgets: (widgets: Partial<MetricWidgetQueryParams>[]) => void;
   duplicateWidget: (index: number) => void;
   focusArea: FocusArea | null;
+  isDefaultQuery: boolean;
   isLoading: boolean;
   metricsMeta: ReturnType<typeof useMetricsMeta>['data'];
   removeFocusArea: () => void;
   removeWidget: (index: number) => void;
   selectedWidgetIndex: number;
+  setDefaultQuery: (query: Record<string, any> | null) => void;
   setSelectedWidgetIndex: (index: number) => void;
   updateWidget: (index: number, data: Partial<MetricWidgetQueryParams>) => void;
   widgets: MetricWidgetQueryParams[];
 }
 
 export const DDMContext = createContext<DDMContextValue>({
-  selectedWidgetIndex: 0,
-  setSelectedWidgetIndex: () => {},
+  addFocusArea: () => {},
   addWidget: () => {},
   addWidgets: () => {},
-  updateWidget: () => {},
-  removeWidget: () => {},
-  addFocusArea: () => {},
-  removeFocusArea: () => {},
   duplicateWidget: () => {},
-  widgets: [],
-  metricsMeta: [],
-  isLoading: false,
   focusArea: null,
+  isDefaultQuery: false,
+  isLoading: false,
+  metricsMeta: [],
+  removeFocusArea: () => {},
+  removeWidget: () => {},
+  selectedWidgetIndex: 0,
+  setDefaultQuery: () => {},
+  setSelectedWidgetIndex: () => {},
+  updateWidget: () => {},
+  widgets: [],
 });
 
 export function useDDMContext() {
@@ -172,10 +178,37 @@ export function useMetricWidgets() {
   };
 }
 
+const useDefaultQuery = () => {
+  const router = useRouter();
+  const [defaultQuery, setDefaultQuery] = useLocalStorageState<Record<
+    string,
+    any
+  > | null>('ddm:default-query', null);
+
+  useEffect(() => {
+    if (defaultQuery) {
+      router.replace({...router.location, query: defaultQuery});
+    }
+    // Only call on page load
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  return useMemo(
+    () => ({
+      defaultQuery,
+      setDefaultQuery,
+      isDefaultQuery: !!defaultQuery && isEqual(defaultQuery, router.location.query),
+    }),
+    [defaultQuery, router.location.query, setDefaultQuery]
+  );
+};
+
 export function DDMContextProvider({children}: {children: React.ReactNode}) {
   const router = useRouter();
   const updateQuery = useUpdateQuery();
 
+  const {setDefaultQuery, isDefaultQuery} = useDefaultQuery();
+
   const [selectedWidgetIndex, setSelectedWidgetIndex] = useState(0);
   const {widgets, updateWidget, addWidget, addWidgets, removeWidget, duplicateWidget} =
     useMetricWidgets();
@@ -262,20 +295,24 @@ export function DDMContextProvider({children}: {children: React.ReactNode}) {
       focusArea,
       addFocusArea: handleAddFocusArea,
       removeFocusArea: handleRemoveFocusArea,
+      setDefaultQuery,
+      isDefaultQuery,
     }),
     [
-      addWidgets,
       handleAddWidget,
-      handleDuplicate,
+      addWidgets,
+      selectedWidgetIndex,
+      widgets,
       handleUpdateWidget,
       removeWidget,
+      handleDuplicate,
       isLoading,
       metricsMeta,
-      selectedWidgetIndex,
-      widgets,
       focusArea,
       handleAddFocusArea,
       handleRemoveFocusArea,
+      setDefaultQuery,
+      isDefaultQuery,
     ]
   );
 

+ 20 - 19
static/app/views/ddm/contextMenu.tsx

@@ -47,7 +47,10 @@ export function MetricQueryContextMenu({
   const organization = useOrganization();
   const router = useRouter();
   const {removeWidget, duplicateWidget, widgets} = useDDMContext();
-  const createAlert = useCreateAlert(organization, metricsQuery);
+  const createAlert = useMemo(
+    () => getCreateAlert(organization, metricsQuery),
+    [metricsQuery, organization]
+  );
   const createDashboardWidget = useCreateDashboardWidget(
     organization,
     metricsQuery,
@@ -143,24 +146,22 @@ export function MetricQueryContextMenu({
   );
 }
 
-export function useCreateAlert(organization: Organization, metricsQuery: MetricsQuery) {
-  return useMemo(() => {
-    if (
-      !metricsQuery.mri ||
-      !metricsQuery.op ||
-      isCustomMeasurement(metricsQuery) ||
-      !organization.access.includes('alerts:write')
-    ) {
-      return undefined;
-    }
-    return function () {
-      return openModal(deps => (
-        <OrganizationContext.Provider value={organization}>
-          <CreateAlertModal metricsQuery={metricsQuery} {...deps} />
-        </OrganizationContext.Provider>
-      ));
-    };
-  }, [metricsQuery, organization]);
+export function getCreateAlert(organization: Organization, metricsQuery: MetricsQuery) {
+  if (
+    !metricsQuery.mri ||
+    !metricsQuery.op ||
+    isCustomMeasurement(metricsQuery) ||
+    !organization.access.includes('alerts:write')
+  ) {
+    return undefined;
+  }
+  return function () {
+    return openModal(deps => (
+      <OrganizationContext.Provider value={organization}>
+        <CreateAlertModal metricsQuery={metricsQuery} {...deps} />
+      </OrganizationContext.Provider>
+    ));
+  };
 }
 
 export function useCreateDashboardWidget(

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

@@ -6,9 +6,19 @@ import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {t} from 'sentry/locale';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import useOrganization from 'sentry/utils/useOrganization';
-import {DDMContextProvider} from 'sentry/views/ddm/context';
+import {DDMContextProvider, useDDMContext} from 'sentry/views/ddm/context';
 import {DDMLayout} from 'sentry/views/ddm/layout';
 
+function WrappedPageFiltersContainer({children}: {children: React.ReactNode}) {
+  const {isDefaultQuery} = useDDMContext();
+  return (
+    <PageFiltersContainer disablePersistence={isDefaultQuery}>
+      {' '}
+      {children}
+    </PageFiltersContainer>
+  );
+}
+
 function DDM() {
   const organization = useOrganization();
 
@@ -22,11 +32,11 @@ function DDM() {
 
   return (
     <SentryDocumentTitle title={t('Metrics')} orgSlug={organization.slug}>
-      <PageFiltersContainer>
-        <DDMContextProvider>
+      <DDMContextProvider>
+        <WrappedPageFiltersContainer>
           <DDMLayout />
-        </DDMContextProvider>
-      </PageFiltersContainer>
+        </WrappedPageFiltersContainer>
+      </DDMContextProvider>
     </SentryDocumentTitle>
   );
 }

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

@@ -5,10 +5,8 @@ import * as Sentry from '@sentry/react';
 import emptyStateImg from 'sentry-images/spot/custom-metrics-empty-state.svg';
 
 import {Button} from 'sentry/components/button';
-import ButtonBar from 'sentry/components/buttonBar';
 import FeatureBadge from 'sentry/components/featureBadge';
 import FloatingFeedbackWidget from 'sentry/components/feedback/widget/floatingFeedbackWidget';
-import {GithubFeedbackButton} from 'sentry/components/githubFeedbackButton';
 import * as Layout from 'sentry/components/layouts/thirds';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import OnboardingPanel from 'sentry/components/onboardingPanel';
@@ -17,27 +15,21 @@ import {EnvironmentPageFilter} from 'sentry/components/organizations/environment
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
 import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
-import {IconDownload} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {METRICS_DOCS_URL} from 'sentry/utils/metrics';
-import {hasDDMExperimentalFeature} from 'sentry/utils/metrics/features';
-import useOrganization from 'sentry/utils/useOrganization';
 import {useDDMContext} from 'sentry/views/ddm/context';
-import {useDashboardImport} from 'sentry/views/ddm/dashboardImportModal';
 import {useMetricsOnboardingSidebar} from 'sentry/views/ddm/ddmOnboarding/useMetricsOnboardingSidebar';
+import {PageHeaderActions} from 'sentry/views/ddm/pageHeaderActions';
 import {Queries} from 'sentry/views/ddm/queries';
 import {MetricScratchpad} from 'sentry/views/ddm/scratchpad';
-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 hasMetrics = !isLoading && metricsMeta.length > 0;
   const {activateSidebar} = useMetricsOnboardingSidebar();
 
-  const importDashboard = useDashboardImport();
   const addCustomMetric = useCallback(
     (referrer: string) => {
       Sentry.metrics.increment('ddm.add_custom_metric', 1, {
@@ -64,32 +56,10 @@ export const DDMLayout = memo(() => {
           </Layout.Title>
         </Layout.HeaderContent>
         <Layout.HeaderActions>
-          <ButtonBar gap={1}>
-            {hasMetrics && (
-              <Button
-                priority="primary"
-                onClick={() => addCustomMetric('header')}
-                size="sm"
-              >
-                {t('Add Custom Metric')}
-              </Button>
-            )}
-            <ShareButton />
-            <GithubFeedbackButton
-              href="https://github.com/getsentry/sentry/discussions/58584"
-              label={t('Discussion')}
-              title={null}
-            />
-            {hasDDMExperimentalFeature(organization) && (
-              <Button
-                size="sm"
-                icon={<IconDownload size="xs" />}
-                onClick={importDashboard}
-              >
-                {t('Import Dashboard')}
-              </Button>
-            )}
-          </ButtonBar>
+          <PageHeaderActions
+            showCustomMetricButton={hasMetrics}
+            addCustomMetric={addCustomMetric}
+          />
         </Layout.HeaderActions>
       </Layout.Header>
       <Layout.Body>

+ 113 - 0
static/app/views/ddm/pageHeaderActions.tsx

@@ -0,0 +1,113 @@
+import {useMemo} from 'react';
+
+import {Button} from 'sentry/components/button';
+import ButtonBar from 'sentry/components/buttonBar';
+import {DropdownMenu} from 'sentry/components/dropdownMenu';
+import {IconBookmark, IconDashboard, IconEllipsis, IconSiren} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {isCustomMeasurement} from 'sentry/utils/metrics';
+import {MRIToField} from 'sentry/utils/metrics/mri';
+import {middleEllipsis} from 'sentry/utils/middleEllipsis';
+import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import useRouter from 'sentry/utils/useRouter';
+import {useDDMContext} from 'sentry/views/ddm/context';
+import {getCreateAlert} from 'sentry/views/ddm/contextMenu';
+import {QuerySymbol} from 'sentry/views/ddm/querySymbol';
+import {useCreateDashboard} from 'sentry/views/ddm/useCreateDashboard';
+
+interface Props {
+  addCustomMetric: (referrer: string) => void;
+  showCustomMetricButton: boolean;
+}
+
+export function PageHeaderActions({showCustomMetricButton, addCustomMetric}: Props) {
+  const router = useRouter();
+  const organization = useOrganization();
+  const {selection} = usePageFilters();
+  const createDashboard = useCreateDashboard();
+  const {isDefaultQuery, setDefaultQuery, widgets} = useDDMContext();
+
+  const items = useMemo(
+    () => [
+      {
+        leadingItems: [<IconDashboard key="icon" />],
+        key: 'add-dashboard',
+        label: t('Add to Dashboard'),
+        onAction: createDashboard,
+      },
+    ],
+    [createDashboard]
+  );
+
+  const alertItems = useMemo(
+    () =>
+      widgets.map((widget, index) => {
+        const createAlert = getCreateAlert(organization, {
+          datetime: selection.datetime,
+          projects: selection.projects,
+          environments: selection.environments,
+          query: widget.query,
+          mri: widget.mri,
+          groupBy: widget.groupBy,
+          op: widget.op,
+        });
+        return {
+          leadingItems: [<QuerySymbol key="icon" index={index} />],
+          key: `add-alert-${index}`,
+          label: widget.mri
+            ? middleEllipsis(MRIToField(widget.mri, widget.op!), 60, /\.|-|_/)
+            : t('Select a metric to create an alert'),
+          tooltip: isCustomMeasurement({mri: widget.mri})
+            ? t('Custom measurements cannot be used to create alerts')
+            : undefined,
+          disabled: !createAlert,
+          onAction: createAlert,
+        };
+      }),
+    [
+      organization,
+      selection.datetime,
+      selection.environments,
+      selection.projects,
+      widgets,
+    ]
+  );
+
+  return (
+    <ButtonBar gap={1}>
+      {showCustomMetricButton && (
+        <Button priority="primary" onClick={() => addCustomMetric('header')} size="sm">
+          {t('Add Custom Metric')}
+        </Button>
+      )}
+      <Button
+        size="sm"
+        icon={<IconBookmark isSolid={isDefaultQuery} />}
+        onClick={() => setDefaultQuery(isDefaultQuery ? null : router.location.query)}
+      >
+        {isDefaultQuery ? t('Remove Default') : t('Save as default')}
+      </Button>
+      <DropdownMenu
+        items={alertItems}
+        triggerLabel={t('Create Alert')}
+        triggerProps={{
+          size: 'sm',
+          showChevron: false,
+          icon: <IconSiren direction="down" size="xs" />,
+        }}
+        position="bottom-end"
+      />
+      <DropdownMenu
+        items={items}
+        triggerProps={{
+          'aria-label': t('Page actions'),
+          size: 'sm',
+          showChevron: false,
+          icon: <IconEllipsis direction="down" size="xs" />,
+        }}
+        position="bottom-end"
+      />
+    </ButtonBar>
+  );
+}

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

@@ -14,9 +14,9 @@ export function useCreateDashboard() {
   const {selection} = usePageFilters();
 
   return useMemo(() => {
-    return function (scratchpad?: {name: string}) {
+    return function () {
       const newDashboard = {
-        title: scratchpad?.name || 'Metrics Dashboard',
+        title: 'Metrics Dashboard',
         description: '',
         widgets: widgets
           .filter(widget => !!widget.mri)