1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00

feat: edit and delete dependencies menu (#4863)

This commit is contained in:
Mateusz Kwasniewski 2023-09-29 10:03:17 +02:00 committed by GitHub
parent eff47d790a
commit 011aea226c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 212 additions and 17 deletions

View File

@ -1,10 +1,11 @@
import React, { useState } from 'react'; import React, { FC, useState } from 'react';
import { Box, styled, Typography } from '@mui/material'; import { Box, styled, Typography } from '@mui/material';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi'; import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
import { useParentOptions } from 'hooks/api/getters/useParentOptions/useParentOptions'; import { useParentOptions } from 'hooks/api/getters/useParentOptions/useParentOptions';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IAddDependencyDialogueProps { interface IAddDependencyDialogueProps {
project: string; project: string;
@ -23,6 +24,31 @@ const REMOVE_DEPENDENCY_OPTION = {
label: 'none (remove dependency)', label: 'none (remove dependency)',
}; };
// Project can have 100s of parents. We want to read them only when the modal for dependencies opens.
const LazyOptions: FC<{
project: string;
featureId: string;
parent: string;
onSelect: (parent: string) => void;
}> = ({ project, featureId, parent, onSelect }) => {
const { parentOptions, loading } = useParentOptions(project, featureId);
const options = parentOptions
? [
REMOVE_DEPENDENCY_OPTION,
...parentOptions.map(parent => ({ key: parent, label: parent })),
]
: [REMOVE_DEPENDENCY_OPTION];
return (
<StyledSelect
fullWidth
options={options}
value={parent}
onChange={onSelect}
/>
);
};
export const AddDependencyDialogue = ({ export const AddDependencyDialogue = ({
project, project,
featureId, featureId,
@ -32,14 +58,8 @@ export const AddDependencyDialogue = ({
const [parent, setParent] = useState(REMOVE_DEPENDENCY_OPTION.key); const [parent, setParent] = useState(REMOVE_DEPENDENCY_OPTION.key);
const { addDependency, removeDependencies } = const { addDependency, removeDependencies } =
useDependentFeaturesApi(project); useDependentFeaturesApi(project);
const { parentOptions, loading } = useParentOptions(project, featureId);
const { refetchFeature } = useFeature(project, featureId); const { refetchFeature } = useFeature(project, featureId);
const options = parentOptions
? [
REMOVE_DEPENDENCY_OPTION,
...parentOptions.map(parent => ({ key: parent, label: parent })),
]
: [REMOVE_DEPENDENCY_OPTION];
return ( return (
<Dialogue <Dialogue
@ -59,7 +79,6 @@ export const AddDependencyDialogue = ({
parent === REMOVE_DEPENDENCY_OPTION.key ? 'Remove' : 'Add' parent === REMOVE_DEPENDENCY_OPTION.key ? 'Remove' : 'Add'
} }
secondaryButtonText="Cancel" secondaryButtonText="Cancel"
disabledPrimaryButton={loading}
> >
<Box> <Box>
You feature will be evaluated only when the selected parent You feature will be evaluated only when the selected parent
@ -67,11 +86,16 @@ export const AddDependencyDialogue = ({
<br /> <br />
<br /> <br />
<Typography>What feature do you want to depend on?</Typography> <Typography>What feature do you want to depend on?</Typography>
<StyledSelect <ConditionallyRender
fullWidth condition={showDependencyDialogue}
options={options} show={
value={parent} <LazyOptions
onChange={setParent} project={project}
featureId={featureId}
parent={parent}
onSelect={setParent}
/>
}
/> />
</Box> </Box>
</Dialogue> </Dialogue>

View File

@ -0,0 +1,114 @@
import React, { FC, useState } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
IconButton,
ListItemIcon,
ListItemText,
MenuItem,
MenuList,
Popover,
styled,
Tooltip,
Typography,
} from '@mui/material';
import { Delete, Edit, MoreVert } from '@mui/icons-material';
const StyledPopover = styled(Popover)(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(1, 1.5),
}));
export const DependencyActions: 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 (
<span>
<Tooltip title="Dependency actions" arrow describeChild>
<IconButton
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}>
<ConditionallyRender
condition={true}
show={
<MenuItem
onClick={() => {
onEdit();
closeActions();
}}
>
<ListItemIcon>
<Edit />
</ListItemIcon>
<ListItemText>
<Typography variant="body2">
Edit
</Typography>
</ListItemText>
</MenuItem>
}
/>
<ConditionallyRender
condition={true}
show={
<MenuItem
onClick={() => {
onDelete();
closeActions();
}}
>
<ListItemIcon>
<Delete />
</ListItemIcon>
<ListItemText>
<Typography variant="body2">
Delete
</Typography>
</ListItemText>
</MenuItem>
}
/>
</MenuList>
</StyledPopover>
</span>
);
};

View File

@ -3,12 +3,16 @@ import { Button } from '@mui/material';
import { Add } from '@mui/icons-material'; import { Add } from '@mui/icons-material';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue'; import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
import { useUiFlag } from 'hooks/useUiFlag';
import { IFeatureToggle } from 'interfaces/featureToggle'; import { IFeatureToggle } from 'interfaces/featureToggle';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { FlexRow, StyledDetail, StyledLabel, StyledLink } from './StyledRow'; import { FlexRow, StyledDetail, StyledLabel, StyledLink } from './StyledRow';
import { DependencyActions } from './DependencyActions';
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => { export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
const { removeDependencies } = useDependentFeaturesApi(feature.project);
const { refetchFeature } = useFeature(feature.project, feature.name);
const [showDependencyDialogue, setShowDependencyDialogue] = useState(false); const [showDependencyDialogue, setShowDependencyDialogue] = useState(false);
const canAddParentDependency = const canAddParentDependency =
Boolean(feature.project) && Boolean(feature.project) &&
@ -50,6 +54,14 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
{feature.dependencies[0]?.feature} {feature.dependencies[0]?.feature}
</StyledLink> </StyledLink>
</StyledDetail> </StyledDetail>
<DependencyActions
feature={feature.name}
onEdit={() => setShowDependencyDialogue(true)}
onDelete={async () => {
await removeDependencies(feature.name);
await refetchFeature();
}}
/>
</FlexRow> </FlexRow>
} }
/> />

View File

@ -1,4 +1,5 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'utils/testRenderer'; import { render } from 'utils/testRenderer';
import { FeatureOverviewSidePanelDetails } from './FeatureOverviewSidePanelDetails'; import { FeatureOverviewSidePanelDetails } from './FeatureOverviewSidePanelDetails';
import { IDependency, IFeatureToggle } from 'interfaces/featureToggle'; import { IDependency, IFeatureToggle } from 'interfaces/featureToggle';
@ -12,6 +13,12 @@ const setupApi = () => {
dependentFeatures: true, dependentFeatures: true,
}, },
}); });
testServerRoute(server, '/api/admin/projects/default/features/feature', {});
testServerRoute(
server,
'/api/admin/projects/default/features/feature/parents',
{}
);
}; };
beforeEach(() => { beforeEach(() => {
@ -80,7 +87,7 @@ test('show children', async () => {
await screen.findByText('2 features'); await screen.findByText('2 features');
}); });
test('show parent dependencies', async () => { test('delete dependency', async () => {
render( render(
<FeatureOverviewSidePanelDetails <FeatureOverviewSidePanelDetails
feature={ feature={
@ -97,4 +104,41 @@ test('show parent dependencies', async () => {
await screen.findByText('Dependency:'); await screen.findByText('Dependency:');
await screen.findByText('some_parent'); await screen.findByText('some_parent');
const actionsButton = screen.getByRole('button', {
name: /Dependency actions/i,
});
userEvent.click(actionsButton);
const deleteButton = await screen.findByText('Delete');
userEvent.click(deleteButton);
});
test('edit dependency', async () => {
render(
<FeatureOverviewSidePanelDetails
feature={
{
name: 'feature',
project: 'default',
dependencies: [{ feature: 'some_parent' }],
children: [] as string[],
} as IFeatureToggle
}
header={''}
/>
);
await screen.findByText('Dependency:');
await screen.findByText('some_parent');
const actionsButton = screen.getByRole('button', {
name: /Dependency actions/i,
});
userEvent.click(actionsButton);
const editButton = await screen.findByText('Edit');
userEvent.click(editButton);
await screen.findByText('Add parent feature dependency');
}); });

View File

@ -51,7 +51,7 @@ export const FeatureOverviewSidePanelDetails = ({
<FeatureEnvironmentSeen <FeatureEnvironmentSeen
featureLastSeen={feature.lastSeenAt} featureLastSeen={feature.lastSeenAt}
environments={feature.environments} environments={feature.environments}
sx={{ pt: 0 }} sx={{ p: 0 }}
/> />
)} )}
</FlexRow> </FlexRow>

View File

@ -6,6 +6,7 @@ export const FlexRow = styled('div')({
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center',
}); });
export const StyledDetail = styled('div')(({ theme }) => ({ export const StyledDetail = styled('div')(({ theme }) => ({