diff --git a/.betterer.results b/.betterer.results index 41d23da9f1c..8d5def88db4 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1377,8 +1377,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "3"], [0, 0, 0, "Styles should be written using objects.", "4"], [0, 0, 0, "Styles should be written using objects.", "5"], - [0, 0, 0, "Styles should be written using objects.", "6"], - [0, 0, 0, "Styles should be written using objects.", "7"] + [0, 0, 0, "Styles should be written using objects.", "6"] ], "public/app/core/components/RolePicker/RolePickerMenu.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] diff --git a/public/app/core/components/RolePicker/RolePicker.tsx b/public/app/core/components/RolePicker/RolePicker.tsx index 41592ae22a0..2cd858380b5 100644 --- a/public/app/core/components/RolePicker/RolePicker.tsx +++ b/public/app/core/components/RolePicker/RolePicker.tsx @@ -1,12 +1,11 @@ import React, { FormEvent, useCallback, useEffect, useState, useRef } from 'react'; -import { ClickOutsideWrapper, Spinner, useStyles2, useTheme2 } from '@grafana/ui'; +import { ClickOutsideWrapper, useTheme2 } from '@grafana/ui'; import { Role, OrgRole } from 'app/types'; import { RolePickerInput } from './RolePickerInput'; import { RolePickerMenu } from './RolePickerMenu'; import { MENU_MAX_HEIGHT, ROLE_PICKER_SUBMENU_MIN_WIDTH, ROLE_PICKER_WIDTH } from './constants'; -import { getStyles } from './styles'; export interface Props { basicRole?: OrgRole; @@ -50,7 +49,6 @@ export const RolePicker = ({ const [query, setQuery] = useState(''); const [offset, setOffset] = useState({ vertical: 0, horizontal: 0 }); const ref = useRef(null); - const styles = useStyles2(getStyles); const theme = useTheme2(); const widthPx = typeof width === 'number' ? theme.spacing(width) : width; @@ -152,15 +150,6 @@ export const RolePicker = ({ return options; }; - if (isLoading) { - return ( -
- Loading... - -
- ); - } - return (
{isOpen && ( { isFocused?: boolean; disabled?: boolean; width?: string; + isLoading?: boolean; onQueryChange: (query?: string) => void; onOpen: (event: FormEvent) => void; onClose: () => void; @@ -32,6 +33,7 @@ export const RolePickerInput = ({ query, showBasicRole, width, + isLoading, onOpen, onClose, onQueryChange, @@ -63,6 +65,11 @@ export const RolePickerInput = ({ numberOfRoles={appliedRoles.length} showBuiltInRole={showBasicRoleOnLabel} /> + {isLoading && ( +
+ +
+ )}
) : (
@@ -141,22 +148,22 @@ const getRolePickerInputStyles = ( ${styleMixins.focusCss(theme.v1)} `, disabled && styles.inputDisabled, - css` - min-width: ${width || ROLE_PICKER_WIDTH + 'px'}; - width: ${width}; - min-height: 32px; - height: auto; - flex-direction: row; - padding-right: 24px; - max-width: 100%; - align-items: center; - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - position: relative; - box-sizing: border-box; - cursor: default; - `, + css({ + minWidth: width || ROLE_PICKER_WIDTH + 'px', + width: width, + minHeight: '32px', + height: 'auto', + flexDirection: 'row', + paddingRight: theme.spacing(1), + maxWidth: '100%', + alignItems: 'center', + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'flex-start', + position: 'relative', + boxSizing: 'border-box', + cursor: 'default', + }), withPrefix && css` padding-left: 0; @@ -184,6 +191,11 @@ const getRolePickerInputStyles = ( margin-bottom: ${theme.spacing(0.5)}; } `, + spinner: css({ + display: 'flex', + flexGrow: 1, + justifyContent: 'flex-end', + }), }; }; diff --git a/public/app/core/components/RolePicker/TeamRolePicker.tsx b/public/app/core/components/RolePicker/TeamRolePicker.tsx index 98d292da3cf..c380a120944 100644 --- a/public/app/core/components/RolePicker/TeamRolePicker.tsx +++ b/public/app/core/components/RolePicker/TeamRolePicker.tsx @@ -12,6 +12,7 @@ export interface Props { orgId?: number; roleOptions: Role[]; disabled?: boolean; + roles?: Role[]; onApplyRoles?: (newRoles: Role[]) => void; pendingRoles?: Role[]; /** @@ -28,20 +29,26 @@ export interface Props { apply?: boolean; maxWidth?: string | number; width?: string | number; + isLoading?: boolean; } export const TeamRolePicker = ({ teamId, roleOptions, disabled, + roles, onApplyRoles, pendingRoles, apply = false, maxWidth, width, + isLoading, }: Props) => { - const [{ loading, value: appliedRoles = [] }, getTeamRoles] = useAsyncFn(async () => { + const [{ loading, value: appliedRoles = roles || [] }, getTeamRoles] = useAsyncFn(async () => { try { + if (roles) { + return roles; + } if (apply && Boolean(pendingRoles?.length)) { return pendingRoles; } @@ -53,11 +60,11 @@ export const TeamRolePicker = ({ console.error('Error loading options', e); } return []; - }, [teamId, pendingRoles]); + }, [teamId, pendingRoles, roles]); useEffect(() => { getTeamRoles(); - }, [teamId, getTeamRoles, pendingRoles]); + }, [getTeamRoles]); const onRolesChange = async (roles: Role[]) => { if (!apply) { @@ -78,7 +85,7 @@ export const TeamRolePicker = ({ onRolesChange={onRolesChange} roleOptions={roleOptions} appliedRoles={appliedRoles} - isLoading={loading} + isLoading={loading || isLoading} disabled={disabled} basicRoleDisabled={true} canUpdateRoles={canUpdateRoles} diff --git a/public/app/core/components/RolePicker/UserRolePicker.tsx b/public/app/core/components/RolePicker/UserRolePicker.tsx index ca7cecdc07b..56834457191 100644 --- a/public/app/core/components/RolePicker/UserRolePicker.tsx +++ b/public/app/core/components/RolePicker/UserRolePicker.tsx @@ -9,6 +9,7 @@ import { fetchUserRoles, updateUserRoles } from './api'; export interface Props { basicRole: OrgRole; + roles?: Role[]; userId: number; orgId?: number; onBasicRoleChange: (newRole: OrgRole) => void; @@ -32,10 +33,12 @@ export interface Props { pendingRoles?: Role[]; maxWidth?: string | number; width?: string | number; + isLoading?: boolean; } export const UserRolePicker = ({ basicRole, + roles, userId, orgId, onBasicRoleChange, @@ -48,9 +51,13 @@ export const UserRolePicker = ({ pendingRoles, maxWidth, width, + isLoading, }: Props) => { - const [{ loading, value: appliedRoles = [] }, getUserRoles] = useAsyncFn(async () => { + const [{ loading, value: appliedRoles = roles || [] }, getUserRoles] = useAsyncFn(async () => { try { + if (roles) { + return roles; + } if (apply && Boolean(pendingRoles?.length)) { return pendingRoles; } @@ -63,14 +70,14 @@ export const UserRolePicker = ({ console.error('Error loading options'); } return []; - }, [orgId, userId, pendingRoles]); + }, [orgId, userId, pendingRoles, roles]); useEffect(() => { // only load roles when there is an Org selected if (orgId) { getUserRoles(); } - }, [orgId, getUserRoles, pendingRoles]); + }, [getUserRoles, orgId]); const onRolesChange = async (roles: Role[]) => { if (!apply) { @@ -92,7 +99,7 @@ export const UserRolePicker = ({ onRolesChange={onRolesChange} onBasicRoleChange={onBasicRoleChange} roleOptions={roleOptions} - isLoading={loading} + isLoading={loading || isLoading} disabled={disabled} basicRoleDisabled={basicRoleDisabled} basicRoleDisabledMessage={basicRoleDisabledMessage} diff --git a/public/app/features/admin/AdminEditOrgPage.tsx b/public/app/features/admin/AdminEditOrgPage.tsx index 7c4400acc66..e1cab49a294 100644 --- a/public/app/features/admin/AdminEditOrgPage.tsx +++ b/public/app/features/admin/AdminEditOrgPage.tsx @@ -1,42 +1,20 @@ import React, { useState, useEffect } from 'react'; import { useAsyncFn } from 'react-use'; -import { NavModelItem, UrlQueryValue } from '@grafana/data'; -import { getBackendSrv } from '@grafana/runtime'; +import { NavModelItem } from '@grafana/data'; import { Form, Field, Input, Button, Legend, Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { accessControlQueryParam } from 'app/core/utils/accessControl'; import { OrgUser, AccessControlAction, OrgRole } from 'app/types'; import { OrgUsersTable } from './Users/OrgUsersTable'; - -const perPage = 30; +import { getOrg, getOrgUsers, getUsersRoles, removeOrgUser, updateOrgName, updateOrgUserRole } from './api'; interface OrgNameDTO { orgName: string; } -const getOrg = async (orgId: UrlQueryValue) => { - return await getBackendSrv().get(`/api/orgs/${orgId}`); -}; - -const getOrgUsers = async (orgId: UrlQueryValue, page: number) => { - if (contextSrv.hasPermission(AccessControlAction.OrgUsersRead)) { - return getBackendSrv().get(`/api/orgs/${orgId}/users/search`, accessControlQueryParam({ perpage: perPage, page })); - } - return { orgUsers: [] }; -}; - -const updateOrgUserRole = (orgUser: OrgUser, orgId: UrlQueryValue) => { - return getBackendSrv().patch(`/api/orgs/${orgId}/users/${orgUser.userId}`, orgUser); -}; - -const removeOrgUser = (orgUser: OrgUser, orgId: UrlQueryValue) => { - return getBackendSrv().delete(`/api/orgs/${orgId}/users/${orgUser.userId}`); -}; - interface Props extends GrafanaRouteComponentProps<{ id: string }> {} const AdminEditOrgPage = ({ match }: Props) => { @@ -51,6 +29,11 @@ const AdminEditOrgPage = ({ match }: Props) => { const [orgState, fetchOrg] = useAsyncFn(() => getOrg(orgId), []); const [, fetchOrgUsers] = useAsyncFn(async (page) => { const result = await getOrgUsers(orgId, page); + + if (contextSrv.licensedAccessControlEnabled()) { + await getUsersRoles(orgId, result.orgUsers); + } + const totalPages = result?.perPage !== 0 ? Math.ceil(result.totalCount / result.perPage) : 0; setTotalPages(totalPages); setUsers(result.orgUsers); @@ -62,8 +45,8 @@ const AdminEditOrgPage = ({ match }: Props) => { fetchOrgUsers(page); }, [fetchOrg, fetchOrgUsers, page]); - const updateOrgName = async (name: string) => { - return await getBackendSrv().put(`/api/orgs/${orgId}`, { ...orgState.value, name }); + const onUpdateOrgName = async (name: string) => { + await updateOrgName(name, orgId); }; const renderMissingPermissionMessage = () => ( @@ -101,7 +84,7 @@ const AdminEditOrgPage = ({ match }: Props) => { {orgState.value && (
updateOrgName(values.orgName)} + onSubmit={(values: OrgNameDTO) => onUpdateOrgName(values.orgName)} > {({ register, errors }) => ( <> diff --git a/public/app/features/admin/Users/OrgUsersTable.tsx b/public/app/features/admin/Users/OrgUsersTable.tsx index 7fec328d127..0e3df7d60a7 100644 --- a/public/app/features/admin/Users/OrgUsersTable.tsx +++ b/public/app/features/admin/Users/OrgUsersTable.tsx @@ -57,6 +57,7 @@ export interface Props { changePage: (page: number) => void; page: number; totalPages: number; + rolesLoading?: boolean; } export const OrgUsersTable = ({ @@ -68,6 +69,7 @@ export const OrgUsersTable = ({ changePage, page, totalPages, + rolesLoading, }: Props) => { const [userToRemove, setUserToRemove] = useState(null); const [roleOptions, setRoleOptions] = useState([]); @@ -127,6 +129,8 @@ export const OrgUsersTable = ({ return contextSrv.licensedAccessControlEnabled() ? ( { + return await getBackendSrv().get(`/api/orgs/${orgId}`); +}; + +export const getOrgUsers = async (orgId: UrlQueryValue, page: number) => { + if (contextSrv.hasPermission(AccessControlAction.OrgUsersRead)) { + return getBackendSrv().get(`/api/orgs/${orgId}/users/search`, accessControlQueryParam({ perpage: perPage, page })); + } + return { orgUsers: [] }; +}; + +export const getUsersRoles = async (orgId: number, users: OrgUser[]) => { + const userIds = users.map((u) => u.userId); + const roles = await getBackendSrv().post(`/api/access-control/users/roles/search`, { userIds, orgId }); + users.forEach((u) => { + u.roles = roles ? roles[u.userId] || [] : []; + }); +}; + +export const updateOrgUserRole = (orgUser: OrgUser, orgId: UrlQueryValue) => { + return getBackendSrv().patch(`/api/orgs/${orgId}/users/${orgUser.userId}`, orgUser); +}; + +export const removeOrgUser = (orgUser: OrgUser, orgId: UrlQueryValue) => { + return getBackendSrv().delete(`/api/orgs/${orgId}/users/${orgUser.userId}`); +}; + +export const updateOrgName = (name: string, orgId: number) => { + return getBackendSrv().put(`/api/orgs/${orgId}`, { name }); +}; diff --git a/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx b/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx index 2d10943a4d5..f8677e2c100 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx @@ -77,6 +77,7 @@ const ServiceAccountListItem = memo( userId={serviceAccount.id} orgId={serviceAccount.orgId} basicRole={serviceAccount.role} + roles={serviceAccount.roles || []} onBasicRoleChange={(newRole) => onRoleChange(newRole, serviceAccount)} roleOptions={roleOptions} basicRoleDisabled={!canUpdateRole} diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index 7409beccba5..9a9433a24f1 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -11,6 +11,8 @@ import { acOptionsLoaded, pageChanged, queryChanged, + rolesFetchBegin, + rolesFetchEnd, serviceAccountsFetchBegin, serviceAccountsFetched, serviceAccountsFetchEnd, @@ -51,6 +53,18 @@ export function fetchServiceAccounts( serviceAccountStateFilter )}&accesscontrol=true` ); + + if (contextSrv.licensedAccessControlEnabled()) { + dispatch(rolesFetchBegin()); + const orgId = contextSrv.user.orgId; + const userIds = result?.serviceAccounts.map((u: ServiceAccountDTO) => u.id); + const roles = await getBackendSrv().post(`/api/access-control/users/roles/search`, { userIds, orgId }); + result.serviceAccounts.forEach((u: ServiceAccountDTO) => { + u.roles = roles ? roles[u.id] || [] : []; + }); + dispatch(rolesFetchEnd()); + } + dispatch(serviceAccountsFetched(result)); } } catch (error) { diff --git a/public/app/features/serviceaccounts/state/reducers.ts b/public/app/features/serviceaccounts/state/reducers.ts index 0286d295cc9..5794233d434 100644 --- a/public/app/features/serviceaccounts/state/reducers.ts +++ b/public/app/features/serviceaccounts/state/reducers.ts @@ -32,12 +32,24 @@ export const serviceAccountProfileSlice = createSlice({ serviceAccountTokensLoaded: (state, action: PayloadAction): ServiceAccountProfileState => { return { ...state, tokens: action.payload, isLoading: false }; }, + rolesFetchBegin: (state) => { + return { ...state, rolesLoading: true }; + }, + rolesFetchEnd: (state) => { + return { ...state, rolesLoading: false }; + }, }, }); export const serviceAccountProfileReducer = serviceAccountProfileSlice.reducer; -export const { serviceAccountLoaded, serviceAccountTokensLoaded, serviceAccountFetchBegin, serviceAccountFetchEnd } = - serviceAccountProfileSlice.actions; +export const { + serviceAccountLoaded, + serviceAccountTokensLoaded, + serviceAccountFetchBegin, + serviceAccountFetchEnd, + rolesFetchBegin, + rolesFetchEnd, +} = serviceAccountProfileSlice.actions; // serviceAccountsListPage export const initialStateList: ServiceAccountsState = { diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 76dd014644a..c9f4dc4b686 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -31,6 +31,7 @@ const setup = (propOverrides?: object) => { page: 0, hasFetched: false, perPage: 10, + rolesLoading: false, }; Object.assign(props, propOverrides); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 10be5c44637..bd762baf1d0 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -43,6 +43,7 @@ export const TeamList = ({ changeQuery, totalPages, page, + rolesLoading, changePage, changeSort, }: Props) => { @@ -96,7 +97,17 @@ export const TeamList = ({ AccessControlAction.ActionTeamsRolesList, original ); - return canSeeTeamRoles && ; + return ( + canSeeTeamRoles && ( + + ) + ); }, }, ] @@ -132,7 +143,7 @@ export const TeamList = ({ }, }, ], - [displayRolePicker, roleOptions, deleteTeam] + [displayRolePicker, rolesLoading, roleOptions, deleteTeam] ); return ( @@ -203,6 +214,7 @@ function mapStateToProps(state: StoreState) { noTeams: state.teams.noTeams, totalPages: state.teams.totalPages, hasFetched: state.teams.hasFetched, + rolesLoading: state.teams.rolesLoading, }; } diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 3d28044b3eb..d4f5ec470aa 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -16,6 +16,8 @@ import { teamMembersLoaded, teamsLoaded, sortChanged, + rolesFetchBegin, + rolesFetchEnd, } from './reducers'; export function loadTeams(initial = false): ThunkResult { @@ -39,6 +41,16 @@ export function loadTeams(initial = false): ThunkResult { noTeams = response.teams.length === 0; } + if (contextSrv.licensedAccessControlEnabled()) { + dispatch(rolesFetchBegin()); + const teamIds = response?.teams.map((t: Team) => t.id); + const roles = await getBackendSrv().post(`/api/access-control/teams/roles/search`, { teamIds }); + response.teams.forEach((t: Team) => { + t.roles = roles ? roles[t.id] || [] : []; + }); + dispatch(rolesFetchEnd()); + } + dispatch(teamsLoaded({ noTeams, ...response })); }; } diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index a854ddc1bd8..4602f33540c 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -38,10 +38,17 @@ const teamsSlice = createSlice({ sortChanged: (state, action: PayloadAction): TeamsState => { return { ...state, sort: action.payload, page: 1 }; }, + rolesFetchBegin: (state) => { + return { ...state, rolesLoading: true }; + }, + rolesFetchEnd: (state) => { + return { ...state, rolesLoading: false }; + }, }, }); -export const { teamsLoaded, queryChanged, pageChanged, sortChanged } = teamsSlice.actions; +export const { teamsLoaded, queryChanged, pageChanged, sortChanged, rolesFetchBegin, rolesFetchEnd } = + teamsSlice.actions; export const teamsReducer = teamsSlice.reducer; diff --git a/public/app/features/users/UsersListPage.test.tsx b/public/app/features/users/UsersListPage.test.tsx index 6b1e231166b..64b286a417a 100644 --- a/public/app/features/users/UsersListPage.test.tsx +++ b/public/app/features/users/UsersListPage.test.tsx @@ -38,6 +38,7 @@ const setup = (propOverrides?: object) => { changePage: mockToolkitActionCreator(pageChanged), changeSort: mockToolkitActionCreator(sortChanged), isLoading: false, + rolesLoading: false, }; Object.assign(props, propOverrides); diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx index 9fbf57a50b8..2664d689ba1 100644 --- a/public/app/features/users/UsersListPage.tsx +++ b/public/app/features/users/UsersListPage.tsx @@ -26,6 +26,7 @@ function mapStateToProps(state: StoreState) { invitees: selectInvitesMatchingQuery(state.invites, searchQuery), externalUserMngInfo: state.users.externalUserMngInfo, isLoading: state.users.isLoading, + rolesLoading: state.users.rolesLoading, }; } @@ -53,6 +54,7 @@ export const UsersListPageUnconnected = ({ invitees, externalUserMngInfo, isLoading, + rolesLoading, loadUsers, fetchInvitees, changePage, @@ -86,6 +88,7 @@ export const UsersListPageUnconnected = ({ { return async (dispatch, getState) => { try { + dispatch(usersFetchBegin()); const { perPage, page, searchQuery, sort } = getState().users; const users = await getBackendSrv().get( `/api/org/users/search`, accessControlQueryParam({ perpage: perPage, page, query: searchQuery, sort }) ); + + if (contextSrv.licensedAccessControlEnabled()) { + dispatch(rolesFetchBegin()); + const orgId = contextSrv.user.orgId; + const userIds = users?.orgUsers.map((u: OrgUser) => u.userId); + const roles = await getBackendSrv().post(`/api/access-control/users/roles/search`, { userIds, orgId }); + users.orgUsers.forEach((u: OrgUser) => { + u.roles = roles ? roles[u.userId] || [] : []; + }); + dispatch(rolesFetchEnd()); + } dispatch(usersLoaded(users)); } catch (error) { usersFetchEnd(); @@ -42,7 +64,6 @@ export function removeUser(userId: number): ThunkResult { export function changePage(page: number): ThunkResult { return async (dispatch) => { - dispatch(usersFetchBegin()); dispatch(pageChanged(page)); dispatch(loadUsers()); }; @@ -51,7 +72,6 @@ export function changePage(page: number): ThunkResult { export function changeSort({ sortBy }: FetchDataArgs): ThunkResult { const sort = sortBy.length ? `${sortBy[0].id}-${sortBy[0].desc ? 'desc' : 'asc'}` : undefined; return async (dispatch) => { - dispatch(usersFetchBegin()); dispatch(sortChanged(sort)); dispatch(loadUsers()); }; @@ -59,7 +79,6 @@ export function changeSort({ sortBy }: FetchDataArgs): ThunkResult { return async (dispatch) => { - dispatch(usersFetchBegin()); dispatch(searchQueryChanged(query)); fetchUsersWithDebounce(dispatch); }; diff --git a/public/app/features/users/state/reducers.ts b/public/app/features/users/state/reducers.ts index 8b1a987a96e..5d9b576b9a0 100644 --- a/public/app/features/users/state/reducers.ts +++ b/public/app/features/users/state/reducers.ts @@ -13,6 +13,7 @@ export const initialState: UsersState = { externalUserMngLinkName: config.externalUserMngLinkName, externalUserMngLinkUrl: config.externalUserMngLinkUrl, isLoading: false, + rolesLoading: false, }; export interface UsersFetchResult { @@ -22,6 +23,13 @@ export interface UsersFetchResult { totalCount: number; } +export interface UsersRolesFetchResult { + orgUsers: OrgUser[]; + perPage: number; + page: number; + totalCount: number; +} + const usersSlice = createSlice({ name: 'users', initialState, @@ -60,6 +68,12 @@ const usersSlice = createSlice({ usersFetchEnd: (state) => { return { ...state, isLoading: false }; }, + rolesFetchBegin: (state) => { + return { ...state, rolesLoading: true }; + }, + rolesFetchEnd: (state) => { + return { ...state, rolesLoading: false }; + }, }, }); @@ -71,6 +85,8 @@ export const { usersFetchEnd, pageChanged, sortChanged, + rolesFetchBegin, + rolesFetchEnd, } = usersSlice.actions; export const usersReducer = usersSlice.reducer; diff --git a/public/app/types/serviceaccount.ts b/public/app/types/serviceaccount.ts index cad4fea10d9..b14751e122f 100644 --- a/public/app/types/serviceaccount.ts +++ b/public/app/types/serviceaccount.ts @@ -36,6 +36,7 @@ export interface ServiceAccountDTO extends WithAccessControlMetadata { isDisabled: boolean; teams: string[]; role: OrgRole; + roles?: Role[]; } export interface ServiceAccountCreateApiResponse { @@ -52,6 +53,7 @@ export interface ServiceAccountCreateApiResponse { export interface ServiceAccountProfileState { serviceAccount: ServiceAccountDTO; isLoading: boolean; + rolesLoading?: boolean; tokens: ApiKey[]; } diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts index 5e4c7bf16b2..38f5abcf01a 100644 --- a/public/app/types/teams.ts +++ b/public/app/types/teams.ts @@ -1,5 +1,6 @@ import { Team as TeamDTO } from '@grafana/schema/src/raw/team/x/team_types.gen'; +import { Role } from './accessControl'; import { TeamPermissionLevel } from './acl'; // The team resource @@ -37,6 +38,10 @@ export interface Team { * TODO - it seems it's a team_member.permission, unlikely it should belong to the team kind */ permission: TeamPermissionLevel; + /** + * RBAC roles assigned to the team. + */ + roles?: Role[]; } export interface TeamMember { @@ -64,6 +69,7 @@ export interface TeamsState { totalPages: number; hasFetched: boolean; sort?: string; + rolesLoading?: boolean; } export interface TeamState { diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 66107cd1f27..fcbf37ec4ee 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -1,6 +1,8 @@ import { SelectableValue, WithAccessControlMetadata } from '@grafana/data'; +import { Role } from 'app/types'; import { OrgRole } from '.'; + export interface OrgUser extends WithAccessControlMetadata { avatarUrl: string; email: string; @@ -10,6 +12,8 @@ export interface OrgUser extends WithAccessControlMetadata { name: string; orgId: number; role: OrgRole; + // RBAC roles + roles?: Role[]; userId: number; isDisabled: boolean; authLabels?: string[]; @@ -76,6 +80,7 @@ export interface UsersState { externalUserMngLinkName: string; externalUserMngInfo: string; isLoading: boolean; + rolesLoading?: boolean; page: number; perPage: number; totalPages: number;