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:
parent
289324fd02
commit
a1a24ea0b1
@ -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}
|
||||||
|
@ -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 },
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user