123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- import { css, cx } from '@emotion/css';
- import React, { PureComponent, ReactElement } from 'react';
- import { GrafanaTheme, GrafanaTheme2 } from '@grafana/data';
- import {
- Button,
- ConfirmButton,
- Field,
- HorizontalGroup,
- Icon,
- Modal,
- stylesFactory,
- Themeable,
- Tooltip,
- useStyles2,
- useTheme,
- withTheme,
- } from '@grafana/ui';
- import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
- 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, Role, UserDTO, UserOrg } from 'app/types';
- import { OrgRolePicker } from './OrgRolePicker';
- interface Props {
- orgs: UserOrg[];
- user?: UserDTO;
- isExternalUser?: boolean;
- onOrgRemove: (orgId: number) => void;
- onOrgRoleChange: (orgId: number, newRole: OrgRole) => void;
- onOrgAdd: (orgId: number, role: OrgRole) => void;
- }
- interface State {
- showAddOrgModal: boolean;
- }
- export class UserOrgs extends PureComponent<Props, State> {
- addToOrgButtonRef = React.createRef<HTMLButtonElement>();
- state = {
- showAddOrgModal: false,
- };
- showOrgAddModal = () => {
- this.setState({ showAddOrgModal: true });
- };
- dismissOrgAddModal = () => {
- this.setState({ showAddOrgModal: false }, () => {
- this.addToOrgButtonRef.current?.focus();
- });
- };
- render() {
- const { user, orgs, isExternalUser, onOrgRoleChange, onOrgRemove, onOrgAdd } = this.props;
- const { showAddOrgModal } = this.state;
- const addToOrgContainerClass = css`
- margin-top: 0.8rem;
- `;
- const canAddToOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersAdd);
- return (
- <>
- <h3 className="page-heading">Organizations</h3>
- <div className="gf-form-group">
- <div className="gf-form">
- <table className="filter-table form-inline">
- <tbody>
- {orgs.map((org, index) => (
- <OrgRow
- key={`${org.orgId}-${index}`}
- isExternalUser={isExternalUser}
- user={user}
- org={org}
- onOrgRoleChange={onOrgRoleChange}
- onOrgRemove={onOrgRemove}
- />
- ))}
- </tbody>
- </table>
- </div>
- <div className={addToOrgContainerClass}>
- {canAddToOrg && (
- <Button variant="secondary" onClick={this.showOrgAddModal} ref={this.addToOrgButtonRef}>
- Add user to organization
- </Button>
- )}
- </div>
- <AddToOrgModal
- user={user}
- userOrgs={orgs}
- isOpen={showAddOrgModal}
- onOrgAdd={onOrgAdd}
- onDismiss={this.dismissOrgAddModal}
- />
- </div>
- </>
- );
- }
- }
- const getOrgRowStyles = stylesFactory((theme: GrafanaTheme) => {
- return {
- removeButton: css`
- margin-right: 0.6rem;
- text-decoration: underline;
- color: ${theme.palette.blue95};
- `,
- label: css`
- font-weight: 500;
- `,
- disabledTooltip: css`
- display: flex;
- `,
- tooltipItem: css`
- margin-left: 5px;
- `,
- tooltipItemLink: css`
- color: ${theme.palette.blue95};
- `,
- rolePickerWrapper: css`
- display: flex;
- `,
- rolePicker: css`
- flex: auto;
- margin-right: ${theme.spacing.sm};
- `,
- };
- });
- interface OrgRowProps extends Themeable {
- user?: UserDTO;
- org: UserOrg;
- isExternalUser?: boolean;
- onOrgRemove: (orgId: number) => void;
- onOrgRoleChange: (orgId: number, newRole: OrgRole) => void;
- }
- class UnThemedOrgRow extends PureComponent<OrgRowProps> {
- state = {
- currentRole: this.props.org.role,
- isChangingRole: false,
- roleOptions: [],
- builtInRoles: {},
- };
- componentDidMount() {
- if (contextSrv.licensedAccessControlEnabled()) {
- if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) {
- fetchRoleOptions(this.props.org.orgId)
- .then((roles) => this.setState({ roleOptions: roles }))
- .catch((e) => console.error(e));
- }
- if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) {
- fetchRoleOptions(this.props.org.orgId)
- .then((roles) => this.setState({ builtInRoles: roles }))
- .catch((e) => console.error(e));
- }
- }
- }
- onOrgRemove = async () => {
- const { org, user } = this.props;
- this.props.onOrgRemove(org.orgId);
- if (contextSrv.licensedAccessControlEnabled()) {
- if (contextSrv.hasPermission(AccessControlAction.OrgUsersRemove)) {
- user && (await updateUserRoles([], user.id, org.orgId));
- }
- }
- };
- onChangeRoleClick = () => {
- const { org } = this.props;
- this.setState({ isChangingRole: true, currentRole: org.role });
- };
- onOrgRoleChange = (newRole: OrgRole) => {
- this.setState({ currentRole: newRole });
- };
- onOrgRoleSave = () => {
- this.props.onOrgRoleChange(this.props.org.orgId, this.state.currentRole);
- };
- onCancelClick = () => {
- this.setState({ isChangingRole: false });
- };
- onBuiltinRoleChange = (newRole: OrgRole) => {
- this.props.onOrgRoleChange(this.props.org.orgId, newRole);
- };
- render() {
- const { user, org, isExternalUser, theme } = this.props;
- const { currentRole, isChangingRole } = this.state;
- const styles = getOrgRowStyles(theme);
- const labelClass = cx('width-16', styles.label);
- const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersWrite);
- const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove);
- const rolePickerDisabled = isExternalUser || !canChangeRole;
- const inputId = `${org.name}-input`;
- return (
- <tr>
- <td className={labelClass}>
- <label htmlFor={inputId}>{org.name}</label>
- </td>
- {contextSrv.licensedAccessControlEnabled() ? (
- <td>
- <div className={styles.rolePickerWrapper}>
- <div className={styles.rolePicker}>
- <UserRolePicker
- userId={user?.id || 0}
- orgId={org.orgId}
- builtInRole={org.role}
- roleOptions={this.state.roleOptions}
- builtInRoles={this.state.builtInRoles}
- onBuiltinRoleChange={this.onBuiltinRoleChange}
- builtinRolesDisabled={rolePickerDisabled}
- />
- </div>
- {isExternalUser && <ExternalUserTooltip />}
- </div>
- </td>
- ) : (
- <>
- {isChangingRole ? (
- <td>
- <OrgRolePicker inputId={inputId} value={currentRole} onChange={this.onOrgRoleChange} autoFocus />
- </td>
- ) : (
- <td className="width-25">{org.role}</td>
- )}
- <td colSpan={1}>
- <div className="pull-right">
- {canChangeRole && (
- <ChangeOrgButton
- isExternalUser={isExternalUser}
- onChangeRoleClick={this.onChangeRoleClick}
- onCancelClick={this.onCancelClick}
- onOrgRoleSave={this.onOrgRoleSave}
- />
- )}
- </div>
- </td>
- </>
- )}
- <td colSpan={1}>
- <div className="pull-right">
- {canRemoveFromOrg && (
- <ConfirmButton
- confirmText="Confirm removal"
- confirmVariant="destructive"
- onCancel={this.onCancelClick}
- onConfirm={this.onOrgRemove}
- autoFocus
- >
- Remove from organization
- </ConfirmButton>
- )}
- </div>
- </td>
- </tr>
- );
- }
- }
- const OrgRow = withTheme(UnThemedOrgRow);
- const getAddToOrgModalStyles = stylesFactory(() => ({
- modal: css`
- width: 500px;
- `,
- buttonRow: css`
- text-align: center;
- `,
- modalContent: css`
- overflow: visible;
- `,
- }));
- interface AddToOrgModalProps {
- isOpen: boolean;
- user?: UserDTO;
- userOrgs: UserOrg[];
- onOrgAdd(orgId: number, role: string): void;
- onDismiss?(): void;
- }
- 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.Viewer,
- roleOptions: [],
- pendingOrgId: null,
- pendingUserId: null,
- pendingRoles: [],
- };
- onOrgSelect = (org: OrgSelectItem) => {
- 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) => {
- this.setState({
- role: newRole,
- });
- };
- 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.OrgUsersWrite)) {
- 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, user, userOrgs } = this.props;
- const { role, roleOptions, selectedOrg } = this.state;
- const styles = getAddToOrgModalStyles();
- return (
- <Modal
- className={styles.modal}
- contentClassName={styles.modalContent}
- title="Add to an organization"
- isOpen={isOpen}
- onDismiss={this.onCancel}
- >
- <Field label="Organization">
- <OrgPicker inputId="new-org-input" onSelected={this.onOrgSelect} excludeOrgs={userOrgs} autoFocus />
- </Field>
- <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" disabled={selectedOrg === null} onClick={this.onAddUserToOrg}>
- Add to organization
- </Button>
- </HorizontalGroup>
- </Modal.ButtonRow>
- </Modal>
- );
- }
- }
- interface ChangeOrgButtonProps {
- isExternalUser?: boolean;
- onChangeRoleClick: () => void;
- onCancelClick: () => void;
- onOrgRoleSave: () => void;
- }
- const getChangeOrgButtonTheme = (theme: GrafanaTheme2) => ({
- disabledTooltip: css`
- display: flex;
- `,
- tooltipItemLink: css`
- color: ${theme.v1.palette.blue95};
- `,
- });
- export function ChangeOrgButton({
- onChangeRoleClick,
- isExternalUser,
- onOrgRoleSave,
- onCancelClick,
- }: ChangeOrgButtonProps): ReactElement {
- const styles = useStyles2(getChangeOrgButtonTheme);
- return (
- <div className={styles.disabledTooltip}>
- <ConfirmButton
- confirmText="Save"
- onClick={onChangeRoleClick}
- onCancel={onCancelClick}
- onConfirm={onOrgRoleSave}
- disabled={isExternalUser}
- >
- Change role
- </ConfirmButton>
- {isExternalUser && (
- <Tooltip
- placement="right-end"
- content={
- <div>
- This user's role is not editable because it is synchronized from your auth provider. Refer to
- the
- <a
- className={styles.tooltipItemLink}
- href={'https://grafana.com/docs/grafana/latest/auth'}
- rel="noreferrer"
- target="_blank"
- >
- Grafana authentication docs
- </a>
- for details.
- </div>
- }
- >
- <Icon name="question-circle" />
- </Tooltip>
- )}
- </div>
- );
- }
- const ExternalUserTooltip: React.FC = () => {
- const theme = useTheme();
- const styles = getTooltipStyles(theme);
- return (
- <div className={styles.disabledTooltip}>
- <Tooltip
- placement="right-end"
- content={
- <div>
- This user's built-in role is not editable because it is synchronized from your auth provider. Refer to
- the
- <a
- className={styles.tooltipItemLink}
- href={'https://grafana.com/docs/grafana/latest/auth'}
- rel="noreferrer noopener"
- target="_blank"
- >
- Grafana authentication docs
- </a>
- for details.
- </div>
- }
- >
- <Icon name="question-circle" />
- </Tooltip>
- </div>
- );
- };
- const getTooltipStyles = stylesFactory((theme: GrafanaTheme) => ({
- disabledTooltip: css`
- display: flex;
- `,
- tooltipItemLink: css`
- color: ${theme.palette.blue95};
- `,
- }));
|