1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

Styled components batch4.1 (#2812)

Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>
This commit is contained in:
Mateusz Kwasniewski 2023-01-05 09:45:39 +01:00 committed by GitHub
parent 94c90b7731
commit 674e36b40b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 709 additions and 928 deletions

View File

@ -1,37 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
display: 'grid',
lineHeight: 1.25,
gridTemplateColumns: '1fr auto',
alignSelf: 'start',
alignItems: 'start',
gap: '0.5rem',
padding: '0.5rem',
background: theme.palette.grey[200],
borderRadius: theme.shape.borderRadius,
wordBreak: 'break-word',
},
label: {
fontSize: theme.fontSizes.smallBody,
},
description: {
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.grey[700],
},
button: {
all: 'unset',
lineHeight: 0.1,
paddingTop: 1,
display: 'block',
cursor: 'pointer',
'& svg': {
fontSize: '1rem',
opacity: 0.5,
},
'&:hover svg, &:focus-visible svg': {
opacity: 0.75,
},
},
}));

View File

@ -1,6 +1,6 @@
import { useStyles } from 'component/context/ContectFormChip/ContextFormChip.styles';
import { Cancel } from '@mui/icons-material'; import { Cancel } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { styled } from '@mui/material';
interface IContextFormChipProps { interface IContextFormChipProps {
label: string; label: string;
@ -8,27 +8,62 @@ interface IContextFormChipProps {
onRemove: () => void; onRemove: () => void;
} }
const StyledContainer = styled('li')(({ theme }) => ({
display: 'grid',
lineHeight: 1.25,
gridTemplateColumns: '1fr auto',
alignSelf: 'start',
alignItems: 'start',
gap: theme.spacing(1),
padding: theme.spacing(1),
background: theme.palette.secondaryContainer,
borderRadius: theme.shape.borderRadius,
wordBreak: 'break-word',
}));
const StyledLabel = styled('div')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
}));
const StyledDescription = styled('div')(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.neutral.main,
}));
const StyledButton = styled('button')(({ theme }) => ({
all: 'unset',
lineHeight: 0.1,
paddingTop: '1px',
display: 'block',
cursor: 'pointer',
'& svg': {
fontSize: theme.fontSizes.bodySize,
opacity: 0.5,
},
'&:hover svg, &:focus-visible svg': {
opacity: 0.75,
},
}));
export const ContextFormChip = ({ export const ContextFormChip = ({
label, label,
description, description,
onRemove, onRemove,
}: IContextFormChipProps) => { }: IContextFormChipProps) => {
const { classes: styles } = useStyles();
return ( return (
<li className={styles.container}> <StyledContainer>
<div> <div>
<div className={styles.label}>{label}</div> <StyledLabel>{label}</StyledLabel>
<ConditionallyRender <ConditionallyRender
condition={Boolean(description)} condition={Boolean(description)}
show={() => ( show={() => (
<div className={styles.description}>{description}</div> <StyledDescription>{description}</StyledDescription>
)} )}
/> />
</div> </div>
<button onClick={onRemove} className={styles.button}> <StyledButton onClick={onRemove}>
<Cancel titleAccess="Remove" /> <Cancel titleAccess="Remove" />
</button> </StyledButton>
</li> </StyledContainer>
); );
}; };

View File

@ -1,13 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
listStyleType: 'none',
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
padding: 0,
margin: 0,
marginBottom: '1rem !important',
},
}));

View File

@ -1,8 +1,14 @@
import { useStyles } from 'component/context/ContectFormChip/ContextFormChipList.styles';
import React from 'react'; import React from 'react';
import { styled } from '@mui/material';
export const ContextFormChipList: React.FC = ({ children }) => { const StyledContainer = styled('ul')(({ theme }) => ({
const { classes: styles } = useStyles(); listStyleType: 'none',
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(1),
padding: 0,
margin: 0,
marginBottom: '1rem !important',
}));
return <ul className={styles.container}>{children}</ul>; export const ContextFormChipList: React.FC = StyledContainer;
};

View File

@ -1,59 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
maxWidth: '470px',
},
form: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
input: { width: '100%', marginBottom: '1rem' },
inputHeader: {
marginBottom: '0.3rem',
},
label: {
minWidth: '300px',
[theme.breakpoints.down(600)]: {
minWidth: 'auto',
},
},
tagContainer: {
display: 'grid',
gridTemplateColumns: '1fr auto',
gap: '0.5rem',
marginBottom: '1rem',
},
tagInput: {
gridColumn: 1,
},
tagButton: {
gridColumn: 2,
},
buttonContainer: {
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
},
cancelButton: {
marginLeft: '1.5rem',
},
inputDescription: {
marginBottom: '0.5rem',
},
permissionErrorContainer: {
position: 'relative',
},
errorMessage: {
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',
top: '-8px',
},
switchContainer: {
display: 'flex',
alignItems: 'center',
marginLeft: '-9px',
},
}));

View File

