1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +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 { Dialogue } from 'component/common/Dialogue/Dialogue';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
import { useParentOptions } from 'hooks/api/getters/useParentOptions/useParentOptions';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IAddDependencyDialogueProps {
project: string;
@ -23,6 +24,31 @@ const REMOVE_DEPENDENCY_OPTION = {
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 = ({
project,
featureId,
@ -32,14 +58,8 @@ export const AddDependencyDialogue = ({
const [parent, setParent] = useState(REMOVE_DEPENDENCY_OPTION.key);
const { addDependency, removeDependencies } =
useDependentFeaturesApi(project);
const { parentOptions, loading } = useParentOptions(project, featureId);
const { refetchFeature } = useFeature(project, featureId);
const options = parentOptions
? [
REMOVE_DEPENDENCY_OPTION,
...parentOptions.map(parent => ({ key: parent, label: parent })),
]
: [REMOVE_DEPENDENCY_OPTION];
return (
<Dialogue
@ -59,7 +79,6 @@ export const AddDependencyDialogue = ({
parent === REMOVE_DEPENDENCY_OPTION.key ? 'Remove' : 'Add'
}
secondaryButtonText="Cancel"
disabledPrimaryButton={loading}
>
<Box>
You feature will be evaluated only when the selected parent
@ -67,11 +86,16 @@ export const AddDependencyDialogue = ({
<br />
<br />
<Typography>What feature do you want to depend on?</Typography>
<StyledSelect
fullWidth
options={options}
value={parent}
onChange={setParent}
<ConditionallyRender
condition={showDependencyDialogue}
show={
<LazyOptions
project={project}
featureId={featureId}
parent={parent}
onSelect={setParent}
/>
}
/>
</Box>
</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 { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
import { useUiFlag } from 'hooks/useUiFlag';
import { IFeatureToggle } from 'interfaces/featureToggle';
import { FC, useState } from 'react';
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 }) => {
const { removeDependencies } = useDependentFeaturesApi(feature.project);
const { refetchFeature } = useFeature(feature.project, feature.name);
const [showDependencyDialogue, setShowDependencyDialogue] = useState(false);
const canAddParentDependency =
Boolean(feature.project) &&
@ -50,6 +54,14 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
{feature.dependencies[0]?.feature}
</StyledLink>
</StyledDetail>
<DependencyActions
feature={feature.name}
onEdit={() => setShowDependencyDialogue(true)}
onDelete={async () => {
await removeDependencies(feature.name);
await refetchFeature();
}}
/>
</FlexRow>
}
/>

View File

@ -1,4 +1,5 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'utils/testRenderer';
import { FeatureOverviewSidePanelDetails } from './FeatureOverviewSidePanelDetails';
import { IDependency, IFeatureToggle } from 'interfaces/featureToggle';
@ -12,6 +13,12 @@ const setupApi = () => {
dependentFeatures: true,
},
});
testServerRoute(server, '/api/admin/projects/default/features/feature', {});
testServerRoute(
server,
'/api/admin/projects/default/features/feature/parents',
{}
);
};
beforeEach(() => {
@ -80,7 +87,7 @@ test('show children', async () => {
await screen.findByText('2 features');
});
test('show parent dependencies', async () => {
test('delete dependency', async () => {
render(
<FeatureOverviewSidePanelDetails
feature={
@ -97,4 +104,41 @@ test('show parent dependencies', async () => {
await screen.findByText('Dependency:');
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
featureLastSeen={feature.lastSeenAt}
environments={feature.environments}
sx={{ pt: 0 }}
sx={{ p: 0 }}
/>
)}
</FlexRow>

View File

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