mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
feat: show dependencies only when using pro/enterprise or at least on… (#5052)
This commit is contained in:
parent
0064c9e1be
commit
e9e110f702
@ -2,7 +2,7 @@ import { screen } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
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 { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||||
import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer';
|
import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer';
|
||||||
|
|
||||||
@ -30,6 +30,19 @@ const setupApi = () => {
|
|||||||
'delete',
|
'delete',
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
|
testServerRoute(server, '/api/admin/projects/default/dependencies', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupOssWithExistingDependencies = () => {
|
||||||
|
testServerRoute(server, '/api/admin/ui-config', {
|
||||||
|
flags: {
|
||||||
|
dependentFeatures: true,
|
||||||
|
},
|
||||||
|
versionInfo: {
|
||||||
|
current: { oss: 'some value' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
testServerRoute(server, '/api/admin/projects/default/dependencies', true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setupChangeRequestApi = () => {
|
const setupChangeRequestApi = () => {
|
||||||
@ -92,6 +105,36 @@ test('show dependency dialogue', async () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('show dependency dialogue for OSS with dependencies', async () => {
|
||||||
|
setupOssWithExistingDependencies();
|
||||||
|
render(
|
||||||
|
<FeatureOverviewSidePanelDetails
|
||||||
|
feature={
|
||||||
|
{
|
||||||
|
name: 'feature',
|
||||||
|
project: 'default',
|
||||||
|
dependencies: [] as Array<{ feature: string }>,
|
||||||
|
children: [] as string[],
|
||||||
|
} as IFeatureToggle
|
||||||
|
}
|
||||||
|
header={''}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
permissions: [
|
||||||
|
{ permission: 'UPDATE_FEATURE_DEPENDENCY', project: 'default' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const addParentButton = await screen.findByText('Add parent feature');
|
||||||
|
|
||||||
|
addParentButton.click();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText('Add parent feature dependency'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test('show child', async () => {
|
test('show child', async () => {
|
||||||
render(
|
render(
|
||||||
<FeatureOverviewSidePanelDetails
|
<FeatureOverviewSidePanelDetails
|
||||||
|
@ -7,8 +7,8 @@ import { FeatureEnvironmentSeen } from '../../../FeatureEnvironmentSeen/FeatureE
|
|||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { DependencyRow } from './DependencyRow';
|
import { DependencyRow } from './DependencyRow';
|
||||||
import { FlexRow, StyledDetail, StyledLabel } from './StyledRow';
|
import { FlexRow, StyledDetail, StyledLabel } from './StyledRow';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { useShowDependentFeatures } from './useShowDependentFeatures';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -28,7 +28,7 @@ export const FeatureOverviewSidePanelDetails = ({
|
|||||||
}: IFeatureOverviewSidePanelDetailsProps) => {
|
}: IFeatureOverviewSidePanelDetailsProps) => {
|
||||||
const { locationSettings } = useLocationSettings();
|
const { locationSettings } = useLocationSettings();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const dependentFeatures = useUiFlag('dependentFeatures');
|
const showDependentFeatures = useShowDependentFeatures(feature.project);
|
||||||
|
|
||||||
const showLastSeenByEnvironment = Boolean(
|
const showLastSeenByEnvironment = Boolean(
|
||||||
uiConfig.flags.lastSeenByEnvironment,
|
uiConfig.flags.lastSeenByEnvironment,
|
||||||
@ -56,7 +56,7 @@ export const FeatureOverviewSidePanelDetails = ({
|
|||||||
)}
|
)}
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={dependentFeatures}
|
condition={showDependentFeatures}
|
||||||
show={<DependencyRow feature={feature} />}
|
show={<DependencyRow feature={feature} />}
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { useCheckDependenciesExist } from 'hooks/api/getters/useCheckDependenciesExist/useCheckDependenciesExist';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
|
export const useShowDependentFeatures = (project: string) => {
|
||||||
|
const dependentFeatures = useUiFlag('dependentFeatures');
|
||||||
|
const { dependenciesExist } = useCheckDependenciesExist(project);
|
||||||
|
const { isOss } = useUiConfig();
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
isOss() ? dependenciesExist && dependentFeatures : dependentFeatures,
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,32 @@
|
|||||||
|
import { SWRConfiguration } from 'swr';
|
||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||||
|
|
||||||
|
export const useCheckDependenciesExist = (
|
||||||
|
project: string,
|
||||||
|
options: SWRConfiguration = {},
|
||||||
|
) => {
|
||||||
|
const path = formatApiPath(`/api/admin/projects/${project}/dependencies`);
|
||||||
|
const { data, error } = useConditionalSWR(
|
||||||
|
project,
|
||||||
|
false,
|
||||||
|
path,
|
||||||
|
fetcher,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dependenciesExist: data,
|
||||||
|
error,
|
||||||
|
loading: !error && !data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetcher = async (path: string): Promise<boolean> => {
|
||||||
|
const res = await fetch(path).then(
|
||||||
|
handleErrorResponses('Dependencies exist check'),
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
return data;
|
||||||
|
};
|
@ -14,7 +14,7 @@ export const testServerSetup = (): SetupServerApi => {
|
|||||||
export const testServerRoute = (
|
export const testServerRoute = (
|
||||||
server: SetupServerApi,
|
server: SetupServerApi,
|
||||||
path: string,
|
path: string,
|
||||||
json: object,
|
json: object | boolean | string | number,
|
||||||
method: 'get' | 'post' | 'put' | 'delete' = 'get',
|
method: 'get' | 'post' | 'put' | 'delete' = 'get',
|
||||||
status: number = 200,
|
status: number = 200,
|
||||||
) => {
|
) => {
|
||||||
|
@ -38,6 +38,7 @@ interface DeleteDependencyParams extends ProjectParams {
|
|||||||
const PATH = '/:projectId/features';
|
const PATH = '/:projectId/features';
|
||||||
const PATH_FEATURE = `${PATH}/:child`;
|
const PATH_FEATURE = `${PATH}/:child`;
|
||||||
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
|
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
|
||||||
|
const PATH_DEPENDENCIES_CHECK = `/:projectId/dependencies`;
|
||||||
const PATH_PARENTS = `${PATH_FEATURE}/parents`;
|
const PATH_PARENTS = `${PATH_FEATURE}/parents`;
|
||||||
const PATH_DEPENDENCY = `${PATH_FEATURE}/dependencies/:parent`;
|
const PATH_DEPENDENCY = `${PATH_FEATURE}/dependencies/:parent`;
|
||||||
|
|
||||||
@ -165,6 +166,26 @@ export default class DependentFeaturesController extends Controller {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'get',
|
||||||
|
path: PATH_DEPENDENCIES_CHECK,
|
||||||
|
handler: this.checkDependenciesExist,
|
||||||
|
permission: NONE,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Dependencies'],
|
||||||
|
summary: 'Check dependencies exist.',
|
||||||
|
description:
|
||||||
|
'Check if any dependencies exist in this Unleash instance',
|
||||||
|
operationId: 'checkDependenciesExist',
|
||||||
|
responses: {
|
||||||
|
200: createResponseSchema('dependenciesExistSchema'),
|
||||||
|
...getStandardResponses(401, 403),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFeatureDependency(
|
async addFeatureDependency(
|
||||||
@ -255,4 +276,21 @@ export default class DependentFeaturesController extends Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkDependenciesExist(
|
||||||
|
req: IAuthRequest,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const { child } = req.params;
|
||||||
|
|
||||||
|
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
|
||||||
|
const exist =
|
||||||
|
await this.dependentFeaturesService.checkDependenciesExist();
|
||||||
|
res.send(exist);
|
||||||
|
} else {
|
||||||
|
throw new InvalidOperationError(
|
||||||
|
'Dependent features are not enabled',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,5 @@ export interface IDependentFeaturesReadModel {
|
|||||||
getDependencies(children: string[]): Promise<IFeatureDependency[]>;
|
getDependencies(children: string[]): Promise<IFeatureDependency[]>;
|
||||||
getParentOptions(child: string): Promise<string[]>;
|
getParentOptions(child: string): Promise<string[]>;
|
||||||
hasDependencies(feature: string): Promise<boolean>;
|
hasDependencies(feature: string): Promise<boolean>;
|
||||||
|
hasAnyDependencies(): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
@ -90,4 +90,12 @@ export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
|
|||||||
|
|
||||||
return parents.length > 0;
|
return parents.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async hasAnyDependencies(): Promise<boolean> {
|
||||||
|
const result = await this.db.raw(
|
||||||
|
`SELECT EXISTS (SELECT 1 FROM dependent_features) AS present`,
|
||||||
|
);
|
||||||
|
const { present } = result.rows[0];
|
||||||
|
return present;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,6 +197,10 @@ export class DependentFeaturesService {
|
|||||||
return this.dependentFeaturesReadModel.getParentOptions(feature);
|
return this.dependentFeaturesReadModel.getParentOptions(feature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkDependenciesExist(): Promise<boolean> {
|
||||||
|
return this.dependentFeaturesReadModel.hasAnyDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
private async stopWhenChangeRequestsEnabled(project: string, user?: User) {
|
private async stopWhenChangeRequestsEnabled(project: string, user?: User) {
|
||||||
const canBypass =
|
const canBypass =
|
||||||
await this.changeRequestAccessReadModel.canBypassChangeRequestForProject(
|
await this.changeRequestAccessReadModel.canBypassChangeRequestForProject(
|
||||||
|
@ -44,6 +44,10 @@ afterAll(async () => {
|
|||||||
await db.destroy();
|
await db.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.stores.dependentFeaturesStore.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
const addFeatureDependency = async (
|
const addFeatureDependency = async (
|
||||||
childFeature: string,
|
childFeature: string,
|
||||||
payload: CreateDependentFeatureSchema,
|
payload: CreateDependentFeatureSchema,
|
||||||
@ -86,6 +90,12 @@ const getParentOptions = async (childFeature: string, expectedCode = 200) => {
|
|||||||
.expect(expectedCode);
|
.expect(expectedCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkDependenciesExist = async (expectedCode = 200) => {
|
||||||
|
return app.request
|
||||||
|
.get(`/api/admin/projects/default/dependencies`)
|
||||||
|
.expect(expectedCode);
|
||||||
|
};
|
||||||
|
|
||||||
test('should add and delete feature dependencies', async () => {
|
test('should add and delete feature dependencies', async () => {
|
||||||
const parent = uuidv4();
|
const parent = uuidv4();
|
||||||
const child = uuidv4();
|
const child = uuidv4();
|
||||||
@ -167,3 +177,20 @@ test('should not allow to add archived parent dependency', async () => {
|
|||||||
403,
|
403,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should check if any dependencies exist', async () => {
|
||||||
|
const parent = uuidv4();
|
||||||
|
const child = uuidv4();
|
||||||
|
await app.createFeature(child);
|
||||||
|
await app.createFeature(parent);
|
||||||
|
|
||||||
|
const { body: dependenciesExistBefore } = await checkDependenciesExist();
|
||||||
|
expect(dependenciesExistBefore).toBe(false);
|
||||||
|
|
||||||
|
await addFeatureDependency(child, {
|
||||||
|
feature: parent,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: dependenciesExistAfter } = await checkDependenciesExist();
|
||||||
|
expect(dependenciesExistAfter).toBe(true);
|
||||||
|
});
|
||||||
|
@ -27,4 +27,8 @@ export class FakeDependentFeaturesReadModel
|
|||||||
getOrphanParents(parentsAndChildren: string[]): Promise<string[]> {
|
getOrphanParents(parentsAndChildren: string[]): Promise<string[]> {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasAnyDependencies(): Promise<boolean> {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,6 +164,7 @@ import {
|
|||||||
dependentFeatureSchema,
|
dependentFeatureSchema,
|
||||||
createDependentFeatureSchema,
|
createDependentFeatureSchema,
|
||||||
parentFeatureOptionsSchema,
|
parentFeatureOptionsSchema,
|
||||||
|
dependenciesExistSchema,
|
||||||
} from './spec';
|
} from './spec';
|
||||||
import { IServerOption } from '../types';
|
import { IServerOption } from '../types';
|
||||||
import { mapValues, omitKeys } from '../util';
|
import { mapValues, omitKeys } from '../util';
|
||||||
@ -391,6 +392,7 @@ export const schemas: UnleashSchemas = {
|
|||||||
createDependentFeatureSchema,
|
createDependentFeatureSchema,
|
||||||
parentFeatureOptionsSchema,
|
parentFeatureOptionsSchema,
|
||||||
featureDependenciesSchema,
|
featureDependenciesSchema,
|
||||||
|
dependenciesExistSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
||||||
|
@ -35,7 +35,7 @@ const metaRules: Rule[] = [
|
|||||||
metaSchema: {
|
metaSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
type: { type: 'string', enum: ['object', 'array'] },
|
type: { type: 'string', enum: ['object', 'array', 'boolean'] },
|
||||||
},
|
},
|
||||||
required: ['type'],
|
required: ['type'],
|
||||||
},
|
},
|
||||||
|
13
src/lib/openapi/spec/dependencies-exist-schema.ts
Normal file
13
src/lib/openapi/spec/dependencies-exist-schema.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
export const dependenciesExistSchema = {
|
||||||
|
$id: '#/components/schemas/dependenciesExistSchema',
|
||||||
|
type: 'boolean',
|
||||||
|
description:
|
||||||
|
'`true` when any dependencies exist, `false` when no dependencies exist.',
|
||||||
|
components: {},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type DependenciesExistSchema = FromSchema<
|
||||||
|
typeof dependenciesExistSchema
|
||||||
|
>;
|
@ -164,3 +164,4 @@ export * from './dependent-feature-schema';
|
|||||||
export * from './create-dependent-feature-schema';
|
export * from './create-dependent-feature-schema';
|
||||||
export * from './parent-feature-options-schema';
|
export * from './parent-feature-options-schema';
|
||||||
export * from './feature-dependencies-schema';
|
export * from './feature-dependencies-schema';
|
||||||
|
export * from './dependencies-exist-schema';
|
||||||
|
Loading…
Reference in New Issue
Block a user