Browse Source

feat(ecosystem): Move codeowner error component (#45113)

Move this error into its own file, add test
Scott Cooper 2 years ago
parent
commit
def0d89252

+ 22 - 0
fixtures/js-stubs/codeOwner.js

@@ -0,0 +1,22 @@
+import {GitHubIntegration} from './githubIntegration';
+
+export function CodeOwner(params = {}) {
+  return {
+    id: '1225',
+    raw: '',
+    dateCreated: '2022-11-18T15:05:47.450354Z',
+    dateUpdated: '2023-02-24T18:43:08.729490Z',
+    codeMappingId: '11',
+    provider: 'github',
+    codeMapping: GitHubIntegration(),
+    ownershipSyntax: '',
+    errors: {
+      missing_user_emails: [],
+      missing_external_users: [],
+      missing_external_teams: [],
+      teams_without_access: [],
+      users_without_access: [],
+    },
+    ...params,
+  };
+}

+ 1 - 0
fixtures/js-stubs/types.tsx

@@ -24,6 +24,7 @@ type TestStubFixtures = {
   Breadcrumb: OverridableStub;
   Broadcast: OverridableStub;
   BuiltInSymbolSources: OverridableStubList;
+  CodeOwner: OverridableStub;
   Commit: OverridableStub;
   CommitAuthor: OverridableStub;
   Config: OverridableStub;

+ 39 - 0
static/app/views/settings/project/projectOwnership/codeownerErrors.spec.tsx

@@ -0,0 +1,39 @@
+import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+
+import {CodeOwnerErrors} from './codeownerErrors';
+
+describe('CodeownerErrors', () => {
+  const project = TestStubs.Project();
+  const org = TestStubs.Organization();
+
+  it('should render errors', () => {
+    const codeowner = TestStubs.CodeOwner({
+      errors: {
+        missing_user_emails: ['santry@example.com'],
+        missing_external_users: [],
+        missing_external_teams: ['@getsentry/something'],
+        teams_without_access: ['#snuba'],
+        users_without_access: [],
+      },
+    });
+    render(
+      <CodeOwnerErrors
+        codeowners={[codeowner]}
+        projectSlug={project.slug}
+        orgSlug={org.slug}
+      />
+    );
+
+    userEvent.click(
+      screen.getByText(
+        'There were 3 ownership issues within Sentry on the latest sync with the CODEOWNERS file'
+      )
+    );
+    expect(
+      screen.getByText(
+        `The following teams do not have an association in the organization: ${org.slug}`
+      )
+    ).toBeInTheDocument();
+    expect(screen.getByText('@getsentry/something')).toBeInTheDocument();
+  });
+});

+ 210 - 0
static/app/views/settings/project/projectOwnership/codeownerErrors.tsx

@@ -0,0 +1,210 @@
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
+import {Alert} from 'sentry/components/alert';
+import ExternalLink from 'sentry/components/links/externalLink';
+import {space} from 'sentry/styles/space';
+import {CodeOwner, RepositoryProjectPathConfig} from 'sentry/types';
+
+type CodeOwnerErrorKeys = keyof CodeOwner['errors'];
+
+function ErrorMessage({
+  message,
+  values,
+  link,
+  linkValue,
+}: {
+  link: string;
+  linkValue: React.ReactNode;
+  message: string;
+  values: string[];
+}) {
+  return (
+    <Fragment>
+      <ErrorMessageContainer>
+        <span>{message}</span>
+        <b>{values.join(', ')}</b>
+      </ErrorMessageContainer>
+      <ErrorCtaContainer>
+        <ExternalLink href={link}>{linkValue}</ExternalLink>
+      </ErrorCtaContainer>
+    </Fragment>
+  );
+}
+
+function ErrorMessageList({
+  message,
+  values,
+  linkFunction,
+  linkValueFunction,
+}: {
+  linkFunction: (s: string) => string;
+  linkValueFunction: (s: string) => string;
+  message: string;
+  values: string[];
+}) {
+  return (
+    <Fragment>
+      <ErrorMessageContainer>
+        <span>{message}</span>
+      </ErrorMessageContainer>
+      <ErrorMessageListContainer>
+        {values.map((value, index) => (
+          <ErrorInlineContainer key={index}>
+            <b>{value}</b>
+            <ErrorCtaContainer>
+              <ExternalLink href={linkFunction(value)} key={index}>
+                {linkValueFunction(value)}
+              </ExternalLink>
+            </ErrorCtaContainer>
+          </ErrorInlineContainer>
+        ))}
+      </ErrorMessageListContainer>
+    </Fragment>
+  );
+}
+
+interface CodeOwnerErrorsProps {
+  codeowners: CodeOwner[];
+  orgSlug: string;
+  projectSlug: string;
+}
+
+export function CodeOwnerErrors({
+  codeowners,
+  orgSlug,
+  projectSlug,
+}: CodeOwnerErrorsProps) {
+  const errMessage = (
+    codeMapping: RepositoryProjectPathConfig,
+    type: CodeOwnerErrorKeys,
+    values: string[]
+  ) => {
+    switch (type) {
+      case 'missing_external_teams':
+        return (
+          <ErrorMessage
+            message={`The following teams do not have an association in the organization: ${orgSlug}`}
+            values={values}
+            link={`/settings/${orgSlug}/integrations/${codeMapping?.provider?.slug}/${codeMapping?.integrationId}/?tab=teamMappings`}
+            linkValue="Configure Team Mappings"
+          />
+        );
+
+      case 'missing_external_users':
+        return (
+          <ErrorMessage
+            message={`The following usernames do not have an association in the organization: ${orgSlug}`}
+            values={values}
+            link={`/settings/${orgSlug}/integrations/${codeMapping?.provider?.slug}/${codeMapping?.integrationId}/?tab=userMappings`}
+            linkValue="Configure User Mappings"
+          />
+        );
+      case 'missing_user_emails':
+        return (
+          <ErrorMessage
+            message={`The following emails do not have an Sentry user in the organization: ${orgSlug}`}
+            values={values}
+            link={`/settings/${orgSlug}/members/`}
+            linkValue="Invite Users"
+          />
+        );
+
+      case 'teams_without_access':
+        return (
+          <ErrorMessageList
+            message={`The following team do not have access to the project: ${projectSlug}`}
+            values={values}
+            linkFunction={value =>
+              `/settings/${orgSlug}/teams/${value.slice(1)}/projects/`
+            }
+            linkValueFunction={value => `Configure ${value} Permissions`}
+          />
+        );
+
+      case 'users_without_access':
+        return (
+          <ErrorMessageList
+            message={`The following users are not on a team that has access to the project: ${projectSlug}`}
+            values={values}
+            linkFunction={email => `/settings/${orgSlug}/members/?query=${email}`}
+            linkValueFunction={() => `Configure Member Settings`}
+          />
+        );
+      default:
+        return null;
+    }
+  };
+
+  return (
+    <Fragment>
+      {codeowners
+        .filter(({errors}) => Object.values(errors).some(values => values.length))
+        .map(({id, codeMapping, errors}) => {
+          const errorPairs = Object.entries(errors).filter(
+            ([_, values]) => values.length
+          ) as Array<[CodeOwnerErrorKeys, string[]]>;
+          const errorCount = errorPairs.reduce(
+            (acc, [_, values]) => acc + values.length,
+            0
+          );
+          return (
+            <Alert
+              key={id}
+              type="error"
+              showIcon
+              expand={
+                <AlertContentContainer key="container">
+                  {errorPairs.map(([type, values]) => (
+                    <ErrorContainer key={`${id}-${type}`}>
+                      {errMessage(codeMapping!, type, values)}
+                    </ErrorContainer>
+                  ))}
+                </AlertContentContainer>
+              }
+            >
+              {`There were ${errorCount} ownership issues within Sentry on the latest sync with the CODEOWNERS file`}
+            </Alert>
+          );
+        })}
+    </Fragment>
+  );
+}
+
+const AlertContentContainer = styled('div')`
+  overflow-y: auto;
+  max-height: 350px;
+`;
+
+const ErrorContainer = styled('div')`
+  display: grid;
+  grid-template-areas: 'message cta';
+  grid-template-columns: 2fr 1fr;
+  gap: ${space(2)};
+  padding: ${space(1.5)} 0;
+`;
+
+const ErrorInlineContainer = styled(ErrorContainer)`
+  gap: ${space(1.5)};
+  grid-template-columns: 1fr 2fr;
+  align-items: center;
+  padding: 0;
+`;
+
+const ErrorMessageContainer = styled('div')`
+  grid-area: message;
+  display: grid;
+  gap: ${space(1.5)};
+`;
+
+const ErrorMessageListContainer = styled('div')`
+  grid-column: message / cta-end;
+  gap: ${space(1.5)};
+`;
+
+const ErrorCtaContainer = styled('div')`
+  grid-area: cta;
+  justify-self: flex-end;
+  text-align: right;
+  line-height: 1.5;
+`;

+ 6 - 156
static/app/views/settings/project/projectOwnership/index.tsx

@@ -5,7 +5,6 @@ import styled from '@emotion/styled';
 import {openEditOwnershipRules, openModal} from 'sentry/actionCreators/modal';
 import Access from 'sentry/components/acl/access';
 import Feature from 'sentry/components/acl/feature';
-import {Alert} from 'sentry/components/alert';
 import {Button} from 'sentry/components/button';
 import Form from 'sentry/components/forms/form';
 import JsonForm from 'sentry/components/forms/jsonForm';
@@ -18,6 +17,7 @@ import AsyncView from 'sentry/views/asyncView';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
 import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
 import AddCodeOwnerModal from 'sentry/views/settings/project/projectOwnership/addCodeOwnerModal';
+import {CodeOwnerErrors} from 'sentry/views/settings/project/projectOwnership/codeownerErrors';
 import CodeOwnersPanel from 'sentry/views/settings/project/projectOwnership/codeowners';
 import RulesPanel from 'sentry/views/settings/project/projectOwnership/rulesPanel';
 
@@ -125,122 +125,6 @@ tags.sku_class:enterprise #enterprise`;
     });
   };
 
