1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01: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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { styled } from '@mui/material';
interface IContextFormChipProps {
label: string;
@ -8,27 +8,62 @@ interface IContextFormChipProps {
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 = ({
label,
description,
onRemove,
}: IContextFormChipProps) => {
const { classes: styles } = useStyles();
return (
<li className={styles.container}>
<StyledContainer>
<div>
<div className={styles.label}>{label}</div>
<StyledLabel>{label}</StyledLabel>
<ConditionallyRender
condition={Boolean(description)}
show={() => (
<div className={styles.description}>{description}</div>
<StyledDescription>{description}</StyledDescription>
)}
/>
</div>
<button onClick={onRemove} className={styles.button}>
<StyledButton onClick={onRemove}>
<Cancel titleAccess="Remove" />
</button>
</li>
</StyledButton>
</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 { styled } from '@mui/material';
export const ContextFormChipList: React.FC = ({ children }) => {
const { classes: styles } = useStyles();
const StyledContainer = styled('ul')(({ theme }) => ({
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 { TextField, Button, Switch, Typography } from '@mui/material';
import { useStyles } from './ContextForm.styles';
import {
TextField,
Button,
Switch,
Typography,
styled,
Theme,
} from '@mui/material';
import React, { useState, useEffect } from 'react';
import { Add } from '@mui/icons-material';
import { ILegalValue } from 'interfaces/context';
@ -27,6 +33,52 @@ interface IContextForm {
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> = ({
children,
handleSubmit,
@ -45,7 +97,6 @@ export const ContextForm: React.FC<IContextForm> = ({
setErrors,
clearErrors,
}) => {
const { classes: styles } = useStyles();
const [value, setValue] = useState('');
const [valueDesc, setValueDesc] = useState('');
const [valueFocused, setValueFocused] = useState(false);
@ -104,13 +155,13 @@ export const ContextForm: React.FC<IContextForm> = ({
};
return (
<form onSubmit={onSubmit} className={styles.form}>
<div className={styles.container}>
<p className={styles.inputDescription}>
<StyledForm onSubmit={onSubmit}>
<StyledContainer>
<StyledInputDescription>
What is your context name?
</p>
</StyledInputDescription>
<Input
className={styles.input}
sx={styledInput}
label="Context name"
value={contextName}
disabled={mode === 'Edit'}
@ -121,11 +172,11 @@ export const ContextForm: React.FC<IContextForm> = ({
onBlur={validateContext}
autoFocus
/>
<p className={styles.inputDescription}>
<StyledInputDescription>
What is this context for?
</p>
</StyledInputDescription>
<TextField
className={styles.input}
sx={styledInput}
label="Context description (optional)"
variant="outlined"
multiline
@ -134,14 +185,14 @@ export const ContextForm: React.FC<IContextForm> = ({
size="small"
onChange={e => setContextDesc(e.target.value)}
/>
<p className={styles.inputDescription}>
<StyledInputDescription>
Which values do you want to allow?
</p>
<div className={styles.tagContainer}>
</StyledInputDescription>
<StyledTagContainer>
<TextField
label="Legal value (optional)"
name="value"
className={styles.tagInput}
sx={{ gridColumn: 1 }}
value={value}
error={Boolean(errors.tag)}
helperText={errors.tag}
@ -155,7 +206,7 @@ export const ContextForm: React.FC<IContextForm> = ({
/>
<TextField
label="Value description (optional)"
className={styles.tagInput}
sx={{ gridColumn: 1 }}
value={valueDesc}
variant="outlined"
size="small"
@ -166,7 +217,7 @@ export const ContextForm: React.FC<IContextForm> = ({
inputProps={{ maxLength: 100 }}
/>
<Button
className={styles.tagButton}
sx={{ gridColumn: 2 }}
startIcon={<Add />}
onClick={addLegalValue}
variant="outlined"
@ -175,7 +226,7 @@ export const ContextForm: React.FC<IContextForm> = ({
>
Add
</Button>
</div>
</StyledTagContainer>
<ContextFormChipList>
{legalValues.map(legalValue => {
return (
@ -188,7 +239,7 @@ export const ContextForm: React.FC<IContextForm> = ({
);
})}
</ContextFormChipList>
<p className={styles.inputHeader}>Custom stickiness</p>
<StyledInputHeader>Custom stickiness</StyledInputHeader>
<p>
By enabling stickiness on this context field you can use it
together with the flexible-rollout strategy. This will
@ -203,21 +254,21 @@ export const ContextForm: React.FC<IContextForm> = ({
Read more
</a>
</p>
<div className={styles.switchContainer}>
<StyledSwitchContainer>
<Switch
checked={stickiness}
value={stickiness}
onChange={() => setStickiness(!stickiness)}
/>
<Typography>{stickiness ? 'On' : 'Off'}</Typography>
</div>
</div>
<div className={styles.buttonContainer}>
</StyledSwitchContainer>
</StyledContainer>
<StyledButtonContainer>
{children}
<Button onClick={onCancel} className={styles.cancelButton}>
<StyledCancelButton onClick={onCancel}>
Cancel
</Button>
</div>
</form>
</StyledCancelButton>
</StyledButtonContainer>
</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 { useStyles } from './EnvironmentForm.styles';
import { Button, styled } from '@mui/material';
import React from 'react';
import Input from 'component/common/Input/Input';
import EnvironmentTypeSelector from './EnvironmentTypeSelector/EnvironmentTypeSelector';
@ -18,6 +17,40 @@ interface IEnvironmentForm {
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> = ({
children,
handleSubmit,
@ -31,18 +64,15 @@ const EnvironmentForm: React.FC<IEnvironmentForm> = ({
mode,
clearErrors,
}) => {
const { classes: styles } = useStyles();
return (
<form onSubmit={handleSubmit} className={styles.form}>
<h3 className={styles.formHeader}>Environment information</h3>
<StyledForm onSubmit={handleSubmit}>
<StyledFormHeader>Environment information</StyledFormHeader>
<div className={styles.container}>
<p className={styles.inputDescription}>
<StyledContainer>
<StyledInputDescription>
What is your environment name? (Can't be changed later)
</p>
<Input
className={styles.input}
</StyledInputDescription>
<StyledInput
label="Environment name"
value={name}
onChange={e => setName(trim(e.target.value))}
@ -54,21 +84,21 @@ const EnvironmentForm: React.FC<IEnvironmentForm> = ({
autoFocus
/>
<p className={styles.inputDescription}>
<StyledInputDescription>
What type of environment do you want to create?
</p>
</StyledInputDescription>
<EnvironmentTypeSelector
onChange={e => setType(e.currentTarget.value)}
value={type}
/>
</div>
<div className={styles.buttonContainer}>
</StyledContainer>
<StyledButtonContainer>
{children}
<Button onClick={handleCancel} className={styles.cancelButton}>
<StyledCancelButton onClick={handleCancel}>
Cancel
</Button>
</div>
</form>
</StyledCancelButton>
</StyledButtonContainer>
</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,
Radio,
RadioGroup,
styled,
} from '@mui/material';
import { useStyles } from './EnvironmentTypeSelector.styles';
import React from 'react';
interface IEnvironmentTypeSelectorProps {
@ -12,20 +12,23 @@ interface IEnvironmentTypeSelectorProps {
value: string;
}
const StyledRadioGroup = styled(RadioGroup)({
flexDirection: 'row',
});
const StyledRadioButtonGroup = styled('div')({
display: 'flex',
flexDirection: 'column',
});
const EnvironmentTypeSelector = ({
onChange,
value,
}: IEnvironmentTypeSelectorProps) => {
const { classes: styles } = useStyles();
return (
<FormControl component="fieldset">
<RadioGroup
data-loading
value={value}
onChange={onChange}
className={styles.radioGroup}
>
<div className={styles.radioBtnGroup}>
<StyledRadioGroup data-loading value={value} onChange={onChange}>
<StyledRadioButtonGroup>
<FormControlLabel
value="development"
label="Development"
@ -36,8 +39,8 @@ const EnvironmentTypeSelector = ({
label="Test"
control={<Radio />}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
</StyledRadioButtonGroup>
<StyledRadioButtonGroup>
<FormControlLabel
value="preproduction"
label="Pre production"
@ -48,8 +51,8 @@ const EnvironmentTypeSelector = ({
label="Production"
control={<Radio />}
/>
</div>
</RadioGroup>
</StyledRadioButtonGroup>
</StyledRadioGroup>
</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,
FormControl,
FormControlLabel,
styled,
Switch,
Theme,
Typography,
} from '@mui/material';
import { useStyles } from './FeatureForm.styles';
import FeatureTypeSelect from '../FeatureView/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
import { CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from 'utils/testIds';
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
@ -39,6 +40,66 @@ interface IFeatureToggleForm {
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> = ({
children,
type,
@ -58,7 +119,6 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
mode,
clearErrors,
}) => {
const { classes: styles } = useStyles();
const { featureTypes } = useFeatureTypes();
const navigate = useNavigate();
const { permissions } = useAuthPermissions();
@ -69,15 +129,14 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
};
return (
<form onSubmit={handleSubmit} className={styles.form}>
<div className={styles.container}>
<p className={styles.inputDescription}>
<StyledForm onSubmit={handleSubmit}>
<StyledContainer>
<StyledInputDescription>
What would you like to call your toggle?
</p>
<Input
</StyledInputDescription>
<StyledInput
autoFocus
disabled={mode === 'Edit'}
className={styles.input}
label="Name"
id="feature-toggle-name"
error={Boolean(errors.name)}
@ -88,10 +147,11 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
data-testid={CF_NAME_ID}
onBlur={validateToggleName}
/>
<p className={styles.inputDescription}>
<StyledInputDescription>
What kind of feature toggle do you want?
</p>
</StyledInputDescription>
<FeatureTypeSelect
sx={styledSelectInput}
value={type}
onChange={setType}
label={'Toggle type'}
@ -99,17 +159,16 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
editable
data-testid={CF_TYPE_ID}
IconComponent={KeyboardArrowDownOutlined}
className={styles.selectInput}
/>
<p className={styles.typeDescription}>
<StyledTypeDescription>
{renderToggleDescription()}
</p>
</StyledTypeDescription>
<ConditionallyRender
condition={editable}
show={
<p className={styles.inputDescription}>
<StyledInputDescription>
In which project do you want to save the toggle?
</p>
</StyledInputDescription>
}
/>
<FeatureProjectSelect
@ -123,14 +182,13 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
enabled={editable}
filter={projectFilterGenerator(permissions, CREATE_FEATURE)}
IconComponent={KeyboardArrowDownOutlined}
className={styles.selectInput}
sx={styledSelectInput}
/>
<p className={styles.inputDescription}>
<StyledInputDescription>
How would you describe your feature toggle?
</p>
<Input
className={styles.input}
</StyledInputDescription>
<StyledInput
multiline
rows={4}
label="Description"
@ -139,10 +197,10 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
data-testid={CF_DESC_ID}
onChange={e => setDescription(e.target.value)}
/>
<FormControl className={styles.input}>
<StyledFormControl>
<Typography
variant="subtitle1"
className={styles.roleSubtitle}
sx={styledTypography}
data-loading
component="h2"
>
@ -160,7 +218,7 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
the impression data documentation
</a>
</p>
<div className={styles.flexRow}>
<StyledRow>
<FormControlLabel
labelPlacement="start"
style={{ marginLeft: 0 }}
@ -175,17 +233,17 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
}
label="Enable impression data"
/>
</div>
</FormControl>
</div>
</StyledRow>
</StyledFormControl>
</StyledContainer>
<div className={styles.buttonContainer}>
<StyledButtonContainer>
{children}
<Button onClick={handleCancel} className={styles.cancelButton}>
<StyledCancelButton onClick={handleCancel}>
Cancel
</Button>
</div>
</form>
</StyledCancelButton>
</StyledButtonContainer>
</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 { Box } from '@mui/material';
import { Box, Button, styled } from '@mui/material';
import { SectionSeparator } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/SectionSeparator/SectionSeparator';
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
import useToast from 'hooks/useToast';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { useStyles } from './FeatureStrategyEmpty.styles';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
@ -23,12 +22,33 @@ interface IFeatureStrategyEmptyProps {
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 = ({
projectId,
featureId,
environmentId,
}: IFeatureStrategyEmptyProps) => {
const { classes: styles } = useStyles();
const { addStrategyToFeature } = useFeatureStrategyApi();
const { setToastData, setToastApiError } = useToast();
const { refetchFeature } = useFeature(projectId, featureId);
@ -122,16 +142,16 @@ export const FeatureStrategyEmpty = ({
}
/>
<div className={styles.container}>
<div className={styles.title}>
<StyledContainer>
<StyledTitle>
You have not defined any strategies yet.
</div>
<p className={styles.description}>
</StyledTitle>
<StyledDescription>
Strategies added in this environment will only be executed
if the SDK is using an{' '}
<Link to="/admin/api">API key configured</Link> for this
environment.
</p>
</StyledDescription>
<Box
sx={{
w: '100%',
@ -211,7 +231,7 @@ export const FeatureStrategyEmpty = ({
Roll out to a percentage of your userbase.
</AddFromTemplateCard>
</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 { useNavigate } from 'react-router-dom';
import { Alert, Button } from '@mui/material';
import { Alert, Button, styled } from '@mui/material';
import {
IFeatureStrategy,
IFeatureStrategyParameters,
@ -10,7 +10,6 @@ import { FeatureStrategyType } from '../FeatureStrategyType/FeatureStrategyType'
import { FeatureStrategyEnabled } from './FeatureStrategyEnabled/FeatureStrategyEnabled';
import { FeatureStrategyConstraints } from '../FeatureStrategyConstraints/FeatureStrategyConstraints';
import { IFeatureToggle } from 'interfaces/featureToggle';
import { useStyles } from './FeatureStrategyForm.styles';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { STRATEGY_FORM_SUBMIT_ID } from 'utils/testIds';
@ -48,6 +47,26 @@ interface IFeatureStrategyFormProps {
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 = ({
projectId,
feature,
@ -62,7 +81,6 @@ export const FeatureStrategyForm = ({
errors,
isChangeRequest,
}: IFeatureStrategyFormProps) => {
const { classes: styles } = useStyles();
const [showProdGuard, setShowProdGuard] = useState(false);
const hasValidConstraints = useConstraintsValidation(strategy.constraints);
const enableProdGuard = useFeatureStrategyProdGuard(feature, environmentId);
@ -148,7 +166,7 @@ export const FeatureStrategyForm = ({
};
return (
<form className={styles.form} onSubmit={onSubmitWithValidation}>
<StyledForm onSubmit={onSubmitWithValidation}>
<ConditionallyRender
condition={hasChangeRequestInReviewForEnvironment}
show={alert}
@ -188,7 +206,7 @@ export const FeatureStrategyForm = ({
}
/>
</FeatureStrategyEnabled>
<hr className={styles.hr} />
<StyledHr />
<ConditionallyRender
condition={Boolean(uiConfig.flags.SE)}
show={
@ -204,7 +222,7 @@ export const FeatureStrategyForm = ({
strategy={strategy}
setStrategy={setStrategy}
/>
<hr className={styles.hr} />
<StyledHr />
<FeatureStrategyType
strategy={strategy}
strategyDefinition={strategyDefinition}
@ -213,8 +231,8 @@ export const FeatureStrategyForm = ({
errors={errors}
hasAccess={access}
/>
<hr className={styles.hr} />
<div className={styles.buttons}>
<StyledHr />
<StyledButtons>
<PermissionButton
permission={permission}
projectId={feature.project}
@ -248,7 +266,7 @@ export const FeatureStrategyForm = ({
loading={loading}
label="Save strategy"
/>
</div>
</form>
</StyledButtons>
</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 { Link } from 'react-router-dom';
import { useStyles } from './FeatureStrategyMenuCard.styles';
import {
getFeatureStrategyIcon,
formatStrategyName,
} from 'utils/strategyNames';
import { formatCreateStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { styled } from '@mui/material';
interface IFeatureStrategyMenuCardProps {
projectId: string;
@ -15,13 +15,52 @@ interface IFeatureStrategyMenuCardProps {
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 = ({
projectId,
featureId,
environmentId,
strategy,
}: IFeatureStrategyMenuCardProps) => {
const { classes: styles } = useStyles();
const StrategyIcon = getFeatureStrategyIcon(strategy.name);
const strategyName = formatStrategyName(strategy.name);
@ -33,19 +72,18 @@ export const FeatureStrategyMenuCard = ({
);
return (
<Link to={createStrategyPath} className={styles.card}>
<div className={styles.icon}>
<StyledCard to={createStrategyPath}>
<StyledIcon>
<StrategyIcon />
</div>
</StyledIcon>
<div>
<StringTruncator
<StyledName
text={strategy.displayName || strategyName}
className={styles.name}
maxWidth="200"
maxLength={25}
/>
<div className={styles.description}>{strategy.description}</div>
<StyledDescription>{strategy.description}</StyledDescription>
</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,
} from 'component/common/AutocompleteBox/AutocompleteBox';
import { FeatureStrategySegmentList } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentList';
import { useStyles } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.styles';
import { SegmentDocsStrategyWarning } from 'component/segments/SegmentDocs';
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
import { Divider, Typography } from '@mui/material';
import { Divider, styled, Typography } from '@mui/material';
interface IFeatureStrategySegmentProps {
segments: ISegment[];
setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>;
}
const StyledDivider = styled(Divider)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
}));
export const FeatureStrategySegment = ({
segments: selectedSegments,
setSegments: setSelectedSegments,
}: IFeatureStrategySegmentProps) => {
const { segments: allSegments } = useSegments();
const { classes: styles } = useStyles();
const { strategySegmentsLimit } = useSegmentLimits();
const atStrategySegmentsLimit: boolean = Boolean(
@ -68,7 +70,7 @@ export const FeatureStrategySegment = ({
segments={selectedSegments}
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 { ISegment } from 'interfaces/segment';
import { Clear, VisibilityOff, Visibility } from '@mui/icons-material';
import { useStyles } from './FeatureStrategySegmentChip.styles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { constraintAccordionListId } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
import { Tooltip } from '@mui/material';
import { styled, Theme, Tooltip } from '@mui/material';
interface IFeatureStrategySegmentListProps {
segment: ISegment;
@ -14,14 +13,39 @@ interface IFeatureStrategySegmentListProps {
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 = ({
segment,
setSegments,
preview,
setPreview,
}: IFeatureStrategySegmentListProps) => {
const { classes: styles } = useStyles();
const onRemove = () => {
setSegments(prev => {
return prev.filter(s => s.id !== segment.id);
@ -40,8 +64,8 @@ export const FeatureStrategySegmentChip = ({
const togglePreviewIcon = (
<ConditionallyRender
condition={segment === preview}
show={<VisibilityOff titleAccess="Hide" className={styles.icon} />}
elseShow={<Visibility titleAccess="Show" className={styles.icon} />}
show={<VisibilityOff titleAccess="Hide" sx={styledIcon} />}
elseShow={<Visibility titleAccess="Show" sx={styledIcon} />}
/>
);
@ -51,34 +75,25 @@ export const FeatureStrategySegmentChip = ({
: 'Preview segment constraints';
return (
<span className={styles.chip}>
<Link
to={`/segments/edit/${segment.id}`}
target="_blank"
className={styles.link}
>
<StyledChip>
<StyledLink to={`/segments/edit/${segment.id}`} target="_blank">
{segment.name}
</Link>
</StyledLink>
<Tooltip title={previewIconTooltip} arrow>
<button
<StyledButton
type="button"
onClick={onTogglePreview}
className={styles.button}
aria-expanded={segment === preview}
aria-controls={constraintAccordionListId}
>
{togglePreviewIcon}
</button>
</StyledButton>
</Tooltip>
<Tooltip title="Remove segment" arrow>
<button
type="button"
onClick={onRemove}
className={styles.button}
>
<Clear titleAccess="Remove" className={styles.icon} />
</button>
<StyledButton type="button" onClick={onRemove}>
<Clear titleAccess="Remove" sx={styledIcon} />
</StyledButton>
</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 { ISegment } from 'interfaces/segment';
import { useStyles } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentList.styles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FeatureStrategySegmentChip } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentChip';
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem';
import { styled } from '@mui/material';
interface IFeatureStrategySegmentListProps {
segments: 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 = ({
segments,
setSegments,
}: IFeatureStrategySegmentListProps) => {
const { classes: styles } = useStyles();
const [preview, setPreview] = useState<ISegment>();
const lastSegmentIndex = segments.length - 1;
@ -27,12 +49,12 @@ export const FeatureStrategySegmentList = ({
<ConditionallyRender
condition={segments && segments.length > 0}
show={
<p className={styles.selectedSegmentsLabel}>
<StyledSelectedSegmentsLabel>
Selected Segments
</p>
</StyledSelectedSegmentsLabel>
}
/>
<div className={styles.list}>
<StyledList>
{segments.map((segment, i) => (
<Fragment key={segment.id}>
<FeatureStrategySegmentChip
@ -43,11 +65,11 @@ export const FeatureStrategySegmentList = ({
/>
<ConditionallyRender
condition={i < lastSegmentIndex}
show={<span className={styles.and}>AND</span>}
show={<StyledAnd>AND</StyledAnd>}
/>
</Fragment>
))}
</div>
</StyledList>
<ConditionallyRender
condition={Boolean(preview)}
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 { Box, Typography } from '@mui/material';
import { useStyles } from './FeatureStaleCell.styles';
import classnames from 'classnames';
import { Box, styled, Theme, Typography } from '@mui/material';
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
interface IFeatureStaleCellProps {
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 }) => {
const { classes: styles } = useStyles();
return (
<Box sx={{ py: 1.5, px: 2 }}>
<Typography
component="span"
className={classnames(styles.status, value && styles.stale)}
data-loading
>
{value ? 'Stale' : 'Active'}
</Typography>
</Box>
<StyledBox>
<ConditionallyRender
condition={Boolean(value)}
show={
<Typography component="span" sx={staleStatus} data-loading>
Stale
</Typography>
}
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 { useStyles } from './FeatureLog.styles';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
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 projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { classes: styles } = useStyles();
const { feature } = useFeature(projectId, featureId);
if (!feature.name) {
@ -14,14 +19,14 @@ const FeatureLog = () => {
}
return (
<div className={styles.container}>
<StyledContainer>
<EventLog
title="Event log"
project={projectId}
feature={featureId}
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,
} from './FeatureMetricsHours/FeatureMetricsHours';
import { IFeatureMetricsRaw } from 'interfaces/featureToggle';
import { Grid } from '@mui/material';
import { Grid, styled } from '@mui/material';
import { FeatureMetricsContent } from './FeatureMetricsContent/FeatureMetricsContent';
import { useQueryStringNumberState } from 'hooks/useQueryStringNumberState';
import { useQueryStringState } from 'hooks/useQueryStringState';
import { FeatureMetricsChips } from './FeatureMetricsChips/FeatureMetricsChips';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './FeatureMetrics.styles';
import { usePageTitle } from 'hooks/usePageTitle';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
const StyledContainer = styled('div')(({ theme }) => ({
[theme.breakpoints.down('md')]: {
marginTop: theme.spacing(2),
},
}));
export const FeatureMetrics = () => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const environments = useFeatureMetricsEnvironments(projectId, featureId);
const applications = useFeatureMetricsApplications(featureId);
const { classes: styles } = useStyles();
usePageTitle('Metrics');
const [hoursBack = FEATURE_METRIC_HOURS_BACK_MAX, setHoursBack] =
@ -91,12 +95,12 @@ export const FeatureMetrics = () => {
/>
</Grid>
<Grid item xs={12} md={2}>
<div className={styles.mobileMarginTop}>
<StyledContainer>
<FeatureMetricsHours
hoursBack={hoursBack}
setHoursBack={setHoursBack}
/>
</div>
</StyledContainer>
</Grid>
</Grid>
<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 { useStyles } from './FeatureMetricsChips.styles';
import { useThemeStyles } from 'themes/themeStyles';
import { focusable } from 'themes/themeStyles';
interface IFeatureMetricsChipsProps {
title: string;
@ -10,15 +9,36 @@ interface IFeatureMetricsChipsProps {
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 = ({
title,
values,
value,
setValue,
}: IFeatureMetricsChipsProps) => {
const { classes: themeStyles } = useThemeStyles();
const { classes: styles } = useStyles();
const onClick = (value: string) => () => {
if (values.has(value)) {
setValue(value);
@ -33,19 +53,19 @@ export const FeatureMetricsChips = ({
return (
<div>
<h2 className={styles.title}>{title}</h2>
<ul className={styles.list}>
<StyledTitle>{title}</StyledTitle>
<StyledList>
{sortedValues.map(val => (
<li key={val} className={styles.item}>
<StyledItem key={val}>
<Chip
label={val}
onClick={onClick(val)}
aria-pressed={val === value}
className={themeStyles.focusable}
sx={focusable}
/>
</li>
</StyledItem>
))}
</ul>
</StyledList>
</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 { useStyles } from './FeatureMetricsStats.styles';
import { Grid } from '@mui/material';
import { Grid, styled } from '@mui/material';
import { PrettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber';
export interface IFeatureMetricsStatsProps {
@ -11,6 +10,38 @@ export interface IFeatureMetricsStatsProps {
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 = ({
totalYes,
totalNo,
@ -18,8 +49,6 @@ export const FeatureMetricsStats = ({
statsSectionId,
tableSectionId,
}: IFeatureMetricsStatsProps) => {
const { classes: styles } = useStyles();
const hoursSuffix =
hoursBack === 1 ? 'in the last hour' : `in the last ${hoursBack} hours`;
@ -33,40 +62,40 @@ export const FeatureMetricsStats = ({
component="section"
>
<Grid item xs={12} sm={4}>
<article className={styles.item}>
<h3 className={styles.title}>Exposure</h3>
<p className={styles.value}>
<StyledItem>
<StyledTitle>Exposure</StyledTitle>
<StyledValue>
<PrettifyLargeNumber value={totalYes} />
</p>
<p className={styles.text}>
</StyledValue>
<StyledText>
Total exposure of the feature in the environment{' '}
{hoursSuffix}.
</p>
</article>
</StyledText>
</StyledItem>
</Grid>
<Grid item xs={12} sm={4}>
<article className={styles.item}>
<h3 className={styles.title}>Exposure %</h3>
<p className={styles.value}>
<StyledItem>
<StyledTitle>Exposure %</StyledTitle>
<StyledValue>
{calculatePercentage(totalYes + totalNo, totalYes)}%
</p>
<p className={styles.text}>
</StyledValue>
<StyledText>
% total exposure of the feature in the environment{' '}
{hoursSuffix}.
</p>
</article>
</StyledText>
</StyledItem>
</Grid>
<Grid item xs={12} sm={4}>
<article className={styles.item}>
<h3 className={styles.title}>Requests</h3>
<p className={styles.value}>
<StyledItem>
<StyledTitle>Requests</StyledTitle>
<StyledValue>
<PrettifyLargeNumber value={totalYes + totalNo} />
</p>
<p className={styles.text}>
</StyledValue>
<StyledText>
Total requests for the feature in the environment{' '}
{hoursSuffix}.
</p>
</article>
</StyledText>
</StyledItem>
</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 { Link } from 'react-router-dom';
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 { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { styled } from '@mui/material';
const StyledFeatureId = styled('strong')({
wordBreak: 'break-all',
});
export const FeatureNotFound = () => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { archivedFeatures } = useFeaturesArchive();
const { classes: styles } = useStyles();
const { uiConfig } = useUiConfig();
const createFeatureTogglePath = getCreateTogglePath(projectId, {
name: featureId,
@ -28,8 +29,7 @@ export const FeatureNotFound = () => {
if (isArchived) {
return (
<p>
The feature{' '}
<strong className={styles.featureId}>{featureId}</strong> has
The feature <StyledFeatureId>{featureId}</StyledFeatureId> has
been archived. You can find it on the{' '}
<Link to={`/projects/${projectId}/archive`}>
project archive page
@ -41,8 +41,7 @@ export const FeatureNotFound = () => {
return (
<p>
The feature{' '}
<strong className={styles.featureId}>{featureId}</strong> does not
The feature <StyledFeatureId>{featureId}</StyledFeatureId> does not
exist. Would you like to{' '}
<Link to={createFeatureTogglePath}>create it</Link>?
</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 { Dialogue } from 'component/common/Dialogue/Dialogue';
import Input from 'component/common/Input/Input';
import { useStyles } from './AddTagDialog.styles';
import { trim } from 'component/common/util';
import TagSelect from 'component/common/TagSelect/TagSelect';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
@ -28,9 +27,14 @@ interface IDefaultTag {
[index: string]: string;
}
const StyledDialogFormContent = styled('section')(({ theme }) => ({
['& > *']: {
margin: '0.5rem 0',
},
}));
const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
const DEFAULT_TAG: IDefaultTag = { type: 'simple', value: '' };
const { classes: styles } = useStyles();
const featureId = useRequiredPathParam('featureId');
const { addTagToFeature, loading } = useFeatureApi();
const { tags, refetch } = useTags(featureId);
@ -106,7 +110,7 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
Tags allow you to group features together
</Typography>
<form id={formId} onSubmit={onSubmit}>
<section className={styles.dialogFormContent}>
<StyledDialogFormContent>
<TagSelect
autoFocus
name="type"
@ -126,7 +130,7 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
}
required
/>
</section>
</StyledDialogFormContent>
</form>
</>
</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 { useStyles } from './FeatureOverview.styles';
import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
import FeatureOverviewEnvSwitches from './FeatureOverviewEnvSwitches/FeatureOverviewEnvSwitches';
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 { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel';
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 { uiConfig } = useUiConfig();
const { classes: styles } = useStyles();
const navigate = useNavigate();
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
@ -29,7 +45,7 @@ const FeatureOverview = () => {
usePageTitle(featureId);
return (
<div className={styles.container}>
<StyledContainer>
<div>
<FeatureOverviewMetaData />
<ConditionallyRender
@ -43,9 +59,9 @@ const FeatureOverview = () => {
elseShow={<FeatureOverviewEnvSwitches />}
/>
</div>
<div className={styles.mainContent}>
<StyledMainContent>
<FeatureOverviewEnvironments />
</div>
</StyledMainContent>
<Routes>
<Route
path="strategies/create"
@ -72,7 +88,7 @@ const FeatureOverview = () => {
}
/>
</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 { Tab, Tabs, useMediaQuery } from '@mui/material';
import { styled, Tab, Tabs, useMediaQuery } from '@mui/material';
import { Archive, FileCopy, Label, WatchLater } from '@mui/icons-material';
import {
Link,
@ -20,7 +20,6 @@ import FeatureLog from './FeatureLog/FeatureLog';
import FeatureOverview from './FeatureOverview/FeatureOverview';
import FeatureVariants from './FeatureVariants/FeatureVariants';
import { FeatureMetrics } from './FeatureMetrics/FeatureMetrics';
import { useStyles } from './FeatureView.styles';
import { FeatureSettings } from './FeatureSettings/FeatureSettings';
import useLoading from 'hooks/useLoading';
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 { 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 = () => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
@ -50,7 +103,6 @@ export const FeatureView = () => {
featureId
);
const { classes: styles } = useStyles();
const navigate = useNavigate();
const { pathname } = useLocation();
const ref = useLoading(loading);
@ -98,23 +150,23 @@ export const FeatureView = () => {
return (
<div ref={ref}>
<div className={styles.header}>
<div className={styles.innerContainer}>
<div className={styles.toggleInfoContainer}>
<StyledHeader>
<StyledInnerContainer>
<StyledToggleInfoContainer>
<FavoriteIconButton
onClick={onFavorite}
isFavorite={feature?.favorite}
/>
<h1 className={styles.featureViewHeader} data-loading>
<StyledFeatureViewHeader data-loading>
{feature.name}{' '}
</h1>
</StyledFeatureViewHeader>
<ConditionallyRender
condition={!smallScreen}
show={<FeatureStatusChip stale={feature?.stale} />}
/>
</div>
</StyledToggleInfoContainer>
<div className={styles.toolbarContainer}>
<StyledToolbarContainer>
<PermissionIconButton
permission={CREATE_FEATURE}
projectId={projectId}
@ -158,27 +210,26 @@ export const FeatureView = () => {
>
<Label />
</PermissionIconButton>
</div>
</div>
<div className={styles.separator} />
<div className={styles.tabContainer}>
</StyledToolbarContainer>
</StyledInnerContainer>
<StyledSeparator />
<StyledTabContainer>
<Tabs
value={activeTab.path}
indicatorColor="primary"
textColor="primary"
>
{tabData.map(tab => (
<Tab
<StyledTabButton
key={tab.title}
label={tab.title}
value={tab.path}
onClick={() => navigate(tab.path)}
className={styles.tabButton}
/>
))}
</Tabs>
</div>
</div>
</StyledTabContainer>
</StyledHeader>
<Routes>
<Route path="metrics" element={<FeatureMetrics />} />
<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 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 { IPermission } from 'interfaces/user';
import { NavigationMenu } from './NavigationMenu/NavigationMenu';
@ -97,9 +97,7 @@ const styledIconProps = (theme: Theme) => ({
color: theme.palette.neutral.main,
});
const StyledLink = styled(Link)(({ theme }) => ({
...focusable(theme),
}));
const StyledLink = styled(Link)(({ theme }) => focusable(theme));
const StyledIconButton = styled(IconButton)(({ theme }) => ({
...focusable(theme),
@ -158,9 +156,7 @@ const Header: VFC = () => {
<StyledContainer>
<Tooltip title="Menu" arrow>
<IconButton
sx={theme => ({
color: theme.palette.text.tertiaryContrast,
})}
sx={{ color: theme => theme.palette.text.primary }}
onClick={toggleDrawer}
aria-controls="header-drawer"
aria-expanded={openDrawer}