Browse Source

feat(issues): Display if current release is using semver (#53037)

Scott Cooper 1 year ago
parent
commit
8fee41e1ec

+ 31 - 0
static/app/components/actions/resolve.spec.tsx

@@ -153,4 +153,35 @@ describe('ResolveActions', function () {
       },
     });
   });
+
+  it('displays the current release version', async function () {
+    render(
+      <ResolveActions
+        onUpdate={spy}
+        hasRelease
+        projectSlug="proj-1"
+        latestRelease={{version: 'frontend@1.2.3'}}
+      />
+    );
+
+    await userEvent.click(screen.getByLabelText('More resolve options'));
+    expect(screen.getByText('The current release (1.2.3)')).toBeInTheDocument();
+  });
+
+  it('displays if the current release version uses semver', async function () {
+    const organization = TestStubs.Organization({features: ['issue-resolve-semver']});
+    render(
+      <ResolveActions
+        onUpdate={spy}
+        hasRelease
+        projectSlug="proj-1"
+        latestRelease={{version: 'frontend@1.2.3'}}
+      />,
+      {organization}
+    );
+
+    await userEvent.click(screen.getByLabelText('More resolve options'));
+    expect(screen.getByText('The current release')).toBeInTheDocument();
+    expect(screen.getByText('1.2.3 (semver)')).toBeInTheDocument();
+  });
 });

+ 17 - 8
static/app/components/actions/resolve.tsx

@@ -13,15 +13,15 @@ import {IconChevron} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {
   GroupStatusResolution,
-  Release,
+  Project,
   ResolutionStatus,
   ResolutionStatusDetails,
 } from 'sentry/types';
 import {trackAnalytics} from 'sentry/utils/analytics';
-import {formatVersion} from 'sentry/utils/formatters';
+import {formatVersion, isSemverRelease} from 'sentry/utils/formatters';
 import useOrganization from 'sentry/utils/useOrganization';
 