-  renderCodeOwnerErrors = () => {
-    const {project, organization} = this.props;
-    const {codeowners} = this.state;
-
-    const errMessageComponent = (message, values, link, linkValue) => (
-      <Fragment>
-        <ErrorMessageContainer>
-          <span>{message}</span>
-          <b>{values.join(', ')}</b>
-        </ErrorMessageContainer>
-        <ErrorCtaContainer>
-          <ExternalLink href={link}>{linkValue}</ExternalLink>
-        </ErrorCtaContainer>
-      </Fragment>
-    );
-
-    const errMessageListComponent = (
-      message: string,
-      values: string[],
-      linkFunction: (s: string) => string,
-      linkValueFunction: (s: string) => string
-    ) => {
-      return (
-        <Fragment>
-          <ErrorMessageContainer>
-            <span>{message}</span>
-          </ErrorMessageContainer>
-          <ErrorMessageListContainer>
-            {values.map((value, index) => (
-              <ErrorInlineContainer key={index}>
-                <b>{value}</b>
-                <ErrorCtaContainer>
-                  <ExternalLink href={linkFunction(value)} key={index}>
-                    {linkValueFunction(value)}
-                  </ExternalLink>
-                </ErrorCtaContainer>
-              </ErrorInlineContainer>
-            ))}
-          </ErrorMessageListContainer>
-        </Fragment>
-      );
-    };
-
-    return (codeowners || [])
-      .filter(({errors}) => Object.values(errors).flat().length)
-      .map(({id, codeMapping, errors}) => {
-        const errMessage = (type, values) => {
-          switch (type) {
-            case 'missing_external_teams':
-              return errMessageComponent(
-                `The following teams do not have an association in the organization: ${organization.slug}`,
-                values,
-                `/settings/${organization.slug}/integrations/${codeMapping?.provider?.slug}/${codeMapping?.integrationId}/?tab=teamMappings`,
-                'Configure Team Mappings'
-              );
-
-            case 'missing_external_users':
-              return errMessageComponent(
-                `The following usernames do not have an association in the organization: ${organization.slug}`,
-                values,
-                `/settings/${organization.slug}/integrations/${codeMapping?.provider?.slug}/${codeMapping?.integrationId}/?tab=userMappings`,
-                'Configure User Mappings'
-              );
-
-            case 'missing_user_emails':
-              return errMessageComponent(
-                `The following emails do not have an Sentry user in the organization: ${organization.slug}`,
-                values,
-                `/settings/${organization.slug}/members/`,
-                'Invite Users'
-              );
-
-            case 'teams_without_access':
-              return errMessageListComponent(
-                `The following team do not have access to the project: ${project.slug}`,
-                values,
-                value =>
-                  `/settings/${organization.slug}/teams/${value.slice(1)}/projects/`,
-                value => `Configure ${value} Permissions`
-              );
-
-            case 'users_without_access':
-              return errMessageListComponent(
-                `The following users are not on a team that has access to the project: ${project.slug}`,
-                values,
-                email => `/settings/${organization.slug}/members/?query=${email}`,
-                _ => `Configure Member Settings`
-              );
-            default:
-              return null;
-          }
-        };
-        return (
-          <Alert
-            key={id}
-            type="error"
-            showIcon
-            expand={[
-              <AlertContentContainer key="container">
-                {Object.entries(errors)
-                  .filter(([_, values]) => values.length)
-                  .map(([type, values]) => (
-                    <ErrorContainer key={`${id}-${type}`}>
-                      {errMessage(type, values)}
-                    </ErrorContainer>
-                  ))}
-              </AlertContentContainer>,
-            ]}
-          >
-            {`There were ${
-              Object.values(errors).flat().length
-            } ownership issues within Sentry on the latest sync with the CODEOWNERS file`}
-          </Alert>
-        );
-      });
-  };
   renderBody() {
     const {project, organization} = this.props;
     const {ownership, codeowners} = this.state;
@@ -287,7 +171,11 @@ tags.sku_class:enterprise #enterprise`;
         <PermissionAlert
           access={!editOwnershipeRulesDisabled ? ['project:read'] : ['project:write']}
         />
-        {this.renderCodeOwnerErrors()}
+        <CodeOwnerErrors
+          orgSlug={organization.slug}
+          projectSlug={project.slug}
+          codeowners={codeowners ?? []}
+        />
         {ownership && (
           <RulesPanel
             data-test-id="issueowners-panel"
@@ -399,44 +287,6 @@ const CodeOwnerButton = styled(Button)`
   margin-left: ${space(1)};
 `;
 
-const AlertContentContainer = styled('div')`
-  overflow-y: auto;
-  max-height: 350px;
-`;
-
-const ErrorContainer = styled('div')`
-  display: grid;
-  grid-template-areas: 'message cta';
-  grid-template-columns: 2fr 1fr;
-  gap: ${space(2)};
-  padding: ${space(1.5)} 0;
-`;
-
-const ErrorInlineContainer = styled(ErrorContainer)`
-  gap: ${space(1.5)};
-  grid-template-columns: 1fr 2fr;
-  align-items: center;
-  padding: 0;
-`;
-
-const ErrorMessageContainer = styled('div')`
-  grid-area: message;
-  display: grid;
-  gap: ${space(1.5)};
-`;
-
-const ErrorMessageListContainer = styled('div')`
-  grid-column: message / cta-end;
-  gap: ${space(1.5)};
-`;
-
-const ErrorCtaContainer = styled('div')`
-  grid-area: cta;
-  justify-self: flex-end;
-  text-align: right;
-  line-height: 1.5;
-`;
-
 const IssueOwnerDetails = styled('div')`
   padding-bottom: ${space(3)};
 `;