From c70b38a62aa93ab6c35dea0496e322e3fb28e7dd Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 27 Jul 2022 12:00:15 +0200 Subject: [PATCH] Feature toggle page update (#1140) * feat: add icon to custom strategies * feat: update feature toggle screen layout * strategy and constraints separators * style disabled envirnments * strategy constraint style * strategy drag and drop * feature env emtpy state * quick add strategy api * reorder strategies api integration * feature strategy header title * openapi update * style small chip component * fix comments after review * fix issues with strategy constraint operators * Revert "openapi update" This reverts commit 27e7651ebae26f61ca76ec910e1f209bae7f2955. * fix tooltip ref --- .../ConstraintAccordion.styles.ts | 33 ++--- .../CaseSensitiveButton.tsx | 37 +++--- .../InvertedOperatorButton.tsx | 44 ++++--- .../ConstraintAccordionList.styles.ts | 4 +- .../ConstraintAccordionList.tsx | 33 +++-- .../ConstraintAccordionView.tsx | 7 +- .../ConstraintAccordionViewBody.style.ts | 27 ++++ .../ConstraintAccordionViewBody.tsx | 83 ++---------- .../MultipleValues/MultipleValues.tsx | 47 +++++++ .../SingleValue/SingleValue.tsx | 29 ++++ .../ConstraintAccordionViewHeader.tsx | 4 +- .../ConstraintAccordionViewHeaderInfo.tsx | 2 + .../ConstraintViewHeaderOperator.tsx | 16 ++- ...raintAccordionViewHeaderMultipleValues.tsx | 4 +- ...ontraintAccordionViewHeaderSingleValue.tsx | 2 +- .../StyledIconWrapper/StyledIconWrapper.tsx | 36 +++-- .../ConstraintOperator.styles.ts | 2 +- .../ConstraintOperator/ConstraintOperator.tsx | 10 +- .../StrategySeparator/StrategySeparator.tsx | 59 ++++++--- .../TablePlaceholder.styles.ts | 2 +- .../FeatureStrategyEmpty.styles.ts | 22 +++- .../FeatureStrategyEmpty.tsx | 109 +++++++++++---- .../PresetCard/PresetCard.tsx | 35 +++++ .../FeatureStrategyIcon.tsx | 4 +- .../FeatureStrategyMenu.tsx | 2 +- .../FeatureMetricsStats.styles.ts | 2 +- .../EnvironmentAccordionBody.styles.ts | 14 ++ .../EnvironmentAccordionBody.tsx | 113 ++++++++++++++++ .../StrategyDraggableItem.tsx | 35 +++++ .../ConstraintItem/ConstraintItem.styles.ts} | 7 +- .../ConstraintItem/ConstraintItem.tsx} | 11 +- .../StrategyExecution.styles.ts} | 6 + .../StrategyExecution/StrategyExecution.tsx} | 110 ++++++++-------- .../StrategyItem/StrategyItem.styles.ts} | 7 +- .../StrategyItem/StrategyItem.tsx} | 41 ++++-- .../EnvironmentFooter.tsx} | 18 +-- .../FeatureOverviewEnvironment.styles.ts | 56 +++----- .../FeatureOverviewEnvironment.tsx | 124 +++++++++++------- .../FeatureOverviewEnvironmentBody.tsx | 57 -------- ...eatureOverviewEnvironmentMetrics.styles.ts | 2 +- .../FeatureOverviewEnvironmentStrategies.tsx | 26 ---- .../SectionSeparator/SectionSeparator.tsx | 35 +++++ .../FeatureOverviewEnvironments.tsx | 14 +- .../history/EventLog/EventLog.styles.ts | 2 +- .../PlaygroundEditor/PlaygroundEditor.tsx | 2 +- .../project/ProjectCard/ProjectCard.styles.ts | 2 +- .../PasswordChecker/PasswordChecker.styles.ts | 2 +- .../useFeatureStrategyApi.ts | 22 +++- frontend/src/interfaces/strategy.ts | 5 + frontend/src/themes/theme.ts | 26 ++++ frontend/src/utils/strategyNames.ts | 5 +- 51 files changed, 897 insertions(+), 500 deletions(-) create mode 100644 frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody.style.ts create mode 100644 frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/MultipleValues/MultipleValues.tsx create mode 100644 frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/SingleValue/SingleValue.tsx create mode 100644 frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/PresetCard/PresetCard.tsx create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.styles.ts create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx rename frontend/src/component/feature/FeatureView/FeatureOverview/{FeatureOverviewExecution/FeatureOverviewExecutionChips/FeatureOverviewExecutionChips.styles.ts => FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.styles.ts} (62%) rename frontend/src/component/feature/FeatureView/FeatureOverview/{FeatureOverviewExecution/FeatureOverviewExecutionChips/FeatureOverviewExecutionChips.tsx => FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.tsx} (84%) rename frontend/src/component/feature/FeatureView/FeatureOverview/{FeatureOverviewExecution/FeatureOverviewExecution.styles.ts => FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.styles.ts} (59%) rename frontend/src/component/feature/FeatureView/FeatureOverview/{FeatureOverviewExecution/FeatureOverviewExecution.tsx => FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx} (79%) rename frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/{FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategy/FeatureOverviewEnvironmentStrategy.styles.ts => EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.styles.ts} (75%) rename frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/{FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategy/FeatureOverviewEnvironmentStrategy.tsx => EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx} (70%) rename frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/{FeatureOverviewEnvironmentFooter/FeatureOverviewEnvironmentFooter.tsx => EnvironmentFooter/EnvironmentFooter.tsx} (52%) delete mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentBody/FeatureOverviewEnvironmentBody.tsx delete mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategies.tsx create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/SectionSeparator/SectionSeparator.tsx diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordion.styles.ts b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordion.styles.ts index 45fc481d0b..ab801c3e3d 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordion.styles.ts +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordion.styles.ts @@ -33,7 +33,7 @@ export const useStyles = makeStyles()(theme => ({ }, headerMetaInfo: { display: 'flex', - alignItems: 'center', + alignItems: 'stretch', [theme.breakpoints.down(710)]: { flexDirection: 'column' }, }, headerContainer: { @@ -48,12 +48,11 @@ export const useStyles = makeStyles()(theme => ({ }, headerValuesContainerWrapper: { display: 'flex', - flexDirection: 'row', - justifyContent: 'center', + alignItems: 'stretch', }, headerValuesContainer: { display: 'flex', - flexDirection: 'column', + alignItems: 'stretch', }, headerValues: { fontSize: theme.fontSizes.smallBody, @@ -106,7 +105,10 @@ export const useStyles = makeStyles()(theme => ({ }, display: 'inline-flex', }, - headerSelect: { marginRight: '1rem', width: '200px' }, + headerSelect: { + marginRight: '1rem', + width: '200px', + }, chip: { margin: '0 0.5rem 0.5rem 0', }, @@ -121,7 +123,7 @@ export const useStyles = makeStyles()(theme => ({ }, }, accordionDetails: { - borderTop: `1px solid ${theme.palette.grey[300]}`, + borderTop: `1px dashed ${theme.palette.grey[300]}`, display: 'flex', flexDirection: 'column', }, @@ -132,33 +134,16 @@ export const useStyles = makeStyles()(theme => ({ }, summary: { border: 'none', - padding: '0.25rem 1rem', + padding: theme.spacing(0.5, 3), '&:hover .valuesExpandLabel': { textDecoration: 'underline', }, }, - settingsParagraph: { - display: 'flex', - alignItems: 'center', - padding: '0.5rem 0', - }, settingsIcon: { height: '32.5px', width: '32.5px', marginRight: '0.5rem', fill: theme.palette.inactiveIcon, }, - singleValueView: { - display: 'flex', - alignItems: 'center', - [theme.breakpoints.down(600)]: { flexDirection: 'column' }, - }, - singleValueText: { - marginRight: '0.75rem', - [theme.breakpoints.down(600)]: { - marginBottom: '0.75rem', - marginRight: 0, - }, - }, form: { padding: 0, margin: 0, width: '100%' }, })); diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/CaseSensitiveButton/CaseSensitiveButton.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/CaseSensitiveButton/CaseSensitiveButton.tsx index 16edba2e5a..3663818960 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/CaseSensitiveButton/CaseSensitiveButton.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/CaseSensitiveButton/CaseSensitiveButton.tsx @@ -1,4 +1,4 @@ -import { Tooltip } from '@mui/material'; +import { Tooltip, Box } from '@mui/material'; import { ReactComponent as CaseSensitive } from 'assets/icons/24_Text format.svg'; import { ReactComponent as CaseSensitiveOff } from 'assets/icons/24_Text format off.svg'; import React from 'react'; @@ -17,30 +17,35 @@ interface CaseSensitiveButtonProps { export const CaseSensitiveButton = ({ localConstraint, setCaseInsensitive, -}: CaseSensitiveButtonProps) => { - return ( - +}: CaseSensitiveButtonProps) => ( + + + - - } - elseShow={ - + } + elseShow={ - - } - /> - ); -}; + } + /> + + +); diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/InvertedOperatorButton/InvertedOperatorButton.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/InvertedOperatorButton/InvertedOperatorButton.tsx index b05667b4d0..df30a1e712 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/InvertedOperatorButton/InvertedOperatorButton.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/InvertedOperatorButton/InvertedOperatorButton.tsx @@ -1,8 +1,7 @@ -import { Tooltip } from '@mui/material'; -import { ReactComponent as NegatedIcon } from '../../../../../../assets/icons/24_Negator.svg'; -import { ReactComponent as NegatedIconOff } from '../../../../../../assets/icons/24_Negator off.svg'; -import React from 'react'; -import { IConstraint } from '../../../../../../interfaces/strategy'; +import { Box, Tooltip } from '@mui/material'; +import { ReactComponent as NegatedIcon } from 'assets/icons/24_Negator.svg'; +import { ReactComponent as NegatedIconOff } from 'assets/icons/24_Negator off.svg'; +import { IConstraint } from 'interfaces/strategy'; import { StyledToggleButtonOff, StyledToggleButtonOn, @@ -17,30 +16,35 @@ interface InvertedOperatorButtonProps { export const InvertedOperatorButton = ({ localConstraint, setInvertedOperator, -}: InvertedOperatorButtonProps) => { - return ( - +}: InvertedOperatorButtonProps) => ( + + + - - } - elseShow={ - + } + elseShow={ - - } - /> - ); -}; + } + /> + + +); diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.styles.ts b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.styles.ts index 830852c474..8d9e3f4602 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.styles.ts +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.styles.ts @@ -3,8 +3,8 @@ import { makeStyles } from 'tss-react/mui'; export const useStyles = makeStyles()(theme => ({ container: { width: '100%', - display: 'grid', - gap: '1rem', + display: 'flex', + flexDirection: 'column', }, help: { fill: theme.palette.grey[600], diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.tsx index 03672fb694..88e4ba3300 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.tsx @@ -1,5 +1,7 @@ +import React, { forwardRef, Fragment, useImperativeHandle } from 'react'; +import { Button, Tooltip } from '@mui/material'; +import { Help } from '@mui/icons-material'; import { IConstraint } from 'interfaces/strategy'; -import React, { forwardRef, useImperativeHandle } from 'react'; import { ConstraintAccordion } from 'component/common/ConstraintAccordion/ConstraintAccordion'; import produce from 'immer'; import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext'; @@ -8,8 +10,7 @@ import { objectId } from 'utils/objectId'; import { useStyles } from './ConstraintAccordionList.styles'; import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { Button, Tooltip } from '@mui/material'; -import { Help } from '@mui/icons-material'; +import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; interface IConstraintAccordionListProps { constraints: IConstraint[]; @@ -129,16 +130,22 @@ export const ConstraintAccordionList = forwardRef< } /> {constraints.map((constraint, index) => ( - + + 0} + show={} + /> + + ))} ); diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx index 033f5b6c18..44aa810358 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx @@ -1,6 +1,6 @@ +import { useState } from 'react'; import { Accordion, AccordionSummary, AccordionDetails } from '@mui/material'; import { IConstraint } from 'interfaces/strategy'; - import { ConstraintAccordionViewBody } from './ConstraintAccordionViewBody/ConstraintAccordionViewBody'; import { ConstraintAccordionViewHeader } from './ConstraintAccordionViewHeader/ConstraintAccordionViewHeader'; import { oneOf } from 'utils/oneOf'; @@ -9,9 +9,8 @@ import { numOperators, semVerOperators, } from 'constants/operators'; - import { useStyles } from '../ConstraintAccordion.styles'; -import { useState } from 'react'; + interface IConstraintAccordionViewProps { constraint: IConstraint; onDelete?: () => void; @@ -46,7 +45,7 @@ export const ConstraintAccordionView = ({ sx={{ cursor: expandable ? 'pointer' : 'default' }} > diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody.style.ts b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody.style.ts new file mode 100644 index 0000000000..7d0cc2d8b8 --- /dev/null +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody.style.ts @@ -0,0 +1,27 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + chip: { + margin: '0 0.5rem 0.5rem 0', + }, + chipValue: { + whiteSpace: 'pre', + }, + singleValueView: { + display: 'flex', + alignItems: 'center', + [theme.breakpoints.down(600)]: { flexDirection: 'column' }, + }, + singleValueText: { + marginRight: '0.75rem', + [theme.breakpoints.down(600)]: { + marginBottom: '0.75rem', + marginRight: 0, + }, + }, + settingsParagraph: { + display: 'flex', + alignItems: 'center', + padding: '0.5rem 0', + }, +})); diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody.tsx index 16a47142ef..026b87c511 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody.tsx @@ -1,15 +1,14 @@ -import { Chip } from '@mui/material'; import { ImportExportOutlined, TextFormatOutlined } from '@mui/icons-material'; -import StringTruncator from 'component/common/StringTruncator/StringTruncator'; -import { useState } from 'react'; import { stringOperators } from 'constants/operators'; import { IConstraint } from 'interfaces/strategy'; import { oneOf } from 'utils/oneOf'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles'; -import { ConstraintValueSearch } from 'component/common/ConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch'; import { formatConstraintValue } from 'utils/formatConstraintValue'; import { useLocationSettings } from 'hooks/useLocationSettings'; +import { useStyles as useAccordionStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles'; +import { useStyles } from './ConstraintAccordionViewBody.style'; +import { MultipleValues } from './MultipleValues/MultipleValues'; +import { SingleValue } from './SingleValue/SingleValue'; interface IConstraintAccordionViewBodyProps { constraint: IConstraint; @@ -18,7 +17,8 @@ interface IConstraintAccordionViewBodyProps { export const ConstraintAccordionViewBody = ({ constraint, }: IConstraintAccordionViewBodyProps) => { - const { classes: styles } = useStyles(); + const { classes: styles } = useAccordionStyles(); + const { classes } = useStyles(); const { locationSettings } = useLocationSettings(); return ( @@ -29,7 +29,7 @@ export const ConstraintAccordionViewBody = ({ Boolean(constraint.caseInsensitive) } show={ -

+

{' '} Case insensitive setting is active

@@ -39,7 +39,7 @@ export const ConstraintAccordionViewBody = ({ +

{' '} Operator is negated

@@ -56,70 +56,3 @@ export const ConstraintAccordionViewBody = ({ ); }; - -interface ISingleValueProps { - value: string | undefined; - operator: string; -} - -const SingleValue = ({ value, operator }: ISingleValueProps) => { - const { classes: styles } = useStyles(); - if (!value) return null; - - return ( -
-

Value must be {operator}

{' '} - - } - className={styles.chip} - /> -
- ); -}; - -interface IMultipleValuesProps { - values: string[] | undefined; -} - -const MultipleValues = ({ values }: IMultipleValuesProps) => { - const [filter, setFilter] = useState(''); - const { classes: styles } = useStyles(); - - if (!values || values.length === 0) return null; - - return ( - <> - 20} - show={ - - } - /> - {values - .filter(value => value.includes(filter)) - .map((value, index) => ( - - } - className={styles.chip} - /> - ))} - - ); -}; diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/MultipleValues/MultipleValues.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/MultipleValues/MultipleValues.tsx new file mode 100644 index 0000000000..cdcb925720 --- /dev/null +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/MultipleValues/MultipleValues.tsx @@ -0,0 +1,47 @@ +import { useState } from 'react'; +import { Chip } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import { ConstraintValueSearch } from '../../../ConstraintValueSearch/ConstraintValueSearch'; +import { useStyles } from '../ConstraintAccordionViewBody.style'; + +interface IMultipleValuesProps { + values: string[] | undefined; +} + +export const MultipleValues = ({ values }: IMultipleValuesProps) => { + const [filter, setFilter] = useState(''); + const { classes: styles } = useStyles(); + + if (!values || values.length === 0) return null; + + return ( + <> + 20} + show={ + + } + /> + {values + .filter(value => value.includes(filter)) + .map((value, index) => ( + + } + className={styles.chip} + /> + ))} + + ); +}; diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/SingleValue/SingleValue.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/SingleValue/SingleValue.tsx new file mode 100644 index 0000000000..f34ef16d46 --- /dev/null +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/SingleValue/SingleValue.tsx @@ -0,0 +1,29 @@ +import { Chip } from '@mui/material'; +import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import { useStyles } from '../ConstraintAccordionViewBody.style'; + +interface ISingleValueProps { + value: string | undefined; + operator: string; +} + +export const SingleValue = ({ value, operator }: ISingleValueProps) => { + const { classes: styles } = useStyles(); + if (!value) return null; + + return ( +
+

Value must be {operator}

{' '} + + } + className={styles.chip} + /> +
+ ); +}; diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader.tsx index e9eddc7f85..897304438d 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader.tsx @@ -1,10 +1,8 @@ import { ConstraintIcon } from 'component/common/ConstraintAccordion/ConstraintIcon'; import { IConstraint } from 'interfaces/strategy'; - -import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles'; -import React from 'react'; import { ConstraintAccordionViewHeaderInfo } from './ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo'; import { ConstraintAccordionHeaderActions } from '../../ConstraintAccordionHeaderActions/ConstraintAccordionHeaderActions'; +import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles'; interface IConstraintAccordionViewHeaderProps { constraint: IConstraint; diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo.tsx index 3e2fc4e282..90f5d9ad5f 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo.tsx @@ -15,6 +15,8 @@ const StyledHeaderText = styled('span')(({ theme }) => ({ maxWidth: '100px', minWidth: '100px', marginRight: '10px', + marginTop: 'auto', + marginBottom: 'auto', wordBreak: 'break-word', fontSize: theme.fontSizes.smallBody, [theme.breakpoints.down(710)]: { diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator/ConstraintViewHeaderOperator.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator/ConstraintViewHeaderOperator.tsx index 9cd51b9269..6dcbab9655 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator/ConstraintViewHeaderOperator.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator/ConstraintViewHeaderOperator.tsx @@ -1,9 +1,8 @@ import { IConstraint } from '../../../../../../interfaces/strategy'; import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender'; -import { Tooltip } from '@mui/material'; +import { Tooltip, Box } from '@mui/material'; import { ReactComponent as NegatedIcon } from '../../../../../../assets/icons/24_Negator.svg'; import { ConstraintOperator } from '../../../ConstraintOperator/ConstraintOperator'; -import React from 'react'; import { useStyles } from '../../../ConstraintAccordion.styles'; import { StyledIconWrapper } from '../StyledIconWrapper/StyledIconWrapper'; @@ -22,14 +21,19 @@ export const ConstraintViewHeaderOperator = ({ condition={Boolean(constraint.inverted)} show={ - - - + + + + + } />
- +
); diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderMultipleValues/ConstraintAccordionViewHeaderMultipleValues.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderMultipleValues/ConstraintAccordionViewHeaderMultipleValues.tsx index 2ddb041d2b..5367f42b53 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderMultipleValues/ConstraintAccordionViewHeaderMultipleValues.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderMultipleValues/ConstraintAccordionViewHeaderMultipleValues.tsx @@ -17,6 +17,7 @@ const StyledValuesSpan = styled('span')(({ theme }) => ({ overflow: 'hidden', wordBreak: 'break-word', fontSize: theme.fontSizes.smallBody, + margin: 'auto 0', [theme.breakpoints.down(710)]: { margin: theme.spacing(1, 0), textAlign: 'center', @@ -90,8 +91,7 @@ export const ConstraintAccordionViewHeaderMultipleValues = ({ )} > {!expanded - ? `View all ( - ${constraint?.values?.length})` + ? `View all (${constraint?.values?.length})` : 'View less'}

} diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderSingleValue/ContraintAccordionViewHeaderSingleValue.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderSingleValue/ContraintAccordionViewHeaderSingleValue.tsx index 00818af583..c23bb02388 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderSingleValue/ContraintAccordionViewHeaderSingleValue.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ContraintAccordionViewHeaderSingleValue/ContraintAccordionViewHeaderSingleValue.tsx @@ -4,13 +4,13 @@ import { stringOperators } from '../../../../../../constants/operators'; import { Chip, styled, Tooltip } from '@mui/material'; import { ReactComponent as CaseSensitive } from '../../../../../../assets/icons/24_Text format.svg'; import { formatConstraintValue } from '../../../../../../utils/formatConstraintValue'; -import React from 'react'; import { useStyles } from '../../../ConstraintAccordion.styles'; import { StyledIconWrapper } from '../StyledIconWrapper/StyledIconWrapper'; import { IConstraint } from '../../../../../../interfaces/strategy'; import { useLocationSettings } from '../../../../../../hooks/useLocationSettings'; const StyledSingleValueChip = styled(Chip)(({ theme }) => ({ + margin: 'auto 0', [theme.breakpoints.down(710)]: { margin: theme.spacing(1, 0), }, diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/StyledIconWrapper/StyledIconWrapper.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/StyledIconWrapper/StyledIconWrapper.tsx index dd6568c427..0e2ac4e654 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/StyledIconWrapper/StyledIconWrapper.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/StyledIconWrapper/StyledIconWrapper.tsx @@ -1,16 +1,34 @@ import { styled } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { FC, forwardRef } from 'react'; -export const StyledIconWrapper = styled('div')<{ - marginright?: string; -}>(({ theme, marginright }) => ({ +export const StyledIconWrapperBase = styled('div')<{ + prefix?: boolean; +}>(({ theme }) => ({ backgroundColor: theme.palette.grey[200], width: 28, - height: 48, - display: 'inline-flex', + display: 'flex', + alignItems: 'center', justifyContent: 'center', - padding: '10px 0', + alignSelf: 'stretch', color: theme.palette.primary.main, - marginRight: marginright ? marginright : '1rem', - marginTop: 'auto', - marginBottom: 'auto', + marginRight: '1rem', + borderRadius: theme.shape.borderRadius, })); + +const StyledPrefixIconWrapper = styled(StyledIconWrapperBase)(() => ({ + marginRight: 0, + borderTopRightRadius: 0, + borderBottomRightRadius: 0, +})); + +export const StyledIconWrapper = forwardRef< + HTMLDivElement, + { isPrefix?: boolean; children?: React.ReactNode } +>(({ isPrefix, ...props }, ref) => ( + } + elseShow={() => } + /> +)); diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.styles.ts b/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.styles.ts index ff2bac017d..618eb43670 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.styles.ts +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.styles.ts @@ -2,7 +2,7 @@ import { makeStyles } from 'tss-react/mui'; export const useStyles = makeStyles()(theme => ({ container: { - padding: '0.5rem 0.75rem', + padding: theme.spacing(0.5, 1.5), borderRadius: theme.shape.borderRadius, backgroundColor: theme.palette.grey[200], lineHeight: 1.25, diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.tsx index b21c6159a1..991c97b0a9 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.tsx @@ -5,10 +5,12 @@ import React from 'react'; interface IConstraintOperatorProps { constraint: IConstraint; + hasPrefix?: boolean; } export const ConstraintOperator = ({ constraint, + hasPrefix, }: IConstraintOperatorProps) => { const { classes: styles } = useStyles(); @@ -16,7 +18,13 @@ export const ConstraintOperator = ({ const operatorText = formatOperatorDescription(constraint.operator); return ( -
+
{operatorName}
{operatorText}
diff --git a/frontend/src/component/common/StrategySeparator/StrategySeparator.tsx b/frontend/src/component/common/StrategySeparator/StrategySeparator.tsx index 1a56ea9be0..4867a5c2a7 100644 --- a/frontend/src/component/common/StrategySeparator/StrategySeparator.tsx +++ b/frontend/src/component/common/StrategySeparator/StrategySeparator.tsx @@ -1,24 +1,45 @@ -import { useTheme } from '@mui/material'; +import { styled } from '@mui/material'; +import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'; interface IStrategySeparatorProps { - text: string; + text: 'AND' | 'OR'; } -export const StrategySeparator = ({ text }: IStrategySeparatorProps) => { - const theme = useTheme(); +const StyledContainer = styled('div')(({ theme }) => ({ + height: theme.spacing(2), + position: 'relative', + width: '100%', +})); - return ( -
- {text} -
- ); -}; +const StyledContent = styled('div')(({ theme }) => ({ + padding: theme.spacing(0.75, 1.5), + color: theme.palette.text.primary, + fontSize: theme.fontSizes.smallerBody, + backgroundColor: theme.palette.secondaryContainer, + borderRadius: theme.shape.borderRadius, + position: 'absolute', + zIndex: theme.zIndex.fab, + top: '50%', + left: theme.spacing(3), + transform: 'translateY(-50%)', +})); + +const StyledCenteredContent = styled(StyledContent)(({ theme }) => ({ + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + backgroundColor: theme.palette.secondary.light, + border: `1px solid ${theme.palette.primary.border}`, +})); + +export const StrategySeparator = ({ text }: IStrategySeparatorProps) => ( + + {text}} + elseShow={() => ( + {text} + )} + /> + +); diff --git a/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts b/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts index 65c3de6384..4df809bbce 100644 --- a/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts +++ b/frontend/src/component/common/Table/TablePlaceholder/TablePlaceholder.styles.ts @@ -2,7 +2,7 @@ import { makeStyles } from 'tss-react/mui'; export const useStyles = makeStyles()(theme => ({ emptyStateListItem: { - border: `2px dashed ${theme.palette.grey[100]}`, + border: `2px dashed ${theme.palette.neutral.light}`, padding: '0.8rem', textAlign: 'center', display: 'flex', diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.styles.ts b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.styles.ts index 9575516df7..fd2b71c21f 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.styles.ts +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.styles.ts @@ -1,14 +1,22 @@ import { makeStyles } from 'tss-react/mui'; export const useStyles = makeStyles()(theme => ({ - noItemsParagraph: { - margin: '1rem 0', + container: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', }, - link: { - display: 'block', - margin: '1rem 0 0 0', + title: { + fontSize: theme.fontSizes.bodySize, + textAlign: 'center', + color: theme.palette.text.primary, + marginBottom: theme.spacing(1), }, - envName: { - fontWeight: 'bold', + description: { + color: theme.palette.text.secondary, + fontSize: theme.fontSizes.smallBody, + textAlign: 'center', + marginBottom: theme.spacing(3), }, })); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.tsx index e943aefd83..707599909e 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.tsx @@ -1,7 +1,13 @@ -import NoItems from 'component/common/NoItems/NoItems'; -import StringTruncator from 'component/common/StringTruncator/StringTruncator'; -import { useStyles } from './FeatureStrategyEmpty.styles'; +import { Link } from 'react-router-dom'; +import { Box } 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 { FeatureStrategyMenu } from '../FeatureStrategyMenu/FeatureStrategyMenu'; +import { PresetCard } from './PresetCard/PresetCard'; +import { useStyles } from './FeatureStrategyEmpty.styles'; +import { formatUnknownError } from 'utils/formatUnknownError'; interface IFeatureStrategyEmptyProps { projectId: string; @@ -15,30 +21,58 @@ export const FeatureStrategyEmpty = ({ environmentId, }: IFeatureStrategyEmptyProps) => { const { classes: styles } = useStyles(); + const { addStrategyToFeature } = useFeatureStrategyApi(); + const { setToastData, setToastApiError } = useToast(); + const { refetchFeature } = useFeature(projectId, featureId); + + const onAfterAddStrategy = () => { + refetchFeature(); + setToastData({ + title: 'Strategy created', + text: 'Successfully created strategy', + type: 'success', + }); + }; + + const onAddSimpleStrategy = async () => { + try { + await addStrategyToFeature(projectId, featureId, environmentId, { + name: 'default', + parameters: {}, + constraints: [], + }); + onAfterAddStrategy(); + } catch (error) { + setToastApiError(formatUnknownError(error)); + } + }; + + const onAddGradualRolloutStrategy = async () => { + try { + await addStrategyToFeature(projectId, featureId, environmentId, { + name: 'flexibleRollout', + parameters: { + rollout: '50', + stickiness: 'default', + }, + constraints: [], + }); + onAfterAddStrategy(); + } catch (error) { + setToastApiError(formatUnknownError(error)); + } + }; return ( - -

- No strategies added in the{' '} - {' '} - environment -

-

+

+
+ You have not defined any strategies yet. +
+

Strategies added in this environment will only be executed if - the SDK is using an API key configured for this environment. - - Read more here - + the SDK is using an{' '} + API key configured for this + environment.

- + + Or use a strategy template + + + + The standard strategy is strictly on/off for your entire + userbase. + + + Roll out to a percentage of your userbase. + + +
); }; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/PresetCard/PresetCard.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/PresetCard/PresetCard.tsx new file mode 100644 index 0000000000..f4e5cfea3f --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/PresetCard/PresetCard.tsx @@ -0,0 +1,35 @@ +import { Button, Card, CardContent, Typography } from '@mui/material'; +import { FC } from 'react'; + +interface IPresetCardProps { + title: string; + onClick: () => void; +} + +export const PresetCard: FC = ({ + title, + children, + onClick, +}) => ( + + + + {title} + + + {children} + + + + + +); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyIcon/FeatureStrategyIcon.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyIcon/FeatureStrategyIcon.tsx index 1665665354..a90721befa 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyIcon/FeatureStrategyIcon.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyIcon/FeatureStrategyIcon.tsx @@ -16,7 +16,9 @@ export const FeatureStrategyIcon = ({ return ( - + <> + + ); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx index bb8aa10114..481925b40b 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx @@ -1,7 +1,7 @@ +import React, { useState } from 'react'; import PermissionButton, { IPermissionButtonProps, } from 'component/common/PermissionButton/PermissionButton'; -import React, { useState } from 'react'; import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions'; import { Popover } from '@mui/material'; import { FeatureStrategyMenuCards } from './FeatureStrategyMenuCards/FeatureStrategyMenuCards'; diff --git a/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats.styles.ts b/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats.styles.ts index c6cc8001fb..80c05eb61e 100644 --- a/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats.styles.ts +++ b/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats.styles.ts @@ -3,7 +3,7 @@ import { makeStyles } from 'tss-react/mui'; export const useStyles = makeStyles()(theme => ({ item: { padding: theme.spacing(2), - background: theme.palette.grey[100], + background: theme.palette.secondaryContainer, borderRadius: theme.spacing(2), textAlign: 'center', [theme.breakpoints.up('md')]: { diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.styles.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.styles.ts new file mode 100644 index 0000000000..01e1765bca --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.styles.ts @@ -0,0 +1,14 @@ +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', + }, +})); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx new file mode 100644 index 0000000000..4cca8ddd9f --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from 'react'; +import { Alert } from '@mui/material'; +import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi'; +import { formatUnknownError } from 'utils/formatUnknownError'; +import useToast from 'hooks/useToast'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { StrategyDraggableItem } from './StrategyDraggableItem/StrategyDraggableItem'; +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 { + isDisabled: boolean; + featureEnvironment?: IFeatureEnvironment; +} + +const EnvironmentAccordionBody = ({ + featureEnvironment, + isDisabled, +}: IEnvironmentAccordionBodyProps) => { + const projectId = useRequiredPathParam('projectId'); + const featureId = useRequiredPathParam('featureId'); + const { setStrategiesSortOrder } = useFeatureStrategyApi(); + const { setToastData, setToastApiError } = useToast(); + const { refetchFeature } = useFeature(projectId, featureId); + + const [strategies, setStrategies] = useState( + featureEnvironment?.strategies || [] + ); + const { classes: styles } = useStyles(); + useEffect(() => { + // Use state to enable drag and drop, but switch to API output when it arrives + setStrategies(featureEnvironment?.strategies || []); + }, [featureEnvironment?.strategies]); + + if (!featureEnvironment) { + return null; + } + + const onDragAndDrop = async ( + from: number, + to: number, + dropped?: boolean + ) => { + if (from !== to && dropped) { + const newStrategies = [...strategies]; + const movedStrategy = newStrategies.splice(from, 1)[0]; + newStrategies.splice(to, 0, movedStrategy); + setStrategies(newStrategies); + try { + await setStrategiesSortOrder( + projectId, + featureId, + featureEnvironment.name, + [...newStrategies].map((strategy, sortOrder) => ({ + id: strategy.id, + sortOrder, + })) + ); + refetchFeature(); + setToastData({ + title: 'Order of strategies updated', + type: 'success', + }); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + } + }; + + return ( +
+
+ 0 && isDisabled} + show={() => ( + + This environment is disabled, which means that none + of your strategies are executing. + + )} + /> + 0} + show={ + <> + {strategies.map((strategy, index) => ( + + ))} + + } + elseShow={ + + } + /> +
+
+ ); +}; + +export default EnvironmentAccordionBody; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx new file mode 100644 index 0000000000..7465b6b3f6 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx @@ -0,0 +1,35 @@ +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; +import { MoveListItem, useDragItem } from 'hooks/useDragItem'; +import { IFeatureStrategy } from 'interfaces/strategy'; +import { StrategyItem } from './StrategyItem/StrategyItem'; + +interface IStrategyDraggableItemProps { + strategy: IFeatureStrategy; + environmentName: string; + index: number; + onDragAndDrop: MoveListItem; +} + +export const StrategyDraggableItem = ({ + strategy, + index, + environmentName, + onDragAndDrop, +}: IStrategyDraggableItemProps) => { + const ref = useDragItem(index, onDragAndDrop); + + return ( +
+ 0} + show={} + /> + +
+ ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecutionChips/FeatureOverviewExecutionChips.styles.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.styles.ts similarity index 62% rename from frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecutionChips/FeatureOverviewExecutionChips.styles.ts rename to frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.styles.ts index 45ebf5aec8..fb8f37bb11 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecutionChips/FeatureOverviewExecutionChips.styles.ts +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.styles.ts @@ -1,7 +1,12 @@ import { makeStyles } from 'tss-react/mui'; export const useStyles = makeStyles()(theme => ({ - container: { textAlign: 'center' }, + container: { + width: '100%', + padding: theme.spacing(2, 3), + borderRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + }, chip: { margin: '0.25rem', }, diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecutionChips/FeatureOverviewExecutionChips.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.tsx similarity index 84% rename from frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecutionChips/FeatureOverviewExecutionChips.tsx rename to frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.tsx index 42168f0688..727d9ce42d 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecutionChips/FeatureOverviewExecutionChips.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.tsx @@ -1,17 +1,14 @@ import { Chip } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { useStyles } from './FeatureOverviewExecutionChips.styles'; +import { useStyles } from './ConstraintItem.styles'; import StringTruncator from 'component/common/StringTruncator/StringTruncator'; -interface IFeatureOverviewExecutionChipsProps { +interface IConstraintItemProps { value: string[]; text: string; } -const FeatureOverviewExecutionChips = ({ - value, - text, -}: IFeatureOverviewExecutionChipsProps) => { +export const ConstraintItem = ({ value, text }: IConstraintItemProps) => { const { classes: styles } = useStyles(); return (
@@ -44,5 +41,3 @@ const FeatureOverviewExecutionChips = ({
); }; - -export default FeatureOverviewExecutionChips; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecution.styles.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.styles.ts similarity index 59% rename from frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecution.styles.ts rename to frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.styles.ts index 779b243dbd..e2d49389e1 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecution.styles.ts +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.styles.ts @@ -9,4 +9,10 @@ export const useStyles = makeStyles()(theme => ({ valueSeparator: { color: theme.palette.grey[700], }, + summary: { + width: '100%', + padding: theme.spacing(2, 3), + borderRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + }, })); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecution.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx similarity index 79% rename from frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecution.tsx rename to frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx index f832e0e468..e1214863a9 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecution.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx @@ -1,37 +1,29 @@ import { Fragment } from 'react'; -import { - IFeatureStrategy, - IFeatureStrategyParameters, - IConstraint, -} from 'interfaces/strategy'; +import { Box, Chip } from '@mui/material'; +import { IFeatureStrategy } from 'interfaces/strategy'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; -import FeatureOverviewExecutionChips from './FeatureOverviewExecutionChips/FeatureOverviewExecutionChips'; +import { ConstraintItem } from './ConstraintItem/ConstraintItem'; import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; import StringTruncator from 'component/common/StringTruncator/StringTruncator'; 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 'component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecution.styles'; +import { useStyles } from './StrategyExecution.styles'; import { parseParameterString, parseParameterNumber, parseParameterStrings, } from 'utils/parseParameter'; -interface IFeatureOverviewExecutionProps { - parameters: IFeatureStrategyParameters; - constraints?: IConstraint[]; +interface IStrategyExecutionProps { strategy: IFeatureStrategy; percentageFill?: string; } -const FeatureOverviewExecution = ({ - parameters, - constraints = [], - strategy, -}: IFeatureOverviewExecutionProps) => { +export const StrategyExecution = ({ strategy }: IStrategyExecutionProps) => { + const { parameters, constraints = [] } = strategy; const { classes: styles } = useStyles(); const { strategies } = useStrategies(); const { uiConfig } = useUiConfig(); @@ -52,51 +44,51 @@ const FeatureOverviewExecution = ({ case 'rollout': case 'Rollout': return ( - -

- {parameters[key]}% of your base{' '} - {constraints.length > 0 - ? 'who match constraints' - : ''}{' '} - is included. -

- + -
+
+ {' '} + of your base{' '} + {constraints.length > 0 + ? 'who match constraints' + : ''}{' '} + is included. +
+ ); case 'userIds': case 'UserIds': const users = parseParameterStrings(parameters[key]); return ( - + ); case 'hostNames': case 'HostNames': const hosts = parseParameterStrings(parameters[key]); return ( - + ); case 'IPs': const IPs = parseParameterStrings(parameters[key]); - return ( - - ); + return ; case 'stickiness': case 'groupId': return null; @@ -117,10 +109,7 @@ const FeatureOverviewExecution = ({ ); return ( - + } @@ -130,13 +119,21 @@ const FeatureOverviewExecution = ({ case 'percentage': return ( -

- {strategy?.parameters[param.name]}% of your base{' '} +

+ {' '} + of your base{' '} {constraints?.length > 0 ? 'who match constraints' : ''}{' '} is included. -

+
The standard strategy is on for all users.

} + show={ + + The standard strategy is{' '} + {' '} + for all users. + + } /> {renderParameters()} {renderCustomStrategy()} ); }; - -export default FeatureOverviewExecution; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategy/FeatureOverviewEnvironmentStrategy.styles.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.styles.ts similarity index 75% rename from frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategy/FeatureOverviewEnvironmentStrategy.styles.ts rename to frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.styles.ts index 4bfd97e8ff..2972a4ee62 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategy/FeatureOverviewEnvironmentStrategy.styles.ts +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.styles.ts @@ -2,18 +2,20 @@ import { makeStyles } from 'tss-react/mui'; export const useStyles = makeStyles()(theme => ({ container: { - borderRadius: theme.shape.borderRadius, + borderRadius: theme.shape.borderRadiusMedium, border: `1px solid ${theme.palette.grey[300]}`, '& + &': { marginTop: '1rem', }, + background: theme.palette.background.default, }, header: { - padding: '0.5rem', + padding: theme.spacing(0.5, 2), display: 'flex', gap: '0.5rem', alignItems: 'center', borderBottom: `1px solid ${theme.palette.grey[300]}`, + fontWeight: theme.typography.fontWeightMedium, }, icon: { fill: theme.palette.inactiveIcon, @@ -25,7 +27,6 @@ export const useStyles = makeStyles()(theme => ({ body: { padding: '1rem', display: 'grid', - gap: '1rem', justifyItems: 'center', }, })); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategy/FeatureOverviewEnvironmentStrategy.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx similarity index 70% rename from frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategy/FeatureOverviewEnvironmentStrategy.tsx rename to frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx index 7844f5706d..f44f82ce6a 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategy/FeatureOverviewEnvironmentStrategy.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx @@ -1,5 +1,5 @@ -import { Edit } from '@mui/icons-material'; -import { useTheme } from '@mui/material/styles'; +import { DragIndicator, Edit } from '@mui/icons-material'; +import { styled, useTheme, IconButton } from '@mui/material'; import { Link } from 'react-router-dom'; import { IFeatureStrategy } from 'interfaces/strategy'; import { @@ -8,28 +8,36 @@ import { } from 'utils/strategyNames'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions'; -import FeatureOverviewExecution from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecution'; -import { useStyles } from './FeatureOverviewEnvironmentStrategy.styles'; import { formatEditStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit'; import { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/FeatureStrategyRemove/FeatureStrategyRemove'; import StringTruncator from 'component/common/StringTruncator/StringTruncator'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import { StrategyExecution } from './StrategyExecution/StrategyExecution'; +import { useStyles } from './StrategyItem.styles'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -interface IFeatureOverviewEnvironmentStrategyProps { +interface IStrategyItemProps { environmentId: string; strategy: IFeatureStrategy; + isDraggable?: boolean; } -const FeatureOverviewEnvironmentStrategy = ({ +const DragIcon = styled(IconButton)(({ theme }) => ({ + padding: 0, + cursor: 'inherit', + transition: 'color 0.2s ease-in-out', +})); + +export const StrategyItem = ({ environmentId, strategy, -}: IFeatureOverviewEnvironmentStrategyProps) => { + isDraggable, +}: IStrategyItemProps) => { const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); const theme = useTheme(); const { classes: styles } = useStyles(); const Icon = getFeatureStrategyIcon(strategy.name); - const { parameters, constraints } = strategy; const editStrategyPath = formatEditStrategyPath( projectId, @@ -41,6 +49,17 @@ const FeatureOverviewEnvironmentStrategy = ({ return (
+ ( + + + + )} + />
-
); }; - -export default FeatureOverviewEnvironmentStrategy; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentFooter/FeatureOverviewEnvironmentFooter.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentFooter/EnvironmentFooter.tsx similarity index 52% rename from frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentFooter/FeatureOverviewEnvironmentFooter.tsx rename to frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentFooter/EnvironmentFooter.tsx index 56ef5df658..7c2ea3285e 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentFooter/FeatureOverviewEnvironmentFooter.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentFooter/EnvironmentFooter.tsx @@ -1,27 +1,22 @@ import { IFeatureEnvironmentMetrics } from 'interfaces/featureToggle'; -import { useStyles } from '../FeatureOverviewEnvironment.styles'; import { FeatureMetricsStats } from 'component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats'; +import { SectionSeparator } from '../SectionSeparator/SectionSeparator'; -interface IFeatureOverviewEnvironmentFooterProps { +interface IEnvironmentFooterProps { environmentMetric?: IFeatureEnvironmentMetrics; } -const FeatureOverviewEnvironmentFooter = ({ +export const EnvironmentFooter = ({ environmentMetric, -}: IFeatureOverviewEnvironmentFooterProps) => { - const { classes: styles } = useStyles(); - +}: IEnvironmentFooterProps) => { if (!environmentMetric) { return null; } return ( <> -
-
-
Result
-
-
+ Feature toggle exposure +
); }; -export default FeatureOverviewEnvironmentFooter; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.styles.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.styles.ts index c5858bb049..3e82138c84 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.styles.ts +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.styles.ts @@ -2,13 +2,13 @@ import { makeStyles } from 'tss-react/mui'; export const useStyles = makeStyles()(theme => ({ featureOverviewEnvironment: { - borderRadius: '12.5px', - padding: '0.2rem', - marginBottom: '1rem', - backgroundColor: '#fff', + borderRadius: theme.shape.borderRadiusLarge, + marginBottom: theme.spacing(2), + background: theme.palette.background.default, }, - accordionContainer: { - width: '100%', + accordion: { + boxShadow: 'none', + background: 'none', }, accordionHeader: { boxShadow: 'none', @@ -22,16 +22,26 @@ export const useStyles = makeStyles()(theme => ({ 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}`, + }, + accordionDetailsDisabled: { + borderBottom: `4px solid ${theme.palette.dividerAlternative}`, + }, accordionBody: { width: '100%', position: 'relative', - paddingBottom: '1rem', + paddingBottom: theme.spacing(2), }, header: { display: 'flex', justifyContent: 'center', flexDirection: 'column', - paddingTop: '1.5rem', + // paddingTop: '1.5rem', }, headerTitle: { display: 'flex', @@ -46,14 +56,6 @@ export const useStyles = makeStyles()(theme => ({ marginBottom: '0.5rem', }, }, - disabledIndicatorPos: { - position: 'absolute', - top: '15px', - left: '20px', - [theme.breakpoints.down(560)]: { - top: '13px', - }, - }, iconContainer: { backgroundColor: theme.palette.primary.light, borderRadius: '50%', @@ -69,32 +71,14 @@ export const useStyles = makeStyles()(theme => ({ width: '17px', height: '17px', }, - resultInfo: { - display: 'flex', - alignItems: 'center', - margin: '1rem 0', - }, - leftWing: { - height: '2px', - backgroundColor: theme.palette.grey[300], - width: '90%', - }, - separatorText: { - fontSize: theme.fontSizes.smallBody, - textAlign: 'center', - padding: '0 1rem', - }, - rightWing: { - height: '2px', - backgroundColor: theme.palette.grey[300], - width: '90%', - }, linkContainer: { display: 'flex', justifyContent: 'flex-end', marginBottom: '1rem', }, truncator: { + fontSize: theme.fontSizes.bodySize, + fontWeight: theme.typography.fontWeightMedium, [theme.breakpoints.down(560)]: { textAlign: 'center', }, diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx index 3a16ca9372..fee4f75c1c 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx @@ -1,4 +1,12 @@ -import { Accordion, AccordionDetails, AccordionSummary } from '@mui/material'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Chip, + useTheme, +} 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'; @@ -8,14 +16,14 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon'; import StringTruncator from 'component/common/StringTruncator/StringTruncator'; import { useStyles } from './FeatureOverviewEnvironment.styles'; -import FeatureOverviewEnvironmentBody from './FeatureOverviewEnvironmentBody/FeatureOverviewEnvironmentBody'; -import FeatureOverviewEnvironmentFooter from './FeatureOverviewEnvironmentFooter/FeatureOverviewEnvironmentFooter'; +import EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAccordionBody'; +import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter'; import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics'; import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu'; 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'; +// import { Badge } from 'component/common/Badge/Badge'; interface IFeatureOverviewEnvironmentProps { env: IFeatureEnvironment; @@ -25,6 +33,7 @@ const FeatureOverviewEnvironment = ({ env, }: IFeatureOverviewEnvironmentProps) => { const { classes: styles } = useStyles(); + const theme = useTheme(); const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); const { metrics } = useFeatureMetrics(projectId, featureId); @@ -38,38 +47,57 @@ const FeatureOverviewEnvironment = ({ featureEnvironment => featureEnvironment.name === env.name ); - const getOverviewText = () => { - if (env.enabled) { - return `${environmentMetric?.yes} received this feature because the following strategies are executing`; - } - return `This environment is disabled, which means that none of your strategies are executing`; - }; - return ( -
+
} > -
+
-

- Feature toggle execution for  +

-

+ + } + /> +
- - Disabled - - } - />
- -
- - 0 - } - show={ - + + 0 + } + show={ + <> + + + + - } - /> -
+ + } + />
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentBody/FeatureOverviewEnvironmentBody.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentBody/FeatureOverviewEnvironmentBody.tsx deleted file mode 100644 index 33f8ebfc5a..0000000000 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentBody/FeatureOverviewEnvironmentBody.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import FeatureOverviewEnvironmentStrategies from '../FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategies'; -import { useStyles } from '../FeatureOverviewEnvironment.styles'; -import { IFeatureEnvironment } from 'interfaces/featureToggle'; -import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty'; -import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; - -interface IFeatureOverviewEnvironmentBodyProps { - getOverviewText: () => string; - featureEnvironment?: IFeatureEnvironment; -} - -const FeatureOverviewEnvironmentBody = ({ - featureEnvironment, - getOverviewText, -}: IFeatureOverviewEnvironmentBodyProps) => { - const projectId = useRequiredPathParam('projectId'); - const featureId = useRequiredPathParam('featureId'); - const { classes: styles } = useStyles(); - - if (!featureEnvironment) { - return null; - } - - return ( -
-
-
-
-
- {getOverviewText()} -
-
-
- 0} - show={ - <> - - - } - elseShow={ - - } - /> -
-
- ); -}; -export default FeatureOverviewEnvironmentBody; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.styles.ts b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.styles.ts index 2d6af9211d..c44e909ede 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.styles.ts +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.styles.ts @@ -20,7 +20,7 @@ export const useStyles = makeStyles()(theme => ({ }, }, infoParagraph: { - maxWidth: '215px', + maxWidth: '270px', marginTop: '0.25rem', fontSize: theme.fontSizes.smallBody, [theme.breakpoints.down(700)]: { diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategies.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategies.tsx deleted file mode 100644 index 782e33fff1..0000000000 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategies.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { IFeatureStrategy } from 'interfaces/strategy'; -import FeatureOverviewEnvironmentStrategy from './FeatureOverviewEnvironmentStrategy/FeatureOverviewEnvironmentStrategy'; - -interface IFeatureOverviewEnvironmentStrategiesProps { - strategies: IFeatureStrategy[]; - environmentName: string; -} - -const FeatureOverviewEnvironmentStrategies = ({ - strategies, - environmentName, -}: IFeatureOverviewEnvironmentStrategiesProps) => { - return ( - <> - {strategies.map(strategy => ( - - ))} - - ); -}; - -export default FeatureOverviewEnvironmentStrategies; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/SectionSeparator/SectionSeparator.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/SectionSeparator/SectionSeparator.tsx new file mode 100644 index 0000000000..d3c39ae7a1 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/SectionSeparator/SectionSeparator.tsx @@ -0,0 +1,35 @@ +import { FC } from 'react'; +import { styled } from '@mui/material'; + +const SeparatorContainer = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + margin: '1rem 0', + position: 'relative', + '&:before': { + content: '""', + position: 'absolute', + top: '50%', + transform: 'translateY(-50%)', + height: 2, + width: '100%', + backgroundColor: theme.palette.divider, + }, +})); + +const SeparatorContent = styled('span')(({ theme }) => ({ + fontSize: theme.fontSizes.subHeader, + textAlign: 'center', + padding: '0 1rem', + background: theme.palette.secondaryContainer, + position: 'relative', + maxWidth: '80%', + color: theme.palette.text.secondary, +})); + +export const SectionSeparator: FC = ({ children }) => ( + + {children} + +); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironments.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironments.tsx index 95e27e9b81..1742a78cbc 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironments.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironments.tsx @@ -11,13 +11,13 @@ const FeatureOverviewEnvironments = () => { const { environments } = feature; - const renderEnvironments = () => { - return environments?.map(env => { - return ; - }); - }; - - return <>{renderEnvironments()}; + return ( + <> + {environments?.map(env => ( + + ))} + + ); }; export default FeatureOverviewEnvironments; diff --git a/frontend/src/component/history/EventLog/EventLog.styles.ts b/frontend/src/component/history/EventLog/EventLog.styles.ts index 29b650ec73..cff9878cf4 100644 --- a/frontend/src/component/history/EventLog/EventLog.styles.ts +++ b/frontend/src/component/history/EventLog/EventLog.styles.ts @@ -2,7 +2,7 @@ import { makeStyles } from 'tss-react/mui'; export const useStyles = makeStyles()(theme => ({ eventEntry: { - border: `1px solid ${theme.palette.grey[100]}`, + border: `1px solid ${theme.palette.neutral.light}`, padding: '1rem', margin: '1rem 0', borderRadius: theme.shape.borderRadius, diff --git a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundEditor/PlaygroundEditor.tsx b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundEditor/PlaygroundEditor.tsx index 9fae5b6bf7..d57b527dd1 100644 --- a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundEditor/PlaygroundEditor.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundEditor/PlaygroundEditor.tsx @@ -14,7 +14,7 @@ interface IPlaygroundEditorProps { const StyledEditorHeader = styled('aside')(({ theme }) => ({ height: '50px', - backgroundColor: theme.palette.grey[100], + backgroundColor: theme.palette.neutral.light, borderTopRightRadius: theme.shape.borderRadiusMedium, borderTopLeftRadius: theme.shape.borderRadiusMedium, padding: theme.spacing(1, 2), diff --git a/frontend/src/component/project/ProjectCard/ProjectCard.styles.ts b/frontend/src/component/project/ProjectCard/ProjectCard.styles.ts index 2fdecd2dda..1f20c987bf 100644 --- a/frontend/src/component/project/ProjectCard/ProjectCard.styles.ts +++ b/frontend/src/component/project/ProjectCard/ProjectCard.styles.ts @@ -16,7 +16,7 @@ export const useStyles = makeStyles()(theme => ({ }, '&:hover': { transition: 'background-color 0.2s ease-in-out', - backgroundColor: theme.palette.grey[100], + backgroundColor: theme.palette.neutral.light, }, }, header: { diff --git a/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts index 4cfcabeb91..34a62cc5c3 100644 --- a/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts +++ b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts @@ -10,7 +10,7 @@ export const useStyles = makeStyles()(theme => ({ }, headerContainer: { display: 'flex', padding: '0.5rem' }, divider: { - backgroundColor: theme.palette.grey[100], + backgroundColor: theme.palette.neutral.light, height: '1px', width: '100%', }, diff --git a/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts b/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts index bf695c844a..f22a3a3220 100644 --- a/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts +++ b/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts @@ -1,4 +1,8 @@ -import { IFeatureStrategyPayload, IFeatureStrategy } from 'interfaces/strategy'; +import { + IFeatureStrategyPayload, + IFeatureStrategy, + IFeatureStrategySortOrder, +} from 'interfaces/strategy'; import useAPI from '../useApi/useApi'; const useFeatureStrategyApi = () => { @@ -52,10 +56,26 @@ const useFeatureStrategyApi = () => { await makeRequest(req.caller, req.id); }; + const setStrategiesSortOrder = async ( + projectId: string, + featureId: string, + environmentId: string, + payload: IFeatureStrategySortOrder[] + ): Promise => { + const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies/set-sort-order`; + const req = createRequest( + path, + { method: 'POST', body: JSON.stringify(payload) }, + 'setStrategiesSortOrderOnFeature' + ); + await makeRequest(req.caller, req.id); + }; + return { addStrategyToFeature, updateStrategyOnFeature, deleteStrategyFromFeature, + setStrategiesSortOrder, loading, errors, }; diff --git a/frontend/src/interfaces/strategy.ts b/frontend/src/interfaces/strategy.ts index f81df26ad6..a9564f1aca 100644 --- a/frontend/src/interfaces/strategy.ts +++ b/frontend/src/interfaces/strategy.ts @@ -51,3 +51,8 @@ export interface IConstraint { operator: Operator; contextName: string; } + +export interface IFeatureStrategySortOrder { + id: string; + sortOrder: number; +} diff --git a/frontend/src/themes/theme.ts b/frontend/src/themes/theme.ts index 8f23875c5e..14558c5061 100644 --- a/frontend/src/themes/theme.ts +++ b/frontend/src/themes/theme.ts @@ -26,6 +26,9 @@ export default createTheme({ fontSize: '1.5rem', lineHeight: 1.875, }, + caption: { + fontSize: `${12 / 16}rem`, + }, }, fontSizes: { mainHeader: '1.25rem', @@ -287,5 +290,28 @@ export default createTheme({ }, }, }, + MuiChip: { + styleOverrides: { + root: ({ ownerState, theme }) => ({ + ...(ownerState.variant === 'outlined' && + ownerState.size === 'small' && { + borderRadius: theme.shape.borderRadius, + margin: 0, + borderWidth: 1, + borderStyle: 'solid', + fontWeight: theme.typography.fontWeightBold, + fontSize: theme.typography.caption.fontSize, + ...(ownerState.color === 'success' && { + backgroundColor: colors.green[50], + borderColor: theme.palette.success.border, + color: theme.palette.success.dark, + }), + ...(ownerState.color === 'default' && { + color: theme.palette.text.secondary, + }), + }), + }), + }, + }, }, }); diff --git a/frontend/src/utils/strategyNames.ts b/frontend/src/utils/strategyNames.ts index dfc6b163bb..da608aa082 100644 --- a/frontend/src/utils/strategyNames.ts +++ b/frontend/src/utils/strategyNames.ts @@ -2,6 +2,7 @@ import LocationOnIcon from '@mui/icons-material/LocationOn'; import PeopleIcon from '@mui/icons-material/People'; import LanguageIcon from '@mui/icons-material/Language'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; +import CodeIcon from '@mui/icons-material/Code'; import { ReactComponent as RolloutIcon } from 'assets/icons/rollout.svg'; import { ElementType } from 'react'; @@ -11,6 +12,8 @@ export const formatStrategyName = (strategyName: string): string => { export const getFeatureStrategyIcon = (strategyName: string): ElementType => { switch (strategyName) { + case 'default': + return PowerSettingsNewIcon; case 'remoteAddress': return LanguageIcon; case 'flexibleRollout': @@ -20,7 +23,7 @@ export const getFeatureStrategyIcon = (strategyName: string): ElementType => { case 'applicationHostname': return LocationOnIcon; default: - return PowerSettingsNewIcon; + return CodeIcon; } };