1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-26 01:17:00 +02:00

feat: implement demo guide steps (#3569)

https://linear.app/unleash/issue/2-924/implement-the-guide-steps

While https://github.com/Unleash/unleash/pull/3528 implemented the basic
guide logic with some example steps, this PR adds the guide steps as per
the current demo, along with any necessary changes and features that
were needed to accomplish this.

Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#3537


https://user-images.githubusercontent.com/14320932/233133252-a3790f11-ec5d-4ee3-952d-88348212bd3c.mp4
This commit is contained in:
Nuno Góis 2023-04-19 19:40:29 +01:00 committed by GitHub
parent 169b2bd0c5
commit 58fb1a21fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 751 additions and 256 deletions

View File

@ -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<IBadgeProps> = forwardRef(
(
{
as = 'div',
color = 'neutral',
icon,
iconRight,
@ -80,6 +82,7 @@ export const Badge: FC<IBadgeProps> = forwardRef(
ref: ForwardedRef<HTMLDivElement>
) => (
<StyledBadge
as={as}
tabIndex={0}
color={color}
icon={icon}

View File

@ -1,18 +1,10 @@
import { Typography } from '@mui/material';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import { Badge } from 'component/common/Badge/Badge';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useEffect, useState } from 'react';
import { Step } from 'react-joyride';
import { DemoTopics } from './DemoTopics/DemoTopics';
import { DemoSteps } from './DemoSteps/DemoSteps';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { createLocalStorage } from 'utils/createLocalStorage';
export interface ITutorialTopic {
title: string;
steps: Step[];
}
import { TOPICS } from './demo-topics';
const defaultProgress = {
expanded: true,
@ -24,167 +16,20 @@ const defaultProgress = {
const { value: storedProgress, setValue: setStoredProgress } =
createLocalStorage('Tutorial:v1', defaultProgress);
const TOPICS: ITutorialTopic[] = [
{
title: 'Import',
steps: [
{
target: 'button[data-testid="IMPORT_BUTTON"]',
title: (
<Typography fontWeight="bold">
Import toggle configuration
</Typography>
),
content: (
<>
<Typography variant="body2" color="text.secondary">
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.
</Typography>
</>
),
disableBeacon: true,
},
],
},
{
title: 'New feature toggle',
steps: [
{
target: 'button[data-testid="NAVIGATE_TO_CREATE_FEATURE"]',
title: (
<Typography fontWeight="bold">
Add a new feature toggle
</Typography>
),
content: (
<>
<Typography variant="body2" color="text.secondary">
You can use this button to add a new feature toggle.
This is just an example and not part of the final
guide.
</Typography>
</>
),
disableBeacon: true,
},
],
},
{
title: 'Enable/disable a feature toggle',
steps: [
{
target: '.MuiSwitch-sizeMedium',
title: (
<Typography fontWeight="bold">
Enable/disable a feature toggle
</Typography>
),
content: (
<>
<Typography variant="body2" color="text.secondary">
The simplest way to use a feature toggle is to
enable or disable it for everyone (on/off).
</Typography>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page when toggling!
</Badge>
</>
),
disableBeacon: true,
},
],
},
{
title: 'Community',
steps: [
{
target: 'a[href="https://twitter.com/getunleash"]',
title: (
<Typography fontWeight="bold">
Follow us on Twitter!
</Typography>
),
content: (
<>
<Typography variant="body2" color="text.secondary">
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.
</Typography>
</>
),
disableBeacon: true,
},
{
target: 'a[href="https://www.linkedin.com/company/getunleash"]',
title: (
<Typography fontWeight="bold">
Follow us on LinkedIn!
</Typography>
),
content: (
<>
<Typography variant="body2" color="text.secondary">
You can also follow us LinkedIn. This is just an
example and not part of the final guide.
</Typography>
</>
),
disableBeacon: true,
},
{
target: 'a[href="https://github.com/Unleash/unleash"]',
title: (
<Typography fontWeight="bold">
Check out Unleash on GitHub!
</Typography>
),
content: (
<>
<Typography variant="body2" color="text.secondary">
Unleash is open-source, check out the project on
GitHub. This is just an example and not part of the
final guide.
</Typography>
</>
),
disableBeacon: true,
},
{
target: 'a[href="https://slack.unleash.run"]',
title: (
<Typography fontWeight="bold">Join us on Slack!</Typography>
),
content: (
<>
<Typography variant="body2" color="text.secondary">
Join our community in Slack. This is just an example
and not part of the final guide.
</Typography>
</>
),
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);
}, []);

View File

@ -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 (
<Joyride
run={run}
stepIndex={steps[topic]}
callback={joyrideCallback}
steps={topics[topic].steps}
steps={joyrideSteps}
disableScrolling
disableOverlayClose
spotlightClicks
spotlightPadding={0}
floaterProps={{
disableAnimation: true,
styles: {
@ -175,58 +234,71 @@ export const DemoSteps = ({
}}
tooltipComponent={({
step,
primaryProps,
tooltipProps,
}: TooltipRenderProps) => {
const { onClick } = primaryProps;
return (
<StyledTooltip {...tooltipProps}>
<StyledTooltipTitle>
{step.title}
}: TooltipRenderProps & {
step: ITutorialTopicStep;
}) => (
<StyledTooltip {...tooltipProps}>
<StyledTooltipTitle>
<ConditionallyRender
condition={Boolean(step.title)}
show={step.title}
elseShow={
<Typography fontWeight="bold">
{topics[topic].title}
</Typography>
}
/>
<ConditionallyRender
condition={topics[topic].steps.length > 1}
show={
<Typography
variant="body2"
color="text.secondary"
flexShrink={0}
>
(step {steps[topic] + 1} of{' '}
{topics[topic].steps.length})
</Typography>
}
/>
</StyledTooltipTitle>
{step.content}
<StyledTooltipActions>
<Button variant="text" onClick={skip}>
Skip
</Button>
<StyledTooltipPrimaryActions>
<ConditionallyRender
condition={topics[topic].steps.length > 1}
condition={topic > 0 || steps[topic] > 0}
show={
<Typography
variant="body2"
color="text.secondary"
flexShrink={0}
<Button
variant="outlined"
onClick={() => onBack(step)}
>
(step {steps[topic] + 1} of{' '}
{topics[topic].steps.length})
</Typography>
Back
</Button>
}
/>
</StyledTooltipTitle>
{step.content}
<StyledTooltipActions>
<Button variant="text" onClick={skip}>
Skip
</Button>
<StyledTooltipPrimaryActions>
<ConditionallyRender
condition={topic > 0 || steps[topic] > 0}
show={
<Button
variant="outlined"
onClick={back}
>
Back
</Button>
}
/>
<Button onClick={onClick} variant="contained">
{topic === topics.length - 1 &&
steps[topic] ===
topics[topic].steps.length - 1
? 'Finish'
: 'Next'}
</Button>
</StyledTooltipPrimaryActions>
</StyledTooltipActions>
</StyledTooltip>
);
}}
<ConditionallyRender
condition={Boolean(step.nextButton)}
show={
<Button
onClick={() => next(steps[topic])}
variant="contained"
>
{topic === topics.length - 1 &&
steps[topic] ===
topics[topic].steps.length - 1
? 'Finish'
: 'Next'}
</Button>
}
/>
</StyledTooltipPrimaryActions>
</StyledTooltipActions>
</StyledTooltip>
)}
/>
);
};

View File

@ -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 }) => ({

View File

@ -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',
},
},
},
]),
}
);
}
};

