Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 25 additions & 27 deletions static/app/views/organizationStats/teamInsights/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ import {LocationDescriptorObject} from 'history';
import pick from 'lodash/pick';
import moment from 'moment';

import {Client} from 'app/api';
import {DateTimeObject} from 'app/components/charts/utils';
import TeamSelector from 'app/components/forms/teamSelector';
import * as Layout from 'app/components/layouts/thirds';
import LoadingIndicator from 'app/components/loadingIndicator';
import NoProjectMessage from 'app/components/noProjectMessage';
import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams';
import {ChangeData} from 'app/components/organizations/timeRangeSelector';
import PageTimeRangeSelector from 'app/components/pageTimeRangeSelector';
import {t} from 'app/locale';
import space from 'app/styles/space';
import {DateString, Organization, RelativePeriod, TeamWithProjects} from 'app/types';
import {DateString, RelativePeriod, TeamWithProjects} from 'app/types';
import trackAdvancedAnalyticsEvent from 'app/utils/analytics/trackAdvancedAnalyticsEvent';
import {isActiveSuperuser} from 'app/utils/isActiveSuperuser';
import localStorage from 'app/utils/localStorage';
import withApi from 'app/utils/withApi';
import withOrganization from 'app/utils/withOrganization';
import withTeamsForUser from 'app/utils/withTeamsForUser';
import {useOrganization} from 'app/utils/useOrganization';
import useTeams from 'app/utils/useTeams';

import Header from '../header';

Expand All @@ -48,22 +48,14 @@ const PAGE_QUERY_PARAMS = [
'team',
];

type Props = {
api: Client;
organization: Organization;
teams: TeamWithProjects[];
loadingTeams: boolean;
error: Error | null;
} & RouteComponentProps<{orgId: string}, {}>;

