mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-13 11:17:26 +02:00
feat: add tags to the new feature overview sidepanel (#2488)
https://linear.app/unleash/issue/2-423/update-feature-toggle-overview-sidepanel
This commit is contained in:
parent
d5fbd0b743
commit
ac16e7e3ba
@ -11,8 +11,10 @@ import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
|||||||
import useTags from 'hooks/api/getters/useTags/useTags';
|
import useTags from 'hooks/api/getters/useTags/useTags';
|
||||||
import FeatureOverviewTags from './FeatureOverviewTags/FeatureOverviewTags';
|
import FeatureOverviewTags from './FeatureOverviewTags/FeatureOverviewTags';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const FeatureOverviewMetaData = () => {
|
const FeatureOverviewMetaData = () => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
@ -78,7 +80,10 @@ const FeatureOverviewMetaData = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={tags.length > 0}
|
condition={
|
||||||
|
tags.length > 0 &&
|
||||||
|
!Boolean(uiConfig.flags.variantsPerEnvironment)
|
||||||
|
}
|
||||||
show={
|
show={
|
||||||
<div className={styles.paddingContainerBottom}>
|
<div className={styles.paddingContainerBottom}>
|
||||||
<FeatureOverviewTags projectId={projectId} />
|
<FeatureOverviewTags projectId={projectId} />
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { styled } from '@mui/material';
|
import { Divider, 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';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
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',
|
||||||
padding: '1.5rem',
|
|
||||||
maxWidth: '350px',
|
maxWidth: '350px',
|
||||||
minWidth: '350px',
|
minWidth: '350px',
|
||||||
marginRight: '1rem',
|
marginRight: '1rem',
|
||||||
@ -56,6 +56,15 @@ export const FeatureOverviewSidePanel = () => {
|
|||||||
}
|
}
|
||||||
feature={feature}
|
feature={feature}
|
||||||
/>
|
/>
|
||||||
|
<Divider />
|
||||||
|
<FeatureOverviewSidePanelTags
|
||||||
|
header={
|
||||||
|
<StyledHeader data-loading>
|
||||||
|
Tags for this feature toggle
|
||||||
|
</StyledHeader>
|
||||||
|
}
|
||||||
|
feature={feature}
|
||||||
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,10 @@ import { Link, styled } from '@mui/material';
|
|||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledSwitchLabel = styled('div')(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
}));
|
}));
|
||||||
@ -38,7 +42,7 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
|
|||||||
const [environmentName, setEnvironmentName] = useState('');
|
const [environmentName, setEnvironmentName] = useState('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<StyledContainer>
|
||||||
{header}
|
{header}
|
||||||
{feature.environments.map(environment => {
|
{feature.environments.map(environment => {
|
||||||
const strategiesLabel =
|
const strategiesLabel =
|
||||||
@ -72,13 +76,13 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
|
|||||||
setShowInfoBox(true);
|
setShowInfoBox(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledContainer>
|
<StyledSwitchLabel>
|
||||||
<StyledLabel>{environment.name}</StyledLabel>
|
<StyledLabel>{environment.name}</StyledLabel>
|
||||||
<StyledSubLabel>
|
<StyledSubLabel>
|
||||||
{strategiesLabel}
|
{strategiesLabel}
|
||||||
{variantsLink}
|
{variantsLink}
|
||||||
</StyledSubLabel>
|
</StyledSubLabel>
|
||||||
</StyledContainer>
|
</StyledSwitchLabel>
|
||||||
</FeatureOverviewSidePanelEnvironmentSwitch>
|
</FeatureOverviewSidePanelEnvironmentSwitch>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -89,6 +93,6 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
|
|||||||
featureId={feature.name}
|
featureId={feature.name}
|
||||||
environmentName={environmentName}
|
environmentName={environmentName}
|
||||||
/>
|
/>
|
||||||
</>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,148 @@
|
|||||||
|
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
|
import { useContext, useState } from 'react';
|
||||||
|
import { Button, Chip, Divider, styled } from '@mui/material';
|
||||||
|
import useTags from 'hooks/api/getters/useTags/useTags';
|
||||||
|
import { Add, Cancel } from '@mui/icons-material';
|
||||||
|
import AddTagDialog from 'component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog';
|
||||||
|
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import AccessContext from 'contexts/AccessContext';
|
||||||
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
import { 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';
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledTagContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledChip = styled(Chip)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
'.MuiChip-deleteIcon': {
|
||||||
|
color: theme.palette.neutral.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||||
|
margin: theme.spacing(3),
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)(({ theme }) => ({
|
||||||
|
maxWidth: theme.spacing(20),
|
||||||
|
alignSelf: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IFeatureOverviewSidePanelTagsProps {
|
||||||
|
feature: IFeatureToggle;
|
||||||
|
header: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureOverviewSidePanelTags = ({
|
||||||
|
feature,
|
||||||
|
header,
|
||||||
|
}: IFeatureOverviewSidePanelTagsProps) => {
|
||||||
|
const { tags, refetch } = useTags(feature.name);
|
||||||
|
const { deleteTagFromFeature } = useFeatureApi();
|
||||||
|
|
||||||
|
const [openTagDialog, setOpenTagDialog] = useState(false);
|
||||||
|
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||||
|
const [selectedTag, setSelectedTag] = useState<ITag>();
|
||||||
|
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
const canUpdateTags = hasAccess(UPDATE_FEATURE, feature.project);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!selectedTag) return;
|
||||||
|
try {
|
||||||
|
await deleteTagFromFeature(
|
||||||
|
feature.name,
|
||||||
|
selectedTag.type,
|
||||||
|
selectedTag.value
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Tag deleted',
|
||||||
|
text: 'Successfully deleted tag',
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
{header}
|
||||||
|
<StyledTagContainer>
|
||||||
|
{tags.map(tag => {
|
||||||
|
const tagLabel = `${tag.type}:${tag.value}`;
|
||||||
|
return (
|
||||||
|
<StyledChip
|
||||||
|
key={tagLabel}
|
||||||
|
label={tagLabel}
|
||||||
|
deleteIcon={<Cancel titleAccess="Remove" />}
|
||||||
|
onDelete={
|
||||||
|
canUpdateTags
|
||||||
|
? () => {
|
||||||
|
setShowDelDialog(true);
|
||||||
|
setSelectedTag(tag);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</StyledTagContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={canUpdateTags}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={tags.length > 0}
|
||||||
|
show={<StyledDivider />}
|
||||||
|
/>
|
||||||
|
<StyledButton
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<Add />}
|
||||||
|
onClick={() => setOpenTagDialog(true)}
|
||||||
|
>
|
||||||
|
Add new tag
|
||||||
|
</StyledButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AddTagDialog open={openTagDialog} setOpen={setOpenTagDialog} />
|
||||||
|
<Dialogue
|
||||||
|
open={showDelDialog}
|
||||||
|
primaryButtonText="Delete tag"
|
||||||
|
secondaryButtonText="Cancel"
|
||||||
|
onClose={() => {
|
||||||
|
setShowDelDialog(false);
|
||||||
|
setSelectedTag(undefined);
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setShowDelDialog(false);
|
||||||
|
handleDelete();
|
||||||
|
setSelectedTag(undefined);
|
||||||
|
}}
|
||||||
|
title="Delete tag?"
|
||||||
|
>
|
||||||
|
You are about to delete tag:{' '}
|
||||||
|
<strong>
|
||||||
|
{selectedTag?.type}:{selectedTag?.value}
|
||||||
|
</strong>
|
||||||
|
</Dialogue>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user