mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +01:00
chore: new feature flag overview metadata panel (#8663)
https://linear.app/unleash/issue/2-2920/update-the-flag-overview-metadata-properties-to-match-the-new-design Updates the feature flag overview metadata panel to match the new design. This redesign is behind a feature flag, so we opted to go with a duplicate file approach. We should remember to clean this up if we decide to remove the flag. ![image](https://github.com/user-attachments/assets/0eb8464c-8279-46a8-9f64-9d914f56db36)
This commit is contained in:
parent
d6e722b7b3
commit
314a4d7113
@ -1,4 +1,4 @@
|
||||
import FeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData';
|
||||
import NewFeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData';
|
||||
import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
|
||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||
@ -8,12 +8,15 @@ import {
|
||||
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { usePageTitle } from 'hooks/usePageTitle';
|
||||
import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel';
|
||||
import { FeatureOverviewSidePanel as NewFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel';
|
||||
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
|
||||
import { styled } from '@mui/material';
|
||||
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
||||
import { useEffect } from 'react';
|
||||
import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import OldFeatureOverviewMetaData from './FeatureOverviewMetaData/OldFeatureOverviewMetaData';
|
||||
import { OldFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@ -46,6 +49,14 @@ const FeatureOverview = () => {
|
||||
setLastViewed({ featureId, projectId });
|
||||
}, [featureId]);
|
||||
|
||||
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||
const FeatureOverviewMetaData = flagOverviewRedesign
|
||||
? NewFeatureOverviewMetaData
|
||||
: OldFeatureOverviewMetaData;
|
||||
const FeatureOverviewSidePanel = flagOverviewRedesign
|
||||
? NewFeatureOverviewSidePanel
|
||||
: OldFeatureOverviewSidePanel;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type React from 'react';
|
||||
import { type FC, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
@ -16,16 +16,27 @@ import Delete from '@mui/icons-material/Delete';
|
||||
import Edit from '@mui/icons-material/Edit';
|
||||
import MoreVert from '@mui/icons-material/MoreVert';
|
||||
|
||||
const StyledIconButton = styled(IconButton)(({ theme }) => ({
|
||||
height: theme.spacing(3.5),
|
||||
width: theme.spacing(3.5),
|
||||
}));
|
||||
|
||||
const StyledPopover = styled(Popover)(({ theme }) => ({
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
padding: theme.spacing(1, 1.5),
|
||||
}));
|
||||
|
||||
export const DependencyActions: FC<{
|
||||
interface IDependencyActionsProps {
|
||||
feature: string;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}> = ({ feature, onEdit, onDelete }) => {
|
||||
}
|
||||
|
||||
export const DependencyActions = ({
|
||||
feature,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: IDependencyActionsProps) => {
|
||||
const id = `dependency-${feature}-actions`;
|
||||
const menuId = `${id}-menu`;
|
||||
|
||||
@ -42,8 +53,7 @@ export const DependencyActions: FC<{
|
||||
return (
|
||||
<Box>
|
||||
<Tooltip title='Dependency actions' arrow describeChild>
|
||||
<IconButton
|
||||
sx={{ mr: 0.25 }}
|
||||
<StyledIconButton
|
||||
id={id}
|
||||
aria-controls={open ? menuId : undefined}
|
||||
aria-haspopup='true'
|
||||
@ -52,7 +62,7 @@ export const DependencyActions: FC<{
|
||||
type='button'
|
||||
>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
</StyledIconButton>
|
||||
</Tooltip>
|
||||
<StyledPopover
|
||||
id={menuId}
|
||||
|
@ -1,13 +1,8 @@
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
|
||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { type FC, useState } from 'react';
|
||||
import {
|
||||
FlexRow,
|
||||
StyledDetail,
|
||||
StyledLabel,
|
||||
StyledLink,
|
||||
} from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
|
||||
import { useState } from 'react';
|
||||
import { StyledLink } from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
|
||||
import { DependencyActions } from './DependencyActions';
|
||||
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
@ -23,6 +18,20 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { VariantsTooltip } from './VariantsTooltip';
|
||||
import { styled } from '@mui/material';
|
||||
import {
|
||||
StyledMetaDataItem,
|
||||
StyledMetaDataItemLabel,
|
||||
StyledMetaDataItemValue,
|
||||
} from './FeatureOverviewMetaData';
|
||||
|
||||
const StyledPermissionButton = styled(PermissionButton)(({ theme }) => ({
|
||||
'&&&': {
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
lineHeight: 1,
|
||||
margin: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
const useDeleteDependency = (project: string, featureId: string) => {
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
@ -83,7 +92,11 @@ const useDeleteDependency = (project: string, featureId: string) => {
|
||||
return deleteDependency;
|
||||
};
|
||||
|
||||
export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
||||
interface IDependencyRowProps {
|
||||
feature: IFeatureToggle;
|
||||
}
|
||||
|
||||
export const DependencyRow = ({ feature }: IDependencyRowProps) => {
|
||||
const [showDependencyDialogue, setShowDependencyDialogue] = useState(false);
|
||||
const canAddParentDependency =
|
||||
Boolean(feature.project) &&
|
||||
@ -103,55 +116,54 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
||||
<ConditionallyRender
|
||||
condition={canAddParentDependency}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency:</StyledLabel>
|
||||
<PermissionButton
|
||||
size='small'
|
||||
permission={UPDATE_FEATURE_DEPENDENCY}
|
||||
projectId={feature.project}
|
||||
variant='text'
|
||||
onClick={() => {
|
||||
setShowDependencyDialogue(true);
|
||||
}}
|
||||
sx={(theme) => ({
|
||||
marginBottom: theme.spacing(0.4),
|
||||
})}
|
||||
>
|
||||
Add parent feature
|
||||
</PermissionButton>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
<StyledMetaDataItem>
|
||||
<StyledMetaDataItemLabel>
|
||||
Dependency:
|
||||
</StyledMetaDataItemLabel>
|
||||
<StyledPermissionButton
|
||||
size='small'
|
||||
permission={UPDATE_FEATURE_DEPENDENCY}
|
||||
projectId={feature.project}
|
||||
variant='text'
|
||||
onClick={() => {
|
||||
setShowDependencyDialogue(true);
|
||||
}}
|
||||
>
|
||||
Add parent feature
|
||||
</StyledPermissionButton>
|
||||
</StyledMetaDataItem>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasParentDependency}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency:</StyledLabel>
|
||||
<StyledMetaDataItem>
|
||||
<StyledMetaDataItemLabel>
|
||||
Dependency:
|
||||
</StyledMetaDataItemLabel>
|
||||
<StyledMetaDataItemValue>
|
||||
<StyledLink
|
||||
to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`}
|
||||
>
|
||||
{feature.dependencies[0]?.feature}
|
||||
</StyledLink>
|
||||
</StyledDetail>
|
||||
<ConditionallyRender
|
||||
condition={checkAccess(
|
||||
UPDATE_FEATURE_DEPENDENCY,
|
||||
environment,
|
||||
)}
|
||||
show={
|
||||
<DependencyActions
|
||||
feature={feature.name}
|
||||
onEdit={() =>
|
||||
setShowDependencyDialogue(true)
|
||||
}
|
||||
onDelete={deleteDependency}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FlexRow>
|
||||
<ConditionallyRender
|
||||
condition={checkAccess(
|
||||
UPDATE_FEATURE_DEPENDENCY,
|
||||
environment,
|
||||
)}
|
||||
show={
|
||||
<DependencyActions
|
||||
feature={feature.name}
|
||||
onEdit={() =>
|
||||
setShowDependencyDialogue(true)
|
||||
}
|
||||
onDelete={deleteDependency}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledMetaDataItemValue>
|
||||
</StyledMetaDataItem>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
@ -159,12 +171,12 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
||||
hasParentDependency && !feature.dependencies[0]?.enabled
|
||||
}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency value:</StyledLabel>
|
||||
<span>disabled</span>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
<StyledMetaDataItem>
|
||||
<StyledMetaDataItemLabel>
|
||||
Dependency value:
|
||||
</StyledMetaDataItemLabel>
|
||||
<span>disabled</span>
|
||||
</StyledMetaDataItem>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
@ -173,30 +185,28 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
||||
Boolean(feature.dependencies[0]?.variants?.length)
|
||||
}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency value:</StyledLabel>
|
||||
<VariantsTooltip
|
||||
variants={
|
||||
feature.dependencies[0]?.variants || []
|
||||
}
|
||||
/>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
<StyledMetaDataItem>
|
||||
<StyledMetaDataItemLabel>
|
||||
Dependency value:
|
||||
</StyledMetaDataItemLabel>
|
||||
<VariantsTooltip
|
||||
variants={feature.dependencies[0]?.variants || []}
|
||||
/>
|
||||
</StyledMetaDataItem>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasChildren}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Children:</StyledLabel>
|
||||
<ChildrenTooltip
|
||||
childFeatures={feature.children}
|
||||
project={feature.project}
|
||||
/>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
<StyledMetaDataItem>
|
||||
<StyledMetaDataItemLabel>
|
||||
Children:
|
||||
</StyledMetaDataItemLabel>
|
||||
<ChildrenTooltip
|
||||
childFeatures={feature.children}
|
||||
project={feature.project}
|
||||
/>
|
||||
</StyledMetaDataItem>
|
||||
}
|
||||
/>
|
||||
|
||||
|
@ -1,262 +1,201 @@
|
||||
import { Box, capitalize, styled } from '@mui/material';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { capitalize, styled } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import Edit from '@mui/icons-material/Edit';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { useState } from 'react';
|
||||
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
|
||||
import { StyledDetail } from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
|
||||
import { formatDateYMD } from 'utils/formatDate';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { FeatureEnvironmentSeen } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
||||
import { DependencyRow } from './DependencyRow';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import { useShowDependentFeatures } from './useShowDependentFeatures';
|
||||
import type { ILastSeenEnvironments } from 'interfaces/featureToggle';
|
||||
import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle';
|
||||
import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue';
|
||||
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||
import { TagRow } from './TagRow';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
const StyledMetaDataContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(3),
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '350px',
|
||||
minWidth: '350px',
|
||||
marginRight: theme.spacing(2),
|
||||
gap: theme.spacing(2),
|
||||
width: '350px',
|
||||
[theme.breakpoints.down(1000)]: {
|
||||
width: '100%',
|
||||
maxWidth: 'none',
|
||||
minWidth: 'auto',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledPaddingContainerTop = styled('div')({
|
||||
padding: '1.5rem 1.5rem 0 1.5rem',
|
||||
});
|
||||
|
||||
const StyledMetaDataHeader = styled('div')({
|
||||
const StyledMetaDataHeader = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const StyledHeader = styled('h2')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.mainHeader,
|
||||
fontWeight: 'normal',
|
||||
margin: 0,
|
||||
gap: theme.spacing(2),
|
||||
'& > svg': {
|
||||
height: theme.spacing(5),
|
||||
width: theme.spacing(5),
|
||||
padding: theme.spacing(0.5),
|
||||
backgroundColor: theme.palette.background.alternative,
|
||||
fill: theme.palette.primary.contrastText,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
},
|
||||
'& > h2': {
|
||||
fontSize: theme.fontSizes.mainHeader,
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledBody = styled('div')(({ theme }) => ({
|
||||
margin: theme.spacing(2, 0),
|
||||
const StyledBody = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const StyledMetaDataItem = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
minHeight: theme.spacing(4.25),
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
}));
|
||||
|
||||
const BodyItemWithIcon = styled('div')(({ theme }) => ({}));
|
||||
|
||||
const SpacedBodyItem = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(1, 0),
|
||||
}));
|
||||
|
||||
const StyledDescriptionContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const StyledDetailsContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const StyledDescription = styled('p')({
|
||||
wordBreak: 'break-word',
|
||||
});
|
||||
|
||||
const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({
|
||||
margin: theme.spacing(1),
|
||||
}));
|
||||
|
||||
export const StyledLabel = styled('span')(({ theme }) => ({
|
||||
export const StyledMetaDataItemLabel = styled('span')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
marginRight: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledMetaDataItemText = styled('span')({
|
||||
overflowWrap: 'anywhere',
|
||||
});
|
||||
|
||||
export const StyledMetaDataItemValue = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({
|
||||
height: theme.spacing(3.5),
|
||||
width: theme.spacing(3.5),
|
||||
}));
|
||||
|
||||
const FeatureOverviewMetaData = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||
const { project, description, type } = feature;
|
||||
const navigate = useNavigate();
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] =
|
||||
useState(false);
|
||||
|
||||
const { locationSettings } = useLocationSettings();
|
||||
const showDependentFeatures = useShowDependentFeatures(feature.project);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const lastSeenEnvironments: ILastSeenEnvironments[] =
|
||||
feature.environments?.map((env) => ({
|
||||
name: env.name,
|
||||
lastSeenAt: env.lastSeenAt,
|
||||
enabled: env.enabled,
|
||||
yes: env.yes,
|
||||
no: env.no,
|
||||
}));
|
||||
const [archiveDialogOpen, setArchiveDialogOpen] = useState(false);
|
||||
const [markCompletedDialogueOpen, setMarkCompletedDialogueOpen] =
|
||||
useState(false);
|
||||
|
||||
const IconComponent = getFeatureTypeIcons(type);
|
||||
const { project, description, type } = feature;
|
||||
|
||||
const showDependentFeatures = useShowDependentFeatures(project);
|
||||
|
||||
const FlagTypeIcon = getFeatureTypeIcons(type);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledPaddingContainerTop>
|
||||
<>
|
||||
<StyledMetaDataContainer>
|
||||
<StyledMetaDataHeader data-loading>
|
||||
<IconComponent
|
||||
sx={(theme) => ({
|
||||
marginRight: theme.spacing(2),
|
||||
height: '40px',
|
||||
width: '40px',
|
||||
padding: theme.spacing(0.5),
|
||||
backgroundColor:
|
||||
theme.palette.background.alternative,
|
||||
fill: theme.palette.primary.contrastText,
|
||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||
})}
|
||||
/>{' '}
|
||||
<StyledHeader>{capitalize(type || '')} toggle</StyledHeader>
|
||||
<FlagTypeIcon />
|
||||
<h2>{capitalize(type || '')} flag</h2>
|
||||
</StyledMetaDataHeader>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(description)}
|
||||
show={
|
||||
<StyledMetaDataItem data-loading>
|
||||
<StyledMetaDataItemText>
|
||||
{description}
|
||||
</StyledMetaDataItemText>
|
||||
</StyledMetaDataItem>
|
||||
}
|
||||
/>
|
||||
<StyledBody>
|
||||
<SpacedBodyItem data-loading>
|
||||
<StyledLabel>Project:</StyledLabel>
|
||||
<Box sx={{ wordBreak: 'break-all' }}>{project}</Box>
|
||||
</SpacedBodyItem>
|
||||
<StyledMetaDataItem>
|
||||
<StyledMetaDataItemLabel>
|
||||
Project:
|
||||
</StyledMetaDataItemLabel>
|
||||
<StyledMetaDataItemText data-loading>
|
||||
{project}
|
||||
</StyledMetaDataItemText>
|
||||
</StyledMetaDataItem>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(feature.lifecycle)}
|
||||
show={
|
||||
<SpacedBodyItem data-loading>
|
||||
<StyledLabel>Lifecycle:</StyledLabel>
|
||||
<StyledMetaDataItem data-loading>
|
||||
<StyledMetaDataItemLabel>
|
||||
Lifecycle:
|
||||
</StyledMetaDataItemLabel>
|
||||
<FeatureLifecycle
|
||||
feature={feature}
|
||||
onArchive={() => setShowDelDialog(true)}
|
||||
onArchive={() => setArchiveDialogOpen(true)}
|
||||
onComplete={() =>
|
||||
setShowMarkCompletedDialogue(true)
|
||||
setMarkCompletedDialogueOpen(true)
|
||||
}
|
||||
onUncomplete={refetchFeature}
|
||||
/>
|
||||
</SpacedBodyItem>
|
||||
</StyledMetaDataItem>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={Boolean(description)}
|
||||
show={
|
||||
<BodyItemWithIcon data-loading sx={{ pt: 1 }}>
|
||||
<StyledLabel>Description:</StyledLabel>
|
||||
<StyledDescriptionContainer>
|
||||
<StyledDescription>
|
||||
{description}
|
||||
</StyledDescription>
|
||||
<PermissionIconButton
|
||||
size='medium'
|
||||
projectId={projectId}
|
||||
permission={UPDATE_FEATURE}
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/features/${featureId}/settings`}
|
||||
tooltipProps={{
|
||||
title: 'Edit description',
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
</StyledDescriptionContainer>
|
||||
</BodyItemWithIcon>
|
||||
}
|
||||
elseShow={
|
||||
<div data-loading>
|
||||
<StyledDescriptionContainer>
|
||||
No description.{' '}
|
||||
<PermissionIconButton
|
||||
size='medium'
|
||||
projectId={projectId}
|
||||
permission={UPDATE_FEATURE}
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/features/${featureId}/settings`}
|
||||
tooltipProps={{
|
||||
title: 'Edit description',
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
</StyledDescriptionContainer>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<BodyItemWithIcon>
|
||||
<StyledDetailsContainer>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Created at:</StyledLabel>
|
||||
<span>
|
||||
{formatDateYMD(
|
||||
parseISO(feature.createdAt),
|
||||
locationSettings.locale,
|
||||
)}
|
||||
</span>
|
||||
</StyledDetail>
|
||||
|
||||
<FeatureEnvironmentSeen
|
||||
featureLastSeen={feature.lastSeenAt}
|
||||
environments={lastSeenEnvironments}
|
||||
/>
|
||||
</StyledDetailsContainer>
|
||||
</BodyItemWithIcon>
|
||||
<StyledMetaDataItem>
|
||||
<StyledMetaDataItemLabel>
|
||||
Created at:
|
||||
</StyledMetaDataItemLabel>
|
||||
<StyledMetaDataItemText data-loading>
|
||||
{formatDateYMD(
|
||||
parseISO(feature.createdAt),
|
||||
locationSettings.locale,
|
||||
)}
|
||||
</StyledMetaDataItemText>
|
||||
</StyledMetaDataItem>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(feature.createdBy)}
|
||||
show={() => (
|
||||
<BodyItemWithIcon>
|
||||
<StyledDetailsContainer>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Created by:</StyledLabel>
|
||||
<span>{feature.createdBy?.name}</span>
|
||||
</StyledDetail>
|
||||
<StyledMetaDataItem>
|
||||
<StyledMetaDataItemLabel>
|
||||
Created by:
|
||||
</StyledMetaDataItemLabel>
|
||||
<StyledMetaDataItemValue>
|
||||
<StyledMetaDataItemText data-loading>
|
||||
{feature.createdBy?.name}
|
||||
</StyledMetaDataItemText>
|
||||
<StyledUserAvatar
|
||||
user={feature.createdBy}
|
||||
/>
|
||||
</StyledDetailsContainer>
|
||||
</BodyItemWithIcon>
|
||||
</StyledMetaDataItemValue>
|
||||
</StyledMetaDataItem>
|
||||
)}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={showDependentFeatures}
|
||||
show={<DependencyRow feature={feature} />}
|
||||
/>
|
||||
<TagRow feature={feature} />
|
||||
</StyledBody>
|
||||
</StyledPaddingContainerTop>
|
||||
</StyledMetaDataContainer>
|
||||
<ConditionallyRender
|
||||
condition={feature.children.length > 0}
|
||||
show={
|
||||
<FeatureArchiveNotAllowedDialog
|
||||
features={feature.children}
|
||||
project={projectId}
|
||||
isOpen={showDelDialog}
|
||||
onClose={() => setShowDelDialog(false)}
|
||||
isOpen={archiveDialogOpen}
|
||||
onClose={() => setArchiveDialogOpen(false)}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<FeatureArchiveDialog
|
||||
isOpen={showDelDialog}
|
||||
isOpen={archiveDialogOpen}
|
||||
onConfirm={() => {
|
||||
navigate(`/projects/${projectId}`);
|
||||
}}
|
||||
onClose={() => setShowDelDialog(false)}
|
||||
onClose={() => setArchiveDialogOpen(false)}
|
||||
projectId={projectId}
|
||||
featureIds={[featureId]}
|
||||
/>
|
||||
@ -266,15 +205,15 @@ const FeatureOverviewMetaData = () => {
|
||||
condition={Boolean(feature.project)}
|
||||
show={
|
||||
<MarkCompletedDialogue
|
||||
isOpen={showMarkCompletedDialogue}
|
||||
setIsOpen={setShowMarkCompletedDialogue}
|
||||
isOpen={markCompletedDialogueOpen}
|
||||
setIsOpen={setMarkCompletedDialogueOpen}
|
||||
projectId={feature.project}
|
||||
featureId={feature.name}
|
||||
onComplete={refetchFeature}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,104 @@
|
||||
import type React from 'react';
|
||||
import { type FC, useState } from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Popover,
|
||||
styled,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import Delete from '@mui/icons-material/Delete';
|
||||
import Edit from '@mui/icons-material/Edit';
|
||||
import MoreVert from '@mui/icons-material/MoreVert';
|
||||
|
||||
const StyledPopover = styled(Popover)(({ theme }) => ({
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
padding: theme.spacing(1, 1.5),
|
||||
}));
|
||||
|
||||
export const OldDependencyActions: FC<{
|
||||
feature: string;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}> = ({ feature, onEdit, onDelete }) => {
|
||||
const id = `dependency-${feature}-actions`;
|
||||
const menuId = `${id}-menu`;
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const openActions = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const closeActions = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Tooltip title='Dependency actions' arrow describeChild>
|
||||
<IconButton
|
||||
sx={{ mr: 0.25 }}
|
||||
id={id}
|
||||
aria-controls={open ? menuId : undefined}
|
||||
aria-haspopup='true'
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={openActions}
|
||||
type='button'
|
||||
>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<StyledPopover
|
||||
id={menuId}
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={closeActions}
|
||||
transformOrigin={{
|
||||
horizontal: 'right',
|
||||
vertical: 'top',
|
||||
}}
|
||||
anchorOrigin={{
|
||||
horizontal: 'right',
|
||||
vertical: 'bottom',
|
||||
}}
|
||||
disableScrollLock={true}
|
||||
>
|
||||
<MenuList aria-labelledby={id}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onEdit();
|
||||
closeActions();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Edit />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant='body2'>Edit</Typography>
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
closeActions();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Delete />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant='body2'>Delete</Typography>
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</StyledPopover>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -0,0 +1,219 @@
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
|
||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { type FC, useState } from 'react';
|
||||
import {
|
||||
FlexRow,
|
||||
StyledDetail,
|
||||
StyledLabel,
|
||||
StyledLink,
|
||||
} from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
|
||||
import { OldDependencyActions } from './OldDependencyActions';
|
||||
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { ChildrenTooltip } from './ChildrenTooltip';
|
||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||
import { UPDATE_FEATURE_DEPENDENCY } from 'component/providers/AccessProvider/permissions';
|
||||
import { useCheckProjectAccess } from 'hooks/useHasAccess';
|
||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { VariantsTooltip } from './VariantsTooltip';
|
||||
|
||||
const useDeleteDependency = (project: string, featureId: string) => {
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
const { addChange } = useChangeRequestApi();
|
||||
const { refetch: refetchChangeRequests } =
|
||||
usePendingChangeRequests(project);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { refetchFeature } = useFeature(project, featureId);
|
||||
const environment = useHighestPermissionChangeRequestEnvironment(project)();
|
||||
const { isChangeRequestConfiguredInAnyEnv } =
|
||||
useChangeRequestsEnabled(project);
|
||||
const { removeDependencies } = useDependentFeaturesApi(project);
|
||||
|
||||
const handleAddChange = async () => {
|
||||
if (!environment) {
|
||||
console.error('No change request environment');
|
||||
return;
|
||||
}
|
||||
await addChange(project, environment, [
|
||||
{
|
||||
action: 'deleteDependency',
|
||||
feature: featureId,
|
||||
payload: undefined,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const deleteDependency = async () => {
|
||||
try {
|
||||
if (isChangeRequestConfiguredInAnyEnv()) {
|
||||
await handleAddChange();
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType: 'delete dependency added to change request',
|
||||
},
|
||||
});
|
||||
setToastData({
|
||||
text: `${featureId} dependency will be removed`,
|
||||
type: 'success',
|
||||
title: 'Change added to a draft',
|
||||
});
|
||||
await refetchChangeRequests();
|
||||
} else {
|
||||
await removeDependencies(featureId);
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType: 'dependency removed',
|
||||
},
|
||||
});
|
||||
setToastData({ title: 'Dependency removed', type: 'success' });
|
||||
await refetchFeature();
|
||||
}
|
||||
} catch (error) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
return deleteDependency;
|
||||
};
|
||||
|
||||
export const OldDependencyRow: FC<{ feature: IFeatureToggle }> = ({
|
||||
feature,
|
||||
}) => {
|
||||
const [showDependencyDialogue, setShowDependencyDialogue] = useState(false);
|
||||
const canAddParentDependency =
|
||||
Boolean(feature.project) &&
|
||||
feature.dependencies.length === 0 &&
|
||||
feature.children.length === 0;
|
||||
const hasParentDependency =
|
||||
Boolean(feature.project) && Boolean(feature.dependencies.length > 0);
|
||||
const hasChildren = Boolean(feature.project) && feature.children.length > 0;
|
||||
const environment = useHighestPermissionChangeRequestEnvironment(
|
||||
feature.project,
|
||||
)();
|
||||
const checkAccess = useCheckProjectAccess(feature.project);
|
||||
const deleteDependency = useDeleteDependency(feature.project, feature.name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={canAddParentDependency}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency:</StyledLabel>
|
||||
<PermissionButton
|
||||
size='small'
|
||||
permission={UPDATE_FEATURE_DEPENDENCY}
|
||||
projectId={feature.project}
|
||||
variant='text'
|
||||
onClick={() => {
|
||||
setShowDependencyDialogue(true);
|
||||
}}
|
||||
sx={(theme) => ({
|
||||
marginBottom: theme.spacing(0.4),
|
||||
})}
|
||||
>
|
||||
Add parent feature
|
||||
</PermissionButton>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasParentDependency}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency:</StyledLabel>
|
||||
<StyledLink
|
||||
to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`}
|
||||
>
|
||||
{feature.dependencies[0]?.feature}
|
||||
</StyledLink>
|
||||
</StyledDetail>
|
||||
<ConditionallyRender
|
||||
condition={checkAccess(
|
||||
UPDATE_FEATURE_DEPENDENCY,
|
||||
environment,
|
||||
)}
|
||||
show={
|
||||
<OldDependencyActions
|
||||
feature={feature.name}
|
||||
onEdit={() =>
|
||||
setShowDependencyDialogue(true)
|
||||
}
|
||||
onDelete={deleteDependency}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FlexRow>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
hasParentDependency && !feature.dependencies[0]?.enabled
|
||||
}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency value:</StyledLabel>
|
||||
<span>disabled</span>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
hasParentDependency &&
|
||||
Boolean(feature.dependencies[0]?.variants?.length)
|
||||
}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency value:</StyledLabel>
|
||||
<VariantsTooltip
|
||||
variants={
|
||||
feature.dependencies[0]?.variants || []
|
||||
}
|
||||
/>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasChildren}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Children:</StyledLabel>
|
||||
<ChildrenTooltip
|
||||
childFeatures={feature.children}
|
||||
project={feature.project}
|
||||
/>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={Boolean(feature.project)}
|
||||
show={
|
||||
<AddDependencyDialogue
|
||||
project={feature.project}
|
||||
featureId={feature.name}
|
||||
parentDependency={feature.dependencies[0]}
|
||||
onClose={() => setShowDependencyDialogue(false)}
|
||||
showDependencyDialogue={showDependencyDialogue}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,281 @@
|
||||
import { Box, capitalize, styled } from '@mui/material';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import Edit from '@mui/icons-material/Edit';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { useState } from 'react';
|
||||
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
|
||||
import { StyledDetail } from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
|
||||
import { formatDateYMD } from 'utils/formatDate';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { FeatureEnvironmentSeen } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
||||
import { OldDependencyRow } from './OldDependencyRow';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import { useShowDependentFeatures } from './useShowDependentFeatures';
|
||||
import type { ILastSeenEnvironments } from 'interfaces/featureToggle';
|
||||
import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle';
|
||||
import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue';
|
||||
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '350px',
|
||||
minWidth: '350px',
|
||||
marginRight: theme.spacing(2),
|
||||
[theme.breakpoints.down(1000)]: {
|
||||
width: '100%',
|
||||
maxWidth: 'none',
|
||||
minWidth: 'auto',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledPaddingContainerTop = styled('div')({
|
||||
padding: '1.5rem 1.5rem 0 1.5rem',
|
||||
});
|
||||
|
||||
const StyledMetaDataHeader = styled('div')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const StyledHeader = styled('h2')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.mainHeader,
|
||||
fontWeight: 'normal',
|
||||
margin: 0,
|
||||
}));
|
||||
|
||||
const StyledBody = styled('div')(({ theme }) => ({
|
||||
margin: theme.spacing(2, 0),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
}));
|
||||
|
||||
const BodyItemWithIcon = styled('div')(({ theme }) => ({}));
|
||||
|
||||
const SpacedBodyItem = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(1, 0),
|
||||
}));
|
||||
|
||||
const StyledDescriptionContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const StyledDetailsContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const StyledDescription = styled('p')({
|
||||
wordBreak: 'break-word',
|
||||
});
|
||||
|
||||
const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({
|
||||
margin: theme.spacing(1),
|
||||
}));
|
||||
|
||||
export const StyledLabel = styled('span')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
marginRight: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const OldFeatureOverviewMetaData = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||
const { project, description, type } = feature;
|
||||
const navigate = useNavigate();
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] =
|
||||
useState(false);
|
||||
|
||||
const { locationSettings } = useLocationSettings();
|
||||
const showDependentFeatures = useShowDependentFeatures(feature.project);
|
||||
|
||||
const lastSeenEnvironments: ILastSeenEnvironments[] =
|
||||
feature.environments?.map((env) => ({
|
||||
name: env.name,
|
||||
lastSeenAt: env.lastSeenAt,
|
||||
enabled: env.enabled,
|
||||
yes: env.yes,
|
||||
no: env.no,
|
||||
}));
|
||||
|
||||
const IconComponent = getFeatureTypeIcons(type);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledPaddingContainerTop>
|
||||
<StyledMetaDataHeader data-loading>
|
||||
<IconComponent
|
||||
sx={(theme) => ({
|
||||
marginRight: theme.spacing(2),
|
||||
height: '40px',
|
||||
width: '40px',
|
||||
padding: theme.spacing(0.5),
|
||||
backgroundColor:
|
||||
theme.palette.background.alternative,
|
||||
fill: theme.palette.primary.contrastText,
|
||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||
})}
|
||||
/>{' '}
|
||||
<StyledHeader>{capitalize(type || '')} toggle</StyledHeader>
|
||||
</StyledMetaDataHeader>
|
||||
<StyledBody>
|
||||
<SpacedBodyItem data-loading>
|
||||
<StyledLabel>Project:</StyledLabel>
|
||||
<Box sx={{ wordBreak: 'break-all' }}>{project}</Box>
|
||||
</SpacedBodyItem>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(feature.lifecycle)}
|
||||
show={
|
||||
<SpacedBodyItem data-loading>
|
||||
<StyledLabel>Lifecycle:</StyledLabel>
|
||||
<FeatureLifecycle
|
||||
feature={feature}
|
||||
onArchive={() => setShowDelDialog(true)}
|
||||
onComplete={() =>
|
||||
setShowMarkCompletedDialogue(true)
|
||||
}
|
||||
onUncomplete={refetchFeature}
|
||||
/>
|
||||
</SpacedBodyItem>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={Boolean(description)}
|
||||
show={
|
||||
<BodyItemWithIcon data-loading sx={{ pt: 1 }}>
|
||||
<StyledLabel>Description:</StyledLabel>
|
||||
<StyledDescriptionContainer>
|
||||
<StyledDescription>
|
||||
{description}
|
||||
</StyledDescription>
|
||||
<PermissionIconButton
|
||||
size='medium'
|
||||
projectId={projectId}
|
||||
permission={UPDATE_FEATURE}
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/features/${featureId}/settings`}
|
||||
tooltipProps={{
|
||||
title: 'Edit description',
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
</StyledDescriptionContainer>
|
||||
</BodyItemWithIcon>
|
||||
}
|
||||
elseShow={
|
||||
<div data-loading>
|
||||
<StyledDescriptionContainer>
|
||||
No description.{' '}
|
||||
<PermissionIconButton
|
||||
size='medium'
|
||||
projectId={projectId}
|
||||
permission={UPDATE_FEATURE}
|
||||
component={Link}
|
||||
to={`/projects/${projectId}/features/${featureId}/settings`}
|
||||
tooltipProps={{
|
||||
title: 'Edit description',
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
</StyledDescriptionContainer>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<BodyItemWithIcon>
|
||||
<StyledDetailsContainer>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Created at:</StyledLabel>
|
||||
<span>
|
||||
{formatDateYMD(
|
||||
parseISO(feature.createdAt),
|
||||
locationSettings.locale,
|
||||
)}
|
||||
</span>
|
||||
</StyledDetail>
|
||||
|
||||
<FeatureEnvironmentSeen
|
||||
featureLastSeen={feature.lastSeenAt}
|
||||
environments={lastSeenEnvironments}
|
||||
/>
|
||||
</StyledDetailsContainer>
|
||||
</BodyItemWithIcon>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(feature.createdBy)}
|
||||
show={() => (
|
||||
<BodyItemWithIcon>
|
||||
<StyledDetailsContainer>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Created by:</StyledLabel>
|
||||
<span>{feature.createdBy?.name}</span>
|
||||
</StyledDetail>
|
||||
<StyledUserAvatar
|
||||
user={feature.createdBy}
|
||||
/>
|
||||
</StyledDetailsContainer>
|
||||
</BodyItemWithIcon>
|
||||
)}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={showDependentFeatures}
|
||||
show={<OldDependencyRow feature={feature} />}
|
||||
/>
|
||||
</StyledBody>
|
||||
</StyledPaddingContainerTop>
|
||||
<ConditionallyRender
|
||||
condition={feature.children.length > 0}
|
||||
show={
|
||||
<FeatureArchiveNotAllowedDialog
|
||||
features={feature.children}
|
||||
project={projectId}
|
||||
isOpen={showDelDialog}
|
||||
onClose={() => setShowDelDialog(false)}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<FeatureArchiveDialog
|
||||
isOpen={showDelDialog}
|
||||
onConfirm={() => {
|
||||
navigate(`/projects/${projectId}`);
|
||||
}}
|
||||
onClose={() => setShowDelDialog(false)}
|
||||
projectId={projectId}
|
||||
featureIds={[featureId]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(feature.project)}
|
||||
show={
|
||||
<MarkCompletedDialogue
|
||||
isOpen={showMarkCompletedDialogue}
|
||||
setIsOpen={setShowMarkCompletedDialogue}
|
||||
projectId={feature.project}
|
||||
featureId={feature.name}
|
||||
onComplete={refetchFeature}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default OldFeatureOverviewMetaData;
|
@ -0,0 +1,203 @@
|
||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { useContext, useState } from 'react';
|
||||
import { Chip, styled, Tooltip } from '@mui/material';
|
||||
import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags';
|
||||
import Add from '@mui/icons-material/Add';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import { ManageTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog';
|
||||
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
import AccessContext from 'contexts/AccessContext';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import type { ITag } from 'interfaces/tags';
|
||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import {
|
||||
StyledMetaDataItem,
|
||||
StyledMetaDataItemLabel,
|
||||
} from './FeatureOverviewMetaData';
|
||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||
|
||||
const StyledPermissionButton = styled(PermissionButton)(({ theme }) => ({
|
||||
'&&&': {
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
lineHeight: 1,
|
||||
margin: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledTagRow = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'start',
|
||||
minHeight: theme.spacing(4.25),
|
||||
lineHeight: theme.spacing(4.25),
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
justifyContent: 'start',
|
||||
}));
|
||||
|
||||
const StyledTagContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
gap: theme.spacing(1),
|
||||
flexWrap: 'wrap',
|
||||
marginTop: theme.spacing(0.75),
|
||||
}));
|
||||
|
||||
const StyledChip = styled(Chip)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
overflowWrap: 'anywhere',
|
||||
backgroundColor: theme.palette.neutral.light,
|
||||
color: theme.palette.neutral.dark,
|
||||
'&&& > svg': {
|
||||
color: theme.palette.neutral.dark,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAddedTag = styled(StyledChip)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.secondary.light,
|
||||
color: theme.palette.secondary.dark,
|
||||
'&&& > svg': {
|
||||
color: theme.palette.secondary.dark,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IFeatureOverviewSidePanelTagsProps {
|
||||
feature: IFeatureToggle;
|
||||
}
|
||||
|
||||
export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => {
|
||||
const { tags, refetch } = useFeatureTags(feature.name);
|
||||
const { deleteTagFromFeature } = useFeatureApi();
|
||||
|
||||
const [manageTagsOpen, setManageTagsOpen] = useState(false);
|
||||
const [removeTagOpen, setRemoveTagOpen] = useState(false);
|
||||
const [selectedTag, setSelectedTag] = useState<ITag>();
|
||||
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const canUpdateTags = hasAccess(UPDATE_FEATURE, feature.project);
|
||||
|
||||
const handleRemove = async () => {
|
||||
if (!selectedTag) return;
|
||||
try {
|
||||
await deleteTagFromFeature(
|
||||
feature.name,
|
||||
selectedTag.type,
|
||||
selectedTag.value,
|
||||
);
|
||||
refetch();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Tag removed',
|
||||
text: 'Successfully removed tag',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={!tags.length}
|
||||
show={
|
||||
<StyledMetaDataItem>
|
||||
<StyledMetaDataItemLabel>Tags:</StyledMetaDataItemLabel>
|
||||
<StyledPermissionButton
|
||||
size='small'
|
||||
permission={UPDATE_FEATURE}
|
||||
projectId={feature.project}
|
||||
variant='text'
|
||||
onClick={() => {
|
||||
setManageTagsOpen(true);
|
||||
}}
|
||||
>
|
||||
Add tag
|
||||
</StyledPermissionButton>
|
||||
</StyledMetaDataItem>
|
||||
}
|
||||
elseShow={
|
||||
<StyledTagRow>
|
||||
<StyledMetaDataItemLabel>Tags:</StyledMetaDataItemLabel>
|
||||
<StyledTagContainer>
|
||||
{tags.map((tag) => {
|
||||
const tagLabel = `${tag.type}:${tag.value}`;
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
tagLabel.length > 35 ? tagLabel : ''
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<StyledAddedTag
|
||||
key={tagLabel}
|
||||
label={tagLabel}
|
||||
size='small'
|
||||
deleteIcon={
|
||||
<Tooltip
|
||||
title='Remove tag'
|
||||
arrow
|
||||
>
|
||||
<ClearIcon />
|
||||
</Tooltip>
|
||||
}
|
||||
onDelete={
|
||||
canUpdateTags
|
||||
? () => {
|
||||
setRemoveTagOpen(
|
||||
true,
|
||||
);
|
||||
setSelectedTag(tag);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
<ConditionallyRender
|
||||
condition={canUpdateTags}
|
||||
show={
|
||||
<StyledChip
|
||||
icon={<Add />}
|
||||
label='Add tag'
|
||||
size='small'
|
||||
onClick={() => setManageTagsOpen(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledTagContainer>
|
||||
</StyledTagRow>
|
||||
}
|
||||
/>
|
||||
<ManageTagsDialog
|
||||
open={manageTagsOpen}
|
||||
setOpen={setManageTagsOpen}
|
||||
/>
|
||||
<Dialogue
|
||||
open={removeTagOpen}
|
||||
primaryButtonText='Remove tag'
|
||||
secondaryButtonText='Cancel'
|
||||
onClose={() => {
|
||||
setRemoveTagOpen(false);
|
||||
setSelectedTag(undefined);
|
||||
}}
|
||||
onClick={() => {
|
||||
setRemoveTagOpen(false);
|
||||
handleRemove();
|
||||
setSelectedTag(undefined);
|
||||
}}
|
||||
title='Remove tag'
|
||||
>
|
||||
You are about to remove tag:{' '}
|
||||
<strong>
|
||||
{selectedTag?.type}:{selectedTag?.value}
|
||||
</strong>
|
||||
</Dialogue>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,9 +1,8 @@
|
||||
import { Box, Divider, styled } from '@mui/material';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches';
|
||||
import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags';
|
||||
import { Sticky } from 'component/common/Sticky/Sticky';
|
||||
|
||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||
@ -75,15 +74,6 @@ export const FeatureOverviewSidePanel = ({
|
||||
hiddenEnvironments={hiddenEnvironments}
|
||||
setHiddenEnvironments={setHiddenEnvironments}
|
||||
/>
|
||||
<Divider />
|
||||
<FeatureOverviewSidePanelTags
|
||||
header={
|
||||
<StyledHeader data-loading>
|
||||
Tags for this feature flag
|
||||
</StyledHeader>
|
||||
}
|
||||
feature={feature}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,89 @@
|
||||
import { Box, Divider, styled } from '@mui/material';
|
||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches';
|
||||
import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags';
|
||||
import { Sticky } from 'component/common/Sticky/Sticky';
|
||||
|
||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||
top: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '350px',
|
||||
minWidth: '350px',
|
||||
marginRight: '1rem',
|
||||
marginTop: '1rem',
|
||||
[theme.breakpoints.down(1000)]: {
|
||||
marginBottom: '1rem',
|
||||
width: '100%',
|
||||
maxWidth: 'none',
|
||||
minWidth: 'auto',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledHeader = styled('h3')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
alignItems: 'center',
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
margin: 0,
|
||||
marginBottom: theme.spacing(3),
|
||||
|
||||
// Make the help icon align with the text.
|
||||
'& > :last-child': {
|
||||
position: 'relative',
|
||||
top: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IFeatureOverviewSidePanelProps {
|
||||
hiddenEnvironments: Set<String>;
|
||||
setHiddenEnvironments: (environment: string) => void;
|
||||
}
|
||||
|
||||
export const OldFeatureOverviewSidePanel = ({
|
||||
hiddenEnvironments,
|
||||
setHiddenEnvironments,
|
||||
}: IFeatureOverviewSidePanelProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
const isSticky = feature.environments?.length <= 3;
|
||||
|
||||
return (
|
||||
<StyledContainer as={isSticky ? Sticky : Box}>
|
||||
<FeatureOverviewSidePanelEnvironmentSwitches
|
||||
header={
|
||||
<StyledHeader data-loading>
|
||||
Enabled in environments (
|
||||
{
|
||||
feature.environments.filter(
|
||||
({ enabled }) => enabled,
|
||||
).length
|
||||
}
|
||||
)
|
||||
<HelpIcon
|
||||
tooltip='When a feature is switched off in an environment, it will always return false. When switched on, it will return true or false depending on its strategies.'
|
||||
placement='top'
|
||||
/>
|
||||
</StyledHeader>
|
||||
}
|
||||
feature={feature}
|
||||
hiddenEnvironments={hiddenEnvironments}
|
||||
setHiddenEnvironments={setHiddenEnvironments}
|
||||
/>
|
||||
<Divider />
|
||||
<FeatureOverviewSidePanelTags
|
||||
header={
|
||||
<StyledHeader data-loading>
|
||||
Tags for this feature flag
|
||||
</StyledHeader>
|
||||
}
|
||||
feature={feature}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -88,7 +88,7 @@ export const ManageTagsDialog = ({ open, setOpen }: IManageTagsProps) => {
|
||||
tagsToOptions(tags.filter((tag) => tag.type === tagType.name)),
|
||||
);
|
||||
}
|
||||
}, [JSON.stringify(tags), tagType]);
|
||||
}, [JSON.stringify(tags), tagType, open]);
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false);
|
||||
|
Loading…
Reference in New Issue
Block a user