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 React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
CellProps,
|
CellProps,
|
||||||
@ -14,6 +17,7 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
Stack,
|
Stack,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
useStyles2,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
@ -32,6 +36,15 @@ export interface State {
|
|||||||
roleOptions: Role[];
|
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 = ({
|
export const TeamList = ({
|
||||||
teams,
|
teams,
|
||||||
query,
|
query,
|
||||||
@ -47,6 +60,7 @@ export const TeamList = ({
|
|||||||
changeSort,
|
changeSort,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadTeams(true);
|
loadTeams(true);
|
||||||
@ -66,24 +80,47 @@ export const TeamList = ({
|
|||||||
{
|
{
|
||||||
id: 'avatarUrl',
|
id: 'avatarUrl',
|
||||||
header: '',
|
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',
|
id: 'name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
cell: ({ cell: { value } }: Cell<'name'>) => value,
|
cell: ({ cell: { value } }: Cell<'name'>) => {
|
||||||
|
if (!hasFetched) {
|
||||||
|
return <Skeleton width={100} />;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
sortType: 'string',
|
sortType: 'string',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'email',
|
id: 'email',
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
cell: ({ cell: { value } }: Cell<'email'>) => value,
|
cell: ({ cell: { value } }: Cell<'email'>) => {
|
||||||
|
if (!hasFetched) {
|
||||||
|
return <Skeleton width={60} />;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
sortType: 'string',
|
sortType: 'string',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'memberCount',
|
id: 'memberCount',
|
||||||
header: 'Members',
|
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',
|
sortType: 'number',
|
||||||
},
|
},
|
||||||
...(displayRolePicker
|
...(displayRolePicker
|
||||||
@ -92,6 +129,9 @@ export const TeamList = ({
|
|||||||
id: 'role',
|
id: 'role',
|
||||||
header: 'Role',
|
header: 'Role',
|
||||||
cell: ({ cell: { value }, row: { original } }: Cell<'memberCount'>) => {
|
cell: ({ cell: { value }, row: { original } }: Cell<'memberCount'>) => {
|
||||||
|
if (!hasFetched) {
|
||||||
|
return <Skeleton width={320} height={32} containerClassName={styles.blockSkeleton} />;
|
||||||
|
}
|
||||||
const canSeeTeamRoles = contextSrv.hasPermissionInMetadata(
|
const canSeeTeamRoles = contextSrv.hasPermissionInMetadata(
|
||||||
AccessControlAction.ActionTeamsRolesList,
|
AccessControlAction.ActionTeamsRolesList,
|
||||||
original
|
original
|
||||||
@ -112,42 +152,54 @@ export const TeamList = ({
|
|||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
id: 'edit',
|
id: 'actions',
|
||||||
header: '',
|
header: '',
|
||||||
|
disableGrow: true,
|
||||||
cell: ({ row: { original } }: Cell) => {
|
cell: ({ row: { original } }: Cell) => {
|
||||||
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, original);
|
if (!hasFetched) {
|
||||||
return canReadTeam ? (
|
return (
|
||||||
<a href={`org/teams/edit/${original.id}`} aria-label={`Edit team ${original.name}`}>
|
<Stack direction="row" justifyContent="flex-end" alignItems="center">
|
||||||
<Tooltip content={'Edit team'}>
|
<Skeleton containerClassName={styles.blockSkeleton} width={16} height={16} />
|
||||||
<Icon name={'pen'} />
|
<Skeleton containerClassName={styles.blockSkeleton} width={22} height={24} />
|
||||||
</Tooltip>
|
</Stack>
|
||||||
</a>
|
);
|
||||||
) : null;
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'delete',
|
|
||||||
header: '',
|
|
||||||
cell: ({ row: { original } }: Cell) => {
|
|
||||||
const canDelete = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsDelete, original);
|
|
||||||
|
|
||||||
|
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, original);
|
||||||
|
const canDelete = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsDelete, original);
|
||||||
return (
|
return (
|
||||||
<DeleteButton
|
<Stack direction="row" justifyContent="flex-end">
|
||||||
aria-label={`Delete team ${original.name}`}
|
{canReadTeam && (
|
||||||
size="sm"
|
<Tooltip content={'Edit team'}>
|
||||||
disabled={!canDelete}
|
<a href={`org/teams/edit/${original.id}`} aria-label={`Edit team ${original.name}`}>
|
||||||
onConfirm={() => deleteTeam(original.id)}
|
<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 (
|
return (
|
||||||
<Page navId="teams">
|
<Page
|
||||||
<Page.Contents isLoading={!hasFetched}>
|
navId="teams"
|
||||||
|
actions={
|
||||||
|
<LinkButton href={canCreate ? 'org/teams/new' : '#'} disabled={!canCreate}>
|
||||||
|
New Team
|
||||||
|
</LinkButton>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Page.Contents>
|
||||||
{noTeams ? (
|
{noTeams ? (
|
||||||
<EmptyListCTA
|
<EmptyListCTA
|
||||||
title="You haven't created any teams yet."
|
title="You haven't created any teams yet."
|
||||||
@ -166,15 +218,11 @@ export const TeamList = ({
|
|||||||
<InlineField grow>
|
<InlineField grow>
|
||||||
<FilterInput placeholder="Search teams" value={query} onChange={changeQuery} />
|
<FilterInput placeholder="Search teams" value={query} onChange={changeQuery} />
|
||||||
</InlineField>
|
</InlineField>
|
||||||
|
|
||||||
<LinkButton href={canCreate ? 'org/teams/new' : '#'} disabled={!canCreate}>
|
|
||||||
New Team
|
|
||||||
</LinkButton>
|
|
||||||
</div>
|
</div>
|
||||||
<Stack direction={'column'} gap={2}>
|
<Stack direction={'column'} gap={2}>
|
||||||
<InteractiveTable
|
<InteractiveTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={teams}
|
data={hasFetched ? teams : skeletonData}
|
||||||
getRowId={(team) => String(team.id)}
|
getRowId={(team) => String(team.id)}
|
||||||
fetchData={changeSort}
|
fetchData={changeSort}
|
||||||
/>
|
/>
|
||||||
@ -221,3 +269,11 @@ const mapDispatchToProps = {
|
|||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||||
export default connector(TeamList);
|
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