1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00
unleash.unleash/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx
2025-05-07 15:58:51 +02:00

368 lines
14 KiB
TypeScript

import { type FC, useState } from 'react';
import {
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
styled,
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog';
import { formatDateYMD } from 'utils/formatDate';
import { parseISO } from 'date-fns';
import { DependencyRow } from './DependencyRow';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { useShowDependentFeatures } from './useShowDependentFeatures';
import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle';
import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue';
import { TagRow } from './TagRow';
import { capitalizeFirst } from 'utils/capitalizeFirst';
import { Collaborators } from './Collaborators';
import { EnvironmentVisibilityMenu } from './EnvironmentVisibilityMenu/EnvironmentVisibilityMenu';
import { Truncator } from 'component/common/Truncator/Truncator';
import type {
FeatureLink,
IFeatureToggle,
} from '../../../../../interfaces/featureToggle';
import AddIcon from '@mui/icons-material/Add';
import { useUiFlag } from 'hooks/useUiFlag';
import { Badge } from 'component/common/Badge/Badge';
import LinkIcon from '@mui/icons-material/Link';
import { UPDATE_FEATURE } from '../../../../providers/AccessProvider/permissions';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { AddLinkDialogue } from './AddLinkDialogue';
import { useFeatureLinkApi } from 'hooks/api/actions/useFeatureLinkApi/useFeatureLinkApi';
import useToast from 'hooks/useToast';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { formatUnknownError } from 'utils/formatUnknownError';
import { ExtraActions } from './ExtraActions';
const StyledMetaDataContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(3),
borderRadius: theme.shape.borderRadiusLarge,
backgroundColor: theme.palette.background.paper,
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
width: '350px',
border: `1px solid ${theme.palette.divider}`,
[theme.breakpoints.down('md')]: {
width: '100%',
},
marginBottom: theme.spacing(2),
}));
const StyledTitle = styled('h2')(({ theme }) => ({
fontSize: theme.typography.body1.fontSize,
fontWeight: theme.typography.fontWeightBold,
marginBottom: theme.spacing(0.5),
}));
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.5),
fontSize: theme.fontSizes.smallBody,
}));
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),
}));
export const StyledListItemIcon = styled(ListItemIcon)(({ theme }) => ({
minWidth: theme.spacing(5),
}));
type FeatureOverviewMetaDataProps = {
hiddenEnvironments?: string[];
onEnvironmentVisibilityChange?: (environment: string) => void;
feature: IFeatureToggle;
onChange: () => void;
};
interface FeatureLinksProps {
links: FeatureLink[];
project: string;
feature: string;
}
const FeatureLinks: FC<FeatureLinksProps> = ({ links, project, feature }) => {
const [showAddLinkDialogue, setShowAddLinkDialogue] = useState(false);
const { deleteLink, loading } = useFeatureLinkApi(project, feature);
const { setToastData, setToastApiError } = useToast();
const { refetchFeature } = useFeature(project, feature);
const addLinkButton = (
<PermissionButton
size='small'
startIcon={<AddIcon />}
permission={UPDATE_FEATURE}
projectId={project}
variant='text'
onClick={() => setShowAddLinkDialogue(true)}
>
Add link
</PermissionButton>
);
const renderLinkItems = () => (
<List>
{links.map((link) => (
<ListItem
secondaryAction={
<ExtraActions
capabilityId='link'
feature={feature}
onEdit={() => {}}
onDelete={async () => {
try {
await deleteLink(link.id);
setToastData({
text: 'Link removed',
type: 'success',
});
refetchFeature();
} catch (error) {
setToastApiError(formatUnknownError(error));
}
}}
/>
}
key={link.id}
disablePadding
dense
>
<ListItemButton
component='a'
href={link.url}
target='_blank'
rel='noopener noreferrer'
disableGutters
>
<StyledListItemIcon>
<LinkIcon color='primary' />
</StyledListItemIcon>
<ListItemText
primary={link.title}
secondary={link.url}
secondaryTypographyProps={{
sx: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'block',
},
}}
/>
</ListItemButton>
</ListItem>
))}
</List>
);
const emptyStateContent = (
<>
<StyledTitle>
You can now add links{' '}
<Badge color='success' sx={{ ml: 1 }}>
New
</Badge>
</StyledTitle>
<StyledMetaDataItem>
Gather relevant links for external resources such as issue
trackers, code repositories or analytics tooling
</StyledMetaDataItem>
<div>{addLinkButton}</div>
</>
);
const linksContent = (
<>
<StyledTitle>Resources</StyledTitle>
{renderLinkItems()}
<div>{addLinkButton}</div>
</>
);
return (
<>
<StyledMetaDataContainer>
{links.length === 0 ? emptyStateContent : linksContent}
</StyledMetaDataContainer>
<AddLinkDialogue
project={project}
featureId={feature}
showAddLinkDialogue={showAddLinkDialogue}
onClose={() => setShowAddLinkDialogue(false)}
/>
</>
);
};
const FeatureOverviewMetaData: FC<FeatureOverviewMetaDataProps> = ({
hiddenEnvironments,
onEnvironmentVisibilityChange,
feature,
onChange,
}) => {
const { locationSettings } = useLocationSettings();
const navigate = useNavigate();
const [archiveDialogOpen, setArchiveDialogOpen] = useState(false);
const [markCompletedDialogueOpen, setMarkCompletedDialogueOpen] =
useState(false);
const { project, description, type } = feature;
const showDependentFeatures = useShowDependentFeatures(project);
const featureLinksEnabled = useUiFlag('featureLinks');
return (
<>
{featureLinksEnabled ? (
<FeatureLinks
links={feature.links || []}
project={feature.project}
feature={feature.name}
/>
) : null}
<StyledMetaDataContainer>
<div>
<StyledTitle>Flag details</StyledTitle>
{description ? (
<StyledMetaDataItem data-loading>
<StyledMetaDataItemText>
<Truncator arrow lines={5} title={description}>
{description}
</Truncator>
</StyledMetaDataItemText>
</StyledMetaDataItem>
) : null}
</div>
<StyledBody>
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Flag type:
</StyledMetaDataItemLabel>
<StyledMetaDataItemText data-loading>
{capitalizeFirst(type || ' ')} flag
</StyledMetaDataItemText>
</StyledMetaDataItem>
{feature.lifecycle ? (
<StyledMetaDataItem data-loading>
<StyledMetaDataItemLabel>
Lifecycle:
</StyledMetaDataItemLabel>
<FeatureLifecycle
feature={feature}
onArchive={() => setArchiveDialogOpen(true)}
onComplete={() =>
setMarkCompletedDialogueOpen(true)
}
onUncomplete={onChange}
/>
</StyledMetaDataItem>
) : null}
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Created:
</StyledMetaDataItemLabel>
<StyledMetaDataItemText data-loading>
{formatDateYMD(
parseISO(feature.createdAt),
locationSettings.locale,
)}
</StyledMetaDataItemText>
</StyledMetaDataItem>
{feature.createdBy ? (
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Created by:
</StyledMetaDataItemLabel>
<StyledMetaDataItemValue>
<StyledMetaDataItemText data-loading>
{feature.createdBy?.name}
</StyledMetaDataItemText>
</StyledMetaDataItemValue>
</StyledMetaDataItem>
) : null}
{feature.collaborators?.users &&
feature.collaborators?.users.length > 0 ? (
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Collaborators:
</StyledMetaDataItemLabel>
<StyledMetaDataItemValue>
<Collaborators
collaborators={feature.collaborators?.users}
/>
</StyledMetaDataItemValue>
</StyledMetaDataItem>
) : null}
{showDependentFeatures ? (
<DependencyRow feature={feature} />
) : null}
<TagRow feature={feature} />
{onEnvironmentVisibilityChange ? (
<EnvironmentVisibilityMenu
environments={feature.environments || []}
hiddenEnvironments={hiddenEnvironments || []}
onChange={onEnvironmentVisibilityChange}
/>
) : null}
</StyledBody>
</StyledMetaDataContainer>
{feature.children.length > 0 ? (
<FeatureArchiveNotAllowedDialog
features={feature.children}
project={feature.project}
isOpen={archiveDialogOpen}
onClose={() => setArchiveDialogOpen(false)}
/>
) : (
<FeatureArchiveDialog
isOpen={archiveDialogOpen}
onConfirm={() => {
navigate(`/projects/${feature.project}`);
}}
onClose={() => setArchiveDialogOpen(false)}
projectId={feature.project}
featureIds={[feature.name]}
/>
)}
{feature.project ? (
<MarkCompletedDialogue
isOpen={markCompletedDialogueOpen}
setIsOpen={setMarkCompletedDialogueOpen}
projectId={feature.project}
featureId={feature.name}
onComplete={onChange}
/>
) : null}
</>
);
};
export default FeatureOverviewMetaData;