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

refactor: batch of changes for styled components (#2791)

This commit is contained in:
Mateusz Kwasniewski 2023-01-03 09:20:26 +01:00 committed by GitHub
parent 5fe16207db
commit 231b26995c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 411 additions and 494 deletions

View File

@ -1,9 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
label: {
display: 'inline-flex',
alignItems: 'center',
cursor: 'pointer',
},
}));

View File

@ -8,12 +8,12 @@ import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions';
import React from 'react';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useStyles } from './FeatureOverviewEnvSwitch.styles';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
import { UpdateEnabledMessage } from '../../../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { styled } from '@mui/material';
interface IFeatureOverviewEnvSwitchProps {
env: IFeatureEnvironment;
@ -22,6 +22,12 @@ interface IFeatureOverviewEnvSwitchProps {
showInfoBox: () => void;
}
const StyledLabel = styled('label')({
display: 'inline-flex',
alignItems: 'center',
cursor: 'pointer',
});
const FeatureOverviewEnvSwitch = ({
env,
callback,
@ -34,7 +40,6 @@ const FeatureOverviewEnvSwitch = ({
useFeatureApi();
const { refetchFeature } = useFeature(projectId, featureId);
const { setToastData, setToastApiError } = useToast();
const { classes: styles } = useStyles();
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const {
onChangeRequestToggle,
@ -110,7 +115,7 @@ const FeatureOverviewEnvSwitch = ({
return (
<div>
<label className={styles.label}>
<StyledLabel>
<PermissionSwitch
permission={UPDATE_FEATURE_ENVIRONMENT}
projectId={projectId}
@ -119,7 +124,7 @@ const FeatureOverviewEnvSwitch = ({
environmentId={env.name}
/>
{content}
</label>
</StyledLabel>
<ChangeRequestDialogue
isOpen={changeRequestDialogDetails.isOpen}
onClose={onChangeRequestToggleClose}

View File

@ -1,14 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
accordionBodyInnerContainer: {
[theme.breakpoints.down(400)]: {
padding: '0.5rem',
},
},
accordionBody: {
width: '100%',
position: 'relative',
paddingBottom: '1rem',
},
}));

View File

@ -1,5 +1,5 @@
import { DragEventHandler, RefObject, useEffect, useState } from 'react';
import { Alert } from '@mui/material';
import { Alert, styled } from '@mui/material';
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
import { formatUnknownError } from 'utils/formatUnknownError';
import useToast from 'hooks/useToast';
@ -8,7 +8,6 @@ import { StrategyDraggableItem } from './StrategyDraggableItem/StrategyDraggable
import { IFeatureEnvironment } from 'interfaces/featureToggle';
import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useStyles } from './EnvironmentAccordionBody.styles';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
interface IEnvironmentAccordionBodyProps {
@ -17,6 +16,18 @@ interface IEnvironmentAccordionBodyProps {
otherEnvironments?: IFeatureEnvironment['name'][];
}
const StyledAccordionBody = styled('div')(({ theme }) => ({
width: '100%',
position: 'relative',
paddingBottom: theme.spacing(2),
}));
const StyledAccordionBodyInnerContainer = styled('div')(({ theme }) => ({
[theme.breakpoints.down(400)]: {
padding: theme.spacing(1),
},
}));
const EnvironmentAccordionBody = ({
featureEnvironment,
isDisabled,
@ -35,7 +46,6 @@ const EnvironmentAccordionBody = ({
index: number;
height: number;
} | null>(null);
const { classes: styles } = useStyles();
useEffect(() => {
// Use state to enable drag and drop, but switch to API output when it arrives
setStrategies(featureEnvironment?.strategies || []);
@ -128,8 +138,8 @@ const EnvironmentAccordionBody = ({
};
return (
<div className={styles.accordionBody}>
<div className={styles.accordionBodyInnerContainer}>
<StyledAccordionBody>
<StyledAccordionBodyInnerContainer>
<ConditionallyRender
condition={strategies.length > 0 && isDisabled}
show={() => (
@ -166,8 +176,8 @@ const EnvironmentAccordionBody = ({
/>
}
/>
</div>
</div>
</StyledAccordionBodyInnerContainer>
</StyledAccordionBody>
);
};

View File

@ -1,20 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
width: '100%',
padding: theme.spacing(2, 3),
borderRadius: theme.shape.borderRadiusMedium,
border: `1px solid ${theme.palette.divider}`,
},
chip: {
margin: '0.25rem',
},
paragraph: {
display: 'inline',
margin: '0.25rem 0',
maxWidth: '95%',
textAlign: 'center',
wordBreak: 'break-word',
},
}));

View File

@ -1,6 +1,5 @@
import { Chip } from '@mui/material';
import { Chip, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './ConstraintItem.styles';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
interface IConstraintItemProps {
@ -8,22 +7,40 @@ interface IConstraintItemProps {
text: string;
}
const StyledContainer = styled('div')(({ theme }) => ({
width: '100%',
padding: theme.spacing(2, 3),
borderRadius: theme.shape.borderRadiusMedium,
border: `1px solid ${theme.palette.divider}`,
}));
const StyledParagraph = styled('p')(({ theme }) => ({
display: 'inline',
margin: theme.spacing(0.5, 0),
maxWidth: '95%',
textAlign: 'center',
wordBreak: 'break-word',
}));
const StyledChip = styled(Chip)(({ theme }) => ({
margin: theme.spacing(0.5),
}));
export const ConstraintItem = ({ value, text }: IConstraintItemProps) => {
const { classes: styles } = useStyles();
return (
<div className={styles.container}>
<StyledContainer>
<ConditionallyRender
condition={value.length === 0}
show={<p>No {text}s added yet.</p>}
elseShow={
<div>
<p className={styles.paragraph}>
<StyledParagraph>
{value.length}{' '}
{value.length > 1 ? `${text}s` : text} will get
access.
</p>
</StyledParagraph>
{value.map((v: string) => (
<Chip
<StyledChip
key={v}
label={
<StringTruncator
@ -32,12 +49,11 @@ export const ConstraintItem = ({ value, text }: IConstraintItemProps) => {
maxLength={50}
/>
}
className={styles.chip}
/>
))}
</div>
}
/>
</div>
</StyledContainer>
);
};

View File

@ -1,12 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
valueContainer: {
padding: theme.spacing(2, 3),
border: `1px solid ${theme.palette.dividerAlternative}`,
borderRadius: theme.shape.borderRadiusMedium,
},
valueSeparator: {
color: theme.palette.grey[700],
},
}));

View File

@ -1,6 +1,6 @@
import { Fragment, useMemo, VFC } from 'react';
import { Box, Chip } from '@mui/material';
import { IFeatureStrategy, IFeatureStrategyPayload } from 'interfaces/strategy';
import { Box, Chip, styled } from '@mui/material';
import { IFeatureStrategyPayload } from 'interfaces/strategy';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
@ -10,7 +10,6 @@ import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment';
import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
import { useStyles } from './StrategyExecution.styles';
import {
parseParameterNumber,
parseParameterString,
@ -28,11 +27,20 @@ const NoItems: VFC = () => (
</Box>
);
const StyledValueContainer = styled(Box)(({ theme }) => ({
padding: theme.spacing(2, 3),
border: `1px solid ${theme.palette.dividerAlternative}`,
borderRadius: theme.shape.borderRadiusMedium,
}));
const StyledValueSeparator = styled('span')(({ theme }) => ({
color: theme.palette.neutral.main,
}));
export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
strategy,
}) => {
const { parameters, constraints = [] } = strategy;
const { classes: styles } = useStyles();
const { strategies } = useStrategies();
const { uiConfig } = useUiConfig();
const { segments } = useSegments();
@ -54,8 +62,7 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
const percentage = parseParameterNumber(parameters[key]);
return (
<Box
className={styles.valueContainer}
<StyledValueContainer
sx={{ display: 'flex', alignItems: 'center' }}
>
<Box sx={{ mr: 2 }}>
@ -77,7 +84,7 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
: ''}{' '}
is included.
</div>
</Box>
</StyledValueContainer>
);
case 'userIds':
case 'UserIds':
@ -101,12 +108,12 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
return null;
}
});
}, [parameters, definition, constraints, styles]);
}, [parameters, definition, constraints]);
const customStrategyList = useMemo(() => {
if (!parameters || !definition?.editable) return null;
const isSetTo = (
<span className={styles.valueSeparator}>{' is set to '}</span>
<StyledValueSeparator>{' is set to '}</StyledValueSeparator>
);
return definition?.parameters.map(param => {
@ -123,9 +130,9 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
const values = parseParameterStrings(parameters[name]);
return values.length > 0 ? (
<div className={styles.valueContainer}>
<StyledValueContainer>
{nameItem}{' '}
<span className={styles.valueSeparator}>
<StyledValueSeparator>
has {values.length}{' '}
{values.length > 1 ? `items` : 'item'}:{' '}
{values.map((item: string) => (
@ -141,15 +148,14 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
sx={{ mr: 0.5 }}
/>
))}
</span>
</div>
</StyledValueSeparator>
</StyledValueContainer>
) : null;
case 'percentage':
const percentage = parseParameterNumber(parameters[name]);
return parameters[name] !== '' ? (
<Box
className={styles.valueContainer}
<StyledValueContainer
sx={{ display: 'flex', alignItems: 'center' }}
>
<Box sx={{ mr: 2 }}>
@ -168,13 +174,13 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
label={`${percentage}%`}
/>
</div>
</Box>
</StyledValueContainer>
) : null;
case 'boolean':
return parameters[name] === 'true' ||
parameters[name] === 'false' ? (
<div className={styles.valueContainer}>
<StyledValueContainer>
<StringTruncator
maxLength={15}
maxWidth="150"
@ -191,20 +197,20 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
size="small"
label={parameters[name]}
/>
</div>
</StyledValueContainer>
) : null;
case 'string':
const value = parseParameterString(parameters[name]);
return typeof parameters[name] !== 'undefined' ? (
<div className={styles.valueContainer}>
<StyledValueContainer>
{nameItem}
<ConditionallyRender
condition={value === ''}
show={
<span className={styles.valueSeparator}>
<StyledValueSeparator>
{' is an empty string'}
</span>
</StyledValueSeparator>
}
elseShow={
<>
@ -217,13 +223,13 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
</>
}
/>
</div>
</StyledValueContainer>
) : null;
case 'number':
const number = parseParameterNumber(parameters[name]);
return parameters[name] !== '' && number !== undefined ? (
<div className={styles.valueContainer}>
<StyledValueContainer>
{nameItem}
{isSetTo}
<StringTruncator
@ -231,7 +237,7 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
text={String(number)}
maxLength={50}
/>
</div>
</StyledValueContainer>
) : null;
case 'default':
return null;
@ -239,7 +245,7 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
return null;
});
}, [parameters, definition, styles]);
}, [parameters, definition]);
if (!parameters) {
return <NoItems />;
@ -259,7 +265,7 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
),
strategy.name === 'default' && (
<>
<Box sx={{ width: '100%' }} className={styles.valueContainer}>
<StyledValueContainer sx={{ width: '100%' }}>
The standard strategy is{' '}
<Chip
variant="outlined"
@ -268,7 +274,7 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
label="ON"
/>{' '}
for all users.
</Box>
</StyledValueContainer>
</>
),
...(parametersList ?? []),

View File

@ -1,98 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
featureOverviewEnvironment: {
borderRadius: theme.shape.borderRadiusLarge,
marginBottom: theme.spacing(2),
backgroundColor: theme.palette.background.paper,
},
accordion: {
boxShadow: 'none',
background: 'none',
},
accordionHeader: {
boxShadow: 'none',
padding: '1rem 2rem',
[theme.breakpoints.down(400)]: {
padding: '0.5rem 1rem',
},
},
accordionBodyInnerContainer: {
[theme.breakpoints.down(400)]: {
padding: '0.5rem',
},
},
accordionDetails: {
padding: theme.spacing(3),
background: theme.palette.secondaryContainer,
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
borderBottomRightRadius: theme.shape.borderRadiusLarge,
borderBottom: `4px solid ${theme.palette.primary.light}`,
[theme.breakpoints.down('md')]: {
padding: theme.spacing(2, 1),
},
},
accordionDetailsDisabled: {
borderBottom: `4px solid ${theme.palette.neutral.border}`,
},
accordionBody: {
width: '100%',
position: 'relative',
paddingBottom: theme.spacing(2),
},
header: {
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
},
headerTitle: {
display: 'flex',
alignItems: 'center',
[theme.breakpoints.down(560)]: {
flexDirection: 'column',
textAlign: 'center',
},
},
headerIcon: {
[theme.breakpoints.down(560)]: {
marginBottom: '0.5rem',
},
},
iconContainer: {
backgroundColor: theme.palette.primary.light,
borderRadius: '50%',
width: '28px',
height: '28px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '0.5rem',
},
icon: {
fill: '#fff',
width: '17px',
height: '17px',
},
linkContainer: {
display: 'flex',
justifyContent: 'flex-end',
marginBottom: '1rem',
},
truncator: {
fontSize: theme.fontSizes.bodySize,
fontWeight: theme.typography.fontWeightMedium,
[theme.breakpoints.down(560)]: {
textAlign: 'center',
},
},
container: {
display: 'flex',
alignItems: 'center',
marginLeft: '1.8rem',
[theme.breakpoints.down(560)]: {
flexDirection: 'column',
marginLeft: '0',
},
},
}));

