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

feat: new contexts table (#998)

* feat: new contexts table

* improve context list actions

* refactor: disabled icon colors

* fix: update snapshots

* fix: icons

* fix: context fields typo
This commit is contained in:
Tymoteusz Czech 2022-05-20 10:29:23 +02:00 committed by GitHub
parent c28cdab6e8
commit 6d130f61f6
34 changed files with 462 additions and 272 deletions

View File

@ -10,9 +10,6 @@ export const useStyles = makeStyles()(theme => ({
textAlign: 'left', textAlign: 'left',
maxWidth: '300px', maxWidth: '300px',
}, },
icon: {
color: theme.palette.grey[600],
},
description: { description: {
textAlign: 'left', textAlign: 'left',
maxWidth: '300px', maxWidth: '300px',

View File

@ -2,7 +2,7 @@ import { useStyles } from './ProjectRoleListItem.styles';
import { TableCell, TableRow, Typography } from '@mui/material'; import { TableCell, TableRow, Typography } from '@mui/material';
import { Delete, Edit } from '@mui/icons-material'; import { Delete, Edit } from '@mui/icons-material';
import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import SupervisedUserCircleIcon from '@mui/icons-material/SupervisedUserCircle'; import { SupervisedUserCircle } from '@mui/icons-material';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { IProjectRole } from 'interfaces/role'; import { IProjectRole } from 'interfaces/role';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -34,7 +34,7 @@ const RoleListItem = ({
<> <>
<TableRow className={styles.tableRow}> <TableRow className={styles.tableRow}>
<TableCell className={styles.hideXS}> <TableCell className={styles.hideXS}>
<SupervisedUserCircleIcon className={styles.icon} /> <SupervisedUserCircle color="disabled" />
</TableCell> </TableCell>
<TableCell className={styles.leftTableCell}> <TableCell className={styles.leftTableCell}>
<Typography variant="body2" data-loading> <Typography variant="body2" data-loading>

View File

@ -154,7 +154,7 @@ export const useStyles = makeStyles()(theme => ({
height: '32.5px', height: '32.5px',
width: '32.5px', width: '32.5px',
marginRight: '0.5rem', marginRight: '0.5rem',
fill: theme.palette.grey[600], fill: theme.palette.inactiveIcon,
}, },
singleValueView: { singleValueView: {
display: 'flex', display: 'flex',

View File

@ -14,7 +14,7 @@ const EnvironmentIcon = ({ enabled, className }: IEnvironmentIcon) => {
const container = { const container = {
backgroundColor: enabled backgroundColor: enabled
? theme.palette.primary.light ? theme.palette.primary.light
: theme.palette.grey[600], : theme.palette.inactiveIcon,
borderRadius: '50%', borderRadius: '50%',
width: '28px', width: '28px',
height: '28px', height: '28px',

View File

@ -17,6 +17,6 @@ export const useStyles = makeStyles()(theme => ({
}, },
icon: { icon: {
fontSize: '1rem', fontSize: '1rem',
color: theme.palette.grey[600], color: theme.palette.inactiveIcon,
}, },
})); }));

View File

@ -55,9 +55,11 @@ const PermissionIconButton = ({
title={formatAccessText(access, tooltipProps?.title)} title={formatAccessText(access, tooltipProps?.title)}
arrow arrow
> >
<div>
<IconButton {...rest} disabled={!access} size="large"> <IconButton {...rest} disabled={!access} size="large">
{children} {children}
</IconButton> </IconButton>
</div>
</TooltipResolver> </TooltipResolver>
); );
}; };

View File

@ -20,7 +20,7 @@ export const useStyles = makeStyles()(theme => ({
}, },
searchIcon: { searchIcon: {
marginRight: 8, marginRight: 8,
color: theme.palette.grey[600], color: theme.palette.inactiveIcon,
}, },
inputRoot: { inputRoot: {
width: '100%', width: '100%',

View File

@ -55,4 +55,16 @@ export const useStyles = makeStyles()(theme => ({
overflow: 'hidden', overflow: 'hidden',
}, },
}, },
alignLeft: {
justifyContent: 'flex-start',
textAlign: 'left',
},
alignRight: {
justifyContent: 'flex-end',
textAlign: 'right',
},
alignCenter: {
justifyContent: 'center',
textAlign: 'center',
},
})); }));

View File

@ -58,18 +58,18 @@ export const CellSortable: FC<ICellSortableProps> = ({
); );
}; };
const justifyContent = useMemo(() => { const alignClass = useMemo(() => {
switch (align) { switch (align) {
case 'left': case 'left':
return 'flex-start'; return styles.alignLeft;
case 'center': case 'center':
return 'center'; return styles.alignCenter;
case 'right': case 'right':
return 'flex-end'; return styles.alignRight;
default: default:
return undefined; return undefined;
} }
}, [align]); }, [align, styles]);
useEffect(() => { useEffect(() => {
const updateTitle = () => { const updateTitle = () => {
@ -102,10 +102,10 @@ export const CellSortable: FC<ICellSortableProps> = ({
<button <button
className={classnames( className={classnames(
isSorted && styles.sortedButton, isSorted && styles.sortedButton,
styles.sortButton styles.sortButton,
alignClass
)} )}
onClick={onSortClick} onClick={onSortClick}
style={{ justifyContent }}
> >
<span <span
className={styles.label} className={styles.label}
@ -122,7 +122,7 @@ export const CellSortable: FC<ICellSortableProps> = ({
</button> </button>
</Tooltip> </Tooltip>
} }
elseShow={children} elseShow={<div className={alignClass}>{children}</div>}
/> />
</TableCell> </TableCell>
); );

View File

@ -28,7 +28,9 @@ export const SortableTableHeader: VFC<ISortableTableHeaderProps> = ({
return ( return (
<CellSortable <CellSortable
{...column.getHeaderProps( {...column.getHeaderProps(
column.getSortByToggleProps() column.canSort
? column.getSortByToggleProps()
: undefined
)} )}
ariaTitle={ ariaTitle={
typeof content === 'string' typeof content === 'string'

View File

@ -25,7 +25,7 @@ export const useStyles = makeStyles()(theme => ({
}, },
searchIcon: { searchIcon: {
marginRight: 8, marginRight: 8,
color: theme.palette.grey[600], color: theme.palette.inactiveIcon,
}, },
clearContainer: { clearContainer: {
width: '30px', width: '30px',
@ -34,8 +34,6 @@ export const useStyles = makeStyles()(theme => ({
}, },
}, },
clearIcon: { clearIcon: {
// color: theme.palette.grey[700],
color: 'red',
fontSize: '18px', fontSize: '18px',
}, },
inputRoot: { inputRoot: {

View File

@ -25,7 +25,7 @@ export const useStyles = makeStyles()(theme => ({
}, },
searchIcon: { searchIcon: {
marginRight: 8, marginRight: 8,
color: theme.palette.grey[600], color: theme.palette.inactiveIcon,
}, },
clearContainer: { clearContainer: {
width: '30px', width: '30px',

View File

@ -1,60 +0,0 @@
import { FC } from 'react';
import { Link, Typography } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './FeatureLinkCell.styles';
import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
interface IFeatureLinkCellProps {
title?: string;
to: string;
subtitle?: string;
}
export const FeatureLinkCell: FC<IFeatureLinkCellProps> = ({
title,
to,
subtitle,
}) => {
const { classes: styles } = useStyles();
const search = useSearchHighlightContext();
return (
<Link
component={RouterLink}
to={to}
underline="hover"
className={styles.wrapper}
>
<div className={styles.container}>
<span
data-loading
className={styles.title}
style={{
WebkitLineClamp: Boolean(subtitle) ? 1 : 2,
lineClamp: Boolean(subtitle) ? 1 : 2,
}}
>
<Highlighter search={search}>{title}</Highlighter>
</span>
<ConditionallyRender
condition={Boolean(subtitle)}
show={
<>
<Typography
className={styles.description}
component="span"
data-loading
>
<Highlighter search={search}>
{subtitle}
</Highlighter>
</Typography>
</>
}
/>
</div>
</Link>
);
};

View File

@ -8,6 +8,6 @@ export const useStyles = makeStyles()(theme => ({
alignItems: 'center', alignItems: 'center',
}, },
icon: { icon: {
color: theme.palette.grey[600], color: theme.palette.inactiveIcon,
}, },
})); }));

View File

@ -9,6 +9,8 @@ export const useStyles = makeStyles()(theme => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
minHeight: '62px', minHeight: '62px',
},
link: {
'&:hover, &:focus': { '&:hover, &:focus': {
textDecoration: 'none', textDecoration: 'none',
'& > div > span:first-of-type': { '& > div > span:first-of-type': {

View File

@ -0,0 +1,63 @@
import { FC } from 'react';
import { Link, Typography } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './LinkCell.styles';
import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import classnames from 'classnames';
interface ILinkCellProps {
title?: string;
to?: string;
subtitle?: string;
}
export const LinkCell: FC<ILinkCellProps> = ({ title, to, subtitle }) => {
const { classes: styles } = useStyles();
const search = useSearchHighlightContext();
const content = (
<div className={styles.container}>
<span
data-loading
className={styles.title}
style={{
WebkitLineClamp: Boolean(subtitle) ? 1 : 2,
lineClamp: Boolean(subtitle) ? 1 : 2,
}}
>
<Highlighter search={search}>{title}</Highlighter>
</span>
<ConditionallyRender
condition={Boolean(subtitle)}
show={
<>
<Typography
className={styles.description}
component="span"
data-loading
>
<Highlighter search={search}>
{subtitle}
</Highlighter>
</Typography>
</>
}
/>
</div>
);
return to ? (
<Link
component={RouterLink}
to={to}
underline="hover"
className={classnames(styles.wrapper, styles.link)}
>
{content}
</Link>
) : (
<span className={styles.wrapper}>{content}</span>
);
};

View File

@ -0,0 +1,45 @@
import { useContext, VFC } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, IconButton, Tooltip, useMediaQuery } from '@mui/material';
import { Add } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import AccessContext from 'contexts/AccessContext';
import { CREATE_CONTEXT_FIELD } from 'component/providers/AccessProvider/permissions';
interface IAddContextButtonProps {}
export const AddContextButton: VFC<IAddContextButtonProps> = () => {
const smallScreen = useMediaQuery('(max-width:700px)');
const { hasAccess } = useContext(AccessContext);
const navigate = useNavigate();
return (
<ConditionallyRender
condition={hasAccess(CREATE_CONTEXT_FIELD)}
show={
<ConditionallyRender
condition={smallScreen}
show={
<Tooltip title="Add context type" arrow>
<IconButton
onClick={() => navigate('/context/create')}
size="large"
>
<Add />
</IconButton>
</Tooltip>
}
elseShow={
<Button
onClick={() => navigate('/context/create')}
color="primary"
variant="contained"
>
New context field
</Button>
}
/>
}
/>
);
};

View File

@ -0,0 +1,54 @@
import { VFC } from 'react';
import { useNavigate } from 'react-router-dom';
import { Delete, Edit } from '@mui/icons-material';
import { Box } from '@mui/material';
import {
DELETE_CONTEXT_FIELD,
UPDATE_CONTEXT_FIELD,
} from 'component/providers/AccessProvider/permissions';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
interface IContextActionsCellProps {
name: string;
onDelete: () => void;
}
export const ContextActionsCell: VFC<IContextActionsCellProps> = ({
name,
onDelete,
}) => {
const navigate = useNavigate();
return (
<Box
sx={{
display: 'flex',
px: 2,
justifyContent: 'flex-end',
}}
>
<PermissionIconButton
permission={UPDATE_CONTEXT_FIELD}
onClick={() => navigate(`/context/edit/${name}`)}
data-loading
aria-label="edit"
tooltipProps={{
title: 'Edit context field',
}}
>
<Edit />
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_CONTEXT_FIELD}
onClick={onDelete}
data-loading
aria-label="delete"
tooltipProps={{
title: 'Delete context field',
}}
>
<Delete />
</PermissionIconButton>
</Box>
);
};

View File

@ -1,42 +1,133 @@
import { useContext, useState, VFC } from 'react'; import { useContext, useMemo, useState, VFC } from 'react';
import { Add, Album, Delete, Edit } from '@mui/icons-material'; import { useGlobalFilter, useSortBy, useTable } from 'react-table';
import { Link, useNavigate } from 'react-router-dom';
import { import {
Button, Table,
IconButton, SortableTableHeader,
List, TableBody,
ListItem, TableCell,
ListItemIcon, TableRow,
ListItemText, TablePlaceholder,
Tooltip, TableSearch,
useMediaQuery, } from 'component/common/Table';
} from '@mui/material';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { import { UPDATE_CONTEXT_FIELD } from 'component/providers/AccessProvider/permissions';
CREATE_CONTEXT_FIELD,
DELETE_CONTEXT_FIELD,
UPDATE_CONTEXT_FIELD,
} from 'component/providers/AccessProvider/permissions';
import { Dialogue as ConfirmDialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue as ConfirmDialogue } from 'component/common/Dialogue/Dialogue';
import AccessContext from 'contexts/AccessContext'; import AccessContext from 'contexts/AccessContext';
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext'; import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
import useContextsApi from 'hooks/api/actions/useContextsApi/useContextsApi'; import useContextsApi from 'hooks/api/actions/useContextsApi/useContextsApi';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useStyles } from './styles'; import { AddContextButton } from './AddContextButton/AddContextButton';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { sortTypes } from 'utils/sortTypes';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { ContextActionsCell } from './ContextActionsCell/ContextActionsCell';
import { Adjust } from '@mui/icons-material';
import { Box } from '@mui/material';
const ContextList: VFC = () => { const ContextList: VFC = () => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const [showDelDialogue, setShowDelDialogue] = useState(false); const [showDelDialogue, setShowDelDialogue] = useState(false);
const smallScreen = useMediaQuery('(max-width:700px)');
const [name, setName] = useState<string>(); const [name, setName] = useState<string>();
const { context, refetchUnleashContext } = useUnleashContext(); const { context, refetchUnleashContext, loading } = useUnleashContext();
const { removeContext } = useContextsApi(); const { removeContext } = useContextsApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const navigate = useNavigate(); const data = useMemo(() => {
const { classes: styles } = useStyles(); if (loading) {
return Array(5).fill({
name: 'Context name',
description: 'Context description when loading',
});
}
return context
.map(({ name, description, sortOrder }) => ({
name,
description,
sortOrder,
}))
.sort((a, b) => a.sortOrder - b.sortOrder);
}, [context, loading]);
const columns = useMemo(
() => [
{
id: 'Icon',
Cell: () => (
<Box
sx={{
pl: 2,
pr: 1,
display: 'flex',
alignItems: 'center',
}}
>
<Adjust color="disabled" />
</Box>
),
},
{
Header: 'Name',
accessor: 'name',
width: '90%',
Cell: ({
row: {
original: { name, description },
},
}: any) => (
<LinkCell
title={name}
to={
hasAccess(UPDATE_CONTEXT_FIELD)
? `/context/edit/${name}`
: undefined
}
subtitle={description}
/>
),
sortType: 'alphanumeric',
},
{
Header: 'Actions',
id: 'Actions',
align: 'center',
Cell: ({
row: {
original: { name },
},
}: any) => (
<ContextActionsCell
name={name}
onDelete={() => {
setName(name);
setShowDelDialogue(true);
}}
/>
),
width: 150,
disableSortBy: true,
},
{
accessor: 'description',
disableSortBy: true,
},
{
accessor: 'sortOrder',
sortType: 'number',
},
],
[hasAccess]
);
const initialState = useMemo(
() => ({
sortBy: [{ id: 'name', desc: false }],
hiddenColumns: ['description', 'sortOrder'],
}),
[]
);
const onDeleteContext = async () => { const onDeleteContext = async () => {
try { try {
@ -57,103 +148,86 @@ const ContextList: VFC = () => {
setShowDelDialogue(false); setShowDelDialogue(false);
}; };
const contextList = () => const {
context.map(field => ( getTableProps,
<ListItem key={field.name} classes={{ root: styles.listItem }}> getTableBodyProps,
<ListItemIcon> headerGroups,
<Album /> rows,
</ListItemIcon> prepareRow,
<ListItemText state: { globalFilter },
primary={ setGlobalFilter,
<ConditionallyRender } = useTable(
condition={hasAccess(UPDATE_CONTEXT_FIELD)} {
show={ columns: columns as any[], // TODO: fix after `react-table` v8 update
<Link to={`/context/edit/${field.name}`}> data,
<strong>{field.name}</strong> initialState,
</Link> sortTypes,
} autoResetGlobalFilter: false,
elseShow={<strong>{field.name}</strong>} autoResetSortBy: false,
/> disableSortRemove: true,
} },
secondary={field.description} useGlobalFilter,
/> useSortBy
<ConditionallyRender
condition={hasAccess(UPDATE_CONTEXT_FIELD)}
show={
<Tooltip title="Edit context field" arrow>
<IconButton
onClick={() =>
navigate(`/context/edit/${field.name}`)
}
size="large"
>
<Edit />
</IconButton>
</Tooltip>
}
/>
<ConditionallyRender
condition={hasAccess(DELETE_CONTEXT_FIELD)}
show={
<Tooltip title="Delete context field" arrow>
<IconButton
aria-label="delete"
onClick={() => {
setName(field.name);
setShowDelDialogue(true);
}}
size="large"
>
<Delete />
</IconButton>
</Tooltip>
}
/>
</ListItem>
));
const headerButton = () => (
<ConditionallyRender
condition={hasAccess(CREATE_CONTEXT_FIELD)}
show={
<ConditionallyRender
condition={smallScreen}
show={
<Tooltip title="Add context type" arrow>
<IconButton
onClick={() => navigate('/context/create')}
size="large"
>
<Add />
</IconButton>
</Tooltip>
}
elseShow={
<Button
onClick={() => navigate('/context/create')}
color="primary"
variant="contained"
>
New context field
</Button>
}
/>
}
/>
); );
return ( return (
<PageContent <PageContent
header={ header={
<PageHeader actions={headerButton()} title={'Context fields'} /> <PageHeader
title="Context fields"
actions={
<>
<TableSearch
initialValue={globalFilter}
onChange={setGlobalFilter}
/>
<PageHeader.Divider />
<AddContextButton />
</>
}
/>
} }
> >
<List> <SearchHighlightProvider value={globalFilter}>
<Table {...getTableProps()}>
<SortableTableHeader headerGroups={headerGroups} />
<TableBody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row);
return (
<TableRow hover {...row.getRowProps()}>
{row.cells.map(cell => (
<TableCell {...cell.getCellProps()}>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</SearchHighlightProvider>
<ConditionallyRender <ConditionallyRender
condition={context.length > 0} condition={rows.length === 0}
show={contextList} show={
elseShow={<ListItem>No context fields defined</ListItem>} <ConditionallyRender
condition={globalFilter?.length > 0}
show={
<TablePlaceholder>
No contexts found matching &ldquo;
{globalFilter}
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
No contexts available. Get started by adding
one.
</TablePlaceholder>
}
/>
}
/> />
</List>
<ConfirmDialogue <ConfirmDialogue
open={showDelDialogue} open={showDelDialogue}
onClick={onDeleteContext} onClick={onDeleteContext}

View File

@ -1,11 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()({
listItem: {
padding: 0,
'& a': {
textDecoration: 'none',
color: 'inherit',
},
},
});

View File

@ -11,7 +11,7 @@ export const useStyles = makeStyles()(theme => ({
minWidth: '450px', minWidth: '450px',
}, },
icon: { icon: {
fill: theme.palette.grey[600], fill: theme.palette.inactiveIcon,
marginRight: '0.5rem', marginRight: '0.5rem',
}, },
header: { header: {

View File

@ -13,7 +13,7 @@ import {
} from 'component/common/Table'; } from 'component/common/Table';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { DateCell } from '../../../common/Table/cells/DateCell/DateCell'; import { DateCell } from '../../../common/Table/cells/DateCell/DateCell';
import { FeatureLinkCell } from 'component/common/Table/cells/FeatureLinkCell/FeatureLinkCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell'; import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell'; import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
@ -53,7 +53,7 @@ const columns = [
original: { name, description, project }, original: { name, description, project },
}, },
}) => ( }) => (
<FeatureLinkCell <LinkCell
title={name} title={name}
subtitle={description} subtitle={description}
to={`/projects/${project}/features/${name}`} to={`/projects/${project}/features/${name}`}
@ -71,7 +71,7 @@ const columns = [
Header: 'Project ID', Header: 'Project ID',
accessor: 'project', accessor: 'project',
Cell: ({ value }: { value: string }) => ( Cell: ({ value }: { value: string }) => (
<FeatureLinkCell title={value} to={`/projects/${value}`} /> <LinkCell title={value} to={`/projects/${value}`} />
), ),
sortType: 'alphanumeric', sortType: 'alphanumeric',
}, },

View File

@ -116,7 +116,7 @@ export const useStyles = makeStyles()(theme => ({
}, },
}, },
strategyIcon: { strategyIcon: {
fill: theme.palette.grey[600], fill: theme.palette.inactiveIcon,
}, },
container: { container: {
display: 'flex', display: 'flex',

View File

@ -16,7 +16,7 @@ export const useStyles = makeStyles()(theme => ({
borderBottom: `1px solid ${theme.palette.grey[300]}`, borderBottom: `1px solid ${theme.palette.grey[300]}`,
}, },
icon: { icon: {
fill: theme.palette.grey[600], fill: theme.palette.inactiveIcon,
}, },
actions: { actions: {
marginLeft: 'auto', marginLeft: 'auto',

View File

@ -2,6 +2,6 @@ import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({ export const useStyles = makeStyles()(theme => ({
icon: { icon: {
color: theme.palette.grey[600], color: theme.palette.inactiveIcon,
}, },
})); }));

View File

@ -32,6 +32,6 @@ export const useStyles = makeStyles()(theme => ({
}, },
closeIcon: { closeIcon: {
fontSize: '1.5rem', fontSize: '1.5rem',
color: theme.palette.grey[600], color: theme.palette.inactiveIcon,
}, },
})); }));

View File

@ -14,7 +14,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from '@mui/material'; } from '@mui/material';
import ViewColumnIcon from '@mui/icons-material/ViewColumn'; import ColumnIcon from '@mui/icons-material/ViewWeek';
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 { useStyles } from './ColumnsMenu.styles'; import { useStyles } from './ColumnsMenu.styles';
@ -105,7 +105,7 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
className={classes.button} className={classes.button}
data-loading data-loading
> >
<ViewColumnIcon /> <ColumnIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>

View File

@ -11,7 +11,7 @@ import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import { FeatureLinkCell } from 'component/common/Table/cells/FeatureLinkCell/FeatureLinkCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell'; import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
@ -176,7 +176,7 @@ export const ProjectFeatureToggles = ({
Header: 'Feature toggle name', Header: 'Feature toggle name',
accessor: 'name', accessor: 'name',
Cell: ({ value }: { value: string }) => ( Cell: ({ value }: { value: string }) => (
<FeatureLinkCell <LinkCell
title={value} title={value}
to={`/projects/${projectId}/features/${value}`} to={`/projects/${projectId}/features/${value}`}
/> />
@ -271,6 +271,7 @@ 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}

View File

@ -11,7 +11,7 @@ export const useStyles = makeStyles()(theme => ({
maxWidth: '300px', maxWidth: '300px',
}, },
icon: { icon: {
color: theme.palette.grey[600], color: theme.palette.inactiveIcon,
}, },
descriptionCell: { descriptionCell: {
textAlign: 'left', textAlign: 'left',

View File

@ -255,7 +255,7 @@ export const StrategiesList = () => {
strategies.map(strategy => ( strategies.map(strategy => (
<ListItem key={strategy.name} className={styles.listItem}> <ListItem key={strategy.name} className={styles.listItem}>
<ListItemAvatar> <ListItemAvatar>
<Extension style={{ color: '#0000008a' }} /> <Extension color="disabled" />
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={strategyLink(strategy?.name, strategy?.deprecated)} primary={strategyLink(strategy?.name, strategy?.deprecated)}

View File

@ -80,14 +80,9 @@ exports[`renders correctly 1`] = `
> >
<svg <svg
aria-hidden={true} aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium mui-i4bv87-MuiSvgIcon-root" className="MuiSvgIcon-root MuiSvgIcon-colorDisabled MuiSvgIcon-fontSizeMedium mui-1db085k-MuiSvgIcon-root"
data-testid="ExtensionIcon" data-testid="ExtensionIcon"
focusable="false" focusable="false"
style={
{
"color": "#0000008a",
}
}
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path <path
@ -117,11 +112,20 @@ exports[`renders correctly 1`] = `
</p> </p>
</div> </div>
<div> <div>
<button <div
aria-label="Deprecate strategy" aria-label="Deprecate strategy"
aria-labelledby={null} aria-labelledby={null}
className="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeLarge mui-mf1cb5-MuiButtonBase-root-MuiIconButton-root" className=""
data-mui-internal-clone-element={true} data-mui-internal-clone-element={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<button
className="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeLarge mui-mf1cb5-MuiButtonBase-root-MuiIconButton-root"
disabled={false} disabled={false}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}
@ -132,7 +136,6 @@ exports[`renders correctly 1`] = `
onKeyUp={[Function]} onKeyUp={[Function]}
onMouseDown={[Function]} onMouseDown={[Function]}
onMouseLeave={[Function]} onMouseLeave={[Function]}
onMouseOver={[Function]}
onMouseUp={[Function]} onMouseUp={[Function]}
onTouchEnd={[Function]} onTouchEnd={[Function]}
onTouchMove={[Function]} onTouchMove={[Function]}
@ -156,6 +159,7 @@ exports[`renders correctly 1`] = `
/> />
</button> </button>
</div> </div>
</div>
<div <div
aria-label="You cannot delete a built-in strategy" aria-label="You cannot delete a built-in strategy"
aria-labelledby={null} aria-labelledby={null}

View File

@ -1,5 +1,4 @@
import useSWR, { mutate, SWRConfiguration } from 'swr'; import useSWR, { SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import { IUnleashContextDefinition } from 'interfaces/context'; import { IUnleashContextDefinition } from 'interfaces/context';
@ -9,7 +8,6 @@ interface IUnleashContextOutput {
refetchUnleashContext: () => void; refetchUnleashContext: () => void;
loading: boolean; loading: boolean;
error?: Error; error?: Error;
CONTEXT_CACHE_KEY: string;
} }
const useUnleashContext = ( const useUnleashContext = (
@ -30,24 +28,17 @@ const useUnleashContext = (
const CONTEXT_CACHE_KEY = 'api/admin/context'; const CONTEXT_CACHE_KEY = 'api/admin/context';
const { data, error } = useSWR(CONTEXT_CACHE_KEY, fetcher, options); const { data, mutate, error, isValidating } = useSWR(
CONTEXT_CACHE_KEY,
const [loading, setLoading] = useState(!error && !data); fetcher,
options
const refetchUnleashContext = () => { );
mutate(CONTEXT_CACHE_KEY);
};
useEffect(() => {
setLoading(!error && !data);
}, [data, error]);
return { return {
context: data || [], context: data || [],
error, error,
loading, loading: isValidating && !error && !data,
refetchUnleashContext, refetchUnleashContext: mutate,
CONTEXT_CACHE_KEY,
}; };
}; };

View File

@ -100,6 +100,7 @@ export default createTheme({
inactive: colors.orange[200], inactive: colors.orange[200],
abandoned: colors.red[200], abandoned: colors.red[200],
}, },
inactiveIcon: colors.grey[600],
}, },
components: { components: {
MuiLink: { MuiLink: {
@ -244,5 +245,12 @@ export default createTheme({
}, },
}, },
}, },
MuiIcon: {
styleOverrides: {
colorDisabled: {
color: colors.grey[600],
},
},
},
}, },
}); });

View File

@ -61,6 +61,14 @@ declare module '@mui/material/styles' {
* For sidebar container background. * For sidebar container background.
*/ */
sidebarContainer: string; sidebarContainer: string;
/**
* Icon that doesn't have an action (visual only, no click handler).
*
* @deprecated use `<YourIcon color="disabled" />`.
* `color` only works with `import { YourIcon } from "@mui/icons"`
* and not with `import YourIcon from "@mui/icons/YourIcon"`.
*/
inactiveIcon: string;
} }
interface Theme extends CustomTheme {} interface Theme extends CustomTheme {}