From 089a111858e8e6d8f05da31da26f0ff08d1dfa25 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 9 Feb 2022 13:38:46 +0300 Subject: [PATCH] Service accounts: Add token UI (#45081) * Initial token creation dialog * Use date picker for expiration date * Reset state after closing modal * Create token flow * Move modal to separate component * Minor refactor * Apply suggestions from code review Co-authored-by: Dominik Prokop Co-authored-by: Dominik Prokop --- .../serviceaccounts/CreateTokenModal.tsx | 161 ++++++++++++++++++ .../serviceaccounts/ServiceAccountPage.tsx | 46 ++++- .../features/serviceaccounts/state/actions.ts | 12 ++ 3 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 public/app/features/serviceaccounts/CreateTokenModal.tsx diff --git a/public/app/features/serviceaccounts/CreateTokenModal.tsx b/public/app/features/serviceaccounts/CreateTokenModal.tsx new file mode 100644 index 00000000000..c0e7ac2eeb9 --- /dev/null +++ b/public/app/features/serviceaccounts/CreateTokenModal.tsx @@ -0,0 +1,161 @@ +import React, { useState } from 'react'; +import { css } from '@emotion/css'; +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 }, +]; + +interface CreateTokenModalProps { + isOpen: boolean; + token: string; + onCreateToken: (name: string) => void; + onClose: () => void; +} + +export const CreateTokenModal = ({ isOpen, token, onCreateToken, onClose }: CreateTokenModalProps) => { + const [newTokenName, setNewTokenName] = useState(''); + const [isWithExpirationDate, setIsWithExpirationDate] = useState(false); + const [newTokenExpirationDate, setNewTokenExpirationDate] = useState(''); + const [isExpirationDateValid, setIsExpirationDateValid] = useState(false); + const styles = useStyles2(getStyles); + + const onExpirationDateChange = (value: Date | string) => { + const isValid = value !== ''; + setIsExpirationDateValid(isValid); + setNewTokenExpirationDate(value); + }; + + const onCloseInternal = () => { + setNewTokenName(''); + setIsWithExpirationDate(false); + setNewTokenExpirationDate(''); + setIsExpirationDateValid(false); + onClose(); + }; + + const modalTitle = ( +
+ + {!token ? 'Add service account token' : 'Service account token created'} +
+ ); + + return ( + + {!token ? ( + <> +
+ + { + setNewTokenName(e.currentTarget.value); + }} + /> + + + {isWithExpirationDate && ( + + + + )} +
+ + + ) : ( + <> +
+ + +
+ + token} + > + Copy to clipboard + +
+
+
+ + token} onClipboardCopy={onCloseInternal}> + Copy to clipboard and close + + + + + )} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + modal: css` + width: 550px; + `, + 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; + } + `, + }; +}; diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index 4ec31e06009..2c34dfbd543 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -1,13 +1,20 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { getNavModel } from 'app/core/selectors/navModel'; import Page from 'app/core/components/Page/Page'; import { ServiceAccountProfile } from './ServiceAccountProfile'; import { StoreState, ServiceAccountDTO, ApiKey } from 'app/types'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { deleteServiceAccountToken, loadServiceAccount, loadServiceAccountTokens } from './state/actions'; +import { + deleteServiceAccountToken, + loadServiceAccount, + loadServiceAccountTokens, + createServiceAccountToken, +} from './state/actions'; import { ServiceAccountTokensTable } from './ServiceAccountTokensTable'; -import { getTimeZone, NavModel } from '@grafana/data'; +import { getTimeZone, NavModel, OrgRole } from '@grafana/data'; +import { Button, VerticalGroup } from '@grafana/ui'; +import { CreateTokenModal } from './CreateTokenModal'; interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { navModel: NavModel; @@ -28,6 +35,7 @@ function mapStateToProps(state: StoreState) { const mapDispatchToProps = { loadServiceAccount, loadServiceAccountTokens, + createServiceAccountToken, deleteServiceAccountToken, }; @@ -43,8 +51,12 @@ const ServiceAccountPageUnconnected = ({ isLoading, loadServiceAccount, loadServiceAccountTokens, + createServiceAccountToken, deleteServiceAccountToken, }: Props) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [newToken, setNewToken] = useState(''); + useEffect(() => { const serviceAccountId = parseInt(match.params.id, 10); loadServiceAccount(serviceAccountId); @@ -55,6 +67,22 @@ const ServiceAccountPageUnconnected = ({ deleteServiceAccountToken(parseInt(match.params.id, 10), key.id!); }; + const onCreateToken = (name: string) => { + createServiceAccountToken( + serviceAccount.id, + { + name, + role: OrgRole.Viewer, + }, + setNewToken + ); + }; + + const onModalClose = () => { + setIsModalOpen(false); + setNewToken(''); + }; + return ( @@ -77,10 +105,14 @@ const ServiceAccountPageUnconnected = ({ /> )} -

Tokens

- {tokens && ( - - )} + + +

Tokens

+ {tokens && ( + + )} +
+
); diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index 04ebe917b41..4b45b16103d 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -15,6 +15,18 @@ export function loadServiceAccount(saID: number): ThunkResult { }; } +export function createServiceAccountToken( + saID: number, + data: any, + onTokenCreated: (key: string) => void +): ThunkResult { + return async (dispatch) => { + const result = await getBackendSrv().post(`${BASE_URL}/${saID}/tokens`, data); + onTokenCreated(result.key); + dispatch(loadServiceAccountTokens(saID)); + }; +} + export function deleteServiceAccountToken(saID: number, id: number): ThunkResult { return async (dispatch) => { await getBackendSrv().delete(`${BASE_URL}/${saID}/tokens/${id}`);