mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Use UIDs as identifiers for teams frontend (#94345)
* Team frontend now uses UIDs as identifiers. Safe to revert
This commit is contained in:
parent
945dd052b1
commit
9eea0e99fc
@ -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…
Reference in New Issue
Block a user