View File

@ -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<void>;
steps: ITutorialTopicStep[];
}
const Description = (props: TypographyProps) => (
<Typography variant="body2" color="text.secondary" {...props} />
);
export const TOPICS: ITutorialTopic[] = [
{
title: 'Enable/disable a feature toggle',
steps: [
{
href: '/projects/default',
target: 'body',
placement: 'center',
content: (
<>
<Description>
<a
href="https://docs.getunleash.io/reference/feature-toggles"
target="_blank"
>
Feature toggles
</a>{' '}
are the central concept of Unleash.
</Description>
<Description sx={{ mt: 1 }}>
Feature toggles are organized within{' '}
<a
href="https://docs.getunleash.io/reference/projects"
target="_blank"
>
projects
</a>
.
</Description>
</>
),
nextButton: true,
},
{
href: '/projects/default',
target: 'div[data-testid="TOGGLE-demoApp.step1-default"]',
content: (
<>
<Description>
The simplest way to use a feature toggle is to
enable or disable it for everyone (on/off).
</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page when toggling!
</Badge>
</>
),
nextButton: true,
},
],
},
{
title: 'Enable for a specific user',
steps: [
{
href: '/projects/default',
target: 'body',
placement: 'center',
content: (
<>
<Description>
<a
href="https://docs.getunleash.io/reference/activation-strategies"
target="_blank"
>
Activation strategies
</a>{' '}
give you more control over when a feature should be
enabled.
</Description>
<Description sx={{ mt: 1 }}>
Let's try enabling a feature toggle only for a
specific user.
</Description>
</>
),
nextButton: true,
},
{
href: '/projects/default',
target: 'a[href="/projects/default/features/demoApp.step2"]',
content: (
<Description>
First, let's open the feature toggle configuration for{' '}
<Badge as="span">demoApp.step2</Badge>.
</Description>
),
preventDefault: true,
},
{
href: '/projects/default/features/demoApp.step2',
target: 'div[data-testid="FEATURE_ENVIRONMENT_ACCORDION_default"] button',
content: (
<Description>
Add a new strategy to this environment by clicking this
button.
</Description>
),
},
{
target: 'a[href="/projects/default/features/demoApp.step2/strategies/create?environmentId=default&strategyName=userWithId"]',
content: (
<Description>
Select the <Badge as="span">UserIDs</Badge> strategy
type.
</Description>
),
placement: 'right',
backCloseModal: true,
},
{
target: '#input-add-items',
content: (
<>
<Description>
Enter your <Badge as="span">userId</Badge>.
</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
You can find your userId on the demo page.
</Badge>
</>
),
nextButton: true,
backCloseModal: true,
},
{
target: 'button[data-testid="STRATEGY_FORM_SUBMIT_ID"]',
content: (
<>
<Description>
Save your strategy to apply it.
</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page after saving!
</Badge>
</>
),
},
{
target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]',
content: (
<>
<Description>Confirm your changes.</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page after saving!
</Badge>
</>
),
optional: true,
},
],
},
{
title: 'Adjust gradual rollout',
setup: gradualRollout,
steps: [
{
href: '/projects/default',
target: 'body',
placement: 'center',
content: (
<>
<Description>
<a
href="https://docs.getunleash.io/reference/activation-strategies#gradual-rollout"
target="_blank"
>
Gradual rollout
</a>{' '}
is one of the available{' '}
<a
href="https://docs.getunleash.io/reference/activation-strategies"
target="_blank"
>
activation strategies
</a>
.
</Description>
<Description sx={{ mt: 1 }}>
Let's try enabling a feature toggle only for a
certain percentage of users.
</Description>
</>
),
nextButton: true,
},
{
href: '/projects/default',
target: 'a[href="/projects/default/features/demoApp.step3"]',
content: (
<Description>
First, let's open the feature toggle configuration for{' '}
<Badge as="span">demoApp.step3</Badge>.
</Description>
),
preventDefault: true,
},
{
href: '/projects/default/features/demoApp.step3',
target: 'div[data-testid="FEATURE_ENVIRONMENT_ACCORDION_default"] .MuiAccordionSummary-expandIconWrapper',
content: (
<Description>
Expand the environment card to see all the defined
strategies.
</Description>
),
},
{
target: 'div[data-testid="FEATURE_ENVIRONMENT_ACCORDION_default"].Mui-expanded a[data-testid="STRATEGY_EDIT-flexibleRollout"]',
content: (
<Description>
Edit the existing gradual rollout strategy.
</Description>
),
backCollapseExpanded: true,
},
{
target: 'span[data-testid="ROLLOUT_SLIDER_ID"]',
content: (
<Description>Change the rollout percentage.</Description>
),
backCloseModal: true,
},
{
target: 'button[data-testid="STRATEGY_FORM_SUBMIT_ID"]',
content: (
<>
<Description>
Save your strategy to apply it.
</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page after saving!
</Badge>
</>
),
},
{
target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]',
content: (
<>
<Description>Confirm your changes.</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page after saving!
</Badge>
</>
),
optional: true,
},
],
},
{
title: 'Adjust variants',
setup: variants,
steps: [
{
href: '/projects/default',
target: 'body',
placement: 'center',
content: (
<>
<Description>
<a
href="https://docs.getunleash.io/reference/feature-toggle-variants"
target="_blank"
>
Feature toggle variants
</a>{' '}
allow you to define different values for a feature
toggle. They can be used for A/B testing or
segmenting your users.
</Description>
<Description sx={{ mt: 1 }}>
Let's try adding a variant to a feature toggle,
along with an override so our user can see it.
</Description>
</>
),
nextButton: true,
},
{
href: '/projects/default',
target: 'a[href="/projects/default/features/demoApp.step4"]',
content: (
<Description>
First, let's open the feature toggle configuration for{' '}
<Badge as="span">demoApp.step4</Badge>.
</Description>
),
preventDefault: true,
},
{
href: '/projects/default/features/demoApp.step4',
target: 'button[data-testid="TAB-Variants"]',
content: <Description>Select the variants tab.</Description>,
},
{
target: 'button[data-testid="EDIT_VARIANTS_BUTTON"]',
content: <Description>Edit the existing variants.</Description>,
},
{
target: 'button[data-testid="MODAL_ADD_VARIANT_BUTTON"]',
content: (
<Description>Add a new variant to the list.</Description>
),
backCloseModal: true,
},
{
target: 'div[data-testid="VARIANT"]:last-of-type div[data-testid="VARIANT_NAME_INPUT"]',
content: (
<>
<Description>Enter a new variant name.</Description>
<Description sx={{ mt: 1 }}>
We recommend choosing a{' '}
<a
href="https://developer.mozilla.org/en-US/docs/Web/CSS/named-color"
target="_blank"
>
color
</a>
.
</Description>
<Description>
Example: <Badge as="span">aqua</Badge>.
</Description>
</>
),
backCloseModal: true,
nextButton: true,
},
{
target: 'div[data-testid="VARIANT"]:last-of-type #variant-payload-value',
content: (
<Description>
Enter the{' '}
<a
href="https://developer.mozilla.org/en-US/docs/Web/CSS/named-color"
target="_blank"
>
color
</a>{' '}
you chose on the previous step as the payload.
</Description>
),
nextButton: true,
},
{
target: 'div[data-testid="VARIANT"]:last-of-type button[data-testid="VARIANT_ADD_OVERRIDE_BUTTON"]',
content: (
<Description>
Let's also add an override for our user.
</Description>
),
},
{
target: 'div[data-testid="VARIANT"]:last-of-type #override-context-name',
content: <Description>Choose a context field.</Description>,
anyClick: true,
backCloseModal: true,
},
{
target: 'li[data-testid="SELECT_ITEM_ID-userId"]',
content: (
<Description>
Select the <Badge as="span">userId</Badge> context
field.
</Description>
),
placement: 'right',
backCloseModal: true,
},
{
target: 'div[data-testid="VARIANT"]:last-of-type div[data-testid="OVERRIDE_VALUES"]',
content: (
<>
<Description>
Enter your <Badge as="span">userId</Badge>.
</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
You can find your userId on the demo page.
</Badge>
</>
),
nextButton: true,
backCloseModal: true,
},
{
target: 'button[data-testid="DIALOGUE_CONFIRM_ID"]',
content: (
<>
<Description>
Save your variants to apply them.
</Description>
<Badge
sx={{ marginTop: 2 }}
icon={<InfoOutlinedIcon />}
>
Look at the demo page after saving!
</Badge>
</>
),
},
],
},
];

