diff --git a/eslint-suppressions.json b/eslint-suppressions.json index bac173e1cdf..7016dcf8169 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3553,11 +3553,6 @@ "count": 1 } }, - "public/app/features/teams/TeamSettings.tsx": { - "no-restricted-syntax": { - "count": 4 - } - }, "public/app/features/teams/state/reducers.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 diff --git a/packages/grafana-test-utils/src/fixtures/index.ts b/packages/grafana-test-utils/src/fixtures/index.ts index 0fc6300ba49..2fb732d4244 100644 --- a/packages/grafana-test-utils/src/fixtures/index.ts +++ b/packages/grafana-test-utils/src/fixtures/index.ts @@ -1,8 +1,10 @@ import { setupMockStarredDashboards } from './starred'; +import { setupMockTeams } from './teams'; /** * Reset any stateful fixtures that are used to drive mock handler endpoints */ export const resetFixtures = () => { setupMockStarredDashboards(); + setupMockTeams(); }; diff --git a/packages/grafana-test-utils/src/fixtures/starred.ts b/packages/grafana-test-utils/src/fixtures/starred.ts index 77150e54c4a..6eeba4051d2 100644 --- a/packages/grafana-test-utils/src/fixtures/starred.ts +++ b/packages/grafana-test-utils/src/fixtures/starred.ts @@ -11,4 +11,4 @@ export const setupMockStarredDashboards = () => { }); }; -export const mockStarredDashboardsMap = new Map(); +export const mockStarredDashboardsMap = new Map(initialStarredDashboards.map((uid) => [uid, true])); diff --git a/packages/grafana-test-utils/src/fixtures/teams.ts b/packages/grafana-test-utils/src/fixtures/teams.ts index 86c737fd7a7..4039129fb5d 100644 --- a/packages/grafana-test-utils/src/fixtures/teams.ts +++ b/packages/grafana-test-utils/src/fixtures/teams.ts @@ -23,3 +23,14 @@ export const MOCK_TEAMS = [ status: {}, }, ]; + +export const setupMockTeams = () => { + mockTeamsMap.clear(); + MOCK_TEAMS.forEach((team) => { + mockTeamsMap.set(team.metadata.name, { team, groups: [] }); + }); +}; + +export const mockTeamsMap = new Map }>( + MOCK_TEAMS.map((team) => [team.metadata.name, { team, groups: [] }]) +); diff --git a/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts b/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts index 31d513a2263..e9fb6c0a112 100644 --- a/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts +++ b/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts @@ -1,8 +1,8 @@ import { HttpResponse, http } from 'msw'; -import { MOCK_TEAMS } from '../../../fixtures/teams'; +import { MOCK_TEAMS, mockTeamsMap } from '../../../fixtures/teams'; -const k8sTeamToLegacyTeam = (k8sTeam: (typeof MOCK_TEAMS)[number]) => { +const k8sTeamToLegacyTeam = (k8sTeam: (typeof MOCK_TEAMS)[number], addAccessControl?: boolean) => { return { name: k8sTeam.spec.title, email: k8sTeam.spec.email, @@ -14,19 +14,98 @@ const k8sTeamToLegacyTeam = (k8sTeam: (typeof MOCK_TEAMS)[number]) => { avatarUrl: '', memberCount: 0, permission: 0, + accessControl: addAccessControl ? mockAccessControl : null, }; }; +const mockAccessControl = { + 'teams.permissions:read': true, + 'teams.permissions:write': true, + 'teams:delete': true, + 'teams:read': true, + 'teams:write': true, +}; + +const getTeamHandler = () => + http.get<{ uid: string }>('/api/teams/:uid', async ({ params, request }) => { + const teamData = mockTeamsMap.get(params.uid); + const url = new URL(request.url); + const accessControl = url.searchParams.get('accesscontrol') === 'true'; + + if (!teamData) { + return HttpResponse.json({ message: 'Not found' }, { status: 404 }); + } + + const response = k8sTeamToLegacyTeam(teamData.team, accessControl); + + return HttpResponse.json(response); + }); + +const deleteTeamHandler = () => + http.delete<{ uid: string }>('/api/teams/:uid', async ({ params }) => { + const teamData = mockTeamsMap.get(params.uid); + + if (!teamData) { + return HttpResponse.json({ message: 'Not found' }, { status: 404 }); + } + + mockTeamsMap.delete(params.uid); + + return HttpResponse.json({ message: 'Team deleted' }); + }); + +const teamsPreferencesHandler = () => + http.get<{ uid: string }>('/api/teams/:uid/preferences', async ({ params }) => { + const team = Array.from(mockTeamsMap.values()).find( + (teamData) => teamData.team.metadata.labels['grafana.app/deprecatedInternalID'] === params.uid + ); + + if (!team) { + return HttpResponse.json({ message: 'Not found' }, { status: 404 }); + } + + // TODO: Mock preferences data + return HttpResponse.json({}); + }); + +const teamsGroupsHandler = () => + http.get<{ uid: string }>('/api/teams/:uid/groups', async ({ params }) => { + const team = mockTeamsMap.get(params.uid); + + if (!team) { + return HttpResponse.json({ message: 'Not found' }, { status: 404 }); + } + + return HttpResponse.json(team.groups); + }); + +const teamsUpdateGroupsHandler = () => + http.post<{ uid: string }, { groupId: string }>('/api/teams/:uid/groups', async ({ params, request }) => { + const teamData = mockTeamsMap.get(params.uid); + const body = await request.json(); + if (!teamData) { + return HttpResponse.json({ message: 'Not found' }, { status: 404 }); + } + const updatedTeam = { + ...teamData, + groups: [...teamData.groups, body], + }; + mockTeamsMap.set(params.uid, updatedTeam); + + return HttpResponse.json({ message: 'Group added to Team' }); + }); + const searchTeamsHandler = () => http.get('/api/teams/search', async ({ request }) => { const url = new URL(request.url); + const accessControl = url.searchParams.get('accesscontrol') === 'true'; // TODO in future: pagination and mock querying const page = url.searchParams.get('page') ?? 1; const perPage = url.searchParams.get('perPage') ?? 1000; return HttpResponse.json({ - totalCount: MOCK_TEAMS.length, - teams: MOCK_TEAMS.map(k8sTeamToLegacyTeam), + totalCount: mockTeamsMap.size, + teams: Array.from(mockTeamsMap.values()).map((t) => k8sTeamToLegacyTeam(t.team, accessControl)), page, perPage, }); @@ -40,7 +119,7 @@ const createTeamHandler = () => return HttpResponse.json({ message: 'bad request data' }, { status: 400 }); } - const existingTeam = MOCK_TEAMS.find((t) => t.spec.title === body.name); + const existingTeam = Array.from(mockTeamsMap.values()).find((teamData) => teamData.team.spec.title === body.name); if (existingTeam) { return HttpResponse.json({ message: 'Team name taken' }, { status: 409 }); @@ -48,6 +127,14 @@ const createTeamHandler = () => return HttpResponse.json({ message: 'Team created', teamId: 10, uid: 'aethyfifmhwcgd' }, { status: 200 }); }); -const handlers = [searchTeamsHandler(), createTeamHandler()]; +const handlers = [ + teamsPreferencesHandler(), + teamsGroupsHandler(), + teamsUpdateGroupsHandler(), + searchTeamsHandler(), + getTeamHandler(), + deleteTeamHandler(), + createTeamHandler(), +]; export default handlers; diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.test.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.test.tsx index f738dc043df..d3e4baa1f4d 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.test.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.test.tsx @@ -121,7 +121,7 @@ describe('SharedPreferences', () => { await selectComboboxOptionInTest(await screen.findByRole('combobox', { name: 'Week start' }), 'Saturday'); await selectComboboxOptionInTest(await screen.findByRole('combobox', { name: /language/i }), 'Français'); - await user.click(screen.getByText('Save')); + await user.click(screen.getByText('Save preferences')); const requests = await capture; const newPreferences = await getPrefsUpdateRequest(requests); @@ -154,7 +154,7 @@ describe('SharedPreferences', () => { await selectComboboxOptionInTest(screen.getByRole('combobox', { name: /language/i }), 'Default'); - await user.click(screen.getByText('Save')); + await user.click(screen.getByText('Save preferences')); const requests = await capture; const newPreferences = await getPrefsUpdateRequest(requests); expect(newPreferences).toEqual({ @@ -171,7 +171,7 @@ describe('SharedPreferences', () => { it('refreshes the page after saving preferences', async () => { const { user } = await setup(); - await user.click(screen.getByText('Save')); + await user.click(screen.getByText('Save preferences')); expect(mockReload).toHaveBeenCalled(); }); }); diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 7afc8df92a6..073b2e81588 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -367,7 +367,7 @@ export class SharedPreferences extends PureComponent { variant="primary" data-testid={selectors.components.UserProfile.preferencesSaveButton} > - Save + Save preferences ); diff --git a/public/app/features/org/OrgDetailsPage.test.tsx b/public/app/features/org/OrgDetailsPage.test.tsx index 19375f769c5..e34b245cd49 100644 --- a/public/app/features/org/OrgDetailsPage.test.tsx +++ b/public/app/features/org/OrgDetailsPage.test.tsx @@ -1,9 +1,8 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import { mockToolkitActionCreator } from 'test/core/redux/mocks'; -import { TestProvider } from 'test/helpers/TestProvider'; +import { render, screen } from 'test/test-utils'; import { NavModel } from '@grafana/data'; +import { ModalRoot } from '@grafana/ui'; import { Organization } from 'app/types/organization'; import { backendSrv } from '../../core/services/backend_srv'; @@ -46,10 +45,11 @@ const setup = (propOverrides?: object) => { }; Object.assign(props, propOverrides); - render( - + return render( + <> - + + ); }; @@ -86,7 +86,7 @@ describe('Render', () => { }); it('should show a modal when submitting', async () => { - setup({ + const { user } = setup({ organization: { name: 'Cool org', id: 1, @@ -99,8 +99,8 @@ describe('Render', () => { }, }); - await userEvent.click(screen.getByRole('button', { name: 'Save' })); + await user.click(screen.getByRole('button', { name: 'Save preferences' })); - expect(screen.getByText('Confirm preferences update')).toBeInTheDocument(); + expect(await screen.findByText('Confirm preferences update')).toBeInTheDocument(); }); }); diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx index bda03e89d5d..e3bd21b7e3a 100644 --- a/public/app/features/teams/TeamGroupSync.test.tsx +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -1,23 +1,28 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { render, screen } from 'test/test-utils'; -import { TeamGroup } from 'app/types/teams'; +import { setBackendSrv } from '@grafana/runtime'; +import { setupMockServer } from '@grafana/test-utils/server'; +import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { Team, TeamGroup, TeamState } from 'app/types/teams'; -import { Props, TeamGroupSync } from './TeamGroupSync'; +import TeamGroupSync from './TeamGroupSync'; import { getMockTeamGroups } from './mocks/teamMocks'; -const setup = (propOverrides?: object) => { - const props: Props = { - isReadOnly: false, - groups: [] as TeamGroup[], - loadTeamGroups: jest.fn(), - addTeamGroup: jest.fn(), - removeTeamGroup: jest.fn(), - }; +setBackendSrv(backendSrv); +setupMockServer(); - Object.assign(props, propOverrides); - - return render(); +const setup = (preloadedTeamState?: Partial) => { + return render(, { + preloadedState: { + team: { + members: [], + groups: [], + team: { uid: MOCK_TEAMS[0].metadata.name } as Team, + ...preloadedTeamState, + }, + }, + }); }; describe('TeamGroupSync', () => { @@ -32,26 +37,22 @@ describe('TeamGroupSync', () => { }); it('should call add group', async () => { - const mockAddGroup = jest.fn(); - setup({ addTeamGroup: mockAddGroup }); + const { user } = setup(); // Empty List CTA "Add group" button is second in the DOM order - await userEvent.click(screen.getAllByRole('button', { name: /add group/i })[1]); + await user.click(screen.getAllByRole('button', { name: /add group/i })[1]); expect(screen.getByRole('textbox', { name: /add external group/i })).toBeVisible(); - await userEvent.type(screen.getByRole('textbox', { name: /add external group/i }), 'test/group'); - await userEvent.click(screen.getAllByRole('button', { name: /add group/i })[0]); - await waitFor(() => { - expect(mockAddGroup).toHaveBeenCalledWith('test/group'); - }); + await user.type(screen.getByRole('textbox', { name: /add external group/i }), 'test/group'); + await user.click(screen.getAllByRole('button', { name: /add group/i })[0]); + + expect(screen.getByRole('row', { name: /test\/group/i })).toBeInTheDocument(); }); - it('should call remove group', async () => { - const mockRemoveGroup = jest.fn(); + it('should remove group', async () => { const mockGroup: TeamGroup = { teamId: 1, groupId: 'someGroup' }; - setup({ removeTeamGroup: mockRemoveGroup, groups: [mockGroup] }); - await userEvent.click(screen.getByRole('button', { name: 'Remove group someGroup' })); - await waitFor(() => { - expect(mockRemoveGroup).toHaveBeenCalledWith('someGroup'); - }); + const { user } = setup({ groups: [mockGroup] }); + await user.click(screen.getByRole('button', { name: 'Remove group someGroup' })); + + expect(screen.queryByRole('row', { name: /test\/group/i })).not.toBeInTheDocument(); }); }); diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index a5f8adc3f6e..a78cbe2eceb 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -1,63 +1,44 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { TestProvider } from 'test/helpers/TestProvider'; +import { render, screen, waitFor } from 'test/test-utils'; +import { setBackendSrv } from '@grafana/runtime'; +import { setupMockServer } from '@grafana/test-utils/server'; +import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; +import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; -import { Team } from 'app/types/teams'; -import { Props, TeamList } from './TeamList'; -import { getMockTeam, getMultipleMockTeams } from './mocks/teamMocks'; +import TeamList from './TeamList'; -jest.mock('app/core/core', () => ({ - contextSrv: { - hasPermission: (action: string) => true, - licensedAccessControlEnabled: () => false, - user: { - helpFlags1: 0, - }, - }, -})); - -const setup = (propOverrides?: object) => { - const props: Props = { - teams: [] as Team[], - noTeams: false, - loadTeams: jest.fn(), - deleteTeam: jest.fn(), - changePage: jest.fn(), - changeQuery: jest.fn(), - changeSort: jest.fn(), - query: '', - totalPages: 0, - page: 0, - hasFetched: false, - perPage: 10, - rolesLoading: false, - }; - - Object.assign(props, propOverrides); - - render( - - - - ); -}; +setBackendSrv(backendSrv); +setupMockServer(); describe('TeamList', () => { - it('should render teams table', () => { - setup({ teams: getMultipleMockTeams(5), teamsCount: 5, hasFetched: true }); - expect(screen.getAllByRole('row')).toHaveLength(6); // 5 teams plus table header row + beforeEach(() => { + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); + jest.spyOn(contextSrv, 'hasPermissionInMetadata').mockReturnValue(true); + jest.spyOn(contextSrv, 'fetchUserPermissions').mockResolvedValue(); + }); + + it('should render teams table', async () => { + render(); + await waitFor(() => + expect(screen.getAllByRole('row')) + // Number of teams plus table header row + .toHaveLength(MOCK_TEAMS.length + 1) + ); + }); + + it('deletes a team', async () => { + const mockTeam = MOCK_TEAMS[0]; + const { user } = render(); + await user.click(await screen.findByRole('button', { name: `Delete team ${mockTeam.spec.title}` })); + await user.click(screen.getByRole('button', { name: 'Delete' })); + + await waitFor(() => expect(screen.queryByText(mockTeam.spec.title)).not.toBeInTheDocument()); }); describe('when user has access to create a team', () => { - it('should enable the new team button', () => { - jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); - setup({ - teams: getMultipleMockTeams(1), - totalCount: 1, - hasFetched: true, - }); + it('should enable the new team button', async () => { + render(); expect(screen.getByRole('link', { name: /new team/i })).not.toHaveStyle('pointer-events: none'); }); @@ -66,25 +47,9 @@ describe('TeamList', () => { describe('when user does not have access to create a team', () => { it('should disable the new team button', () => { jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); - setup({ - teams: getMultipleMockTeams(1), - totalCount: 1, - hasFetched: true, - }); + render(); expect(screen.getByRole('link', { name: /new team/i })).toHaveStyle('pointer-events: none'); }); }); }); - -it('should call delete team', async () => { - const mockDelete = jest.fn(); - const mockTeam = getMockTeam(); - jest.spyOn(contextSrv, 'hasPermissionInMetadata').mockReturnValue(true); - setup({ deleteTeam: mockDelete, teams: [mockTeam], totalCount: 1, hasFetched: true }); - await userEvent.click(screen.getByRole('button', { name: `Delete team ${mockTeam.name}` })); - await userEvent.click(screen.getByRole('button', { name: 'Delete' })); - await waitFor(() => { - expect(mockDelete).toHaveBeenCalledWith(mockTeam.uid); - }); -}); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 9f580cbb6a9..e2ecbf07c4a 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -3,7 +3,6 @@ import { useEffect, useMemo, useState } from 'react'; import Skeleton from 'react-loading-skeleton'; import { connect, ConnectedProps } from 'react-redux'; -import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Avatar, @@ -50,7 +49,7 @@ const skeletonData: TeamWithRoles[] = new Array(3).fill(null).map((_, index) => isProvisioned: false, })); -export const TeamList = ({ +const TeamList = ({ teams, query, noTeams, @@ -328,7 +327,7 @@ const connector = connect(mapStateToProps, mapDispatchToProps); export type Props = OwnProps & ConnectedProps; export default connector(TeamList); -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = () => ({ blockSkeleton: css({ lineHeight: 1, // needed for things to align properly in the table diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx index ae180bd5e63..82dbbb20e33 100644 --- a/public/app/features/teams/TeamPages.test.tsx +++ b/public/app/features/teams/TeamPages.test.tsx @@ -1,70 +1,25 @@ -import { screen } from '@testing-library/react'; -import { useParams } from 'react-router-dom-v5-compat'; -import { render } from 'test/test-utils'; +import { Route, Routes } from 'react-router-dom-v5-compat'; +import { screen, render, testWithLicenseFeatures, waitFor } from 'test/test-utils'; + +import { setBackendSrv } from '@grafana/runtime'; +import { setupMockServer } from '@grafana/test-utils/server'; +import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; +import { backendSrv } from 'app/core/services/backend_srv'; import TeamPages from './TeamPages'; -import { getMockTeam } from './mocks/teamMocks'; -jest.mock('app/core/components/Select/UserPicker', () => { - return { UserPicker: () => null }; -}); - -jest.mock('app/core/services/context_srv', () => ({ - contextSrv: { - accessControlEnabled: () => true, - hasPermissionInMetadata: () => true, - user: {}, - }, -})); - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - getBackendSrv: () => ({ - get: jest.fn().mockResolvedValue(getMockTeam()), - }), - config: { - ...jest.requireActual('@grafana/runtime').config, - licenseInfo: { - enabledFeatures: { teamsync: true }, - stateInfo: '', - licenseUrl: '', - }, - featureToggles: { accesscontrol: true }, - buildInfo: { - edition: 'Open Source', - version: '7.5.0', - commit: 'abc123', - env: 'production', - latestVersion: '', - hasUpdate: false, - hideVersion: false, - }, - appSubUrl: '', - }, - featureEnabled: () => true, -})); - -// Mock connected child components instead of rendering them -jest.mock('./TeamSettings', () => { - //eslint-disable-next-line - return () =>
Team settings
; -}); - -jest.mock('./TeamGroupSync', () => { - //eslint-disable-next-line - return () =>
Team group sync
; -}); - -jest.mock('react-router-dom-v5-compat', () => ({ - ...jest.requireActual('react-router-dom-v5-compat'), - useParams: jest.fn(), -})); +setBackendSrv(backendSrv); +setupMockServer(); const setup = (propOverrides: { teamUid?: string; pageName?: string } = {}) => { const pageName = propOverrides.pageName ?? 'members'; - const teamUid = propOverrides.teamUid ?? 'aaaaaa'; - (useParams as jest.Mock).mockReturnValue({ uid: `${teamUid}`, page: pageName }); - render(); + const teamUid = propOverrides.teamUid ?? MOCK_TEAMS[0].metadata.name; + render( + + } /> + , + { historyOptions: { initialEntries: [`/org/teams/${teamUid}/${pageName}`] } } + ); }; describe('TeamPages', () => { @@ -73,14 +28,30 @@ describe('TeamPages', () => { pageName: 'settings', }); - expect(await screen.findByText('Team settings')).toBeInTheDocument(); + expect(await screen.findByText('Team details')).toBeInTheDocument(); }); - it('should render group sync page', async () => { - setup({ - pageName: 'groupsync', - }); + describe('teamsync feature disabled', () => { + testWithLicenseFeatures({ disable: ['teamsync'] }); - expect(await screen.findByText('Team group sync')).toBeInTheDocument(); + it('should not render group sync page', async () => { + setup({ + pageName: 'groupsync', + }); + await waitFor(() => expect(screen.queryAllByText(/loading .../i)).toHaveLength(0)); + + expect(screen.queryByRole('heading', { name: /external group sync/i })).not.toBeInTheDocument(); + }); + }); + + describe('teamsync feature enabled', () => { + testWithLicenseFeatures({ enable: ['teamsync'] }); + + it('should render group sync page', async () => { + setup({ + pageName: 'groupsync', + }); + expect(await screen.findByRole('heading', { name: /external group sync/i })).toBeInTheDocument(); + }); }); }); diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx index a203c66eed9..1046cd61154 100644 --- a/public/app/features/teams/TeamSettings.test.tsx +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -1,64 +1,68 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { render, screen, waitFor } from 'test/test-utils'; + +import { setBackendSrv } from '@grafana/runtime'; +import { setupMockServer } from '@grafana/test-utils/server'; +import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; +import { contextSrv } from 'app/core/core'; +import { backendSrv } from 'app/core/services/backend_srv'; import { Props, TeamSettings } from './TeamSettings'; -import { getMockTeam } from './mocks/teamMocks'; -jest.mock('app/core/services/context_srv', () => ({ - contextSrv: { - licensedAccessControlEnabled: () => false, - hasPermission: () => true, - hasPermissionInMetadata: () => true, - user: { orgId: 1 }, - }, -})); +jest.spyOn(contextSrv, 'hasPermission').mockImplementation(() => true); +jest.spyOn(contextSrv, 'hasPermissionInMetadata').mockImplementation(() => true); -jest.mock('app/core/components/SharedPreferences/SharedPreferences', () => { - return { SharedPreferences: () =>
}; -}); - -jest.mock('app/core/components/RolePicker/hooks', () => ({ - useRoleOptions: jest.fn().mockReturnValue([{ roleOptions: [] }, jest.fn()]), -})); +setBackendSrv(backendSrv); +setupMockServer(); const setup = (propOverrides?: object) => { + const team = MOCK_TEAMS[0]; const props: Props = { - team: getMockTeam(), + team: { + id: Number(team.metadata.labels['grafana.app/deprecatedInternalID']), + uid: team.metadata.name, + memberCount: 0, + name: team.spec.title, + orgId: 1, + isProvisioned: false, + }, updateTeam: jest.fn(), }; Object.assign(props, propOverrides); - render(); + return render(); }; describe('Team settings', () => { - it('should render component', () => { + it('should render component', async () => { setup(); - expect(screen.getByText('Team details')).toBeInTheDocument(); + expect(await screen.findByText('Team details')).toBeInTheDocument(); }); it('should validate required fields', async () => { const mockUpdate = jest.fn(); - setup({ updateTeam: mockUpdate }); - await userEvent.clear(screen.getByRole('textbox', { name: /Name/ })); - await userEvent.type(screen.getByLabelText(/Email/i), 'team@test.com'); - // Submitting with userEvent doesn't work here - fireEvent.submit(screen.getByRole('button', { name: 'Save' })); + const { user } = setup({ updateTeam: mockUpdate }); + await screen.findByText('Team details'); + + await user.clear(screen.getByRole('textbox', { name: /Name/ })); + await user.type(screen.getByLabelText(/Email/i), 'team@test.com'); + await user.click(screen.getByRole('button', { name: 'Save team details' })); + expect(await screen.findByText('Name is required')).toBeInTheDocument(); await waitFor(() => expect(mockUpdate).not.toHaveBeenCalled()); }); it('should submit form with correct values', async () => { const mockUpdate = jest.fn(); - setup({ updateTeam: mockUpdate }); - await userEvent.clear(screen.getByRole('textbox', { name: /Name/ })); - await userEvent.clear(screen.getByLabelText(/Email/i)); - await userEvent.type(screen.getByRole('textbox', { name: /Name/ }), 'New team'); - await userEvent.type(screen.getByLabelText(/Email/i), 'team@test.com'); - // Submitting with userEvent doesn't work here - fireEvent.submit(screen.getByRole('button', { name: 'Save' })); + const { user } = setup({ updateTeam: mockUpdate }); + + await user.clear(screen.getByRole('textbox', { name: /Name/ })); + await user.clear(screen.getByLabelText(/Email/i)); + await user.type(screen.getByRole('textbox', { name: /Name/ }), 'New team'); + await user.type(screen.getByLabelText(/Email/i), 'team@test.com'); + await user.click(screen.getByRole('button', { name: 'Save team details' })); + await waitFor(() => expect(mockUpdate).toHaveBeenCalledWith('New team', 'team@test.com')); }); }); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 79e9ff95dd9..a4779df63cc 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -50,40 +50,53 @@ export const TeamSettings = ({ team, updateTeam }: Props) => {
- - - - - - - - {contextSrv.licensedAccessControlEnabled() && canListRoles && ( - - + + + + + + - )} - + + )} - disabled={!canWriteTeamSettings} - > - {/* eslint-disable-next-line @grafana/i18n/no-untranslated-strings */} - - - + + + + +
+
diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index e7a6c5a923a..b6bd50a7c57 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -7,7 +7,7 @@ import { contextSrv } from 'app/core/core'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; import { AccessControlAction } from 'app/types/accessControl'; import { ThunkResult } from 'app/types/store'; -import { Team, TeamMember, TeamWithRoles } from 'app/types/teams'; +import { Team, TeamWithRoles } from 'app/types/teams'; import { buildNavModel } from './navModel'; import { @@ -15,7 +15,6 @@ import { queryChanged, pageChanged, teamLoaded, - teamMembersLoaded, teamsLoaded, sortChanged, rolesFetchBegin, @@ -101,14 +100,6 @@ export function changeSort({ sortBy }: FetchDataArgs): ThunkResult { }; } -export function loadTeamMembers(): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - const response = await getBackendSrv().get(`/api/teams/${team.uid}/members`); - dispatch(teamMembersLoaded(response)); - }; -} - export function updateTeam(name: string, email: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; @@ -141,12 +132,3 @@ export function removeTeamGroup(groupId: string): ThunkResult { dispatch(loadTeamGroups()); }; } - -export function updateTeamMember(member: TeamMember): ThunkResult { - return async (dispatch) => { - await getBackendSrv().put(`/api/teams/${member.teamId}/members/${member.userId}`, { - permission: member.permission, - }); - dispatch(loadTeamMembers()); - }; -} diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index e42e467c6b1..4ea5c963739 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -1,16 +1,14 @@ import { TeamsState, TeamState } from 'app/types/teams'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { getMockTeam, getMockTeamGroups, getMockTeamMember } from '../mocks/teamMocks'; +import { getMockTeam, getMockTeamGroups } from '../mocks/teamMocks'; import { initialTeamsState, initialTeamState, - setSearchMemberQuery, teamGroupsLoaded, teamLoaded, queryChanged, - teamMembersLoaded, teamReducer, teamsLoaded, teamsReducer, @@ -62,30 +60,6 @@ describe('team reducer', () => { }); }); - describe('when loadTeamMembersAction is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamReducer, { ...initialTeamState }) - .whenActionIsDispatched(teamMembersLoaded([getMockTeamMember()])) - .thenStateShouldEqual({ - ...initialTeamState, - members: [getMockTeamMember()], - }); - }); - }); - - describe('when setSearchMemberQueryAction is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamReducer, { ...initialTeamState }) - .whenActionIsDispatched(setSearchMemberQuery('member')) - .thenStateShouldEqual({ - ...initialTeamState, - searchMemberQuery: 'member', - }); - }); - }); - describe('when loadTeamGroupsAction is dispatched', () => { it('then state should be correct', () => { reducerTester() diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index b98c6d21095..2ef9dde3838 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { TeamsState, Team, TeamState, TeamMember, TeamGroup } from 'app/types/teams'; +import { TeamsState, Team, TeamState, TeamGroup } from 'app/types/teams'; export const initialTeamsState: TeamsState = { teams: [], @@ -56,7 +56,6 @@ export const initialTeamState: TeamState = { team: {} as Team, members: [], groups: [], - searchMemberQuery: '', }; const teamSlice = createSlice({ @@ -66,19 +65,13 @@ const teamSlice = createSlice({ teamLoaded: (state, action: PayloadAction): TeamState => { return { ...state, team: action.payload }; }, - teamMembersLoaded: (state, action: PayloadAction): TeamState => { - return { ...state, members: action.payload }; - }, - setSearchMemberQuery: (state, action: PayloadAction): TeamState => { - return { ...state, searchMemberQuery: action.payload }; - }, teamGroupsLoaded: (state, action: PayloadAction): TeamState => { return { ...state, groups: action.payload }; }, }, }); -export const { teamLoaded, teamGroupsLoaded, teamMembersLoaded, setSearchMemberQuery } = teamSlice.actions; +export const { teamLoaded, teamGroupsLoaded } = teamSlice.actions; export const teamReducer = teamSlice.reducer; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index b1fed3d1ef3..983c5d35a71 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -11,7 +11,6 @@ describe('Team selectors', () => { it('should return team if matching with location team', () => { const mockState: TeamState = { team: mockTeam, - searchMemberQuery: '', members: [], groups: [], }; diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts index 123954636bc..f592b362894 100644 --- a/public/app/types/teams.ts +++ b/public/app/types/teams.ts @@ -89,5 +89,4 @@ export interface TeamState { team: Team; members: TeamMember[]; groups: TeamGroup[]; - searchMemberQuery: string; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 15d82375ede..ce12e2f8497 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -12678,6 +12678,7 @@ "theme-label": "Interface theme", "week-start-label": "Week start" }, + "save": "Save preferences", "theme": { "default-label": "Default", "experimental": "Experimental" @@ -13079,13 +13080,13 @@ "placeholder-search-teams": "Search teams" }, "team-settings": { - "description-email": "This is optional and is primarily used to set the team profile avatar (via gravatar service)", + "description-email": "This is optional and is primarily used to set the team profile avatar (via the Gravatar service)", "label-email": "Email", "label-name": "Name", "label-numerical-identifier": "Numerical identifier", "label-role": "Role", "label-team-details": "Team details", - "save": "Save" + "save": "Save team details" }, "team-sync-upgrade-content": { "description": "Team Sync makes it easier for you to manage users' access in Grafana, by immediately updating each user's Grafana teams and permissions based on their single sign-on group membership, instead of when users sign in"