grafana/public/app/features/serviceaccounts/components/CreateTokenModal.tsx
Alexander Zobnin 50538d5309
ServiceAccounts: refactor UI (#49508)
* ServiceAccounts: refactor ServiceAccountRoleRow

* Refactor ServiceAccountRoleRow

* Refactor ServiceAccountProfile

* Refactor components

* Change service accounts icon

* Refine service accounts page header

* Improve service accounts filtering

* Change delete button style

* Tweak account id

* Auto focus name field when create service account

* Add disable/enable button

* Enable/disable service accounts

* Optimize updating service account (do not fetch all)

* Remove status column (replace by enable/disable button)

* Add banner with service accounts description

* Add tokens from main page

* Update tokens count when add token from main page

* Fix action buttons column

* Fix tokens count when change role

* Refine table row classes

* Fix buttons

* Simplify working with state

* Show message when service account updated

* Able to filter disabled accounts

* Mark disabled accounts in a table

* Refine disabled account view

* Move non-critical components to separate folder

* Remove confusing focusing

* Fix date picker position when creating new token

* DatePicker: able to set minimum date that can be selected

* Don't allow to select expiration dates prior today

* Set tomorrow as a default token expiration date

* Fix displaying expiration period

* Rename Add token button

* Refine page styles

* Show modal when disabling SA from main page

* Arrange role picker

* Refine SA page styles

* Generate default token name

* More smooth navigation between SA pages

* Stop loading indicator in case of error

* Remove legacy styles usage

* Tweaks after code review

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Get rid of useDisapatch in favor of mapDispatchToProps

* Tests for ServiceAccountsListPage

* Tests for service account page

* Show new role picker only with license

* Get rid of deprecated css classes

* Apply suggestion from code review

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Fix service accounts icon

* Tests for service account create page

* Return service account info when update

* Add behaviour tests for ServiceAccountsListPage

* Fix disabled cursor on confirm button

* More behavior tests for service account page

* Temporary disable service account migration banner

* Use safe where condition

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Apply review suggestions

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Remove autofocus from search

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
Co-authored-by: Jguer <joao.guerreiro@grafana.com>
2022-06-01 09:35:16 +02:00

214 lines
6.1 KiB
TypeScript

import { css } from '@emotion/css';
import React, { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data';
import {
Button,
ClipboardButton,
DatePickerWithInput,
Field,
FieldSet,
HorizontalGroup,
Icon,
Input,
Label,
Modal,
RadioButtonGroup,
useStyles2,
} from '@grafana/ui';
const EXPIRATION_OPTIONS = [
{ label: 'No expiration', value: false },
{ label: 'Set expiration date', value: true },
];
export type ServiceAccountToken = {
name: string;
secondsToLive?: number;
};
interface Props {
isOpen: boolean;
token: string;
serviceAccountLogin: string;
onCreateToken: (token: ServiceAccountToken) => void;
onClose: () => void;
}
export const CreateTokenModal = ({ isOpen, token, serviceAccountLogin, onCreateToken, onClose }: Props) => {
let tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const [defaultTokenName, setDefaultTokenName] = useState('');
const [newTokenName, setNewTokenName] = useState('');
const [isWithExpirationDate, setIsWithExpirationDate] = useState(false);
const [newTokenExpirationDate, setNewTokenExpirationDate] = useState<Date | string>(tomorrow);
const [isExpirationDateValid, setIsExpirationDateValid] = useState(newTokenExpirationDate !== '');
const styles = useStyles2(getStyles);
useEffect(() => {
// Generate new token name every time we open modal
if (isOpen) {
setDefaultTokenName(`${serviceAccountLogin}-${uuidv4()}`);
}
}, [serviceAccountLogin, isOpen]);
const onExpirationDateChange = (value: Date | string) => {
const isValid = value !== '';
setIsExpirationDateValid(isValid);
setNewTokenExpirationDate(value);
};
const onGenerateToken = () => {
onCreateToken({
name: newTokenName || defaultTokenName,
secondsToLive: isWithExpirationDate ? getSecondsToLive(newTokenExpirationDate) : undefined,
});
};
const onCloseInternal = () => {
setNewTokenName('');
setDefaultTokenName('');
setIsWithExpirationDate(false);
setNewTokenExpirationDate(tomorrow);
setIsExpirationDateValid(newTokenExpirationDate !== '');
onClose();
};
const modalTitle = (
<div className={styles.modalHeaderTitle}>
<Icon className={styles.modalHeaderIcon} name="key-skeleton-alt" size="lg" />
<span>{!token ? 'Add service account token' : 'Service account token created'}</span>
</div>
);
return (
<Modal
isOpen={isOpen}
title={modalTitle}
onDismiss={onCloseInternal}
className={styles.modal}
contentClassName={styles.modalContent}
>
{!token ? (
<div>
<FieldSet>
<Field
label="Display name"
description="Name to easily identify the token"
className={styles.modalRow}
// for now this is required
// need to make this optional in backend as well
required={true}
>
<Input
name="tokenName"
value={newTokenName}
placeholder={defaultTokenName}
onChange={(e) => {
setNewTokenName(e.currentTarget.value);
}}
/>
</Field>
<RadioButtonGroup
className={styles.modalRow}
options={EXPIRATION_OPTIONS}
value={isWithExpirationDate}
onChange={setIsWithExpirationDate}
size="md"
/>
{isWithExpirationDate && (
<Field label="Expiration date" className={styles.modalRow}>
<DatePickerWithInput
onChange={onExpirationDateChange}
value={newTokenExpirationDate}
placeholder=""
minDate={tomorrow}
/>
</Field>
)}
</FieldSet>
<Button onClick={onGenerateToken} disabled={isWithExpirationDate && !isExpirationDateValid}>
Generate token
</Button>
</div>
) : (
<>
<FieldSet>
<Label
description="You will not be able to see or generate it again. Loosing a token requires creating new one."
className={styles.modalRow}
>
Copy the token. It will be showed only once.
</Label>
<Field label="Token" className={styles.modalRow}>
<div className={styles.modalTokenRow}>
<Input name="tokenValue" value={token} readOnly />
<ClipboardButton
className={styles.modalCopyToClipboardButton}
variant="secondary"
size="md"
getText={() => token}
>
<Icon name="copy" /> Copy to clipboard
</ClipboardButton>
</div>
</Field>
</FieldSet>
<HorizontalGroup>
<ClipboardButton variant="primary" getText={() => token} onClipboardCopy={onCloseInternal}>
Copy to clipboard and close
</ClipboardButton>
<Button variant="secondary" onClick={onCloseInternal}>
Close
</Button>
</HorizontalGroup>
</>
)}
</Modal>
);
};
const getSecondsToLive = (date: Date | string) => {
const dateAsDate = new Date(date);
const now = new Date();
return Math.ceil((dateAsDate.getTime() - now.getTime()) / 1000);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
modal: css`
width: 550px;
`,
modalContent: css`
overflow: visible;
`,
modalRow: css`
margin-bottom: ${theme.spacing(4)};
`,
modalTokenRow: css`
display: flex;
`,
modalCopyToClipboardButton: css`
margin-left: ${theme.spacing(0.5)};
`,
modalHeaderTitle: css`
font-size: ${theme.typography.size.lg};
margin: ${theme.spacing(0, 4, 0, 1)};
display: flex;
align-items: center;
position: relative;
top: 2px;
`,
modalHeaderIcon: css`
margin-right: ${theme.spacing(2)};
font-size: inherit;
&:before {
vertical-align: baseline;
}
`,
};
};