1
0
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:
Nuno Góis 2024-11-06 10:41:39 +00:00 committed by GitHub
parent d6e722b7b3
commit 314a4d7113
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1122 additions and 266 deletions

View File

@ -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>

View File

@ -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}

View File

@ -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>
}
/>

View File

@ -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>
</>
);
};

View File

@ -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>
);
};

View File

@ -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}
/>
}
/>
</>
);
};

View File

@ -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;

View File

@ -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>
</>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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);