1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +01:00

Project features list update (#991)

* refactor: column icon position

* project overview horizontal scroll

* updated table headers styles

* fix: feature overview switch title

* refactor: cleanup of sortable header styles

* fix: z-index issue in test

* fix: html semantics after review
This commit is contained in:
Tymoteusz Czech 2022-05-18 11:56:55 +02:00 committed by GitHub
parent 98b6214c28
commit 06b0a29ea8
27 changed files with 132 additions and 75 deletions

View File

@ -2,6 +2,7 @@ import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({ export const useStyles = makeStyles()(theme => ({
rolesListBody: { rolesListBody: {
padding: theme.spacing(4),
paddingBottom: '4rem', paddingBottom: '4rem',
minHeight: '50vh', minHeight: '50vh',
position: 'relative', position: 'relative',

View File

@ -1,7 +1,8 @@
import { makeStyles } from 'tss-react/mui'; import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(() => ({ export const useStyles = makeStyles()(theme => ({
userListBody: { userListBody: {
padding: theme.spacing(4),
paddingBottom: '4rem', paddingBottom: '4rem',
minHeight: '50vh', minHeight: '50vh',
position: 'relative', position: 'relative',

View File

@ -15,9 +15,9 @@ export const useStyles = makeStyles()(theme => ({
}, },
}, },
bodyContainer: { bodyContainer: {
padding: '2rem', padding: theme.spacing(4),
[theme.breakpoints.down('md')]: { [theme.breakpoints.down('md')]: {
padding: '1rem', padding: theme.spacing(2),
}, },
}, },
paddingDisabled: { paddingDisabled: {

View File

@ -38,11 +38,13 @@ export const PageContent: FC<IPageContentProps> = ({
[styles.borderDisabled]: disableBorder, [styles.borderDisabled]: disableBorder,
}); });
const bodyClasses = classnames(styles.bodyContainer, { const bodyClasses = classnames(
[styles.paddingDisabled]: disablePadding, bodyClass ? bodyClass : styles.bodyContainer,
[styles.borderDisabled]: disableBorder, {
[bodyClass]: bodyClass, [styles.paddingDisabled]: disablePadding,
}); [styles.borderDisabled]: disableBorder,
}
);
const paperProps = disableBorder ? { elevation: 0 } : {}; const paperProps = disableBorder ? { elevation: 0 } : {};

View File

@ -10,7 +10,12 @@ export const useStyles = makeStyles()(theme => ({
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
position: 'relative', position: 'relative',
flexWrap: 'wrap', },
header: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
marginRight: theme.spacing(2),
}, },
headerTitle: { headerTitle: {
fontSize: theme.fontSizes.mainHeader, fontSize: theme.fontSizes.mainHeader,
@ -21,6 +26,7 @@ export const useStyles = makeStyles()(theme => ({
flexGrow: 1, flexGrow: 1,
justifyContent: 'flex-end', justifyContent: 'flex-end',
alignItems: 'center', alignItems: 'center',
gap: theme.spacing(1),
}, },
verticalSeparator: { verticalSeparator: {
height: '100%', height: '100%',

View File

@ -35,7 +35,10 @@ const PageHeaderComponent: FC<IPageHeaderProps> & { Divider: VFC } = ({
return ( return (
<div className={styles.headerContainer}> <div className={styles.headerContainer}>
<div className={styles.topContainer}> <div className={styles.topContainer}>
<div className={headerClasses} data-loading> <div
className={classnames(styles.header, headerClasses)}
data-loading
>
<Typography <Typography
variant={variant || 'h1'} variant={variant || 'h1'}
className={classnames(styles.headerTitle, className)} className={classnames(styles.headerTitle, className)}

View File

@ -19,11 +19,17 @@ export const useStyles = makeStyles()(theme => ({
padding: theme.spacing(2), padding: theme.spacing(2),
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
width: '100%', width: '100%',
'& .hover-only': {
visibility: 'hidden',
},
':hover, :focus, &:focus-visible, &:active': { ':hover, :focus, &:focus-visible, &:active': {
outline: 'revert', outline: 'revert',
'& svg': { '& svg': {
color: 'inherit', color: 'inherit',
}, },
'& .hover-only': {
visibility: 'visible',
},
}, },
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',

View File

@ -41,7 +41,7 @@ export const SortArrow: VFC<ISortArrowProps> = ({
} }
elseShow={ elseShow={
<UnfoldMoreOutlined <UnfoldMoreOutlined
className={styles.icon} className={classnames(styles.icon, 'hover-only')}
fontSize="inherit" fontSize="inherit"
/> />
} }

View File

@ -15,7 +15,7 @@ interface ITableActionsProps {
} }
/** /**
* @deprecated * @deprecated Use <PageHeader actions={} /> instead
*/ */
export const TableActions: FC<ITableActionsProps> = ({ export const TableActions: FC<ITableActionsProps> = ({
initialSearchValue: search, initialSearchValue: search,

View File

@ -1,9 +1,15 @@
import { FC } from 'react'; import { FC } from 'react';
import classnames from 'classnames';
import { TableCell as MUITableCell, TableCellProps } from '@mui/material'; import { TableCell as MUITableCell, TableCellProps } from '@mui/material';
import { useStyles } from './TableCell.styles'; import { useStyles } from './TableCell.styles';
export const TableCell: FC<TableCellProps> = ({ ...props }) => { export const TableCell: FC<TableCellProps> = ({ className, ...props }) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
return <MUITableCell className={styles.tableCell} {...props} />; return (
<MUITableCell
className={classnames(styles.tableCell, className)}
{...props}
/>
);
}; };

View File

@ -37,7 +37,7 @@ const FeatureOverviewEnvSwitch = ({
await toggleFeatureEnvironmentOn(projectId, featureId, env.name); await toggleFeatureEnvironmentOn(projectId, featureId, env.name);
setToastData({ setToastData({
type: 'success', type: 'success',
title: 'Available in production', title: `Available in ${env.name}`,
text: `${featureId} is now available in ${env.name} based on its defined strategies.`, text: `${featureId} is now available in ${env.name} based on its defined strategies.`,
}); });
refetchFeature(); refetchFeature();
@ -61,7 +61,7 @@ const FeatureOverviewEnvSwitch = ({
await toggleFeatureEnvironmentOff(projectId, featureId, env.name); await toggleFeatureEnvironmentOff(projectId, featureId, env.name);
setToastData({ setToastData({
type: 'success', type: 'success',
title: 'Unavailable in production', title: `Unavailable in ${env.name}`,
text: `${featureId} is unavailable in ${env.name} and its strategies will no longer have any effect.`, text: `${featureId} is unavailable in ${env.name} and its strategies will no longer have any effect.`,
}); });
refetchFeature(); refetchFeature();

View File

@ -8,7 +8,10 @@ export const useStyles = makeStyles()(theme => ({
flexDirection: 'column', flexDirection: 'column',
}, },
}, },
projectToggles: { width: '100%', minHeight: '100%' }, projectToggles: {
width: '100%',
minWidth: 0,
},
header: { header: {
backgroundColor: '#fff', backgroundColor: '#fff',
borderRadius: theme.shape.borderRadiusLarge, borderRadius: theme.shape.borderRadiusLarge,
@ -36,13 +39,15 @@ export const useStyles = makeStyles()(theme => ({
}, },
}, },
title: { title: {
margin: 0,
width: '100%',
fontSize: theme.fontSizes.mainHeader, fontSize: theme.fontSizes.mainHeader,
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: '0.5rem', marginBottom: '0.5rem',
display: 'grid', display: 'flex',
gridTemplateColumns: '1fr auto', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
gridGap: '1rem', gap: '1rem',
}, },
titleText: { titleText: {
overflow: 'hidden', overflow: 'hidden',

View File

@ -90,7 +90,6 @@ const Project = () => {
return tabData.map((tab, index) => { return tabData.map((tab, index) => {
return ( return (
<Tab <Tab
data-loading
key={tab.title} key={tab.title}
id={`tab-${index}`} id={`tab-${index}`}
aria-controls={`tabpanel-${index}`} aria-controls={`tabpanel-${index}`}
@ -116,12 +115,10 @@ const Project = () => {
<div ref={ref}> <div ref={ref}>
<div className={styles.header}> <div className={styles.header}>
<div className={styles.innerContainer}> <div className={styles.innerContainer}>
<h2 <h2 className={styles.title}>
data-loading <div className={styles.titleText} data-loading>
className={styles.title} {project?.name || projectId}
style={{ margin: 0, width: '100%' }} </div>
>
<div className={styles.titleText}>{project?.name}</div>
<PermissionIconButton <PermissionIconButton
permission={UPDATE_PROJECT} permission={UPDATE_PROJECT}
projectId={project?.id} projectId={project?.id}

View File

@ -1,9 +1,15 @@
import { makeStyles } from 'tss-react/mui'; import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({ export const useStyles = makeStyles()(theme => ({
cell: {
display: 'flex',
justifyContent: 'center',
paddingRight: theme.spacing(2),
},
menuContainer: { menuContainer: {
borderRadius: theme.shape.borderRadiusLarge, borderRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(1), padding: theme.spacing(1),
paddingRight: theme.spacing(3),
}, },
item: { item: {
borderRadius: theme.shape.borderRadius, borderRadius: theme.shape.borderRadius,

View File

@ -57,7 +57,7 @@ export const ActionsCell: VFC<IActionsCellProps> = ({ projectId, row }) => {
const menuId = `${id}-menu`; const menuId = `${id}-menu`;
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'center' }}> <Box className={classes.cell}>
<Tooltip <Tooltip
title="Feature toggle actions" title="Feature toggle actions"
arrow arrow

View File

@ -17,12 +17,11 @@ import {
import ViewColumnIcon from '@mui/icons-material/ViewColumn'; import ViewColumnIcon from '@mui/icons-material/ViewColumn';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { capitalize } from 'lodash';
import { useStyles } from './ColumnsMenu.styles'; import { useStyles } from './ColumnsMenu.styles';
interface IColumnsMenuProps { interface IColumnsMenuProps {
allColumns: { allColumns: {
Header: string | any; Header?: string | any;
id: string; id: string;
isVisible: boolean; isVisible: boolean;
toggleHidden: (state: boolean) => void; toggleHidden: (state: boolean) => void;
@ -104,6 +103,7 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
onClick={handleClick} onClick={handleClick}
type="button" type="button"
className={classes.button} className={classes.button}
data-loading
> >
<ViewColumnIcon /> <ViewColumnIcon />
</IconButton> </IconButton>
@ -165,14 +165,12 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
primary={ primary={
<Typography variant="body2"> <Typography variant="body2">
<ConditionallyRender <ConditionallyRender
condition={ condition={Boolean(
typeof column.Header === typeof column.Header ===
'string' 'string' && column.Header
} )}
show={() => <>{column.Header}</>} show={() => <>{column.Header}</>}
elseShow={() => elseShow={() => column.id}
capitalize(column.id)
}
/> />
</Typography> </Typography>
} }

View File

@ -43,12 +43,13 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
key={`${featureName}-${environmentName}`} // Prevent animation when archiving rows key={`${featureName}-${environmentName}`} // Prevent animation when archiving rows
> >
<PermissionSwitch <PermissionSwitch
checked={isChecked} checked={value}
environmentId={environmentName} environmentId={environmentName}
projectId={projectId} projectId={projectId}
permission={UPDATE_FEATURE_ENVIRONMENT} permission={UPDATE_FEATURE_ENVIRONMENT}
inputProps={{ 'aria-label': environmentName }} inputProps={{ 'aria-label': environmentName }}
onClick={onClick} onClick={onClick}
disabled={isChecked !== value}
/> />
</Box> </Box>
); );

View File

@ -7,7 +7,6 @@ export const useStyles = makeStyles()(theme => ({
minHeight: '100%', minHeight: '100%',
width: 'calc(100% - 1rem)', width: 'calc(100% - 1rem)',
position: 'relative', position: 'relative',
paddingBottom: '4rem',
[theme.breakpoints.down('md')]: { [theme.breakpoints.down('md')]: {
marginLeft: '0', marginLeft: '0',
paddingBottom: '4rem', paddingBottom: '4rem',
@ -25,7 +24,10 @@ export const useStyles = makeStyles()(theme => ({
}, },
}, },
}, },
bodyClass: { padding: '0.5rem 1rem' }, bodyClass: {
overflowX: 'auto',
padding: theme.spacing(4),
},
header: { header: {
padding: '1rem', padding: '1rem',
}, },

View File

@ -70,13 +70,15 @@ export const ProjectFeatureToggles = ({
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const navigate = useNavigate(); const navigate = useNavigate();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const environments = useEnvironmentsRef(newEnvironments); const environments = useEnvironmentsRef(
loading ? ['a', 'b', 'c'] : newEnvironments
);
const { refetch } = useProject(projectId); const { refetch } = useProject(projectId);
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const data = useMemo<ListItemType[]>(() => { const data = useMemo<ListItemType[]>(() => {
if (loading) { if (loading) {
return Array(12).fill({ return Array(6).fill({
type: '-', type: '-',
name: 'Feature name', name: 'Feature name',
createdAt: new Date(), createdAt: new Date(),
@ -180,7 +182,8 @@ export const ProjectFeatureToggles = ({
/> />
), ),
width: '99%', width: '99%',
minWdith: 100, minWidth: 100,
maxWidth: 200,
sortType: 'alphanumeric', sortType: 'alphanumeric',
}, },
{ {
@ -191,9 +194,9 @@ export const ProjectFeatureToggles = ({
align: 'center', align: 'center',
}, },
...environments.map(name => ({ ...environments.map(name => ({
Header: name, Header: loading ? () => '' : name,
maxWidth: 103, maxWidth: 90,
minWidth: 103, minWidth: 90,
accessor: `environments.${name}`, accessor: `environments.${name}`,
align: 'center', align: 'center',
Cell: ({ Cell: ({
@ -218,25 +221,16 @@ export const ProjectFeatureToggles = ({
}, },
})), })),
{ {
Header: ({ allColumns, setHiddenColumns }: any) => ( id: 'Actions',
<ColumnsMenu maxWidth: 56,
allColumns={allColumns} width: 56,
staticColumns={['actions', 'name']}
dividerAfter={['createdAt']}
dividerBefore={['actions']}
setHiddenColumns={setHiddenColumns}
/>
),
maxWidth: 60,
width: 60,
id: 'actions',
Cell: (props: { row: { original: ListItemType } }) => ( Cell: (props: { row: { original: ListItemType } }) => (
<ActionsCell projectId={projectId} {...props} /> <ActionsCell projectId={projectId} {...props} />
), ),
disableSortBy: true, disableSortBy: true,
}, },
], ],
[projectId, environments, onToggle] [projectId, environments, onToggle, loading]
); );
const initialState = useMemo( const initialState = useMemo(
@ -250,13 +244,15 @@ export const ProjectFeatureToggles = ({
); );
const { const {
state: { filters }, allColumns,
getTableProps,
getTableBodyProps,
headerGroups, headerGroups,
rows, rows,
state: { filters },
getTableBodyProps,
getTableProps,
prepareRow, prepareRow,
setFilter, setFilter,
setHiddenColumns,
} = useTable( } = useTable(
{ {
columns: columns as any[], // TODO: fix after `react-table` v8 update columns: columns as any[], // TODO: fix after `react-table` v8 update
@ -275,7 +271,6 @@ export const ProjectFeatureToggles = ({
() => filters?.find(filterRow => filterRow?.id === 'name')?.value || '', () => filters?.find(filterRow => filterRow?.id === 'name')?.value || '',
[filters] [filters]
); );
return ( return (
<PageContent <PageContent
isLoading={loading} isLoading={loading}
@ -291,6 +286,13 @@ export const ProjectFeatureToggles = ({
initialValue={filter} initialValue={filter}
onChange={value => setFilter('name', value)} onChange={value => setFilter('name', value)}
/> />
<ColumnsMenu
allColumns={allColumns}
staticColumns={['Actions', 'name']}
dividerAfter={['createdAt']}
dividerBefore={['Actions']}
setHiddenColumns={setHiddenColumns}
/>
<PageHeader.Divider /> <PageHeader.Divider />
<ResponsiveButton <ResponsiveButton
onClick={() => onClick={() =>

View File

@ -41,7 +41,7 @@ export const useStyles = makeStyles()(theme => ({
}, },
}, },
subtitle: { subtitle: {
marginBottom: '1.25rem', marginBottom: '1rem',
}, },
emphazisedText: { emphazisedText: {
fontSize: '1.5rem', fontSize: '1.5rem',

View File

@ -121,7 +121,7 @@ const ProjectInfo = ({
/> />
</div> </div>
<div className={styles.idContainer}> <div className={styles.idContainer}>
<p data-loading>projectId: {id}</p> <p>projectId: {id}</p>
</div> </div>
</div> </div>

View File

@ -13,10 +13,10 @@ exports[`renders correctly 1`] = `
className="tss-1ylehva-headerContainer" className="tss-1ylehva-headerContainer"
> >
<div <div
className="tss-1dw2af8-topContainer" className="tss-1uxyh7x-topContainer"
> >
<div <div
className="" className="tss-sd6bs4-header"
data-loading={true} data-loading={true}
> >
<h1 <h1
@ -26,7 +26,7 @@ exports[`renders correctly 1`] = `
</h1> </h1>
</div> </div>
<div <div
className="tss-1u1bjy8-headerActions" className="tss-u5t8ea-headerActions"
> >
<span <span
id="useId-0" id="useId-0"
@ -66,7 +66,7 @@ exports[`renders correctly 1`] = `
</div> </div>
</div> </div>
<div <div
className="tss-f35ha2-bodyContainer" className="tss-54jt3w-bodyContainer"
> >
<ul <ul
className="MuiList-root MuiList-padding mui-h4y409-MuiList-root" className="MuiList-root MuiList-padding mui-h4y409-MuiList-root"

View File

@ -13,10 +13,10 @@ exports[`renders an empty list correctly 1`] = `
className="tss-1ylehva-headerContainer" className="tss-1ylehva-headerContainer"
> >
<div <div
className="tss-1dw2af8-topContainer" className="tss-1uxyh7x-topContainer"
> >
<div <div
className="" className="tss-sd6bs4-header"
data-loading={true} data-loading={true}
> >
<h1 <h1
@ -26,7 +26,7 @@ exports[`renders an empty list correctly 1`] = `
</h1> </h1>
</div> </div>
<div <div
className="tss-1u1bjy8-headerActions" className="tss-u5t8ea-headerActions"
> >
<button <button
className="MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonBase-root mui-1aw3qf3-MuiButtonBase-root-MuiButton-root" className="MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonBase-root mui-1aw3qf3-MuiButtonBase-root-MuiButton-root"
@ -54,7 +54,7 @@ exports[`renders an empty list correctly 1`] = `
</div> </div>
</div> </div>
<div <div
className="tss-f35ha2-bodyContainer" className="tss-54jt3w-bodyContainer"
> >
<ul <ul
className="MuiList-root MuiList-padding mui-h4y409-MuiList-root" className="MuiList-root MuiList-padding mui-h4y409-MuiList-root"

View File

@ -7,9 +7,13 @@ type UsePersistentGlobalState<T> = () => [
setValue: React.Dispatch<React.SetStateAction<T>> setValue: React.Dispatch<React.SetStateAction<T>>
]; ];
// Create a hook that stores global state (shared across all hook instances). /**
// The state is also persisted to localStorage and restored on page load. * Create a hook that stores global state (shared across all hook instances).
// The localStorage state is not synced between tabs. * The state is also persisted to localStorage and restored on page load.
* The localStorage state is not synced between tabs.
*
* @deprecated
*/
export const createPersistentGlobalStateHook = <T extends object>( export const createPersistentGlobalStateHook = <T extends object>(
key: string, key: string,
initialValue: T initialValue: T

View File

@ -4,6 +4,8 @@
* @see https://www.figma.com/file/qdwpPfuitJUNinm6mvmCmG/Unleash-application?node-id=7175%3A44590 * @see https://www.figma.com/file/qdwpPfuitJUNinm6mvmCmG/Unleash-application?node-id=7175%3A44590
*/ */
export const colors = { export const colors = {
white: '#FFFFFF',
black: '#000000',
grey: { grey: {
900: '#202021', 900: '#202021',
800: '#6E6E70', 800: '#6E6E70',

View File

@ -234,5 +234,15 @@ export default createTheme({
}, },
}, },
}, },
MuiSwitch: {
styleOverrides: {
switchBase: {
zIndex: 1,
'&:not(.Mui-checked) .MuiTouchRipple-child': {
color: colors.grey['500'],
},
},
},
},
}, },
}); });

View File

@ -77,5 +77,10 @@ declare module '@mui/system/createTheme/shape' {
borderRadiusExtraLarge: string; borderRadiusExtraLarge: string;
} }
} }
declare module '@mui/material/styles/zIndex' {
interface ZIndex {
sticky: number;
}
}
export {}; export {};