Chore: Use UIDs as identifiers for teams frontend (#94345)

* Team frontend now uses UIDs as identifiers. Safe to revert
pull/94376/head
Jo 10 months ago committed by GitHub
parent 945dd052b1
commit 9eea0e99fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      .betterer.results
  2. 6
      pkg/services/team/teamapi/team.go
  3. 10
      public/api-enterprise-spec.json
  4. 3
      public/api-merged.json
  5. 2
      public/app/features/profile/UserProfileEditPage.test.tsx
  6. 4
      public/app/features/profile/state/reducers.test.ts
  7. 2
      public/app/features/teams/CreateTeam.tsx
  8. 2
      public/app/features/teams/TeamGroupSync.tsx
  9. 2
      public/app/features/teams/TeamList.test.tsx
  10. 6
      public/app/features/teams/TeamList.tsx
  11. 6
      public/app/features/teams/TeamPages.test.tsx
  12. 20
      public/app/features/teams/TeamPages.tsx
  13. 15
      public/app/features/teams/__mocks__/teamMocks.ts
  14. 30
      public/app/features/teams/state/actions.ts
  15. 16
      public/app/features/teams/state/navModel.ts
  16. 2
      public/app/features/teams/state/selectors.test.ts
  17. 4
      public/app/features/teams/state/selectors.ts
  18. 2
      public/app/routes/routes.tsx
  19. 3
      public/openapi3.json

@ -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"]
],

@ -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]
}

@ -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"
}
}
}

@ -23160,6 +23160,9 @@
"teamId": {
"type": "integer",
"format": "int64"
},
"uid": {
"type": "string"
}
}
}

@ -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',

@ -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 })],
});
});
});

@ -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}`);
}
};

@ -51,7 +51,7 @@ export class TeamGroupSync extends PureComponent<Props, State> {
}
async fetchTeamGroups() {
await this.props.loadTeamGroups();
this.props.loadTeamGroups();
}
onToggleAdding = () => {

@ -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);
});
});

@ -104,7 +104,7 @@ export const TeamList = ({
}
return (
<TextLink color="primary" inline={false} href={`/org/teams/edit/${original.id}`} title="Edit team">
<TextLink color="primary" inline={false} href={`/org/teams/edit/${original.uid}`} title="Edit team">
{value}
</TextLink>
);
@ -182,7 +182,7 @@ export const TeamList = ({
<Stack direction="row" justifyContent="flex-end" gap={2}>
{canReadTeam && (
<LinkButton
href={`org/teams/edit/${original.id}`}
href={`org/teams/edit/${original.uid}`}
aria-label={`Edit team ${original.name}`}
icon="pen"
size="sm"
@ -194,7 +194,7 @@ export const TeamList = ({
aria-label={`Delete team ${original.name}`}
size="sm"
disabled={!canDelete}
onConfirm={() => deleteTeam(original.id)}
onConfirm={() => deleteTeam(original.uid)}
/>
</Stack>
);

@ -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(<TeamPages />);
};

@ -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<TeamPageRouteParams>();
const team = useSelector((state) => teamSelector(state, teamId));
const { uid: teamUid = '', page } = useParams<TeamPageRouteParams>();
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];

@ -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 },

@ -60,17 +60,17 @@ export function loadTeams(initial = false): ThunkResult<void> {
const loadTeamsWithDebounce = debounce((dispatch) => dispatch(loadTeams()), 500);
export function loadTeam(id: string | number): ThunkResult<Promise<void>> {
export function loadTeam(uid: string): ThunkResult<Promise<void>> {
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<void> {
export function deleteTeam(uid: string): ThunkResult<void> {
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<Team>): ThunkResult<void> {
export function loadTeamMembers(): ThunkResult<void> {
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<void> {
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<void> {
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<void> {
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));
};
}

@ -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;

@ -15,7 +15,7 @@ describe('Team selectors', () => {
groups: [],
};
const team = getTeam(mockState, '1');
const team = getTeam(mockState, 'aaaaaa');
expect(team).toEqual(mockTeam);
});
});

@ -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;
}

@ -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')),

@ -445,6 +445,9 @@
"teamId": {
"format": "int64",
"type": "integer"
},
"uid": {
"type": "string"
}
},
"type": "object"

Loading…
Cancel
Save