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:
parent
169b2bd0c5
commit
58fb1a21fd
@ -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}
|
||||
|
@ -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);
|
||||
}, []);
|
||||
|
||||
|
@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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 }) => ({
|
||||
|
114
frontend/src/component/demo/demo-setup.ts
Normal file
114
frontend/src/component/demo/demo-setup.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
451
frontend/src/component/demo/demo-topics.tsx
Normal file
451
frontend/src/component/demo/demo-topics.tsx
Normal 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>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -225,6 +225,7 @@ export const FeatureView = () => {
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
data-testid={`TAB-${tab.title}`}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user