mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -06:00
340 lines
11 KiB
TypeScript
340 lines
11 KiB
TypeScript
import React, { memo, useEffect } from 'react';
|
|
import { connect, ConnectedProps } from 'react-redux';
|
|
import { Button, ConfirmModal, FilterInput, Icon, LinkButton, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
|
import { css, cx } from '@emotion/css';
|
|
|
|
import Page from 'app/core/components/Page/Page';
|
|
import { StoreState, ServiceAccountDTO, AccessControlAction, Role } from 'app/types';
|
|
import {
|
|
changeFilter,
|
|
changeQuery,
|
|
fetchACOptions,
|
|
fetchServiceAccounts,
|
|
removeServiceAccount,
|
|
updateServiceAccount,
|
|
setServiceAccountToRemove,
|
|
} from './state/actions';
|
|
import { getNavModel } from 'app/core/selectors/navModel';
|
|
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
|
import { GrafanaTheme2, OrgRole } from '@grafana/data';
|
|
import { contextSrv } from 'app/core/core';
|
|
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
|
|
import { OrgRolePicker } from '../admin/OrgRolePicker';
|
|
import pluralize from 'pluralize';
|
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
|
|
|
interface OwnProps {}
|
|
|
|
type Props = OwnProps & ConnectedProps<typeof connector>;
|
|
|
|
function mapStateToProps(state: StoreState) {
|
|
return {
|
|
navModel: getNavModel(state.navIndex, 'serviceaccounts'),
|
|
...state.serviceAccounts,
|
|
};
|
|
}
|
|
|
|
const mapDispatchToProps = {
|
|
fetchServiceAccounts,
|
|
fetchACOptions,
|
|
updateServiceAccount,
|
|
removeServiceAccount,
|
|
setServiceAccountToRemove,
|
|
changeFilter,
|
|
changeQuery,
|
|
};
|
|
|
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
|
|
|
const ServiceAccountsListPage = ({
|
|
fetchServiceAccounts,
|
|
removeServiceAccount,
|
|
fetchACOptions,
|
|
updateServiceAccount,
|
|
setServiceAccountToRemove,
|
|
navModel,
|
|
serviceAccounts,
|
|
isLoading,
|
|
roleOptions,
|
|
builtInRoles,
|
|
changeFilter,
|
|
changeQuery,
|
|
query,
|
|
filters,
|
|
serviceAccountToRemove,
|
|
}: Props) => {
|
|
const styles = useStyles2(getStyles);
|
|
|
|
useEffect(() => {
|
|
fetchServiceAccounts();
|
|
if (contextSrv.licensedAccessControlEnabled()) {
|
|
fetchACOptions();
|
|
}
|
|
}, [fetchServiceAccounts, fetchACOptions]);
|
|
|
|
const onRoleChange = (role: OrgRole, serviceAccount: ServiceAccountDTO) => {
|
|
const updatedServiceAccount = { ...serviceAccount, role: role };
|
|
updateServiceAccount(updatedServiceAccount);
|
|
};
|
|
return (
|
|
<Page navModel={navModel}>
|
|
<Page.Contents>
|
|
<h2>Service accounts</h2>
|
|
<div className="page-action-bar" style={{ justifyContent: 'flex-end' }}>
|
|
<FilterInput
|
|
placeholder="Search service account by name."
|
|
autoFocus={true}
|
|
value={query}
|
|
onChange={changeQuery}
|
|
/>
|
|
<RadioButtonGroup
|
|
options={[
|
|
{ label: 'All service accounts', value: false },
|
|
{ label: 'Expired tokens', value: true },
|
|
]}
|
|
onChange={(value) => changeFilter({ name: 'expiredTokens', value })}
|
|
value={filters.find((f) => f.name === 'expiredTokens')?.value}
|
|
className={styles.filter}
|
|
/>
|
|
</div>
|
|
{isLoading && <PageLoader />}
|
|
{!isLoading && serviceAccounts.length === 0 && (
|
|
<>
|
|
<EmptyListCTA
|
|
title="You haven't created any service accounts yet."
|
|
buttonIcon="key-skeleton-alt"
|
|
buttonLink="org/serviceaccounts/create"
|
|
buttonTitle=" New service account"
|
|
buttonDisabled={!contextSrv.hasPermission(AccessControlAction.ServiceAccountsCreate)}
|
|
proTip="Remember, you can provide specific permissions for API access to other applications."
|
|
proTipLink=""
|
|
proTipLinkTitle=""
|
|
proTipTarget="_blank"
|
|
/>
|
|
</>
|
|
)}
|
|
{!isLoading && serviceAccounts.length !== 0 && (
|
|
<>
|
|
{contextSrv.hasPermission(AccessControlAction.ServiceAccountsCreate) && (
|
|
<LinkButton href="org/serviceaccounts/create" variant="primary">
|
|
New service account
|
|
</LinkButton>
|
|
)}
|
|
<div className={cx(styles.table, 'admin-list-table')}>
|
|
<table className="filter-table form-inline filter-table--hover">
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th>Display name</th>
|
|
<th>ID</th>
|
|
<th>Roles</th>
|
|
<th>Status</th>
|
|
<th>Tokens</th>
|
|
<th style={{ width: '34px' }} />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{serviceAccounts.map((serviceAccount: ServiceAccountDTO) => (
|
|
<ServiceAccountListItem
|
|
serviceAccount={serviceAccount}
|
|
key={serviceAccount.id}
|
|
builtInRoles={builtInRoles}
|
|
roleOptions={roleOptions}
|
|
onRoleChange={onRoleChange}
|
|
onSetToRemove={setServiceAccountToRemove}
|
|
/>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</>
|
|
)}
|
|
{serviceAccountToRemove && (
|
|
<ConfirmModal
|
|
body={
|
|
<div>
|
|
Are you sure you want to delete '{serviceAccountToRemove.name}'
|
|
{Boolean(serviceAccountToRemove.tokens) &&
|
|
` and ${serviceAccountToRemove.tokens} accompanying ${pluralize(
|
|
'token',
|
|
serviceAccountToRemove.tokens
|
|
)}`}
|
|
?
|
|
</div>
|
|
}
|
|
confirmText="Delete"
|
|
title="Delete service account"
|
|
onDismiss={() => {
|
|
setServiceAccountToRemove(null);
|
|
}}
|
|
isOpen={true}
|
|
onConfirm={() => {
|
|
removeServiceAccount(serviceAccountToRemove.id);
|
|
setServiceAccountToRemove(null);
|
|
}}
|
|
/>
|
|
)}
|
|
</Page.Contents>
|
|
</Page>
|
|
);
|
|
};
|
|
|
|
type ServiceAccountListItemProps = {
|
|
serviceAccount: ServiceAccountDTO;
|
|
onRoleChange: (role: OrgRole, serviceAccount: ServiceAccountDTO) => void;
|
|
roleOptions: Role[];
|
|
builtInRoles: Record<string, Role[]>;
|
|
onSetToRemove: (serviceAccount: ServiceAccountDTO) => void;
|
|
};
|
|
|
|
const getServiceAccountsAriaLabel = (name: string) => {
|
|
return `Edit service account's ${name} details`;
|
|
};
|
|
const getServiceAccountsEnabledStatus = (disabled: boolean) => {
|
|
return disabled ? 'Disabled' : 'Enabled';
|
|
};
|
|
|
|
const ServiceAccountListItem = memo(
|
|
({ serviceAccount, onRoleChange, roleOptions, builtInRoles, onSetToRemove }: ServiceAccountListItemProps) => {
|
|
const editUrl = `org/serviceaccounts/${serviceAccount.id}`;
|
|
const styles = useStyles2(getStyles);
|
|
const canUpdateRole = contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsWrite, serviceAccount);
|
|
const rolePickerDisabled = !canUpdateRole;
|
|
|
|
return (
|
|
<tr key={serviceAccount.id}>
|
|
<td className="width-4 text-center link-td">
|
|
<a href={editUrl} aria-label={getServiceAccountsAriaLabel(serviceAccount.name)}>
|
|
<img
|
|
className="filter-table__avatar"
|
|
src={serviceAccount.avatarUrl}
|
|
alt={`Avatar for user ${serviceAccount.name}`}
|
|
/>
|
|
</a>
|
|
</td>
|
|
<td className="link-td max-width-10">
|
|
<a
|
|
className="ellipsis"
|
|
href={editUrl}
|
|
title={serviceAccount.name}
|
|
aria-label={getServiceAccountsAriaLabel(serviceAccount.name)}
|
|
>
|
|
{serviceAccount.name}
|
|
</a>
|
|
</td>
|
|
<td className="link-td max-width-10">
|
|
<a
|
|
className="ellipsis"
|
|
href={editUrl}
|
|
title={serviceAccount.login}
|
|
aria-label={getServiceAccountsAriaLabel(serviceAccount.name)}
|
|
>
|
|
{serviceAccount.login}
|
|
</a>
|
|
</td>
|
|
<td className={cx('link-td', styles.iconRow)}>
|
|
{contextSrv.licensedAccessControlEnabled() ? (
|
|
<UserRolePicker
|
|
userId={serviceAccount.id}
|
|
orgId={serviceAccount.orgId}
|
|
builtInRole={serviceAccount.role}
|
|
onBuiltinRoleChange={(newRole) => onRoleChange(newRole, serviceAccount)}
|
|
roleOptions={roleOptions}
|
|
builtInRoles={builtInRoles}
|
|
disabled={rolePickerDisabled}
|
|
/>
|
|
) : (
|
|
<OrgRolePicker
|
|
aria-label="Role"
|
|
value={serviceAccount.role}
|
|
disabled={!canUpdateRole}
|
|
onChange={(newRole) => onRoleChange(newRole, serviceAccount)}
|
|
/>
|
|
)}
|
|
</td>
|
|
<td className="link-td max-width-10">
|
|
<a
|
|
className="ellipsis"
|
|
href={editUrl}
|
|
title={getServiceAccountsEnabledStatus(serviceAccount.isDisabled)}
|
|
aria-label={getServiceAccountsAriaLabel(serviceAccount.name)}
|
|
>
|
|
{getServiceAccountsEnabledStatus(serviceAccount.isDisabled)}
|
|
</a>
|
|
</td>
|
|
<td className="link-td max-width-10">
|
|
<a
|
|
className="ellipsis"
|
|
href={editUrl}
|
|
title="Tokens"
|
|
aria-label={getServiceAccountsAriaLabel(serviceAccount.name)}
|
|
>
|
|
<span>
|
|
<Icon name={'key-skeleton-alt'}></Icon>
|
|
</span>
|
|
{serviceAccount.tokens}
|
|
</a>
|
|
</td>
|
|
{contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsDelete, serviceAccount) && (
|
|
<td>
|
|
<Button
|
|
size="sm"
|
|
variant="destructive"
|
|
onClick={() => {
|
|
onSetToRemove(serviceAccount);
|
|
}}
|
|
icon="times"
|
|
aria-label="Delete service account"
|
|
/>
|
|
</td>
|
|
)}
|
|
</tr>
|
|
);
|
|
}
|
|
);
|
|
ServiceAccountListItem.displayName = 'ServiceAccountListItem';
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => {
|
|
return {
|
|
table: css`
|
|
margin-top: ${theme.spacing(3)};
|
|
`,
|
|
filter: css`
|
|
margin: 0 ${theme.spacing(1)};
|
|
`,
|
|
iconRow: css`
|
|
svg {
|
|
margin-left: ${theme.spacing(0.5)};
|
|
}
|
|
`,
|
|
row: css`
|
|
display: flex;
|
|
align-items: center;
|
|
height: 100% !important;
|
|
|
|
a {
|
|
padding: ${theme.spacing(0.5)} 0 !important;
|
|
}
|
|
`,
|
|
unitTooltip: css`
|
|
display: flex;
|
|
flex-direction: column;
|
|
`,
|
|
unitItem: css`
|
|
cursor: pointer;
|
|
padding: ${theme.spacing(0.5)} 0;
|
|
margin-right: ${theme.spacing(1)};
|
|
`,
|
|
disabled: css`
|
|
color: ${theme.colors.text.disabled};
|
|
`,
|
|
link: css`
|
|
color: inherit;
|
|
cursor: pointer;
|
|
text-decoration: underline;
|
|
`,
|
|
};
|
|
};
|
|
|
|
export default connector(ServiceAccountsListPage);
|