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

fix: misc UI improvements (#806)

* fix: link color in project features

Co-authored-by: olav <mail@olav.io>

* fix: link in health page

Co-authored-by: olav <mail@olav.io>

* fix: keep sorting state in project toggles list

Co-authored-by: olav <mail@olav.io>

* fix: style link in the toggle list project

Co-authored-by: olav <mail@olav.io>

* refactor: update browser list

Co-authored-by: olav <mail@olav.io>

Co-authored-by: olav <mail@olav.io>
This commit is contained in:
Youssef Khedher 2022-03-25 15:30:52 +01:00 committed by GitHub
parent cfc2338e78
commit 4589a19e03
12 changed files with 218 additions and 166 deletions

View File

@ -1,29 +1,26 @@
import React, { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Paper, MenuItem } from '@material-ui/core'; import { Paper, MenuItem } from '@material-ui/core';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ReportToggleListItem from './ReportToggleListItem/ReportToggleListItem'; import ReportToggleListItem from './ReportToggleListItem/ReportToggleListItem';
import ReportToggleListHeader from './ReportToggleListHeader/ReportToggleListHeader'; import ReportToggleListHeader from './ReportToggleListHeader/ReportToggleListHeader';
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
import DropdownMenu from '../../common/DropdownMenu/DropdownMenu'; import DropdownMenu from '../../common/DropdownMenu/DropdownMenu';
import { import {
getObjectProperties, getObjectProperties,
getCheckedState, getCheckedState,
applyCheckedToFeatures, applyCheckedToFeatures,
} from '../utils'; } from '../utils';
import { useStyles } from './ReportToggleList.styles';
import useSort from '../../../hooks/useSort'; import { useFeaturesSort } from 'hooks/useFeaturesSort';
import styles from './ReportToggleList.module.scss';
/* FLAG TO TOGGLE UNFINISHED BULK ACTIONS FEATURE */ /* FLAG TO TOGGLE UNFINISHED BULK ACTIONS FEATURE */
const BULK_ACTIONS_ON = false; const BULK_ACTIONS_ON = false;
const ReportToggleList = ({ features, selectedProject }) => { const ReportToggleList = ({ features, selectedProject }) => {
const styles = useStyles();
const [checkAll, setCheckAll] = useState(false); const [checkAll, setCheckAll] = useState(false);
const [localFeatures, setFeatures] = useState([]); const [localFeatures, setFeatures] = useState([]);
const [sort, setSortData] = useSort(); const { setSort, sorted } = useFeaturesSort(localFeatures);
useEffect(() => { useEffect(() => {
const formattedFeatures = features.map(feature => ({ const formattedFeatures = features.map(feature => ({
@ -52,7 +49,7 @@ const ReportToggleList = ({ features, selectedProject }) => {
}; };
const renderListRows = () => const renderListRows = () =>
sort(localFeatures).map(feature => ( sorted.map(feature => (
<ReportToggleListItem <ReportToggleListItem
key={feature.name} key={feature.name}
{...feature} {...feature}
@ -88,7 +85,7 @@ const ReportToggleList = ({ features, selectedProject }) => {
<ReportToggleListHeader <ReportToggleListHeader
handleCheckAll={handleCheckAll} handleCheckAll={handleCheckAll}
checkAll={checkAll} checkAll={checkAll}
setSortData={setSortData} setSort={setSort}
bulkActionsOn={BULK_ACTIONS_ON} bulkActionsOn={BULK_ACTIONS_ON}
/> />

View File

@ -1,97 +0,0 @@
.reportToggleList {
width: 100%;
margin: var(--card-margin-y) 0;
border-radius: 10px;
box-shadow: none;
}
.bulkAction {
background-color: #f2f2f2;
font-size: var(--p-size);
}
.sortIcon {
margin-left: 8px;
}
.reportToggleListHeader {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f1f1f1;
padding: 1rem var(--card-padding-x);
}
.reportToggleListInnerContainer {
padding: var(--card-padding);
}
.reportToggleListHeading {
font-size: var(--h1-size);
margin: 0;
font-weight: 'bold';
}
.reportingToggleTable {
width: 100%;
border-spacing: 0 0.8rem;
}
.reportingToggleTable th {
text-align: left;
}
.expired {
color: var(--danger);
}
.active {
color: var(--success);
}
.stale {
color: var(--danger);
}
.reportStatus {
display: flex;
align-items: center;
}
.reportIcon {
font-size: 1.5rem;
margin-right: 5px;
}
.tableRow {
cursor: pointer;
}
.tableRow:hover {
background-color: #eeeeee;
}
.checkbox {
margin: 0;
padding: 0;
}
@media only screen and (max-width: 800px) {
.hideColumn {
display: none;
}
th {
min-width: 120px;
}
}
@media only screen and (max-width: 550px) {
.hideColumnStatus {
display: none;
}
}
@media only screen and (max-width: 425px) {
.hideColumnLastSeen {
display: none;
}
}

View File

@ -0,0 +1,103 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
reportToggleList: {
width: '100%',
margin: 'var(--card-margin-y) 0',
borderRadius: 10,
boxShadow: 'none',
},
bulkAction: {
backgroundColor: '#f2f2f2',
fontSize: 'var(--p-size)',
},
sortIcon: {
marginLeft: 8,
},
reportToggleListHeader: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderBottom: '1px solid #f1f1f1',
padding: '1rem var(--card-padding-x)',
},
reportToggleListInnerContainer: {
padding: 'var(--card-padding)',
},
reportToggleListHeading: {
fontSize: 'var(--h1-size)',
margin: 0,
fontWeight: 'bold',
},
reportIcon: {
fontsize: '1.5rem',
marginRight: 5,
},
reportingToggleTable: {
width: ' 100%',
borderSpacing: '0 0.8rem',
'& th': {
textAlign: 'left',
cursor: 'pointer',
},
},
expired: {
color: 'var(--danger)',
},
active: {
color: 'var(--success)',
},
stale: {
color: 'var(--danger)',
},
reportStatus: {
display: 'flex',
alignItems: 'center',
},
tableRow: {
'&:hover': {
backgroundColor: '#eeeeee',
},
},
checkbox: {
margin: 0,
padding: 0,
},
link: {
color: theme.palette.primary.main,
textDecoration: 'none',
fontWeight: theme.fontWeight.bold,
},
[theme.breakpoints.down(800)]: {
hideColumn: {
display: 'none',
},
th: {
minWidth: '120px',
},
},
[theme.breakpoints.down(550)]: {
hideColumnStatus: {
display: 'none',
},
},
[theme.breakpoints.down(425)]: {
hideColumnLastSeen: {
display: 'none',
},
},
}));

View File

@ -5,27 +5,21 @@ import PropTypes from 'prop-types';
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
import { import { NAME, LAST_SEEN, CREATED, EXPIRED, STATUS } from '../../constants';
NAME,
LAST_SEEN,
CREATED,
EXPIRED,
STATUS,
REPORT,
} from '../../constants';
import styles from '../ReportToggleList.module.scss'; import { useStyles } from '../ReportToggleList.styles';
const ReportToggleListHeader = ({ const ReportToggleListHeader = ({
handleCheckAll, handleCheckAll,
checkAll, checkAll,
setSortData, setSort,
bulkActionsOn, bulkActionsOn,
}) => { }) => {
const styles = useStyles();
const handleSort = type => { const handleSort = type => {
setSortData(prev => ({ setSort(prev => ({
sortKey: type, type,
ascending: !prev.ascending, desc: !prev.desc,
})); }));
}; };
@ -94,7 +88,7 @@ const ReportToggleListHeader = ({
<th <th
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => handleSort(REPORT)} onClick={() => handleSort(EXPIRED)}
> >
Report Report
<UnfoldMoreOutlinedIcon className={styles.sortIcon} /> <UnfoldMoreOutlinedIcon className={styles.sortIcon} />
@ -106,7 +100,7 @@ const ReportToggleListHeader = ({
ReportToggleListHeader.propTypes = { ReportToggleListHeader.propTypes = {
checkAll: PropTypes.bool.isRequired, checkAll: PropTypes.bool.isRequired,
setSortData: PropTypes.func.isRequired, setSort: PropTypes.func.isRequired,
bulkActionsOn: PropTypes.bool.isRequired, bulkActionsOn: PropTypes.bool.isRequired,
handleCheckAll: PropTypes.func.isRequired, handleCheckAll: PropTypes.func.isRequired,
}; };

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Checkbox } from '@material-ui/core'; import { Checkbox } from '@material-ui/core';
import CheckIcon from '@material-ui/icons/Check'; import CheckIcon from '@material-ui/icons/Check';
@ -21,7 +21,7 @@ import {
PERMISSION, PERMISSION,
} from '../../../../constants/featureToggleTypes'; } from '../../../../constants/featureToggleTypes';
import styles from '../ReportToggleList.module.scss'; import { useStyles } from '../ReportToggleList.styles';
import { getTogglePath } from '../../../../utils/routePathHelpers'; import { getTogglePath } from '../../../../utils/routePathHelpers';
const ReportToggleListItem = ({ const ReportToggleListItem = ({
@ -35,8 +35,8 @@ const ReportToggleListItem = ({
bulkActionsOn, bulkActionsOn,
setFeatures, setFeatures,
}) => { }) => {
const styles = useStyles();
const nameMatches = feature => feature.name === name; const nameMatches = feature => feature.name === name;
const history = useHistory();
const handleChange = () => { const handleChange = () => {
setFeatures(prevState => { setFeatures(prevState => {
@ -116,21 +116,12 @@ const ReportToggleListItem = ({
); );
}; };
const navigateToFeature = () => {
history.push(getTogglePath(project, name));
};
const statusClasses = classnames(styles.active, styles.hideColumnStatus, { const statusClasses = classnames(styles.active, styles.hideColumnStatus, {
[styles.stale]: stale, [styles.stale]: stale,
}); });
return ( return (
<tr <tr className={styles.tableRow}>
role="button"
tabIndex={0}
onClick={navigateToFeature}
className={styles.tableRow}
>
<ConditionallyRender <ConditionallyRender
condition={bulkActionsOn} condition={bulkActionsOn}
show={ show={
@ -144,7 +135,11 @@ const ReportToggleListItem = ({
</td> </td>
} }
/> />
<td>{name}</td> <td>
<Link to={getTogglePath(project, name)} className={styles.link}>
{name}
</Link>
</td>
<td className={styles.hideColumnLastSeen}>{formatLastSeenAt()}</td> <td className={styles.hideColumnLastSeen}>{formatLastSeenAt()}</td>
<td className={styles.hideColumn}>{formatCreatedAt()}</td> <td className={styles.hideColumn}>{formatCreatedAt()}</td>
<td className={`${styles.expired} ${styles.hideColumn}`}> <td className={`${styles.expired} ${styles.hideColumn}`}>

View File

@ -1,6 +1,6 @@
/* SORT TYPES */ /* SORT TYPES */
export const NAME = 'name'; export const NAME = 'name';
export const LAST_SEEN = 'lastSeen'; export const LAST_SEEN = 'last-seen';
export const CREATED = 'created'; export const CREATED = 'created';
export const EXPIRED = 'expired'; export const EXPIRED = 'expired';
export const STATUS = 'status'; export const STATUS = 'status';

View File

@ -16,5 +16,6 @@ export const useStyles = makeStyles(theme => ({
'& > *': { '& > *': {
verticalAlign: 'middle', verticalAlign: 'middle',
}, },
color: theme.palette.primary.main,
}, },
})); }));

View File

@ -2,7 +2,6 @@ import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({ export const useStyles = makeStyles(theme => ({
tableRow: { tableRow: {
cursor: 'pointer',
'&:hover': { '&:hover': {
backgroundColor: theme.palette.grey[200], backgroundColor: theme.palette.grey[200],
}, },
@ -46,6 +45,7 @@ export const useStyles = makeStyles(theme => ({
}, },
link: { link: {
textDecoration: 'none', textDecoration: 'none',
color: 'inherit', color: theme.palette.primary.main,
fontWeight: theme.fontWeight.bold,
}, },
})); }));

View File

@ -9,15 +9,15 @@ import {
import classnames from 'classnames'; import classnames from 'classnames';
import { useStyles } from './FeatureToggleListNew.styles'; import { useStyles } from './FeatureToggleListNew.styles';
import FeatureToggleListNewItem from './FeatureToggleListNewItem/FeatureToggleListNewItem'; import FeatureToggleListNewItem from './FeatureToggleListNewItem/FeatureToggleListNewItem';
import usePagination from '../../../hooks/usePagination'; import usePagination from 'hooks/usePagination';
import loadingFeatures from './FeatureToggleListNewItem/loadingFeatures'; import loadingFeatures from './FeatureToggleListNewItem/loadingFeatures';
import { import {
IFeatureToggle, IFeatureToggle,
IFeatureToggleListItem, IFeatureToggleListItem,
} from '../../../interfaces/featureToggle'; } from 'interfaces/featureToggle';
import PaginateUI from '../../common/PaginateUI/PaginateUI'; import PaginateUI from 'component/common/PaginateUI/PaginateUI';
import StringTruncator from '../../common/StringTruncator/StringTruncator'; import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { createGlobalStateHook } from 'hooks/useGlobalState';
interface IFeatureToggleListNewProps { interface IFeatureToggleListNewProps {
features: IFeatureToggleListItem[]; features: IFeatureToggleListItem[];
loading: boolean; loading: boolean;
@ -66,17 +66,24 @@ const sortList = (list, sortOpt) => {
return list; return list;
}; };
interface ISortedState {
field: string;
type: string;
direction: number;
}
const useFeatureToggLeProjectSort = createGlobalStateHook<ISortedState>(
'useFeatureToggLeProjectSort',
{ field: 'name', type: 'string', direction: 0 }
);
const FeatureToggleListNew = ({ const FeatureToggleListNew = ({
features, features,
loading, loading,
projectId, projectId,
}: IFeatureToggleListNewProps) => { }: IFeatureToggleListNewProps) => {
const styles = useStyles(); const styles = useStyles();
const [sortOpt, setSortOpt] = useState({ const [sortOpt, setSortOpt] = useFeatureToggLeProjectSort();
field: 'name',
type: 'string',
direction: 0,
});
const [sortedFeatures, setSortedFeatures] = useState( const [sortedFeatures, setSortedFeatures] = useState(
sortList([...features], sortOpt) sortList([...features], sortOpt)
); );

View File

@ -1,12 +1,10 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { TableCell, TableRow } from '@material-ui/core'; import { TableCell, TableRow } from '@material-ui/core';
import { useHistory } from 'react-router';
import { useStyles } from '../FeatureToggleListNew.styles'; import { useStyles } from '../FeatureToggleListNew.styles';
import useToggleFeatureByEnv from '../../../../hooks/api/actions/useToggleFeatureByEnv/useToggleFeatureByEnv'; import useToggleFeatureByEnv from '../../../../hooks/api/actions/useToggleFeatureByEnv/useToggleFeatureByEnv';
import { IEnvironments } from '../../../../interfaces/featureToggle'; import { IEnvironments } from '../../../../interfaces/featureToggle';
import useToast from '../../../../hooks/useToast'; import useToast from '../../../../hooks/useToast';
import { getTogglePath } from 'utils/routePathHelpers'; import { getTogglePath } from 'utils/routePathHelpers';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import FeatureStatus from '../../FeatureView/FeatureStatus/FeatureStatus'; import FeatureStatus from '../../FeatureView/FeatureStatus/FeatureStatus';
import FeatureType from '../../FeatureView/FeatureType/FeatureType'; import FeatureType from '../../FeatureView/FeatureType/FeatureType';
import classNames from 'classnames'; import classNames from 'classnames';
@ -41,10 +39,8 @@ const FeatureToggleListNewItem = ({
name name
); );
const { uiConfig } = useUiConfig();
const { refetch } = useProject(projectId); const { refetch } = useProject(projectId);
const styles = useStyles(); const styles = useStyles();
const history = useHistory();
const ref = useRef<HTMLButtonElement>(null); const ref = useRef<HTMLButtonElement>(null);
const [showInfoBox, setShowInfoBox] = useState(false); const [showInfoBox, setShowInfoBox] = useState(false);
const [environmentName, setEnvironmentName] = useState(''); const [environmentName, setEnvironmentName] = useState('');
@ -53,12 +49,6 @@ const FeatureToggleListNewItem = ({
setShowInfoBox(false); setShowInfoBox(false);
}; };
const onClick = (e: React.MouseEvent) => {
if (!ref.current?.contains(e.target as Node)) {
history.push(getTogglePath(projectId, name));
}
};
const handleToggle = (env: IEnvironments) => { const handleToggle = (env: IEnvironments) => {
toggleFeatureByEnvironment(env.name, env.enabled) toggleFeatureByEnvironment(env.name, env.enabled)
.then(() => { .then(() => {
@ -87,7 +77,6 @@ const FeatureToggleListNewItem = ({
styles.tableCellStatus styles.tableCellStatus
)} )}
align="left" align="left"
onClick={onClick}
> >
<FeatureStatus <FeatureStatus
lastSeenAt={lastSeenAt} lastSeenAt={lastSeenAt}
@ -100,7 +89,6 @@ const FeatureToggleListNewItem = ({
styles.tableCellType styles.tableCellType
)} )}
align="center" align="center"
onClick={onClick}
> >
<FeatureType type={type} /> <FeatureType type={type} />
</TableCell> </TableCell>
@ -110,11 +98,9 @@ const FeatureToggleListNewItem = ({
styles.tableCellName styles.tableCellName
)} )}
align="left" align="left"
onClick={onClick}
> >
<Link <Link
// @ts-expect-error to={getTogglePath(projectId, name)}
to={getTogglePath(projectId, name, uiConfig.flags.E)}
className={styles.link} className={styles.link}
> >
<span data-loading>{name}</span> <span data-loading>{name}</span>
@ -126,7 +112,6 @@ const FeatureToggleListNewItem = ({
styles.tableCellCreated styles.tableCellCreated
)} )}
align="left" align="left"
onClick={onClick}
> >
<CreatedAt time={createdAt} /> <CreatedAt time={createdAt} />
</TableCell> </TableCell>

View File

@ -40,6 +40,7 @@ export const useStyles = makeStyles(theme => ({
}, },
link: { link: {
textDecoration: 'none', textDecoration: 'none',
color: theme.palette.primary.main,
}, },
actionsContainer: { actionsContainer: {
display: 'flex', display: 'flex',

View File

@ -2,18 +2,27 @@ import { IFeatureToggle } from '../interfaces/featureToggle';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { getBasePath } from 'utils/formatPath'; import { getBasePath } from 'utils/formatPath';
import { createPersistentGlobalStateHook } from './usePersistentGlobalState'; import { createPersistentGlobalStateHook } from './usePersistentGlobalState';
import { parseISO } from 'date-fns';
import {
expired,
getDiffInDays,
toggleExpiryByTypeMap,
} from 'component/Reporting/utils';
type FeaturesSortType = type FeaturesSortType =
| 'name' | 'name'
| 'expired'
| 'type' | 'type'
| 'enabled' | 'enabled'
| 'stale' | 'stale'
| 'created' | 'created'
| 'last-seen' | 'last-seen'
| 'status'
| 'project'; | 'project';
interface IFeaturesSort { interface IFeaturesSort {
type: FeaturesSortType; type: FeaturesSortType;
desc?: boolean;
} }
export interface IFeaturesSortOutput { export interface IFeaturesSortOutput {
@ -63,7 +72,7 @@ export const createFeaturesFilterSortOptions =
]; ];
}; };
const sortFeatures = ( const sortAscendingFeatures = (
features: IFeatureToggle[], features: IFeatureToggle[],
sort: IFeaturesSort sort: IFeaturesSort
): IFeatureToggle[] => { ): IFeatureToggle[] => {
@ -82,12 +91,29 @@ const sortFeatures = (
return sortByProject(features); return sortByProject(features);
case 'type': case 'type':
return sortByType(features); return sortByType(features);
case 'expired':
return sortByExpired(features);
case 'status':
return sortByStatus(features);
default: default:
console.error(`Unknown feature sort type: ${sort.type}`); console.error(`Unknown feature sort type: ${sort.type}`);
return features; return features;
} }
}; };
const sortFeatures = (
features: IFeatureToggle[],
sort: IFeaturesSort
): IFeatureToggle[] => {
const sorted = sortAscendingFeatures(features, sort);
if (sort.desc) {
return [...sorted].reverse();
}
return sorted;
};
const sortByEnabled = ( const sortByEnabled = (
features: Readonly<IFeatureToggle[]> features: Readonly<IFeatureToggle[]>
): IFeatureToggle[] => { ): IFeatureToggle[] => {
@ -137,3 +163,43 @@ const sortByProject = (
const sortByType = (features: Readonly<IFeatureToggle[]>): IFeatureToggle[] => { const sortByType = (features: Readonly<IFeatureToggle[]>): IFeatureToggle[] => {
return [...features].sort((a, b) => a.type.localeCompare(b.type)); return [...features].sort((a, b) => a.type.localeCompare(b.type));
}; };
const sortByExpired = (
features: Readonly<IFeatureToggle[]>
): IFeatureToggle[] => {
return [...features].sort((a, b) => {
const now = new Date();
const dateA = parseISO(a.createdAt);
const dateB = parseISO(b.createdAt);
const diffA = getDiffInDays(dateA, now);
const diffB = getDiffInDays(dateB, now);
if (!expired(diffA, a.type) && expired(diffB, b.type)) {
return 1;
}
if (expired(diffA, a.type) && !expired(diffB, b.type)) {
return -1;
}
const expiration = toggleExpiryByTypeMap as Record<string, number>;
const expiredByA = diffA - expiration[a.type];
const expiredByB = diffB - expiration[b.type];
return expiredByB - expiredByA;
});
};
const sortByStatus = (
features: Readonly<IFeatureToggle[]>
): IFeatureToggle[] => {
return [...features].sort((a, b) => {
if (a.stale) {
return 1;
} else if (b.stale) {
return -1;
} else {
return 0;
}
});
};