1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

fix: make toggle list sortable inside a project (#436)

* fix: make toggle list sortable inside a project

* fix: minor cleanup
This commit is contained in:
Ivar Conradi Østhus 2021-10-15 11:20:14 +02:00 committed by GitHub
parent 4230fc98ed
commit d42d412bc8
13 changed files with 233 additions and 159 deletions

View File

@ -37,6 +37,14 @@ const PaginateUI = ({
} }
}, [matches]); }, [matches]);
useEffect(() => {
if(pageIndex === 0 && start !== 0) {
setStart(0);
setLimit(STARTLIMIT);
}
}, [pageIndex, start])
return ( return (
<ConditionallyRender <ConditionallyRender
condition={pages.length > 1} condition={pages.length > 1}

View File

@ -2,9 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { MenuItem, Typography } from '@material-ui/core'; import { MenuItem, Typography } from '@material-ui/core';
import { HourglassEmpty, HourglassFull } from '@material-ui/icons'; // import { HourglassEmpty, HourglassFull } from '@material-ui/icons';
// import { MenuItemWithIcon } from '../../../common';
import { MenuItemWithIcon } from '../../../common';
import DropdownMenu from '../../../common/DropdownMenu/DropdownMenu'; import DropdownMenu from '../../../common/DropdownMenu/DropdownMenu';
import ProjectSelect from '../../../common/ProjectSelect'; import ProjectSelect from '../../../common/ProjectSelect';
import { useStyles } from './styles'; import { useStyles } from './styles';
@ -53,6 +52,7 @@ const FeatureToggleListActions = ({
</MenuItem> </MenuItem>
)); ));
/*
const renderMetricsOptions = () => [ const renderMetricsOptions = () => [
<MenuItemWithIcon <MenuItemWithIcon
style={{ fontSize: '14px' }} style={{ fontSize: '14px' }}
@ -71,12 +71,14 @@ const FeatureToggleListActions = ({
key={2} key={2}
/>, />,
]; ];
*/
return ( return (
<div className={styles.actions} ref={ref}> <div className={styles.actions} ref={ref}>
<Typography variant="body2" data-loading> <Typography variant="body2" data-loading>
Sorted by: Sorted by:
</Typography> </Typography>
{/* }
<DropdownMenu <DropdownMenu
id={'metric'} id={'metric'}
label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`} label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`}
@ -87,6 +89,7 @@ const FeatureToggleListActions = ({
style={{ textTransform: 'lowercase', fontWeight: 'normal' }} style={{ textTransform: 'lowercase', fontWeight: 'normal' }}
data-loading data-loading
/> />
{*/}
<DropdownMenu <DropdownMenu
id={'sorting'} id={'sorting'}
label={`By ${settings.sort}`} label={`By ${settings.sort}`}

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Chip, IconButton, ListItem } from '@material-ui/core'; import { Chip, IconButton, ListItem, Tooltip } from '@material-ui/core';
import { Undo } from '@material-ui/icons'; import { Undo } from '@material-ui/icons';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
@ -58,9 +58,11 @@ const FeatureToggleListItem = ({
commonStyles.truncate commonStyles.truncate
)} )}
> >
<span className={commonStyles.toggleName}> <Tooltip title={description}>
{name}&nbsp; <span className={commonStyles.toggleName}>
</span> {name}&nbsp;
</span>
</Tooltip>
<span className={styles.listItemToggle}> <span className={styles.listItemToggle}>
</span> </span>
<small> <small>
@ -74,9 +76,11 @@ const FeatureToggleListItem = ({
</Link> </Link>
} elseShow={ } elseShow={
<> <>
<span className={commonStyles.toggleName}> <Tooltip title={description}>
{name}&nbsp; <span className={commonStyles.toggleName}>
</span> {name}&nbsp;
z </span>
</Tooltip>
<span className={styles.listItemToggle}> <span className={styles.listItemToggle}>
</span> </span>
<small> <small>
@ -100,7 +104,9 @@ const FeatureToggleListItem = ({
)} )}
> >
<Status stale={stale} showActive={false} /> <Status stale={stale} showActive={false} />
<Chip color="primary" variant="outlined" className={styles.typeChip} style={{marginLeft: '8px'}} title={`Project: ${project}`} label={project}/> <Link to={`/projects/${project}`} style={{textDecoration: 'none'}}>
<Chip color="primary" variant="outlined" className={styles.typeChip} style={{marginLeft: '8px' }} title={`Project: ${project}`} label={project}/>
</Link>
</span> </span>
<ConditionallyRender <ConditionallyRender
condition={revive} condition={revive}