@ -1,6 +1,12 @@
import Input from 'component/common/Input/Input'; import Input from 'component/common/Input/Input';
import { TextField, Button, Switch, Typography } from '@mui/material'; import {
import { useStyles } from './ContextForm.styles'; TextField,
Button,
Switch,
Typography,
styled,
Theme,
} from '@mui/material';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Add } from '@mui/icons-material'; import { Add } from '@mui/icons-material';
import { ILegalValue } from 'interfaces/context'; import { ILegalValue } from 'interfaces/context';
@ -27,6 +33,52 @@ interface IContextForm {
const ENTER = 'Enter'; const ENTER = 'Enter';
const StyledForm = styled('form')({
display: 'flex',
flexDirection: 'column',
height: '100%',
});
const StyledContainer = styled('div')({
maxWidth: '470px',
});
const StyledInputDescription = styled('p')(({ theme }) => ({
marginBottom: theme.spacing(1),
}));
const styledInput = (theme: Theme) => ({
width: '100%',
marginBottom: theme.spacing(2),
});
const StyledTagContainer = styled('div')(({ theme }) => ({
display: 'grid',
gridTemplateColumns: '1fr auto',
gap: theme.spacing(1),
marginBottom: theme.spacing(2),
}));
const StyledInputHeader = styled('p')(({ theme }) => ({
marginBottom: theme.spacing(0.5),
}));
const StyledSwitchContainer = styled('div')({
display: 'flex',
alignItems: 'center',
marginLeft: '-9px',
});
const StyledButtonContainer = styled('div')({
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
});
const StyledCancelButton = styled(Button)(({ theme }) => ({
marginLeft: theme.spacing(3),
}));
export const ContextForm: React.FC<IContextForm> = ({ export const ContextForm: React.FC<IContextForm> = ({
children, children,
handleSubmit, handleSubmit,
@ -45,7 +97,6 @@ export const ContextForm: React.FC<IContextForm> = ({
setErrors, setErrors,
clearErrors, clearErrors,
}) => { }) => {
const { classes: styles } = useStyles();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [valueDesc, setValueDesc] = useState(''); const [valueDesc, setValueDesc] = useState('');
const [valueFocused, setValueFocused] = useState(false); const [valueFocused, setValueFocused] = useState(false);
@ -104,13 +155,13 @@ export const ContextForm: React.FC<IContextForm> = ({
}; };
return ( return (
<form onSubmit={onSubmit} className={styles.form}> <StyledForm onSubmit={onSubmit}>
<div className={styles.container}> <StyledContainer>
<p className={styles.inputDescription}> <StyledInputDescription>
What is your context name? What is your context name?
</p> </StyledInputDescription>
<Input <Input
className={styles.input} sx={styledInput}
label="Context name" label="Context name"
value={contextName} value={contextName}
disabled={mode === 'Edit'} disabled={mode === 'Edit'}
@ -121,11 +172,11 @@ export const ContextForm: React.FC<IContextForm> = ({
onBlur={validateContext} onBlur={validateContext}
autoFocus autoFocus
/> />
<p className={styles.inputDescription}> <StyledInputDescription>
What is this context for? What is this context for?
</p> </StyledInputDescription>
<TextField <TextField
className={styles.input} sx={styledInput}
label="Context description (optional)" label="Context description (optional)"
variant="outlined" variant="outlined"
multiline multiline
@ -134,14 +185,14 @@ export const ContextForm: React.FC<IContextForm> = ({
size="small" size="small"
onChange={e => setContextDesc(e.target.value)} onChange={e => setContextDesc(e.target.value)}
/> />
<p className={styles.inputDescription}> <StyledInputDescription>
Which values do you want to allow? Which values do you want to allow?
</p> </StyledInputDescription>
<div className={styles.tagContainer}> <StyledTagContainer>
<TextField <TextField
label="Legal value (optional)" label="Legal value (optional)"
name="value" name="value"
className={styles.tagInput} sx={{ gridColumn: 1 }}
value={value} value={value}
error={Boolean(errors.tag)} error={Boolean(errors.tag)}
helperText={errors.tag} helperText={errors.tag}
@ -155,7 +206,7 @@ export const ContextForm: React.FC<IContextForm> = ({
/> />
<TextField <TextField
label="Value description (optional)" label="Value description (optional)"
className={styles.tagInput} sx={{ gridColumn: 1 }}
value={valueDesc} value={valueDesc}
variant="outlined" variant="outlined"
size="small" size="small"
@ -166,7 +217,7 @@ export const ContextForm: React.FC<IContextForm> = ({
inputProps={{ maxLength: 100 }} inputProps={{ maxLength: 100 }}
/> />
<Button <Button
className={styles.tagButton} sx={{ gridColumn: 2 }}
startIcon={<Add />} startIcon={<Add />}
onClick={addLegalValue} onClick={addLegalValue}
variant="outlined" variant="outlined"
@ -175,7 +226,7 @@ export const ContextForm: React.FC<IContextForm> = ({
> >
Add Add
</Button> </Button>
</div> </StyledTagContainer>
<ContextFormChipList> <ContextFormChipList>
{legalValues.map(legalValue => { {legalValues.map(legalValue => {
return ( return (
@ -188,7 +239,7 @@ export const ContextForm: React.FC<IContextForm> = ({
); );
})} })}
</ContextFormChipList> </ContextFormChipList>
<p className={styles.inputHeader}>Custom stickiness</p> <StyledInputHeader>Custom stickiness</StyledInputHeader>
<p> <p>
By enabling stickiness on this context field you can use it By enabling stickiness on this context field you can use it
together with the flexible-rollout strategy. This will together with the flexible-rollout strategy. This will
@ -203,21 +254,21 @@ export const ContextForm: React.FC<IContextForm> = ({
Read more Read more
</a> </a>
</p> </p>
<div className={styles.switchContainer}> <StyledSwitchContainer>
<Switch <Switch
checked={stickiness} checked={stickiness}
value={stickiness} value={stickiness}
onChange={() => setStickiness(!stickiness)} onChange={() => setStickiness(!stickiness)}
/> />
<Typography>{stickiness ? 'On' : 'Off'}</Typography> <Typography>{stickiness ? 'On' : 'Off'}</Typography>
</div> </StyledSwitchContainer>
</div> </StyledContainer>
<div className={styles.buttonContainer}> <StyledButtonContainer>
{children} {children}
<Button onClick={onCancel} className={styles.cancelButton}> <StyledCancelButton onClick={onCancel}>
Cancel Cancel
</Button> </StyledCancelButton>
</div> </StyledButtonContainer>
</form> </StyledForm>
); );
}; };

View File

@ -1,31 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
display: 'flex',
flexDirection: 'column',
border: `1px solid ${theme.palette.grey[200]}`,
padding: '1.5rem',
borderRadius: '5px',
margin: '1.5rem 0',
minWidth: '450px',
},
icon: {
fill: theme.palette.inactiveIcon,
marginRight: '0.5rem',
},
header: {
display: 'flex',
alignItems: 'center',
marginBottom: '0.25rem',
},
infoContainer: {
marginTop: '1rem',
display: 'flex',
justifyContent: 'space-around',
},
infoInnerContainer: {
textAlign: 'center',
},
infoTitle: { fontWeight: 'bold', marginBottom: '0.25rem' },
}));

View File

@ -1,38 +0,0 @@
import { CloudCircle } from '@mui/icons-material';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { useStyles } from 'component/environments/EnvironmentCard/EnvironmentCard.styles';
interface IEnvironmentProps {
name: string;
type: string;
}
const EnvironmentCard = ({ name, type }: IEnvironmentProps) => {
const { classes: styles } = useStyles();
return (
<div className={styles.container}>
<div className={styles.header}>
<CloudCircle className={styles.icon} /> Environment
</div>
<div className={styles.infoContainer}>
<div className={styles.infoInnerContainer}>
<div className={styles.infoTitle}>Id</div>
<div>
<StringTruncator
text={name}
maxWidth={'250'}
maxLength={30}
/>
</div>
</div>
<div className={styles.infoInnerContainer}>
<div className={styles.infoTitle}>Type</div>
<div>{type}</div>
</div>
</div>
</div>
);
};
export default EnvironmentCard;

View File

@ -1,46 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
maxWidth: '440px',
},
form: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
input: { width: '100%', marginBottom: '1rem' },
label: {
minWidth: '30px',
[theme.breakpoints.down(600)]: {
minWidth: 'auto',
},
},
buttonContainer: {
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
},
cancelButton: {
marginLeft: '1.5rem',
},
inputDescription: {
marginBottom: '0.5rem',
},
formHeader: {
fontWeight: 'normal',
marginTop: '0',
},
header: {
fontWeight: 'normal',
},
permissionErrorContainer: {
position: 'relative',
},
errorMessage: {
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',
top: '-8px',
},
}));

View File

@ -1,5 +1,4 @@
import { Button } from '@mui/material'; import { Button, styled } from '@mui/material';
import { useStyles } from './EnvironmentForm.styles';
import React from 'react'; import React from 'react';
import Input from 'component/common/Input/Input'; import Input from 'component/common/Input/Input';
import EnvironmentTypeSelector from './EnvironmentTypeSelector/EnvironmentTypeSelector'; import EnvironmentTypeSelector from './EnvironmentTypeSelector/EnvironmentTypeSelector';
@ -18,6 +17,40 @@ interface IEnvironmentForm {
clearErrors: () => void; clearErrors: () => void;
} }
const StyledForm = styled('form')({
display: 'flex',
flexDirection: 'column',
height: '100%',
});
const StyledFormHeader = styled('h3')({
fontWeight: 'normal',
marginTop: '0',
});
const StyledContainer = styled('div')({
maxWidth: '440px',
});
const StyledInputDescription = styled('p')(({ theme }) => ({
marginBottom: theme.spacing(1),
}));
const StyledInput = styled(Input)(({ theme }) => ({
width: '100%',
marginBottom: theme.spacing(2),
}));
const StyledButtonContainer = styled('div')(({ theme }) => ({
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
}));
const StyledCancelButton = styled(Button)(({ theme }) => ({
marginLeft: theme.spacing(3),
}));
const EnvironmentForm: React.FC<IEnvironmentForm> = ({ const EnvironmentForm: React.FC<IEnvironmentForm> = ({
children, children,
handleSubmit, handleSubmit,
@ -31,18 +64,15 @@ const EnvironmentForm: React.FC<IEnvironmentForm> = ({
mode, mode,
clearErrors, clearErrors,
}) => { }) => {
const { classes: styles } = useStyles();
return ( return (
<form onSubmit={handleSubmit} className={styles.form}> <StyledForm onSubmit={handleSubmit}>
<h3 className={styles.formHeader}>Environment information</h3> <StyledFormHeader>Environment information</StyledFormHeader>
<div className={styles.container}> <StyledContainer>
<p className={styles.inputDescription}> <StyledInputDescription>
What is your environment name? (Can't be changed later) What is your environment name? (Can't be changed later)
</p> </StyledInputDescription>
<Input <StyledInput
className={styles.input}
label="Environment name" label="Environment name"
value={name} value={name}
onChange={e => setName(trim(e.target.value))} onChange={e => setName(trim(e.target.value))}
@ -54,21 +84,21 @@ const EnvironmentForm: React.FC<IEnvironmentForm> = ({
autoFocus autoFocus
/> />
<p className={styles.inputDescription}> <StyledInputDescription>
What type of environment do you want to create? What type of environment do you want to create?
</p> </StyledInputDescription>
<EnvironmentTypeSelector <EnvironmentTypeSelector
onChange={e => setType(e.currentTarget.value)} onChange={e => setType(e.currentTarget.value)}
value={type} value={type}
/> />
</div> </StyledContainer>
<div className={styles.buttonContainer}> <StyledButtonContainer>
{children} {children}
<Button onClick={handleCancel} className={styles.cancelButton}> <StyledCancelButton onClick={handleCancel}>
Cancel Cancel
</Button> </StyledCancelButton>
</div> </StyledButtonContainer>
</form> </StyledForm>
); );
}; };

View File

@ -1,14 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
radioGroup: {
flexDirection: 'row',
},
formHeader: {
fontWeight: 'bold',
fontSize: theme.fontSizes.bodySize,
marginTop: '1.5rem',
marginBottom: '0.5rem',
},
radioBtnGroup: { display: 'flex', flexDirection: 'column' },
}));

View File

@ -3,8 +3,8 @@ import {
FormControlLabel, FormControlLabel,
Radio, Radio,
RadioGroup, RadioGroup,
styled,
} from '@mui/material'; } from '@mui/material';
import { useStyles } from './EnvironmentTypeSelector.styles';
import React from 'react'; import React from 'react';
interface IEnvironmentTypeSelectorProps { interface IEnvironmentTypeSelectorProps {
@ -12,20 +12,23 @@ interface IEnvironmentTypeSelectorProps {
value: string; value: string;
} }
const StyledRadioGroup = styled(RadioGroup)({
flexDirection: 'row',
});
const StyledRadioButtonGroup = styled('div')({
display: 'flex',
flexDirection: 'column',
});
const EnvironmentTypeSelector = ({ const EnvironmentTypeSelector = ({
onChange, onChange,
value, value,
}: IEnvironmentTypeSelectorProps) => { }: IEnvironmentTypeSelectorProps) => {
const { classes: styles } = useStyles();
return ( return (
<FormControl component="fieldset"> <FormControl component="fieldset">
<RadioGroup <StyledRadioGroup data-loading value={value} onChange={onChange}>
data-loading <StyledRadioButtonGroup>
value={value}
onChange={onChange}
className={styles.radioGroup}
>
<div className={styles.radioBtnGroup}>
<FormControlLabel <FormControlLabel
value="development" value="development"
label="Development" label="Development"
@ -36,8 +39,8 @@ const EnvironmentTypeSelector = ({
label="Test" label="Test"
control={<Radio />} control={<Radio />}
/> />
</div> </StyledRadioButtonGroup>
<div style={{ display: 'flex', flexDirection: 'column' }}> <StyledRadioButtonGroup>
<FormControlLabel <FormControlLabel
value="preproduction" value="preproduction"
label="Pre production" label="Pre production"
@ -48,8 +51,8 @@ const EnvironmentTypeSelector = ({
label="Production" label="Production"
control={<Radio />} control={<Radio />}
/> />
</div> </StyledRadioButtonGroup>
</RadioGroup> </StyledRadioGroup>
</FormControl> </FormControl>
); );
}; };

View File

@ -1,68 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
maxWidth: '400px',
},
form: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
input: { width: '100%', marginBottom: '1rem' },
selectInput: {
marginBottom: '1rem',
minWidth: '400px',
[theme.breakpoints.down(600)]: {
minWidth: '379px',
},
},
label: {
minWidth: '300px',
[theme.breakpoints.down(600)]: {
minWidth: 'auto',
},
},
buttonContainer: {
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
},
cancelButton: {
marginLeft: '1.5rem',
},
inputDescription: {
marginBottom: '0.5rem',
color: theme.palette.text.secondary,
},
typeDescription: {
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
top: '-13px',
position: 'relative',
},
formHeader: {
fontWeight: 'normal',
marginTop: '0',
},
header: {
fontWeight: 'normal',
},
permissionErrorContainer: {
position: 'relative',
},
errorMessage: {
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',
top: '-8px',
},
roleSubtitle: {
margin: '0.5rem 0',
},
flexRow: {
display: 'flex',
alignItems: 'center',
marginTop: '0.5rem',
},
}));

View File

@ -2,10 +2,11 @@ import {
Button, Button,
FormControl, FormControl,
FormControlLabel, FormControlLabel,
styled,
Switch, Switch,
Theme,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { useStyles } from './FeatureForm.styles';
import FeatureTypeSelect from '../FeatureView/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect'; import FeatureTypeSelect from '../FeatureView/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
import { CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from 'utils/testIds'; import { CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from 'utils/testIds';
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes'; import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
@ -39,6 +40,66 @@ interface IFeatureToggleForm {
clearErrors: () => void; clearErrors: () => void;
} }
const StyledForm = styled('form')({
display: 'flex',
flexDirection: 'column',
height: '100%',
});
const StyledContainer = styled('div')({
maxWidth: '400px',
});
const StyledInputDescription = styled('p')(({ theme }) => ({
marginBottom: theme.spacing(1),
color: theme.palette.text.secondary,
}));
const StyledInput = styled(Input)(({ theme }) => ({
width: '100%',
marginBottom: theme.spacing(2),
}));
const StyledFormControl = styled(FormControl)(({ theme }) => ({
width: '100%',
marginBottom: theme.spacing(2),
}));
const styledSelectInput = (theme: Theme) => ({
marginBottom: theme.spacing(2),
minWidth: '400px',
[theme.breakpoints.down('sm')]: {
minWidth: '379px',
},
});
const StyledTypeDescription = styled('p')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
top: '-13px',
position: 'relative',
}));
const StyledButtonContainer = styled('div')({
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
});
const StyledRow = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginTop: theme.spacing(1),
}));
const StyledCancelButton = styled(Button)(({ theme }) => ({
marginLeft: theme.spacing(3),
}));
const styledTypography = (theme: Theme) => ({
margin: theme.spacing(1, 0),
});
const FeatureForm: React.FC<IFeatureToggleForm> = ({ const FeatureForm: React.FC<IFeatureToggleForm> = ({
children, children,
type, type,
@ -58,7 +119,6 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
mode, mode,
clearErrors, clearErrors,
}) => { }) => {
const { classes: styles } = useStyles();
const { featureTypes } = useFeatureTypes(); const { featureTypes } = useFeatureTypes();
const navigate = useNavigate(); const navigate = useNavigate();
const { permissions } = useAuthPermissions(); const { permissions } = useAuthPermissions();
@ -69,15 +129,14 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
}; };
return ( return (
<form onSubmit={handleSubmit} className={styles.form}> <StyledForm onSubmit={handleSubmit}>
<div className={styles.container}> <StyledContainer>
<p className={styles.inputDescription}> <StyledInputDescription>
What would you like to call your toggle? What would you like to call your toggle?
</p> </StyledInputDescription>
<Input <StyledInput
autoFocus autoFocus
disabled={mode === 'Edit'} disabled={mode === 'Edit'}
className={styles.input}
label="Name" label="Name"
id="feature-toggle-name" id="feature-toggle-name"
error={Boolean(errors.name)} error={Boolean(errors.name)}
@ -88,10 +147,11 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
data-testid={CF_NAME_ID} data-testid={CF_NAME_ID}
onBlur={validateToggleName} onBlur={validateToggleName}
/> />
<p className={styles.inputDescription}> <StyledInputDescription>
What kind of feature toggle do you want? What kind of feature toggle do you want?
</p> </StyledInputDescription>
<FeatureTypeSelect <FeatureTypeSelect
sx={styledSelectInput}
value={type} value={type}
onChange={setType} onChange={setType}
label={'Toggle type'} label={'Toggle type'}
@ -99,17 +159,16 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
editable editable
data-testid={CF_TYPE_ID} data-testid={CF_TYPE_ID}
IconComponent={KeyboardArrowDownOutlined} IconComponent={KeyboardArrowDownOutlined}
className={styles.selectInput}
/> />
<p className={styles.typeDescription}> <StyledTypeDescription>
{renderToggleDescription()} {renderToggleDescription()}
</p> </StyledTypeDescription>
<ConditionallyRender <ConditionallyRender
condition={editable} condition={editable}
show={ show={
<p className={styles.inputDescription}> <StyledInputDescription>
In which project do you want to save the toggle? In which project do you want to save the toggle?
</p> </StyledInputDescription>
} }
/> />
<FeatureProjectSelect <FeatureProjectSelect
@ -123,14 +182,13 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
enabled={editable} enabled={editable}
filter={projectFilterGenerator(permissions, CREATE_FEATURE)} filter={projectFilterGenerator(permissions, CREATE_FEATURE)}
IconComponent={KeyboardArrowDownOutlined} IconComponent={KeyboardArrowDownOutlined}
className={styles.selectInput} sx={styledSelectInput}
/> />
<p className={styles.inputDescription}> <StyledInputDescription>
How would you describe your feature toggle? How would you describe your feature toggle?
</p> </StyledInputDescription>
<Input <StyledInput
className={styles.input}
multiline multiline
rows={4} rows={4}
label="Description" label="Description"
@ -139,10 +197,10 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
data-testid={CF_DESC_ID} data-testid={CF_DESC_ID}
onChange={e => setDescription(e.target.value)} onChange={e => setDescription(e.target.value)}
/> />
<FormControl className={styles.input}> <StyledFormControl>
<Typography <Typography
variant="subtitle1" variant="subtitle1"
className={styles.roleSubtitle} sx={styledTypography}
data-loading data-loading
component="h2" component="h2"
> >
@ -160,7 +218,7 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
the impression data documentation the impression data documentation
</a> </a>
</p> </p>
<div className={styles.flexRow}> <StyledRow>
<FormControlLabel <FormControlLabel
labelPlacement="start" labelPlacement="start"
style={{ marginLeft: 0 }} style={{ marginLeft: 0 }}
@ -175,17 +233,17 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
} }
label="Enable impression data" label="Enable impression data"
/> />
</div> </StyledRow>
</FormControl> </StyledFormControl>
</div> </StyledContainer>
<div className={styles.buttonContainer}> <StyledButtonContainer>
{children} {children}
<Button onClick={handleCancel} className={styles.cancelButton}> <StyledCancelButton onClick={handleCancel}>
Cancel Cancel
</Button> </StyledCancelButton>
</div> </StyledButtonContainer>
</form> </StyledForm>
); );
}; };

View File

@ -1,23 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
paddingTop: theme.spacing(2),
},
title: {
fontSize: theme.fontSizes.bodySize,
textAlign: 'center',
color: theme.palette.text.primary,
marginBottom: theme.spacing(1),
},
description: {
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallBody,
textAlign: 'center',
marginBottom: theme.spacing(3),
},
}));

