ServiceAccounts: allow deleting from service account list (#45239)

* ServiceAccounts: allow deleting from service account list

* Make delete confirmation more explicit
This commit is contained in:
J Guerreiro 2022-02-10 17:21:04 +00:00 committed by GitHub
parent d2b9da9dde
commit cb461d931f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 71 additions and 3 deletions

View File

@ -1,11 +1,17 @@
import React, { memo, useEffect } from 'react'; import React, { memo, useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import { Icon, LinkButton, useStyles2 } from '@grafana/ui'; import { Button, ConfirmModal, Icon, LinkButton, useStyles2 } from '@grafana/ui';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import Page from 'app/core/components/Page/Page'; import Page from 'app/core/components/Page/Page';
import { StoreState, ServiceAccountDTO, AccessControlAction, Role } from 'app/types'; import { StoreState, ServiceAccountDTO, AccessControlAction, Role } from 'app/types';
import { fetchACOptions, loadServiceAccounts, removeServiceAccount, updateServiceAccount } from './state/actions'; import {
fetchACOptions,
loadServiceAccounts,
removeServiceAccount,
updateServiceAccount,
setServiceAccountToRemove,
} from './state/actions';
import { getNavModel } from 'app/core/selectors/navModel'; import { getNavModel } from 'app/core/selectors/navModel';
import { getServiceAccounts, getServiceAccountsSearchPage, getServiceAccountsSearchQuery } from './state/selectors'; import { getServiceAccounts, getServiceAccountsSearchPage, getServiceAccountsSearchQuery } from './state/selectors';
import PageLoader from 'app/core/components/PageLoader/PageLoader'; import PageLoader from 'app/core/components/PageLoader/PageLoader';
@ -13,6 +19,7 @@ import { GrafanaTheme2, OrgRole } from '@grafana/data';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
import { OrgRolePicker } from '../admin/OrgRolePicker'; import { OrgRolePicker } from '../admin/OrgRolePicker';
import pluralize from 'pluralize';
export type Props = ConnectedProps<typeof connector>; export type Props = ConnectedProps<typeof connector>;
function mapStateToProps(state: StoreState) { function mapStateToProps(state: StoreState) {
@ -24,6 +31,7 @@ function mapStateToProps(state: StoreState) {
isLoading: state.serviceAccounts.isLoading, isLoading: state.serviceAccounts.isLoading,
roleOptions: state.serviceAccounts.roleOptions, roleOptions: state.serviceAccounts.roleOptions,
builtInRoles: state.serviceAccounts.builtInRoles, builtInRoles: state.serviceAccounts.builtInRoles,
toRemove: state.serviceAccounts.serviceAccountToRemove,
}; };
} }
@ -32,21 +40,26 @@ const mapDispatchToProps = {
fetchACOptions, fetchACOptions,
updateServiceAccount, updateServiceAccount,
removeServiceAccount, removeServiceAccount,
setServiceAccountToRemove,
}; };
const connector = connect(mapStateToProps, mapDispatchToProps); const connector = connect(mapStateToProps, mapDispatchToProps);
const ServiceAccountsListPage = ({ const ServiceAccountsListPage = ({
loadServiceAccounts, loadServiceAccounts,
removeServiceAccount,
fetchACOptions, fetchACOptions,
updateServiceAccount, updateServiceAccount,
setServiceAccountToRemove,
navModel, navModel,
serviceAccounts, serviceAccounts,
isLoading, isLoading,
roleOptions, roleOptions,
builtInRoles, builtInRoles,
toRemove,
}: Props) => { }: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
useEffect(() => { useEffect(() => {
loadServiceAccounts(); loadServiceAccounts();
if (contextSrv.accessControlEnabled()) { if (contextSrv.accessControlEnabled()) {
@ -84,6 +97,7 @@ const ServiceAccountsListPage = ({
<th>ID</th> <th>ID</th>
<th>Roles</th> <th>Roles</th>
<th>Tokens</th> <th>Tokens</th>
<th style={{ width: '34px' }} />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -94,6 +108,7 @@ const ServiceAccountsListPage = ({
builtInRoles={builtInRoles} builtInRoles={builtInRoles}
roleOptions={roleOptions} roleOptions={roleOptions}
onRoleChange={onRoleChange} onRoleChange={onRoleChange}
onSetToRemove={setServiceAccountToRemove}
/> />
))} ))}
</tbody> </tbody>
@ -101,6 +116,28 @@ const ServiceAccountsListPage = ({
</div> </div>
</> </>
)} )}
{toRemove && (
<ConfirmModal
body={
<div>
Are you sure you want to delete &apos;{toRemove.name}&apos;
{Boolean(toRemove.tokens) &&
` and ${toRemove.tokens} accompanying ${pluralize('token', toRemove.tokens)}`}
?
</div>
}
confirmText="Delete"
title="Delete service account"
onDismiss={() => {
setServiceAccountToRemove(null);
}}
isOpen={true}
onConfirm={() => {
removeServiceAccount(toRemove.id);
setServiceAccountToRemove(null);
}}
/>
)}
</Page.Contents> </Page.Contents>
</Page> </Page>
); );
@ -111,6 +148,7 @@ type ServiceAccountListItemProps = {
onRoleChange: (role: OrgRole, serviceAccount: ServiceAccountDTO) => void; onRoleChange: (role: OrgRole, serviceAccount: ServiceAccountDTO) => void;
roleOptions: Role[]; roleOptions: Role[];
builtInRoles: Record<string, Role[]>; builtInRoles: Record<string, Role[]>;
onSetToRemove: (serviceAccount: ServiceAccountDTO) => void;
}; };
const getServiceAccountsAriaLabel = (name: string) => { const getServiceAccountsAriaLabel = (name: string) => {
@ -118,7 +156,7 @@ const getServiceAccountsAriaLabel = (name: string) => {
}; };
const ServiceAccountListItem = memo( const ServiceAccountListItem = memo(
({ serviceAccount, onRoleChange, roleOptions, builtInRoles }: ServiceAccountListItemProps) => { ({ serviceAccount, onRoleChange, roleOptions, builtInRoles, onSetToRemove }: ServiceAccountListItemProps) => {
const editUrl = `org/serviceAccounts/${serviceAccount.id}`; const editUrl = `org/serviceAccounts/${serviceAccount.id}`;
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const canUpdateRole = contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsWrite, serviceAccount); const canUpdateRole = contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsWrite, serviceAccount);
@ -188,6 +226,19 @@ const ServiceAccountListItem = memo(
{serviceAccount.tokens} {serviceAccount.tokens}
</a> </a>
</td> </td>
{contextSrv.hasPermissionInMetadata(AccessControlAction.ServiceAccountsDelete, serviceAccount) && (
<td>
<Button
size="sm"
variant="destructive"
onClick={() => {
onSetToRemove(serviceAccount);
}}
icon="times"
aria-label="Delete service account"
/>
</td>
)}
</tr> </tr>
); );
} }

View File

@ -6,6 +6,7 @@ import {
serviceAccountLoaded, serviceAccountLoaded,
serviceAccountsLoaded, serviceAccountsLoaded,
serviceAccountTokensLoaded, serviceAccountTokensLoaded,
serviceAccountToRemoveLoaded,
} from './reducers'; } from './reducers';
import { accessControlQueryParam } from 'app/core/utils/accessControl'; import { accessControlQueryParam } from 'app/core/utils/accessControl';
import { fetchBuiltinRoles, fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { fetchBuiltinRoles, fetchRoleOptions } from 'app/core/components/RolePicker/api';
@ -25,6 +26,16 @@ export function fetchACOptions(): ThunkResult<void> {
}; };
} }
export function setServiceAccountToRemove(serviceAccount: ServiceAccountDTO | null): ThunkResult<void> {
return async (dispatch) => {
try {
dispatch(serviceAccountToRemoveLoaded(serviceAccount));
} catch (error) {
console.error(error);
}
};
}
export function loadServiceAccount(saID: number): ThunkResult<void> { export function loadServiceAccount(saID: number): ThunkResult<void> {
return async (dispatch) => { return async (dispatch) => {
try { try {

View File

@ -9,6 +9,7 @@ export const initialState: ServiceAccountsState = {
isLoading: true, isLoading: true,
builtInRoles: {}, builtInRoles: {},
roleOptions: [], roleOptions: [],
serviceAccountToRemove: null,
}; };
export const initialStateProfile: ServiceAccountProfileState = { export const initialStateProfile: ServiceAccountProfileState = {
@ -50,6 +51,9 @@ const serviceAccountsSlice = createSlice({
builtInRolesLoaded: (state, action: PayloadAction<Record<string, Role[]>>): ServiceAccountsState => { builtInRolesLoaded: (state, action: PayloadAction<Record<string, Role[]>>): ServiceAccountsState => {
return { ...state, builtInRoles: action.payload }; return { ...state, builtInRoles: action.payload };
}, },
serviceAccountToRemoveLoaded: (state, action: PayloadAction<ServiceAccountDTO | null>): ServiceAccountsState => {
return { ...state, serviceAccountToRemove: action.payload };
},
}, },
}); });
@ -59,6 +63,7 @@ export const {
serviceAccountsLoaded, serviceAccountsLoaded,
acOptionsLoaded, acOptionsLoaded,
builtInRolesLoaded, builtInRolesLoaded,
serviceAccountToRemoveLoaded,
} = serviceAccountsSlice.actions; } = serviceAccountsSlice.actions;
export const { serviceAccountLoaded, serviceAccountTokensLoaded } = serviceAccountProfileSlice.actions; export const { serviceAccountLoaded, serviceAccountTokensLoaded } = serviceAccountProfileSlice.actions;

View File

@ -46,5 +46,6 @@ export interface ServiceAccountsState {
searchPage: number; searchPage: number;
isLoading: boolean; isLoading: boolean;
roleOptions: Role[]; roleOptions: Role[];
serviceAccountToRemove: ServiceAccountDTO | null;
builtInRoles: Record<string, Role[]>; builtInRoles: Record<string, Role[]>;
} }