View File

@ -13,12 +13,14 @@ export const useStyles = makeStyles(theme => ({
listItemType: { listItemType: {
width: '40px', width: '40px',
textAlign: 'center', textAlign: 'center',
marginRight: '0',
flexShrink: '0',
}, },
listItemSvg: { listItemSvg: {
fill: theme.palette.icons.lightGrey, fill: theme.palette.icons.lightGrey,
}, },
listItemLink: { listItemLink: {
marginLeft: '10px', marginLeft: '0.25rem',
minWidth: '0', minWidth: '0',
}, },
listItemStrategies: { listItemStrategies: {

View File

@ -23,7 +23,7 @@ exports[`renders correctly with one feature 1`] = `
"fontSize": "0.8rem", "fontSize": "0.8rem",
} }
} }
title="No usage reported" title="No usage reported from connected applications"
> >
<span <span
style={ style={
@ -68,7 +68,15 @@ exports[`renders correctly with one feature 1`] = `
onClick={[Function]} onClick={[Function]}
> >
<span <span
aria-describedby={null}
className="toggleName" className="toggleName"
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="another's description"
> >
Another Another
   
@ -96,23 +104,33 @@ exports[`renders correctly with one feature 1`] = `
<span <span
className="makeStyles-listItemStrategies-6 hideLt920" className="makeStyles-listItemStrategies-6 hideLt920"
> >
<div <a
className="MuiChip-root MuiChip-colorPrimary MuiChip-outlined MuiChip-outlinedPrimary" href="/projects/default"
onKeyDown={[Function]} onClick={[Function]}
onKeyUp={[Function]}
style={ style={
Object { Object {
"marginLeft": "8px", "textDecoration": "none",
} }
} }
title="Project: default"
> >
<span <div
className="MuiChip-label" className="MuiChip-root MuiChip-colorPrimary MuiChip-outlined MuiChip-outlinedPrimary"
onKeyDown={[Function]}
onKeyUp={[Function]}
style={
Object {
"marginLeft": "8px",
}
}
title="Project: default"
> >
default <span
</span> className="MuiChip-label"
</div> >
default
</span>
</div>
</a>
</span> </span>
</li> </li>
`; `;
@ -140,7 +158,7 @@ exports[`renders correctly with one feature without permission 1`] = `
"fontSize": "0.8rem", "fontSize": "0.8rem",
} }
} }
title="No usage reported" title="No usage reported from connected applications"
> >
<span <span
style={ style={
@ -185,7 +203,15 @@ exports[`renders correctly with one feature without permission 1`] = `
onClick={[Function]} onClick={[Function]}
> >
<span <span
aria-describedby={null}
className="toggleName" className="toggleName"
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="another's description"
> >
Another Another
   
@ -213,21 +239,31 @@ exports[`renders correctly with one feature without permission 1`] = `
<span <span
className="makeStyles-listItemStrategies-6 hideLt920" className="makeStyles-listItemStrategies-6 hideLt920"
> >
<div <a
className="MuiChip-root MuiChip-colorPrimary MuiChip-outlined MuiChip-outlinedPrimary" href="/projects/undefined"
onKeyDown={[Function]} onClick={[Function]}
onKeyUp={[Function]}
style={ style={
Object { Object {
"marginLeft": "8px", "textDecoration": "none",
} }
} }
title="Project: undefined"
> >
<span <div
className="MuiChip-label" className="MuiChip-root MuiChip-colorPrimary MuiChip-outlined MuiChip-outlinedPrimary"
/> onKeyDown={[Function]}
</div> onKeyUp={[Function]}
style={
Object {
"marginLeft": "8px",
}
}
title="Project: undefined"
>
<span
className="MuiChip-label"
/>
</div>
</a>
</span> </span>
</li> </li>
`; `;

View File

@ -85,60 +85,6 @@ exports[`renders correctly with one feature 1`] = `
> >
Sorted by: Sorted by:
</p> </p>
<button
aria-controls="metric"
aria-haspopup="true"
className="MuiButtonBase-root MuiButton-root MuiButton-text"
data-loading={true}
disabled={false}
id="metric"
onBlur={[Function]}
onClick={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
style={
Object {
"fontWeight": "normal",
"textTransform": "lowercase",
}
}
tabIndex={0}
title="Metric interval"
type="button"
>
<span
className="MuiButton-label"
>
Last minute
<span
className="MuiButton-endIcon MuiButton-iconSizeMedium"
>
<span
aria-hidden={true}
className="material-icons MuiIcon-root"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
</span>
</span>
</span>
</button>
<button <button
aria-controls="sorting" aria-controls="sorting"
aria-haspopup="true" aria-haspopup="true"
@ -336,63 +282,6 @@ exports[`renders correctly with one feature without permissions 1`] = `
> >
Sorted by: Sorted by:
</p> </p>
<button
aria-controls="metric"
aria-haspopup="true"
className="MuiButtonBase-root MuiButton-root MuiButton-text"
data-loading={true}
disabled={false}
id="metric"
onBlur={[Function]}
onClick={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
style={
Object {
"fontWeight": "normal",
"textTransform": "lowercase",
}
}
tabIndex={0}
title="Metric interval"
type="button"
>
<span
className="MuiButton-label"
>
Last minute
<span
className="MuiButton-endIcon MuiButton-iconSizeMedium"
>
<span
aria-hidden={true}
className="material-icons MuiIcon-root"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
</span>
</span>
</span>
<span
className="MuiTouchRipple-root"
/>
</button>
<button <button
aria-controls="sorting" aria-controls="sorting"
aria-haspopup="true" aria-haspopup="true"

View File

@ -14,6 +14,9 @@ export const useStyles = makeStyles(theme => ({
color: theme.palette.grey[600], color: theme.palette.grey[600],
borderBottom: '1px solid ' + theme.palette.grey[200], borderBottom: '1px solid ' + theme.palette.grey[200],
}, },
tableCellHeaderSortable: {
cursor: 'pointer',
},
tableCellStatus: { tableCellStatus: {
width: '50px', width: '50px',
}, },
@ -26,6 +29,11 @@ export const useStyles = makeStyles(theme => ({
display: 'none', display: 'none',
}, },
}, },
tableCellCreated: {
[theme.breakpoints.down('sm')]: {
display: 'none',
},
},
tableCellType: { tableCellType: {
width: '32px', width: '32px',
alignItems: 'center', alignItems: 'center',

View File

@ -1,3 +1,4 @@
import { useState } from 'react';
import { import {
Table, Table,
TableBody, TableBody,
@ -22,14 +23,86 @@ interface IFeatureToggleListNewProps {
projectId: string; projectId: string;
} }
//@ts-ignore
const sortList = (list, sortOpt) => {
if(!list) {
return list;
}
if(!sortOpt.field) {
return list;
}
if (sortOpt.type === 'string') {
//@ts-ignore
return list.sort((a, b) => {
const fieldA = a[sortOpt.field]?.toUpperCase();
const fieldB = b[sortOpt.field]?.toUpperCase();
const direction = sortOpt.direction;
if (fieldA < fieldB) {
return direction === 0 ? -1 : 1;
}
if (fieldA > fieldB) {
return direction === 0 ? 1 : -1;
}
return 0;
})
}
if (sortOpt.type === 'date') {
//@ts-ignore
return list.sort((a, b) => {
const fieldA = new Date(a[sortOpt.field]);
const fieldB = new Date(b[sortOpt.field]);
if (fieldA < fieldB) {
return sortOpt.direction === 0 ? 1 : -1;
}
if (fieldA > fieldB) {
return sortOpt.direction === 0 ? -1 : 1;
}
return 0;
})
}
return list;
}
const FeatureToggleListNew = ({ const FeatureToggleListNew = ({
features, features,
loading, loading,
projectId, projectId,
}: IFeatureToggleListNewProps) => { }: IFeatureToggleListNewProps) => {
const styles = useStyles(); const styles = useStyles();
const [sortOpt, setSortOpt] = useState({
field: 'name',
type: 'string',
direction: 0,
});
const [sortedFeatures, setSortedFeatures] = useState(sortList([...features], sortOpt));
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } = const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
usePagination(features, 50); usePagination(sortedFeatures, 50);
const updateSort = (field: string) => {
let newSortOpt;
if(field === sortOpt.field) {
newSortOpt = {...sortOpt, direction: (sortOpt.direction + 1) % 2};
}
else if(['createdAt', 'lastSeenAt'].includes(field)) {
newSortOpt = {
field,
type: 'date',
direction: 0
};
} else {
newSortOpt = {
field,
type: 'string',
direction: 0
};
}
setSortOpt(newSortOpt);
setSortedFeatures(sortList([...features], newSortOpt));
setPageIndex(0);
};
const getEnvironments = () => { const getEnvironments = () => {
if (features.length > 0) { if (features.length > 0) {
@ -55,6 +128,7 @@ const FeatureToggleListNew = ({
type={feature.type} type={feature.type}
environments={feature.environments} environments={feature.environments}
projectId={projectId} projectId={projectId}
createdAt={new Date()}
/> />
); );
}); });
@ -69,6 +143,7 @@ const FeatureToggleListNew = ({
environments={feature.environments} environments={feature.environments}
projectId={projectId} projectId={projectId}
lastSeenAt={feature.lastSeenAt} lastSeenAt={feature.lastSeenAt}
createdAt={feature.createdAt}
/> />
); );
}); });
@ -83,31 +158,45 @@ const FeatureToggleListNew = ({
className={classnames( className={classnames(
styles.tableCell, styles.tableCell,
styles.tableCellStatus, styles.tableCellStatus,
styles.tableCellHeader styles.tableCellHeader,
styles.tableCellHeaderSortable,
)} )}
align="left" align="left"
> >
<span data-loading>Status</span> <span data-loading onClick={() => updateSort('lastSeenAt')}>Status</span>
</TableCell> </TableCell>
<TableCell <TableCell
className={classnames( className={classnames(
styles.tableCell, styles.tableCell,
styles.tableCellType, styles.tableCellType,
styles.tableCellHeader styles.tableCellHeader,
styles.tableCellHeaderSortable,
)} )}
align="center" align="center"
> >
<span data-loading>Type</span> <span data-loading onClick={() => updateSort('type')}>Type</span>
</TableCell> </TableCell>
<TableCell <TableCell
className={classnames( className={classnames(
styles.tableCell, styles.tableCell,
styles.tableCellName, styles.tableCellName,
styles.tableCellHeader styles.tableCellHeader,
styles.tableCellHeaderSortable,
)} )}
align="left" align="left"
> >
<span data-loading>Name</span> <span data-loading onClick={() => updateSort('name')}>Name</span>
</TableCell>
<TableCell
className={classnames(
styles.tableCell,
styles.tableCellCreated,
styles.tableCellHeader,
styles.tableCellHeaderSortable,
)}
align="left"
>
<span data-loading onClick={() => updateSort('createdAt')}>Created</span>
</TableCell> </TableCell>
{getEnvironments().map((env: any) => { {getEnvironments().map((env: any) => {
return ( return (
@ -116,7 +205,8 @@ const FeatureToggleListNew = ({
className={classnames( className={classnames(
styles.tableCell, styles.tableCell,
styles.tableCellEnv, styles.tableCellEnv,
styles.tableCellHeader styles.tableCellHeader,
styles.tableCellHeaderSortable,
)} )}
align="center" align="center"
> >

View File

@ -0,0 +1,25 @@
import { Tooltip } from '@material-ui/core';
import { connect } from 'react-redux';
import { formatDateWithLocale, formatFullDateTimeWithLocale } from '../../../common/util';
interface CreatedAtProps {
time: Date;
//@ts-ignore
location: any;
}
const CreatedAt = ({time, location}: CreatedAtProps) => {
return (
<Tooltip title={`Created at ${formatFullDateTimeWithLocale(time, location.locale)}`}>
<span>
{formatDateWithLocale(time, location.locale)}
</span>
</Tooltip>
);
}
const mapStateToProps = (state: any) => ({
location: state.settings.toJS().location,
});
export default connect(mapStateToProps)(CreatedAt);

View File

@ -16,6 +16,7 @@ import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import FeatureStatus from '../../FeatureView2/FeatureStatus/FeatureStatus'; import FeatureStatus from '../../FeatureView2/FeatureStatus/FeatureStatus';
import FeatureType from '../../FeatureView2/FeatureType/FeatureType'; import FeatureType from '../../FeatureView2/FeatureType/FeatureType';
import classNames from 'classnames'; import classNames from 'classnames';
import CreatedAt from './CreatedAt';
interface IFeatureToggleListNewItemProps { interface IFeatureToggleListNewItemProps {
name: string; name: string;
@ -23,6 +24,7 @@ interface IFeatureToggleListNewItemProps {
environments: IFeatureEnvironment[]; environments: IFeatureEnvironment[];
projectId: string; projectId: string;
lastSeenAt?: Date; lastSeenAt?: Date;
createdAt: Date;
} }
const FeatureToggleListNewItem = ({ const FeatureToggleListNewItem = ({
@ -31,6 +33,7 @@ const FeatureToggleListNewItem = ({
type, type,
environments, environments,
projectId, projectId,
createdAt,
}: IFeatureToggleListNewItemProps) => { }: IFeatureToggleListNewItemProps) => {
const { toast, setToastData } = useToast(); const { toast, setToastData } = useToast();
const { toggleFeatureByEnvironment } = useToggleFeatureByEnv( const { toggleFeatureByEnvironment } = useToggleFeatureByEnv(
@ -87,6 +90,10 @@ const FeatureToggleListNewItem = ({
styles.tableCell, styles.tableCellName)} align="left" onClick={onClick}> styles.tableCell, styles.tableCellName)} align="left" onClick={onClick}>
<span data-loading>{name}</span> <span data-loading>{name}</span>
</TableCell> </TableCell>
<TableCell className={classNames(
styles.tableCell, styles.tableCellCreated)} align="left" onClick={onClick}>
<CreatedAt time={createdAt} />
</TableCell>
{environments.map((env: IEnvironments) => { {environments.map((env: IEnvironments) => {

View File

@ -83,7 +83,7 @@ const FeatureStatus = ({ lastSeenAt }: FeatureStatusProps) => {
) => { ) => {
return ( return (
<Wrapper <Wrapper
toolTip={`Last usage reported ${value} ${unit} ${suffix}`} toolTip={`Last usage reported ${value} ${unit}${value !== 1 ? 's' : ''} ${suffix}`}
color={getColor(unit)} color={getColor(unit)}
> >
{value} {value}
@ -94,7 +94,7 @@ const FeatureStatus = ({ lastSeenAt }: FeatureStatusProps) => {
/> />
} }
elseShow={ elseShow={
<Wrapper toolTip="No usage reported" color={getColor()}> <Wrapper toolTip="No usage reported from connected applications" color={getColor()}>
<span style={{ fontSize: '1.4rem' }}></span> <span style={{ fontSize: '1.4rem' }}></span>
</Wrapper> </Wrapper>
} }

View File

@ -14,7 +14,7 @@ export const useStyles = makeStyles(theme => ({
width: 'inherit', width: 'inherit',
}, },
}, },
bodyClass: { padding: '0.5rem 2rem' }, bodyClass: { padding: '0.5rem 1rem' },
header: { header: {
padding: '1rem', padding: '1rem',
}, },

View File

@ -14,8 +14,8 @@ export interface IEnvironments {
export interface IFeatureToggle { export interface IFeatureToggle {
stale: boolean; stale: boolean;
archived: boolean; archived: boolean;
createdAt: string; createdAt: Date;
lastSeenAt: Date; lastSeenAt?: Date;
description: string; description: string;
environments: IFeatureEnvironment[]; environments: IFeatureEnvironment[];
name: string; name: string;