View File

@ -1,10 +1,9 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Box } from '@mui/material'; import { Box, Button, styled } from '@mui/material';
import { SectionSeparator } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/SectionSeparator/SectionSeparator'; import { SectionSeparator } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/SectionSeparator/SectionSeparator';
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi'; import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { useStyles } from './FeatureStrategyEmpty.styles';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable'; import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
@ -23,12 +22,33 @@ interface IFeatureStrategyEmptyProps {
environmentId: string; environmentId: string;
} }
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
paddingTop: theme.spacing(2),
}));
const StyledTitle = styled('div')(({ theme }) => ({
fontSize: theme.fontSizes.bodySize,
textAlign: 'center',
color: theme.palette.text.primary,
marginBottom: theme.spacing(1),
}));
const StyledDescription = styled('p')(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallBody,
textAlign: 'center',
marginBottom: theme.spacing(3),
}));
export const FeatureStrategyEmpty = ({ export const FeatureStrategyEmpty = ({
projectId, projectId,
featureId, featureId,
environmentId, environmentId,
}: IFeatureStrategyEmptyProps) => { }: IFeatureStrategyEmptyProps) => {
const { classes: styles } = useStyles();
const { addStrategyToFeature } = useFeatureStrategyApi(); const { addStrategyToFeature } = useFeatureStrategyApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { refetchFeature } = useFeature(projectId, featureId); const { refetchFeature } = useFeature(projectId, featureId);
@ -122,16 +142,16 @@ export const FeatureStrategyEmpty = ({
} }
/> />
<div className={styles.container}> <StyledContainer>
<div className={styles.title}> <StyledTitle>
You have not defined any strategies yet. You have not defined any strategies yet.
</div> </StyledTitle>
<p className={styles.description}> <StyledDescription>
Strategies added in this environment will only be executed Strategies added in this environment will only be executed
if the SDK is using an{' '} if the SDK is using an{' '}
<Link to="/admin/api">API key configured</Link> for this <Link to="/admin/api">API key configured</Link> for this
environment. environment.
</p> </StyledDescription>
<Box <Box
sx={{ sx={{
w: '100%', w: '100%',
@ -211,7 +231,7 @@ export const FeatureStrategyEmpty = ({
Roll out to a percentage of your userbase. Roll out to a percentage of your userbase.
</AddFromTemplateCard> </AddFromTemplateCard>
</Box> </Box>
</div> </StyledContainer>
</> </>
); );
}; };

View File

@ -1,37 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
form: {
display: 'grid',
gap: '1rem',
},
hr: {
width: '100%',
height: 1,
margin: '1rem 0',
border: 'none',
background: theme.palette.grey[200],
},
title: {
display: 'grid',
gridTemplateColumns: 'auto 1fr',
gridGap: '.5rem',
fontSize: theme.fontSizes.bodySize,
},
icon: {
color: theme.palette.primary.main,
},
name: {
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontWeight: theme.fontWeight.thin,
},
buttons: {
display: 'flex',
justifyContent: 'end',
gap: '1rem',
paddingBottom: '5rem',
},
}));

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Alert, Button } from '@mui/material'; import { Alert, Button, styled } from '@mui/material';
import { import {
IFeatureStrategy, IFeatureStrategy,
IFeatureStrategyParameters, IFeatureStrategyParameters,
@ -10,7 +10,6 @@ import { FeatureStrategyType } from '../FeatureStrategyType/FeatureStrategyType'
import { FeatureStrategyEnabled } from './FeatureStrategyEnabled/FeatureStrategyEnabled'; import { FeatureStrategyEnabled } from './FeatureStrategyEnabled/FeatureStrategyEnabled';
import { FeatureStrategyConstraints } from '../FeatureStrategyConstraints/FeatureStrategyConstraints'; import { FeatureStrategyConstraints } from '../FeatureStrategyConstraints/FeatureStrategyConstraints';
import { IFeatureToggle } from 'interfaces/featureToggle'; import { IFeatureToggle } from 'interfaces/featureToggle';
import { useStyles } from './FeatureStrategyForm.styles';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { STRATEGY_FORM_SUBMIT_ID } from 'utils/testIds'; import { STRATEGY_FORM_SUBMIT_ID } from 'utils/testIds';
@ -48,6 +47,26 @@ interface IFeatureStrategyFormProps {
errors: IFormErrors; errors: IFormErrors;
} }
const StyledForm = styled('form')(({ theme }) => ({
display: 'grid',
gap: theme.spacing(2),
}));
const StyledHr = styled('hr')(({ theme }) => ({
width: '100%',
height: '1px',
margin: theme.spacing(2, 0),
border: 'none',
background: theme.palette.tertiary.light,
}));
const StyledButtons = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'end',
gap: theme.spacing(2),
paddingBottom: theme.spacing(10),
}));
export const FeatureStrategyForm = ({ export const FeatureStrategyForm = ({
projectId, projectId,
feature, feature,
@ -62,7 +81,6 @@ export const FeatureStrategyForm = ({
errors, errors,
isChangeRequest, isChangeRequest,
}: IFeatureStrategyFormProps) => { }: IFeatureStrategyFormProps) => {
const { classes: styles } = useStyles();
const [showProdGuard, setShowProdGuard] = useState(false); const [showProdGuard, setShowProdGuard] = useState(false);
const hasValidConstraints = useConstraintsValidation(strategy.constraints); const hasValidConstraints = useConstraintsValidation(strategy.constraints);
const enableProdGuard = useFeatureStrategyProdGuard(feature, environmentId); const enableProdGuard = useFeatureStrategyProdGuard(feature, environmentId);
@ -148,7 +166,7 @@ export const FeatureStrategyForm = ({
}; };
return ( return (
<form className={styles.form} onSubmit={onSubmitWithValidation}> <StyledForm onSubmit={onSubmitWithValidation}>
<ConditionallyRender <ConditionallyRender
condition={hasChangeRequestInReviewForEnvironment} condition={hasChangeRequestInReviewForEnvironment}
show={alert} show={alert}
@ -188,7 +206,7 @@ export const FeatureStrategyForm = ({
} }
/> />
</FeatureStrategyEnabled> </FeatureStrategyEnabled>
<hr className={styles.hr} /> <StyledHr />
<ConditionallyRender <ConditionallyRender
condition={Boolean(uiConfig.flags.SE)} condition={Boolean(uiConfig.flags.SE)}
show={ show={
@ -204,7 +222,7 @@ export const FeatureStrategyForm = ({
strategy={strategy} strategy={strategy}
setStrategy={setStrategy} setStrategy={setStrategy}
/> />
<hr className={styles.hr} /> <StyledHr />
<FeatureStrategyType <FeatureStrategyType
strategy={strategy} strategy={strategy}
strategyDefinition={strategyDefinition} strategyDefinition={strategyDefinition}
@ -213,8 +231,8 @@ export const FeatureStrategyForm = ({
errors={errors} errors={errors}
hasAccess={access} hasAccess={access}
/> />
<hr className={styles.hr} /> <StyledHr />
<div className={styles.buttons}> <StyledButtons>
<PermissionButton <PermissionButton
permission={permission} permission={permission}
projectId={feature.project} projectId={feature.project}
@ -248,7 +266,7 @@ export const FeatureStrategyForm = ({
loading={loading} loading={loading}
label="Save strategy" label="Save strategy"
/> />
</div> </StyledButtons>
</form> </StyledForm>
); );
}; };

View File

@ -1,40 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
card: {
display: 'grid',
gridTemplateColumns: '3rem 1fr',
width: '20rem',
padding: '1rem',
color: 'inherit',
textDecoration: 'inherit',
lineHeight: 1.25,
borderWidth: 1,
borderStyle: 'solid',
borderColor: theme.palette.grey[400],
borderRadius: theme.spacing(1),
'&:hover, &:focus': {
borderColor: theme.palette.primary.main,
},
},
icon: {
width: '2rem',
height: 'auto',
'& > svg': {
// Styling for SVG icons.
fill: theme.palette.primary.main,
},
'& > div': {
// Styling for the Rollout icon.
height: '1rem',
marginLeft: '-.75rem',
color: theme.palette.primary.main,
},
},
name: {
fontWeight: theme.fontWeight.bold,
},
description: {
fontSize: theme.fontSizes.smallBody,
},
}));

View File

@ -1,12 +1,12 @@
import { IStrategy } from 'interfaces/strategy'; import { IStrategy } from 'interfaces/strategy';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useStyles } from './FeatureStrategyMenuCard.styles';
import { import {
getFeatureStrategyIcon, getFeatureStrategyIcon,
formatStrategyName, formatStrategyName,
} from 'utils/strategyNames'; } from 'utils/strategyNames';
import { formatCreateStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate'; import { formatCreateStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
import StringTruncator from 'component/common/StringTruncator/StringTruncator'; import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { styled } from '@mui/material';
interface IFeatureStrategyMenuCardProps { interface IFeatureStrategyMenuCardProps {
projectId: string; projectId: string;
@ -15,13 +15,52 @@ interface IFeatureStrategyMenuCardProps {
strategy: IStrategy; strategy: IStrategy;
} }
const StyledIcon = styled('div')(({ theme }) => ({
width: theme.spacing(4),
height: 'auto',
'& > svg': {
// Styling for SVG icons.
fill: theme.palette.primary.main,
},
'& > div': {
// Styling for the Rollout icon.
height: theme.spacing(2),
marginLeft: '-.75rem',
color: theme.palette.primary.main,
},
}));
const StyledDescription = styled('div')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
}));
const StyledName = styled(StringTruncator)(({ theme }) => ({
fontWeight: theme.fontWeight.bold,
}));
const StyledCard = styled(Link)(({ theme }) => ({
display: 'grid',
gridTemplateColumns: '3rem 1fr',
width: '20rem',
padding: theme.spacing(2),
color: 'inherit',
textDecoration: 'inherit',
lineHeight: 1.25,
borderWidth: '1px',
borderStyle: 'solid',
borderColor: theme.palette.dividerAlternative,
borderRadius: theme.spacing(1),
'&:hover, &:focus': {
borderColor: theme.palette.primary.main,
},
}));
export const FeatureStrategyMenuCard = ({ export const FeatureStrategyMenuCard = ({
projectId, projectId,
featureId, featureId,
environmentId, environmentId,
strategy, strategy,
}: IFeatureStrategyMenuCardProps) => { }: IFeatureStrategyMenuCardProps) => {
const { classes: styles } = useStyles();
const StrategyIcon = getFeatureStrategyIcon(strategy.name); const StrategyIcon = getFeatureStrategyIcon(strategy.name);
const strategyName = formatStrategyName(strategy.name); const strategyName = formatStrategyName(strategy.name);
@ -33,19 +72,18 @@ export const FeatureStrategyMenuCard = ({
); );
return ( return (
<Link to={createStrategyPath} className={styles.card}> <StyledCard to={createStrategyPath}>
<div className={styles.icon}> <StyledIcon>
<StrategyIcon /> <StrategyIcon />
</div> </StyledIcon>
<div> <div>
<StringTruncator <StyledName
text={strategy.displayName || strategyName} text={strategy.displayName || strategyName}
className={styles.name}
maxWidth="200" maxWidth="200"
maxLength={25} maxLength={25}
/> />
<div className={styles.description}>{strategy.description}</div> <StyledDescription>{strategy.description}</StyledDescription>
</div> </div>
</Link> </StyledCard>
); );
}; };

View File

@ -1,9 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
divider: {
border: `1px dashed ${theme.palette.divider}`,
marginTop: theme.spacing(1),
marginBottom: theme.spacing(2),
},
}));

View File

@ -6,22 +6,24 @@ import {
IAutocompleteBoxOption, IAutocompleteBoxOption,
} from 'component/common/AutocompleteBox/AutocompleteBox'; } from 'component/common/AutocompleteBox/AutocompleteBox';
import { FeatureStrategySegmentList } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentList'; import { FeatureStrategySegmentList } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentList';
import { useStyles } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.styles';
import { SegmentDocsStrategyWarning } from 'component/segments/SegmentDocs'; import { SegmentDocsStrategyWarning } from 'component/segments/SegmentDocs';
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits'; import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
import { Divider, Typography } from '@mui/material'; import { Divider, styled, Typography } from '@mui/material';
interface IFeatureStrategySegmentProps { interface IFeatureStrategySegmentProps {
segments: ISegment[]; segments: ISegment[];
setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>; setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>;
} }
const StyledDivider = styled(Divider)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
}));
export const FeatureStrategySegment = ({ export const FeatureStrategySegment = ({
segments: selectedSegments, segments: selectedSegments,
setSegments: setSelectedSegments, setSegments: setSelectedSegments,
}: IFeatureStrategySegmentProps) => { }: IFeatureStrategySegmentProps) => {
const { segments: allSegments } = useSegments(); const { segments: allSegments } = useSegments();
const { classes: styles } = useStyles();
const { strategySegmentsLimit } = useSegmentLimits(); const { strategySegmentsLimit } = useSegmentLimits();
const atStrategySegmentsLimit: boolean = Boolean( const atStrategySegmentsLimit: boolean = Boolean(
@ -68,7 +70,7 @@ export const FeatureStrategySegment = ({
segments={selectedSegments} segments={selectedSegments}
setSegments={setSelectedSegments} setSegments={setSelectedSegments}
/> />
<Divider className={styles.divider} /> <StyledDivider />
</> </>
); );
}; };

View File

@ -1,29 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
chip: {
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
paddingInlineStart: '1rem',
paddingInlineEnd: '0.5rem',
paddingBlockStart: 4,
paddingBlockEnd: 4,
borderRadius: '100rem',
background: theme.palette.featureStrategySegmentChipBackground,
color: 'white',
},
link: {
marginRight: '.5rem',
color: 'inherit',
textDecoration: 'none',
},
button: {
all: 'unset',
height: '1rem',
cursor: 'pointer',
},
icon: {
fontSize: '1rem',
},
}));

