mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
parent
30ead91d38
commit
74cb1423c1
@ -1,6 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import {
|
||||
Avatar,
|
||||
CellProps,
|
||||
@ -14,6 +17,7 @@ import {
|
||||
Pagination,
|
||||
Stack,
|
||||
Tooltip,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
@ -32,6 +36,15 @@ export interface State {
|
||||
roleOptions: Role[];
|
||||
}
|
||||
|
||||
// this is dummy data to pass to the table while the real data is loading
|
||||
const skeletonData: Team[] = new Array(3).fill(null).map((_, index) => ({
|
||||
id: index,
|
||||
memberCount: 0,
|
||||
name: '',
|
||||
orgId: 0,
|
||||
permission: 0,
|
||||
}));
|
||||
|
||||
export const TeamList = ({
|
||||
teams,
|
||||
query,
|
||||
@ -47,6 +60,7 @@ export const TeamList = ({
|
||||
changeSort,
|
||||
}: Props) => {
|
||||
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
loadTeams(true);
|
||||
@ -66,24 +80,47 @@ export const TeamList = ({
|
||||
{
|
||||
id: 'avatarUrl',
|
||||
header: '',
|
||||
cell: ({ cell: { value } }: Cell<'avatarUrl'>) => value && <Avatar src={value} alt="User avatar" />,
|
||||
disableGrow: true,
|
||||
cell: ({ cell: { value } }: Cell<'avatarUrl'>) => {
|
||||
if (!hasFetched) {
|
||||
return <Skeleton containerClassName={styles.blockSkeleton} width={24} height={24} circle />;
|
||||
}
|
||||
|
||||
return value && <Avatar src={value} alt="User avatar" />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Name',
|
||||
cell: ({ cell: { value } }: Cell<'name'>) => value,
|
||||
cell: ({ cell: { value } }: Cell<'name'>) => {
|
||||
if (!hasFetched) {
|
||||
return <Skeleton width={100} />;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
sortType: 'string',
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
header: 'Email',
|
||||
cell: ({ cell: { value } }: Cell<'email'>) => value,
|
||||
cell: ({ cell: { value } }: Cell<'email'>) => {
|
||||
if (!hasFetched) {
|
||||
return <Skeleton width={60} />;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
sortType: 'string',
|
||||
},
|
||||
{
|
||||
id: 'memberCount',
|
||||
header: 'Members',
|
||||
cell: ({ cell: { value } }: Cell<'memberCount'>) => value,
|
||||
disableGrow: true,
|
||||
cell: ({ cell: { value } }: Cell<'memberCount'>) => {
|
||||
if (!hasFetched) {
|
||||
return <Skeleton width={40} />;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
sortType: 'number',
|
||||
},
|
||||
...(displayRolePicker
|
||||
@ -92,6 +129,9 @@ export const TeamList = ({
|
||||
id: 'role',
|
||||
header: 'Role',
|
||||
cell: ({ cell: { value }, row: { original } }: Cell<'memberCount'>) => {
|
||||
if (!hasFetched) {
|
||||
return <Skeleton width={320} height={32} containerClassName={styles.blockSkeleton} />;
|
||||
}
|
||||
const canSeeTeamRoles = contextSrv.hasPermissionInMetadata(
|
||||
AccessControlAction.ActionTeamsRolesList,
|
||||
original
|
||||
@ -112,42 +152,54 @@ export const TeamList = ({
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'edit',
|
||||
id: 'actions',
|
||||
header: '',
|
||||
disableGrow: true,
|
||||
cell: ({ row: { original } }: Cell) => {
|
||||
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, original);
|
||||
return canReadTeam ? (
|
||||
<a href={`org/teams/edit/${original.id}`} aria-label={`Edit team ${original.name}`}>
|
||||
<Tooltip content={'Edit team'}>
|
||||
<Icon name={'pen'} />
|
||||
</Tooltip>
|
||||
</a>
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
header: '',
|
||||
cell: ({ row: { original } }: Cell) => {
|
||||
const canDelete = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsDelete, original);
|
||||
if (!hasFetched) {
|
||||
return (
|
||||
<Stack direction="row" justifyContent="flex-end" alignItems="center">
|
||||
<Skeleton containerClassName={styles.blockSkeleton} width={16} height={16} />
|
||||
<Skeleton containerClassName={styles.blockSkeleton} width={22} height={24} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, original);
|
||||
const canDelete = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsDelete, original);
|
||||
return (
|
||||
<DeleteButton
|
||||
aria-label={`Delete team ${original.name}`}
|
||||
size="sm"
|
||||
disabled={!canDelete}
|
||||
onConfirm={() => deleteTeam(original.id)}
|
||||
/>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
{canReadTeam && (
|
||||
<Tooltip content={'Edit team'}>
|
||||
<a href={`org/teams/edit/${original.id}`} aria-label={`Edit team ${original.name}`}>
|
||||
<Icon name={'pen'} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
<DeleteButton
|
||||
aria-label={`Delete team ${original.name}`}
|
||||
size="sm"
|
||||
disabled={!canDelete}
|
||||
onConfirm={() => deleteTeam(original.id)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[displayRolePicker, rolesLoading, roleOptions, deleteTeam]
|
||||
[displayRolePicker, hasFetched, rolesLoading, roleOptions, deleteTeam, styles]
|
||||
);
|
||||
|
||||
return (
|
||||
<Page navId="teams">
|
||||
<Page.Contents isLoading={!hasFetched}>
|
||||
<Page
|
||||
navId="teams"
|
||||
actions={
|
||||
<LinkButton href={canCreate ? 'org/teams/new' : '#'} disabled={!canCreate}>
|
||||
New Team
|
||||
</LinkButton>
|
||||
}
|
||||
>
|
||||
<Page.Contents>
|
||||
{noTeams ? (
|
||||
<EmptyListCTA
|
||||
title="You haven't created any teams yet."
|
||||
@ -166,15 +218,11 @@ export const TeamList = ({
|
||||
<InlineField grow>
|
||||
<FilterInput placeholder="Search teams" value={query} onChange={changeQuery} />
|
||||
</InlineField>
|
||||
|
||||
<LinkButton href={canCreate ? 'org/teams/new' : '#'} disabled={!canCreate}>
|
||||
New Team
|
||||
</LinkButton>
|
||||
</div>
|
||||
<Stack direction={'column'} gap={2}>
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={teams}
|
||||
data={hasFetched ? teams : skeletonData}
|
||||
getRowId={(team) => String(team.id)}
|
||||
fetchData={changeSort}
|
||||
/>
|
||||
@ -221,3 +269,11 @@ const mapDispatchToProps = {
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
export default connector(TeamList);
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
blockSkeleton: css({
|
||||
lineHeight: 1,
|
||||
// needed for things to align properly in the table
|
||||
display: 'flex',
|
||||
}),
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user