diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx index 9aa5ca4af28..6957986c02e 100644 --- a/public/app/features/teams/TeamPages.test.tsx +++ b/public/app/features/teams/TeamPages.test.tsx @@ -1,14 +1,11 @@ -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import React from 'react'; -import { match } from 'react-router-dom'; -import { TestProvider } from 'test/helpers/TestProvider'; +import { Route, Router } from 'react-router-dom'; +import { render } from 'test/test-utils'; -import { createTheme } from '@grafana/data'; -import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; +import { locationService } from '@grafana/runtime'; -import { Team } from '../../types'; - -import { Props, TeamPages } from './TeamPages'; +import TeamPages from './TeamPages'; import { getMockTeam } from './__mocks__/teamMocks'; jest.mock('app/core/components/Select/UserPicker', () => { @@ -26,7 +23,7 @@ jest.mock('app/core/services/context_srv', () => ({ jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => ({ - get: jest.fn().mockResolvedValue([{ userId: 1, login: 'Test' }]), + get: jest.fn().mockResolvedValue(getMockTeam()), }), config: { ...jest.requireActual('@grafana/runtime').config, @@ -62,43 +59,24 @@ jest.mock('./TeamGroupSync', () => { return () =>
Team group sync
; }); -const setup = (propOverrides?: object) => { - const props: Props = { - ...getRouteComponentProps({ - match: { - params: { - id: '1', - page: null, - }, - } as unknown as match, - }), - pageNav: { text: 'Cool team ' }, - teamId: 1, - loadTeam: jest.fn(), - pageName: 'members', - team: {} as Team, - theme: createTheme(), - }; - - Object.assign(props, propOverrides); +const setup = (propOverrides: { teamId?: number; pageName?: string } = {}) => { + const pageName = propOverrides.pageName ?? 'members'; + const teamId = propOverrides.teamId ?? 1; + locationService.push({ pathname: `/org/teams/edit/${teamId}/${pageName}` }); render( - - - + + + + + ); }; describe('TeamPages', () => { it('should render settings and preferences page', async () => { setup({ - team: getMockTeam(), pageName: 'settings', - preferences: { - homeDashboardUID: 'home-dashboard', - theme: 'Default', - timezone: 'Default', - }, }); expect(await screen.findByText('Team settings')).toBeInTheDocument(); @@ -106,7 +84,6 @@ describe('TeamPages', () => { it('should render group sync page', async () => { setup({ - team: getMockTeam(), pageName: 'groupsync', }); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 240a113dda5..83bcaa76400 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -1,16 +1,15 @@ -import { includes } from 'lodash'; -import React, { PureComponent } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { createSelector } from '@reduxjs/toolkit'; +import React, { useMemo, useRef } from 'react'; +import { useParams } from 'react-router'; +import { useAsync } from 'react-use'; import { featureEnabled } from '@grafana/runtime'; -import { Themeable2, withTheme2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { UpgradeBox } from 'app/core/components/Upgrade/UpgradeBox'; import config from 'app/core/config'; -import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, StoreState } from 'app/types'; +import { AccessControlAction, StoreState, useDispatch, useSelector } from 'app/types'; import TeamGroupSync, { TeamSyncUpgradeContent } from './TeamGroupSync'; import TeamPermissions from './TeamPermissions'; @@ -21,14 +20,7 @@ import { getTeam } from './state/selectors'; interface TeamPageRouteParams { id: string; - page: string | null; -} - -export interface OwnProps extends GrafanaRouteComponentProps, Themeable2 {} - -interface State { - isSyncEnabled: boolean; - isLoading: boolean; + page?: string; } enum PageTypes { @@ -37,78 +29,44 @@ enum PageTypes { GroupSync = 'groupsync', } -function mapStateToProps(state: StoreState, props: OwnProps) { - const teamId = parseInt(props.match.params.id, 10); - const team = getTeam(state.team, teamId); +const PAGES = ['members', 'settings', 'groupsync']; + +const teamSelector = createSelector( + [(state: StoreState) => state.team, (_: StoreState, teamId: number) => teamId], + (team, teamId) => getTeam(team, teamId) +); + +const pageNavSelector = createSelector( + [ + (state: StoreState) => state.navIndex, + (_state: StoreState, pageName: string) => pageName, + (_state: StoreState, _pageName: string, teamId: number) => teamId, + ], + (navIndex, pageName, teamId) => { + const teamLoadingNav = getTeamLoadingNav(pageName); + return getNavModel(navIndex, `team-${pageName}-${teamId}`, teamLoadingNav).main; + } +); + +const TeamPages = React.memo(() => { + const isSyncEnabled = useRef(featureEnabled('teamsync')); + const params = useParams(); + const teamId = useMemo(() => parseInt(params.id, 10), [params]); + const team = useSelector((state) => teamSelector(state, teamId)); + let defaultPage = 'members'; // With RBAC the settings page will always be available if (!team || !contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsPermissionsRead, team)) { defaultPage = 'settings'; } - const pageName = props.match.params.page ?? defaultPage; - const teamLoadingNav = getTeamLoadingNav(pageName); - const pageNav = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav).main; + const pageName = params.page ?? defaultPage; + const pageNav = useSelector((state) => pageNavSelector(state, pageName, teamId)); - return { - pageNav, - teamId: teamId, - pageName: pageName, - team, - }; -} + const dispatch = useDispatch(); + const { loading: isLoading } = useAsync(async () => dispatch(loadTeam(teamId)), [teamId]); -const mapDispatchToProps = { - loadTeam, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); - -export type Props = OwnProps & ConnectedProps; - -export class TeamPages extends PureComponent { - constructor(props: Props) { - super(props); - - this.state = { - isLoading: false, - isSyncEnabled: featureEnabled('teamsync'), - }; - } - - async componentDidMount() { - await this.fetchTeam(); - } - - async fetchTeam() { - const { loadTeam, teamId } = this.props; - this.setState({ isLoading: true }); - const team = await loadTeam(teamId); - this.setState({ isLoading: false }); - return team; - } - - getCurrentPage() { - const pages = ['members', 'settings', 'groupsync']; - const currentPage = this.props.pageName; - return includes(pages, currentPage) ? currentPage : pages[0]; - } - - textsAreEqual = (text1: string, text2: string) => { - if (!text1 && !text2) { - return true; - } - - if (!text1 || !text2) { - return false; - } - - return text1.toLocaleLowerCase() === text2.toLocaleLowerCase(); - }; - - renderPage(): React.ReactNode { - const { isSyncEnabled } = this.state; - const { team } = this.props; - const currentPage = this.getCurrentPage(); + const renderPage = () => { + const currentPage = PAGES.includes(pageName) ? pageName : PAGES[0]; const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, team!); const canReadTeamPermissions = contextSrv.hasPermissionInMetadata( @@ -128,7 +86,7 @@ export class TeamPages extends PureComponent { case PageTypes.Settings: return canReadTeam && ; case PageTypes.GroupSync: - if (isSyncEnabled) { + if (isSyncEnabled.current) { if (canReadTeamPermissions) { return ; } @@ -143,19 +101,15 @@ export class TeamPages extends PureComponent { } return null; - } + }; - render() { - const { team, pageNav } = this.props; + return ( + + {team && Object.keys(team).length !== 0 && renderPage()} + + ); +}); - return ( - - - {team && Object.keys(team).length !== 0 && this.renderPage()} - - - ); - } -} +TeamPages.displayName = 'TeamPages'; -export default connector(withTheme2(TeamPages)); +export default TeamPages; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index caffdba46d3..fc745433f28 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -60,7 +60,7 @@ export function loadTeams(initial = false): ThunkResult { const loadTeamsWithDebounce = debounce((dispatch) => dispatch(loadTeams()), 500); -export function loadTeam(id: number): ThunkResult { +export function loadTeam(id: number): ThunkResult> { return async (dispatch) => { const response = await getBackendSrv().get(`/api/teams/${id}`, accessControlQueryParam()); dispatch(teamLoaded(response));