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

chore: drag-n-drop tooltip for strategies (#9623)

Implements the drag-n-drop tooltip the first time the user sees a
strategy drag handle on the feature env overview. It uses React Joyride,
which is the same system we use for the demo.

The design is a little different from the sketches because I couldn't
find a quick way to move the content (and the arrow) to be shifted
correctly.

If the demo is also active the first time a user visits a strategy page,
it'll render both the demo steps and this, but this tooltip doesn't
prevent the user from finishing the tour. It might be possible to avoid
that through checking state in localstorage, but I'd like to get this
approved first.

The tooltip uses the auth splash system to decide whether to show the
tooltip, meaning it's stored per user in the DB. To avoid it
re-rendering before you refetch from the back end, we also use a
temporary variable to check whether the user has closed it.

Rendered:

![image](https://github.com/user-attachments/assets/5912d055-10d5-4a1d-93f4-f12ff4ef7419)

If the tour is also active:

![image](https://github.com/user-attachments/assets/b0028a0f-3a0f-48aa-9ab9-8d7cf399055a)
This commit is contained in:
Thomas Heartman 2025-03-27 11:16:37 +01:00 committed by GitHub
parent 6aae9be19c
commit 138e93c41a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 135 additions and 3 deletions

View File

@ -96,6 +96,7 @@ export const StrategyItemContainer: FC<StrategyItemContainerProps> = ({
<StyledHeader disabled={Boolean(strategy?.disabled)}>
{onDragStart ? (
<DragIcon
className='strategy-drag-handle'
draggable
disableRipple
size='small'

View File

@ -9,12 +9,15 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { usePageTitle } from 'hooks/usePageTitle';
import { styled } from '@mui/material';
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
import { useUiFlag } from 'hooks/useUiFlag';
import { FeatureOverviewEnvironments } from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
import { default as LegacyFleatureOverview } from './LegacyFeatureOverview';
import { useEnvironmentVisibility } from './FeatureOverviewMetaData/EnvironmentVisibilityMenu/hooks/useEnvironmentVisibility';
import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi';
import { useAuthSplash } from 'hooks/api/getters/useAuth/useAuthSplash';
import { StrategyDragTooltip } from './StrategyDragTooltip';
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
@ -51,6 +54,22 @@ export const FeatureOverview = () => {
return <LegacyFleatureOverview />;
}
const { setSplashSeen } = useSplashApi();
const { splash } = useAuthSplash();
const dragTooltipSplashId = 'strategy-drag-tooltip';
const shouldShowStrategyDragTooltip = !splash?.[dragTooltipSplashId];
const [showTooltip, setShowTooltip] = useState(false);
const [hasClosedTooltip, setHasClosedTooltip] = useState(false);
const toggleShowTooltip = (envIsOpen: boolean) => {
setShowTooltip(
!hasClosedTooltip && shouldShowStrategyDragTooltip && envIsOpen,
);
};
const onTooltipClose = () => {
setHasClosedTooltip(true);
setSplashSeen(dragTooltipSplashId);
};
return (
<StyledContainer>
<div>
@ -63,6 +82,7 @@ export const FeatureOverview = () => {
</div>
<StyledMainContent>
<FeatureOverviewEnvironments
onToggleEnvOpen={toggleShowTooltip}
hiddenEnvironments={hiddenEnvironments}
/>
</StyledMainContent>
@ -92,6 +112,8 @@ export const FeatureOverview = () => {
}
/>
</Routes>
<StrategyDragTooltip show={showTooltip} onClose={onTooltipClose} />
</StyledContainer>
);
};

View File

@ -59,12 +59,14 @@ type FeatureOverviewEnvironmentProps = {
};
metrics?: Pick<IFeatureEnvironmentMetrics, 'yes' | 'no'>;
otherEnvironments?: string[];
onToggleEnvOpen?: (isOpen: boolean) => void;
};
export const FeatureOverviewEnvironment = ({
environment,
metrics = { yes: 0, no: 0 },
otherEnvironments = [],
onToggleEnvOpen = () => {},
}: FeatureOverviewEnvironmentProps) => {
const [isOpen, setIsOpen] = useState(false);
const projectId = useRequiredPathParam('projectId');
@ -83,7 +85,11 @@ export const FeatureOverviewEnvironment = ({
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${environment.name}`}
expanded={isOpen && hasActivations}
disabled={!hasActivations}
onChange={() => setIsOpen(isOpen ? !isOpen : hasActivations)}
onChange={() => {
const state = isOpen ? !isOpen : hasActivations;
onToggleEnvOpen(state);
setIsOpen(state);
}}
>
<EnvironmentHeader
environmentMetadata={{

View File

@ -10,6 +10,7 @@ import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePla
type FeatureOverviewEnvironmentsProps = {
hiddenEnvironments?: string[];
onToggleEnvOpen?: (isOpen: boolean) => void;
};
const FeatureOverviewWithReleasePlans: FC<
@ -33,7 +34,7 @@ const FeatureOverviewWithReleasePlans: FC<
export const FeatureOverviewEnvironments: FC<
FeatureOverviewEnvironmentsProps
> = ({ hiddenEnvironments = [] }) => {
> = ({ hiddenEnvironments = [], onToggleEnvOpen }) => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { feature } = useFeature(projectId, featureId);
@ -60,6 +61,7 @@ export const FeatureOverviewEnvironments: FC<
?.filter((env) => !hiddenEnvironments.includes(env.name))
.map((env) => (
<FeatureOverviewWithReleasePlans
onToggleEnvOpen={onToggleEnvOpen}
environment={env}
key={env.name}
metrics={featureMetrics.find(

View File

@ -0,0 +1,101 @@
import Close from '@mui/icons-material/Close';
import { Box, Button, IconButton, styled } from '@mui/material';
import type { FC } from 'react';
import Joyride, { type TooltipRenderProps } from 'react-joyride';
const StyledTooltip = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
maxWidth: '300px',
background: '#201e42',
borderRadius: theme.shape.borderRadiusMedium,
color: theme.palette.common.white,
padding: theme.spacing(2),
paddingRight: theme.spacing(1),
fontSize: theme.typography.body2.fontSize,
}));
const OkButton = styled(Button)(({ theme }) => ({
color: theme.palette.secondary.border,
alignSelf: 'start',
marginLeft: theme.spacing(-1),
}));
const StyledCloseButton = styled(IconButton)(({ theme }) => ({
color: theme.palette.common.white,
background: 'none',
border: 'none',
position: 'absolute',
top: theme.spacing(1),
right: theme.spacing(1),
svg: {
width: theme.spacing(2),
height: theme.spacing(2),
},
}));
const StyledHeader = styled('p')(({ theme }) => ({
fontSize: theme.typography.body1.fontSize,
fontWeight: 'bold',
}));
const CustomTooltip = ({ closeProps }: TooltipRenderProps) => {
return (
<StyledTooltip component='article'>
<StyledCloseButton type='button' {...closeProps}>
<Close />
</StyledCloseButton>
<StyledHeader>Decide the order evaluation</StyledHeader>
<p>
Strategies are evaluated in the order presented here. Drag and
rearrange the strategies to get the order you prefer.
</p>
<OkButton
type='button'
data-action={closeProps['data-action']}
onClick={closeProps.onClick}
>
Ok, got it!
</OkButton>
</StyledTooltip>
);
};
type Props = {
show: boolean;
onClose: () => void;
};
export const StrategyDragTooltip: FC<Props> = ({ show, onClose }) => {
return (
<Joyride
callback={({ action }) => {
if (action === 'close') {
onClose();
}
}}
floaterProps={{
styles: {
arrow: {
color: '#201e42',
spread: 16,
length: 10,
},
},
}}
run={show}
disableOverlay
disableScrolling
tooltipComponent={CustomTooltip}
steps={[
{
disableBeacon: true,
offset: 0,
target: '.strategy-drag-handle',
content: <></>,
},
]}
/>
);
};