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

Merge branch 'master' into fix/search-field

This commit is contained in:
Fredrik Strand Oseberg 2021-11-24 14:43:01 +01:00 committed by GitHub
commit dd2b661928
20 changed files with 324 additions and 87 deletions

View File

@ -0,0 +1,13 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
indicator: {
padding: '0.2rem',
borderRadius: '5px',
marginLeft: '0.5rem',
backgroundColor: '#000',
color: '#fff',
fontSize: '0.9rem',
fontWeight: 'bold',
},
}));

View File

@ -0,0 +1,16 @@
import { useStyles } from './DisabledIndicator.styles';
import classnames from 'classnames';
interface IDisabledIndicator {
className?: string;
}
const DisabledIndicator = ({ className }: IDisabledIndicator) => {
const styles = useStyles();
return (
<span className={classnames(styles.indicator, className)}>
disabled
</span>
);
};
export default DisabledIndicator;

View File

@ -0,0 +1,26 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
container: {
display: 'flex',
alignItems: 'center',
position: 'relative',
width: '50px',
height: '100%',
},
vertical: {
borderRadius: '1px',
height: '50px',
width: '50px',
},
circle: {
width: '15px',
height: '15px',
},
pos: {
position: 'absolute',
right: 0,
left: 0,
margin: '0 auto',
},
}));

View File

@ -0,0 +1,24 @@
import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord';
import Remove from '@material-ui/icons/Remove';
import { useStyles } from './RolloutIcon.styles';
import classnames from 'classnames';
interface IRolloutIconProps {
className?: string;
}
const RolloutIcon = ({ className }: IRolloutIconProps) => {
const styles = useStyles();
return (
<div className={styles.container}>
<Remove
className={classnames(styles.vertical, styles.pos, className)}
/>
<FiberManualRecordIcon
className={classnames(styles.circle, styles.pos, className)}
/>
</div>
);
};
export default RolloutIcon;

View File

