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

Merge branch 'main' into task/Add_strategy_information_to_playground_results

# Conflicts:
#	src/component/common/StrategySeparator/StrategySeparator.tsx
This commit is contained in:
andreas-unleash 2022-08-04 14:18:21 +03:00
commit 13a536904f
46 changed files with 386 additions and 234 deletions

View File

@ -49,7 +49,7 @@
"@testing-library/jest-dom": "5.16.4",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "14.3.0",
"@testing-library/user-event": "14.4.1",
"@types/debounce": "1.2.1",
"@types/deep-diff": "1.0.1",
"@types/jest": "28.1.6",
@ -66,7 +66,7 @@
"chart.js": "3.8.2",
"chartjs-adapter-date-fns": "2.0.0",
"classnames": "2.3.1",
"copy-to-clipboard": "3.3.1",
"copy-to-clipboard": "3.3.2",
"cypress": "9.7.0",
"date-fns": "2.29.1",
"debounce": "1.2.1",
@ -91,7 +91,7 @@
"react-table": "7.8.0",
"react-test-renderer": "17.0.2",
"react-timeago": "7.1.0",
"sass": "1.54.0",
"sass": "1.54.2",
"semver": "7.3.7",
"swr": "1.3.0",
"tss-react": "3.7.1",
@ -103,7 +103,7 @@
"vitest": "0.20.3",
"whatwg-fetch": "^3.6.2",
"@codemirror/lang-json": "6.0.0",
"@codemirror/state": "6.1.0",
"@codemirror/state": "6.1.1",
"@uiw/react-codemirror": "^4.11.4",
"codemirror": "^6.0.1"
},

View File

