mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-28 17:55:15 +02:00
feat: delete dependnecy button through change request (#4983)
This commit is contained in:
parent
0c069b1385
commit
2f84ac88e6
@ -9,11 +9,63 @@ import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
|||||||
import { ChildrenTooltip } from './ChildrenTooltip';
|
import { ChildrenTooltip } from './ChildrenTooltip';
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import { UPDATE_FEATURE_DEPENDENCY } from 'component/providers/AccessProvider/permissions';
|
import { UPDATE_FEATURE_DEPENDENCY } from 'component/providers/AccessProvider/permissions';
|
||||||
import { useCheckProjectPermissions } from 'hooks/useHasAccess';
|
import { useCheckProjectAccess } from 'hooks/useHasAccess';
|
||||||
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment';
|
||||||
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
|
||||||
|
const useDeleteDependency = (project: string, featureId: string) => {
|
||||||
|
const { addChange } = useChangeRequestApi();
|
||||||
|
const { refetch: refetchChangeRequests } =
|
||||||
|
usePendingChangeRequests(project);
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { refetchFeature } = useFeature(project, featureId);
|
||||||
|
const environment = useHighestPermissionChangeRequestEnvironment(project)();
|
||||||
|
const { isChangeRequestConfiguredInAnyEnv } =
|
||||||
|
useChangeRequestsEnabled(project);
|
||||||
|
const { removeDependencies } = useDependentFeaturesApi(project);
|
||||||
|
|
||||||
|
const handleAddChange = async () => {
|
||||||
|
if (!environment) {
|
||||||
|
console.error('No change request environment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await addChange(project, environment, [
|
||||||
|
{
|
||||||
|
action: 'deleteDependency',
|
||||||
|
feature: featureId,
|
||||||
|
payload: undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteDependency = async () => {
|
||||||
|
try {
|
||||||
|
if (isChangeRequestConfiguredInAnyEnv()) {
|
||||||
|
await handleAddChange();
|
||||||
|
setToastData({
|
||||||
|
text: `${featureId} dependency will be removed`,
|
||||||
|
type: 'success',
|
||||||
|
title: 'Change added to a draft',
|
||||||
|
});
|
||||||
|
await refetchChangeRequests();
|
||||||
|
} else {
|
||||||
|
await removeDependencies(featureId);
|
||||||
|
setToastData({ title: 'Dependency removed', type: 'success' });
|
||||||
|
await refetchFeature();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return deleteDependency;
|
||||||
|
};
|
||||||
|
|
||||||
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) &&
|
||||||
@ -22,7 +74,11 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
|||||||
const hasParentDependency =
|
const hasParentDependency =
|
||||||
Boolean(feature.project) && Boolean(feature.dependencies.length > 0);
|
Boolean(feature.project) && Boolean(feature.dependencies.length > 0);
|
||||||
const hasChildren = Boolean(feature.project) && feature.children.length > 0;
|
const hasChildren = Boolean(feature.project) && feature.children.length > 0;
|
||||||
const checkAccess = useCheckProjectPermissions(feature.project);
|
const environment = useHighestPermissionChangeRequestEnvironment(
|
||||||
|
feature.project,
|
||||||
|
)();
|
||||||
|
const checkAccess = useCheckProjectAccess(feature.project);
|
||||||
|
const deleteDependency = useDeleteDependency(feature.project, feature.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -59,17 +115,17 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
|||||||
</StyledLink>
|
</StyledLink>
|
||||||
</StyledDetail>
|
</StyledDetail>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={checkAccess(UPDATE_FEATURE_DEPENDENCY)}
|
condition={checkAccess(
|
||||||
|
UPDATE_FEATURE_DEPENDENCY,
|
||||||
|
environment,
|
||||||
|
)}
|
||||||
show={
|
show={
|
||||||
<DependencyActions
|
<DependencyActions
|
||||||
feature={feature.name}
|
feature={feature.name}
|
||||||
onEdit={() =>
|
onEdit={() =>
|
||||||
setShowDependencyDialogue(true)
|
setShowDependencyDialogue(true)
|
||||||
}
|
}
|
||||||
onDelete={async () => {
|
onDelete={deleteDependency}
|
||||||
await removeDependencies(feature.name);
|
|
||||||
await refetchFeature();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -4,6 +4,7 @@ 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';
|
||||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||||
|
import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer';
|
||||||
|
|
||||||
const server = testServerSetup();
|
const server = testServerSetup();
|
||||||
|
|
||||||
@ -12,6 +13,9 @@ const setupApi = () => {
|
|||||||
flags: {
|
flags: {
|
||||||
dependentFeatures: true,
|
dependentFeatures: true,
|
||||||
},
|
},
|
||||||
|
versionInfo: {
|
||||||
|
current: { oss: 'irrelevant', enterprise: 'some value' },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
testServerRoute(server, '/api/admin/projects/default/features/feature', {});
|
testServerRoute(server, '/api/admin/projects/default/features/feature', {});
|
||||||
testServerRoute(
|
testServerRoute(
|
||||||
@ -19,6 +23,40 @@ const setupApi = () => {
|
|||||||
'/api/admin/projects/default/features/feature/parents',
|
'/api/admin/projects/default/features/feature/parents',
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/projects/default/features/feature/dependencies',
|
||||||
|
{},
|
||||||
|
'delete',
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupChangeRequestApi = () => {
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/projects/default/change-requests/config',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
environment: 'development',
|
||||||
|
type: 'development',
|
||||||
|
requiredApprovals: null,
|
||||||
|
changeRequestEnabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'api/admin/projects/default/change-requests/pending',
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'api/admin/projects/default/environments/development/change-requests',
|
||||||
|
{},
|
||||||
|
'post',
|
||||||
|
200,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -94,17 +132,20 @@ test('show children', async () => {
|
|||||||
|
|
||||||
test('delete dependency', async () => {
|
test('delete dependency', async () => {
|
||||||
render(
|
render(
|
||||||
<FeatureOverviewSidePanelDetails
|
<>
|
||||||
feature={
|
<ToastRenderer />
|
||||||
{
|
<FeatureOverviewSidePanelDetails
|
||||||
name: 'feature',
|
feature={
|
||||||
project: 'default',
|
{
|
||||||
dependencies: [{ feature: 'some_parent' }],
|
name: 'feature',
|
||||||
children: [] as string[],
|
project: 'default',
|
||||||
} as IFeatureToggle
|
dependencies: [{ feature: 'some_parent' }],
|
||||||
}
|
children: [] as string[],
|
||||||
header={''}
|
} as IFeatureToggle
|
||||||
/>,
|
}
|
||||||
|
header={''}
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
{
|
{
|
||||||
permissions: [
|
permissions: [
|
||||||
{ permission: 'UPDATE_FEATURE_DEPENDENCY', project: 'default' },
|
{ permission: 'UPDATE_FEATURE_DEPENDENCY', project: 'default' },
|
||||||
@ -122,6 +163,46 @@ test('delete dependency', async () => {
|
|||||||
|
|
||||||
const deleteButton = await screen.findByText('Delete');
|
const deleteButton = await screen.findByText('Delete');
|
||||||
userEvent.click(deleteButton);
|
userEvent.click(deleteButton);
|
||||||
|
|
||||||
|
await screen.findByText('Dependency removed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete dependency with change request', async () => {
|
||||||
|
setupChangeRequestApi();
|
||||||
|
render(
|
||||||
|
<>
|
||||||
|
<ToastRenderer />
|
||||||
|
<FeatureOverviewSidePanelDetails
|
||||||
|
feature={
|
||||||
|
{
|
||||||
|
name: 'feature',
|
||||||
|
project: 'default',
|
||||||
|
dependencies: [{ feature: 'some_parent' }],
|
||||||
|
children: [] as string[],
|
||||||
|
} as IFeatureToggle
|
||||||
|
}
|
||||||
|
header={''}
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{
|
||||||
|
permissions: [
|
||||||
|
/* deliberately no permissions */
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText('Dependency:');
|
||||||
|
await screen.findByText('some_parent');
|
||||||
|
|
||||||
|
const actionsButton = await screen.findByRole('button', {
|
||||||
|
name: /Dependency actions/i,
|
||||||
|
});
|
||||||
|
userEvent.click(actionsButton);
|
||||||
|
|
||||||
|
const deleteButton = await screen.findByText('Delete');
|
||||||
|
userEvent.click(deleteButton);
|
||||||
|
|
||||||
|
await screen.findByText('Change added to a draft');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('edit dependency', async () => {
|
test('edit dependency', async () => {
|
||||||
@ -147,7 +228,7 @@ test('edit dependency', async () => {
|
|||||||
await screen.findByText('Dependency:');
|
await screen.findByText('Dependency:');
|
||||||
await screen.findByText('some_parent');
|
await screen.findByText('some_parent');
|
||||||
|
|
||||||
const actionsButton = screen.getByRole('button', {
|
const actionsButton = await screen.findByRole('button', {
|
||||||
name: /Dependency actions/i,
|
name: /Dependency actions/i,
|
||||||
});
|
});
|
||||||
userEvent.click(actionsButton);
|
userEvent.click(actionsButton);
|
||||||
|
@ -60,9 +60,9 @@ export const useCheckProjectAccess = (projectId: string) => {
|
|||||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
const checkAccess = useCheckProjectPermissions(projectId);
|
const checkAccess = useCheckProjectPermissions(projectId);
|
||||||
|
|
||||||
return (permission: string, environment: string) => {
|
return (permission: string, environment?: string) => {
|
||||||
return (
|
return (
|
||||||
isChangeRequestConfigured(environment) ||
|
(environment && isChangeRequestConfigured(environment)) ||
|
||||||
checkAccess(permission, environment)
|
checkAccess(permission, environment)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user