@ -23,6 +23,7 @@ import {
} from '../../../providers/AccessProvider/permissions';
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd';
import { XYCoord } from 'dnd-core';
import DisabledIndicator from '../../../common/DisabledIndicator/DisabledIndicator';
interface IEnvironmentListItemProps {
env: IEnvironment;
@ -118,7 +119,7 @@ const EnvironmentListItem = ({
if (updatePermission) {
drag(drop(ref));
}
return (
<ListItem
style={{ position: 'relative', opacity }}
@ -134,20 +135,7 @@ const EnvironmentListItem = ({
<strong>{env.name}</strong>
<ConditionallyRender
condition={!env.enabled}
show={
<span
style={{
padding: '0.2rem',
borderRadius: '5px',
marginLeft: '0.5rem',
backgroundColor: '#000',
color: '#fff',
fontWeight: 'bold',
}}
>
disabled
</span>
}
show={<DisabledIndicator />}
/>
</>
}

View File

@ -21,7 +21,7 @@ export const useStyles = makeStyles(theme => ({
cursor: 'pointer',
},
tableCellStatus: {
width: '50px',
width: '60px',
},
tableCellName: {
paddingLeft: '10px',
@ -44,9 +44,9 @@ export const useStyles = makeStyles(theme => ({
display: 'none',
},
},
link:{
link: {
textDecoration: 'none',
color: 'inherit'
color: 'inherit',
},
envName: {
display: 'inline-block',
@ -54,5 +54,5 @@ export const useStyles = makeStyles(theme => ({
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
}
},
}));

View File

@ -1,4 +1,4 @@
import { useState, useEffect} from 'react';
import { useState, useEffect } from 'react';
import {
Table,
TableBody,
@ -25,12 +25,12 @@ interface IFeatureToggleListNewProps {
//@ts-ignore
const sortList = (list, sortOpt) => {
if(!list) {
if (!list) {
return list;
}
if(!sortOpt.field) {
if (!sortOpt.field) {
return list;
}
}
if (sortOpt.type === 'string') {
//@ts-ignore
return list.sort((a, b) => {
@ -45,7 +45,7 @@ const sortList = (list, sortOpt) => {
return direction === 0 ? 1 : -1;
}
return 0;
})
});
}
if (sortOpt.type === 'date') {
//@ts-ignore
@ -60,10 +60,10 @@ const sortList = (list, sortOpt) => {
return sortOpt.direction === 0 ? -1 : 1;
}
return 0;
})
});
}
return list;
}
};
const FeatureToggleListNew = ({
features,
@ -76,32 +76,33 @@ const FeatureToggleListNew = ({
type: 'string',
direction: 0,
});
const [sortedFeatures, setSortedFeatures] = useState(sortList([...features], sortOpt));
const [sortedFeatures, setSortedFeatures] = useState(
sortList([...features], sortOpt)
);
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
usePagination(sortedFeatures, 50);
useEffect(() => {
setSortedFeatures(sortList([...features], sortOpt))
setSortedFeatures(sortList([...features], sortOpt));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [features])
}, [features]);
const updateSort = (field: string) => {
let newSortOpt;
if(field === sortOpt.field) {
newSortOpt = {...sortOpt, direction: (sortOpt.direction + 1) % 2};
}
else if(['createdAt', 'lastSeenAt'].includes(field)) {
if (field === sortOpt.field) {
newSortOpt = { ...sortOpt, direction: (sortOpt.direction + 1) % 2 };
} else if (['createdAt', 'lastSeenAt'].includes(field)) {
newSortOpt = {
field,
type: 'date',
direction: 0
direction: 0,
};
} else {
newSortOpt = {
field,
type: 'string',
direction: 0
direction: 0,
};
}
setSortOpt(newSortOpt);
@ -164,44 +165,64 @@ const FeatureToggleListNew = ({
styles.tableCell,
styles.tableCellStatus,
styles.tableCellHeader,
styles.tableCellHeaderSortable,
styles.tableCellHeaderSortable
)}
align="left"
>
<span data-loading onClick={() => updateSort('lastSeenAt')}>Status</span>
<span
data-loading
onClick={() => updateSort('lastSeenAt')}
>
Last use
</span>
</TableCell>
<TableCell
className={classnames(
styles.tableCell,
styles.tableCellType,
styles.tableCellHeader,
styles.tableCellHeaderSortable,
styles.tableCellHeaderSortable
)}
align="center"
>
<span data-loading onClick={() => updateSort('type')}>Type</span>
<span
data-loading
onClick={() => updateSort('type')}
>
Type
</span>
</TableCell>
<TableCell
className={classnames(
styles.tableCell,
styles.tableCellName,
styles.tableCellHeader,
styles.tableCellHeaderSortable,
styles.tableCellHeaderSortable
)}
align="left"
>
<span data-loading onClick={() => updateSort('name')}>Name</span>
<span
data-loading
onClick={() => updateSort('name')}
>
Name
</span>
</TableCell>
<TableCell
className={classnames(
styles.tableCell,
styles.tableCellCreated,
styles.tableCellHeader,
styles.tableCellHeaderSortable,
styles.tableCellHeaderSortable
)}
align="left"
>
<span data-loading onClick={() => updateSort('createdAt')}>Created</span>
<span
data-loading
onClick={() => updateSort('createdAt')}
>
Created
</span>
</TableCell>
{getEnvironments().map((env: any) => {
return (
@ -211,12 +232,15 @@ const FeatureToggleListNew = ({
styles.tableCell,
styles.tableCellEnv,
styles.tableCellHeader,
styles.tableCellHeaderSortable,
styles.tableCellHeaderSortable
)}
align="center"
>
<span data-loading className={styles.envName} >
{env.name}
<span
data-loading
className={styles.envName}
>
{env.name}
</span>
</TableCell>
);

View File

@ -23,6 +23,11 @@ export const useStyles = makeStyles(theme => ({
display: 'flex',
alignItems: 'center',
},
disabledIndicatorPos: {
position: 'absolute',
top: '15px',
left: '20px',
},
iconContainer: {
backgroundColor: theme.palette.primary.light,
borderRadius: '50%',
@ -118,6 +123,9 @@ export const useStyles = makeStyles(theme => ({
},
},
[theme.breakpoints.down(560)]: {
disabledIndicatorPos: {
top: '-8px',
},
headerTitle: {
flexDirection: 'column',
},

View File

@ -10,6 +10,8 @@ import useFeatureMetrics from '../../../../../../hooks/api/getters/useFeatureMet
import { IFeatureEnvironment } from '../../../../../../interfaces/featureToggle';
import { IFeatureViewParams } from '../../../../../../interfaces/params';
import { getFeatureMetrics } from '../../../../../../utils/get-feature-metrics';
import ConditionallyRender from '../../../../../common/ConditionallyRender';
import DisabledIndicator from '../../../../../common/DisabledIndicator/DisabledIndicator';
import EnvironmentIcon from '../../../../../common/EnvironmentIcon/EnvironmentIcon';
import StringTruncator from '../../../../../common/StringTruncator/StringTruncator';
@ -64,6 +66,14 @@ const FeatureOverviewEnvironment = ({
className={styles.truncator}
maxWidth="120"
/>
<ConditionallyRender
condition={!env.enabled}
show={
<DisabledIndicator
className={styles.disabledIndicatorPos}
/>
}
/>
</div>
<FeatureOverviewEnvironmentMetrics

View File

@ -15,7 +15,6 @@ export const useStyles = makeStyles(theme => ({
},
icon: {
fill: theme.palette.grey[600],
marginRight: '0.5rem',
},
editStrategy: {
marginLeft: 'auto',

View File

@ -40,9 +40,8 @@ export const useStyles = makeStyles(theme => ({
marginLeft: 'auto',
},
icon: {
marginRight: '0.5rem',
fill: theme.palette.primary.main,
minWidth: '35px',
minWidth: '50px',
},
rollout: {
fontSize: theme.fontSizes.smallBody,

View File

@ -65,6 +65,7 @@ const FeatureStrategyAccordion: React.FC<IFeatureStrategyAccordionProps> = ({
>
<div className={styles.accordionSummary}>
<Icon className={styles.icon} />
<p className={styles.accordionHeader}>{strategyName}</p>
<ConditionallyRender

View File

@ -14,7 +14,11 @@ export const useStyles = makeStyles(theme => ({
borderRadius: '10px',
marginBottom: '1rem',
},
innerContainer: { padding: '2rem' },
innerContainer: {
padding: '1rem 2rem',
display: 'flex',
alignItems: 'center',
},
separator: {
width: '100%',
backgroundColor: theme.palette.grey[200],

View File

@ -5,7 +5,7 @@ import useLoading from '../../../hooks/useLoading';
import ApiError from '../../common/ApiError/ApiError';
import ConditionallyRender from '../../common/ConditionallyRender';
import { useStyles } from './Project.styles';
import { IconButton, Tab, Tabs } from '@material-ui/core';
import { Tab, Tabs } from '@material-ui/core';
import { Edit } from '@material-ui/icons';
import useToast from '../../../hooks/useToast';
import useQueryParams from '../../../hooks/useQueryParams';
@ -17,9 +17,11 @@ import EditProject from '../edit-project-container';
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
import ProjectOverview from './ProjectOverview';
import ProjectHealth from './ProjectHealth/ProjectHealth';
import { UPDATE_PROJECT } from '../../../store/project/actions';
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
const Project = () => {
const { id, activeTab } = useParams<{ id: string, activeTab: string }>();
const { id, activeTab } = useParams<{ id: string; activeTab: string }>();
const params = useQueryParams();
const { project, error, loading, refetch } = useProject(id);
const ref = useLoading(loading);
@ -52,18 +54,24 @@ const Project = () => {
},
{
title: 'Environments',
component: <ProjectEnvironment projectId={id} />,
component: <ProjectEnvironment projectId={id} />,
path: `${basePath}/environments`,
name: 'environments',
},
{
title: 'Settings',
// @ts-ignore (fix later)
component: <EditProject projectId={id} history={history} title="Edit project" />,
component: (
<EditProject
projectId={id}
history={history}
title="Edit project"
/>
),
path: `${basePath}/settings`,
name: 'settings',
},
]
];
useEffect(() => {
const created = params.get('created');
@ -85,32 +93,29 @@ const Project = () => {
useEffect(() => {
const tabIdx = tabData.findIndex(tab => tab.name === activeTab);
if(tabIdx > 0) {
if (tabIdx > 0) {
setActiveTab(tabIdx);
} else {
setActiveTab(0);
}
/* eslint-disable-next-line */
}, []);
const goToTabWithName = (name: string) => {
const index = tabData.findIndex(t => t.name === name);
if(index >= 0) {
if (index >= 0) {
const tab = tabData[index];
history.push(tab.path);
setActiveTab(index);
}
}
};
const renderTabs = () => {
return tabData.map((tab, index) => {
return (
<Tab
data-loading
data-loading
key={tab.title}
label={tab.title}
{...a11yProps(index)}
@ -134,18 +139,26 @@ const Project = () => {
});
};
return (
<div ref={ref}>
<div className={styles.header}>
<div className={styles.innerContainer}>
<h2 data-loading className={commonStyles.title} style={{margin: 0}}>
<h2
data-loading
className={commonStyles.title}
style={{ margin: 0 }}
>
Project: {project?.name}{' '}
<IconButton onClick={() => goToTabWithName('settings')}>
<PermissionIconButton
permission={UPDATE_PROJECT}
tooltip={'Edit description'}
projectId={project?.id}
onClick={() => goToTabWithName('settings')}
data-loading
>
<Edit />
</IconButton>
</PermissionIconButton>
</h2>
<p data-loading>{project?.description}</p>
</div>
<ConditionallyRender
condition={error}

View File

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

View File

@ -39,7 +39,7 @@ const ProjectFeatureToggles = ({
headerContent={
<HeaderTitle
className={styles.title}
title="Feature toggles"
title={`Feature toggles (${features.length})`}
actions={
<>
<ConditionallyRender
@ -55,11 +55,14 @@ const ProjectFeatureToggles = ({
</IconButton>
}
/>
<ResponsiveButton
onClick={() =>
history.push(
getCreateTogglePath(id, uiConfig.flags.E)
getCreateTogglePath(
id,
uiConfig.flags.E
)
)
}
maxWidth="700px"
@ -93,7 +96,10 @@ const ProjectFeatureToggles = ({
condition={hasAccess(CREATE_FEATURE, id)}
show={
<Link
to={getCreateTogglePath(id, uiConfig.flags.E)}
to={getCreateTogglePath(
id,
uiConfig.flags.E
)}
className={styles.link}
data-loading
>

View File

@ -14,6 +14,18 @@ export const useStyles = makeStyles(theme => ({
marginBottom: '1rem',
},
},
description: {
textAlign: 'left',
marginBottom: '0.5rem',
},
descriptionContainer: {
display: 'flex',
justifyContent: 'space-between',
},
idContainer: {
display: 'flex',
width: '100%',
},
percentageContainer: {
display: 'flex',
justifyContent: 'center',
@ -66,6 +78,9 @@ export const useStyles = makeStyles(theme => ({
color: '#635dc5',
marginLeft: '0.5rem',
},
permissionButtonShortDesc: {
transform: `translateY(-10px)`,
},
infoLink: {
textDecoration: 'none',
color: '#635dc5',
@ -74,6 +89,15 @@ export const useStyles = makeStyles(theme => ({
bottom: '5px',
},
},
accordion: {
boxShadow: 'none',
textAlign: 'left',
},
accordionBody: { padding: '0' },
accordionActions: {
padding: '0',
justifyContent: 'flex-start',
},
linkText: {
[theme.breakpoints.down('sm')]: {
display: 'none',

View File

@ -2,23 +2,34 @@ import { useStyles } from './ProjectInfo.styles';
import { Link } from 'react-router-dom';
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
import classnames from 'classnames';
import { Edit, ExpandMore } from '@material-ui/icons';
import { useCommonStyles } from '../../../../common.styles';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import PercentageCircle from '../../../common/PercentageCircle/PercentageCircle';
import PermissionIconButton from '../../../common/PermissionIconButton/PermissionIconButton';
import { UPDATE_PROJECT } from '../../../../store/project/actions';
import ConditionallyRender from '../../../common/ConditionallyRender';
import {
Accordion,
AccordionActions,
AccordionDetails,
AccordionSummary,
} from '@material-ui/core';
interface IProjectInfoProps {
id: string;
memberCount: number;
featureCount: number;
health: number;
description: string;
}
const ProjectInfo = ({
id,
memberCount,
featureCount,
health,
description,
}: IProjectInfoProps) => {
const commonStyles = useCommonStyles();
const styles = useStyles();
@ -30,9 +41,87 @@ const ProjectInfo = ({
link = `/projects/${id}/access`;
}
const LONG_DESCRIPTION = 100;
const permissionButtonClass = classnames({
[styles.permissionButtonShortDesc]:
description.length < LONG_DESCRIPTION,
});
const permissionButton = (
<PermissionIconButton
permission={UPDATE_PROJECT}
tooltip={'Edit description'}
projectId={id}
component={Link}
className={permissionButtonClass}
data-loading
to={`/projects/${id}/settings`}
>
<Edit />
</PermissionIconButton>
);
return (
<aside>
<div className={styles.projectInfo}>
<div className={styles.infoSection}>
<div className={styles.descriptionContainer}>
<ConditionallyRender
condition={Boolean(description)}
show={
<ConditionallyRender
condition={
description.length < LONG_DESCRIPTION
}
show={
<p
data-loading
className={styles.description}
>
{description}
</p>
}
elseShow={
<Accordion className={styles.accordion}>
<AccordionSummary
expandIcon={<ExpandMore />}
className={styles.accordionBody}
>
Description
</AccordionSummary>
<AccordionDetails
className={styles.accordionBody}
>
{description}
</AccordionDetails>
<AccordionActions
className={
styles.accordionActions
}
>
Edit description{' '}
{permissionButton}
</AccordionActions>
</Accordion>
}
/>
}
elseShow={
<p data-loading className={styles.description}>
No description
</p>
}
/>
<ConditionallyRender
condition={description.length < LONG_DESCRIPTION}
show={permissionButton}
/>
</div>
<div className={styles.idContainer}>
<p data-loading>projectId: {id}</p>
</div>
</div>
<div className={styles.infoSection}>
<div data-loading className={styles.percentageContainer}>
<PercentageCircle percentage={health} />
@ -62,15 +151,6 @@ const ProjectInfo = ({
</Link>
</div>
<div className={styles.infoSection}>
<p className={styles.subtitle} data-loading>
Feature toggles
</p>
<p className={styles.emphazisedText} data-loading>
{featureCount}
</p>
</div>
<div
className={styles.infoSection}
style={{ marginBottom: '0' }}

View File

@ -11,7 +11,7 @@ const ProjectOverview = ({ projectId }: ProjectOverviewProps) => {
const { project, loading } = useProject(projectId, {
refreshInterval: 10000,
});
const { members, features, health } = project;
const { members, features, health, description } = project;
const styles = useStyles();
return (
@ -19,6 +19,7 @@ const ProjectOverview = ({ projectId }: ProjectOverviewProps) => {
<div className={styles.containerStyles}>
<ProjectInfo
id={projectId}
description={description}
memberCount={members}
health={health}
featureCount={features?.length}

View File

@ -2,7 +2,7 @@ import LocationOnIcon from '@material-ui/icons/LocationOn';
import PeopleIcon from '@material-ui/icons/People';
import LanguageIcon from '@material-ui/icons/Language';
import MapIcon from '@material-ui/icons/Map';
import { DonutLarge } from '@material-ui/icons';
import RolloutIcon from '../component/common/RolloutIcon/RolloutIcon';
const nameMapping = {
applicationHostname: {
@ -61,7 +61,7 @@ export const getFeatureStrategyIcon = strategyName => {
case 'remoteAddress':
return LanguageIcon;
case 'flexibleRollout':
return DonutLarge;
return RolloutIcon;
case 'userWithId':
return PeopleIcon;
case 'applicationHostname':