View File

@ -4,9 +4,8 @@ import {
AccordionSummary,
Box,
Chip,
useTheme,
styled,
} from '@mui/material';
import classNames from 'classnames';
import { ExpandMore } from '@mui/icons-material';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics';
@ -15,7 +14,6 @@ import { getFeatureMetrics } from 'utils/getFeatureMetrics';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { useStyles } from './FeatureOverviewEnvironment.styles';
import EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAccordionBody';
import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter';
import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
@ -23,17 +21,104 @@ import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureSt
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
// import { Badge } from 'component/common/Badge/Badge';
interface IFeatureOverviewEnvironmentProps {
env: IFeatureEnvironment;
}
const StyledFeatureOverviewEnvironment = styled('div', {
shouldForwardProp: prop => prop !== 'enabled',
})<{ enabled: boolean }>(({ theme, enabled }) => ({
borderRadius: theme.shape.borderRadiusLarge,
marginBottom: theme.spacing(2),
backgroundColor: theme.palette.background.paper,
background: enabled
? theme.palette.background.paper
: theme.palette.neutral.light,
}));
const StyledAccordion = styled(Accordion)({
boxShadow: 'none',
background: 'none',
});
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
boxShadow: 'none',
padding: theme.spacing(2, 4),
[theme.breakpoints.down(400)]: {
padding: theme.spacing(1, 2),
},
}));
const StyledAccordionDetails = styled(AccordionDetails, {
shouldForwardProp: prop => prop !== 'enabled',
})<{ enabled: boolean }>(({ theme, enabled }) => ({
padding: theme.spacing(3),
background: theme.palette.secondaryContainer,
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
borderBottomRightRadius: theme.shape.borderRadiusLarge,
borderBottom: `4px solid ${
enabled ? theme.palette.primary.light : theme.palette.neutral.border
}`,
[theme.breakpoints.down('md')]: {
padding: theme.spacing(2, 1),
},
}));
const StyledEnvironmentAccordionBody = styled(EnvironmentAccordionBody)(
({ theme }) => ({
width: '100%',
position: 'relative',
paddingBottom: theme.spacing(2),
})
);
const StyledHeader = styled('div', {
shouldForwardProp: prop => prop !== 'enabled',
})<{ enabled: boolean }>(({ theme, enabled }) => ({
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
color: enabled ? theme.palette.text.primary : theme.palette.text.secondary,
}));
const StyledHeaderTitle = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
[theme.breakpoints.down(560)]: {
flexDirection: 'column',
textAlign: 'center',
},
}));
const StyledEnvironmentIcon = styled(EnvironmentIcon)(({ theme }) => ({
[theme.breakpoints.down(560)]: {
marginBottom: '0.5rem',
},
}));
const StyledStringTruncator = styled(StringTruncator)(({ theme }) => ({
fontSize: theme.fontSizes.bodySize,
fontWeight: theme.typography.fontWeightMedium,
[theme.breakpoints.down(560)]: {
textAlign: 'center',
},
}));
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginLeft: '1.8rem',
[theme.breakpoints.down(560)]: {
flexDirection: 'column',
marginLeft: '0',
},
}));
const FeatureOverviewEnvironment = ({
env,
}: IFeatureOverviewEnvironmentProps) => {
const { classes: styles } = useStyles();
const theme = useTheme();
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { metrics } = useFeatureMetrics(projectId, featureId);
@ -48,40 +133,19 @@ const FeatureOverviewEnvironment = ({
);
return (
<div
className={styles.featureOverviewEnvironment}
style={{
background: !env.enabled
? theme.palette.neutral.light
: theme.palette.background.paper,
}}
>
<Accordion
className={styles.accordion}
<StyledFeatureOverviewEnvironment enabled={env.enabled}>
<StyledAccordion
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`}
>
<AccordionSummary
className={styles.accordionHeader}
<StyledAccordionSummary
expandIcon={<ExpandMore titleAccess="Toggle" />}
>
<div
className={styles.header}
data-loading
style={{
color: !env.enabled
? theme.palette.text.secondary
: theme.palette.text.primary,
}}
>
<div className={styles.headerTitle}>
<EnvironmentIcon
enabled={env.enabled}
className={styles.headerIcon}
/>
<StyledHeader data-loading enabled={env.enabled}>
<StyledHeaderTitle>
<StyledEnvironmentIcon enabled={env.enabled} />
<div>
<StringTruncator
<StyledStringTruncator
text={env.name}
className={styles.truncator}
maxWidth="100"
maxLength={15}
/>
@ -97,8 +161,8 @@ const FeatureOverviewEnvironment = ({
/>
}
/>
</div>
<div className={styles.container}>
</StyledHeaderTitle>
<StyledContainer>
<FeatureStrategyMenu
label="Add strategy"
projectId={projectId}
@ -109,21 +173,17 @@ const FeatureOverviewEnvironment = ({
<FeatureStrategyIcons
strategies={featureEnvironment?.strategies}
/>
</div>
</div>
</StyledContainer>
</StyledHeader>
<FeatureOverviewEnvironmentMetrics
environmentMetric={environmentMetric}
disabled={!env.enabled}
/>
</AccordionSummary>
</StyledAccordionSummary>
<AccordionDetails
className={classNames(styles.accordionDetails, {
[styles.accordionDetailsDisabled]: !env.enabled,
})}
>
<EnvironmentAccordionBody
<StyledAccordionDetails enabled={env.enabled}>
<StyledEnvironmentAccordionBody
featureEnvironment={featureEnvironment}
isDisabled={!env.enabled}
otherEnvironments={feature?.environments
@ -156,9 +216,9 @@ const FeatureOverviewEnvironment = ({
</>
}
/>
</AccordionDetails>
</Accordion>
</div>
</StyledAccordionDetails>
</StyledAccordion>
</StyledFeatureOverviewEnvironment>
);
};

View File

@ -1,42 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
marginLeft: 'auto',
display: 'flex',
alignItems: 'center',
},
info: {
marginRight: '0.5rem',
display: 'flex',
flexDirection: 'column',
},
icon: {
fill: theme.palette.grey[300],
height: '75px',
width: '75px',
[theme.breakpoints.down(500)]: {
display: 'none',
},
},
infoParagraph: {
maxWidth: '270px',
marginTop: '0.25rem',
fontSize: theme.fontSizes.smallBody,
textAlign: 'right',
[theme.breakpoints.down(700)]: {
display: 'none',
},
},
percentage: {
color: theme.palette.primary.main,
textAlign: 'right',
fontSize: theme.fontSizes.bodySize,
},
percentageCircle: {
margin: '0 1rem',
[theme.breakpoints.down(500)]: {
display: 'none',
},
},
}));

View File

@ -3,19 +3,62 @@ import { useTheme } from '@mui/system';
import { IFeatureEnvironmentMetrics } from 'interfaces/featureToggle';
import { calculatePercentage } from 'utils/calculatePercentage';
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
import { useStyles } from './FeatureOverviewEnvironmentMetrics.styles';
import { PrettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber';
import { styled } from '@mui/material';
interface IFeatureOverviewEnvironmentMetrics {
environmentMetric?: IFeatureEnvironmentMetrics;
disabled?: boolean;
}
const StyledContainer = styled('div')({
marginLeft: 'auto',
display: 'flex',
alignItems: 'center',
});
const StyledInfo = styled('div')(({ theme }) => ({
marginRight: theme.spacing(1),
display: 'flex',
flexDirection: 'column',
}));
const StyledPercentage = styled('p')(({ theme }) => ({
color: theme.palette.primary.main,
textAlign: 'right',
fontSize: theme.fontSizes.bodySize,
}));
const StyledInfoParagraph = styled('p')(({ theme }) => ({
maxWidth: '270px',
marginTop: theme.spacing(0.5),
fontSize: theme.fontSizes.smallBody,
textAlign: 'right',
[theme.breakpoints.down(700)]: {
display: 'none',
},
}));
const StyledIcon = styled(FiberManualRecord)(({ theme }) => ({
fill: theme.palette.standaloneBackground,
height: '75px',
width: '75px',
[theme.breakpoints.down(500)]: {
display: 'none',
},
}));
const StyledPercentageCircle = styled('div')(({ theme }) => ({
margin: theme.spacing(0, 2),
[theme.breakpoints.down(500)]: {
display: 'none',
},
}));
const FeatureOverviewEnvironmentMetrics = ({
environmentMetric,
disabled = false,
}: IFeatureOverviewEnvironmentMetrics) => {
const { classes: styles } = useStyles();
const theme = useTheme();
if (!environmentMetric) return null;
@ -28,10 +71,9 @@ const FeatureOverviewEnvironmentMetrics = ({
(environmentMetric.yes === 0 && environmentMetric.no === 0)
) {
return (
<div className={styles.container}>
<div className={styles.info}>
<p
className={styles.percentage}
<StyledContainer>
<StyledInfo>
<StyledPercentage
style={{
color: disabled
? theme.palette.text.secondary
@ -40,9 +82,8 @@ const FeatureOverviewEnvironmentMetrics = ({
data-loading
>
{percentage}%
</p>
<p
className={styles.infoParagraph}
</StyledPercentage>
<StyledInfoParagraph
style={{
color: disabled
? theme.palette.text.secondary
@ -53,22 +94,18 @@ const FeatureOverviewEnvironmentMetrics = ({
The feature has been requested <b>0 times</b> and
exposed
<b> 0 times</b> in the last hour
</p>
</div>
<FiberManualRecord
className={styles.icon}
style={{ transform: 'scale(1.1)' }}
data-loading
/>
</div>
</StyledInfoParagraph>
</StyledInfo>
<StyledIcon style={{ transform: 'scale(1.1)' }} data-loading />
</StyledContainer>
);
}
return (
<div className={styles.container}>
<div className={styles.info}>
<p className={styles.percentage}>{percentage}%</p>
<p className={styles.infoParagraph}>
<StyledContainer>
<StyledInfo>
<StyledPercentage>{percentage}%</StyledPercentage>
<StyledInfoParagraph>
The feature has been requested{' '}
<b>
<PrettifyLargeNumber value={total} /> times
@ -79,12 +116,12 @@ const FeatureOverviewEnvironmentMetrics = ({
times
</b>{' '}
in the last hour
</p>
</div>
<div className={styles.percentageCircle} data-loading>
</StyledInfoParagraph>
</StyledInfo>
<StyledPercentageCircle data-loading>
<PercentageCircle percentage={percentage} size="3rem" />
</div>
</div>
</StyledPercentageCircle>
</StyledContainer>
);
};

View File

@ -1,10 +1,8 @@
import { capitalize } from '@mui/material';
import classnames from 'classnames';
import { capitalize, styled } from '@mui/material';
import { Link } from 'react-router-dom';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './FeatureOverviewMetadata.styles';
import { Edit } from '@mui/icons-material';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
@ -13,9 +11,62 @@ import FeatureOverviewTags from './FeatureOverviewTags/FeatureOverviewTags';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
const StyledContainer = styled('div')(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
color: theme.palette.text.tertiaryContrast,
backgroundColor: theme.palette.featureMetaData,
display: 'flex',
flexDirection: 'column',
maxWidth: '350px',
minWidth: '350px',
marginRight: theme.spacing(2),
[theme.breakpoints.down(1000)]: {
width: '100%',
maxWidth: 'none',
minWidth: 'auto',
},
}));
const StyledPaddingContainerTop = styled('div')({
padding: '1.5rem 1.5rem 0 1.5rem',
});
const StyledPaddingContainerBottom = styled('div')(({ theme }) => ({
padding: '0 1.5rem 1.5rem 1.5rem',
borderTop: `1px solid ${theme.palette.divider}`,
}));
const StyledMetaDataHeader = styled('div')({
display: 'flex',
alignItems: 'center',
});
const StyledHeader = styled('h2')(({ theme }) => ({
fontSize: theme.fontSizes.bodySize,
fontWeight: 'normal',
margin: 0,
}));
const StyledBody = styled('div')(({ theme }) => ({
margin: theme.spacing(2, 0),
display: 'flex',
flexDirection: 'column',
}));
const StyledBodyItem = styled('span')(({ theme }) => ({
margin: theme.spacing(1, 0),
fontSize: theme.fontSizes.bodySize,
wordBreak: 'break-all',
}));
const StyledDescriptionContainer = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
color: theme.palette.text.tertiaryContrast,
}));
const FeatureOverviewMetaData = () => {
const { uiConfig } = useUiConfig();
const { classes: styles } = useStyles();
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { tags } = useTags(featureId);
@ -25,24 +76,29 @@ const FeatureOverviewMetaData = () => {
const IconComponent = getFeatureTypeIcons(type);
return (
<div className={classnames(styles.container)}>
<div className={styles.paddingContainerTop}>
<div className={styles.metaDataHeader} data-loading>
<IconComponent className={styles.headerIcon} />{' '}
<h2 className={styles.header}>
{capitalize(type || '')} toggle
</h2>
</div>
<div className={styles.body}>
<span className={styles.bodyItem} data-loading>
<StyledContainer>
<StyledPaddingContainerTop>
<StyledMetaDataHeader data-loading>
<IconComponent
sx={theme => ({
marginRight: theme.spacing(2),
height: '40px',
width: '40px',
fill: theme.palette.text.tertiaryContrast,
})}
/>{' '}
<StyledHeader>{capitalize(type || '')} toggle</StyledHeader>
</StyledMetaDataHeader>
<StyledBody>
<StyledBodyItem data-loading>
Project: {project}
</span>
</StyledBodyItem>
<ConditionallyRender
condition={Boolean(description)}
show={
<span className={styles.bodyItem} data-loading>
<StyledBodyItem data-loading>
<div>Description:</div>
<div className={styles.descriptionContainer}>
<StyledDescriptionContainer>
<p>{description}</p>
<PermissionIconButton
projectId={projectId}
@ -53,14 +109,19 @@ const FeatureOverviewMetaData = () => {
title: 'Edit description',
}}
>
<Edit className={styles.editIcon} />
<Edit
sx={theme => ({
color: theme.palette.text
.tertiaryContrast,
})}
/>
</PermissionIconButton>
</div>
</span>
</StyledDescriptionContainer>
</StyledBodyItem>
}
elseShow={
<span data-loading>
<div className={styles.descriptionContainer}>
<StyledDescriptionContainer>
No description.{' '}
<PermissionIconButton
projectId={projectId}
@ -71,26 +132,31 @@ const FeatureOverviewMetaData = () => {
title: 'Edit description',
}}
>
<Edit className={styles.editIcon} />
<Edit
sx={theme => ({
color: theme.palette.text
.tertiaryContrast,
})}
/>
</PermissionIconButton>
</div>
</StyledDescriptionContainer>
</span>
}
/>
</div>
</div>
</StyledBody>
</StyledPaddingContainerTop>
<ConditionallyRender
condition={
tags.length > 0 &&
!Boolean(uiConfig.flags.variantsPerEnvironment)
}
show={
<div className={styles.paddingContainerBottom}>
<StyledPaddingContainerBottom>
<FeatureOverviewTags projectId={projectId} />
</div>
</StyledPaddingContainerBottom>
}
/>
</div>
</StyledContainer>
);
};

View File

@ -1,59 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
borderRadius: theme.shape.borderRadiusLarge,
color: '#fff',
backgroundColor: theme.palette.featureMetaData,
display: 'flex',
flexDirection: 'column',
maxWidth: '350px',
minWidth: '350px',
marginRight: '1rem',
[theme.breakpoints.down(1000)]: {
width: '100%',
maxWidth: 'none',
minWidth: 'auto',
},
},
paddingContainerTop: {
padding: '1.5rem 1.5rem 0 1.5rem',
},
paddingContainerBottom: {
padding: '0 1.5rem 1.5rem 1.5rem',
borderTop: `1px solid ${theme.palette.grey[300]}`,
},
metaDataHeader: {
display: 'flex',
alignItems: 'center',
},
header: {
fontSize: theme.fontSizes.bodySize,
fontWeight: 'normal',
margin: 0,
},
body: {
margin: '1rem 0',
display: 'flex',
flexDirection: 'column',
},
bodyItem: {
margin: '0.5rem 0',
fontSize: theme.fontSizes.bodySize,
wordBreak: 'break-all',
},
headerIcon: {
marginRight: '1rem',
height: '40px',
width: '40px',
fill: '#fff',
},
descriptionContainer: {
display: 'flex',
alignItems: 'center',
color: '#fff',
},
editIcon: {
color: '#fff',
},
}));

View File

@ -1,38 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
borderRadius: theme.shape.borderRadiusLarge,
backgroundColor: theme.palette.primary.main,
display: 'flex',
flexDirection: 'column',
marginRight: '1rem',
marginTop: '1rem',
[theme.breakpoints.down(800)]: {
width: '100%',
maxWidth: 'none',
},
},
tagHeader: {
display: 'flex',
alignItems: 'center',
},
tag: {
height: '40px',
width: '40px',
fill: theme.palette.primary.main,
marginRight: '0.8rem',
},
tagChip: {
marginRight: '0.25rem',
marginTop: '0.5rem',
backgroundColor: '#fff',
fontSize: theme.fontSizes.smallBody,
},
closeIcon: {
color: theme.palette.primary.light,
'&:hover': {
color: theme.palette.primary.light,
},
},
}));

View File

@ -1,8 +1,7 @@
import React, { useContext, useState } from 'react';
import { Chip } from '@mui/material';
import { Chip, styled } from '@mui/material';
import { Close, Label } from '@mui/icons-material';
import useTags from 'hooks/api/getters/useTags/useTags';
import { useStyles } from './FeatureOverviewTags.styles';
import slackIcon from 'assets/icons/slack.svg';
import jiraIcon from 'assets/icons/jira.svg';
import webhookIcon from 'assets/icons/webhooks.svg';
@ -18,10 +17,37 @@ import AccessContext from 'contexts/AccessContext';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
interface IFeatureOverviewTagsProps extends React.HTMLProps<HTMLDivElement> {
interface IFeatureOverviewTagsProps {
projectId: string;
}
const StyledContainer = styled('div')(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
backgroundColor: theme.palette.primary.main,
display: 'flex',
flexDirection: 'column',
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
[theme.breakpoints.down(800)]: {
width: '100%',
maxWidth: 'none',
},
}));
const StyledTagChip = styled(Chip)(({ theme }) => ({
marginRight: theme.spacing(0.5),
marginTop: theme.spacing(1),
backgroundColor: theme.palette.text.tertiaryContrast,
fontSize: theme.fontSizes.smallBody,
}));
const StyledCloseIcon = styled(Close)(({ theme }) => ({
color: theme.palette.primary.light,
'&:hover': {
color: theme.palette.primary.light,
},
}));
const FeatureOverviewTags: React.FC<IFeatureOverviewTagsProps> = ({
projectId,
...rest
@ -31,7 +57,6 @@ const FeatureOverviewTags: React.FC<IFeatureOverviewTagsProps> = ({
value: '',
type: '',
});
const { classes: styles } = useStyles();
const featureId = useRequiredPathParam('featureId');
const { tags, refetch } = useTags(featureId);
const { tagTypes } = useTagTypes();
@ -98,15 +123,12 @@ const FeatureOverviewTags: React.FC<IFeatureOverviewTagsProps> = ({
};
const renderTag = (t: ITag) => (
<Chip
<StyledTagChip
icon={tagIcon(t.type)}
className={styles.tagChip}
data-loading
label={t.value}
key={`${t.type}:${t.value}`}
deleteIcon={
<Close className={styles.closeIcon} titleAccess="Remove" />
}
deleteIcon={<StyledCloseIcon titleAccess="Remove" />}
onDelete={
canDeleteTag
? () => {
@ -119,7 +141,7 @@ const FeatureOverviewTags: React.FC<IFeatureOverviewTagsProps> = ({
);
return (
<div className={styles.container} {...rest}>
<StyledContainer {...rest}>
<Dialogue
open={showDelDialog}
onClose={() => {
@ -141,7 +163,7 @@ const FeatureOverviewTags: React.FC<IFeatureOverviewTagsProps> = ({
elseShow={<p data-loading>No tags to display</p>}
/>
</div>
</div>
</StyledContainer>
);
};

View File

@ -1,30 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
innerContainer: {
display: 'flex',
},
bodyContainer: {
padding: 0,
},
listContainer: {
width: '20%',
borderRight: `1px solid ${theme.palette.grey[300]}`,
padding: '1rem 0',
[theme.breakpoints.down('md')]: {
width: '35%',
},
},
listItem: {
padding: '0.75rem 2rem',
},
innerBodyContainer: {
padding: '2rem',
display: 'flex',
flexDirection: 'column',
width: 400,
['& > *']: {
margin: '0.5rem 0',
},
},
}));

View File

@ -1,7 +1,6 @@
import { useState } from 'react';
import { PageContent } from 'component/common/PageContent/PageContent';
import { useStyles } from './FeatureSettings.styles';
import { List, ListItem } from '@mui/material';
import { Box, List, ListItem, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import FeatureSettingsProject from './FeatureSettingsProject/FeatureSettingsProject';
import { FeatureSettingsInformation } from './FeatureSettingsInformation/FeatureSettingsInformation';
@ -11,21 +10,39 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
const METADATA = 'metadata';
const PROJECT = 'project';
const StyledListContainer = styled('div')(({ theme }) => ({
width: '20%',
borderRight: `1px solid ${theme.palette.divider}`,
padding: theme.spacing(2, 0),
[theme.breakpoints.down('md')]: {
width: '35%',
},
}));
const StyledInnerBodyContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(4),
display: 'flex',
flexDirection: 'column',
width: 400,
['& > *']: {
margin: theme.spacing(1, 0),
},
}));
export const FeatureSettings = () => {
const { classes: styles } = useStyles();
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const [settings, setSettings] = useState(METADATA);
const { uiConfig } = useUiConfig();
return (
<PageContent header="Settings" bodyClass={styles.bodyContainer}>
<div className={styles.innerContainer}>
<div className={styles.listContainer}>
<PageContent header="Settings" sx={{ padding: 0 }}>
<Box sx={{ display: 'flex' }}>
<StyledListContainer>
<List>
<ListItem
key={0}
className={styles.listItem}
sx={{ padding: '0.75rem 2rem' }}
button
onClick={() => setSettings(METADATA)}
selected={settings === METADATA}
@ -34,7 +51,7 @@ export const FeatureSettings = () => {
</ListItem>
<ListItem
key={1}
className={styles.listItem}
sx={{ padding: '0.75rem 2rem' }}
button
onClick={() => setSettings(PROJECT)}
selected={settings === PROJECT}
@ -43,8 +60,8 @@ export const FeatureSettings = () => {
Project
</ListItem>
</List>
</div>
<div className={styles.innerBodyContainer}>
</StyledListContainer>
<StyledInnerBodyContainer>
<ConditionallyRender
condition={settings === METADATA}
show={
@ -58,8 +75,8 @@ export const FeatureSettings = () => {
condition={settings === PROJECT && uiConfig.flags.P}
show={<FeatureSettingsProject />}
/>
</div>
</div>
</StyledInnerBodyContainer>
</Box>
</PageContent>
);
};