function TeamInsightsOverview({
organization,
teams,
loadingTeams,
location,
router,
}: Props) {
type Props = {} & RouteComponentProps<{orgId: string}, {}>;

function TeamInsightsOverview({location, router}: Props) {
const isSuperuser = isActiveSuperuser();
const organization = useOrganization();
const {teams, initiallyLoaded} = useTeams({provideUserTeams: true});
const theme = useTheme();

const query = location?.query ?? {};
const localStorageKey = `teamInsightsSelectedTeamId:${organization.slug}`;

Expand All @@ -73,7 +65,9 @@ function TeamInsightsOverview({
localTeamId = null;
}
const currentTeamId = localTeamId ?? teams[0]?.id;
const currentTeam = teams.find(team => team.id === currentTeamId);
const currentTeam = teams.find(team => team.id === currentTeamId) as
| TeamWithProjects
| undefined;
const projects = currentTeam?.projects ?? [];

useEffect(() => {
Expand Down Expand Up @@ -171,22 +165,27 @@ function TeamInsightsOverview({
}
const {period, start, end, utc} = dataDatetime();

if (teams.length === 0) {
return (
<NoProjectMessage organization={organization} superuserNeedsToBeProjectMember />
);
}

return (
<Fragment>
<Header organization={organization} activeTab="team" />

<Body>
{loadingTeams && <LoadingIndicator />}
{!loadingTeams && (
{!initiallyLoaded && <LoadingIndicator />}
{initiallyLoaded && (
<Layout.Main fullWidth>
<ControlsWrapper>
<StyledTeamSelector
name="select-team"
inFieldLabel={t('Team: ')}
value={currentTeam?.slug}
isLoading={loadingTeams}
onChange={choice => handleChangeTeam(choice.actor.id)}
teamFilter={filterTeam => filterTeam.isMember}
teamFilter={isSuperuser ? undefined : filterTeam => filterTeam.isMember}
styles={{
singleValue(provided: any) {
const custom = {
Expand Down Expand Up @@ -339,8 +338,7 @@ function TeamInsightsOverview({
);
}

export {TeamInsightsOverview};
export default withApi(withOrganization(withTeamsForUser(TeamInsightsOverview)));
export default TeamInsightsOverview;

const Body = styled(Layout.Body)`
margin-bottom: -20px;
Expand Down
100 changes: 68 additions & 32 deletions tests/js/spec/views/organizationStats/teamInsights/overview.spec.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import {
act,
fireEvent,
mountWithTheme,
screen,
waitFor,
} from 'sentry-test/reactTestingLibrary';
import {fireEvent, mountWithTheme, screen} from 'sentry-test/reactTestingLibrary';

import ProjectsStore from 'app/stores/projectsStore';
import TeamStore from 'app/stores/teamStore';
import {isActiveSuperuser} from 'app/utils/isActiveSuperuser';
import localStorage from 'app/utils/localStorage';
import {TeamInsightsOverview} from 'app/views/organizationStats/teamInsights/overview';
import {OrganizationContext} from 'app/views/organizationContext';
import TeamInsightsOverview from 'app/views/organizationStats/teamInsights/overview';

jest.mock('app/utils/localStorage');
jest.mock('app/utils/isActiveSuperuser', () => ({
isActiveSuperuser: jest.fn(),
}));

describe('TeamInsightsOverview', () => {
const project1 = TestStubs.Project({id: '2', name: 'js', slug: 'js'});
Expand All @@ -20,12 +20,21 @@ describe('TeamInsightsOverview', () => {
slug: 'frontend',
name: 'frontend',
projects: [project1],
isMember: true,
});
const team2 = TestStubs.Team({
id: '3',
slug: 'backend',
name: 'backend',
projects: [project2],
isMember: true,
});
const team3 = TestStubs.Team({
id: '4',
slug: 'internal',
name: 'internal',
projects: [],
isMember: false,
});
const mockRouter = {push: jest.fn()};

Expand Down Expand Up @@ -88,58 +97,85 @@ describe('TeamInsightsOverview', () => {
url: `/teams/org-slug/${team1.slug}/release-count/`,
body: [],
});
act(() => void TeamStore.loadInitialData([team1, team2]));
MockApiClient.addMockResponse({
url: `/teams/org-slug/${team2.slug}/alerts-triggered/`,
body: TestStubs.TeamAlertsTriggered(),
});
MockApiClient.addMockResponse({
url: `/teams/org-slug/${team2.slug}/time-to-resolution/`,
body: TestStubs.TeamResolutionTime(),
});
MockApiClient.addMockResponse({
url: `/teams/org-slug/${team2.slug}/issue-breakdown/`,
body: TestStubs.TeamIssuesReviewed(),
});
MockApiClient.addMockResponse({
method: 'GET',
url: `/teams/org-slug/${team2.slug}/release-count/`,
body: [],
});
});

afterEach(() => {
jest.resetAllMocks();
});

function createWrapper() {
const teams = [team1, team2];
const teams = [team1, team2, team3];
const projects = [project1, project2];
const organization = TestStubs.Organization({teams, projects});
const context = TestStubs.routerContext([{organization}]);
TeamStore.loadInitialData(teams);

return mountWithTheme(
<TeamInsightsOverview
api={new MockApiClient()}
loadingTeams={false}
error={null}
organization={organization}
teams={teams}
router={mockRouter}
location={{}}
/>,
<OrganizationContext.Provider value={organization}>
<TeamInsightsOverview router={mockRouter} location={{}} />
</OrganizationContext.Provider>,
{
context,
}
);
}

it('defaults to first team', async () => {
it('defaults to first team', () => {
createWrapper();
await waitFor(() => {
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
});

expect(screen.getByText('#frontend')).toBeInTheDocument();
expect(screen.getByText('#backend')).toBeInTheDocument();
expect(screen.getByText('Key transaction')).toBeInTheDocument();
});

it('allows team switching', async () => {
it('allows team switching', () => {
createWrapper();
await waitFor(() => {
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
});

fireEvent.mouseDown(screen.getByText('#frontend'));
expect(screen.getByText('#backend')).toBeInTheDocument();
fireEvent.click(screen.getByText('#backend'));
expect(mockRouter.push).toHaveBeenCalledWith({query: {team: team2.id}});
fireEvent.mouseDown(screen.getByText('#backend'));
expect(screen.getByText('#frontend')).toBeInTheDocument();
// Teams user is not a member of are hidden
expect(screen.queryByText('#internal')).not.toBeInTheDocument();
fireEvent.click(screen.getByText('#frontend'));
expect(mockRouter.push).toHaveBeenCalledWith({query: {team: team1.id}});
expect(localStorage.setItem).toHaveBeenCalledWith(
'teamInsightsSelectedTeamId:org-slug',
team2.id
team1.id
);
});

it('superusers can switch to any team', () => {
isActiveSuperuser.mockReturnValue(true);
createWrapper();

expect(screen.getByText('#backend')).toBeInTheDocument();
fireEvent.mouseDown(screen.getByText('#backend'));
expect(screen.getByText('#frontend')).toBeInTheDocument();
// User is not a member of internal team
expect(screen.getByText('#internal')).toBeInTheDocument();
});

it('shows users with no teams the join team button', () => {
createWrapper();
ProjectsStore.loadInitialData([{...project1, isMember: false}]);
TeamStore.loadInitialData([]);

expect(screen.getByText('Join a Team')).toBeInTheDocument();
});
});