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:
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 { 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>
|
||||||
|
@ -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 { 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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -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');
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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 }) => ({
|
||||||
|
Loading…
Reference in New Issue
Block a user