grafana/public/app/features/serviceaccounts/ServiceAccountProfile.tsx
Eric Leijonmarck b43e9b50b4
Service accounts: RBAC the service account UI (#47788)
* WIP

* fix: bug for saving name did not remove edit

* refactor: better error msg

* Display the column Roles even when user can't see the role picker

* Remove spaces when building the search query request

* Disable Edit button and fix token addition and deletion

* Fix the error message text

Co-authored-by: Vardan Torosyan <vardants@gmail.com>
2022-04-14 23:06:08 +01:00

291 lines
8.6 KiB
TypeScript

import React, { PureComponent, useRef, useState } from 'react';
import { Role, ServiceAccountDTO, AccessControlAction } from 'app/types';
import { css, cx } from '@emotion/css';
import { dateTimeFormat, GrafanaTheme2, OrgRole, TimeZone } from '@grafana/data';
import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, useStyles2 } from '@grafana/ui';
import { ServiceAccountRoleRow } from './ServiceAccountRoleRow';
import { contextSrv } from 'app/core/core';
interface Props {
serviceAccount: ServiceAccountDTO;
timeZone: TimeZone;
roleOptions: Role[];
builtInRoles: Record<string, Role[]>;
deleteServiceAccount: (serviceAccountId: number) => void;
updateServiceAccount: (serviceAccount: ServiceAccountDTO) => void;
}
export function ServiceAccountProfile({
serviceAccount,
timeZone,
roleOptions,
builtInRoles,
deleteServiceAccount,
updateServiceAccount,
}: Props) {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showDisableModal, setShowDisableModal] = useState(false);
const ableToWrite = contextSrv.hasPermission(AccessControlAction.ServiceAccountsWrite);
const deleteServiceAccountRef = useRef<HTMLButtonElement | null>(null);
const showDeleteServiceAccountModal = (show: boolean) => () => {
setShowDeleteModal(show);
if (!show && deleteServiceAccountRef.current) {
deleteServiceAccountRef.current.focus();
}
};
const disableServiceAccountRef = useRef<HTMLButtonElement | null>(null);
const showDisableServiceAccountModal = (show: boolean) => () => {
setShowDisableModal(show);
if (!show && disableServiceAccountRef.current) {
disableServiceAccountRef.current.focus();
}
};
const handleServiceAccountDelete = () => {
deleteServiceAccount(serviceAccount.id);
};
const handleServiceAccountDisable = () => {
updateServiceAccount({ ...serviceAccount, isDisabled: true });
setShowDisableModal(false);
};
const handleServiceAccountEnable = () => {
updateServiceAccount({ ...serviceAccount, isDisabled: false });
};
const handleServiceAccountRoleChange = (role: OrgRole) => {
updateServiceAccount({ ...serviceAccount, role: role });
};
const onServiceAccountNameChange = (newValue: string) => {
updateServiceAccount({ ...serviceAccount, name: newValue });
};
const styles = useStyles2(getStyles);
return (
<>
<div style={{ marginBottom: '10px' }}>
<a href="org/serviceaccounts" style={{ display: 'inline-block', verticalAlign: 'middle' }}>
<Button variant="link" icon="backward" />
</a>
<h1
className="page-heading"
style={{ display: 'inline-block', verticalAlign: 'middle', margin: '0!important', marginBottom: '0px' }}
>
{serviceAccount.name}
</h1>
</div>
<span style={{ marginBottom: '10px' }}>Information</span>
<div className="gf-form-group">
<div className="gf-form">
<table className="filter-table form-inline">
<tbody>
<ServiceAccountProfileRow
label="Name"
value={serviceAccount.name}
onChange={onServiceAccountNameChange}
disabled={!ableToWrite}
/>
<ServiceAccountProfileRow label="ID" value={serviceAccount.login} />
<ServiceAccountRoleRow
label="Roles"
serviceAccount={serviceAccount}
onRoleChange={handleServiceAccountRoleChange}
builtInRoles={builtInRoles}
roleOptions={roleOptions}
/>
{/* <ServiceAccountProfileRow label="Teams" value={serviceAccount.teams.join(', ')} /> */}
<ServiceAccountProfileRow
label="Creation date"
value={dateTimeFormat(serviceAccount.createdAt, { timeZone })}
/>
</tbody>
</table>
</div>
<div className={styles.buttonRow}>
<>
<Button
type={'button'}
variant="destructive"
onClick={showDeleteServiceAccountModal(true)}
ref={deleteServiceAccountRef}
disabled={!contextSrv.hasPermission(AccessControlAction.ServiceAccountsDelete)}
>
Delete service account
</Button>
<ConfirmModal
isOpen={showDeleteModal}
title="Delete service account"
body="Are you sure you want to delete this service account?"
confirmText="Delete service account"
onConfirm={handleServiceAccountDelete}
onDismiss={showDeleteServiceAccountModal(false)}
/>
</>
{serviceAccount.isDisabled ? (
<Button type={'button'} variant="secondary" onClick={handleServiceAccountEnable} disabled={!ableToWrite}>
Enable service account
</Button>
) : (
<>
<Button
type={'button'}
variant="secondary"
onClick={showDisableServiceAccountModal(true)}
ref={disableServiceAccountRef}
disabled={!ableToWrite}
>
Disable service account
</Button>
<ConfirmModal
isOpen={showDisableModal}
title="Disable service account"
body="Are you sure you want to disable this service account?"
confirmText="Disable service account"
onConfirm={handleServiceAccountDisable}
onDismiss={showDisableServiceAccountModal(false)}
/>
</>
)}
</div>
</div>
</>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
buttonRow: css`
margin-top: ${theme.spacing(1.5)};
> * {
margin-right: ${theme.spacing(2)};
}
`,
};
};
interface ServiceAccountProfileRowProps {
label: string;
value?: string;
inputType?: string;
onChange?: (value: string) => void;
disabled?: boolean;
}
interface ServiceAccountProfileRowState {
value: string;
editing: boolean;
}
export class ServiceAccountProfileRow extends PureComponent<
ServiceAccountProfileRowProps,
ServiceAccountProfileRowState
> {
inputElem?: HTMLInputElement;
static defaultProps: Partial<ServiceAccountProfileRowProps> = {
value: '',
inputType: 'text',
};
state = {
editing: false,
value: this.props.value || '',
};
setInputElem = (elem: any) => {
this.inputElem = elem;
};
onEditClick = () => {
this.setState({ editing: true }, this.focusInput);
};
onCancelClick = () => {
this.setState({ editing: false, value: this.props.value || '' });
};
onInputChange = (event: React.ChangeEvent<HTMLInputElement>, status?: LegacyInputStatus) => {
if (status === LegacyInputStatus.Invalid) {
return;
}
this.setState({ value: event.target.value });
};
onInputBlur = (event: React.FocusEvent<HTMLInputElement>, status?: LegacyInputStatus) => {
if (status === LegacyInputStatus.Invalid) {
return;
}
this.setState({ value: event.target.value });
};
focusInput = () => {
if (this.inputElem && this.inputElem.focus) {
this.inputElem.focus();
}
};
onSave = () => {
this.setState({ editing: false });
if (this.props.onChange) {
this.props.onChange(this.state.value);
}
};
render() {
const { label, inputType } = this.props;
const { value } = this.state;
const labelClass = cx(
'width-16',
css`
font-weight: 500;
`
);
const inputId = `${label}-input`;
return (
<tr>
<td className={labelClass}>
<label htmlFor={inputId}>{label}</label>
</td>
<td className="width-25" colSpan={2}>
{!this.props.disabled && this.state.editing ? (
<Input
id={inputId}
type={inputType}
defaultValue={value}
onBlur={this.onInputBlur}
onChange={this.onInputChange}
ref={this.setInputElem}
width={30}
/>
) : (
<span>{this.props.value}</span>
)}
</td>
<td>
{this.props.onChange && (
<ConfirmButton
closeOnConfirm
confirmText="Save"
onConfirm={this.onSave}
onClick={this.onEditClick}
onCancel={this.onCancelClick}
disabled={this.props.disabled}
>
Edit
</ConfirmButton>
)}
</td>
</tr>
);
}
}