mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02: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. 
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 FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
|
||||||
import { Route, Routes, useNavigate } from 'react-router-dom';
|
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
@ -8,12 +8,15 @@ import {
|
|||||||
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
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 { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
|
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 }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -46,6 +49,14 @@ const FeatureOverview = () => {
|
|||||||
setLastViewed({ featureId, projectId });
|
setLastViewed({ featureId, projectId });
|
||||||
}, [featureId]);
|
}, [featureId]);
|
||||||
|
|
||||||
|
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
|
||||||
|
const FeatureOverviewMetaData = flagOverviewRedesign
|
||||||
|
? NewFeatureOverviewMetaData
|
||||||
|
: OldFeatureOverviewMetaData;
|
||||||
|
const FeatureOverviewSidePanel = flagOverviewRedesign
|
||||||
|
? NewFeatureOverviewSidePanel
|
||||||
|
: OldFeatureOverviewSidePanel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { type FC, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IconButton,
|
IconButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
@ -16,16 +16,27 @@ import Delete from '@mui/icons-material/Delete';
|
|||||||
import Edit from '@mui/icons-material/Edit';
|
import Edit from '@mui/icons-material/Edit';
|
||||||
import MoreVert from '@mui/icons-material/MoreVert';
|
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 }) => ({
|
const StyledPopover = styled(Popover)(({ theme }) => ({
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
padding: theme.spacing(1, 1.5),
|
padding: theme.spacing(1, 1.5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const DependencyActions: FC<{
|
interface IDependencyActionsProps {
|
||||||
feature: string;
|
feature: string;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}> = ({ feature, onEdit, onDelete }) => {
|
}
|
||||||
|
|
||||||
|
export const DependencyActions = ({
|
||||||
|
feature,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
}: IDependencyActionsProps) => {
|
||||||
const id = `dependency-${feature}-actions`;
|
const id = `dependency-${feature}-actions`;
|
||||||
const menuId = `${id}-menu`;
|
const menuId = `${id}-menu`;
|
||||||
|
|
||||||
@ -42,8 +53,7 @@ export const DependencyActions: FC<{
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Tooltip title='Dependency actions' arrow describeChild>
|
<Tooltip title='Dependency actions' arrow describeChild>
|
||||||
<IconButton
|
<StyledIconButton
|
||||||
sx={{ mr: 0.25 }}
|
|
||||||
id={id}
|
id={id}
|
||||||
aria-controls={open ? menuId : undefined}
|
aria-controls={open ? menuId : undefined}
|
||||||
aria-haspopup='true'
|
aria-haspopup='true'
|
||||||
@ -52,7 +62,7 @@ export const DependencyActions: FC<{
|
|||||||
type='button'
|
type='button'
|
||||||
>
|
>
|
||||||
<MoreVert />
|
<MoreVert />
|
||||||
</IconButton>
|
</StyledIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<StyledPopover
|
<StyledPopover
|
||||||
id={menuId}
|
id={menuId}
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
|
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
|
||||||
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
import type { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
import { type FC, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import { StyledLink } from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
|
||||||
FlexRow,
|
|
||||||
StyledDetail,
|
|
||||||
StyledLabel,
|
|
||||||
StyledLink,
|
|
||||||
} from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
|
|
||||||
import { DependencyActions } from './DependencyActions';
|
import { DependencyActions } from './DependencyActions';
|
||||||
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
|
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
@ -23,6 +18,20 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
|||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import { VariantsTooltip } from './VariantsTooltip';
|
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 useDeleteDependency = (project: string, featureId: string) => {
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
@ -83,7 +92,11 @@ const useDeleteDependency = (project: string, featureId: string) => {
|
|||||||
return deleteDependency;
|
return deleteDependency;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
interface IDependencyRowProps {
|
||||||
|
feature: IFeatureToggle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DependencyRow = ({ feature }: IDependencyRowProps) => {
|
||||||
const [showDependencyDialogue, setShowDependencyDialogue] = useState(false);
|
const [showDependencyDialogue, setShowDependencyDialogue] = useState(false);
|
||||||
const canAddParentDependency =
|
const canAddParentDependency =
|
||||||
Boolean(feature.project) &&
|
Boolean(feature.project) &&
|
||||||
@ -103,55 +116,54 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={canAddParentDependency}
|
condition={canAddParentDependency}
|
||||||
show={
|
show={
|
||||||
<FlexRow>
|
<StyledMetaDataItem>
|
||||||
<StyledDetail>
|
<StyledMetaDataItemLabel>
|
||||||
<StyledLabel>Dependency:</StyledLabel>
|
Dependency:
|
||||||
<PermissionButton
|
</StyledMetaDataItemLabel>
|
||||||
size='small'
|
<StyledPermissionButton
|
||||||
permission={UPDATE_FEATURE_DEPENDENCY}
|
size='small'
|
||||||
projectId={feature.project}
|
permission={UPDATE_FEATURE_DEPENDENCY}
|
||||||
variant='text'
|
projectId={feature.project}
|
||||||
onClick={() => {
|
variant='text'
|
||||||
setShowDependencyDialogue(true);
|
onClick={() => {
|
||||||
}}
|
setShowDependencyDialogue(true);
|
||||||
sx={(theme) => ({
|
}}
|
||||||
marginBottom: theme.spacing(0.4),
|
>
|
||||||
})}
|
Add parent feature
|
||||||
>
|
</StyledPermissionButton>
|
||||||
Add parent feature
|
</StyledMetaDataItem>
|
||||||
</PermissionButton>
|
|
||||||
</StyledDetail>
|
|
||||||
</FlexRow>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasParentDependency}
|
condition={hasParentDependency}
|
||||||
show={
|
show={
|
||||||
<FlexRow>
|
<StyledMetaDataItem>
|
||||||
<StyledDetail>
|
<StyledMetaDataItemLabel>
|
||||||
<StyledLabel>Dependency:</StyledLabel>
|
Dependency:
|
||||||
|
</StyledMetaDataItemLabel>
|
||||||
|
<StyledMetaDataItemValue>
|
||||||
<StyledLink
|
<StyledLink
|
||||||
to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`}
|
to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`}
|
||||||
>
|
>
|
||||||
{feature.dependencies[0]?.feature}
|
{feature.dependencies[0]?.feature}
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
</StyledDetail>
|
<ConditionallyRender
|
||||||
<ConditionallyRender
|
condition={checkAccess(
|
||||||
condition={checkAccess(
|
UPDATE_FEATURE_DEPENDENCY,
|
||||||
UPDATE_FEATURE_DEPENDENCY,
|
environment,
|
||||||
environment,
|
)}
|
||||||
)}
|
show={
|
||||||
show={
|
<DependencyActions
|
||||||
<DependencyActions
|
feature={feature.name}
|
||||||
feature={feature.name}
|
onEdit={() =>
|
||||||
onEdit={() =>
|
setShowDependencyDialogue(true)
|
||||||
setShowDependencyDialogue(true)
|
}
|
||||||
}
|
onDelete={deleteDependency}
|
||||||
onDelete={deleteDependency}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</StyledMetaDataItemValue>
|
||||||
</FlexRow>
|
</StyledMetaDataItem>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -159,12 +171,12 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
|||||||
hasParentDependency && !feature.dependencies[0]?.enabled
|
hasParentDependency && !feature.dependencies[0]?.enabled
|
||||||
}
|
}
|
||||||
show={
|
show={
|
||||||
<FlexRow>
|
<StyledMetaDataItem>
|
||||||
<StyledDetail>
|
<StyledMetaDataItemLabel>
|
||||||
<StyledLabel>Dependency value:</StyledLabel>
|
Dependency value:
|
||||||
<span>disabled</span>
|
</StyledMetaDataItemLabel>
|
||||||
</StyledDetail>
|
<span>disabled</span>
|
||||||
</FlexRow>
|
</StyledMetaDataItem>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -173,30 +185,28 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
|||||||
Boolean(feature.dependencies[0]?.variants?.length)
|
Boolean(feature.dependencies[0]?.variants?.length)
|
||||||
}
|
}
|
||||||
show={
|
show={
|
||||||
<FlexRow>
|
<StyledMetaDataItem>
|
||||||
<StyledDetail>
|
<StyledMetaDataItemLabel>
|
||||||
<StyledLabel>Dependency value:</StyledLabel>
|
Dependency value:
|
||||||
<VariantsTooltip
|
</StyledMetaDataItemLabel>
|
||||||
variants={
|
<VariantsTooltip
|
||||||
feature.dependencies[0]?.variants || []
|
variants={feature.dependencies[0]?.variants || []}
|
||||||
}
|
/>
|
||||||
/>
|
</StyledMetaDataItem>
|
||||||
</StyledDetail>
|
|
||||||
</FlexRow>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasChildren}
|
condition={hasChildren}
|
||||||
show={
|
show={
|
||||||
<FlexRow>
|
<StyledMetaDataItem>
|
||||||
<StyledDetail>
|
<StyledMetaDataItemLabel>
|
||||||
<StyledLabel>Children:</StyledLabel>
|
Children:
|
||||||
<ChildrenTooltip
|
</StyledMetaDataItemLabel>
|
||||||
childFeatures={feature.children}
|
<ChildrenTooltip
|
||||||
project={feature.project}
|
childFeatures={feature.children}
|
||||||
/>
|
project={feature.project}
|
||||||
</StyledDetail>
|
/>
|
||||||
</FlexRow>
|
</StyledMetaDataItem>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -1,262 +1,201 @@
|
|||||||
import { Box, capitalize, styled } from '@mui/material';
|
import { capitalize, styled } from '@mui/material';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
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 { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
|
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
|
||||||
import { StyledDetail } from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow';
|
|
||||||
import { formatDateYMD } from 'utils/formatDate';
|
import { formatDateYMD } from 'utils/formatDate';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { FeatureEnvironmentSeen } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
|
||||||
import { DependencyRow } from './DependencyRow';
|
import { DependencyRow } from './DependencyRow';
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
import { useShowDependentFeatures } from './useShowDependentFeatures';
|
import { useShowDependentFeatures } from './useShowDependentFeatures';
|
||||||
import type { ILastSeenEnvironments } from 'interfaces/featureToggle';
|
|
||||||
import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle';
|
import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle';
|
||||||
import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue';
|
import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue';
|
||||||
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
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,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
maxWidth: '350px',
|
gap: theme.spacing(2),
|
||||||
minWidth: '350px',
|
width: '350px',
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
[theme.breakpoints.down(1000)]: {
|
[theme.breakpoints.down(1000)]: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: 'none',
|
|
||||||
minWidth: 'auto',
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledPaddingContainerTop = styled('div')({
|
const StyledMetaDataHeader = styled('div')(({ theme }) => ({
|
||||||
padding: '1.5rem 1.5rem 0 1.5rem',
|
|
||||||
});
|
|
||||||
|
|
||||||
const StyledMetaDataHeader = styled('div')({
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
});
|
gap: theme.spacing(2),
|
||||||
|
'& > svg': {
|
||||||
const StyledHeader = styled('h2')(({ theme }) => ({
|
height: theme.spacing(5),
|
||||||
fontSize: theme.fontSizes.mainHeader,
|
width: theme.spacing(5),
|
||||||
fontWeight: 'normal',
|
padding: theme.spacing(0.5),
|
||||||
margin: 0,
|
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 }) => ({
|
const StyledBody = styled('div')({
|
||||||
margin: theme.spacing(2, 0),
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StyledMetaDataItem = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
minHeight: theme.spacing(4.25),
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const BodyItemWithIcon = styled('div')(({ theme }) => ({}));
|
export const StyledMetaDataItemLabel = styled('span')(({ 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,
|
color: theme.palette.text.secondary,
|
||||||
marginRight: theme.spacing(1),
|
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 FeatureOverviewMetaData = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const { feature, refetchFeature } = useFeature(projectId, 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 { locationSettings } = useLocationSettings();
|
||||||
const showDependentFeatures = useShowDependentFeatures(feature.project);
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const lastSeenEnvironments: ILastSeenEnvironments[] =
|
const [archiveDialogOpen, setArchiveDialogOpen] = useState(false);
|
||||||
feature.environments?.map((env) => ({
|
const [markCompletedDialogueOpen, setMarkCompletedDialogueOpen] =
|
||||||
name: env.name,
|
useState(false);
|
||||||
lastSeenAt: env.lastSeenAt,
|
|
||||||
enabled: env.enabled,
|
|
||||||
yes: env.yes,
|
|
||||||
no: env.no,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const IconComponent = getFeatureTypeIcons(type);
|
const { project, description, type } = feature;
|
||||||
|
|
||||||
|
const showDependentFeatures = useShowDependentFeatures(project);
|
||||||
|
|
||||||
|
const FlagTypeIcon = getFeatureTypeIcons(type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<>
|
||||||
<StyledPaddingContainerTop>
|
<StyledMetaDataContainer>
|
||||||
<StyledMetaDataHeader data-loading>
|
<StyledMetaDataHeader data-loading>
|
||||||
<IconComponent
|
<FlagTypeIcon />
|
||||||
sx={(theme) => ({
|
<h2>{capitalize(type || '')} flag</h2>
|
||||||
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>
|
</StyledMetaDataHeader>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(description)}
|
||||||
|
show={
|
||||||
|
<StyledMetaDataItem data-loading>
|
||||||
|
<StyledMetaDataItemText>
|
||||||
|
{description}
|
||||||
|
</StyledMetaDataItemText>
|
||||||
|
</StyledMetaDataItem>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<StyledBody>
|
<StyledBody>
|
||||||
<SpacedBodyItem data-loading>
|
<StyledMetaDataItem>
|
||||||
<StyledLabel>Project:</StyledLabel>
|
<StyledMetaDataItemLabel>
|
||||||
<Box sx={{ wordBreak: 'break-all' }}>{project}</Box>
|
Project:
|
||||||
</SpacedBodyItem>
|
</StyledMetaDataItemLabel>
|
||||||
|
<StyledMetaDataItemText data-loading>
|
||||||
|
{project}
|
||||||
|
</StyledMetaDataItemText>
|
||||||
|
</StyledMetaDataItem>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(feature.lifecycle)}
|
condition={Boolean(feature.lifecycle)}
|
||||||
show={
|
show={
|
||||||
<SpacedBodyItem data-loading>
|
<StyledMetaDataItem data-loading>
|
||||||
<StyledLabel>Lifecycle:</StyledLabel>
|
<StyledMetaDataItemLabel>
|
||||||
|
Lifecycle:
|
||||||
|
</StyledMetaDataItemLabel>
|
||||||
<FeatureLifecycle
|
<FeatureLifecycle
|
||||||
feature={feature}
|
feature={feature}
|
||||||
onArchive={() => setShowDelDialog(true)}
|
onArchive={() => setArchiveDialogOpen(true)}
|
||||||
onComplete={() =>
|
onComplete={() =>
|
||||||
setShowMarkCompletedDialogue(true)
|
setMarkCompletedDialogueOpen(true)
|
||||||
}
|
}
|
||||||
onUncomplete={refetchFeature}
|
onUncomplete={refetchFeature}
|
||||||
/>
|
/>
|
||||||
</SpacedBodyItem>
|
</StyledMetaDataItem>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<StyledMetaDataItem>
|
||||||
<ConditionallyRender
|
<StyledMetaDataItemLabel>
|
||||||
condition={Boolean(description)}
|
Created at:
|
||||||
show={
|
</StyledMetaDataItemLabel>
|
||||||
<BodyItemWithIcon data-loading sx={{ pt: 1 }}>
|
<StyledMetaDataItemText data-loading>
|
||||||
<StyledLabel>Description:</StyledLabel>
|
{formatDateYMD(
|
||||||
<StyledDescriptionContainer>
|
parseISO(feature.createdAt),
|
||||||
<StyledDescription>
|
locationSettings.locale,
|
||||||
{description}
|
)}
|
||||||
</StyledDescription>
|
</StyledMetaDataItemText>
|
||||||
<PermissionIconButton
|
</StyledMetaDataItem>
|
||||||
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
|
<ConditionallyRender
|
||||||
condition={Boolean(feature.createdBy)}
|
condition={Boolean(feature.createdBy)}
|
||||||
show={() => (
|
show={() => (
|
||||||
<BodyItemWithIcon>
|
<StyledMetaDataItem>
|
||||||
<StyledDetailsContainer>
|
<StyledMetaDataItemLabel>
|
||||||
<StyledDetail>
|
Created by:
|
||||||
<StyledLabel>Created by:</StyledLabel>
|
</StyledMetaDataItemLabel>
|
||||||
<span>{feature.createdBy?.name}</span>
|
<StyledMetaDataItemValue>
|
||||||
</StyledDetail>
|
<StyledMetaDataItemText data-loading>
|
||||||
|
{feature.createdBy?.name}
|
||||||
|
</StyledMetaDataItemText>
|
||||||
<StyledUserAvatar
|
<StyledUserAvatar
|
||||||
user={feature.createdBy}
|
user={feature.createdBy}
|
||||||
/>
|
/>
|
||||||
</StyledDetailsContainer>
|
</StyledMetaDataItemValue>
|
||||||
</BodyItemWithIcon>
|
</StyledMetaDataItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={showDependentFeatures}
|
condition={showDependentFeatures}
|
||||||
show={<DependencyRow feature={feature} />}
|
show={<DependencyRow feature={feature} />}
|
||||||
/>
|
/>
|
||||||
|
<TagRow feature={feature} />
|
||||||
</StyledBody>
|
</StyledBody>
|
||||||
</StyledPaddingContainerTop>
|
</StyledMetaDataContainer>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={feature.children.length > 0}
|
condition={feature.children.length > 0}
|
||||||
show={
|
show={
|
||||||
<FeatureArchiveNotAllowedDialog
|
<FeatureArchiveNotAllowedDialog
|
||||||
features={feature.children}
|
features={feature.children}
|
||||||
project={projectId}
|
project={projectId}
|
||||||
isOpen={showDelDialog}
|
isOpen={archiveDialogOpen}
|
||||||
onClose={() => setShowDelDialog(false)}
|
onClose={() => setArchiveDialogOpen(false)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<FeatureArchiveDialog
|
<FeatureArchiveDialog
|
||||||
isOpen={showDelDialog}
|
isOpen={archiveDialogOpen}
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
navigate(`/projects/${projectId}`);
|
navigate(`/projects/${projectId}`);
|
||||||
}}
|
}}
|
||||||
onClose={() => setShowDelDialog(false)}
|
onClose={() => setArchiveDialogOpen(false)}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
featureIds={[featureId]}
|
featureIds={[featureId]}
|
||||||
/>
|
/>
|
||||||
@ -266,15 +205,15 @@ const FeatureOverviewMetaData = () => {
|
|||||||
condition={Boolean(feature.project)}
|
condition={Boolean(feature.project)}
|
||||||
show={
|
show={
|
||||||
<MarkCompletedDialogue
|
<MarkCompletedDialogue
|
||||||
isOpen={showMarkCompletedDialogue}
|
isOpen={markCompletedDialogueOpen}
|
||||||
setIsOpen={setShowMarkCompletedDialogue}
|
setIsOpen={setMarkCompletedDialogueOpen}
|
||||||
projectId={feature.project}
|
projectId={feature.project}
|
||||||
featureId={feature.name}
|
featureId={feature.name}
|
||||||
onComplete={refetchFeature}
|
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 { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches';
|
import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches';
|
||||||
import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags';
|
|
||||||
import { Sticky } from 'component/common/Sticky/Sticky';
|
import { Sticky } from 'component/common/Sticky/Sticky';
|
||||||
|
|
||||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||||
@ -75,15 +74,6 @@ export const FeatureOverviewSidePanel = ({
|
|||||||
hiddenEnvironments={hiddenEnvironments}
|
hiddenEnvironments={hiddenEnvironments}
|
||||||
setHiddenEnvironments={setHiddenEnvironments}
|
setHiddenEnvironments={setHiddenEnvironments}
|
||||||
/>
|
/>
|
||||||
<Divider />
|
|
||||||
<FeatureOverviewSidePanelTags
|
|
||||||
header={
|
|
||||||
<StyledHeader data-loading>
|
|
||||||
Tags for this feature flag
|
|
||||||
</StyledHeader>
|
|
||||||
}
|
|
||||||
feature={feature}
|
|
||||||
/>
|
|
||||||
</StyledContainer>
|
</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)),
|
tagsToOptions(tags.filter((tag) => tag.type === tagType.name)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [JSON.stringify(tags), tagType]);
|
}, [JSON.stringify(tags), tagType, open]);
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
Loading…
Reference in New Issue
Block a user