1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +01: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:
Nuno Góis 2022-11-22 09:14:57 +00:00 committed by GitHub
parent d5fbd0b743
commit ac16e7e3ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 173 additions and 7 deletions

View File

@ -11,8 +11,10 @@ import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import useTags from 'hooks/api/getters/useTags/useTags';
import FeatureOverviewTags from './FeatureOverviewTags/FeatureOverviewTags';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
const FeatureOverviewMetaData = () => {
const { uiConfig } = useUiConfig();
const { classes: styles } = useStyles();
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
@ -78,7 +80,10 @@ const FeatureOverviewMetaData = () => {
</div>
</div>
<ConditionallyRender
condition={tags.length > 0}
condition={
tags.length > 0 &&
!Boolean(uiConfig.flags.variantsPerEnvironment)
}
show={
<div className={styles.paddingContainerBottom}>
<FeatureOverviewTags projectId={projectId} />

View File

@ -1,15 +1,15 @@
import { styled } from '@mui/material';
import { 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';
const StyledContainer = styled('div')(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
backgroundColor: theme.palette.background.paper,
display: 'flex',
flexDirection: 'column',
padding: '1.5rem',
maxWidth: '350px',
minWidth: '350px',
marginRight: '1rem',
@ -56,6 +56,15 @@ export const FeatureOverviewSidePanel = () => {
}
feature={feature}
/>
<Divider />
<FeatureOverviewSidePanelTags
header={
<StyledHeader data-loading>
Tags for this feature toggle
</StyledHeader>
}
feature={feature}
/>
</StyledContainer>
);
};

View File

@ -6,6 +6,10 @@ import { Link, styled } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
const StyledContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(3),
}));
const StyledSwitchLabel = styled('div')(() => ({
display: 'flex',
flexDirection: 'column',
}));
@ -38,7 +42,7 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
const [environmentName, setEnvironmentName] = useState('');
return (
<>
<StyledContainer>
{header}
{feature.environments.map(environment => {
const strategiesLabel =
@ -72,13 +76,13 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
setShowInfoBox(true);
}}
>
<StyledContainer>
<StyledSwitchLabel>
<StyledLabel>{environment.name}</StyledLabel>
<StyledSubLabel>
{strategiesLabel}
{variantsLink}
</StyledSubLabel>
</StyledContainer>
</StyledSwitchLabel>
</FeatureOverviewSidePanelEnvironmentSwitch>
);
})}
@ -89,6 +93,6 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
featureId={feature.name}
environmentName={environmentName}
/>
</>
</StyledContainer>
);
};

View File

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