1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: flag exposure in personal dashboard (#8247)

This commit is contained in:
Mateusz Kwasniewski 2024-09-25 11:11:30 +02:00 committed by GitHub
parent 289324fd02
commit a1a24ea0b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 132 additions and 35 deletions

View File

@ -43,6 +43,7 @@ export const FeatureLifecycle: FC<{
return currentStage ? ( return currentStage ? (
<FeatureLifecycleTooltip <FeatureLifecycleTooltip
stage={currentStage!} stage={currentStage!}
project={feature.project}
onArchive={onArchive} onArchive={onArchive}
onComplete={onComplete} onComplete={onComplete}
onUncomplete={onUncompleteHandler} onUncomplete={onUncompleteHandler}

View File

@ -8,7 +8,6 @@ import {
DELETE_FEATURE, DELETE_FEATURE,
UPDATE_FEATURE, UPDATE_FEATURE,
} from 'component/providers/AccessProvider/permissions'; } from 'component/providers/AccessProvider/permissions';
import { Route, Routes } from 'react-router-dom';
const currentTime = '2024-04-25T08:05:00.000Z'; const currentTime = '2024-04-25T08:05:00.000Z';
const twoMinutesAgo = '2024-04-25T08:03:00.000Z'; const twoMinutesAgo = '2024-04-25T08:03:00.000Z';
@ -23,24 +22,17 @@ const renderOpenTooltip = (
loading = false, loading = false,
) => { ) => {
render( render(
<Routes> <FeatureLifecycleTooltip
<Route stage={stage}
path={'/projects/:projectId'} onArchive={onArchive}
element={ onComplete={onComplete}
<FeatureLifecycleTooltip onUncomplete={onUncomplete}
stage={stage} loading={loading}
onArchive={onArchive} project={'default'}
onComplete={onComplete} >
onUncomplete={onUncomplete} <span>child</span>
loading={loading} </FeatureLifecycleTooltip>,
>
<span>child</span>
</FeatureLifecycleTooltip>
}
/>
</Routes>,
{ {
route: '/projects/default',
permissions: [ permissions: [
{ permission: DELETE_FEATURE }, { permission: DELETE_FEATURE },
{ permission: UPDATE_FEATURE }, { permission: UPDATE_FEATURE },

View File

@ -25,7 +25,6 @@ import { isSafeToArchive } from './isSafeToArchive';
import { useLocationSettings } from 'hooks/useLocationSettings'; import { useLocationSettings } from 'hooks/useLocationSettings';
import { formatDateYMDHMS } from 'utils/formatDate'; import { formatDateYMDHMS } from 'utils/formatDate';
import { formatDistanceToNow, parseISO } from 'date-fns'; import { formatDistanceToNow, parseISO } from 'date-fns';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
const TimeLabel = styled('span')(({ theme }) => ({ const TimeLabel = styled('span')(({ theme }) => ({
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
@ -96,7 +95,7 @@ const StageBox = styled(Box, {
...(active && { ...(active && {
backgroundColor: theme.palette.primary.light, backgroundColor: theme.palette.primary.light,
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
fontWeight: theme.fontWeight.bold, fontWeight: theme.typography.fontWeightBold,
borderRadius: theme.spacing(0.5), borderRadius: theme.spacing(0.5),
}), }),
}, },
@ -247,17 +246,16 @@ const PreLiveStageDescription: FC<{ children?: React.ReactNode }> = ({
const BoldTitle = styled(Typography)(({ theme }) => ({ const BoldTitle = styled(Typography)(({ theme }) => ({
marginTop: theme.spacing(1), marginTop: theme.spacing(1),
marginBottom: theme.spacing(1), marginBottom: theme.spacing(1),
fontSize: theme.fontSizes.smallBody, fontSize: theme.typography.body2.fontSize,
fontWeight: theme.fontWeight.bold, fontWeight: theme.typography.fontWeightBold,
})); }));
const LiveStageDescription: FC<{ const LiveStageDescription: FC<{
onComplete: () => void; onComplete: () => void;
loading: boolean; loading: boolean;
children?: React.ReactNode; children?: React.ReactNode;
}> = ({ children, onComplete, loading }) => { project: string;
const projectId = useRequiredPathParam('projectId'); }> = ({ children, onComplete, loading, project }) => {
return ( return (
<> <>
<BoldTitle>Is this feature complete?</BoldTitle> <BoldTitle>Is this feature complete?</BoldTitle>
@ -276,7 +274,7 @@ const LiveStageDescription: FC<{
size='small' size='small'
onClick={onComplete} onClick={onComplete}
disabled={loading} disabled={loading}
projectId={projectId} projectId={project}
> >
Mark completed Mark completed
</PermissionButton> </PermissionButton>
@ -294,9 +292,8 @@ const SafeToArchive: FC<{
onArchive: () => void; onArchive: () => void;
onUncomplete: () => void; onUncomplete: () => void;
loading: boolean; loading: boolean;
}> = ({ onArchive, onUncomplete, loading }) => { project: string;
const projectId = useRequiredPathParam('projectId'); }> = ({ onArchive, onUncomplete, loading, project }) => {
return ( return (
<> <>
<BoldTitle>Safe to archive</BoldTitle> <BoldTitle>Safe to archive</BoldTitle>
@ -324,7 +321,7 @@ const SafeToArchive: FC<{
size='small' size='small'
onClick={onUncomplete} onClick={onUncomplete}
disabled={loading} disabled={loading}
projectId={projectId} projectId={project}
> >
Revert to live Revert to live
</PermissionButton> </PermissionButton>
@ -335,7 +332,7 @@ const SafeToArchive: FC<{
size='small' size='small'
sx={{ mb: 2 }} sx={{ mb: 2 }}
onClick={onArchive} onClick={onArchive}
projectId={projectId} projectId={project}
> >
Archive feature Archive feature
</PermissionButton> </PermissionButton>
@ -393,7 +390,15 @@ const CompletedStageDescription: FC<{
lastSeenAt: string; lastSeenAt: string;
}>; }>;
children?: React.ReactNode; children?: React.ReactNode;
}> = ({ children, environments, onArchive, onUncomplete, loading }) => { project: string;
}> = ({
children,
environments,
onArchive,
onUncomplete,
loading,
project,
}) => {
return ( return (
<ConditionallyRender <ConditionallyRender
condition={isSafeToArchive(environments)} condition={isSafeToArchive(environments)}
@ -402,6 +407,7 @@ const CompletedStageDescription: FC<{
onArchive={onArchive} onArchive={onArchive}
onUncomplete={onUncomplete} onUncomplete={onUncomplete}
loading={loading} loading={loading}
project={project}
/> />
} }
elseShow={ elseShow={
@ -432,11 +438,20 @@ const FormatElapsedTime: FC<{
export const FeatureLifecycleTooltip: FC<{ export const FeatureLifecycleTooltip: FC<{
children: React.ReactElement<any, any>; children: React.ReactElement<any, any>;
stage: LifecycleStage; stage: LifecycleStage;
project: string;
onArchive: () => void; onArchive: () => void;
onComplete: () => void; onComplete: () => void;
onUncomplete: () => void; onUncomplete: () => void;
loading: boolean; loading: boolean;
}> = ({ children, stage, onArchive, onComplete, onUncomplete, loading }) => ( }> = ({
children,
stage,
project,
onArchive,
onComplete,
onUncomplete,
loading,
}) => (
<HtmlTooltip <HtmlTooltip
maxHeight={800} maxHeight={800}
maxWidth={350} maxWidth={350}
@ -482,6 +497,7 @@ export const FeatureLifecycleTooltip: FC<{
<LiveStageDescription <LiveStageDescription
onComplete={onComplete} onComplete={onComplete}
loading={loading} loading={loading}
project={project}
> >
<Environments environments={stage.environments} /> <Environments environments={stage.environments} />
</LiveStageDescription> </LiveStageDescription>
@ -492,6 +508,7 @@ export const FeatureLifecycleTooltip: FC<{
onArchive={onArchive} onArchive={onArchive}
onUncomplete={onUncomplete} onUncomplete={onUncomplete}
loading={loading} loading={loading}
project={project}
> >
<Environments environments={stage.environments} /> <Environments environments={stage.environments} />
</CompletedStageDescription> </CompletedStageDescription>

View File

@ -0,0 +1,72 @@
import { type FC, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import type { ILastSeenEnvironments } from 'interfaces/featureToggle';
import { Box } from '@mui/material';
import { FeatureEnvironmentSeen } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
import { FeatureLifecycle } from './FeatureLifecycle';
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
import { MarkCompletedDialogue } from './MarkCompletedDialogue';
export const FlagExposure: FC<{
project: string;
flagName: string;
onArchive: () => void;
}> = ({ project, flagName, onArchive }) => {
const navigate = useNavigate();
const { feature, refetchFeature } = useFeature(project, flagName);
const lastSeenEnvironments: ILastSeenEnvironments[] =
feature.environments?.map((env) => ({
name: env.name,
lastSeenAt: env.lastSeenAt,
enabled: env.enabled,
yes: env.yes,
no: env.no,
}));
const [showDelDialog, setShowDelDialog] = useState(false);
const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] =
useState(false);
return (
<Box sx={{ display: 'flex' }}>
<FeatureEnvironmentSeen
featureLastSeen={feature.lastSeenAt}
environments={lastSeenEnvironments}
/>
<FeatureLifecycle
feature={feature}
onArchive={() => setShowDelDialog(true)}
onComplete={() => setShowMarkCompletedDialogue(true)}
onUncomplete={refetchFeature}
/>
{feature.children.length > 0 ? (
<FeatureArchiveNotAllowedDialog
features={feature.children}
project={project}
isOpen={showDelDialog}
onClose={() => setShowDelDialog(false)}
/>
) : (
<FeatureArchiveDialog
isOpen={showDelDialog}
onConfirm={onArchive}
onClose={() => setShowDelDialog(false)}
projectId={project}
featureIds={[flagName]}
/>
)}
{feature.project ? (
<MarkCompletedDialogue
isOpen={showMarkCompletedDialogue}
setIsOpen={setShowMarkCompletedDialogue}
projectId={feature.project}
featureId={feature.name}
onComplete={refetchFeature}
/>
) : null}
</Box>
);
};

View File

@ -24,6 +24,7 @@ import { ProjectSetupComplete } from './ProjectSetupComplete';
import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboard'; import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboard';
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
import type { PersonalDashboardSchema } from '../../openapi'; import type { PersonalDashboardSchema } from '../../openapi';
import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure';
const ScreenExplanation = styled(Typography)(({ theme }) => ({ const ScreenExplanation = styled(Typography)(({ theme }) => ({
marginTop: theme.spacing(1), marginTop: theme.spacing(1),
@ -177,7 +178,8 @@ export const PersonalDashboard = () => {
const { projects, activeProject, setActiveProject } = useProjects(); const { projects, activeProject, setActiveProject } = useProjects();
const { personalDashboard } = usePersonalDashboard(); const { personalDashboard, refetch: refetchDashboard } =
usePersonalDashboard();
const [activeFlag, setActiveFlag] = useState< const [activeFlag, setActiveFlag] = useState<
PersonalDashboardSchema['flags'][0] | null PersonalDashboardSchema['flags'][0] | null
>(null); >(null);
@ -298,7 +300,20 @@ export const PersonalDashboard = () => {
<SpacedGridItem item lg={4} md={1}> <SpacedGridItem item lg={4} md={1}>
<Typography variant='h3'>My feature flags</Typography> <Typography variant='h3'>My feature flags</Typography>
</SpacedGridItem> </SpacedGridItem>
<SpacedGridItem item lg={8} md={1} /> <SpacedGridItem
item
lg={8}
md={1}
sx={{ display: 'flex', justifyContent: 'flex-end' }}
>
{activeFlag ? (
<FlagExposure
project={activeFlag.project}
flagName={activeFlag.name}
onArchive={refetchDashboard}
/>
) : null}
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}> <SpacedGridItem item lg={4} md={1}>
{personalDashboard && personalDashboard.flags.length > 0 ? ( {personalDashboard && personalDashboard.flags.length > 0 ? (
<List <List