mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 04:22:13 +08:00
Access Control: Add rolepicker when Fine Grained Access Control is enabled (#48347)
This commit is contained in:
@ -16,8 +16,9 @@ export interface Props {
|
||||
disabled?: boolean;
|
||||
builtinRolesDisabled?: boolean;
|
||||
showBuiltInRole?: boolean;
|
||||
onRolesChange: (newRoles: string[]) => void;
|
||||
onRolesChange: (newRoles: Role[]) => void;
|
||||
onBuiltinRoleChange?: (newRole: OrgRole) => void;
|
||||
updateDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const RolePicker = ({
|
||||
@ -30,6 +31,7 @@ export const RolePicker = ({
|
||||
showBuiltInRole,
|
||||
onRolesChange,
|
||||
onBuiltinRoleChange,
|
||||
updateDisabled,
|
||||
}: Props): JSX.Element | null => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [selectedRoles, setSelectedRoles] = useState<Role[]>(appliedRoles);
|
||||
@ -39,8 +41,9 @@ export const RolePicker = ({
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedBuiltInRole(builtInRole);
|
||||
setSelectedRoles(appliedRoles);
|
||||
}, [appliedRoles]);
|
||||
}, [appliedRoles, builtInRole]);
|
||||
|
||||
useEffect(() => {
|
||||
const dimensions = ref?.current?.getBoundingClientRect();
|
||||
@ -94,7 +97,7 @@ export const RolePicker = ({
|
||||
setSelectedBuiltInRole(role);
|
||||
};
|
||||
|
||||
const onUpdate = (newRoles: string[], newBuiltInRole?: OrgRole) => {
|
||||
const onUpdate = (newRoles: Role[], newBuiltInRole?: OrgRole) => {
|
||||
if (onBuiltinRoleChange && newBuiltInRole) {
|
||||
onBuiltinRoleChange(newBuiltInRole);
|
||||
}
|
||||
@ -144,6 +147,7 @@ export const RolePicker = ({
|
||||
showGroups={query.length === 0 || query.trim() === ''}
|
||||
builtinRolesDisabled={builtinRolesDisabled}
|
||||
showBuiltInRole={showBuiltInRole}
|
||||
updateDisabled={updateDisabled || false}
|
||||
offset={offset}
|
||||
/>
|
||||
)}
|
||||
|
@ -39,8 +39,9 @@ interface RolePickerMenuProps {
|
||||
showBuiltInRole?: boolean;
|
||||
onSelect: (roles: Role[]) => void;
|
||||
onBuiltInRoleSelect?: (role: OrgRole) => void;
|
||||
onUpdate: (newRoles: string[], newBuiltInRole?: OrgRole) => void;
|
||||
onUpdate: (newRoles: Role[], newBuiltInRole?: OrgRole) => void;
|
||||
onClear?: () => void;
|
||||
updateDisabled?: boolean;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
@ -55,6 +56,7 @@ export const RolePickerMenu = ({
|
||||
onBuiltInRoleSelect,
|
||||
onUpdate,
|
||||
onClear,
|
||||
updateDisabled,
|
||||
offset,
|
||||
}: RolePickerMenuProps): JSX.Element => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<Role[]>(appliedRoles);
|
||||
@ -166,11 +168,12 @@ export const RolePickerMenu = ({
|
||||
|
||||
const onUpdateInternal = () => {
|
||||
const selectedCustomRoles: string[] = [];
|
||||
// TODO: needed?
|
||||
for (const key in selectedOptions) {
|
||||
const roleUID = selectedOptions[key]?.uid;
|
||||
selectedCustomRoles.push(roleUID);
|
||||
}
|
||||
onUpdate(selectedCustomRoles, selectedBuiltInRole);
|
||||
onUpdate(selectedOptions, selectedBuiltInRole);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -270,7 +273,7 @@ export const RolePickerMenu = ({
|
||||
Clear all
|
||||
</Button>
|
||||
<Button size="sm" onClick={onUpdateInternal}>
|
||||
Update
|
||||
{updateDisabled ? `Apply` : `Update`}
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
|
@ -4,6 +4,7 @@ import { useAsyncFn } from 'react-use';
|
||||
import { Role } from 'app/types';
|
||||
|
||||
import { RolePicker } from './RolePicker';
|
||||
// @ts-ignore
|
||||
import { fetchTeamRoles, updateTeamRoles } from './api';
|
||||
|
||||
export interface Props {
|
||||
@ -29,7 +30,7 @@ export const TeamRolePicker: FC<Props> = ({ teamId, orgId, roleOptions, disabled
|
||||
getTeamRoles();
|
||||
}, [orgId, teamId, getTeamRoles]);
|
||||
|
||||
const onRolesChange = async (roles: string[]) => {
|
||||
const onRolesChange = async (roles: Role[]) => {
|
||||
await updateTeamRoles(roles, teamId, orgId);
|
||||
await getTeamRoles();
|
||||
};
|
||||
|
@ -16,6 +16,9 @@ export interface Props {
|
||||
builtInRoles?: { [key: string]: Role[] };
|
||||
disabled?: boolean;
|
||||
builtinRolesDisabled?: boolean;
|
||||
updateDisabled?: boolean;
|
||||
onApplyRoles?: (newRoles: Role[], userId: number, orgId: number | undefined) => void;
|
||||
pendingRoles?: Role[];
|
||||
}
|
||||
|
||||
export const UserRolePicker: FC<Props> = ({
|
||||
@ -27,9 +30,17 @@ export const UserRolePicker: FC<Props> = ({
|
||||
builtInRoles,
|
||||
disabled,
|
||||
builtinRolesDisabled,
|
||||
updateDisabled,
|
||||
onApplyRoles,
|
||||
pendingRoles,
|
||||
}) => {
|
||||
const [{ loading, value: appliedRoles = [] }, getUserRoles] = useAsyncFn(async () => {
|
||||
try {
|
||||
if (updateDisabled) {
|
||||
if (pendingRoles?.length! > 0) {
|
||||
return pendingRoles;
|
||||
}
|
||||
}
|
||||
if (contextSrv.hasPermission(AccessControlAction.ActionUserRolesList)) {
|
||||
return await fetchUserRoles(userId, orgId);
|
||||
}
|
||||
@ -38,29 +49,39 @@ export const UserRolePicker: FC<Props> = ({
|
||||
console.error('Error loading options');
|
||||
}
|
||||
return [];
|
||||
}, [orgId, userId]);
|
||||
}, [orgId, userId, pendingRoles]);
|
||||
|
||||
useEffect(() => {
|
||||
getUserRoles();
|
||||
}, [orgId, userId, getUserRoles]);
|
||||
// only load roles when there is an Org selected
|
||||
if (orgId) {
|
||||
getUserRoles();
|
||||
}
|
||||
}, [orgId, getUserRoles, pendingRoles]);
|
||||
|
||||
const onRolesChange = async (roles: string[]) => {
|
||||
await updateUserRoles(roles, userId, orgId);
|
||||
await getUserRoles();
|
||||
const onRolesChange = async (roles: Role[]) => {
|
||||
if (!updateDisabled) {
|
||||
await updateUserRoles(roles, userId, orgId);
|
||||
await getUserRoles();
|
||||
} else {
|
||||
if (onApplyRoles) {
|
||||
onApplyRoles(roles, userId, orgId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<RolePicker
|
||||
appliedRoles={appliedRoles}
|
||||
builtInRole={builtInRole}
|
||||
onRolesChange={onRolesChange}
|
||||
onBuiltinRoleChange={onBuiltinRoleChange}
|
||||
roleOptions={roleOptions}
|
||||
appliedRoles={appliedRoles}
|
||||
builtInRoles={builtInRoles}
|
||||
isLoading={loading}
|
||||
disabled={disabled}
|
||||
builtinRolesDisabled={builtinRolesDisabled}
|
||||
showBuiltInRole
|
||||
updateDisabled={updateDisabled || false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -38,11 +38,12 @@ export const fetchUserRoles = async (userId: number, orgId?: number): Promise<Ro
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserRoles = (roleUids: string[], userId: number, orgId?: number) => {
|
||||
export const updateUserRoles = (roles: Role[], userId: number, orgId?: number) => {
|
||||
let userRolesUrl = `/api/access-control/users/${userId}/roles`;
|
||||
if (orgId) {
|
||||
userRolesUrl += `?targetOrgId=${orgId}`;
|
||||
}
|
||||
const roleUids = roles.flatMap((x) => x.uid);
|
||||
return getBackendSrv().put(userRolesUrl, {
|
||||
orgId,
|
||||
roleUids,
|
||||
@ -66,11 +67,13 @@ export const fetchTeamRoles = async (teamId: number, orgId?: number): Promise<Ro
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTeamRoles = (roleUids: string[], teamId: number, orgId?: number) => {
|
||||
export const updateTeamRoles = (roles: Role[], teamId: number, orgId?: number) => {
|
||||
let teamRolesUrl = `/api/access-control/teams/${teamId}/roles`;
|
||||
if (orgId) {
|
||||
teamRolesUrl += `?targetOrgId=${orgId}`;
|
||||
}
|
||||
const roleUids = roles.flatMap((x) => x.uid);
|
||||
|
||||
return getBackendSrv().put(teamRolesUrl, {
|
||||
orgId,
|
||||
roleUids,
|
||||
|
@ -4,7 +4,7 @@ import { useAsyncFn } from 'react-use';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { AsyncSelect } from '@grafana/ui';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { Organization } from 'app/types';
|
||||
import { Organization, UserOrg } from 'app/types';
|
||||
|
||||
export type OrgSelectItem = SelectableValue<Organization>;
|
||||
|
||||
@ -13,9 +13,10 @@ export interface Props {
|
||||
className?: string;
|
||||
inputId?: string;
|
||||
autoFocus?: boolean;
|
||||
excludeOrgs?: UserOrg[];
|
||||
}
|
||||
|
||||
export function OrgPicker({ onSelected, className, inputId, autoFocus }: Props) {
|
||||
export function OrgPicker({ onSelected, className, inputId, autoFocus, excludeOrgs }: Props) {
|
||||
// For whatever reason the autoFocus prop doesn't seem to work
|
||||
// with AsyncSelect, hence this workaround. Maybe fixed in a later version?
|
||||
useEffect(() => {
|
||||
@ -26,7 +27,16 @@ export function OrgPicker({ onSelected, className, inputId, autoFocus }: Props)
|
||||
|
||||
const [orgOptionsState, getOrgOptions] = useAsyncFn(async () => {
|
||||
const orgs: Organization[] = await getBackendSrv().get('/api/orgs');
|
||||
return orgs.map((org) => ({ value: { id: org.id, name: org.name }, label: org.name }));
|
||||
const allOrgs = orgs.map((org) => ({ value: { id: org.id, name: org.name }, label: org.name }));
|
||||
if (excludeOrgs) {
|
||||
let idArray = excludeOrgs.map((anOrg) => anOrg.orgId);
|
||||
const filteredOrgs = allOrgs.filter((item) => {
|
||||
return !idArray.includes(item.value.id);
|
||||
});
|
||||
return filteredOrgs;
|
||||
} else {
|
||||
return allOrgs;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -17,10 +17,10 @@ import {
|
||||
withTheme,
|
||||
} from '@grafana/ui';
|
||||
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
|
||||
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
||||
import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api';
|
||||
import { OrgPicker, OrgSelectItem } from 'app/core/components/Select/OrgPicker';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction, Organization, OrgRole, UserDTO, UserOrg } from 'app/types';
|
||||
import { AccessControlAction, Organization, OrgRole, Role, UserDTO, UserOrg } from 'app/types';
|
||||
|
||||
import { OrgRolePicker } from './OrgRolePicker';
|
||||
|
||||
@ -88,7 +88,13 @@ export class UserOrgs extends PureComponent<Props, State> {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<AddToOrgModal isOpen={showAddOrgModal} onOrgAdd={onOrgAdd} onDismiss={this.dismissOrgAddModal} />
|
||||
<AddToOrgModal
|
||||
user={user}
|
||||
userOrgs={orgs}
|
||||
isOpen={showAddOrgModal}
|
||||
onOrgAdd={onOrgAdd}
|
||||
onDismiss={this.dismissOrgAddModal}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@ -155,8 +161,9 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps> {
|
||||
}
|
||||
}
|
||||
|
||||
onOrgRemove = () => {
|
||||
const { org } = this.props;
|
||||
onOrgRemove = async () => {
|
||||
const { org, user } = this.props;
|
||||
user && (await updateUserRoles([], user.id, org.orgId));
|
||||
this.props.onOrgRemove(org.orgId);
|
||||
};
|
||||
|
||||
@ -272,7 +279,8 @@ const getAddToOrgModalStyles = stylesFactory(() => ({
|
||||
|
||||
interface AddToOrgModalProps {
|
||||
isOpen: boolean;
|
||||
|
||||
user?: UserDTO;
|
||||
userOrgs: UserOrg[];
|
||||
onOrgAdd(orgId: number, role: string): void;
|
||||
|
||||
onDismiss?(): void;
|
||||
@ -281,16 +289,32 @@ interface AddToOrgModalProps {
|
||||
interface AddToOrgModalState {
|
||||
selectedOrg: Organization | null;
|
||||
role: OrgRole;
|
||||
roleOptions: Role[];
|
||||
pendingOrgId: number | null;
|
||||
pendingUserId: number | null;
|
||||
pendingRoles: Role[];
|
||||
}
|
||||
|
||||
export class AddToOrgModal extends PureComponent<AddToOrgModalProps, AddToOrgModalState> {
|
||||
state: AddToOrgModalState = {
|
||||
selectedOrg: null,
|
||||
role: OrgRole.Admin,
|
||||
role: OrgRole.Viewer,
|
||||
roleOptions: [],
|
||||
pendingOrgId: null,
|
||||
pendingUserId: null,
|
||||
pendingRoles: [],
|
||||
};
|
||||
|
||||
onOrgSelect = (org: OrgSelectItem) => {
|
||||
this.setState({ selectedOrg: org.value! });
|
||||
const userOrg = this.props.userOrgs.find((userOrg) => userOrg.orgId === org.value?.id);
|
||||
this.setState({ selectedOrg: org.value!, role: userOrg?.role || OrgRole.Viewer });
|
||||
if (contextSrv.licensedAccessControlEnabled()) {
|
||||
if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) {
|
||||
fetchRoleOptions(org.value?.id)
|
||||
.then((roles) => this.setState({ roleOptions: roles }))
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onOrgRoleChange = (newRole: OrgRole) => {
|
||||
@ -299,20 +323,48 @@ export class AddToOrgModal extends PureComponent<AddToOrgModalProps, AddToOrgMod
|
||||
});
|
||||
};
|
||||
|
||||
onAddUserToOrg = () => {
|
||||
onAddUserToOrg = async () => {
|
||||
const { selectedOrg, role } = this.state;
|
||||
this.props.onOrgAdd(selectedOrg!.id, role);
|
||||
// add the stored userRoles also
|
||||
if (contextSrv.licensedAccessControlEnabled()) {
|
||||
if (contextSrv.hasPermission(AccessControlAction.OrgUsersRoleUpdate)) {
|
||||
if (this.state.pendingUserId) {
|
||||
await updateUserRoles(this.state.pendingRoles, this.state.pendingUserId!, this.state.pendingOrgId!);
|
||||
// clear pending state
|
||||
this.state.pendingOrgId = null;
|
||||
this.state.pendingRoles = [];
|
||||
this.state.pendingUserId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
// clear selectedOrg when modal is canceled
|
||||
this.setState({
|
||||
selectedOrg: null,
|
||||
pendingRoles: [],
|
||||
pendingOrgId: null,
|
||||
pendingUserId: null,
|
||||
});
|
||||
if (this.props.onDismiss) {
|
||||
this.props.onDismiss();
|
||||
}
|
||||
};
|
||||
|
||||
onRoleUpdate = async (roles: Role[], userId: number, orgId: number | undefined) => {
|
||||
// keep the new role assignments for user
|
||||
this.setState({
|
||||
pendingRoles: roles,
|
||||
pendingOrgId: orgId!,
|
||||
pendingUserId: userId,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isOpen } = this.props;
|
||||
const { role } = this.state;
|
||||
const { isOpen, user, userOrgs } = this.props;
|
||||
const { role, roleOptions, selectedOrg } = this.state;
|
||||
const styles = getAddToOrgModalStyles();
|
||||
return (
|
||||
<Modal
|
||||
@ -323,17 +375,31 @@ export class AddToOrgModal extends PureComponent<AddToOrgModalProps, AddToOrgMod
|
||||
onDismiss={this.onCancel}
|
||||
>
|
||||
<Field label="Organization">
|
||||
<OrgPicker inputId="new-org-input" onSelected={this.onOrgSelect} autoFocus />
|
||||
<OrgPicker inputId="new-org-input" onSelected={this.onOrgSelect} excludeOrgs={userOrgs} autoFocus />
|
||||
</Field>
|
||||
<Field label="Role">
|
||||
<OrgRolePicker inputId="new-org-role-input" value={role} onChange={this.onOrgRoleChange} />
|
||||
<Field label="Role" disabled={selectedOrg === null}>
|
||||
{contextSrv.accessControlEnabled() ? (
|
||||
<UserRolePicker
|
||||
userId={user?.id || 0}
|
||||
orgId={selectedOrg?.id}
|
||||
builtInRole={role}
|
||||
onBuiltinRoleChange={this.onOrgRoleChange}
|
||||
builtinRolesDisabled={false}
|
||||
roleOptions={roleOptions}
|
||||
updateDisabled={true}
|
||||
onApplyRoles={this.onRoleUpdate}
|
||||
pendingRoles={this.state.pendingRoles}
|
||||
/>
|
||||
) : (
|
||||
<OrgRolePicker inputId="new-org-role-input" value={role} onChange={this.onOrgRoleChange} />
|
||||
)}
|
||||
</Field>
|
||||
<Modal.ButtonRow>
|
||||
<HorizontalGroup spacing="md" justify="center">
|
||||
<Button variant="secondary" fill="outline" onClick={this.onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={this.onAddUserToOrg}>
|
||||
<Button variant="primary" disabled={selectedOrg === null} onClick={this.onAddUserToOrg}>
|
||||
Add to organization
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
@ -6,6 +6,10 @@ import { NavModel } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { Form, Button, Input, Field } from '@grafana/ui';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
|
||||
import { fetchBuiltinRoles, fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction, OrgRole, Role, ServiceAccountCreateApiResponse, ServiceAccountDTO } from 'app/types';
|
||||
|
||||
import { getNavModel } from '../../core/selectors/navModel';
|
||||
import { StoreState } from '../../types';
|
||||
@ -13,23 +17,89 @@ import { StoreState } from '../../types';
|
||||
interface ServiceAccountCreatePageProps {
|
||||
navModel: NavModel;
|
||||
}
|
||||
interface ServiceAccountDTO {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const createServiceAccount = async (sa: ServiceAccountDTO) => getBackendSrv().post('/api/serviceaccounts/', sa);
|
||||
const updateServiceAccount = async (id: number, sa: ServiceAccountDTO) =>
|
||||
getBackendSrv().patch(`/api/serviceaccounts/${id}`, sa);
|
||||
|
||||
const ServiceAccountCreatePage: React.FC<ServiceAccountCreatePageProps> = ({ navModel }) => {
|
||||
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
||||
const [builtinRoles, setBuiltinRoles] = useState<{ [key: string]: Role[] }>({});
|
||||
const [pendingRoles, setPendingRoles] = useState<Role[]>([]);
|
||||
|
||||
const currentOrgId = contextSrv.user.orgId;
|
||||
const [serviceAccount, setServiceAccount] = useState<ServiceAccountDTO>({
|
||||
id: 0,
|
||||
orgId: contextSrv.user.orgId,
|
||||
role: OrgRole.Viewer,
|
||||
tokens: 0,
|
||||
name: '',
|
||||
login: '',
|
||||
isDisabled: false,
|
||||
createdAt: '',
|
||||
teams: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchOptions() {
|
||||
try {
|
||||
if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) {
|
||||
let options = await fetchRoleOptions(currentOrgId);
|
||||
setRoleOptions(options);
|
||||
}
|
||||
|
||||
if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) {
|
||||
const builtInRoles = await fetchBuiltinRoles(currentOrgId);
|
||||
setBuiltinRoles(builtInRoles);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading options', e);
|
||||
}
|
||||
}
|
||||
if (contextSrv.licensedAccessControlEnabled()) {
|
||||
fetchOptions();
|
||||
}
|
||||
}, [currentOrgId]);
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: ServiceAccountDTO) => {
|
||||
await createServiceAccount(data);
|
||||
data.role = serviceAccount.role;
|
||||
const response = await createServiceAccount(data);
|
||||
try {
|
||||
const newAccount: ServiceAccountCreateApiResponse = {
|
||||
avatarUrl: response.avatarUrl,
|
||||
id: response.id,
|
||||
isDisabled: response.isDisabled,
|
||||
login: response.login,
|
||||
name: response.name,
|
||||
orgId: response.orgId,
|
||||
role: response.role,
|
||||
tokens: response.tokens,
|
||||
};
|
||||
await updateServiceAccount(response.id, data);
|
||||
await updateUserRoles(pendingRoles, newAccount.id, newAccount.orgId);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
history.push('/org/serviceaccounts/');
|
||||
},
|
||||
[history]
|
||||
[history, serviceAccount.role, pendingRoles]
|
||||
);
|
||||
|
||||
const onRoleChange = (role: OrgRole) => {
|
||||
setServiceAccount({
|
||||
...serviceAccount,
|
||||
role: role,
|
||||
});
|
||||
};
|
||||
|
||||
const onPendingRolesUpdate = (roles: Role[], userId: number, orgId: number | undefined) => {
|
||||
// keep the new role assignments for user
|
||||
setPendingRoles(roles);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
@ -46,6 +116,22 @@ const ServiceAccountCreatePage: React.FC<ServiceAccountCreatePageProps> = ({ nav
|
||||
>
|
||||
<Input id="display-name-input" {...register('name', { required: true })} />
|
||||
</Field>
|
||||
{contextSrv.accessControlEnabled() && (
|
||||
<Field label="Role">
|
||||
<UserRolePicker
|
||||
userId={serviceAccount.id || 0}
|
||||
orgId={serviceAccount.orgId}
|
||||
builtInRole={serviceAccount.role}
|
||||
builtInRoles={builtinRoles}
|
||||
onBuiltinRoleChange={(newRole) => onRoleChange(newRole)}
|
||||
builtinRolesDisabled={false}
|
||||
roleOptions={roleOptions}
|
||||
updateDisabled={true}
|
||||
onApplyRoles={onPendingRolesUpdate}
|
||||
pendingRoles={pendingRoles}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
<Button type="submit">Create</Button>
|
||||
</>
|
||||
);
|
||||
|
@ -66,16 +66,23 @@ const ServiceAccountsListPage = ({
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
fetchServiceAccounts();
|
||||
if (contextSrv.licensedAccessControlEnabled()) {
|
||||
fetchACOptions();
|
||||
}
|
||||
const fetchData = async () => {
|
||||
await fetchServiceAccounts();
|
||||
if (contextSrv.licensedAccessControlEnabled()) {
|
||||
await fetchACOptions();
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [fetchServiceAccounts, fetchACOptions]);
|
||||
|
||||
const onRoleChange = async (role: OrgRole, serviceAccount: ServiceAccountDTO) => {
|
||||
const updatedServiceAccount = { ...serviceAccount, role: role };
|
||||
await updateServiceAccount(updatedServiceAccount);
|
||||
// need to refetch to display the new value in the list
|
||||
await fetchServiceAccounts();
|
||||
if (contextSrv.licensedAccessControlEnabled()) {
|
||||
fetchACOptions();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -38,6 +38,17 @@ export interface ServiceAccountDTO extends WithAccessControlMetadata {
|
||||
role: OrgRole;
|
||||
}
|
||||
|
||||
export interface ServiceAccountCreateApiResponse {
|
||||
avatarUrl?: string;
|
||||
id: number;
|
||||
isDisabled: boolean;
|
||||
login: string;
|
||||
name: string;
|
||||
orgId: number;
|
||||
role: OrgRole;
|
||||
tokens: number;
|
||||
}
|
||||
|
||||
export interface ServiceAccountProfileState {
|
||||
serviceAccount: ServiceAccountDTO;
|
||||
isLoading: boolean;
|
||||
|
Reference in New Issue
Block a user