View File

@ -2,10 +2,9 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ISegment } from 'interfaces/segment'; import { ISegment } from 'interfaces/segment';
import { Clear, VisibilityOff, Visibility } from '@mui/icons-material'; import { Clear, VisibilityOff, Visibility } from '@mui/icons-material';
import { useStyles } from './FeatureStrategySegmentChip.styles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { constraintAccordionListId } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList'; import { constraintAccordionListId } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
import { Tooltip } from '@mui/material'; import { styled, Theme, Tooltip } from '@mui/material';
interface IFeatureStrategySegmentListProps { interface IFeatureStrategySegmentListProps {
segment: ISegment; segment: ISegment;
@ -14,14 +13,39 @@ interface IFeatureStrategySegmentListProps {
setPreview: React.Dispatch<React.SetStateAction<ISegment | undefined>>; setPreview: React.Dispatch<React.SetStateAction<ISegment | undefined>>;
} }
const StyledChip = styled('span')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(0.5),
paddingInlineStart: theme.spacing(2),
paddingInlineEnd: theme.spacing(1),
paddingBlockStart: theme.spacing(0.5),
paddingBlockEnd: theme.spacing(0.5),
borderRadius: '100rem',
background: theme.palette.featureStrategySegmentChipBackground,
color: theme.palette.text.tertiaryContrast,
}));
const StyledButton = styled('button')(({ theme }) => ({
all: 'unset',
height: theme.spacing(2),
cursor: 'pointer',
}));
const StyledLink = styled(Link)(({ theme }) => ({
marginRight: theme.spacing(1),
color: 'inherit',
textDecoration: 'none',
}));
const styledIcon = (theme: Theme) => ({ fontSize: theme.fontSizes.bodySize });
export const FeatureStrategySegmentChip = ({ export const FeatureStrategySegmentChip = ({
segment, segment,
setSegments, setSegments,
preview, preview,
setPreview, setPreview,
}: IFeatureStrategySegmentListProps) => { }: IFeatureStrategySegmentListProps) => {
const { classes: styles } = useStyles();
const onRemove = () => { const onRemove = () => {
setSegments(prev => { setSegments(prev => {
return prev.filter(s => s.id !== segment.id); return prev.filter(s => s.id !== segment.id);
@ -40,8 +64,8 @@ export const FeatureStrategySegmentChip = ({
const togglePreviewIcon = ( const togglePreviewIcon = (
<ConditionallyRender <ConditionallyRender
condition={segment === preview} condition={segment === preview}
show={<VisibilityOff titleAccess="Hide" className={styles.icon} />} show={<VisibilityOff titleAccess="Hide" sx={styledIcon} />}
elseShow={<Visibility titleAccess="Show" className={styles.icon} />} elseShow={<Visibility titleAccess="Show" sx={styledIcon} />}
/> />
); );
@ -51,34 +75,25 @@ export const FeatureStrategySegmentChip = ({
: 'Preview segment constraints'; : 'Preview segment constraints';
return ( return (
<span className={styles.chip}> <StyledChip>
<Link <StyledLink to={`/segments/edit/${segment.id}`} target="_blank">
to={`/segments/edit/${segment.id}`}
target="_blank"
className={styles.link}
>
{segment.name} {segment.name}
</Link> </StyledLink>
<Tooltip title={previewIconTooltip} arrow> <Tooltip title={previewIconTooltip} arrow>
<button <StyledButton
type="button" type="button"
onClick={onTogglePreview} onClick={onTogglePreview}
className={styles.button}
aria-expanded={segment === preview} aria-expanded={segment === preview}
aria-controls={constraintAccordionListId} aria-controls={constraintAccordionListId}
> >
{togglePreviewIcon} {togglePreviewIcon}
</button> </StyledButton>
</Tooltip> </Tooltip>
<Tooltip title="Remove segment" arrow> <Tooltip title="Remove segment" arrow>
<button <StyledButton type="button" onClick={onRemove}>
type="button" <Clear titleAccess="Remove" sx={styledIcon} />
onClick={onRemove} </StyledButton>
className={styles.button}
>
<Clear titleAccess="Remove" className={styles.icon} />
</button>
</Tooltip> </Tooltip>
</span> </StyledChip>
); );
}; };

View File

@ -1,29 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
title: {
margin: 0,
fontSize: theme.fontSizes.bodySize,
fontWeight: theme.fontWeight.thin,
},
list: {
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
},
and: {
fontSize: theme.fontSizes.smallerBody,
padding: theme.spacing(0.75, 1),
display: 'block',
marginTop: 'auto',
marginBottom: 'auto',
alignItems: 'center',
borderRadius: theme.shape.borderRadius,
lineHeight: 1,
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondaryContainer,
},
selectedSegmentsLabel: {
color: theme.palette.text.secondary,
},
}));

View File

@ -1,20 +1,42 @@
import React, { Fragment, useState } from 'react'; import React, { Fragment, useState } from 'react';
import { ISegment } from 'interfaces/segment'; import { ISegment } from 'interfaces/segment';
import { useStyles } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentList.styles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FeatureStrategySegmentChip } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentChip'; import { FeatureStrategySegmentChip } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentChip';
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem'; import { SegmentItem } from 'component/common/SegmentItem/SegmentItem';
import { styled } from '@mui/material';
interface IFeatureStrategySegmentListProps { interface IFeatureStrategySegmentListProps {
segments: ISegment[]; segments: ISegment[];
setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>; setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>;
} }
const StyledList = styled('div')(({ theme }) => ({
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(1),
}));
const StyledSelectedSegmentsLabel = styled('p')(({ theme }) => ({
color: theme.palette.text.secondary,
}));
const StyledAnd = styled('p')(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
padding: theme.spacing(0.75, 1),
display: 'block',
marginTop: 'auto',
marginBottom: 'auto',
alignItems: 'center',
borderRadius: theme.shape.borderRadius,
lineHeight: 1,
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondaryContainer,
}));
export const FeatureStrategySegmentList = ({ export const FeatureStrategySegmentList = ({
segments, segments,
setSegments, setSegments,
}: IFeatureStrategySegmentListProps) => { }: IFeatureStrategySegmentListProps) => {
const { classes: styles } = useStyles();
const [preview, setPreview] = useState<ISegment>(); const [preview, setPreview] = useState<ISegment>();
const lastSegmentIndex = segments.length - 1; const lastSegmentIndex = segments.length - 1;
@ -27,12 +49,12 @@ export const FeatureStrategySegmentList = ({
<ConditionallyRender <ConditionallyRender
condition={segments && segments.length > 0} condition={segments && segments.length > 0}
show={ show={
<p className={styles.selectedSegmentsLabel}> <StyledSelectedSegmentsLabel>
Selected Segments Selected Segments
</p> </StyledSelectedSegmentsLabel>
} }
/> />
<div className={styles.list}> <StyledList>
{segments.map((segment, i) => ( {segments.map((segment, i) => (
<Fragment key={segment.id}> <Fragment key={segment.id}>
<FeatureStrategySegmentChip <FeatureStrategySegmentChip
@ -43,11 +65,11 @@ export const FeatureStrategySegmentList = ({
/> />
<ConditionallyRender <ConditionallyRender
condition={i < lastSegmentIndex} condition={i < lastSegmentIndex}
show={<span className={styles.and}>AND</span>} show={<StyledAnd>AND</StyledAnd>}
/> />
</Fragment> </Fragment>
))} ))}
</div> </StyledList>
<ConditionallyRender <ConditionallyRender
condition={Boolean(preview)} condition={Boolean(preview)}
show={() => <SegmentItem segment={preview!} isExpanded />} show={() => <SegmentItem segment={preview!} isExpanded />}

View File

@ -1,11 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
status: {
color: theme.palette.success.dark,
fontSize: 'inherit',
},
stale: {
color: theme.palette.error.dark,
},
}));

View File

@ -1,23 +1,41 @@
import { VFC } from 'react'; import { VFC } from 'react';
import { Box, Typography } from '@mui/material'; import { Box, styled, Theme, Typography } from '@mui/material';
import { useStyles } from './FeatureStaleCell.styles'; import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
import classnames from 'classnames';
interface IFeatureStaleCellProps { interface IFeatureStaleCellProps {
value?: boolean; value?: boolean;
} }
const staleStatus = (theme: Theme) => ({
color: theme.palette.error.dark,
fontSize: 'inherit',
});
const activeStatus = (theme: Theme) => ({
color: theme.palette.success.dark,
fontSize: 'inherit',
});
const StyledBox = styled(Box)(({ theme }) => ({
padding: theme.spacing(1.5, 2),
}));
export const FeatureStaleCell: VFC<IFeatureStaleCellProps> = ({ value }) => { export const FeatureStaleCell: VFC<IFeatureStaleCellProps> = ({ value }) => {
const { classes: styles } = useStyles();
return ( return (
<Box sx={{ py: 1.5, px: 2 }}> <StyledBox>
<Typography <ConditionallyRender
component="span" condition={Boolean(value)}
className={classnames(styles.status, value && styles.stale)} show={
data-loading <Typography component="span" sx={staleStatus} data-loading>
> Stale
{value ? 'Stale' : 'Active'} </Typography>
</Typography> }
</Box> elseShow={
<Typography component="span" sx={activeStatus} data-loading>
Active
</Typography>
}
/>
</StyledBox>
); );
}; };

View File

@ -1,9 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
borderRadius: '12.5px',
backgroundColor: theme.palette.background.paper,
padding: '2rem',
},
}));

View File

@ -1,12 +1,17 @@
import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { useStyles } from './FeatureLog.styles';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { EventLog } from 'component/events/EventLog/EventLog'; import { EventLog } from 'component/events/EventLog/EventLog';
import { styled } from '@mui/material';
const StyledContainer = styled('div')(({ theme }) => ({
borderRadius: theme.spacing(1.5),
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(4),
}));
const FeatureLog = () => { const FeatureLog = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const { classes: styles } = useStyles();
const { feature } = useFeature(projectId, featureId); const { feature } = useFeature(projectId, featureId);
if (!feature.name) { if (!feature.name) {
@ -14,14 +19,14 @@ const FeatureLog = () => {
} }
return ( return (
<div className={styles.container}> <StyledContainer>
<EventLog <EventLog
title="Event log" title="Event log"
project={projectId} project={projectId}
feature={featureId} feature={featureId}
displayInline displayInline
/> />
</div> </StyledContainer>
); );
}; };

View File

@ -1,9 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
mobileMarginTop: {
[theme.breakpoints.down('md')]: {
marginTop: theme.spacing(2),
},
},
}));

