Browse Source

feat(dashboards): uniform metrics widgets (#64657)

Ogi 1 year ago
parent
commit
3424664a3a

+ 10 - 0
static/app/utils/metrics/useMetricsMeta.tsx

@@ -8,6 +8,16 @@ import type {MetricMeta, MRI, UseCase} from '../../types/metrics';
 
 const DEFAULT_USE_CASES = ['sessions', 'transactions', 'custom', 'spans'];
 
+export function getMetricsMetaQueryKeys(
+  orgSlug: string,
+  projects: PageFilters['projects'],
+  useCases?: UseCase[]
+): ApiQueryKey[] {
+  return (
+    useCases?.map(useCase => getMetricsMetaQueryKey(orgSlug, projects, useCase)) ?? []
+  );
+}
+
 export function getMetricsMetaQueryKey(
   orgSlug: string,
   projects: PageFilters['projects'],

+ 5 - 1
static/app/views/dashboards/dashboard.tsx

@@ -116,7 +116,6 @@ class Dashboard extends Component<Props, State> {
       },
       windowWidth: window.innerWidth,
     };
-    connectDashboardCharts(DASHBOARD_CHART_GROUP);
   }
 
   static getDerivedStateFromProps(props, state) {
@@ -160,6 +159,8 @@ class Dashboard extends Component<Props, State> {
 
     // Get member list data for issue widgets
     this.fetchMemberList();
+
+    connectDashboardCharts(DASHBOARD_CHART_GROUP);
   }
 
   componentDidUpdate(prevProps: Props) {
@@ -540,6 +541,9 @@ class Dashboard extends Component<Props, State> {
       if (widgetType === WidgetType.RELEASE) {
         return organization.features.includes('dashboards-rh-widget');
       }
+      if (widgetType === WidgetType.METRICS) {
+        return hasDDMFeature(organization);
+      }
       return true;
     });
 

+ 8 - 0
static/app/views/dashboards/detail.spec.tsx

@@ -93,6 +93,10 @@ describe('Dashboards > Detail', function () {
         url: '/organizations/org-slug/releases/',
         body: [],
       });
+      MockApiClient.addMockResponse({
+        url: '/organizations/org-slug/metrics/meta/',
+        body: [],
+      });
     });
 
     afterEach(function () {
@@ -356,6 +360,10 @@ describe('Dashboards > Detail', function () {
         url: '/organizations/org-slug/prompts-activity/',
         body: {},
       });
+      MockApiClient.addMockResponse({
+        url: '/organizations/org-slug/metrics/meta/',
+        body: [],
+      });
     });
 
     afterEach(function () {

+ 213 - 206
static/app/views/dashboards/detail.tsx

@@ -50,6 +50,7 @@ import {
   resetPageFilters,
 } from 'sentry/views/dashboards/utils';
 import {DataSet} from 'sentry/views/dashboards/widgetBuilder/utils';
+import {MetricsDashboardContextProvider} from 'sentry/views/dashboards/widgetCard/metricsContext';
 import {MetricsDataSwitcherAlert} from 'sentry/views/performance/landing/metricsDataSwitcherAlert';
 
 import {defaultMetricWidget} from '../../utils/metrics/dashboard';
@@ -746,74 +747,76 @@ class DashboardDetail extends Component<Props, State> {
       >
         <Layout.Page withPadding>
           <OnDemandControlProvider location={location}>
-            <MetricsResultsMetaProvider>
-              <NoProjectMessage organization={organization}>
-                <StyledPageHeader>
-                  <Layout.Title>
-                    <DashboardTitle
-                      dashboard={modifiedDashboard ?? dashboard}
-                      onUpdate={this.setModifiedDashboard}
-                      isEditingDashboard={this.isEditingDashboard}
+            <MetricsDashboardContextProvider>
+              <MetricsResultsMetaProvider>
+                <NoProjectMessage organization={organization}>
+                  <StyledPageHeader>
+                    <Layout.Title>
+                      <DashboardTitle
+                        dashboard={modifiedDashboard ?? dashboard}
+                        onUpdate={this.setModifiedDashboard}
+                        isEditingDashboard={this.isEditingDashboard}
+                      />
+                    </Layout.Title>
+                    <Controls
+                      organization={organization}
+                      dashboards={dashboards}
+                      onEdit={this.onEdit}
+                      onCancel={this.onCancel}
+                      onCommit={this.onCommit}
+                      onAddWidget={this.onAddWidget}
+                      onDelete={this.onDelete(dashboard)}
+                      dashboardState={dashboardState}
+                      widgetLimitReached={widgetLimitReached}
                     />
-                  </Layout.Title>
-                  <Controls
-                    organization={organization}
-                    dashboards={dashboards}
-                    onEdit={this.onEdit}
-                    onCancel={this.onCancel}
-                    onCommit={this.onCommit}
-                    onAddWidget={this.onAddWidget}
-                    onDelete={this.onDelete(dashboard)}
-                    dashboardState={dashboardState}
-                    widgetLimitReached={widgetLimitReached}
+                  </StyledPageHeader>
+                  <HookHeader organization={organization} />
+                  <FiltersBar
+                    filters={{}} // Default Dashboards don't have filters set
+                    location={location}
+                    hasUnsavedChanges={false}
+                    isEditingDashboard={false}
+                    isPreview={false}
+                    onDashboardFilterChange={this.handleChangeFilter}
                   />
-                </StyledPageHeader>
-                <HookHeader organization={organization} />
-                <FiltersBar
-                  filters={{}} // Default Dashboards don't have filters set
-                  location={location}
-                  hasUnsavedChanges={false}
-                  isEditingDashboard={false}
-                  isPreview={false}
-                  onDashboardFilterChange={this.handleChangeFilter}
-                />
-                <MetricsCardinalityProvider
-                  organization={organization}
-                  location={location}
-                >
-                  <MetricsDataSwitcher
+                  <MetricsCardinalityProvider
                     organization={organization}
-                    eventView={EventView.fromLocation(location)}
                     location={location}
                   >
-                    {metricsDataSide => (
-                      <MEPSettingProvider
-                        location={location}
-                        forceTransactions={metricsDataSide.forceTransactionsOnly}
-                      >
-                        <Dashboard
-                          paramDashboardId={dashboardId}
-                          dashboard={modifiedDashboard ?? dashboard}
-                          organization={organization}
-                          isEditingDashboard={this.isEditingDashboard}
-                          widgetLimitReached={widgetLimitReached}
-                          onUpdate={this.onUpdateWidget}
-                          handleUpdateWidgetList={this.handleUpdateWidgetList}
-                          handleAddCustomWidget={this.handleAddCustomWidget}
-                          handleAddMetricWidget={this.handleAddMetricWidget}
-                          editingWidgetIndex={this.state.editingWidgetIndex}
-                          onStartEditMetricWidget={this.handleStartEditMetricWidget}
-                          onEndEditMetricWidget={this.handleEndEditMetricWidget}
-                          isPreview={this.isPreview}
-                          router={router}
+                    <MetricsDataSwitcher
+                      organization={organization}
+                      eventView={EventView.fromLocation(location)}
+                      location={location}
+                    >
+                      {metricsDataSide => (
+                        <MEPSettingProvider
                           location={location}
-                        />
-                      </MEPSettingProvider>
-                    )}
-                  </MetricsDataSwitcher>
-                </MetricsCardinalityProvider>
-              </NoProjectMessage>
-            </MetricsResultsMetaProvider>
+                          forceTransactions={metricsDataSide.forceTransactionsOnly}
+                        >
+                          <Dashboard
+                            paramDashboardId={dashboardId}
+                            dashboard={modifiedDashboard ?? dashboard}
+                            organization={organization}
+                            isEditingDashboard={this.isEditingDashboard}
+                            widgetLimitReached={widgetLimitReached}
+                            onUpdate={this.onUpdateWidget}
+                            handleUpdateWidgetList={this.handleUpdateWidgetList}
+                            handleAddCustomWidget={this.handleAddCustomWidget}
+                            handleAddMetricWidget={this.handleAddMetricWidget}
+                            editingWidgetIndex={this.state.editingWidgetIndex}
+                            onStartEditMetricWidget={this.handleStartEditMetricWidget}
+                            onEndEditMetricWidget={this.handleEndEditMetricWidget}
+                            isPreview={this.isPreview}
+                            router={router}
+                            location={location}
+                          />
+                        </MEPSettingProvider>
+                      )}
+                    </MetricsDataSwitcher>
+                  </MetricsCardinalityProvider>
+                </NoProjectMessage>
+              </MetricsResultsMetaProvider>
+            </MetricsDashboardContextProvider>
           </OnDemandControlProvider>
         </Layout.Page>
       </PageFiltersContainer>
@@ -876,158 +879,162 @@ class DashboardDetail extends Component<Props, State> {
         >
           <Layout.Page>
             <OnDemandControlProvider location={location}>
-              <MetricsResultsMetaProvider>
-                <NoProjectMessage organization={organization}>
-                  <Layout.Header>
-                    <Layout.HeaderContent>
-                      <Breadcrumbs
-                        crumbs={[
-                          {
-                            label: t('Dashboards'),
-                            to: `/organizations/${organization.slug}/dashboards/`,
-                          },
-                          {
-                            label: this.getBreadcrumbLabel(),
-                          },
-                        ]}
-                      />
-                      <Layout.Title>
-                        <DashboardTitle
-                          dashboard={modifiedDashboard ?? dashboard}
-                          onUpdate={this.setModifiedDashboard}
-                          isEditingDashboard={this.isEditingDashboard}
+              <MetricsDashboardContextProvider>
+                <MetricsResultsMetaProvider>
+                  <NoProjectMessage organization={organization}>
+                    <Layout.Header>
+                      <Layout.HeaderContent>
+                        <Breadcrumbs
+                          crumbs={[
+                            {
+                              label: t('Dashboards'),
+                              to: `/organizations/${organization.slug}/dashboards/`,
+                            },
+                            {
+                              label: this.getBreadcrumbLabel(),
+                            },
+                          ]}
                         />
-                      </Layout.Title>
-                    </Layout.HeaderContent>
-                    <Layout.HeaderActions>
-                      <Controls
-                        organization={organization}
-                        dashboards={dashboards}
-                        hasUnsavedFilters={hasUnsavedFilters}
-                        onEdit={this.onEdit}
-                        onCancel={this.onCancel}
-                        onCommit={this.onCommit}
-                        onAddWidget={this.onAddWidget}
-                        onDelete={this.onDelete(dashboard)}
-                        dashboardState={dashboardState}
-                        widgetLimitReached={widgetLimitReached}
-                      />
-                    </Layout.HeaderActions>
-                  </Layout.Header>
-                  <Layout.Body>
-                    <Layout.Main fullWidth>
-                      <MetricsCardinalityProvider
-                        organization={organization}
-                        location={location}
-                      >
-                        <MetricsDataSwitcher
+                        <Layout.Title>
+                          <DashboardTitle
+                            dashboard={modifiedDashboard ?? dashboard}
+                            onUpdate={this.setModifiedDashboard}
+                            isEditingDashboard={this.isEditingDashboard}
+                          />
+                        </Layout.Title>
+                      </Layout.HeaderContent>
+                      <Layout.HeaderActions>
+                        <Controls
+                          organization={organization}
+                          dashboards={dashboards}
+                          hasUnsavedFilters={hasUnsavedFilters}
+                          onEdit={this.onEdit}
+                          onCancel={this.onCancel}
+                          onCommit={this.onCommit}
+                          onAddWidget={this.onAddWidget}
+                          onDelete={this.onDelete(dashboard)}
+                          dashboardState={dashboardState}
+                          widgetLimitReached={widgetLimitReached}
+                        />
+                      </Layout.HeaderActions>
+                    </Layout.Header>
+                    <Layout.Body>
+                      <Layout.Main fullWidth>
+                        <MetricsCardinalityProvider
                           organization={organization}
-                          eventView={eventView}
                           location={location}
                         >
-                          {metricsDataSide => (
-                            <MEPSettingProvider
-                              location={location}
-                              forceTransactions={metricsDataSide.forceTransactionsOnly}
-                            >
-                              {isDashboardUsingTransaction ? (
-                                <MetricsDataSwitcherAlert
-                                  organization={organization}
-                                  eventView={eventView}
-                                  projects={projects}
-                                  location={location}
-                                  router={router}
-                                  source={DiscoverQueryPageSource.DISCOVER}
-                                  {...metricsDataSide}
-                                />
-                              ) : null}
-                              <FiltersBar
-                                filters={(modifiedDashboard ?? dashboard).filters}
+                          <MetricsDataSwitcher
+                            organization={organization}
+                            eventView={eventView}
+                            location={location}
+                          >
+                            {metricsDataSide => (
+                              <MEPSettingProvider
                                 location={location}
-                                hasUnsavedChanges={hasUnsavedFilters}
-                                isEditingDashboard={
-                                  dashboardState !== DashboardState.CREATE &&
-                                  this.isEditingDashboard
-                                }
-                                isPreview={this.isPreview}
-                                onDashboardFilterChange={this.handleChangeFilter}
-                                onCancel={() => {
-                                  resetPageFilters(dashboard, location);
-                                  this.setState({
-                                    modifiedDashboard: {
-                                      ...(modifiedDashboard ?? dashboard),
-                                      filters: dashboard.filters,
-                                    },
-                                  });
-                                }}
-                                onSave={() => {
-                                  const newModifiedDashboard = {
-                                    ...cloneDashboard(modifiedDashboard ?? dashboard),
-                                    ...getCurrentPageFilters(location),
-                                    filters:
-                                      getDashboardFiltersFromURL(location) ??
-                                      (modifiedDashboard ?? dashboard).filters,
-                                  };
-                                  updateDashboard(
-                                    api,
-                                    organization.slug,
-                                    newModifiedDashboard
-                                  ).then(
-                                    (newDashboard: DashboardDetails) => {
-                                      if (onDashboardUpdate) {
-                                        onDashboardUpdate(newDashboard);
-                                        this.setState({
-                                          modifiedDashboard: null,
-                                        });
-                                      }
-                                      addSuccessMessage(t('Dashboard filters updated'));
-                                      browserHistory.replace(
-                                        normalizeUrl({
-                                          pathname: `/organizations/${organization.slug}/dashboard/${newDashboard.id}/`,
-                                          query: omit(
-                                            location.query,
-                                            Object.values(DashboardFilterKeys)
-                                          ),
-                                        })
-                                      );
-                                    },
-                                    // `updateDashboard` does its own error handling
-                                    () => undefined
-                                  );
-                                }}
-                              />
-
-                              <WidgetViewerContext.Provider value={{seriesData, setData}}>
-                                <Dashboard
-                                  paramDashboardId={dashboardId}
-                                  dashboard={modifiedDashboard ?? dashboard}
-                                  organization={organization}
-                                  isEditingDashboard={this.isEditingDashboard}
-                                  widgetLimitReached={widgetLimitReached}
-                                  onUpdate={this.onUpdateWidget}
-                                  handleUpdateWidgetList={this.handleUpdateWidgetList}
-                                  handleAddCustomWidget={this.handleAddCustomWidget}
-                                  handleAddMetricWidget={this.handleAddMetricWidget}
-                                  onStartEditMetricWidget={
-                                    this.handleStartEditMetricWidget
-                                  }
-                                  onEndEditMetricWidget={this.handleEndEditMetricWidget}
-                                  editingWidgetIndex={this.state.editingWidgetIndex}
-                                  router={router}
+                                forceTransactions={metricsDataSide.forceTransactionsOnly}
+                              >
+                                {isDashboardUsingTransaction ? (
+                                  <MetricsDataSwitcherAlert
+                                    organization={organization}
+                                    eventView={eventView}
+                                    projects={projects}
+                                    location={location}
+                                    router={router}
+                                    source={DiscoverQueryPageSource.DISCOVER}
+                                    {...metricsDataSide}
+                                  />
+                                ) : null}
+                                <FiltersBar
+                                  filters={(modifiedDashboard ?? dashboard).filters}
                                   location={location}
-                                  newWidget={newWidget}
-                                  onSetNewWidget={onSetNewWidget}
+                                  hasUnsavedChanges={hasUnsavedFilters}
+                                  isEditingDashboard={
+                                    dashboardState !== DashboardState.CREATE &&
+                                    this.isEditingDashboard
+                                  }
                                   isPreview={this.isPreview}
+                                  onDashboardFilterChange={this.handleChangeFilter}
+                                  onCancel={() => {
+                                    resetPageFilters(dashboard, location);
+                                    this.setState({
+                                      modifiedDashboard: {
+                                        ...(modifiedDashboard ?? dashboard),
+                                        filters: dashboard.filters,
+                                      },
+                                    });
+                                  }}
+                                  onSave={() => {
+                                    const newModifiedDashboard = {
+                                      ...cloneDashboard(modifiedDashboard ?? dashboard),
+                                      ...getCurrentPageFilters(location),
+                                      filters:
+                                        getDashboardFiltersFromURL(location) ??
+                                        (modifiedDashboard ?? dashboard).filters,
+                                    };
+                                    updateDashboard(
+                                      api,
+                                      organization.slug,
+                                      newModifiedDashboard
+                                    ).then(
+                                      (newDashboard: DashboardDetails) => {
+                                        if (onDashboardUpdate) {
+                                          onDashboardUpdate(newDashboard);
+                                          this.setState({
+                                            modifiedDashboard: null,
+                                          });
+                                        }
+                                        addSuccessMessage(t('Dashboard filters updated'));
+                                        browserHistory.replace(
+                                          normalizeUrl({
+                                            pathname: `/organizations/${organization.slug}/dashboard/${newDashboard.id}/`,
+                                            query: omit(
+                                              location.query,
+                                              Object.values(DashboardFilterKeys)
+                                            ),
+                                          })
+                                        );
+                                      },
+                                      // `updateDashboard` does its own error handling
+                                      () => undefined
+                                    );
+                                  }}
                                 />
-                              </WidgetViewerContext.Provider>
-                            </MEPSettingProvider>
-                          )}
-                        </MetricsDataSwitcher>
-                      </MetricsCardinalityProvider>
-                    </Layout.Main>
-                  </Layout.Body>
-                </NoProjectMessage>
-              </MetricsResultsMetaProvider>
+
+                                <WidgetViewerContext.Provider
+                                  value={{seriesData, setData}}
+                                >
+                                  <Dashboard
+                                    paramDashboardId={dashboardId}
+                                    dashboard={modifiedDashboard ?? dashboard}
+                                    organization={organization}
+                                    isEditingDashboard={this.isEditingDashboard}
+                                    widgetLimitReached={widgetLimitReached}
+                                    onUpdate={this.onUpdateWidget}
+                                    handleUpdateWidgetList={this.handleUpdateWidgetList}
+                                    handleAddCustomWidget={this.handleAddCustomWidget}
+                                    handleAddMetricWidget={this.handleAddMetricWidget}
+                                    onStartEditMetricWidget={
+                                      this.handleStartEditMetricWidget
+                                    }
+                                    onEndEditMetricWidget={this.handleEndEditMetricWidget}
+                                    editingWidgetIndex={this.state.editingWidgetIndex}
+                                    router={router}
+                                    location={location}
+                                    newWidget={newWidget}
+                                    onSetNewWidget={onSetNewWidget}
+                                    isPreview={this.isPreview}
+                                  />
+                                </WidgetViewerContext.Provider>
+                              </MEPSettingProvider>
+                            )}
+                          </MetricsDataSwitcher>
+                        </MetricsCardinalityProvider>
+                      </Layout.Main>
+                    </Layout.Body>
+                  </NoProjectMessage>
+                </MetricsResultsMetaProvider>
+              </MetricsDashboardContextProvider>
             </OnDemandControlProvider>
           </Layout.Page>
         </PageFiltersContainer>

+ 1 - 1
static/app/views/dashboards/layoutUtils.tsx

@@ -12,7 +12,7 @@ import type {Widget, WidgetLayout} from './types';
 import {DisplayType} from './types';
 
 export const DEFAULT_WIDGET_WIDTH = 2;
-export const METRIC_WIDGET_MIN_SIZE = {minH: 2, h: 2, w: 3};
+export const METRIC_WIDGET_MIN_SIZE = {minH: 2, h: 2, w: 2};
 
 const WIDGET_PREFIX = 'grid-item';
 

+ 1 - 0
static/app/views/dashboards/widgetCard/index.tsx

@@ -286,6 +286,7 @@ class WidgetCard extends Component<Props, State> {
             selection={selection}
             widget={widget}
             dashboardFilters={dashboardFilters}
+            renderErrorMessage={renderErrorMessage}
           />
         );
       }

+ 102 - 26
static/app/views/dashboards/widgetCard/metricWidgetCard/index.tsx

@@ -1,23 +1,34 @@
-import {useCallback, useMemo, useState} from 'react';
+import {Fragment, useCallback, useMemo, useRef, useState} from 'react';
 import type {InjectedRouter} from 'react-router';
 import styled from '@emotion/styled';
 import type {Location} from 'history';
 
 import ErrorPanel from 'sentry/components/charts/errorPanel';
 import {HeaderTitle} from 'sentry/components/charts/styles';
+import TransitionChart from 'sentry/components/charts/transitionChart';
+import EmptyMessage from 'sentry/components/emptyMessage';
 import TextOverflow from 'sentry/components/textOverflow';
-import {IconWarning} from 'sentry/icons';
+import {IconSearch, IconWarning} from 'sentry/icons';
+import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {MRI, Organization, PageFilters} from 'sentry/types';
+import type {ReactEchartsRef} from 'sentry/types/echarts';
 import {stringifyMetricWidget} from 'sentry/utils/metrics';
-import type {MetricWidgetQueryParams} from 'sentry/utils/metrics/types';
+import {
+  MetricDisplayType,
+  type MetricWidgetQueryParams,
+} from 'sentry/utils/metrics/types';
+import {useMetricsDataZoom} from 'sentry/utils/metrics/useMetricsData';
 import {WidgetCardPanel, WidgetTitleRow} from 'sentry/views/dashboards/widgetCard';
 import type {AugmentedEChartDataZoomHandler} from 'sentry/views/dashboards/widgetCard/chart';
 import {DashboardsMEPContext} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext';
 import {InlineEditor} from 'sentry/views/dashboards/widgetCard/metricWidgetCard/inlineEditor';
 import {Toolbar} from 'sentry/views/dashboards/widgetCard/toolbar';
 import WidgetCardContextMenu from 'sentry/views/dashboards/widgetCard/widgetCardContextMenu';
-import {MetricWidgetBody} from 'sentry/views/ddm/widget';
+import {MetricChart} from 'sentry/views/ddm/chart';
+import {createChartPalette} from 'sentry/views/ddm/metricsChartPalette';
+import {getChartTimeseries} from 'sentry/views/ddm/widget';
+import {LoadingScreen} from 'sentry/views/starfish/components/chart';
 
 import {
   convertToDashboardWidget,
@@ -26,6 +37,7 @@ import {
 import {parseField} from '../../../../utils/metrics/mri';
 import {DASHBOARD_CHART_GROUP} from '../../dashboard';
 import type {DashboardFilters, Widget} from '../../types';
+import {useMetricsDashboardContext} from '../metricsContext';
 
 type Props = {
   isEditingDashboard: boolean;
@@ -43,6 +55,7 @@ type Props = {
   onEdit?: (index: string) => void;
   onUpdate?: (widget: Widget | null) => void;
   onZoom?: AugmentedEChartDataZoomHandler;
+  renderErrorMessage?: (errorMessage?: string) => React.ReactNode;
   showSlider?: boolean;
   tableItemLimit?: number;
   windowWidth?: number;
@@ -62,7 +75,10 @@ export function MetricWidgetCard({
   router,
   index,
   dashboardFilters,
+  renderErrorMessage,
 }: Props) {
+  useMetricsDashboardContext();
+
   const [metricWidgetQueryParams, setMetricWidgetQueryParams] =
     useState<MetricWidgetQueryParams>(convertFromWidget(widget));
 
@@ -164,14 +180,14 @@ export function MetricWidgetCard({
             )}
           </ContextMenuWrapper>
         </WidgetHeaderWrapper>
-        <MetricWidgetChartWrapper>
-          <MetricWidgetChartContainer
-            selection={selection}
-            widget={widget}
-            editorParams={metricWidgetQueryParams}
-            dashboardFilters={dashboardFilters}
-          />
-        </MetricWidgetChartWrapper>
+
+        <MetricWidgetChartContainer
+          selection={selection}
+          widget={widget}
+          editorParams={metricWidgetQueryParams}
+          dashboardFilters={dashboardFilters}
+          renderErrorMessage={renderErrorMessage}
+        />
         {isEditingDashboard && <Toolbar onDelete={onDelete} onDuplicate={onDuplicate} />}
       </WidgetCardPanel>
     </DashboardsMEPContext.Provider>
@@ -183,6 +199,7 @@ type MetricWidgetChartContainerProps = {
   widget: Widget;
   dashboardFilters?: DashboardFilters;
   editorParams?: Partial<MetricWidgetQueryParams>;
+  renderErrorMessage?: (errorMessage?: string) => React.ReactNode;
 };
 
 export function MetricWidgetChartContainer({
@@ -190,25 +207,83 @@ export function MetricWidgetChartContainer({
   widget,
   editorParams = {},
   dashboardFilters,
+  renderErrorMessage,
 }: MetricWidgetChartContainerProps) {
   const metricWidgetQueryParams = {
     ...convertFromWidget(widget),
     ...editorParams,
   };
 
+  const {projects, environments, datetime} = selection;
+  const {mri, op, groupBy, displayType} = metricWidgetQueryParams;
+
+  const {
+    data: timeseriesData,
+    isLoading,
+    isError,
+    error,
+  } = useMetricsDataZoom(
+    {
+      mri,
+      op,
+      query: extendQuery(metricWidgetQueryParams.query, dashboardFilters),
+      groupBy,
+      projects,
+      environments,
+      datetime,
+    },
+    {fidelity: displayType === MetricDisplayType.BAR ? 'low' : 'high'}
+  );
+
+  const chartRef = useRef<ReactEchartsRef>(null);
+
+  const chartSeries = useMemo(() => {
+    return timeseriesData
+      ? getChartTimeseries(timeseriesData, {
+          getChartPalette: createChartPalette,
+          mri,
+          groupBy,
+        })
+      : [];
+  }, [timeseriesData, mri, groupBy]);
+
+  if (isError) {
+    const errorMessage =
+      error?.responseJSON?.detail?.toString() || t('Error while fetching metrics data');
+    return (
+      <Fragment>
+        {renderErrorMessage?.(errorMessage)}
+        <ErrorPanel>
+          <IconWarning color="gray500" size="lg" />
+        </ErrorPanel>
+      </Fragment>
+    );
+  }
+
+  if (timeseriesData?.groups.length === 0) {
+    return (
+      <EmptyMessage
+        icon={<IconSearch size="xxl" />}
+        title={t('No results')}
+        description={t('No results found for the given query')}
+      />
+    );
+  }
+
   return (
-    <MetricWidgetBody
-      widgetIndex={0}
-      datetime={selection.datetime}
-      projects={selection.projects}
-      environments={selection.environments}
-      mri={metricWidgetQueryParams.mri}
-      op={metricWidgetQueryParams.op}
-      query={extendQuery(metricWidgetQueryParams.query, dashboardFilters)}
-      groupBy={metricWidgetQueryParams.groupBy}
-      displayType={toMetricDisplayType(metricWidgetQueryParams.displayType)}
-      chartGroup={DASHBOARD_CHART_GROUP}
-    />
+    <MetricWidgetChartWrapper>
+      <TransitionChart loading={isLoading} reloading={isLoading}>
+        <LoadingScreen loading={isLoading} />
+        <MetricChart
+          ref={chartRef}
+          series={chartSeries}
+          displayType={displayType}
+          operation={op}
+          widgetIndex={0}
+          group={DASHBOARD_CHART_GROUP}
+        />
+      </TransitionChart>
+    </MetricWidgetChartWrapper>
   );
 }
 
@@ -278,5 +353,6 @@ const WidgetTitle = styled(HeaderTitle)`
 const MetricWidgetChartWrapper = styled('div')`
   height: 100%;
   width: 100%;
-  padding: ${space(2)};
-`;
+  padding: ${space(3)};
+  padding-top: ${space(2)};
+  `;

+ 2 - 2
static/app/views/dashboards/widgetCard/metricWidgetCard/inlineEditor.tsx

@@ -33,11 +33,11 @@ import type {
   MetricWidgetQueryParams,
 } from 'sentry/utils/metrics/types';
 import {MetricDisplayType} from 'sentry/utils/metrics/types';
-import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
 import {useMetricsTags} from 'sentry/utils/metrics/useMetricsTags';
 import {MetricSearchBar} from 'sentry/views/ddm/metricSearchBar';
 
 import {formatMRI} from '../../../../utils/metrics/mri';
+import {useMetricsDashboardContext} from '../metricsContext';
 
 type InlineEditorProps = {
   displayType: MetricDisplayType;
@@ -72,7 +72,7 @@ export const InlineEditor = memo(function InlineEditor({
   size = 'sm',
 }: InlineEditorProps) {
   const [editingName, setEditingName] = useState(false);
-  const {data: meta, isLoading: isMetaLoading} = useMetricsMeta(projects);
+  const {metricsMeta: meta, isLoading: isMetaLoading} = useMetricsDashboardContext();
 
   const {data: tags = []} = useMetricsTags(metricsQuery.mri, projects);
 

+ 36 - 0
static/app/views/dashboards/widgetCard/metricsContext.tsx

@@ -0,0 +1,36 @@
+import {createContext, useContext, useMemo} from 'react';
+
+import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
+import usePageFilters from 'sentry/utils/usePageFilters';
+
+interface MetricsDashboardContextValue {
+  isLoading: boolean;
+  metricsMeta: ReturnType<typeof useMetricsMeta>['data'];
+}
+
+export const MetricsDashboardContext = createContext<MetricsDashboardContextValue>({
+  metricsMeta: [],
+  isLoading: false,
+});
+
+export function useMetricsDashboardContext() {
+  return useContext(MetricsDashboardContext);
+}
+
+export function MetricsDashboardContextProvider({children}: {children: React.ReactNode}) {
+  const pageFilters = usePageFilters().selection;
+  const metricsMetaQuery = useMetricsMeta(pageFilters.projects);
+
+  const contextValue = useMemo(() => {
+    return {
+      metricsMeta: metricsMetaQuery.data,
+      isLoading: metricsMetaQuery.isLoading,
+    };
+  }, [metricsMetaQuery]);
+
+  return (
+    <MetricsDashboardContext.Provider value={contextValue}>
+      {children}
+    </MetricsDashboardContext.Provider>
+  );
+}

+ 1 - 1
static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx

@@ -258,7 +258,7 @@ const StyledTransparentLoadingMask = styled(props => (
   align-items: center;
 `;
 
-function LoadingScreen({loading}: {loading: boolean}) {
+export function LoadingScreen({loading}: {loading: boolean}) {
   if (!loading) {
     return null;
   }

Some files were not shown because too many files changed in this diff