Browse Source

fix(projects): Show join a team if person doesn't have a team (#55190)

This case is not happening in normal Sentry use but it came up with fly
partnership where users can create projects without coming to Sentry at
all. These people have access to projects that they or their team
members deployed from fly but right now our projects tab loads empty
because they have no team and project tab is filtered by team.

Fix: show them the placeholder to join a team

(There will be another fix where we add users to a team automatically at
the time of provisioning but if they don't provision or we can't find
the right team we still need to show this view)


![Screenshot 2023-08-23 at 10 17 19
AM](https://github.com/getsentry/sentry/assets/132939361/e9fb64b7-1428-4230-b1b4-88759105aec2)
Athena Moghaddam 1 year ago
parent
commit
25197cdd72

+ 36 - 0
static/app/components/noProjectMessage.spec.tsx

@@ -1,5 +1,6 @@
 import {render, screen} from 'sentry-test/reactTestingLibrary';
 
+import {Label} from 'sentry/components/editableText';
 import NoProjectMessage from 'sentry/components/noProjectMessage';
 import ConfigStore from 'sentry/stores/configStore';
 import ProjectsStore from 'sentry/stores/projectsStore';
@@ -26,6 +27,41 @@ describe('NoProjectMessage', function () {
     expect(screen.getByText('Remain Calm')).toBeInTheDocument();
   });
 
+  it('shows "Join a Team" when member has no teams', function () {
+    const organization = TestStubs.Organization({
+      slug: 'org-slug',
+      access: ['org:read', 'team:read'],
+    });
+    const childrenMock = jest.fn().mockReturnValue(null);
+    ProjectsStore.loadInitialData([]);
+
+    render(
+      <NoProjectMessage organization={organization}>{childrenMock}</NoProjectMessage>
+    );
+
+    expect(childrenMock).not.toHaveBeenCalled();
+    expect(screen.getByRole('button', {name: 'Join a Team'})).toBeInTheDocument();
+  });
+
+  it('does not show up when user has at least a project and a team', function () {
+    const organization = TestStubs.Organization({slug: 'org-slug'});
+    const team = TestStubs.Team({slug: 'team-slug', isMember: true});
+    const project = TestStubs.Project({slug: 'project1', teams: [team]});
+    ProjectsStore.loadInitialData([{...project, hasAccess: true}]);
+    TeamStore.loadInitialData([{...team, access: ['team:read']}]);
+
+    render(
+      <NoProjectMessage organization={organization}>
+        <Label data-test-id="project-row" isDisabled>
+          Some Project
+        </Label>
+      </NoProjectMessage>
+    );
+
+    expect(screen.getByTestId('project-row')).toBeInTheDocument();
+    expect(screen.queryByText('Remain Calm')).not.toBeInTheDocument();
+  });
+
   it('shows "Create Project" button when there are no projects', function () {
     ProjectsStore.loadInitialData([]);
 

+ 3 - 2
static/app/components/noProjectMessage.tsx

@@ -26,6 +26,7 @@ function NoProjectMessage({
 }: Props) {
   const {projects, initiallyLoaded: projectsLoaded} = useProjects();
   const {teams, initiallyLoaded: teamsLoaded} = useTeams();
+  const isTeamMember = teams.some(team => team.isMember);
 
   const orgSlug = organization.slug;
   const {canCreateProject} = useProjectCreationAccess({organization, teams});
@@ -39,7 +40,7 @@ function NoProjectMessage({
       ? !!projects?.some(p => p.hasAccess)
       : !!projects?.some(p => p.isMember && p.hasAccess);
 
-  if (hasProjectAccess || !projectsLoaded || !teamsLoaded) {
+  if (isTeamMember && (hasProjectAccess || !projectsLoaded || !teamsLoaded)) {
     return <Fragment>{children}</Fragment>;
   }
 
@@ -81,7 +82,7 @@ function NoProjectMessage({
         <Layout.Title>{t('Remain Calm')}</Layout.Title>
         <HelpMessage>{t('You need at least one project to use this view')}</HelpMessage>
         <Actions gap={1}>
-          {!orgHasProjects ? (
+          {!orgHasProjects && canCreateProject ? (
             createProjectAction
           ) : (
             <Fragment>

+ 2 - 0
static/app/views/alerts/create.spec.tsx

@@ -76,6 +76,8 @@ describe('ProjectAlertsCreate', function () {
   const createWrapper = (props = {}, location = {}) => {
     const {organization, project, router, routerContext} = initializeOrg(props);
     ProjectsStore.loadInitialData([project]);
+    const team = TestStubs.Team({slug: 'team-slug', isMember: true});
+    TeamStore.loadInitialData([{...team, access: ['team:read']}]);
     const params = {orgId: organization.slug, projectId: project.slug};
     const wrapper = render(
       <AlertsContainer>

+ 6 - 0
static/app/views/alerts/index.spec.tsx

@@ -1,8 +1,14 @@
 import {render, screen} from 'sentry-test/reactTestingLibrary';
 
+import TeamStore from 'sentry/stores/teamStore';
 import AlertsContainer from 'sentry/views/alerts';
 
 describe('AlertsContainer', function () {
+  beforeEach(() => {
+    const team = TestStubs.Team({slug: 'team-slug', isMember: true});
+    TeamStore.loadInitialData([{...team, access: ['team:read']}]);
+  });
+
   function SubView({hasMetricAlerts}: {hasMetricAlerts?: boolean}) {
     return <div>{hasMetricAlerts ? 'access' : 'no access'}</div>;
   }

+ 2 - 0
static/app/views/alerts/list/incidents/index.spec.tsx

@@ -35,6 +35,8 @@ describe('IncidentsList', () => {
   };
 
   beforeEach(() => {
+    const team = TestStubs.Team({slug: 'team-slug', isMember: true});
+    TeamStore.loadInitialData([{...team, access: ['team:read']}]);
     MockApiClient.addMockResponse({
       url: '/organizations/org-slug/incidents/',
       body: [

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

@@ -12,6 +12,7 @@ import {
 
 import * as modals from 'sentry/actionCreators/modal';
 import ProjectsStore from 'sentry/stores/projectsStore';
+import TeamStore from 'sentry/stores/teamStore';
 import CreateDashboard from 'sentry/views/dashboards/create';
 import * as types from 'sentry/views/dashboards/types';
 import ViewEditDashboard from 'sentry/views/dashboards/view';
@@ -27,6 +28,8 @@ describe('Dashboards > Detail', function () {
     let initialData;
 
     beforeEach(function () {
+      const team = TestStubs.Team({slug: 'team-slug', isMember: true});
+      TeamStore.loadInitialData([{...team, access: ['team:read']}]);
       act(() => ProjectsStore.loadInitialData(projects));
       initialData = initializeOrg({organization});
 

+ 3 - 0
static/app/views/dashboards/manage/index.spec.tsx

@@ -4,6 +4,7 @@ import selectEvent from 'react-select-event';
 import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
 
 import ProjectsStore from 'sentry/stores/projectsStore';
+import TeamStore from 'sentry/stores/teamStore';
 import ManageDashboards from 'sentry/views/dashboards/manage';
 
 const FEATURES = [
@@ -22,6 +23,8 @@ describe('Dashboards > Detail', function () {
     features: FEATURES,
   });
   beforeEach(function () {
+    const team = TestStubs.Team({slug: 'team-slug', isMember: true});
+    TeamStore.loadInitialData([{...team, access: ['team:read']}]);
     act(() => ProjectsStore.loadInitialData([TestStubs.Project()]));
 
     MockApiClient.addMockResponse({

+ 3 - 0
static/app/views/discover/eventDetails/index.spec.tsx

@@ -2,6 +2,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
 import {act, render, screen} from 'sentry-test/reactTestingLibrary';
 
 import ProjectsStore from 'sentry/stores/projectsStore';
+import TeamStore from 'sentry/stores/teamStore';
 import EventView from 'sentry/utils/discover/eventView';
 import {ALL_VIEWS, DEFAULT_EVENT_VIEW} from 'sentry/views/discover/data';
 import EventDetails from 'sentry/views/discover/eventDetails';
@@ -13,6 +14,8 @@ describe('Discover > EventDetails', function () {
   );
 
   beforeEach(function () {
+    const team = TestStubs.Team({slug: 'team-slug', isMember: true});
+    TeamStore.loadInitialData([{...team, access: ['team:read']}]);
     act(() => ProjectsStore.loadInitialData([TestStubs.Project()]));
 
     MockApiClient.addMockResponse({

+ 3 - 0
static/app/views/organizationStats/teamInsights/index.spec.tsx

@@ -1,6 +1,7 @@
 import {render, screen} from 'sentry-test/reactTestingLibrary';
 
 import ProjectsStore from 'sentry/stores/projectsStore';
+import TeamStore from 'sentry/stores/teamStore';
 import TeamInsightsContainer from 'sentry/views/organizationStats/teamInsights';
 
 describe('TeamInsightsContainer', () => {
@@ -22,6 +23,8 @@ describe('TeamInsightsContainer', () => {
   });
   it('allows access for orgs with flag', () => {
     ProjectsStore.loadInitialData([TestStubs.Project()]);
+    const team = TestStubs.Team({slug: 'team-slug', isMember: true});
+    TeamStore.loadInitialData([{...team, access: ['team:read']}]);
     const organization = TestStubs.Organization({features: ['team-insights']});
     const context = TestStubs.routerContext([{organization}]);
     render(

+ 3 - 0
static/app/views/projectDetail/index.spec.tsx

@@ -4,6 +4,7 @@ import {textWithMarkupMatcher} from 'sentry-test/utils';
 
 import PageFiltersStore from 'sentry/stores/pageFiltersStore';
 import ProjectsStore from 'sentry/stores/projectsStore';
+import TeamStore from 'sentry/stores/teamStore';
 import ProjectDetails from 'sentry/views/projectDetail/projectDetail';
 
 describe('ProjectDetail', function () {
@@ -13,6 +14,8 @@ describe('ProjectDetail', function () {
   beforeEach(() => {
     PageFiltersStore.reset();
     ProjectsStore.reset();
+    const team = TestStubs.Team({slug: 'team-slug', isMember: true});
+    TeamStore.loadInitialData([{...team, access: ['team:read']}]);
     // eslint-disable-next-line no-console
     jest.spyOn(console, 'error').mockImplementation(jest.fn());
 

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