View File

@ -6,23 +6,27 @@ import {
FeatureMetricsHours, FeatureMetricsHours,
} from './FeatureMetricsHours/FeatureMetricsHours'; } from './FeatureMetricsHours/FeatureMetricsHours';
import { IFeatureMetricsRaw } from 'interfaces/featureToggle'; import { IFeatureMetricsRaw } from 'interfaces/featureToggle';
import { Grid } from '@mui/material'; import { Grid, styled } from '@mui/material';
import { FeatureMetricsContent } from './FeatureMetricsContent/FeatureMetricsContent'; import { FeatureMetricsContent } from './FeatureMetricsContent/FeatureMetricsContent';
import { useQueryStringNumberState } from 'hooks/useQueryStringNumberState'; import { useQueryStringNumberState } from 'hooks/useQueryStringNumberState';
import { useQueryStringState } from 'hooks/useQueryStringState'; import { useQueryStringState } from 'hooks/useQueryStringState';
import { FeatureMetricsChips } from './FeatureMetricsChips/FeatureMetricsChips'; import { FeatureMetricsChips } from './FeatureMetricsChips/FeatureMetricsChips';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './FeatureMetrics.styles';
import { usePageTitle } from 'hooks/usePageTitle'; import { usePageTitle } from 'hooks/usePageTitle';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
const StyledContainer = styled('div')(({ theme }) => ({
[theme.breakpoints.down('md')]: {
marginTop: theme.spacing(2),
},
}));
export const FeatureMetrics = () => { export const FeatureMetrics = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const environments = useFeatureMetricsEnvironments(projectId, featureId); const environments = useFeatureMetricsEnvironments(projectId, featureId);
const applications = useFeatureMetricsApplications(featureId); const applications = useFeatureMetricsApplications(featureId);
const { classes: styles } = useStyles();
usePageTitle('Metrics'); usePageTitle('Metrics');
const [hoursBack = FEATURE_METRIC_HOURS_BACK_MAX, setHoursBack] = const [hoursBack = FEATURE_METRIC_HOURS_BACK_MAX, setHoursBack] =
@ -91,12 +95,12 @@ export const FeatureMetrics = () => {
/> />
</Grid> </Grid>
<Grid item xs={12} md={2}> <Grid item xs={12} md={2}>
<div className={styles.mobileMarginTop}> <StyledContainer>
<FeatureMetricsHours <FeatureMetricsHours
hoursBack={hoursBack} hoursBack={hoursBack}
setHoursBack={setHoursBack} setHoursBack={setHoursBack}
/> />
</div> </StyledContainer>
</Grid> </Grid>
</Grid> </Grid>
<FeatureMetricsContent <FeatureMetricsContent

View File

@ -1,25 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
title: {
margin: 0,
marginBottom: '.5rem',
fontSize: theme.fontSizes.smallBody,
fontWeight: theme.fontWeight.thin,
color: theme.palette.grey[800],
},
list: {
display: 'flex',
flexWrap: 'wrap',
gap: '.5rem',
listStyleType: 'none',
padding: 0,
minHeight: '100%',
},
item: {
'& > [aria-pressed=true]': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
},
},
}));

