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:
parent
eff47d790a
commit
011aea226c
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
}
|
||||
/>
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -51,7 +51,7 @@ export const FeatureOverviewSidePanelDetails = ({
|
||||
<FeatureEnvironmentSeen
|
||||
featureLastSeen={feature.lastSeenAt}
|
||||
environments={feature.environments}
|
||||
sx={{ pt: 0 }}
|
||||
sx={{ p: 0 }}
|
||||
/>
|
||||
)}
|
||||
</FlexRow>
|
||||
|
@ -6,6 +6,7 @@ export const FlexRow = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const StyledDetail = styled('div')(({ theme }) => ({
|
||||
|
Loading…
Reference in New Issue
Block a user