-interface ResolveActionsProps {
+export interface ResolveActionsProps {
   hasRelease: boolean;
   onUpdate: (data: GroupStatusResolution) => void;
   confirmLabel?: string;
@@ -30,7 +30,7 @@ interface ResolveActionsProps {
   disabled?: boolean;
   isAutoResolved?: boolean;
   isResolved?: boolean;
-  latestRelease?: Release;
+  latestRelease?: Project['latestRelease'];
   priority?: 'primary';
   projectFetchError?: boolean;
   projectSlug?: string;
@@ -150,6 +150,8 @@ function ResolveActions({
       });
     };
 
+    const isSemver = latestRelease ? isSemverRelease(latestRelease.version) : false;
+    const hasIssueResolveSemver = organization.features.includes('issue-resolve-semver');
     const items: MenuItemProps[] = [
       {
         key: 'next-release',
@@ -159,10 +161,17 @@ function ResolveActions({
       },
       {
         key: 'current-release',
-        label: latestRelease
-          ? t('The current release (%s)', formatVersion(latestRelease.version))
-          : t('The current release'),
-        details: actionTitle,
+        label:
+          hasIssueResolveSemver || !latestRelease
+            ? t('The current release')
+            : t('The current release (%s)', formatVersion(latestRelease.version)),
+        details: actionTitle
+          ? actionTitle
+          : hasIssueResolveSemver && latestRelease
+          ? `${formatVersion(latestRelease.version)} (${
+              isSemver ? t('semver') : t('timestamp')
+            })`
+          : null,
         onAction: () => onActionOrConfirm(handleCurrentReleaseResolution),
       },
       {

+ 2 - 2
static/app/types/project.tsx

@@ -4,7 +4,7 @@ import type {Scope, TimeseriesValue} from './core';
 import type {SDKUpdatesSuggestion} from './event';
 import type {Plugin} from './integrations';
 import type {Organization, Team} from './organization';
-import type {Deploy, Release} from './release';
+import type {Deploy} from './release';
 import type {DynamicSamplingBias, DynamicSamplingRule} from './sampling';
 
 // Minimal project representation for use with avatars.
@@ -52,7 +52,7 @@ export type Project = {
   dynamicSamplingRules?: DynamicSamplingRule[] | null;
   hasUserReports?: boolean;
   latestDeploys?: Record<string, Pick<Deploy, 'dateFinished' | 'version'>> | null;
-  latestRelease?: Release;
+  latestRelease?: {version: string} | null;
   options?: Record<string, boolean | string>;
   sessionStats?: {
     currentCrashFreeRate: number | null;

+ 5 - 0
static/app/utils/formatters.tsx

@@ -19,6 +19,11 @@ export function userDisplayName(user: User | CommitAuthor, includeEmail = true):
   return displayName;
 }
 
+export const isSemverRelease = (rawVersion: string): boolean => {
+  const parsedVersion = new Release(rawVersion);
+  return !!parsedVersion.versionParsed;
+};
+
 export const formatVersion = (rawVersion: string, withPackage = false) => {
   try {
     const parsedVersion = new Release(rawVersion);

+ 3 - 3
static/app/views/issueList/actions/actionSet.tsx

@@ -229,13 +229,13 @@ function ActionSet({
                 onUpdate={onUpdate}
                 anySelected={anySelected}
                 params={{
-                  hasReleases: selectedProject.hasOwnProperty('features')
+                  hasRelease: selectedProject.hasOwnProperty('features')
                     ? (selectedProject as Project).features.includes('releases')
                     : false,
                   latestRelease: selectedProject.hasOwnProperty('latestRelease')
                     ? (selectedProject as Project).latestRelease
                     : undefined,
-                  projectId: selectedProject.slug,
+                  projectSlug: selectedProject.slug,
                   confirm,
                   label,
                   loadingProjects: !initiallyLoaded,
@@ -251,7 +251,7 @@ function ActionSet({
           onUpdate={onUpdate}
           anySelected={anySelected}
           params={{
-            hasReleases: false,
+            hasRelease: false,
             confirm,
             label,
           }}

+ 10 - 13
static/app/views/issueList/actions/resolveActions.tsx

@@ -1,5 +1,4 @@
-import ResolveActions from 'sentry/components/actions/resolve';
-import {Release} from 'sentry/types';
+import ResolveActions, {ResolveActionsProps} from 'sentry/components/actions/resolve';
 
 import {ConfirmAction, getConfirm, getLabel} from './utils';
 
@@ -7,15 +6,13 @@ type Props = {
   anySelected: boolean;
   onShouldConfirm: (action: ConfirmAction) => boolean;
   onUpdate: (data?: any) => void;
-  params: {
+  params: Pick<
+    ResolveActionsProps,
+    'disabled' | 'hasRelease' | 'latestRelease' | 'projectSlug' | 'projectFetchError'
+  > & {
     confirm: ReturnType<typeof getConfirm>;
-    hasReleases: boolean;
     label: ReturnType<typeof getLabel>;
-    disabled?: boolean;
-    latestRelease?: Release;
     loadingProjects?: boolean;
-    projectFetchError?: boolean;
-    projectId?: string;
   };
 };
 
@@ -26,9 +23,9 @@ function ResolveActionsContainer({
   onUpdate,
 }: Props) {
   const {
-    hasReleases,
+    hasRelease,
     latestRelease,
-    projectId,
+    projectSlug,
     confirm,
     label,
     loadingProjects,
@@ -39,14 +36,14 @@ function ResolveActionsContainer({
   // projectId is null when 0 or >1 projects are selected.
   const resolveDisabled = Boolean(!anySelected || projectFetchError);
   const resolveDropdownDisabled = Boolean(
-    !anySelected || !projectId || loadingProjects || projectFetchError
+    !anySelected || !projectSlug || loadingProjects || projectFetchError
   );
 
   return (
     <ResolveActions
-      hasRelease={hasReleases}
+      hasRelease={hasRelease}
       latestRelease={latestRelease}
-      projectSlug={projectId}
+      projectSlug={projectSlug}
       onUpdate={onUpdate}
       shouldConfirm={onShouldConfirm(ConfirmAction.RESOLVE)}
       confirmMessage={confirm({action: ConfirmAction.RESOLVE, canBeUndone: true})}