import { css } from '@emotion/css'; import pluralize from 'pluralize'; import { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { GrafanaTheme2, OrgRole } from '@grafana/data'; import { ConfirmModal, FilterInput, LinkButton, RadioButtonGroup, InlineField, EmptyState, Box, Stack, useStyles2, } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { Trans, t } from 'app/core/internationalization'; import { StoreState, ServiceAccountDTO, AccessControlAction, ServiceAccountStateFilter } from 'app/types'; import { ServiceAccountTable } from './ServiceAccountTable'; import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal'; import { changeQuery, changePage, fetchACOptions, fetchServiceAccounts, deleteServiceAccount, updateServiceAccount, changeStateFilter, createServiceAccountToken, } from './state/actions'; interface OwnProps {} export type Props = OwnProps & ConnectedProps; function mapStateToProps(state: StoreState) { return { ...state.serviceAccounts, }; } const mapDispatchToProps = { changePage, changeQuery, fetchACOptions, fetchServiceAccounts, deleteServiceAccount, updateServiceAccount, changeStateFilter, createServiceAccountToken, }; const connector = connect(mapStateToProps, mapDispatchToProps); const availableFilters = [ { label: 'All', value: ServiceAccountStateFilter.All }, { label: 'With expired tokens', value: ServiceAccountStateFilter.WithExpiredTokens }, { label: 'Disabled', value: ServiceAccountStateFilter.Disabled }, ]; if (config.featureToggles.externalServiceAccounts) { availableFilters.push({ label: 'Managed', value: ServiceAccountStateFilter.External }); } export const ServiceAccountsListPageUnconnected = ({ page, changePage, totalPages, serviceAccounts, isLoading, roleOptions, query, serviceAccountStateFilter, changeQuery, fetchACOptions, fetchServiceAccounts, deleteServiceAccount, updateServiceAccount, changeStateFilter, createServiceAccountToken, }: Props): JSX.Element => { const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false); const [isDisableModalOpen, setIsDisableModalOpen] = useState(false); const [newToken, setNewToken] = useState(''); const [currentServiceAccount, setCurrentServiceAccount] = useState(null); const styles = useStyles2(getStyles); useEffect(() => { fetchServiceAccounts({ withLoadingIndicator: true }); if (contextSrv.licensedAccessControlEnabled()) { fetchACOptions(); } }, [fetchACOptions, fetchServiceAccounts]); const noServiceAccountsCreated = serviceAccounts.length === 0 && serviceAccountStateFilter === ServiceAccountStateFilter.All && !query; const onRoleChange = async (role: OrgRole, serviceAccount: ServiceAccountDTO) => { const updatedServiceAccount = { ...serviceAccount, role: role }; updateServiceAccount(updatedServiceAccount); if (contextSrv.licensedAccessControlEnabled()) { fetchACOptions(); } }; const onQueryChange = (value: string) => { changeQuery(value); }; const onStateFilterChange = (value: ServiceAccountStateFilter) => { changeStateFilter(value); }; const onRemoveButtonClick = (serviceAccount: ServiceAccountDTO) => { setCurrentServiceAccount(serviceAccount); setIsRemoveModalOpen(true); }; const onServiceAccountRemove = async () => { if (currentServiceAccount) { deleteServiceAccount(currentServiceAccount.uid); } onRemoveModalClose(); }; const onDisableButtonClick = (serviceAccount: ServiceAccountDTO) => { setCurrentServiceAccount(serviceAccount); setIsDisableModalOpen(true); }; const onDisable = () => { if (currentServiceAccount) { updateServiceAccount({ ...currentServiceAccount, isDisabled: true }); } onDisableModalClose(); }; const onEnable = (serviceAccount: ServiceAccountDTO) => { updateServiceAccount({ ...serviceAccount, isDisabled: false }); }; const onTokenAdd = (serviceAccount: ServiceAccountDTO) => { setCurrentServiceAccount(serviceAccount); setIsAddModalOpen(true); }; const onTokenCreate = async (token: ServiceAccountToken) => { if (currentServiceAccount) { createServiceAccountToken(currentServiceAccount.uid, token, setNewToken); } }; const onAddModalClose = () => { setIsAddModalOpen(false); setCurrentServiceAccount(null); setNewToken(''); }; const onRemoveModalClose = () => { setIsRemoveModalOpen(false); setCurrentServiceAccount(null); }; const onDisableModalClose = () => { setIsDisableModalOpen(false); setCurrentServiceAccount(null); }; const docsLink = ( documentation. ); const subTitle = ( Service accounts and their tokens can be used to authenticate against the Grafana API. Find out more in our{' '} {docsLink} ); return ( {!noServiceAccountsCreated && contextSrv.hasPermission(AccessControlAction.ServiceAccountsCreate) && ( Add service account )} } > {!isLoading && !noServiceAccountsCreated && serviceAccounts.length === 0 && ( )} {!isLoading && noServiceAccountsCreated && ( Add service account } message={t('service-accounts.empty-state.title', "You haven't created any service accounts yet")} > Remember, you can provide specific permissions for API access to other applications )} {(isLoading || serviceAccounts.length !== 0) && ( )} {currentServiceAccount && ( <> )} ); }; const getStyles = (theme: GrafanaTheme2) => ({ filterInput: css({ maxWidth: theme.spacing(50), }), }); const ServiceAccountsListPage = connector(ServiceAccountsListPageUnconnected); export default ServiceAccountsListPage;