View File

@ -69,7 +69,10 @@ export const StrategyItem: FC<IStrategyItemProps> = ({
projectId={projectId}
component={Link}
to={editStrategyPath}
tooltipProps={{ title: 'Edit strategy' }}
tooltipProps={{
title: 'Edit strategy',
}}
data-testid={`STRATEGY_EDIT-${strategy.name}`}
>
<Edit />
</PermissionIconButton>

View File

@ -304,7 +304,7 @@ export const VariantForm = ({
}, [variant.weight]);
return (
<StyledVariantForm>
<StyledVariantForm data-testid="VARIANT">
<StyledDeleteButtonTooltip
arrow
title={
@ -441,6 +441,7 @@ export const VariantForm = ({
onClick={onAddOverride}
variant="text"
color="primary"
data-testid="VARIANT_ADD_OVERRIDE_BUTTON"
>
Add override
</StyledAddOverrideButton>

View File

@ -93,6 +93,7 @@ export const OverrideConfig: VFC<IOverrideConfigProps> = ({
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<IOverrideConfigProps> = ({
placeholder=""
values={override.values}
updateValues={updateValues(index)}
data-testid="OVERRIDE_VALUES"
/>
}
/>

View File

@ -225,6 +225,7 @@ export const FeatureView = () => {
label={tab.title}
value={tab.path}
onClick={() => navigate(tab.path)}
data-testid={`TAB-${tab.title}`}
/>
))}
</Tabs>

View File

@ -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<IFeatureToggleSwitchProps> = ({
);
};
const key = `${featureName}-${environmentName}`;
return (
<StyledBoxContainer
key={`${featureName}-${environmentName}`} // Prevent animation when archiving rows
key={key} // Prevent animation when archiving rows
data-testid={`TOGGLE-${key}`}
>
<PermissionSwitch
checked={value}