diff --git a/frontend/src/component/common/Badge/Badge.tsx b/frontend/src/component/common/Badge/Badge.tsx index 8d330362b2..a6a16a9da2 100644 --- a/frontend/src/component/common/Badge/Badge.tsx +++ b/frontend/src/component/common/Badge/Badge.tsx @@ -12,6 +12,7 @@ import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender' type Color = 'info' | 'success' | 'warning' | 'error' | 'secondary' | 'neutral'; interface IBadgeProps { + as?: React.ElementType; color?: Color; icon?: ReactElement; iconRight?: boolean; @@ -69,6 +70,7 @@ const BadgeIcon = (color: Color, icon: ReactElement, iconRight = false) => ( export const Badge: FC = forwardRef( ( { + as = 'div', color = 'neutral', icon, iconRight, @@ -80,6 +82,7 @@ export const Badge: FC = forwardRef( ref: ForwardedRef ) => ( - Import toggle configuration - - ), - content: ( - <> - - This is a cool feature that enables you to import - your toggle configuration. This is just an example - and not part of the final guide. - - - ), - disableBeacon: true, - }, - ], - }, - { - title: 'New feature toggle', - steps: [ - { - target: 'button[data-testid="NAVIGATE_TO_CREATE_FEATURE"]', - title: ( - - Add a new feature toggle - - ), - content: ( - <> - - You can use this button to add a new feature toggle. - This is just an example and not part of the final - guide. - - - ), - disableBeacon: true, - }, - ], - }, - { - title: 'Enable/disable a feature toggle', - steps: [ - { - target: '.MuiSwitch-sizeMedium', - title: ( - - Enable/disable a feature toggle - - ), - content: ( - <> - - The simplest way to use a feature toggle is to - enable or disable it for everyone (on/off). - - } - > - Look at the demo page when toggling! - - - ), - disableBeacon: true, - }, - ], - }, - { - title: 'Community', - steps: [ - { - target: 'a[href="https://twitter.com/getunleash"]', - title: ( - - Follow us on Twitter! - - ), - content: ( - <> - - Following us on Twitter is one of the easiest ways - of keeping up with us. This is just an example and - not part of the final guide. - - - ), - disableBeacon: true, - }, - { - target: 'a[href="https://www.linkedin.com/company/getunleash"]', - title: ( - - Follow us on LinkedIn! - - ), - content: ( - <> - - You can also follow us LinkedIn. This is just an - example and not part of the final guide. - - - ), - disableBeacon: true, - }, - { - target: 'a[href="https://github.com/Unleash/unleash"]', - title: ( - - Check out Unleash on GitHub! - - ), - content: ( - <> - - Unleash is open-source, check out the project on - GitHub. This is just an example and not part of the - final guide. - - - ), - disableBeacon: true, - }, - { - target: 'a[href="https://slack.unleash.run"]', - title: ( - Join us on Slack! - ), - content: ( - <> - - Join our community in Slack. This is just an example - and not part of the final guide. - - - ), - disableBeacon: true, - }, - ], - }, -]; - export const Demo = () => { const { uiConfig } = useUiConfig(); const [loaded, setLoaded] = useState(false); const [expanded, setExpanded] = useState(storedProgress.expanded ?? true); - const [run, setRun] = useState(storedProgress.run ?? false); + const [run, setRun] = useState(false); const [topic, setTopic] = useState(storedProgress.topic ?? 0); const [steps, setSteps] = useState(storedProgress.steps ?? [0]); useEffect(() => { setTimeout(() => { setLoaded(true); + if (storedProgress.run) { + setRun(true); + } }, 1000); }, []); diff --git a/frontend/src/component/demo/DemoSteps/DemoSteps.tsx b/frontend/src/component/demo/DemoSteps/DemoSteps.tsx index 7bef1012e6..8eabe169fe 100644 --- a/frontend/src/component/demo/DemoSteps/DemoSteps.tsx +++ b/frontend/src/component/demo/DemoSteps/DemoSteps.tsx @@ -1,14 +1,13 @@ import Joyride, { ACTIONS, CallBackProps, - EVENTS, - STATUS, TooltipRenderProps, } from 'react-joyride'; import { Button, Typography, styled, useTheme } from '@mui/material'; -import { ITutorialTopic } from '../Demo'; -import { useEffect } from 'react'; +import { ITutorialTopic, ITutorialTopicStep } from '../demo-topics'; +import { useEffect, useState } from 'react'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useLocation, useNavigate } from 'react-router-dom'; const StyledTooltip = styled('div')(({ theme }) => ({ backgroundColor: theme.palette.background.paper, @@ -66,34 +65,62 @@ export const DemoSteps = ({ topics, }: IDemoStepsProps) => { const theme = useTheme(); + const navigate = useNavigate(); + const location = useLocation(); + const [flow, setFlow] = useState<'next' | 'back'>('next'); + + const abortController = new AbortController(); const skip = () => { - setRun(false); + abortController.abort(); setTopic(-1); setExpanded(false); }; + const setStep = (topic: number, step: number) => { + setSteps(steps => { + const newSteps = [...steps]; + newSteps[topic] = step; + return newSteps; + }); + }; + const back = () => { + setFlow('back'); if (steps[topic] === 0) { - setRun(false); const newTopic = topic - 1; setTopic(newTopic); - setSteps(steps => { - const newSteps = [...steps]; - newSteps[newTopic] = topics[newTopic].steps.length - 1; - return newSteps; - }); + setStep(newTopic, topics[newTopic].steps.length - 1); } else { - setSteps(steps => { - const newSteps = [...steps]; - newSteps[topic] = steps[topic] - 1; - return newSteps; - }); + setStep(topic, steps[topic] - 1); } }; - const joyrideCallback = (data: CallBackProps) => { - const { action, index, status, type, step } = data; + const nextTopic = () => { + if (topic === topics.length - 1) { + setTopic(-1); + setExpanded(false); + } else { + const newTopic = topic + 1; + setTopic(newTopic); + setStep(newTopic, 0); + } + }; + + const next = (index = steps[topic]) => { + setFlow('next'); + setStep(topic, index + 1); + if (index === topics[topic].steps.length - 1) { + nextTopic(); + } + }; + + const joyrideCallback = ( + data: CallBackProps & { + step: ITutorialTopicStep; + } + ) => { + const { action, index, step } = data; if (action === ACTIONS.UPDATE) { const el = document.querySelector(step.target as string); @@ -101,54 +128,86 @@ export const DemoSteps = ({ el.scrollIntoView({ block: 'center', }); + if (!step.nextButton) { + const clickHandler = (e: Event) => { + abortController.abort(); + next(index); + if (step.preventDefault) { + e.preventDefault(); + } + }; + + if (step.anyClick) { + window.addEventListener('click', clickHandler, { + signal: abortController.signal, + }); + } else { + el.addEventListener('click', clickHandler, { + signal: abortController.signal, + }); + } + } } } - if ( - ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND] as string[]).includes( - type - ) - ) { - const newStep = index + (action === ACTIONS.PREV ? -1 : 1); - setSteps(steps => { - const newSteps = [...steps]; - newSteps[topic] = newStep; - return newSteps; - }); - } else if ( - ([STATUS.FINISHED, STATUS.SKIPPED] as string[]).includes(status) - ) { - setRun(false); - if (topic === topics.length - 1) { - setTopic(-1); - setExpanded(false); + if (run && !document.querySelector(step.target as string)) { + if (step.optional && flow === 'next') { + next(); } else { - const newTopic = topic + 1; - setTopic(newTopic); - setSteps(steps => { - const newSteps = [...steps]; - newSteps[newTopic] = 0; - return newSteps; - }); + back(); } } }; + const onBack = (step: ITutorialTopicStep) => { + if (step.backCloseModal) { + ( + document.querySelector('.MuiModal-backdrop') as HTMLElement + )?.click(); + } + if (step.backCollapseExpanded) { + ( + document.querySelector( + '.Mui-expanded[role="button"]' + ) as HTMLElement + )?.click(); + } + back(); + }; + useEffect(() => { - setRun(true); + setRun(false); + if (topic === -1) return; + const currentTopic = topics[topic]; + const currentStep = steps[topic]; + const href = currentTopic.steps[currentStep]?.href; + if (href && location.pathname !== href) { + navigate(href); + } + currentTopic.setup?.(); + + setTimeout(() => { + setRun(true); + }, 200); }, [topic, steps]); if (topic === -1) return null; + const joyrideSteps = topics[topic].steps.map(step => ({ + ...step, + disableBeacon: true, + })); + return ( { - const { onClick } = primaryProps; - - return ( - - - {step.title} + }: TooltipRenderProps & { + step: ITutorialTopicStep; + }) => ( + + + + {topics[topic].title} + + } + /> + 1} + show={ + + (step {steps[topic] + 1} of{' '} + {topics[topic].steps.length}) + + } + /> + + {step.content} + + + 1} + condition={topic > 0 || steps[topic] > 0} show={ - onBack(step)} > - (step {steps[topic] + 1} of{' '} - {topics[topic].steps.length}) - + Back + } /> - - {step.content} - - - - 0 || steps[topic] > 0} - show={ - - } - /> - - - - - ); - }} + next(steps[topic])} + variant="contained" + > + {topic === topics.length - 1 && + steps[topic] === + topics[topic].steps.length - 1 + ? 'Finish' + : 'Next'} + + } + /> + + + + )} /> ); }; diff --git a/frontend/src/component/demo/DemoTopics/DemoTopics.tsx b/frontend/src/component/demo/DemoTopics/DemoTopics.tsx index 2105e08d71..27159dfaf7 100644 --- a/frontend/src/component/demo/DemoTopics/DemoTopics.tsx +++ b/frontend/src/component/demo/DemoTopics/DemoTopics.tsx @@ -9,7 +9,7 @@ import { styled, } from '@mui/material'; import { CheckCircle, CircleOutlined, ExpandMore } from '@mui/icons-material'; -import { ITutorialTopic } from '../Demo'; +import { ITutorialTopic } from '../demo-topics'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; const StyledAccordion = styled(Accordion)(({ theme }) => ({ diff --git a/frontend/src/component/demo/demo-setup.ts b/frontend/src/component/demo/demo-setup.ts new file mode 100644 index 0000000000..da9183b7d2 --- /dev/null +++ b/frontend/src/component/demo/demo-setup.ts @@ -0,0 +1,114 @@ +import { IFeatureToggle } from 'interfaces/featureToggle'; +import { formatApiPath } from 'utils/formatPath'; + +export const gradualRollout = async () => { + const projectId = 'default'; + const featureId = 'demoApp.step3'; + const environmentId = 'default'; + + const { environments }: IFeatureToggle = await fetch( + formatApiPath( + `api/admin/projects/${projectId}/features/${featureId}?variantEnvironments=true` + ) + ).then(res => res.json()); + + const strategies = + environments.find(({ name }) => name === environmentId)?.strategies || + []; + + if (!strategies.find(({ name }) => name === 'flexibleRollout')) { + await fetch( + formatApiPath( + `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies` + ), + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: 'flexibleRollout', + constraints: [], + parameters: { + rollout: '50', + stickiness: 'userId', + groupId: featureId, + }, + }), + } + ); + } +}; + +export const variants = async () => { + const projectId = 'default'; + const featureId = 'demoApp.step4'; + const environmentId = 'default'; + + const { variants }: IFeatureToggle = await fetch( + formatApiPath( + `api/admin/projects/${projectId}/features/${featureId}?variantEnvironments=true` + ) + ).then(res => res.json()); + + if (!variants.length) { + await fetch( + formatApiPath( + `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/variants` + ), + { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify([ + { + op: 'add', + path: '/0', + value: { + name: 'red', + weightType: 'variable', + weight: 333, + overrides: [], + stickiness: 'default', + payload: { + type: 'string', + value: 'red', + }, + }, + }, + { + op: 'add', + path: '/1', + value: { + name: 'green', + weightType: 'variable', + weight: 333, + overrides: [], + stickiness: 'default', + payload: { + type: 'string', + value: 'green', + }, + }, + }, + { + op: 'add', + path: '/2', + value: { + name: 'blue', + weightType: 'variable', + weight: 333, + overrides: [], + stickiness: 'default', + payload: { + type: 'string', + value: 'blue', + }, + }, + }, + ]), + } + ); + } +}; diff --git a/frontend/src/component/demo/demo-topics.tsx b/frontend/src/component/demo/demo-topics.tsx new file mode 100644 index 0000000000..974254f1d8 --- /dev/null +++ b/frontend/src/component/demo/demo-topics.tsx @@ -0,0 +1,451 @@ +import { Typography, TypographyProps } from '@mui/material'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import { Badge } from 'component/common/Badge/Badge'; +import { Step } from 'react-joyride'; +import { gradualRollout, variants } from './demo-setup'; + +export interface ITutorialTopicStep extends Step { + href?: string; + nextButton?: boolean; + backCloseModal?: boolean; + backCollapseExpanded?: boolean; + preventDefault?: boolean; + anyClick?: boolean; + optional?: boolean; +} + +export interface ITutorialTopic { + title: string; + setup?: () => Promise; + steps: ITutorialTopicStep[]; +} + +const Description = (props: TypographyProps) => ( + +); + +export const TOPICS: ITutorialTopic[] = [ + { + title: 'Enable/disable a feature toggle', + steps: [ + { + href: '/projects/default', + target: 'body', + placement: 'center', + content: ( + <> + + + Feature toggles + {' '} + are the central concept of Unleash. + + + Feature toggles are organized within{' '} + + projects + + . + + + ), + nextButton: true, + }, + { + href: '/projects/default', + target: 'div[data-testid="TOGGLE-demoApp.step1-default"]', + content: ( + <> + + The simplest way to use a feature toggle is to + enable or disable it for everyone (on/off). + + } + > + Look at the demo page when toggling! + + + ), + nextButton: true, + }, + ], + }, + { + title: 'Enable for a specific user', + steps: [ + { + href: '/projects/default', + target: 'body', + placement: 'center', + content: ( + <> + + + Activation strategies + {' '} + give you more control over when a feature should be + enabled. + + + Let's try enabling a feature toggle only for a + specific user. + + + ), + nextButton: true, + }, + { + href: '/projects/default', + target: 'a[href="/projects/default/features/demoApp.step2"]', + content: ( + + First, let's open the feature toggle configuration for{' '} + demoApp.step2. + + ), + preventDefault: true, + }, + { + href: '/projects/default/features/demoApp.step2', + target: 'div[data-testid="FEATURE_ENVIRONMENT_ACCORDION_default"] button', + content: ( + + Add a new strategy to this environment by clicking this + button. + + ), + }, + { + target: 'a[href="/projects/default/features/demoApp.step2/strategies/create?environmentId=default&strategyName=userWithId"]', + content: ( + + Select the UserIDs strategy + type. + + ), + placement: 'right', + backCloseModal: true, + }, + { + target: '#input-add-items', + content: ( + <> + + Enter your userId. + + } + > + You can find your userId on the demo page. + + + ), + nextButton: true, + backCloseModal: true, + }, + { + target: 'button[data-testid="STRATEGY_FORM_SUBMIT_ID"]', + content: ( + <> + + Save your strategy to apply it. + + } + > + Look at the demo page after saving! + + + ), + }, + { + target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]', + content: ( + <> + Confirm your changes. + } + > + Look at the demo page after saving! + + + ), + optional: true, + }, + ], + }, + { + title: 'Adjust gradual rollout', + setup: gradualRollout, + steps: [ + { + href: '/projects/default', + target: 'body', + placement: 'center', + content: ( + <> + + + Gradual rollout + {' '} + is one of the available{' '} + + activation strategies + + . + + + Let's try enabling a feature toggle only for a + certain percentage of users. + + + ), + nextButton: true, + }, + { + href: '/projects/default', + target: 'a[href="/projects/default/features/demoApp.step3"]', + content: ( + + First, let's open the feature toggle configuration for{' '} + demoApp.step3. + + ), + preventDefault: true, + }, + { + href: '/projects/default/features/demoApp.step3', + target: 'div[data-testid="FEATURE_ENVIRONMENT_ACCORDION_default"] .MuiAccordionSummary-expandIconWrapper', + content: ( + + Expand the environment card to see all the defined + strategies. + + ), + }, + { + target: 'div[data-testid="FEATURE_ENVIRONMENT_ACCORDION_default"].Mui-expanded a[data-testid="STRATEGY_EDIT-flexibleRollout"]', + content: ( + + Edit the existing gradual rollout strategy. + + ), + backCollapseExpanded: true, + }, + { + target: 'span[data-testid="ROLLOUT_SLIDER_ID"]', + content: ( + Change the rollout percentage. + ), + backCloseModal: true, + }, + { + target: 'button[data-testid="STRATEGY_FORM_SUBMIT_ID"]', + content: ( + <> + + Save your strategy to apply it. + + } + > + Look at the demo page after saving! + + + ), + }, + { + target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]', + content: ( + <> + Confirm your changes. + } + > + Look at the demo page after saving! + + + ), + optional: true, + }, + ], + }, + { + title: 'Adjust variants', + setup: variants, + steps: [ + { + href: '/projects/default', + target: 'body', + placement: 'center', + content: ( + <> + + + Feature toggle variants + {' '} + allow you to define different values for a feature + toggle. They can be used for A/B testing or + segmenting your users. + + + Let's try adding a variant to a feature toggle, + along with an override so our user can see it. + + + ), + nextButton: true, + }, + { + href: '/projects/default', + target: 'a[href="/projects/default/features/demoApp.step4"]', + content: ( + + First, let's open the feature toggle configuration for{' '} + demoApp.step4. + + ), + preventDefault: true, + }, + { + href: '/projects/default/features/demoApp.step4', + target: 'button[data-testid="TAB-Variants"]', + content: Select the variants tab., + }, + { + target: 'button[data-testid="EDIT_VARIANTS_BUTTON"]', + content: Edit the existing variants., + }, + { + target: 'button[data-testid="MODAL_ADD_VARIANT_BUTTON"]', + content: ( + Add a new variant to the list. + ), + backCloseModal: true, + }, + { + target: 'div[data-testid="VARIANT"]:last-of-type div[data-testid="VARIANT_NAME_INPUT"]', + content: ( + <> + Enter a new variant name. + + We recommend choosing a{' '} + + color + + . + + + Example: aqua. + + + ), + backCloseModal: true, + nextButton: true, + }, + { + target: 'div[data-testid="VARIANT"]:last-of-type #variant-payload-value', + content: ( + + Enter the{' '} + + color + {' '} + you chose on the previous step as the payload. + + ), + nextButton: true, + }, + { + target: 'div[data-testid="VARIANT"]:last-of-type button[data-testid="VARIANT_ADD_OVERRIDE_BUTTON"]', + content: ( + + Let's also add an override for our user. + + ), + }, + { + target: 'div[data-testid="VARIANT"]:last-of-type #override-context-name', + content: Choose a context field., + anyClick: true, + backCloseModal: true, + }, + { + target: 'li[data-testid="SELECT_ITEM_ID-userId"]', + content: ( + + Select the userId context + field. + + ), + placement: 'right', + backCloseModal: true, + }, + { + target: 'div[data-testid="VARIANT"]:last-of-type div[data-testid="OVERRIDE_VALUES"]', + content: ( + <> + + Enter your userId. + + } + > + You can find your userId on the demo page. + + + ), + nextButton: true, + backCloseModal: true, + }, + { + target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]', + content: ( + <> + + Save your variants to apply them. + + } + > + Look at the demo page after saving! + + + ), + }, + ], + }, +]; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx index 5fe010fe42..d7dea23e63 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx @@ -69,7 +69,10 @@ export const StrategyItem: FC = ({ projectId={projectId} component={Link} to={editStrategyPath} - tooltipProps={{ title: 'Edit strategy' }} + tooltipProps={{ + title: 'Edit strategy', + }} + data-testid={`STRATEGY_EDIT-${strategy.name}`} > diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm.tsx index 8fe29b5519..5c253aa673 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantForm.tsx @@ -304,7 +304,7 @@ export const VariantForm = ({ }, [variant.weight]); return ( - + Add override diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantOverrides/VariantOverrides.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantOverrides/VariantOverrides.tsx index c4cc63e053..63798efb8a 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantOverrides/VariantOverrides.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsModal/VariantForm/VariantOverrides/VariantOverrides.tsx @@ -93,6 +93,7 @@ export const OverrideConfig: VFC = ({ id="override-context-name" name="contextName" label="Context Field" + data-testid="context_field" value={override.contextName} options={contextNames} onChange={e => @@ -140,6 +141,7 @@ export const OverrideConfig: VFC = ({ placeholder="" values={override.values} updateValues={updateValues(index)} + data-testid="OVERRIDE_VALUES" /> } /> diff --git a/frontend/src/component/feature/FeatureView/FeatureView.tsx b/frontend/src/component/feature/FeatureView/FeatureView.tsx index 8861d00e03..4d1fd577a5 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureView.tsx @@ -225,6 +225,7 @@ export const FeatureView = () => { label={tab.title} value={tab.path} onClick={() => navigate(tab.path)} + data-testid={`TAB-${tab.title}`} /> ))} diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.tsx index 2585683312..157d7f5039 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.tsx @@ -5,7 +5,7 @@ import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/p import { useOptimisticUpdate } from './hooks/useOptimisticUpdate'; import { flexRow } from 'themes/themeStyles'; -const StyledBoxContainer = styled(Box)(() => ({ +const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({ mx: 'auto', ...flexRow, })); @@ -40,9 +40,12 @@ export const FeatureToggleSwitch: VFC = ({ ); }; + const key = `${featureName}-${environmentName}`; + return (