mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-01 13:47:27 +02:00
Merge branch 'main' into fix/login-e2e-ci-test
This commit is contained in:
commit
3ad83ad8e6
@ -6,10 +6,12 @@ import Loader from './common/Loader/Loader';
|
|||||||
import { useLocalStorageState } from 'hooks/useLocalStorageState';
|
import { useLocalStorageState } from 'hooks/useLocalStorageState';
|
||||||
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
||||||
|
|
||||||
|
const defaultPage = '/personal';
|
||||||
|
|
||||||
export const useLastViewedPage = (location?: Location) => {
|
export const useLastViewedPage = (location?: Location) => {
|
||||||
const [state, setState] = useLocalStorageState<string>(
|
const [state, setState] = useLocalStorageState<string>(
|
||||||
'lastViewedPage',
|
'lastViewedPage',
|
||||||
'/personal',
|
defaultPage,
|
||||||
7 * 24 * 60 * 60 * 1000, // 7 days, left to promote seeing Personal dashboard from time to time
|
7 * 24 * 60 * 60 * 1000, // 7 days, left to promote seeing Personal dashboard from time to time
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -55,5 +57,9 @@ export const InitialRedirect = () => {
|
|||||||
return <Navigate to={`/projects/${lastViewedProject}`} replace />;
|
return <Navigate to={`/projects/${lastViewedProject}`} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastViewedPage && lastViewedPage !== '/') {
|
||||||
return <Navigate to={lastViewedPage} replace />;
|
return <Navigate to={lastViewedPage} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Navigate to={defaultPage} replace />;
|
||||||
};
|
};
|
||||||
|
@ -63,12 +63,18 @@ const StyledTooltipTitle = styled('div')(({ theme }) => ({
|
|||||||
const StyledTooltipActions = styled('div')(({ theme }) => ({
|
const StyledTooltipActions = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
marginTop: theme.spacing(3),
|
marginTop: theme.spacing(3),
|
||||||
'&&& button': {
|
'&&& button': {
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledBackButton = styled(Button)({
|
||||||
|
padding: 0,
|
||||||
|
minWidth: 0,
|
||||||
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export interface IDemoStepTooltipProps extends TooltipRenderProps {
|
export interface IDemoStepTooltipProps extends TooltipRenderProps {
|
||||||
step: ITutorialTopicStep;
|
step: ITutorialTopicStep;
|
||||||
@ -92,7 +98,7 @@ export const DemoStepTooltip = ({
|
|||||||
}: IDemoStepTooltipProps) => {
|
}: IDemoStepTooltipProps) => {
|
||||||
const nextLabel =
|
const nextLabel =
|
||||||
stepIndex === 0
|
stepIndex === 0
|
||||||
? 'Start'
|
? 'Start tutorial'
|
||||||
: stepIndex === topics[topic].steps.length - 1
|
: stepIndex === topics[topic].steps.length - 1
|
||||||
? 'Finish'
|
? 'Finish'
|
||||||
: 'Next';
|
: 'Next';
|
||||||
@ -112,28 +118,23 @@ export const DemoStepTooltip = ({
|
|||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</StyledCloseButton>
|
</StyledCloseButton>
|
||||||
<StyledTooltipTitle>
|
<StyledTooltipTitle>
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(step.title)}
|
|
||||||
show={step.title}
|
|
||||||
elseShow={
|
|
||||||
<Typography fontWeight='bold'>
|
<Typography fontWeight='bold'>
|
||||||
{topics[topic].title}
|
{step.title || topics[topic].title}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledTooltipTitle>
|
</StyledTooltipTitle>
|
||||||
{step.content}
|
{step.content}
|
||||||
<StyledTooltipActions>
|
<StyledTooltipActions>
|
||||||
<div>
|
<div>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={topic > 0 || stepIndex > 0}
|
condition={
|
||||||
|
!step.hideBackButton && stepIndex > 0
|
||||||
|
}
|
||||||
show={
|
show={
|
||||||
<Button
|
<StyledBackButton
|
||||||
variant='text'
|
|
||||||
onClick={() => onBack(step)}
|
onClick={() => onBack(step)}
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</StyledBackButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -164,25 +165,19 @@ export const DemoStepTooltip = ({
|
|||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</StyledCloseButton>
|
</StyledCloseButton>
|
||||||
<StyledTooltipTitle>
|
<StyledTooltipTitle>
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(step.title)}
|
|
||||||
show={step.title}
|
|
||||||
elseShow={
|
|
||||||
<Typography fontWeight='bold'>
|
<Typography fontWeight='bold'>
|
||||||
{topics[topic].title}
|
{step.title || topics[topic].title}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledTooltipTitle>
|
</StyledTooltipTitle>
|
||||||
{step.content}
|
{step.content}
|
||||||
<StyledTooltipActions>
|
<StyledTooltipActions>
|
||||||
<div>
|
<div>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={topic > 0 || stepIndex > 0}
|
condition={!step.hideBackButton && stepIndex > 0}
|
||||||
show={
|
show={
|
||||||
<Button variant='text' onClick={() => onBack(step)}>
|
<StyledBackButton onClick={() => onBack(step)}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</StyledBackButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,6 +118,8 @@ export const DemoSteps = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === ACTIONS.UPDATE) {
|
if (action === ACTIONS.UPDATE) {
|
||||||
|
if (step.target === 'body') return;
|
||||||
|
|
||||||
const el = document.querySelector(
|
const el = document.querySelector(
|
||||||
step.target as string,
|
step.target as string,
|
||||||
) as HTMLElement | null;
|
) as HTMLElement | null;
|
||||||
|
@ -37,10 +37,10 @@ const ENVIRONMENT = 'dev';
|
|||||||
|
|
||||||
export const TOPICS: ITutorialTopic[] = [
|
export const TOPICS: ITutorialTopic[] = [
|
||||||
{
|
{
|
||||||
title: 'Enable/disable a feature flag',
|
title: 'How to enable/disable a feature flag',
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
title: 'Enable/disable a feature flag',
|
title: 'How to enable/disable a feature flag',
|
||||||
href: `/projects/${PROJECT}?sort=name`,
|
href: `/projects/${PROJECT}?sort=name`,
|
||||||
target: 'body',
|
target: 'body',
|
||||||
placement: 'center',
|
placement: 'center',
|
||||||
@ -91,11 +91,11 @@ export const TOPICS: ITutorialTopic[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Enable for a specific user',
|
title: 'Next: How to enable for a specific user',
|
||||||
setup: specificUser,
|
setup: specificUser,
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
title: 'Enable for a specific user',
|
title: 'Next: How to enable for a specific user',
|
||||||
href: `/projects/${PROJECT}?sort=name`,
|
href: `/projects/${PROJECT}?sort=name`,
|
||||||
target: 'body',
|
target: 'body',
|
||||||
placement: 'center',
|
placement: 'center',
|
||||||
@ -335,11 +335,11 @@ export const TOPICS: ITutorialTopic[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Adjust gradual rollout',
|
title: 'Next: How to adjust gradual rollout',
|
||||||
setup: gradualRollout,
|
setup: gradualRollout,
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
title: 'Adjust gradual rollout',
|
title: 'Next: How to adjust gradual rollout',
|
||||||
href: `/projects/${PROJECT}?sort=name`,
|
href: `/projects/${PROJECT}?sort=name`,
|
||||||
target: 'body',
|
target: 'body',
|
||||||
placement: 'center',
|
placement: 'center',
|
||||||
@ -468,11 +468,11 @@ export const TOPICS: ITutorialTopic[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Adjust variants',
|
title: 'Next: How to adjust variants',
|
||||||
setup: variants,
|
setup: variants,
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
title: 'Adjust variants',
|
title: 'Next: How to adjust variants',
|
||||||
href: `/projects/${PROJECT}?sort=name`,
|
href: `/projects/${PROJECT}?sort=name`,
|
||||||
target: 'body',
|
target: 'body',
|
||||||
placement: 'center',
|
placement: 'center',
|
||||||
|
@ -59,6 +59,7 @@ test('render remove flag from code reminder', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await screen.findByText('Time to remove flag from code?');
|
await screen.findByText('Time to remove flag from code?');
|
||||||
|
await screen.findByText('Revert to production');
|
||||||
|
|
||||||
const reminder = await screen.findByText('Remind me later');
|
const reminder = await screen.findByText('Remind me later');
|
||||||
reminder.click();
|
reminder.click();
|
||||||
|
@ -18,6 +18,8 @@ import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveD
|
|||||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useFlagReminders } from './useFlagReminders';
|
import { useFlagReminders } from './useFlagReminders';
|
||||||
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import { useUncomplete } from '../FeatureOverview/FeatureLifecycle/useUncomplete';
|
||||||
|
|
||||||
const StyledBox = styled(Box)(({ theme }) => ({
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
marginRight: theme.spacing(2),
|
marginRight: theme.spacing(2),
|
||||||
@ -37,10 +39,16 @@ export const CleanupReminder: FC<{
|
|||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
}> = ({ feature, onChange }) => {
|
}> = ({ feature, onChange }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
|
||||||
const [markCompleteDialogueOpen, setMarkCompleteDialogueOpen] =
|
const [markCompleteDialogueOpen, setMarkCompleteDialogueOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [archiveDialogueOpen, setArchiveDialogueOpen] = useState(false);
|
const [archiveDialogueOpen, setArchiveDialogueOpen] = useState(false);
|
||||||
|
const { onUncompleteHandler, loading } = useUncomplete({
|
||||||
|
feature: feature.name,
|
||||||
|
project: feature.project,
|
||||||
|
onChange,
|
||||||
|
});
|
||||||
|
|
||||||
const currentStage = populateCurrentStage(feature);
|
const currentStage = populateCurrentStage(feature);
|
||||||
const isRelevantType =
|
const isRelevantType =
|
||||||
@ -123,7 +131,14 @@ export const CleanupReminder: FC<{
|
|||||||
<ActionsBox>
|
<ActionsBox>
|
||||||
<Button
|
<Button
|
||||||
size='medium'
|
size='medium'
|
||||||
onClick={() => snoozeReminder(feature.name)}
|
onClick={() => {
|
||||||
|
snoozeReminder(feature.name);
|
||||||
|
trackEvent('feature-lifecycle', {
|
||||||
|
props: {
|
||||||
|
eventType: 'snoozeReminder',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Remind me later
|
Remind me later
|
||||||
</Button>
|
</Button>
|
||||||
@ -171,12 +186,31 @@ export const CleanupReminder: FC<{
|
|||||||
severity='warning'
|
severity='warning'
|
||||||
icon={<CleaningServicesIcon />}
|
icon={<CleaningServicesIcon />}
|
||||||
action={
|
action={
|
||||||
|
<ActionsBox>
|
||||||
<Button
|
<Button
|
||||||
size='medium'
|
size='medium'
|
||||||
onClick={() => snoozeReminder(feature.name)}
|
onClick={() => {
|
||||||
|
snoozeReminder(feature.name);
|
||||||
|
trackEvent('feature-lifecycle', {
|
||||||
|
props: {
|
||||||
|
eventType: 'snoozeReminder',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Remind me later
|
Remind me later
|
||||||
</Button>
|
</Button>
|
||||||
|
<PermissionButton
|
||||||
|
variant='outlined'
|
||||||
|
permission={UPDATE_FEATURE}
|
||||||
|
size='medium'
|
||||||
|
onClick={onUncompleteHandler}
|
||||||
|
disabled={loading}
|
||||||
|
projectId={feature.project}
|
||||||
|
>
|
||||||
|
Revert to production
|
||||||
|
</PermissionButton>
|
||||||
|
</ActionsBox>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<b>Time to remove flag from code?</b>
|
<b>Time to remove flag from code?</b>
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { FeatureLifecycleStageIcon } from 'component/common/FeatureLifecycle/FeatureLifecycleStageIcon';
|
import { FeatureLifecycleStageIcon } from 'component/common/FeatureLifecycle/FeatureLifecycleStageIcon';
|
||||||
import { FeatureLifecycleTooltip } from './FeatureLifecycleTooltip';
|
import { FeatureLifecycleTooltip } from './FeatureLifecycleTooltip';
|
||||||
import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi';
|
|
||||||
import { populateCurrentStage } from './populateCurrentStage';
|
import { populateCurrentStage } from './populateCurrentStage';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { Lifecycle } from 'interfaces/featureToggle';
|
import type { Lifecycle } from 'interfaces/featureToggle';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
|
||||||
import { getFeatureLifecycleName } from 'component/common/FeatureLifecycle/getFeatureLifecycleName';
|
import { getFeatureLifecycleName } from 'component/common/FeatureLifecycle/getFeatureLifecycleName';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
|
import { useUncomplete } from './useUncomplete';
|
||||||
|
|
||||||
export interface LifecycleFeature {
|
export interface LifecycleFeature {
|
||||||
lifecycle?: Lifecycle;
|
lifecycle?: Lifecycle;
|
||||||
@ -28,18 +27,12 @@ export const FeatureLifecycle: FC<{
|
|||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
}> = ({ feature, expanded, onComplete, onUncomplete, onArchive }) => {
|
}> = ({ feature, expanded, onComplete, onUncomplete, onArchive }) => {
|
||||||
const currentStage = populateCurrentStage(feature);
|
const currentStage = populateCurrentStage(feature);
|
||||||
const { markFeatureUncompleted, loading } = useFeatureLifecycleApi();
|
|
||||||
const { trackEvent } = usePlausibleTracker();
|
|
||||||
|
|
||||||
const onUncompleteHandler = async () => {
|
const { onUncompleteHandler, loading } = useUncomplete({
|
||||||
await markFeatureUncompleted(feature.name, feature.project);
|
feature: feature.name,
|
||||||
onUncomplete?.();
|
project: feature.project,
|
||||||
trackEvent('feature-lifecycle', {
|
onChange: onUncomplete,
|
||||||
props: {
|
|
||||||
eventType: 'uncomplete',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
return currentStage ? (
|
return currentStage ? (
|
||||||
<Box sx={(theme) => ({ display: 'flex', gap: theme.spacing(0.5) })}>
|
<Box sx={(theme) => ({ display: 'flex', gap: theme.spacing(0.5) })}>
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
|
||||||
|
export const useUncomplete = ({
|
||||||
|
feature,
|
||||||
|
project,
|
||||||
|
onChange,
|
||||||
|
}: { feature: string; project: string; onChange?: () => void }) => {
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
const { setToastApiError } = useToast();
|
||||||
|
const { markFeatureUncompleted, loading } = useFeatureLifecycleApi();
|
||||||
|
|
||||||
|
const onUncompleteHandler = async () => {
|
||||||
|
try {
|
||||||
|
await markFeatureUncompleted(feature, project);
|
||||||
|
onChange?.();
|
||||||
|
|
||||||
|
trackEvent('feature-lifecycle', {
|
||||||
|
props: {
|
||||||
|
eventType: 'uncomplete',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setToastApiError(formatUnknownError(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { onUncompleteHandler, loading };
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user