mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Serviceaccounts: feat - tabview for serviceaccounts (#43573)
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { HorizontalGroup, Pagination, VerticalGroup } from '@grafana/ui';
|
||||
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import ServiceAccountsTable from './ServiceAccountsTable';
|
||||
import { OrgServiceAccount, OrgRole, StoreState } from 'app/types';
|
||||
import { loadServiceAccounts, removeServiceAccount, updateServiceAccount } from './state/actions';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { getServiceAccounts, getServiceAccountsSearchPage, getServiceAccountsSearchQuery } from './state/selectors';
|
||||
import { setServiceAccountsSearchPage } from './state/reducers';
|
||||
export type Props = ConnectedProps<typeof connector>;
|
||||
|
||||
export interface State {}
|
||||
|
||||
const ITEMS_PER_PAGE = 30;
|
||||
|
||||
export class ServiceAccountsListPage extends PureComponent<Props, State> {
|
||||
componentDidMount() {
|
||||
this.fetchServiceAccounts();
|
||||
}
|
||||
|
||||
async fetchServiceAccounts() {
|
||||
return this.props.loadServiceAccounts();
|
||||
}
|
||||
|
||||
onRoleChange = (role: OrgRole, serviceAccount: OrgServiceAccount) => {
|
||||
const updatedServiceAccount = { ...serviceAccount, role: role };
|
||||
|
||||
this.props.updateServiceAccount(updatedServiceAccount);
|
||||
};
|
||||
|
||||
getPaginatedServiceAccounts = (serviceAccounts: OrgServiceAccount[]) => {
|
||||
const offset = (this.props.searchPage - 1) * ITEMS_PER_PAGE;
|
||||
return serviceAccounts.slice(offset, offset + ITEMS_PER_PAGE);
|
||||
};
|
||||
|
||||
renderTable() {
|
||||
const { serviceAccounts } = this.props;
|
||||
const paginatedServiceAccounts = this.getPaginatedServiceAccounts(serviceAccounts);
|
||||
const totalPages = Math.ceil(serviceAccounts.length / ITEMS_PER_PAGE);
|
||||
|
||||
return (
|
||||
<VerticalGroup spacing="md">
|
||||
<ServiceAccountsTable
|
||||
serviceAccounts={paginatedServiceAccounts}
|
||||
onRoleChange={(role, serviceAccount) => this.onRoleChange(role, serviceAccount)}
|
||||
onRemoveServiceaccount={(serviceAccount) => this.props.removeServiceAccount(serviceAccount.serviceAccountId)}
|
||||
/>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Pagination
|
||||
onNavigate={setServiceAccountsSearchPage}
|
||||
currentPage={this.props.searchPage}
|
||||
numberOfPages={totalPages}
|
||||
hideWhenSinglePage={true}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navModel, hasFetched } = this.props;
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents isLoading={!hasFetched}>
|
||||
<>{hasFetched && this.renderTable()}</>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: StoreState) {
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, 'serviceaccounts'),
|
||||
serviceAccounts: getServiceAccounts(state.serviceAccounts),
|
||||
searchQuery: getServiceAccountsSearchQuery(state.serviceAccounts),
|
||||
searchPage: getServiceAccountsSearchPage(state.serviceAccounts),
|
||||
hasFetched: state.serviceAccounts.isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadServiceAccounts,
|
||||
updateServiceAccount,
|
||||
removeServiceAccount,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export default connector(ServiceAccountsListPage);
|
||||
133
public/app/features/serviceaccounts/ServiceAccountsTable.tsx
Normal file
133
public/app/features/serviceaccounts/ServiceAccountsTable.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { AccessControlAction, Role, OrgServiceAccount } from 'app/types';
|
||||
import { OrgRolePicker } from '../admin/OrgRolePicker';
|
||||
import { Button, ConfirmModal } from '@grafana/ui';
|
||||
import { OrgRole } from '@grafana/data';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { fetchBuiltinRoles, fetchRoleOptions, UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
|
||||
|
||||
export interface Props {
|
||||
serviceAccounts: OrgServiceAccount[];
|
||||
orgId?: number;
|
||||
onRoleChange: (role: OrgRole, serviceaccount: OrgServiceAccount) => void;
|
||||
onRemoveServiceaccount: (serviceaccount: OrgServiceAccount) => void;
|
||||
}
|
||||
|
||||
const ServiceaccountsTable: FC<Props> = (props) => {
|
||||
const { serviceAccounts, orgId, onRoleChange, onRemoveServiceaccount: onRemoveserviceaccount } = props;
|
||||
const canUpdateRole = contextSrv.hasPermission(AccessControlAction.OrgUsersRoleUpdate);
|
||||
const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove);
|
||||
const rolePickerDisabled = !canUpdateRole;
|
||||
|
||||
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
||||
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
||||
const [builtinRoles, setBuiltinRoles] = useState<Record<string, Role[]>>({});
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchOptions() {
|
||||
try {
|
||||
let options = await fetchRoleOptions(orgId);
|
||||
setRoleOptions(options);
|
||||
const builtInRoles = await fetchBuiltinRoles(orgId);
|
||||
setBuiltinRoles(builtInRoles);
|
||||
} catch (e) {
|
||||
console.error('Error loading options');
|
||||
}
|
||||
}
|
||||
if (contextSrv.accessControlEnabled()) {
|
||||
fetchOptions();
|
||||
}
|
||||
}, [orgId]);
|
||||
|
||||
const getRoleOptions = async () => roleOptions;
|
||||
const getBuiltinRoles = async () => builtinRoles;
|
||||
|
||||
return (
|
||||
<table className="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th>Name</th>
|
||||
<th>Seen</th>
|
||||
<th>Role</th>
|
||||
<th style={{ width: '34px' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{serviceAccounts.map((serviceAccount, index) => {
|
||||
return (
|
||||
<tr key={`${serviceAccount.serviceAccountId}-${index}`}>
|
||||
<td className="width-2 text-center">
|
||||
<img className="filter-table__avatar" src={serviceAccount.avatarUrl} alt="serviceaccount avatar" />
|
||||
</td>
|
||||
<td className="max-width-6">
|
||||
<span className="ellipsis" title={serviceAccount.login}>
|
||||
{serviceAccount.login}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="max-width-5">
|
||||
<span className="ellipsis" title={serviceAccount.email}>
|
||||
{serviceAccount.email}
|
||||
</span>
|
||||
</td>
|
||||
<td className="max-width-5">
|
||||
<span className="ellipsis" title={serviceAccount.name}>
|
||||
{serviceAccount.name}
|
||||
</span>
|
||||
</td>
|
||||
<td className="width-1">{serviceAccount.lastSeenAtAge}</td>
|
||||
|
||||
<td className="width-8">
|
||||
{contextSrv.accessControlEnabled() ? (
|
||||
<UserRolePicker
|
||||
userId={serviceAccount.serviceAccountId}
|
||||
orgId={orgId}
|
||||
builtInRole={serviceAccount.role}
|
||||
onBuiltinRoleChange={(newRole) => onRoleChange(newRole, serviceAccount)}
|
||||
getRoleOptions={getRoleOptions}
|
||||
getBuiltinRoles={getBuiltinRoles}
|
||||
disabled={rolePickerDisabled}
|
||||
/>
|
||||
) : (
|
||||
<OrgRolePicker
|
||||
aria-label="Role"
|
||||
value={serviceAccount.role}
|
||||
disabled={!canUpdateRole}
|
||||
onChange={(newRole) => onRoleChange(newRole, serviceAccount)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{canRemoveFromOrg && (
|
||||
<td>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => setShowRemoveModal(Boolean(serviceAccount.login))}
|
||||
icon="times"
|
||||
aria-label="Delete serviceaccount"
|
||||
/>
|
||||
<ConfirmModal
|
||||
body={`Are you sure you want to delete serviceaccount ${serviceAccount.login}?`}
|
||||
confirmText="Delete"
|
||||
title="Delete"
|
||||
onDismiss={() => setShowRemoveModal(false)}
|
||||
isOpen={Boolean(serviceAccount.login) === showRemoveModal}
|
||||
onConfirm={() => {
|
||||
onRemoveserviceaccount(serviceAccount);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceaccountsTable;
|
||||
28
public/app/features/serviceaccounts/state/actions.ts
Normal file
28
public/app/features/serviceaccounts/state/actions.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ThunkResult } from '../../../types';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { OrgServiceAccount as OrgServiceAccount } from 'app/types';
|
||||
import { serviceAccountsLoaded } from './reducers';
|
||||
|
||||
export function loadServiceAccounts(): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
const serviceAccounts = await getBackendSrv().get('/api/serviceaccounts');
|
||||
dispatch(serviceAccountsLoaded(serviceAccounts));
|
||||
};
|
||||
}
|
||||
|
||||
export function updateServiceAccount(serviceAccount: OrgServiceAccount): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
// TODO: implement on backend
|
||||
await getBackendSrv().patch(`/api/serviceaccounts/${serviceAccount.serviceAccountId}`, {
|
||||
role: serviceAccount.role,
|
||||
});
|
||||
dispatch(loadServiceAccounts());
|
||||
};
|
||||
}
|
||||
|
||||
export function removeServiceAccount(serviceAccountId: number): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
await getBackendSrv().delete(`/api/serviceaccounts/${serviceAccountId}`);
|
||||
dispatch(loadServiceAccounts());
|
||||
};
|
||||
}
|
||||
39
public/app/features/serviceaccounts/state/reducers.ts
Normal file
39
public/app/features/serviceaccounts/state/reducers.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { OrgServiceAccount, ServiceAccountsState } from 'app/types';
|
||||
|
||||
export const initialState: ServiceAccountsState = {
|
||||
serviceAccounts: [] as OrgServiceAccount[],
|
||||
searchQuery: '',
|
||||
searchPage: 1,
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
const serviceAccountsSlice = createSlice({
|
||||
name: 'serviceaccounts',
|
||||
initialState,
|
||||
reducers: {
|
||||
serviceAccountsLoaded: (state, action: PayloadAction<OrgServiceAccount[]>): ServiceAccountsState => {
|
||||
return { ...state, isLoading: true, serviceAccounts: action.payload };
|
||||
},
|
||||
setServiceAccountsSearchQuery: (state, action: PayloadAction<string>): ServiceAccountsState => {
|
||||
// reset searchPage otherwise search results won't appear
|
||||
return { ...state, searchQuery: action.payload, searchPage: initialState.searchPage };
|
||||
},
|
||||
setServiceAccountsSearchPage: (state, action: PayloadAction<number>): ServiceAccountsState => {
|
||||
return { ...state, searchPage: action.payload };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setServiceAccountsSearchQuery,
|
||||
setServiceAccountsSearchPage,
|
||||
serviceAccountsLoaded,
|
||||
} = serviceAccountsSlice.actions;
|
||||
|
||||
export const serviceAccountsReducer = serviceAccountsSlice.reducer;
|
||||
|
||||
export default {
|
||||
serviceAccounts: serviceAccountsReducer,
|
||||
};
|
||||
12
public/app/features/serviceaccounts/state/selectors.ts
Normal file
12
public/app/features/serviceaccounts/state/selectors.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ServiceAccountsState } from 'app/types';
|
||||
|
||||
export const getServiceAccounts = (state: ServiceAccountsState) => {
|
||||
const regex = new RegExp(state.searchQuery, 'i');
|
||||
|
||||
return state.serviceAccounts.filter((serviceaccount) => {
|
||||
return regex.test(serviceaccount.login) || regex.test(serviceaccount.email) || regex.test(serviceaccount.name);
|
||||
});
|
||||
};
|
||||
|
||||
export const getServiceAccountsSearchQuery = (state: ServiceAccountsState) => state.searchQuery;
|
||||
export const getServiceAccountsSearchPage = (state: ServiceAccountsState) => state.searchPage;
|
||||
Reference in New Issue
Block a user