View File

@ -1,7 +1,6 @@
import { Chip } from '@mui/material'; import { Chip, styled } from '@mui/material';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useStyles } from './FeatureMetricsChips.styles'; import { focusable } from 'themes/themeStyles';
import { useThemeStyles } from 'themes/themeStyles';
interface IFeatureMetricsChipsProps { interface IFeatureMetricsChipsProps {
title: string; title: string;
@ -10,15 +9,36 @@ interface IFeatureMetricsChipsProps {
setValue: (value: string) => void; setValue: (value: string) => void;
} }
const StyledTitle = styled('h2')(({ theme }) => ({
margin: 0,
marginBottom: theme.spacing(1),
fontSize: theme.fontSizes.smallBody,
fontWeight: theme.fontWeight.thin,
color: theme.palette.text.secondary,
}));
const StyledList = styled('ul')(({ theme }) => ({
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(1),
listStyleType: 'none',
padding: 0,
minHeight: '100%',
}));
const StyledItem = styled('li')(({ theme }) => ({
'& > [aria-pressed=true]': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
},
}));
export const FeatureMetricsChips = ({ export const FeatureMetricsChips = ({
title, title,
values, values,
value, value,
setValue, setValue,
}: IFeatureMetricsChipsProps) => { }: IFeatureMetricsChipsProps) => {
const { classes: themeStyles } = useThemeStyles();
const { classes: styles } = useStyles();
const onClick = (value: string) => () => { const onClick = (value: string) => () => {
if (values.has(value)) { if (values.has(value)) {
setValue(value); setValue(value);
@ -33,19 +53,19 @@ export const FeatureMetricsChips = ({
return ( return (
<div> <div>
<h2 className={styles.title}>{title}</h2> <StyledTitle>{title}</StyledTitle>
<ul className={styles.list}> <StyledList>
{sortedValues.map(val => ( {sortedValues.map(val => (
<li key={val} className={styles.item}> <StyledItem key={val}>
<Chip <Chip
label={val} label={val}
onClick={onClick(val)} onClick={onClick(val)}
aria-pressed={val === value} aria-pressed={val === value}
className={themeStyles.focusable} sx={focusable}
/> />
</li> </StyledItem>
))} ))}
</ul> </StyledList>
</div> </div>
); );
}; };

View File

@ -1,32 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
item: {
padding: theme.spacing(2),
background: 'transparent',
borderRadius: theme.spacing(2),
textAlign: 'center',
[theme.breakpoints.up('md')]: {
padding: theme.spacing(4),
},
},
title: {
margin: 0,
fontSize: theme.fontSizes.bodySize,
fontWeight: theme.fontWeight.thin,
},
value: {
fontSize: '2.25rem',
fontWeight: theme.fontWeight.bold,
color: theme.palette.primary.main,
},
text: {
margin: '.5rem 0 0 0',
padding: '1rem 0 0 0',
borderTopWidth: 1,
borderTopStyle: 'solid',
borderTopColor: theme.palette.grey[300],
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.grey[800],
},
}));

View File

@ -1,6 +1,5 @@
import { calculatePercentage } from 'utils/calculatePercentage'; import { calculatePercentage } from 'utils/calculatePercentage';
import { useStyles } from './FeatureMetricsStats.styles'; import { Grid, styled } from '@mui/material';
import { Grid } from '@mui/material';
import { PrettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber'; import { PrettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber';
export interface IFeatureMetricsStatsProps { export interface IFeatureMetricsStatsProps {
@ -11,6 +10,38 @@ export interface IFeatureMetricsStatsProps {
tableSectionId?: string; tableSectionId?: string;
} }
const StyledItem = styled('article')(({ theme }) => ({
padding: theme.spacing(2),
background: 'transparent',
borderRadius: theme.spacing(2),
textAlign: 'center',
[theme.breakpoints.up('md')]: {
padding: theme.spacing(4),
},
}));
const StyledTitle = styled('h3')(({ theme }) => ({
margin: 0,
fontSize: theme.fontSizes.bodySize,
fontWeight: theme.fontWeight.thin,
}));
const StyledValue = styled('p')(({ theme }) => ({
fontSize: '2.25rem',
fontWeight: theme.fontWeight.bold,
color: theme.palette.primary.main,
}));
const StyledText = styled('p')(({ theme }) => ({
margin: theme.spacing(1, 0, 0, 0),
padding: theme.spacing(2, 0, 0, 0),
borderTopWidth: '1px',
borderTopStyle: 'solid',
borderTopColor: theme.palette.dividerAlternative,
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.text.secondary,
}));
export const FeatureMetricsStats = ({ export const FeatureMetricsStats = ({
totalYes, totalYes,
totalNo, totalNo,
@ -18,8 +49,6 @@ export const FeatureMetricsStats = ({
statsSectionId, statsSectionId,
tableSectionId, tableSectionId,
}: IFeatureMetricsStatsProps) => { }: IFeatureMetricsStatsProps) => {
const { classes: styles } = useStyles();
const hoursSuffix = const hoursSuffix =
hoursBack === 1 ? 'in the last hour' : `in the last ${hoursBack} hours`; hoursBack === 1 ? 'in the last hour' : `in the last ${hoursBack} hours`;
@ -33,40 +62,40 @@ export const FeatureMetricsStats = ({
component="section" component="section"
> >
<Grid item xs={12} sm={4}> <Grid item xs={12} sm={4}>
<article className={styles.item}> <StyledItem>
<h3 className={styles.title}>Exposure</h3> <StyledTitle>Exposure</StyledTitle>
<p className={styles.value}> <StyledValue>
<PrettifyLargeNumber value={totalYes} /> <PrettifyLargeNumber value={totalYes} />
</p> </StyledValue>
<p className={styles.text}> <StyledText>
Total exposure of the feature in the environment{' '} Total exposure of the feature in the environment{' '}
{hoursSuffix}. {hoursSuffix}.
</p> </StyledText>
</article> </StyledItem>
</Grid> </Grid>
<Grid item xs={12} sm={4}> <Grid item xs={12} sm={4}>
<article className={styles.item}> <StyledItem>
<h3 className={styles.title}>Exposure %</h3> <StyledTitle>Exposure %</StyledTitle>
<p className={styles.value}> <StyledValue>
{calculatePercentage(totalYes + totalNo, totalYes)}% {calculatePercentage(totalYes + totalNo, totalYes)}%
</p> </StyledValue>
<p className={styles.text}> <StyledText>
% total exposure of the feature in the environment{' '} % total exposure of the feature in the environment{' '}
{hoursSuffix}. {hoursSuffix}.
</p> </StyledText>
</article> </StyledItem>
</Grid> </Grid>
<Grid item xs={12} sm={4}> <Grid item xs={12} sm={4}>
<article className={styles.item}> <StyledItem>
<h3 className={styles.title}>Requests</h3> <StyledTitle>Requests</StyledTitle>
<p className={styles.value}> <StyledValue>
<PrettifyLargeNumber value={totalYes + totalNo} /> <PrettifyLargeNumber value={totalYes + totalNo} />
</p> </StyledValue>
<p className={styles.text}> <StyledText>
Total requests for the feature in the environment{' '} Total requests for the feature in the environment{' '}
{hoursSuffix}. {hoursSuffix}.
</p> </StyledText>
</article> </StyledItem>
</Grid> </Grid>
</Grid> </Grid>
); );

View File

@ -1,7 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
featureId: {
wordBreak: 'break-all',
},
}));

View File

@ -1,17 +1,18 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { getCreateTogglePath } from 'utils/routePathHelpers'; import { getCreateTogglePath } from 'utils/routePathHelpers';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useStyles } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound.styles';
import { useFeaturesArchive } from 'hooks/api/getters/useFeaturesArchive/useFeaturesArchive'; import { useFeaturesArchive } from 'hooks/api/getters/useFeaturesArchive/useFeaturesArchive';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { styled } from '@mui/material';
const StyledFeatureId = styled('strong')({
wordBreak: 'break-all',
});
export const FeatureNotFound = () => { export const FeatureNotFound = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const { archivedFeatures } = useFeaturesArchive(); const { archivedFeatures } = useFeaturesArchive();
const { classes: styles } = useStyles();
const { uiConfig } = useUiConfig();
const createFeatureTogglePath = getCreateTogglePath(projectId, { const createFeatureTogglePath = getCreateTogglePath(projectId, {
name: featureId, name: featureId,
@ -28,8 +29,7 @@ export const FeatureNotFound = () => {
if (isArchived) { if (isArchived) {
return ( return (
<p> <p>
The feature{' '} The feature <StyledFeatureId>{featureId}</StyledFeatureId> has
<strong className={styles.featureId}>{featureId}</strong> has
been archived. You can find it on the{' '} been archived. You can find it on the{' '}
<Link to={`/projects/${projectId}/archive`}> <Link to={`/projects/${projectId}/archive`}>
project archive page project archive page
@ -41,8 +41,7 @@ export const FeatureNotFound = () => {
return ( return (
<p> <p>
The feature{' '} The feature <StyledFeatureId>{featureId}</StyledFeatureId> does not
<strong className={styles.featureId}>{featureId}</strong> does not
exist. Would you like to{' '} exist. Would you like to{' '}
<Link to={createFeatureTogglePath}>create it</Link>? <Link to={createFeatureTogglePath}>create it</Link>?
</p> </p>

View File

@ -1,9 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
dialogFormContent: {
['& > *']: {
margin: '0.5rem 0',
},
},
}));

View File

@ -2,7 +2,6 @@ import { styled, Typography } from '@mui/material';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import Input from 'component/common/Input/Input'; import Input from 'component/common/Input/Input';
import { useStyles } from './AddTagDialog.styles';
import { trim } from 'component/common/util'; import { trim } from 'component/common/util';
import TagSelect from 'component/common/TagSelect/TagSelect'; import TagSelect from 'component/common/TagSelect/TagSelect';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
@ -28,9 +27,14 @@ interface IDefaultTag {
[index: string]: string; [index: string]: string;
} }
const StyledDialogFormContent = styled('section')(({ theme }) => ({
['& > *']: {
margin: '0.5rem 0',
},
}));
const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => { const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
const DEFAULT_TAG: IDefaultTag = { type: 'simple', value: '' }; const DEFAULT_TAG: IDefaultTag = { type: 'simple', value: '' };
const { classes: styles } = useStyles();
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const { addTagToFeature, loading } = useFeatureApi(); const { addTagToFeature, loading } = useFeatureApi();
const { tags, refetch } = useTags(featureId); const { tags, refetch } = useTags(featureId);
@ -106,7 +110,7 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
Tags allow you to group features together Tags allow you to group features together
</Typography> </Typography>
<form id={formId} onSubmit={onSubmit}> <form id={formId} onSubmit={onSubmit}>
<section className={styles.dialogFormContent}> <StyledDialogFormContent>
<TagSelect <TagSelect
autoFocus autoFocus
name="type" name="type"
@ -126,7 +130,7 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
} }
required required
/> />
</section> </StyledDialogFormContent>
</form> </form>
</> </>
</Dialogue> </Dialogue>

View File

@ -1,26 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
display: 'flex',
width: '100%',
[theme.breakpoints.down(1000)]: {
flexDirection: 'column',
},
},
mainContent: {
display: 'flex',
flexDirection: 'column',
width: `calc(100% - (350px + 1rem))`,
[theme.breakpoints.down(1000)]: {
width: '100%',
},
},
trafficContainer: {
display: 'flex',
flexWrap: 'wrap',
[theme.breakpoints.down(1000)]: {
marginTop: '1rem',
},
},
}));

View File

@ -1,5 +1,4 @@
import FeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData'; import FeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData';
import { useStyles } from './FeatureOverview.styles';
import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments'; import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
import FeatureOverviewEnvSwitches from './FeatureOverviewEnvSwitches/FeatureOverviewEnvSwitches'; import FeatureOverviewEnvSwitches from './FeatureOverviewEnvSwitches/FeatureOverviewEnvSwitches';
import { Routes, Route, useNavigate } from 'react-router-dom'; import { Routes, Route, useNavigate } from 'react-router-dom';
@ -15,10 +14,27 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel'; import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel';
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments'; import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
import { styled } from '@mui/material';
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
width: '100%',
[theme.breakpoints.down(1000)]: {
flexDirection: 'column',
},
}));
const StyledMainContent = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
width: `calc(100% - (350px + 1rem))`,
[theme.breakpoints.down(1000)]: {
width: '100%',
},
}));
const FeatureOverview = () => { const FeatureOverview = () => {
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { classes: styles } = useStyles();
const navigate = useNavigate(); const navigate = useNavigate();
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
@ -29,7 +45,7 @@ const FeatureOverview = () => {
usePageTitle(featureId); usePageTitle(featureId);
return ( return (
<div className={styles.container}> <StyledContainer>
<div> <div>
<FeatureOverviewMetaData /> <FeatureOverviewMetaData />
<ConditionallyRender <ConditionallyRender
@ -43,9 +59,9 @@ const FeatureOverview = () => {
elseShow={<FeatureOverviewEnvSwitches />} elseShow={<FeatureOverviewEnvSwitches />}
/> />
</div> </div>
<div className={styles.mainContent}> <StyledMainContent>
<FeatureOverviewEnvironments /> <FeatureOverviewEnvironments />
</div> </StyledMainContent>
<Routes> <Routes>
<Route <Route
path="strategies/create" path="strategies/create"
@ -72,7 +88,7 @@ const FeatureOverview = () => {
} }
/> />
</Routes> </Routes>
</div> </StyledContainer>
); );
}; };

View File

@ -1,58 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
borderRadius: theme.shape.borderRadiusLarge,
boxShadow: 'none',
display: 'flex',
},
header: {
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadiusLarge,
marginBottom: '1rem',
},
toggleInfoContainer: {
display: 'flex',
alignItems: 'center',
},
toolbarContainer: {
flexShrink: 0,
display: 'flex',
},
innerContainer: {
padding: theme.spacing(2, 4, 2, 2),
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
[theme.breakpoints.down(500)]: {
flexDirection: 'column',
},
},
separator: {
width: '100%',
backgroundColor: theme.palette.grey[200],
height: '1px',
},
tabContainer: {
padding: '0 2rem',
},
tabButton: {
textTransform: 'none',
width: 'auto',
fontSize: '1rem',
padding: '0 !important',
[theme.breakpoints.up('md')]: {
minWidth: 160,
},
},
featureViewHeader: {
fontSize: theme.fontSizes.mainHeader,
fontWeight: 'normal',
display: 'flex',
alignItems: 'center',
wordBreak: 'break-all',
},
statusContainer: {
marginLeft: '0.5rem',
},
}));

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Tab, Tabs, useMediaQuery } from '@mui/material'; import { styled, Tab, Tabs, useMediaQuery } from '@mui/material';
import { Archive, FileCopy, Label, WatchLater } from '@mui/icons-material'; import { Archive, FileCopy, Label, WatchLater } from '@mui/icons-material';
import { import {
Link, Link,
@ -20,7 +20,6 @@ import FeatureLog from './FeatureLog/FeatureLog';
import FeatureOverview from './FeatureOverview/FeatureOverview'; import FeatureOverview from './FeatureOverview/FeatureOverview';
import FeatureVariants from './FeatureVariants/FeatureVariants'; import FeatureVariants from './FeatureVariants/FeatureVariants';
import { FeatureMetrics } from './FeatureMetrics/FeatureMetrics'; import { FeatureMetrics } from './FeatureMetrics/FeatureMetrics';
import { useStyles } from './FeatureView.styles';
import { FeatureSettings } from './FeatureSettings/FeatureSettings'; import { FeatureSettings } from './FeatureSettings/FeatureSettings';
import useLoading from 'hooks/useLoading'; import useLoading from 'hooks/useLoading';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
@ -33,6 +32,60 @@ import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/Feat
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton'; import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
const StyledHeader = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadiusLarge,
marginBottom: theme.spacing(2),
}));
const StyledInnerContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(2, 4, 2, 2),
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
[theme.breakpoints.down(500)]: {
flexDirection: 'column',
},
}));
const StyledToggleInfoContainer = styled('div')({
display: 'flex',
alignItems: 'center',
});
const StyledFeatureViewHeader = styled('h1')(({ theme }) => ({
fontSize: theme.fontSizes.mainHeader,
fontWeight: 'normal',
display: 'flex',
alignItems: 'center',
wordBreak: 'break-all',
}));
const StyledToolbarContainer = styled('div')({
flexShrink: 0,
display: 'flex',
});
const StyledSeparator = styled('div')(({ theme }) => ({
width: '100%',
backgroundColor: theme.palette.tertiary.light,
height: '1px',
}));
const StyledTabContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 4),
}));
const StyledTabButton = styled(Tab)(({ theme }) => ({
textTransform: 'none',
width: 'auto',
fontSize: theme.fontSizes.bodySize,
padding: '0 !important',
[theme.breakpoints.up('md')]: {
minWidth: 160,
},
}));
export const FeatureView = () => { export const FeatureView = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
@ -50,7 +103,6 @@ export const FeatureView = () => {
featureId featureId
); );
const { classes: styles } = useStyles();
const navigate = useNavigate(); const navigate = useNavigate();
const { pathname } = useLocation(); const { pathname } = useLocation();
const ref = useLoading(loading); const ref = useLoading(loading);
@ -98,23 +150,23 @@ export const FeatureView = () => {
return ( return (
<div ref={ref}> <div ref={ref}>
<div className={styles.header}> <StyledHeader>
<div className={styles.innerContainer}> <StyledInnerContainer>
<div className={styles.toggleInfoContainer}> <StyledToggleInfoContainer>
<FavoriteIconButton <FavoriteIconButton
onClick={onFavorite} onClick={onFavorite}
isFavorite={feature?.favorite} isFavorite={feature?.favorite}
/> />
<h1 className={styles.featureViewHeader} data-loading> <StyledFeatureViewHeader data-loading>
{feature.name}{' '} {feature.name}{' '}
</h1> </StyledFeatureViewHeader>
<ConditionallyRender <ConditionallyRender
condition={!smallScreen} condition={!smallScreen}
show={<FeatureStatusChip stale={feature?.stale} />} show={<FeatureStatusChip stale={feature?.stale} />}
/> />
</div> </StyledToggleInfoContainer>
<div className={styles.toolbarContainer}> <StyledToolbarContainer>
<PermissionIconButton <PermissionIconButton
permission={CREATE_FEATURE} permission={CREATE_FEATURE}
projectId={projectId} projectId={projectId}
@ -158,27 +210,26 @@ export const FeatureView = () => {
> >
<Label /> <Label />
</PermissionIconButton> </PermissionIconButton>
</div> </StyledToolbarContainer>
</div> </StyledInnerContainer>
<div className={styles.separator} /> <StyledSeparator />
<div className={styles.tabContainer}> <StyledTabContainer>
<Tabs <Tabs
value={activeTab.path} value={activeTab.path}
indicatorColor="primary" indicatorColor="primary"
textColor="primary" textColor="primary"
> >
{tabData.map(tab => ( {tabData.map(tab => (
<Tab <StyledTabButton
key={tab.title} key={tab.title}
label={tab.title} label={tab.title}
value={tab.path} value={tab.path}
onClick={() => navigate(tab.path)} onClick={() => navigate(tab.path)}
className={styles.tabButton}
/> />
))} ))}
</Tabs> </Tabs>
</div> </StyledTabContainer>
</div> </StyledHeader>
<Routes> <Routes>
<Route path="metrics" element={<FeatureMetrics />} /> <Route path="metrics" element={<FeatureMetrics />} />
<Route path="logs" element={<FeatureLog />} /> <Route path="logs" element={<FeatureLog />} />

View File

@ -22,7 +22,7 @@ import { ReactComponent as UnleashLogoWhite } from 'assets/img/logoWithWhiteText
import { DrawerMenu } from './DrawerMenu/DrawerMenu'; import { DrawerMenu } from './DrawerMenu/DrawerMenu';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { flexRow, focusable, useThemeStyles } from 'themes/themeStyles'; import { flexRow, focusable } from 'themes/themeStyles';
import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { IPermission } from 'interfaces/user'; import { IPermission } from 'interfaces/user';
import { NavigationMenu } from './NavigationMenu/NavigationMenu'; import { NavigationMenu } from './NavigationMenu/NavigationMenu';
@ -97,9 +97,7 @@ const styledIconProps = (theme: Theme) => ({
color: theme.palette.neutral.main, color: theme.palette.neutral.main,
}); });
const StyledLink = styled(Link)(({ theme }) => ({ const StyledLink = styled(Link)(({ theme }) => focusable(theme));
...focusable(theme),
}));
const StyledIconButton = styled(IconButton)(({ theme }) => ({ const StyledIconButton = styled(IconButton)(({ theme }) => ({
...focusable(theme), ...focusable(theme),
@ -158,9 +156,7 @@ const Header: VFC = () => {
<StyledContainer> <StyledContainer>
<Tooltip title="Menu" arrow> <Tooltip title="Menu" arrow>
<IconButton <IconButton
sx={theme => ({ sx={{ color: theme => theme.palette.text.primary }}
color: theme.palette.text.tertiaryContrast,
})}
onClick={toggleDrawer} onClick={toggleDrawer}
aria-controls="header-drawer" aria-controls="header-drawer"
aria-expanded={openDrawer} aria-expanded={openDrawer}