diff --git a/.betterer.results b/.betterer.results index 8bb613ae86d..3877333a6ce 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5224,9 +5224,6 @@ exports[`better eslint`] = { "public/app/features/teams/state/reducers.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/teams/state/selectors.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/templating/fieldAccessorCache.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/pkg/services/team/teamapi/team.go b/pkg/services/team/teamapi/team.go index 71d90504c38..a76bd28c3e9 100644 --- a/pkg/services/team/teamapi/team.go +++ b/pkg/services/team/teamapi/team.go @@ -59,6 +59,7 @@ func (tapi *TeamAPI) createTeam(c *contextmodel.ReqContext) response.Response { return response.JSON(http.StatusOK, &util.DynMap{ "teamId": t.ID, + "uid": t.UID, "message": "Team created", }) } @@ -230,7 +231,7 @@ func (tapi *TeamAPI) getTeamByID(c *contextmodel.ReqContext) response.Response { } // Add accesscontrol metadata - queryResult.AccessControl = tapi.getAccessControlMetadata(c, c.SignedInUser.GetOrgID(), "teams:id:", strconv.FormatInt(queryResult.ID, 10)) + queryResult.AccessControl = tapi.getAccessControlMetadata(c, "teams:id:", strconv.FormatInt(queryResult.ID, 10)) queryResult.AvatarURL = dtos.GetGravatarUrlWithDefault(tapi.cfg, queryResult.Email, queryResult.Name) return response.JSON(http.StatusOK, &queryResult) @@ -362,6 +363,7 @@ type CreateTeamResponse struct { // in: body Body struct { TeamId int64 `json:"teamId"` + Uid string `json:"uid"` Message string `json:"message"` } `json:"body"` } @@ -384,7 +386,7 @@ func (tapi *TeamAPI) getMultiAccessControlMetadata(c *contextmodel.ReqContext, // Metadata helpers // getAccessControlMetadata returns the accesscontrol metadata associated with a given resource func (tapi *TeamAPI) getAccessControlMetadata(c *contextmodel.ReqContext, - orgID int64, prefix string, resourceID string) accesscontrol.Metadata { + prefix string, resourceID string) accesscontrol.Metadata { ids := map[string]bool{resourceID: true} return tapi.getMultiAccessControlMetadata(c, prefix, ids)[resourceID] } diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 815f7deac31..f4d013234d2 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -5442,7 +5442,12 @@ "DASHBOARD", "DATASOURCE", "FOLDER", - "LIBRARY_ELEMENT" + "LIBRARY_ELEMENT", + "ALERT_RULE", + "CONTACT_POINT", + "NOTIFICATION_POLICY", + "NOTIFICATION_TEMPLATE", + "MUTE_TIMING" ] } } @@ -8819,6 +8824,9 @@ "teamId": { "type": "integer", "format": "int64" + }, + "uid": { + "type": "string" } } } diff --git a/public/api-merged.json b/public/api-merged.json index 82a9f456c24..4905b5aef68 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -23160,6 +23160,9 @@ "teamId": { "type": "integer", "format": "int64" + }, + "uid": { + "type": "string" } } } diff --git a/public/app/features/profile/UserProfileEditPage.test.tsx b/public/app/features/profile/UserProfileEditPage.test.tsx index a8e27dcd80d..a33dae5a4da 100644 --- a/public/app/features/profile/UserProfileEditPage.test.tsx +++ b/public/app/features/profile/UserProfileEditPage.test.tsx @@ -39,7 +39,7 @@ const defaultProps: Props = { orgId: 0, }, teams: [ - getMockTeam(0, { + getMockTeam(0, 'aaaaaa', { name: 'Team One', email: 'team.one@test.com', avatarUrl: '/avatar/07d881f402480a2a511a9a15b5fa82c0', diff --git a/public/app/features/profile/state/reducers.test.ts b/public/app/features/profile/state/reducers.test.ts index 2429a287a16..0c004377328 100644 --- a/public/app/features/profile/state/reducers.test.ts +++ b/public/app/features/profile/state/reducers.test.ts @@ -90,13 +90,13 @@ describe('userReducer', () => { .givenReducer(userReducer, { ...initialUserState, teamsAreLoading: true }) .whenActionIsDispatched( teamsLoaded({ - teams: [getMockTeam(1, { permission: TeamPermissionLevel.Admin })], + teams: [getMockTeam(1, 'aaaaaa', { permission: TeamPermissionLevel.Admin })], }) ) .thenStateShouldEqual({ ...initialUserState, teamsAreLoading: false, - teams: [getMockTeam(1, { permission: TeamPermissionLevel.Admin })], + teams: [getMockTeam(1, 'aaaaaa', { permission: TeamPermissionLevel.Admin })], }); }); }); diff --git a/public/app/features/teams/CreateTeam.tsx b/public/app/features/teams/CreateTeam.tsx index bd77f098fd5..bba646823e0 100644 --- a/public/app/features/teams/CreateTeam.tsx +++ b/public/app/features/teams/CreateTeam.tsx @@ -42,7 +42,7 @@ export const CreateTeam = (): JSX.Element => { } catch (e) { console.error(e); } - locationService.push(`/org/teams/edit/${newTeam.teamId}`); + locationService.push(`/org/teams/edit/${newTeam.uid}`); } }; diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index 64dd5b5e3ec..5acc2975f90 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -51,7 +51,7 @@ export class TeamGroupSync extends PureComponent { } async fetchTeamGroups() { - await this.props.loadTeamGroups(); + this.props.loadTeamGroups(); } onToggleAdding = () => { diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 8f444095424..abbfff37583 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -83,6 +83,6 @@ it('should call delete team', async () => { 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.id); + expect(mockDelete).toHaveBeenCalledWith(mockTeam.uid); }); }); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index a96bd633fd0..35dd58c3355 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -104,7 +104,7 @@ export const TeamList = ({ } return ( - + {value} ); @@ -182,7 +182,7 @@ export const TeamList = ({ {canReadTeam && ( deleteTeam(original.id)} + onConfirm={() => deleteTeam(original.uid)} /> ); diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx index 9c3de64e177..f3a83495df5 100644 --- a/public/app/features/teams/TeamPages.test.tsx +++ b/public/app/features/teams/TeamPages.test.tsx @@ -61,10 +61,10 @@ jest.mock('react-router-dom-v5-compat', () => ({ useParams: jest.fn(), })); -const setup = (propOverrides: { teamId?: number; pageName?: string } = {}) => { +const setup = (propOverrides: { teamUid?: string; pageName?: string } = {}) => { const pageName = propOverrides.pageName ?? 'members'; - const teamId = propOverrides.teamId ?? 1; - (useParams as jest.Mock).mockReturnValue({ id: `${teamId}`, page: pageName }); + const teamUid = propOverrides.teamUid ?? 'aaaaaa'; + (useParams as jest.Mock).mockReturnValue({ uid: `${teamUid}`, page: pageName }); render(); }; diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 8004f19f683..940f6950d1a 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -19,7 +19,7 @@ import { getTeamLoadingNav } from './state/navModel'; import { getTeam } from './state/selectors'; type TeamPageRouteParams = { - id: string; + uid: string; page?: string; }; @@ -32,26 +32,26 @@ enum PageTypes { const PAGES = ['members', 'settings', 'groupsync']; const teamSelector = createSelector( - [(state: StoreState) => state.team, (_: StoreState, teamId: string) => teamId], - (team, teamId) => getTeam(team, teamId) + [(state: StoreState) => state.team, (_: StoreState, teamUid: string) => teamUid], + (team, teamUid) => getTeam(team, teamUid) ); const pageNavSelector = createSelector( [ (state: StoreState) => state.navIndex, (_state: StoreState, pageName: string) => pageName, - (_state: StoreState, _pageName: string, teamId: string) => teamId, + (_state: StoreState, _pageName: string, teamUid: string) => teamUid, ], - (navIndex, pageName, teamId) => { + (navIndex, pageName, teamUid) => { const teamLoadingNav = getTeamLoadingNav(pageName); - return getNavModel(navIndex, `team-${pageName}-${teamId}`, teamLoadingNav).main; + return getNavModel(navIndex, `team-${pageName}-${teamUid}`, teamLoadingNav).main; } ); const TeamPages = memo(() => { const isSyncEnabled = useRef(featureEnabled('teamsync')); - const { id: teamId = '', page } = useParams(); - const team = useSelector((state) => teamSelector(state, teamId)); + const { uid: teamUid = '', page } = useParams(); + const team = useSelector((state) => teamSelector(state, teamUid)); let defaultPage = 'members'; // With RBAC the settings page will always be available @@ -59,10 +59,10 @@ const TeamPages = memo(() => { defaultPage = 'settings'; } const pageName = page ?? defaultPage; - const pageNav = useSelector((state) => pageNavSelector(state, pageName, teamId)); + const pageNav = useSelector((state) => pageNavSelector(state, pageName, teamUid)); const dispatch = useDispatch(); - const { loading: isLoading } = useAsync(async () => dispatch(loadTeam(teamId)), [teamId]); + const { loading: isLoading } = useAsync(async () => dispatch(loadTeam(teamUid)), [teamUid]); const renderPage = () => { const currentPage = PAGES.includes(pageName) ? pageName : PAGES[0]; diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 0ee357c4daa..5cfbe77a143 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -1,5 +1,11 @@ +import { randomBytes } from 'crypto'; + import { Team, TeamGroup, TeamMember, TeamPermissionLevel } from 'app/types'; +function generateShortUid(): string { + return randomBytes(3).toString('hex'); // Generate a short UID +} + export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { const teams: Team[] = []; for (let i = 1; i <= numberOfTeams; i++) { @@ -9,13 +15,14 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { return teams; }; -export const getMockTeam = (i = 1, overrides = {}): Team => { +export const getMockTeam = (i = 1, uid = 'aaaaaa', overrides = {}): Team => { + uid = uid || generateShortUid(); return { id: i, - uid: '', - name: `test-${i}`, + uid: uid, + name: `test-${uid}`, avatarUrl: 'some/url/', - email: `test-${i}@test.com`, + email: `test-${uid}@test.com`, memberCount: i, permission: TeamPermissionLevel.Member, accessControl: { isEditor: false }, diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 81a1986af61..38f3a110a97 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -60,17 +60,17 @@ export function loadTeams(initial = false): ThunkResult { const loadTeamsWithDebounce = debounce((dispatch) => dispatch(loadTeams()), 500); -export function loadTeam(id: string | number): ThunkResult> { +export function loadTeam(uid: string): ThunkResult> { return async (dispatch) => { - const response = await getBackendSrv().get(`/api/teams/${id}`, accessControlQueryParam()); + const response = await getBackendSrv().get(`/api/teams/${uid}`, accessControlQueryParam()); dispatch(teamLoaded(response)); dispatch(updateNavIndex(buildNavModel(response))); }; } -export function deleteTeam(id: number): ThunkResult { +export function deleteTeam(uid: string): ThunkResult { return async (dispatch) => { - await getBackendSrv().delete(`/api/teams/${id}`); + await getBackendSrv().delete(`/api/teams/${uid}`); // Update users permissions in case they lost teams.read with the deletion await contextSrv.fetchUserPermissions(); dispatch(loadTeams()); @@ -102,32 +102,16 @@ 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.id}/members`); + const response = await getBackendSrv().get(`/api/teams/${team.uid}/members`); dispatch(teamMembersLoaded(response)); }; } -export function addTeamMember(id: number): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - await getBackendSrv().post(`/api/teams/${team.id}/members`, { userId: id }); - dispatch(loadTeamMembers()); - }; -} - -export function removeTeamMember(id: number): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - await getBackendSrv().delete(`/api/teams/${team.id}/members/${id}`); - dispatch(loadTeamMembers()); - }; -} - export function updateTeam(name: string, email: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - await getBackendSrv().put(`/api/teams/${team.id}`, { name, email }); - dispatch(loadTeam(team.id)); + await getBackendSrv().put(`/api/teams/${team.uid}`, { name, email }); + dispatch(loadTeam(team.uid)); }; } diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts index 8969845b0e9..5205e8aac98 100644 --- a/public/app/features/teams/state/navModel.ts +++ b/public/app/features/teams/state/navModel.ts @@ -22,9 +22,9 @@ const loadingTeam = { export function buildNavModel(team: Team): NavModelItem { const navModel: NavModelItem = { img: team.avatarUrl, - id: 'team-' + team.id, + id: 'team-' + team.uid, subTitle: 'Manage members and settings', - url: `org/teams/edit/${team.id}`, + url: `org/teams/edit/${team.uid}`, text: team.name, children: [ // With RBAC this tab will always be available (but not always editable) @@ -32,9 +32,9 @@ export function buildNavModel(team: Team): NavModelItem { { active: false, icon: 'sliders-v-alt', - id: `team-settings-${team.id}`, + id: `team-settings-${team.uid}`, text: 'Settings', - url: `org/teams/edit/${team.id}/settings`, + url: `org/teams/edit/${team.uid}/settings`, }, ], }; @@ -49,18 +49,18 @@ export function buildNavModel(team: Team): NavModelItem { navModel.children!.unshift({ active: false, icon: 'users-alt', - id: `team-members-${team.id}`, + id: `team-members-${team.uid}`, text: 'Members', - url: `org/teams/edit/${team.id}/members`, + url: `org/teams/edit/${team.uid}/members`, }); } const teamGroupSync: NavModelItem = { active: false, icon: 'sync', - id: `team-groupsync-${team.id}`, + id: `team-groupsync-${team.uid}`, text: 'External group sync', - url: `org/teams/edit/${team.id}/groupsync`, + url: `org/teams/edit/${team.uid}/groupsync`, }; const isLoadingTeam = team === loadingTeam; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index ee7664586c5..644dad1c867 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -15,7 +15,7 @@ describe('Team selectors', () => { groups: [], }; - const team = getTeam(mockState, '1'); + const team = getTeam(mockState, 'aaaaaa'); expect(team).toEqual(mockTeam); }); }); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 5caa33469f1..ec8e6eddfc4 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -2,8 +2,8 @@ import { Team, TeamState } from 'app/types'; export const getTeamGroups = (state: TeamState) => state.groups; -export const getTeam = (state: TeamState, currentTeamId: any): Team | null => { - if (state.team.id === parseInt(currentTeamId, 10)) { +export const getTeam = (state: TeamState, currentTeamUid: string): Team | null => { + if (state.team.uid === currentTeamUid) { return state.team; } diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index b496c73550c..405c76ee35f 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -269,7 +269,7 @@ export function getAppRoutes(): RouteDescriptor[] { component: SafeDynamicImport(() => import(/* webpackChunkName: "CreateTeam" */ 'app/features/teams/CreateTeam')), }, { - path: '/org/teams/edit/:id/:page?', + path: '/org/teams/edit/:uid/:page?', roles: () => contextSrv.evaluatePermission([AccessControlAction.ActionTeamsRead, AccessControlAction.ActionTeamsCreate]), component: SafeDynamicImport(() => import(/* webpackChunkName: "TeamPages" */ 'app/features/teams/TeamPages')), diff --git a/public/openapi3.json b/public/openapi3.json index 0f796668d00..9043bae330a 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -445,6 +445,9 @@ "teamId": { "format": "int64", "type": "integer" + }, + "uid": { + "type": "string" } }, "type": "object"