@ -255,7 +255,7 @@ export const Group: VFC = () => {
onClick={() => setRemoveOpen(true)}
permission={ADMIN}
tooltipProps={{
title: 'Remove group',
title: 'Delete group',
}}
>
<StyledDelete />

View File

@ -81,6 +81,7 @@ export const GroupForm: FC<IGroupForm> = ({
value={name}
onChange={e => setName(e.target.value)}
data-testid={UG_NAME_ID}
required
/>
<StyledInputDescription>
How would you describe your group?

View File

@ -2,25 +2,11 @@ import { capitalize, MenuItem, Select, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { Role } from 'interfaces/group';
import { Badge } from 'component/common/Badge/Badge';
import { StarRounded } from '@mui/icons-material';
const StyledBadge = styled('div')(({ theme }) => ({
padding: theme.spacing(0.5, 1),
textDecoration: 'none',
color: theme.palette.text.secondary,
border: `1px solid ${theme.palette.dividerAlternative}`,
background: theme.palette.activityIndicators.unknown,
display: 'inline-block',
borderRadius: theme.shape.borderRadius,
marginLeft: theme.spacing(1.5),
fontSize: theme.fontSizes.smallerBody,
fontWeight: theme.fontWeight.bold,
lineHeight: 1,
}));
const StyledOwnerBadge = styled(StyledBadge)(({ theme }) => ({
color: theme.palette.success.dark,
border: `1px solid ${theme.palette.success.border}`,
background: theme.palette.success.light,
const StyledPopupStar = styled(StarRounded)(({ theme }) => ({
color: theme.palette.warning.main,
}));
interface IGroupUserRoleCellProps {
@ -35,8 +21,12 @@ export const GroupUserRoleCell = ({
const renderBadge = () => (
<ConditionallyRender
condition={value === Role.Member}
show={<StyledBadge>{capitalize(value)}</StyledBadge>}
elseShow={<StyledOwnerBadge>{capitalize(value)}</StyledOwnerBadge>}
show={<Badge>{capitalize(value)}</Badge>}
elseShow={
<Badge color="success" icon={<StyledPopupStar />}>
{capitalize(value)}
</Badge>
}
/>
);

View File

@ -1,6 +1,6 @@
import { styled, Tooltip } from '@mui/material';
import { IGroup } from 'interfaces/group';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { GroupCardAvatars } from './GroupCardAvatars/GroupCardAvatars';
import { Badge } from 'component/common/Badge/Badge';
@ -20,9 +20,15 @@ const StyledGroupCard = styled('aside')(({ theme }) => ({
border: `1px solid ${theme.palette.dividerAlternative}`,
borderRadius: theme.shape.borderRadiusLarge,
boxShadow: theme.boxShadows.card,
display: 'flex',
flexDirection: 'column',
[theme.breakpoints.up('md')]: {
padding: theme.spacing(4),
},
'&:hover': {
transition: 'background-color 0.2s ease-in-out',
backgroundColor: theme.palette.neutral.light,
},
}));
const StyledRow = styled('div')(() => ({
@ -31,6 +37,14 @@ const StyledRow = styled('div')(() => ({
justifyContent: 'space-between',
}));
const StyledTitleRow = styled(StyledRow)(() => ({
alignItems: 'flex-start',
}));
const StyledBottomRow = styled(StyledRow)(() => ({
marginTop: 'auto',
}));
const StyledHeaderTitle = styled('h2')(({ theme }) => ({
fontSize: theme.fontSizes.mainHeader,
fontWeight: theme.fontWeight.medium,
@ -55,7 +69,13 @@ const StyledCounterDescription = styled('span')(({ theme }) => ({
marginLeft: theme.spacing(1),
}));
const ProjectBadgeContainer = styled('div')(() => ({}));
const ProjectBadgeContainer = styled('div')(() => ({
maxWidth: '50%',
}));
const StyledBadge = styled(Badge)(() => ({
marginRight: 0.5,
}));
interface IGroupCardProps {
group: IGroup;
@ -63,12 +83,12 @@ interface IGroupCardProps {
export const GroupCard = ({ group }: IGroupCardProps) => {
const [removeOpen, setRemoveOpen] = useState(false);
const navigate = useNavigate();
return (
<>
<StyledLink key={group.id} to={`/admin/groups/${group.id}`}>
<StyledGroupCard>
<StyledRow>
<StyledTitleRow>
<StyledHeaderTitle>{group.name}</StyledHeaderTitle>
<StyledHeaderActions>
<GroupCardActions
@ -76,9 +96,9 @@ export const GroupCard = ({ group }: IGroupCardProps) => {
onRemove={() => setRemoveOpen(true)}
/>
</StyledHeaderActions>
</StyledRow>
</StyledTitleRow>
<StyledDescription>{group.description}</StyledDescription>
<StyledRow>
<StyledBottomRow>
<ConditionallyRender
condition={group.users?.length > 0}
show={<GroupCardAvatars users={group.users} />}
@ -92,13 +112,26 @@ export const GroupCard = ({ group }: IGroupCardProps) => {
<ConditionallyRender
condition={group.projects.length > 0}
show={group.projects.map(project => (
<Badge
color="secondary"
icon={<TopicOutlinedIcon />}
sx={{ marginRight: 0.5 }}
<Tooltip
key={project}
title="View project"
arrow
placement="bottom-end"
describeChild
>
{project}
</Badge>
<StyledBadge
onClick={e => {
e.preventDefault();
navigate(
`/projects/${project}/access`
);
}}
color="secondary"
icon={<TopicOutlinedIcon />}
>
{project}
</StyledBadge>
</Tooltip>
))}
elseShow={
<Tooltip
@ -111,7 +144,7 @@ export const GroupCard = ({ group }: IGroupCardProps) => {
}
/>
</ProjectBadgeContainer>
</StyledRow>
</StyledBottomRow>
</StyledGroupCard>
</StyledLink>
<RemoveGroup

View File

@ -97,7 +97,7 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
</ListItemIcon>
<ListItemText>
<Typography variant="body2">
Remove group
Delete group
</Typography>
</ListItemText>
</MenuItem>

View File

@ -15,6 +15,9 @@ const StyledAvatars = styled('div')(({ theme }) => ({
const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
marginLeft: theme.spacing(-1),
'&:hover': {
outlineColor: theme.palette.primary.main,
},
}));
interface IGroupCardAvatarsProps {
@ -44,6 +47,7 @@ export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
<StyledAvatars>
{shownUsers.map(user => (
<StyledAvatar
key={user.id}
user={user}
star={user.role === Role.Owner}
onMouseEnter={event => {

View File

@ -1,21 +1,18 @@
import { Badge, Popover, styled } from '@mui/material';
import { Popover, styled } from '@mui/material';
import { IGroupUser, Role } from 'interfaces/group';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Badge as StyledBadge } from 'component/common/Badge/Badge';
import StarIcon from '@mui/icons-material/Star';
import { Badge } from 'component/common/Badge/Badge';
import { StarRounded } from '@mui/icons-material';
const StyledPopover = styled(Popover)(({ theme }) => ({
pointerEvents: 'none',
'.MuiPaper-root': {
padding: '12px',
padding: theme.spacing(2),
},
}));
const StyledPopupStar = styled(StarIcon)(({ theme }) => ({
const StyledPopupStar = styled(StarRounded)(({ theme }) => ({
color: theme.palette.warning.main,
fontSize: theme.fontSizes.smallBody,
marginLeft: theme.spacing(0.1),
marginTop: theme.spacing(2),
}));
const StyledName = styled('div')(({ theme }) => ({
@ -55,22 +52,10 @@ export const GroupPopover = ({
>
<ConditionallyRender
condition={user?.role === Role.Member}
show={<StyledBadge color="success">{user?.role}</StyledBadge>}
show={<Badge>{user?.role}</Badge>}
elseShow={
<Badge
overlap="circular"
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
badgeContent={<StyledPopupStar />}
>
<StyledBadge
color="success"
sx={{ paddingLeft: '16px' }}
>
{user?.role}
</StyledBadge>
<Badge color="success" icon={<StyledPopupStar />}>
{user?.role}
</Badge>
}
/>

View File

@ -0,0 +1,35 @@
import { Button, styled, Typography } from '@mui/material';
import { Link } from 'react-router-dom';
export const GroupEmpty = () => {
const StyledContainerDiv = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
margin: theme.spacing(6),
marginLeft: 'auto',
marginRight: 'auto',
}));
const StyledTitle = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.bodySize,
marginBottom: theme.spacing(2.5),
}));
return (
<StyledContainerDiv>
<StyledTitle>
No groups available. Get started by adding a new group.
</StyledTitle>
<Button
to="/admin/groups/create-group"
component={Link}
variant="outlined"
color="secondary"
>
Create your first group
</Button>
</StyledContainerDiv>
);
};

View File

@ -11,6 +11,7 @@ import theme from 'themes/theme';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { TablePlaceholder } from 'component/common/Table';
import { GroupCard } from './GroupCard/GroupCard';
import { GroupEmpty } from './GroupEmpty/GroupEmpty';
type PageQueryType = Partial<Record<'search', string>>;
@ -123,12 +124,7 @@ export const GroupsList: VFC = () => {
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
No groups available. Get started by adding a new
group.
</TablePlaceholder>
}
elseShow={<GroupEmpty />}
/>
}
/>

View File

@ -48,10 +48,10 @@ export const RemoveGroup: FC<IRemoveGroupProps> = ({
onClose={() => {
setOpen(false);
}}
title="Remove group"
title="Delete group"
>
<Typography>
Are you sure you wish to remove <strong>{group.name}</strong>?
Are you sure you wish to delete <strong>{group.name}</strong>?
If this group is currently assigned to one or more projects then
users belonging to this group may lose access to those projects.
</Typography>

View File

@ -1,5 +1,5 @@
import { styled, SxProps, Theme } from '@mui/material';
import {
import React, {
cloneElement,
FC,
ForwardedRef,
@ -17,6 +17,8 @@ interface IBadgeProps {
className?: string;
sx?: SxProps<Theme>;
children?: ReactNode;
title?: string;
onClick?: (event: React.SyntheticEvent) => void;
}
interface IBadgeIconProps {

View File

@ -8,8 +8,7 @@ export const useStyles = makeStyles()(theme => ({
alignItems: 'center',
justifyContent: 'center',
marginRight: theme.spacing(1),
[theme.breakpoints.down(650)]: {
marginBottom: '1rem',
[theme.breakpoints.down(710)]: {
marginRight: 0,
},
},
@ -17,8 +16,8 @@ export const useStyles = makeStyles()(theme => ({
fill: '#fff',
},
accordion: {
border: `1px solid ${theme.palette.grey[400]}`,
borderRadius: '8px',
border: `1px solid ${theme.palette.dividerAlternative}`,
borderRadius: theme.shape.borderRadiusMedium,
backgroundColor: '#fff',
boxShadow: 'none',
margin: 0,
@ -27,6 +26,9 @@ export const useStyles = makeStyles()(theme => ({
'&:before': {
opacity: '0 !important',
},
'&:first-of-type, &:last-of-type': {
borderRadius: theme.shape.borderRadiusMedium,
},
},
accordionEdit: {
backgroundColor: '#F6F6FA',
@ -34,7 +36,10 @@ export const useStyles = makeStyles()(theme => ({
headerMetaInfo: {
display: 'flex',
alignItems: 'stretch',
[theme.breakpoints.down(710)]: { flexDirection: 'column' },
[theme.breakpoints.down(710)]: {
flexDirection: 'column',
alignItems: 'center',
},
},
headerContainer: {
display: 'flex',
@ -76,6 +81,9 @@ export const useStyles = makeStyles()(theme => ({
minWidth: '152px',
paddingRight: '0.5rem',
},
[theme.breakpoints.down(710)]: {
paddingRight: 0,
},
},
editingBadge: {
borderRadius: theme.shape.borderRadiusExtraLarge,

View File

@ -1,6 +1,7 @@
import { Paper, styled } from '@mui/material';
import { usePageTitle } from 'hooks/usePageTitle';
import { ReactNode } from 'react';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
const StyledMainHeader = styled(Paper)(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
@ -49,7 +50,15 @@ export const MainHeader = ({
<StyledTitle>{title}</StyledTitle>
<StyledActions>{actions}</StyledActions>
</StyledTitleHeader>
Description:<StyledDescription>{description}</StyledDescription>
<ConditionallyRender
condition={Boolean(description?.length)}
show={
<>
Description:
<StyledDescription>{description}</StyledDescription>
</>
}
/>
</StyledMainHeader>
);
};

View File

@ -1,4 +1,4 @@
import { styled, SxProps, Theme } from '@mui/material';
import { Box, styled, useTheme, SxProps, Theme } from '@mui/material';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
interface IStrategySeparatorProps {
@ -6,14 +6,8 @@ interface IStrategySeparatorProps {
sx?: SxProps<Theme>;
}
const StyledContainer = styled('div')(({ theme }) => ({
height: theme.spacing(1),
position: 'relative',
width: '100%',
}));
const StyledContent = styled('div')(({ theme }) => ({
padding: theme.spacing(0.75, 1.5),
padding: theme.spacing(0.75, 1),
color: theme.palette.text.primary,
fontSize: theme.fontSizes.smallerBody,
backgroundColor: theme.palette.secondaryContainer,
@ -21,26 +15,39 @@ const StyledContent = styled('div')(({ theme }) => ({
position: 'absolute',
zIndex: theme.zIndex.fab,
top: '50%',
left: theme.spacing(3),
left: theme.spacing(2),
transform: 'translateY(-50%)',
lineHeight: 1,
}));
const StyledCenteredContent = styled(StyledContent)(({ theme }) => ({
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: theme.palette.secondary.light,
backgroundColor: theme.palette.activityIndicators.primary,
border: `1px solid ${theme.palette.primary.border}`,
borderRadius: theme.shape.borderRadiusLarge,
}));
export const StrategySeparator = ({ text, sx }: IStrategySeparatorProps) => (
<StyledContainer sx={sx}>
<ConditionallyRender
condition={text === 'AND'}
show={() => <StyledContent>{text}</StyledContent>}
elseShow={() => (
<StyledCenteredContent>{text}</StyledCenteredContent>
)}
/>
</StyledContainer>
);
export const StrategySeparator = ({ text, sx }: IStrategySeparatorProps) => {
const theme = useTheme();
return (
<Box
sx={{
height: theme.spacing(text === 'AND' ? 1 : 1.5),
position: 'relative',
width: '100%',
..sx
}}
>
<ConditionallyRender
condition={text === 'AND'}
show={() => <StyledContent>{text}</StyledContent>}
elseShow={() => (
<StyledCenteredContent>{text}</StyledCenteredContent>
)}
/>
</Box>
);
};

View File

@ -9,11 +9,11 @@ import {
import { IUser } from 'interfaces/user';
import { FC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import StarIcon from '@mui/icons-material/Star';
import { StarRounded } from '@mui/icons-material';
const StyledAvatar = styled(Avatar)(({ theme }) => ({
width: theme.spacing(4),
height: theme.spacing(4),
width: theme.spacing(3.5),
height: theme.spacing(3.5),
margin: 'auto',
backgroundColor: theme.palette.secondary.light,
color: theme.palette.text.primary,
@ -21,7 +21,7 @@ const StyledAvatar = styled(Avatar)(({ theme }) => ({
fontWeight: theme.fontWeight.bold,
}));
const StyledStar = styled(StarIcon)(({ theme }) => ({
const StyledStar = styled(StarRounded)(({ theme }) => ({
color: theme.palette.warning.main,
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadiusExtraLarge,

View File

@ -6,6 +6,7 @@ export const useStyles = makeStyles()(theme => ({
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
paddingTop: theme.spacing(2),
},
title: {
fontSize: theme.fontSizes.bodySize,

View File

@ -161,7 +161,7 @@ export const FeatureStrategyEmpty = ({
display: 'grid',
width: '100%',
gap: 2,
gridTemplateColumns: '1fr 1fr',
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' },
}}
>
<PresetCard

View File

@ -40,7 +40,14 @@ export const PresetCard: FC<IPresetCardProps> = ({
{children}
</Typography>
<Box sx={{ ml: 'auto', mt: 'auto', pt: 1 }}>
<Box
sx={{
ml: 'auto',
mt: 'auto',
pt: 1,
mr: { xs: 'auto', sm: 0 },
}}
>
<PermissionButton
permission={CREATE_FEATURE_STRATEGY}
projectId={projectId}

View File

@ -1,11 +1,6 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
title: {
margin: 0,
fontSize: theme.fontSizes.bodySize,
fontWeight: theme.fontWeight.bold,
},
divider: {
border: `1px dashed ${theme.palette.divider}`,
},

View File

@ -9,7 +9,7 @@ import { FeatureStrategySegmentList } from 'component/feature/FeatureStrategy/Fe
import { useStyles } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.styles';
import { SegmentDocsStrategyWarning } from 'component/segments/SegmentDocs/SegmentDocs';
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
import { Divider } from '@mui/material';
import { Divider, Typography } from '@mui/material';
interface IFeatureStrategySegmentProps {
segments: ISegment[];
@ -53,7 +53,9 @@ export const FeatureStrategySegment = ({
return (
<>
<h3 className={styles.title}>Segmentation</h3>
<Typography component="h3" sx={{ m: 0 }} variant="h3">
Segmentation
</Typography>
{atStrategySegmentsLimit && <SegmentDocsStrategyWarning />}
<p>Add a predefined segment to constrain this feature toggle:</p>
<AutocompleteBox

View File

@ -16,11 +16,13 @@ export const useStyles = makeStyles()(theme => ({
fontSize: theme.fontSizes.smallerBody,
border: '1px solid',
borderColor: theme.palette.grey[300],
paddingInline: '0.4rem',
marginBlock: '0.2rem',
display: 'grid',
padding: theme.spacing(0.75, 1),
display: 'block',
marginTop: 'auto',
marginBottom: 'auto',
alignItems: 'center',
borderRadius: theme.shape.borderRadius,
lineHeight: 1,
},
selectedSegmentsLabel: {
color: theme.palette.text.secondary,

View File

@ -15,7 +15,7 @@ const FeatureLog = () => {
return (
<div className={styles.container}>
<FeatureEventHistory toggleName={feature.name} />
<FeatureEventHistory featureId={feature.name} />
</div>
);
};

View File

@ -1,3 +1,4 @@
import { Box, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { MoveListItem, useDragItem } from 'hooks/useDragItem';
@ -13,6 +14,18 @@ interface IStrategyDraggableItemProps {
onDragAndDrop: MoveListItem;
}
const StyledIndexLabel = styled('div')(({ theme }) => ({
fontSize: theme.typography.fontSize,
color: theme.palette.text.secondary,
position: 'absolute',
display: 'none',
right: 'calc(100% + 6px)',
top: theme.spacing(2.5),
[theme.breakpoints.up('md')]: {
display: 'block',
},
}));
export const StrategyDraggableItem = ({
strategy,
index,
@ -23,17 +36,20 @@ export const StrategyDraggableItem = ({
const ref = useDragItem(index, onDragAndDrop);
return (
<div key={strategy.id} ref={ref}>
<Box key={strategy.id} ref={ref}>
<ConditionallyRender
condition={index > 0}
show={<StrategySeparator text="OR" />}
/>
<StrategyItem
strategy={strategy}
environmentId={environmentName}
otherEnvironments={otherEnvironments}
isDraggable
/>
</div>
<Box sx={{ position: 'relative' }}>
<StyledIndexLabel>{index + 1}</StyledIndexLabel>
<StrategyItem
strategy={strategy}
environmentId={environmentName}
otherEnvironments={otherEnvironments}
isDraggable
/>
</Box>
</Box>
);
};

View File

@ -4,7 +4,7 @@ export const useStyles = makeStyles()(theme => ({
container: {
width: '100%',
padding: theme.spacing(2, 3),
borderRadius: theme.shape.borderRadius,
borderRadius: theme.shape.borderRadiusMedium,
border: `1px solid ${theme.palette.divider}`,
},
chip: {

View File

@ -4,7 +4,7 @@ export const useStyles = makeStyles()(theme => ({
valueContainer: {
padding: theme.spacing(2, 3),
border: `1px solid ${theme.palette.dividerAlternative}`,
borderRadius: theme.shape.borderRadius,
borderRadius: theme.shape.borderRadiusMedium,
},
valueSeparator: {
color: theme.palette.grey[700],

View File

@ -17,6 +17,9 @@ export const useStyles = makeStyles()(theme => ({
borderBottom: `1px solid ${theme.palette.divider}`,
fontWeight: theme.typography.fontWeightMedium,
},
headerDraggable: {
paddingLeft: theme.spacing(1),
},
icon: {
fill: theme.palette.inactiveIcon,
},

View File

@ -1,6 +1,7 @@
import { DragIndicator, Edit } from '@mui/icons-material';
import { styled, useTheme, IconButton } from '@mui/material';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { IFeatureEnvironment } from 'interfaces/featureToggle';
import { IFeatureStrategy } from 'interfaces/strategy';
import {
@ -52,7 +53,11 @@ export const StrategyItem = ({
return (
<div className={styles.container}>
<div className={styles.header}>
<div
className={classNames(styles.header, {
[styles.headerDraggable]: isDraggable,
})}
>
<ConditionallyRender
condition={Boolean(isDraggable)}
show={() => (
@ -60,6 +65,7 @@ export const StrategyItem = ({
<DragIndicator
titleAccess="Drag to reorder"
cursor="grab"
sx={{ color: 'neutral.main' }}
/>
</DragIcon>
)}

View File

@ -28,9 +28,13 @@ export const useStyles = makeStyles()(theme => ({
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
borderBottomRightRadius: theme.shape.borderRadiusLarge,
borderBottom: `4px solid ${theme.palette.primary.light}`,
[theme.breakpoints.down('md')]: {
padding: theme.spacing(2, 1),
},
},
accordionDetailsDisabled: {
borderBottom: `4px solid ${theme.palette.dividerAlternative}`,
borderBottom: `4px solid ${theme.palette.neutral.border}`,
},
accordionBody: {
width: '100%',

View File

@ -85,18 +85,18 @@ const FeatureOverviewEnvironment = ({
maxWidth="100"
maxLength={15}
/>
<ConditionallyRender
condition={!env.enabled}
show={
<Chip
size="small"
variant="outlined"
label="Disabled"
sx={{ ml: 1 }}
/>
}
/>
</div>
<ConditionallyRender
condition={!env.enabled}
show={
<Chip
size="small"
variant="outlined"
label="Disabled"
sx={{ ml: 1 }}
/>
}
/>
</div>
<div className={styles.container}>
<FeatureStrategyMenu

View File

@ -14,7 +14,7 @@ const SeparatorContainer = styled('div')(({ theme }) => ({
transform: 'translateY(-50%)',
height: 2,
width: '100%',
backgroundColor: theme.palette.divider,
backgroundColor: theme.palette.dividerAlternative,
},
}));
@ -25,7 +25,7 @@ const SeparatorContent = styled('span')(({ theme }) => ({
background: theme.palette.secondaryContainer,
position: 'relative',
maxWidth: '80%',
color: theme.palette.text.secondary,
color: theme.palette.text.primary,
}));
export const SectionSeparator: FC = ({ children }) => (

View File

@ -3,6 +3,8 @@ import { Link } from 'react-router-dom';
import { DonutLarge } from '@mui/icons-material';
import { useStyles } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment.styles';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
interface IFeatureOverviewSegmentProps {
strategyId: string;
@ -20,8 +22,12 @@ export const FeatureOverviewSegment = ({
return (
<>
{segments.map(segment => (
{segments.map((segment, index) => (
<Fragment key={segment.id}>
<ConditionallyRender
condition={index > 0}
show={<StrategySeparator text="AND" />}
/>
<div className={styles.container}>
<DonutLarge color="secondary" sx={{ mr: 1 }} /> Segment:{' '}
<Link

View File

@ -81,9 +81,9 @@ const RolloutSlider = ({
<div className={classes.slider}>
<Typography
id="discrete-slider-always"
variant="subtitle2"
variant="h3"
gutterBottom
component="h2"
component="h3"
>
{name}
</Typography>

View File

@ -8,5 +8,5 @@ export const EventHistory = () => {
return null;
}
return <EventLog history={events} title="Event log" />;
return <EventLog events={events} title="Event log" />;
};

View File

@ -1,9 +1,14 @@
import EventDiff from './EventDiff/EventDiff';
import EventDiff from 'component/history/EventLog/EventCard/EventDiff/EventDiff';
import { useStyles } from './EventCard.styles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IEvent } from 'interfaces/event';
const EventCard = ({ entry, timeFormatted }) => {
interface IEventCardProps {
entry: IEvent;
timeFormatted: string;
}
const EventCard = ({ entry, timeFormatted }: IEventCardProps) => {
const { classes: styles } = useStyles();
return (
@ -18,7 +23,7 @@ const EventCard = ({ entry, timeFormatted }) => {
<dt className={styles.eventLogHeader}>Changed by: </dt>
<dd title={entry.createdBy}>{entry.createdBy}</dd>
<ConditionallyRender
condition={entry.project}
condition={Boolean(entry.project)}
show={
<>
<dt className={styles.eventLogHeader}>Project: </dt>
@ -27,7 +32,7 @@ const EventCard = ({ entry, timeFormatted }) => {
}
/>
<ConditionallyRender
condition={entry.featureName}
condition={Boolean(entry.featureName)}
show={
<>
<dt className={styles.eventLogHeader}>Feature: </dt>

View File

@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import { diff } from 'deep-diff';
import { useStyles } from './EventDiff.styles';
import { IEvent } from 'interfaces/event';
const DIFF_PREFIXES = {
A: ' ',
@ -10,7 +9,11 @@ const DIFF_PREFIXES = {
N: '+',
};
const EventDiff = ({ entry }) => {
interface IEventDiffProps {
entry: IEvent;
}
const EventDiff = ({ entry }: IEventDiffProps) => {
const { classes: styles } = useStyles();
const KLASSES = {
@ -25,7 +28,7 @@ const EventDiff = ({ entry }) => {
? diff(entry.preData, entry.data)
: undefined;
const buildItemDiff = (diff, key) => {
const buildItemDiff = (diff: any, key: string) => {
let change;
if (diff.lhs !== undefined) {
change = (
@ -48,7 +51,7 @@ const EventDiff = ({ entry }) => {
return change;
};
const buildDiff = (diff, idx) => {
const buildDiff = (diff: any, idx: number) => {
let change;
const key = diff.path.join('.');
@ -66,7 +69,9 @@ const EventDiff = ({ entry }) => {
</div>
);
} else {
// @ts-expect-error
const spadenClass = KLASSES[diff.kind];
// @ts-expect-error
const prefix = DIFF_PREFIXES[diff.kind];
change = (
@ -95,15 +100,10 @@ const EventDiff = ({ entry }) => {
return (
<pre style={{ overflowX: 'auto', overflowY: 'hidden' }} tabIndex={0}>
<code className="smalltext man">
{changes.length === 0 ? '(no changes)' : changes}
</code>
{/* @ts-expect-error */}
<code>{changes.length === 0 ? '(no changes)' : changes}</code>
</pre>
);
};
EventDiff.propTypes = {
entry: PropTypes.object,
};
export default EventDiff;

View File

@ -1,8 +1,12 @@
import PropTypes from 'prop-types';
import { useStyles } from './EventJson.styles';
import { IEvent } from 'interfaces/event';
const EventJson = ({ entry }) => {
interface IEventJsonProps {
entry: IEvent;
}
const EventJson = ({ entry }: IEventJsonProps) => {
const { classes: styles } = useStyles();
const localEventData = JSON.parse(JSON.stringify(entry));
@ -15,7 +19,7 @@ const EventJson = ({ entry }) => {
return (
<li className={styles.historyItem}>
<div>
<code className="JSON smalltext man">{prettyPrinted}</code>
<code>{prettyPrinted}</code>
</div>
</li>
);

View File

@ -1,35 +1,47 @@
import { List, Switch, FormControlLabel } from '@mui/material';
import PropTypes from 'prop-types';
import EventJson from './EventJson/EventJson';
import EventJson from 'component/history/EventLog/EventJson/EventJson';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import EventCard from './EventCard/EventCard';
import EventCard from 'component/history/EventLog/EventCard/EventCard';
import { useStyles } from './EventLog.styles';
import { formatDateYMDHMS } from 'utils/formatDate';
import { IEventSettings } from 'hooks/useEventSettings';
import { IEvent } from 'interfaces/event';
import React from 'react';
import { ILocationSettings } from 'hooks/useLocationSettings';
interface IEventLogProps {
title: string;
events: IEvent[];
eventSettings: IEventSettings;
setEventSettings: React.Dispatch<React.SetStateAction<IEventSettings>>;
locationSettings: ILocationSettings;
displayInline?: boolean;
}
const EventLog = ({
title,
history,
events,
eventSettings,
setEventSettings,
locationSettings,
displayInline,
}) => {
}: IEventLogProps) => {
const { classes: styles } = useStyles();
const toggleShowDiff = () => {
setEventSettings({ showData: !eventSettings.showData });
};
const formatFulldateTime = v => {
const formatFulldateTime = (v: string) => {
return formatDateYMDHMS(v, locationSettings.locale);
};
if (!history || history.length < 0) {
if (!events || events.length < 0) {
return null;
}
let entries;
const renderListItemCards = entry => (
const renderListItemCards = (entry: IEvent) => (
<li key={entry.id} className={styles.eventEntry}>
<EventCard
entry={entry}
@ -39,11 +51,11 @@ const EventLog = ({
);
if (eventSettings.showData) {
entries = history.map(entry => (
entries = events.map(entry => (
<EventJson key={`log${entry.id}`} entry={entry} />
));
} else {
entries = history.map(renderListItemCards);
entries = events.map(renderListItemCards);
}
return (
@ -75,13 +87,4 @@ const EventLog = ({
);
};
EventLog.propTypes = {
history: PropTypes.array,
eventSettings: PropTypes.object.isRequired,
setEventSettings: PropTypes.func.isRequired,
locationSettings: PropTypes.object.isRequired,
title: PropTypes.string,
displayInline: PropTypes.bool,
};
export default EventLog;

View File

@ -1,10 +1,11 @@
import EventLog from './EventLog';
import EventLog from 'component/history/EventLog/EventLog';
import { useEventSettings } from 'hooks/useEventSettings';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { IEvent } from 'interfaces/event';
interface IEventLogContainerProps {
title: string;
history: unknown[];
events: IEvent[];
displayInline?: boolean;
}
@ -15,7 +16,7 @@ const EventLogContainer = (props: IEventLogContainerProps) => {
return (
<EventLog
title={props.title}
history={props.history}
events={props.events}
eventSettings={eventSettings}
setEventSettings={setEventSettings}
locationSettings={locationSettings}

View File

@ -1,19 +0,0 @@
import PropTypes from 'prop-types';
import EventLog from '../EventLog';
import { useFeatureEvents } from 'hooks/api/getters/useFeatureEvents/useFeatureEvents';
export const FeatureEventHistory = ({ toggleName }) => {
const { events } = useFeatureEvents(toggleName);
if (events.length === 0) {
return null;
}
return (
<EventLog history={events} hideName title="Event log" displayInline />
);
};
FeatureEventHistory.propTypes = {
toggleName: PropTypes.string.isRequired,
};

View File

@ -0,0 +1,18 @@
import EventLog from '../EventLog';
import { useFeatureEvents } from 'hooks/api/getters/useFeatureEvents/useFeatureEvents';
interface IFeatureEventHistoryProps {
featureId: string;
}
export const FeatureEventHistory = ({
featureId,
}: IFeatureEventHistoryProps) => {
const { events } = useFeatureEvents(featureId);
if (events.length === 0) {
return null;
}
return <EventLog events={events} title="Event log" displayInline />;
};

View File

@ -1,9 +1,9 @@
import React from 'react';
import { FeatureEventHistory } from '../FeatureEventHistory/FeatureEventHistory';
import { FeatureEventHistory } from 'component/history/FeatureEventHistory/FeatureEventHistory';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
export const FeatureEventHistoryPage = () => {
const toggleName = useRequiredPathParam('toggleName');
return <FeatureEventHistory toggleName={toggleName} />;
return <FeatureEventHistory featureId={toggleName} />;
};

View File

@ -44,27 +44,44 @@ export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> = ({
const environments = useMemo(() => {
const environments = new Set<string>();
role.permissions
?.filter((permission: any) => permission.environment !== '')
?.filter((permission: any) => permission.environment)
.forEach((permission: any) => {
environments.add(permission.environment);
});
return [...environments].sort();
}, [role]);
const projectPermissions = useMemo(() => {
return role.permissions?.filter(
(permission: any) => !permission.environment
);
}, [role]);
return (
<StyledDescription>
<StyledDescriptionHeader>
Project permissions
</StyledDescriptionHeader>
<StyledDescriptionBlock>
{role.permissions
?.filter((permission: any) => permission.environment === '')
.map((permission: any) => permission.displayName)
.sort()
.map((permission: any) => (
<p key={permission}>{permission}</p>
))}
</StyledDescriptionBlock>
<ConditionallyRender
condition={Boolean(projectPermissions?.length)}
show={
<>
<StyledDescriptionHeader>
Project permissions
</StyledDescriptionHeader>
<StyledDescriptionBlock>
{role.permissions
?.filter(
(permission: any) => !permission.environment
)
.map(
(permission: any) => permission.displayName
)
.sort()
.map((permission: any) => (
<p key={permission}>{permission}</p>
))}
</StyledDescriptionBlock>
</>
}
/>
<ConditionallyRender
condition={Boolean(environments.length)}
show={

View File

@ -26,6 +26,10 @@ export default createTheme({
fontSize: '1.5rem',
lineHeight: 1.875,
},
h3: {
fontSize: '1rem',
fontWeight: '700',
},
caption: {
fontSize: `${12 / 16}rem`,
},
@ -128,6 +132,7 @@ export default createTheme({
recent: colors.green[100],
inactive: colors.orange[200],
abandoned: colors.red[200],
primary: colors.purple[100],
},
inactiveIcon: colors.grey[600],
},

View File

@ -46,13 +46,14 @@ declare module '@mui/material/styles' {
background: string;
};
/**
* For 'Seen' column on feature toggles list.
* For 'Seen' column on feature toggles list and other.
*/
activityIndicators: {
unknown: string;
recent: string;
inactive: string;
abandoned: string;
primary: string;
};
dividerAlternative: string;
/**

View File

@ -1318,7 +1318,12 @@
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
"@codemirror/state@6.1.0", "@codemirror/state@^6.0.0":
"@codemirror/state@6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.1.1.tgz#4f512e5e34ea23a5e10b2c1fe43f6195e90417bb"
integrity sha512-2s+aXsxmAwnR3Rd+JDHPG/1lw0YsA9PEwl7Re88gHJHGfxyfEzKBmsN4rr53RyPIR4lzbbhJX0DCq0WlqlBIRw==
"@codemirror/state@^6.0.0":
version "6.1.0"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.1.0.tgz#c0f1d80f61908c9dcf5e2a3fe931e9dd78f3df8a"
integrity sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg==
@ -1993,10 +1998,10 @@
"@testing-library/dom" "^8.0.0"
"@types/react-dom" "<18.0.0"
"@testing-library/user-event@14.3.0":
version "14.3.0"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.3.0.tgz#0a6750b94b40e4739706d41e8efc2ccf64d2aad9"
integrity sha512-P02xtBBa8yMaLhK8CzJCIns8rqwnF6FxhR9zs810flHOBXUYCFjLd8Io1rQrAkQRWEmW2PGdZIEdMxf/KLsqFA==
"@testing-library/user-event@14.4.1":
version "14.4.1"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.1.tgz#dfa1cceef4833f5288a4090d1b85dce5d8dc20b6"
integrity sha512-Gr20dje1RaNxZ1ehHGPvFkLswfetBQKCfRD/lo6sUJQ52X2TV/QnqUpkjoShfEebrB2KiTPfQkcONwdQiofLhg==
"@tootallnate/once@2":
version "2.0.0"
@ -3139,10 +3144,10 @@ cookie@^0.4.2:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
copy-to-clipboard@3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
copy-to-clipboard@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.2.tgz#5b263ec2366224b100181dded7ce0579b340c107"
integrity sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==
dependencies:
toggle-selection "^1.0.6"
@ -6092,10 +6097,10 @@ safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass@1.54.0:
version "1.54.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.0.tgz#24873673265e2a4fe3d3a997f714971db2fba1f4"
integrity sha512-C4zp79GCXZfK0yoHZg+GxF818/aclhp9F48XBu/+bm9vXEVAYov9iU3FBVRMq3Hx3OA4jfKL+p2K9180mEh0xQ==
sass@1.54.2:
version "1.54.2"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.2.tgz#574cad83814c930ef2475921b9cb5d8203ae8867"
integrity sha512-wbVV26sejsCIbBScZZtNkvnrB/bVCQ8hSlZ01D9nzsVh9zLqCkWrlpvTb3YEb6xsuNi9cx75hncqwikHFSz7tw==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"