mirror of https://github.com/grafana/grafana.git
				
				
				
			Access Control: Add rolepicker when Fine Grained Access Control is enabled (#48347)
This commit is contained in:
		
							parent
							
								
									060af782df
								
							
						
					
					
						commit
						5fc5899462
					
				|  | @ -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; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue