Browse Source

feat(dashboards): Allow project column in Issue Widgets (#31608)

Allows selection of project column for Issue Widgets
Refactors first and last seen to use default date renderers instead
edwardgou-sentry 3 years ago
parent
commit
aa468434ed

+ 5 - 1
static/app/components/charts/simpleTableChart.tsx

@@ -8,6 +8,7 @@ import Truncate from 'sentry/components/truncate';
 import space from 'sentry/styles/space';
 import {Organization} from 'sentry/types';
 import {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
+import {MetaType} from 'sentry/utils/discover/eventView';
 import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
 import {fieldAlignment} from 'sentry/utils/discover/fields';
 import withOrganization from 'sentry/utils/withOrganization';
@@ -23,7 +24,10 @@ type Props = {
   title: string;
   className?: string;
   fieldHeaderMap?: Record<string, string>;
-  getCustomFieldRenderer?: typeof getFieldRenderer;
+  getCustomFieldRenderer?: (
+    field: string,
+    meta: MetaType
+  ) => ReturnType<typeof getFieldRenderer> | null;
   stickyHeaders?: boolean;
 };
 

+ 6 - 28
static/app/utils/dashboards/issueFieldRenderers.tsx

@@ -2,11 +2,9 @@ import * as React from 'react';
 import {css} from '@emotion/react';
 import styled from '@emotion/styled';
 import {Location} from 'history';
-import partial from 'lodash/partial';
 
 import AssigneeSelector from 'sentry/components/assigneeSelector';
 import Count from 'sentry/components/count';
-import DateTime from 'sentry/components/dateTime';
 import Link from 'sentry/components/links/link';
 import {getRelativeSummary} from 'sentry/components/organizations/timeRangeSelector/utils';
 import Tooltip from 'sentry/components/tooltip';
@@ -15,9 +13,8 @@ import {t} from 'sentry/locale';
 import MemberListStore from 'sentry/stores/memberListStore';
 import space from 'sentry/styles/space';
 import {Organization} from 'sentry/types';
-import EventView, {EventData, MetaType} from 'sentry/utils/discover/eventView';
+import EventView, {EventData} from 'sentry/utils/discover/eventView';
 
-import {FIELD_FORMATTERS} from '../discover/fieldRenderers';
 import {Container, FieldShortId, OverflowLink} from '../discover/styles';
 
 /**
@@ -48,9 +45,7 @@ type SpecialFields = {
   assignee: SpecialField;
   count: SpecialField;
   events: SpecialField;
-  firstSeen: SpecialField;
   issue: SpecialField;
-  lastSeen: SpecialField;
   lifetimeCount: SpecialField;
   lifetimeEvents: SpecialField;
   lifetimeUserCount: SpecialField;
@@ -121,14 +116,6 @@ const SPECIAL_FIELDS: SpecialFields = {
     renderFunc: (data, {organization}) =>
       issuesCountRenderer(data, organization, 'users'),
   },
-  firstSeen: {
-    sortField: null,
-    renderFunc: ({firstSeen}) => <StyledDateTime date={firstSeen} />,
-  },
-  lastSeen: {
-    sortField: null,
-    renderFunc: ({lastSeen}) => <StyledDateTime date={lastSeen} />,
-  },
   lifetimeCount: {
     sortField: null,
     renderFunc: (data, {organization}) =>
@@ -279,10 +266,6 @@ const Divider = styled('div')`
   background-color: ${p => p.theme.innerBorder};
 `;
 
-const StyledDateTime = styled(DateTime)`
-  white-space: nowrap;
-`;
-
 const ActorContainer = styled('div')`
   display: flex;
   justify-content: left;
@@ -300,18 +283,13 @@ const ActorContainer = styled('div')`
  * @returns {Function}
  */
 export function getIssueFieldRenderer(
-  field: string,
-  meta: MetaType
-): FieldFormatterRenderFunctionPartial {
+  field: string
+): FieldFormatterRenderFunctionPartial | null {
   if (SPECIAL_FIELDS.hasOwnProperty(field)) {
     return SPECIAL_FIELDS[field].renderFunc;
   }
 
-  const fieldType = meta[field];
-
-  // Defaults to fieldRenderer formatters if the field is not a special issue field
-  if (FIELD_FORMATTERS.hasOwnProperty(fieldType)) {
-    return partial(FIELD_FORMATTERS[fieldType].renderFunc, field);
-  }
-  return partial(FIELD_FORMATTERS.string.renderFunc, field);
+  // Return null if there is no field renderer for this field
+  // Should check the discover field renderer for this field
+  return null;
 }

+ 4 - 2
static/app/views/dashboardsV2/widget/issueWidget/fields.tsx

@@ -23,6 +23,7 @@ export enum FieldKey {
   USERS = 'users',
   LIFETIME_EVENTS = 'lifetimeEvents',
   LIFETIME_USERS = 'lifetimeUsers',
+  PROJECT = 'project',
 }
 
 export const ISSUE_FIELDS: Readonly<Record<FieldKey, ColumnType>> = {
@@ -35,12 +36,13 @@ export const ISSUE_FIELDS: Readonly<Record<FieldKey, ColumnType>> = {
   [FieldKey.IS_BOOKMARKED]: 'boolean',
   [FieldKey.IS_SUBSCRIBED]: 'boolean',
   [FieldKey.IS_HANDLED]: 'boolean',
-  [FieldKey.LAST_SEEN]: 'string',
-  [FieldKey.FIRST_SEEN]: 'string',
+  [FieldKey.LAST_SEEN]: 'date',
+  [FieldKey.FIRST_SEEN]: 'date',
   [FieldKey.EVENTS]: 'string',
   [FieldKey.USERS]: 'string',
   [FieldKey.LIFETIME_EVENTS]: 'string',
   [FieldKey.LIFETIME_USERS]: 'string',
+  [FieldKey.PROJECT]: 'string',
 };
 
 export const ISSUE_FIELD_TO_HEADER_MAP = {

+ 63 - 52
static/app/views/dashboardsV2/widgetCard/issueWidgetQueries.tsx

@@ -123,67 +123,78 @@ class IssueWidgetQueries extends React.Component<Props, State> {
     const {tableResults} = this.state;
     GroupStore.add(tableResults);
     const transformedTableResults: TableDataRow[] = [];
-    tableResults.forEach(group => {
-      const {id, shortId, title, lifetime, filtered, count, userCount, ...resultProps} =
-        group;
-      const transformedResultProps: Omit<TableDataRow, 'id'> = {};
-      Object.keys(resultProps)
-        .filter(key => ['number', 'string'].includes(typeof resultProps[key]))
-        .forEach(key => {
-          transformedResultProps[key] = resultProps[key];
-        });
-
-      const transformedTableResult: TableDataRow = {
-        ...transformedResultProps,
-        events: count,
-        users: userCount,
+    tableResults.forEach(
+      ({
         id,
-        'issue.id': id,
-        issue: shortId,
+        shortId,
         title,
-      };
+        lifetime,
+        filtered,
+        count,
+        userCount,
+        project,
+        ...resultProps
+      }) => {
+        const transformedResultProps: Omit<TableDataRow, 'id'> = {};
+        Object.keys(resultProps)
+          .filter(key => ['number', 'string'].includes(typeof resultProps[key]))
+          .forEach(key => {
+            transformedResultProps[key] = resultProps[key];
+          });
 
-      // Get lifetime stats
-      if (lifetime) {
-        transformedTableResult.lifetimeEvents = lifetime?.count;
-        transformedTableResult.lifetimeUsers = lifetime?.userCount;
-      }
-      // Get filtered stats
-      if (filtered) {
-        transformedTableResult.filteredEvents = filtered?.count;
-        transformedTableResult.filteredUsers = filtered?.userCount;
-      }
+        const transformedTableResult: TableDataRow = {
+          ...transformedResultProps,
+          events: count,
+          users: userCount,
+          id,
+          'issue.id': id,
+          issue: shortId,
+          title,
+          project: project.slug,
+        };
 
-      // Discover Url properties
-      const query = widget.queries[0].conditions;
-      const queryTerms: string[] = [];
-      if (typeof query === 'string') {
-        const queryObj = queryToObj(query);
-        for (const queryTag in queryObj) {
-          if (!DISCOVER_EXCLUSION_FIELDS.includes(queryTag)) {
-            const queryVal = queryObj[queryTag].includes(' ')
-              ? `"${queryObj[queryTag]}"`
-              : queryObj[queryTag];
-            queryTerms.push(`${queryTag}:${queryVal}`);
-          }
+        // Get lifetime stats
+        if (lifetime) {
+          transformedTableResult.lifetimeEvents = lifetime?.count;
+          transformedTableResult.lifetimeUsers = lifetime?.userCount;
+        }
+        // Get filtered stats
+        if (filtered) {
+          transformedTableResult.filteredEvents = filtered?.count;
+          transformedTableResult.filteredUsers = filtered?.userCount;
         }
 
-        if (queryObj.__text) {
-          queryTerms.push(queryObj.__text);
+        // Discover Url properties
+        const query = widget.queries[0].conditions;
+        const queryTerms: string[] = [];
+        if (typeof query === 'string') {
+          const queryObj = queryToObj(query);
+          for (const queryTag in queryObj) {
+            if (!DISCOVER_EXCLUSION_FIELDS.includes(queryTag)) {
+              const queryVal = queryObj[queryTag].includes(' ')
+                ? `"${queryObj[queryTag]}"`
+                : queryObj[queryTag];
+              queryTerms.push(`${queryTag}:${queryVal}`);
+            }
+          }
+
+          if (queryObj.__text) {
+            queryTerms.push(queryObj.__text);
+          }
         }
-      }
-      transformedTableResult.discoverSearchQuery =
-        (queryTerms.length ? ' ' : '') + queryTerms.join(' ');
-      transformedTableResult.projectId = group.project.id;
+        transformedTableResult.discoverSearchQuery =
+          (queryTerms.length ? ' ' : '') + queryTerms.join(' ');
+        transformedTableResult.projectId = project.id;
 
-      const {period, start, end} = selection.datetime || {};
-      if (start && end) {
-        transformedTableResult.start = getUtcDateString(start);
-        transformedTableResult.end = getUtcDateString(end);
+        const {period, start, end} = selection.datetime || {};
+        if (start && end) {
+          transformedTableResult.start = getUtcDateString(start);
+          transformedTableResult.end = getUtcDateString(end);
+        }
+        transformedTableResult.period = period ?? '';
+        transformedTableResults.push(transformedTableResult);
       }
-      transformedTableResult.period = period ?? '';
-      transformedTableResults.push(transformedTableResult);
-    });
+    );
     return transformedTableResults;
   }
 

+ 1 - 0
tests/js/spec/views/dashboardsV2/widget/issueWidget/utils.spec.tsx

@@ -16,6 +16,7 @@ describe('generateIssueWidgetFieldOptions', function () {
       'field:lifetimeEvents',
       'field:lifetimeUsers',
       'field:platform',
+      'field:project',
       'field:status',
       'field:title',
       'field:users',