Admin: Use primitive components for table views (#76512)

* Remove HorizontalGroup and VerticalGroup from OrgUserstable

* Refactor OrgUnits

* Refactor UsersTable

* Add TableWrapper

* Use Stack and Flex for TeamList

* Revert pagination changes

* Update betterer

* Remove div wrapper

* Codeformat
This commit is contained in:
Alex Khomenko 2023-10-17 13:06:28 +02:00 committed by GitHub
parent be5ba68132
commit a4522812fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 112 additions and 181 deletions

View File

@ -1761,11 +1761,6 @@ exports[`better eslint`] = {
"public/app/features/admin/UserSessions.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/admin/Users/OrgUnits.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"]
],
"public/app/features/alerting/AlertTab.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],

View File

@ -1,15 +1,13 @@
import { css } from '@emotion/css';
import React from 'react';
import React, { forwardRef, PropsWithChildren } from 'react';
import { GrafanaTheme2, IconName } from '@grafana/data';
import { Icon, Tooltip, useStyles2 } from '@grafana/ui';
import { IconName } from '@grafana/data';
import { Icon, Tooltip } from '@grafana/ui';
import { Box, Flex } from '@grafana/ui/src/unstable';
import { Unit } from 'app/types';
type OrgUnitProps = { units?: Unit[]; icon: IconName };
export const OrgUnits = ({ units, icon }: OrgUnitProps) => {
const styles = useStyles2(getStyles);
if (!units?.length) {
return null;
}
@ -17,39 +15,25 @@ export const OrgUnits = ({ units, icon }: OrgUnitProps) => {
return units.length > 1 ? (
<Tooltip
placement={'top'}
content={
<div className={styles.unitTooltip}>{units?.map((unit) => <span key={unit.name}>{unit.name}</span>)}</div>
}
content={<Flex direction={'column'}>{units?.map((unit) => <span key={unit.name}>{unit.name}</span>)}</Flex>}
>
<div className={styles.unitItem}>
<Icon name={icon} /> <span>{units.length}</span>
</div>
<Content icon={icon}>{units.length}</Content>
</Tooltip>
) : (
<span className={styles.unitItem}>
<Icon name={icon} /> {units[0].name}
</span>
<Content icon={icon}>{units[0].name}</Content>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
unitTooltip: css`
display: flex;
flex-direction: column;
`,
unitItem: css`
padding: ${theme.spacing(0.5)} 0;
margin-right: ${theme.spacing(1)};
interface ContentProps extends PropsWithChildren {
icon: IconName;
}
svg {
margin-bottom: ${theme.spacing(0.25)};
}
`,
link: css`
color: inherit;
cursor: pointer;
text-decoration: underline;
`,
};
};
export const Content = forwardRef<HTMLElement, ContentProps>(({ children, icon }, ref) => {
return (
<Box ref={ref} display={'flex'} alignItems={'center'} marginRight={1}>
<Icon name={icon} /> <Box marginLeft={1}>{children}</Box>
</Box>
);
});
Content.displayName = 'TooltipContent';

View File

@ -1,7 +1,6 @@
import { css } from '@emotion/css';
import React, { useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2, OrgRole } from '@grafana/data';
import { OrgRole } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import {
Button,
@ -9,16 +8,14 @@ import {
Icon,
Tooltip,
CellProps,
useStyles2,
Tag,
InteractiveTable,
Column,
FetchDataFunc,
Pagination,
HorizontalGroup,
VerticalGroup,
Avatar,
} from '@grafana/ui';
import { Flex, Stack, Box } from '@grafana/ui/src/unstable';
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
@ -28,6 +25,8 @@ import { AccessControlAction, OrgUser, Role } from 'app/types';
import { OrgRolePicker } from '../OrgRolePicker';
import { TableWrapper } from './TableWrapper';
type Cell<T extends keyof OrgUser = keyof OrgUser> = CellProps<OrgUser, OrgUser[T]>;
const disabledRoleMessage = `This user's role is not editable because it is synchronized from your auth provider.
@ -71,7 +70,6 @@ export const OrgUsersTable = ({
}: Props) => {
const [userToRemove, setUserToRemove] = useState<OrgUser | null>(null);
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
const styles = useStyles2(getStyles);
useEffect(() => {
async function fetchOptions() {
@ -148,7 +146,18 @@ export const OrgUsersTable = ({
{
id: 'info',
header: '',
cell: InfoCell,
cell: ({ row: { original } }: Cell) => {
const basicRoleDisabled = getBasicRoleDisabled(original);
return (
basicRoleDisabled && (
<Box display={'flex'} alignItems={'center'} marginLeft={1}>
<Tooltip content={disabledRoleMessage}>
<Icon name="question-circle" />
</Tooltip>
</Box>
)
);
},
},
{
id: 'authLabels',
@ -186,18 +195,18 @@ export const OrgUsersTable = ({
);
return (
<VerticalGroup spacing="md" data-testid={selectors.container}>
<div className={styles.wrapper}>
<Stack gap={2} data-testid={selectors.container}>
<TableWrapper>
<InteractiveTable
columns={columns}
data={users}
getRowId={(user) => String(user.userId)}
fetchData={fetchData}
/>
<HorizontalGroup justify="flex-end">
<Flex justifyContent="flex-end">
<Pagination onNavigate={changePage} currentPage={page} numberOfPages={totalPages} hideWhenSinglePage={true} />
</HorizontalGroup>
</div>
</Flex>
</TableWrapper>
{Boolean(userToRemove) && (
<ConfirmModal
body={`Are you sure you want to delete user ${userToRemove?.login}?`}
@ -216,43 +225,6 @@ export const OrgUsersTable = ({
}}
/>
)}
</VerticalGroup>
</Stack>
);
};
const InfoCell = ({ row: { original } }: Cell) => {
const styles = useStyles2(getStyles);
const basicRoleDisabled = getBasicRoleDisabled(original);
return (
basicRoleDisabled && (
<div className={styles.row}>
<Tooltip content={disabledRoleMessage}>
<Icon name="question-circle" className={styles.icon} />
</Tooltip>
</div>
)
);
};
const getStyles = (theme: GrafanaTheme2) => ({
row: css({
display: 'flex',
alignItems: 'center',
}),
icon: css({
marginLeft: theme.spacing(1),
}),
// Enable RolePicker overflow
wrapper: css({
display: 'flex',
flexDirection: 'column',
overflowX: 'auto',
overflowY: 'hidden',
minHeight: '100vh',
width: '100%',
'& > div': {
overflowX: 'unset',
marginBottom: theme.spacing(2),
},
}),
});

View File

@ -0,0 +1,30 @@
import { css } from '@emotion/css';
import React, { PropsWithChildren } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
/**
* A wrapper component for interactive tables using RolePicker to enable overflow.
* Should be removed when the RolePicker component uses portals to render its menu
*/
export const TableWrapper = ({ children }: PropsWithChildren) => {
const styles = useStyles2(getStyles);
return <div className={styles.wrapper}>{children}</div>;
};
const getStyles = (theme: GrafanaTheme2) => ({
// Enable RolePicker overflow
wrapper: css({
display: 'flex',
flexDirection: 'column',
overflowX: 'auto',
overflowY: 'hidden',
minHeight: '100vh',
width: '100%',
'& > div': {
overflowX: 'unset',
marginBottom: theme.spacing(2),
},
}),
});

View File

@ -1,21 +1,18 @@
import { css } from '@emotion/css';
import React, { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import {
InteractiveTable,
CellProps,
Tooltip,
Icon,
useStyles2,
Tag,
Pagination,
Column,
VerticalGroup,
HorizontalGroup,
FetchDataFunc,
Text,
Avatar,
} from '@grafana/ui';
import { Flex, Stack } from '@grafana/ui/src/unstable';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { UserDTO } from 'app/types';
@ -69,7 +66,18 @@ export const UsersTable = ({
{
id: 'orgs',
header: 'Belongs to',
cell: OrgUnitsCell,
cell: ({ cell: { value, row } }: Cell<'orgs'>) => {
return (
<Flex alignItems={'center'}>
<OrgUnits units={value} icon={'building'} />
{row.original.isAdmin && (
<Tooltip placement="top" content="Grafana Admin">
<Icon name="shield" />
</Tooltip>
)}
</Flex>
);
},
sortType: (a, b) => (a.original.orgs?.length || 0) - (b.original.orgs?.length || 0),
},
...(showLicensedRole
@ -77,7 +85,18 @@ export const UsersTable = ({
{
id: 'licensedRole',
header: 'Licensed role',
cell: LicensedRoleCell,
cell: ({ cell: { value } }: Cell<'licensedRole'>) => {
return value === 'None' ? (
<Text color={'disabled'}>
Not assigned{' '}
<Tooltip placement="top" content="A licensed role will be assigned when this user signs in">
<Icon name="question-circle" />
</Tooltip>
</Text>
) : (
value
);
},
// Needs the assertion here, the types are not inferred correctly due to the conditional assignment
sortType: 'string' as const,
},
@ -90,7 +109,9 @@ export const UsersTable = ({
content: 'Time since user was seen using Grafana',
iconName: 'question-circle',
},
cell: LastSeenAtCell,
cell: ({ cell: { value } }: Cell<'lastSeenAtAge'>) => {
return <>{value && <>{value === '10 years' ? <Text color={'disabled'}>Never</Text> : value}</>}</>;
},
sortType: (a, b) => new Date(a.original.lastSeenAt!).getTime() - new Date(b.original.lastSeenAt!).getTime(),
},
{
@ -122,62 +143,13 @@ export const UsersTable = ({
[showLicensedRole]
);
return (
<VerticalGroup spacing={'md'}>
<Stack gap={2}>
<InteractiveTable columns={columns} data={users} getRowId={(user) => String(user.id)} fetchData={fetchData} />
{showPaging && (
<HorizontalGroup justify={'flex-end'}>
<Flex justifyContent={'flex-end'}>
<Pagination numberOfPages={totalPages} currentPage={currentPage} onNavigate={onChangePage} />
</HorizontalGroup>
</Flex>
)}
</VerticalGroup>
</Stack>
);
};
const OrgUnitsCell = ({ cell: { value, row } }: Cell<'orgs'>) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.row}>
<OrgUnits units={value} icon={'building'} />
{row.original.isAdmin && (
<Tooltip placement="top" content="Grafana Admin">
<Icon name="shield" />
</Tooltip>
)}
</div>
);
};
const LicensedRoleCell = ({ cell: { value } }: Cell<'licensedRole'>) => {
const styles = useStyles2(getStyles);
return (
<>
{value === 'None' ? (
<span className={styles.disabled}>
Not assigned{' '}
<Tooltip placement="top" content="A licensed role will be assigned when this user signs in">
<Icon name="question-circle" />
</Tooltip>
</span>
) : (
value
)}
</>
);
};
const LastSeenAtCell = ({ cell: { value } }: Cell<'lastSeenAtAge'>) => {
const styles = useStyles2(getStyles);
return <>{value && <>{value === '10 years' ? <span className={styles.disabled}>Never</span> : value}</>}</>;
};
const getStyles = (theme: GrafanaTheme2) => {
return {
disabled: css({ color: theme.colors.text.disabled }),
row: css({
display: 'flex',
alignItems: 'center',
}),
};
};

View File

@ -1,8 +1,6 @@
import { css } from '@emotion/css';
import React, { useEffect, useMemo, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { GrafanaTheme2 } from '@grafana/data';
import {
LinkButton,
FilterInput,
@ -13,12 +11,10 @@ import {
Icon,
Tooltip,
Column,
HorizontalGroup,
Pagination,
VerticalGroup,
useStyles2,
Avatar,
} from '@grafana/ui';
import { Stack, Flex } from '@grafana/ui/src/unstable';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { Page } from 'app/core/components/Page/Page';
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
@ -26,6 +22,7 @@ import { contextSrv } from 'app/core/services/context_srv';
import { AccessControlAction, Role, StoreState, Team } from 'app/types';
import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker';
import { TableWrapper } from '../admin/Users/TableWrapper';
import { deleteTeam, loadTeams, changePage, changeQuery, changeSort } from './state/actions';
@ -50,7 +47,6 @@ export const TeamList = ({
changeSort,
}: Props) => {
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
const styles = useStyles2(getStyles);
useEffect(() => {
loadTeams(true);
@ -165,24 +161,24 @@ export const TeamList = ({
New Team
</LinkButton>
</div>
<VerticalGroup spacing={'md'}>
<div className={styles.wrapper}>
<Stack gap={2}>
<TableWrapper>
<InteractiveTable
columns={columns}
data={teams}
getRowId={(team) => String(team.id)}
fetchData={changeSort}
/>
<HorizontalGroup justify="flex-end">
<Flex justifyContent="flex-end">
<Pagination
hideWhenSinglePage
currentPage={page}
numberOfPages={totalPages}
onNavigate={changePage}
/>
</HorizontalGroup>
</div>
</VerticalGroup>
</Flex>
</TableWrapper>
</Stack>
</>
)}
</Page.Contents>
@ -190,24 +186,6 @@ export const TeamList = ({
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
// Enable RolePicker overflow
wrapper: css({
display: 'flex',
flexDirection: 'column',
overflowX: 'auto',
overflowY: 'hidden',
minHeight: '100vh',
width: '100%',
'& > div': {
overflowX: 'unset',
marginBottom: theme.spacing(2),
},
}),
};
};
function shouldDisplayRolePicker(): boolean {
return (
contextSrv.licensedAccessControlEnabled() &&