mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 23:55:47 -06:00
* update eslint, tsconfig + esbuild to handle new jsx transform * remove thing that breaks the new jsx transform * remove react imports * adjust grafana-icons build * is this the correct syntax? * try this * well this was much easier than expected... * change grafana-plugin-configs webpack config * fixes * fix lockfile * fix 2 more violations * use path.resolve instead of require.resolve * remove react import * fix react imports * more fixes * remove React import * remove import React from docs * remove another react import
309 lines
9.2 KiB
TypeScript
309 lines
9.2 KiB
TypeScript
import { css } from '@emotion/css';
|
|
import { 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,
|
|
Column,
|
|
DeleteButton,
|
|
EmptyState,
|
|
FilterInput,
|
|
InlineField,
|
|
InteractiveTable,
|
|
LinkButton,
|
|
Pagination,
|
|
Stack,
|
|
TextLink,
|
|
useStyles2,
|
|
} from '@grafana/ui';
|
|
import { Page } from 'app/core/components/Page/Page';
|
|
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
|
|
import { Trans, t } from 'app/core/internationalization';
|
|
import { contextSrv } from 'app/core/services/context_srv';
|
|
import { AccessControlAction, Role, StoreState, Team } from 'app/types';
|
|
|
|
import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker';
|
|
|
|
import { deleteTeam, loadTeams, changePage, changeQuery, changeSort } from './state/actions';
|
|
|
|
type Cell<T extends keyof Team = keyof Team> = CellProps<Team, Team[T]>;
|
|
export interface OwnProps {}
|
|
|
|
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,
|
|
noTeams,
|
|
hasFetched,
|
|
loadTeams,
|
|
deleteTeam,
|
|
changeQuery,
|
|
totalPages,
|
|
page,
|
|
rolesLoading,
|
|
changePage,
|
|
changeSort,
|
|
}: Props) => {
|
|
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
|
const styles = useStyles2(getStyles);
|
|
|
|
useEffect(() => {
|
|
loadTeams(true);
|
|
}, [loadTeams]);
|
|
|
|
useEffect(() => {
|
|
if (contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionRolesList)) {
|
|
fetchRoleOptions().then((roles) => setRoleOptions(roles));
|
|
}
|
|
}, []);
|
|
|
|
const canCreate = contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate);
|
|
const displayRolePicker = shouldDisplayRolePicker();
|
|
|
|
const columns: Array<Column<Team>> = useMemo(
|
|
() => [
|
|
{
|
|
id: 'avatarUrl',
|
|
header: '',
|
|
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 }, row: { original } }: Cell<'name'>) => {
|
|
if (!hasFetched) {
|
|
return <Skeleton width={100} />;
|
|
}
|
|
|
|
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, original);
|
|
if (!canReadTeam) {
|
|
return value;
|
|
}
|
|
|
|
return (
|
|
<TextLink color="primary" inline={false} href={`/org/teams/edit/${original.id}`} title="Edit team">
|
|
{value}
|
|
</TextLink>
|
|
);
|
|
},
|
|
sortType: 'string',
|
|
},
|
|
{
|
|
id: 'email',
|
|
header: 'Email',
|
|
cell: ({ cell: { value } }: Cell<'email'>) => {
|
|
if (!hasFetched) {
|
|
return <Skeleton width={60} />;
|
|
}
|
|
return value;
|
|
},
|
|
sortType: 'string',
|
|
},
|
|
{
|
|
id: 'memberCount',
|
|
header: 'Members',
|
|
disableGrow: true,
|
|
cell: ({ cell: { value } }: Cell<'memberCount'>) => {
|
|
if (!hasFetched) {
|
|
return <Skeleton width={40} />;
|
|
}
|
|
return value;
|
|
},
|
|
sortType: 'number',
|
|
},
|
|
...(displayRolePicker
|
|
? [
|
|
{
|
|
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
|
|
);
|
|
return (
|
|
canSeeTeamRoles && (
|
|
<TeamRolePicker
|
|
teamId={original.id}
|
|
roles={original.roles || []}
|
|
isLoading={rolesLoading}
|
|
roleOptions={roleOptions}
|
|
width={40}
|
|
/>
|
|
)
|
|
);
|
|
},
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
id: 'actions',
|
|
header: '',
|
|
disableGrow: true,
|
|
cell: ({ row: { original } }: Cell) => {
|
|
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 (
|
|
<Stack direction="row" justifyContent="flex-end" gap={2}>
|
|
{canReadTeam && (
|
|
<LinkButton
|
|
href={`org/teams/edit/${original.id}`}
|
|
aria-label={`Edit team ${original.name}`}
|
|
icon="pen"
|
|
size="sm"
|
|
variant="secondary"
|
|
tooltip={'Edit team'}
|
|
/>
|
|
)}
|
|
<DeleteButton
|
|
aria-label={`Delete team ${original.name}`}
|
|
size="sm"
|
|
disabled={!canDelete}
|
|
onConfirm={() => deleteTeam(original.id)}
|
|
/>
|
|
</Stack>
|
|
);
|
|
},
|
|
},
|
|
],
|
|
[displayRolePicker, hasFetched, rolesLoading, roleOptions, deleteTeam, styles]
|
|
);
|
|
|
|
return (
|
|
<Page
|
|
navId="teams"
|
|
actions={
|
|
!noTeams ? (
|
|
<LinkButton href={canCreate ? 'org/teams/new' : '#'} disabled={!canCreate}>
|
|
New Team
|
|
</LinkButton>
|
|
) : undefined
|
|
}
|
|
>
|
|
<Page.Contents>
|
|
{noTeams ? (
|
|
<EmptyState
|
|
variant="call-to-action"
|
|
button={
|
|
<LinkButton disabled={!canCreate} href="org/teams/new" icon="users-alt" size="lg">
|
|
<Trans i18nKey="teams.empty-state.button-title">New team</Trans>
|
|
</LinkButton>
|
|
}
|
|
message={t('teams.empty-state.title', "You haven't created any teams yet")}
|
|
>
|
|
<Trans i18nKey="teams.empty-state.pro-tip">
|
|
Assign folder and dashboard permissions to teams instead of users to ease administration.{' '}
|
|
<TextLink external href="https://grafana.com/docs/grafana/latest/administration/team-management">
|
|
Learn more
|
|
</TextLink>
|
|
</Trans>
|
|
</EmptyState>
|
|
) : (
|
|
<>
|
|
<div className="page-action-bar">
|
|
<InlineField grow>
|
|
<FilterInput placeholder="Search teams" value={query} onChange={changeQuery} />
|
|
</InlineField>
|
|
</div>
|
|
{hasFetched && teams.length === 0 ? (
|
|
<EmptyState variant="not-found" message={t('teams.empty-state.message', 'No teams found')} />
|
|
) : (
|
|
<Stack direction={'column'} gap={2}>
|
|
<InteractiveTable
|
|
columns={columns}
|
|
data={hasFetched ? teams : skeletonData}
|
|
getRowId={(team) => String(team.id)}
|
|
fetchData={changeSort}
|
|
/>
|
|
<Stack justifyContent="flex-end">
|
|
<Pagination
|
|
hideWhenSinglePage
|
|
currentPage={page}
|
|
numberOfPages={totalPages}
|
|
onNavigate={changePage}
|
|
/>
|
|
</Stack>
|
|
</Stack>
|
|
)}
|
|
</>
|
|
)}
|
|
</Page.Contents>
|
|
</Page>
|
|
);
|
|
};
|
|
|
|
function shouldDisplayRolePicker(): boolean {
|
|
return (
|
|
contextSrv.licensedAccessControlEnabled() &&
|
|
contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList) &&
|
|
contextSrv.hasPermission(AccessControlAction.ActionRolesList)
|
|
);
|
|
}
|
|
|
|
function mapStateToProps(state: StoreState) {
|
|
return {
|
|
teams: state.teams.teams,
|
|
query: state.teams.query,
|
|
perPage: state.teams.perPage,
|
|
page: state.teams.page,
|
|
noTeams: state.teams.noTeams,
|
|
totalPages: state.teams.totalPages,
|
|
hasFetched: state.teams.hasFetched,
|
|
rolesLoading: state.teams.rolesLoading,
|
|
};
|
|
}
|
|
|
|
const mapDispatchToProps = {
|
|
loadTeams,
|
|
deleteTeam,
|
|
changePage,
|
|
changeQuery,
|
|
changeSort,
|
|
};
|
|
|
|
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',
|
|
}),
|
|
});
|