1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00

feat: show dependencies only when using pro/enterprise or at least on… (#5052)

This commit is contained in:
Mateusz Kwasniewski 2023-10-16 20:56:06 +02:00 committed by GitHub
parent 0064c9e1be
commit e9e110f702
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 192 additions and 6 deletions

View File

@ -2,7 +2,7 @@ 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';
import { IFeatureToggle } from 'interfaces/featureToggle';
import { testServerRoute, testServerSetup } from 'utils/testServer';
import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer';
@ -30,6 +30,19 @@ const setupApi = () => {
'delete',
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 = () => {
@ -92,6 +105,36 @@ test('show dependency dialogue', async () => {
).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 () => {
render(
<FeatureOverviewSidePanelDetails

View File

@ -7,8 +7,8 @@ import { FeatureEnvironmentSeen } from '../../../FeatureEnvironmentSeen/FeatureE
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { DependencyRow } from './DependencyRow';
import { FlexRow, StyledDetail, StyledLabel } from './StyledRow';
import { useUiFlag } from 'hooks/useUiFlag';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useShowDependentFeatures } from './useShowDependentFeatures';
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
@ -28,7 +28,7 @@ export const FeatureOverviewSidePanelDetails = ({
}: IFeatureOverviewSidePanelDetailsProps) => {
const { locationSettings } = useLocationSettings();
const { uiConfig } = useUiConfig();
const dependentFeatures = useUiFlag('dependentFeatures');
const showDependentFeatures = useShowDependentFeatures(feature.project);
const showLastSeenByEnvironment = Boolean(
uiConfig.flags.lastSeenByEnvironment,
@ -56,7 +56,7 @@ export const FeatureOverviewSidePanelDetails = ({
)}
</FlexRow>
<ConditionallyRender
condition={dependentFeatures}
condition={showDependentFeatures}
show={<DependencyRow feature={feature} />}
/>
</StyledContainer>

View File

@ -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,
);
};

View File

@ -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;
};

View File

@ -14,7 +14,7 @@ export const testServerSetup = (): SetupServerApi => {
export const testServerRoute = (
server: SetupServerApi,
path: string,
json: object,
json: object | boolean | string | number,
method: 'get' | 'post' | 'put' | 'delete' = 'get',
status: number = 200,
) => {

View File

@ -38,6 +38,7 @@ interface DeleteDependencyParams extends ProjectParams {
const PATH = '/:projectId/features';
const PATH_FEATURE = `${PATH}/:child`;
const PATH_DEPENDENCIES = `${PATH_FEATURE}/dependencies`;
const PATH_DEPENDENCIES_CHECK = `/:projectId/dependencies`;
const PATH_PARENTS = `${PATH_FEATURE}/parents`;
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(
@ -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',
);
}
}
}

View File

@ -10,4 +10,5 @@ export interface IDependentFeaturesReadModel {
getDependencies(children: string[]): Promise<IFeatureDependency[]>;
getParentOptions(child: string): Promise<string[]>;
hasDependencies(feature: string): Promise<boolean>;
hasAnyDependencies(): Promise<boolean>;
}

View File

@ -90,4 +90,12 @@ export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
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;
}
}

View File

@ -197,6 +197,10 @@ export class DependentFeaturesService {
return this.dependentFeaturesReadModel.getParentOptions(feature);
}
async checkDependenciesExist(): Promise<boolean> {
return this.dependentFeaturesReadModel.hasAnyDependencies();
}
private async stopWhenChangeRequestsEnabled(project: string, user?: User) {
const canBypass =
await this.changeRequestAccessReadModel.canBypassChangeRequestForProject(

View File

@ -44,6 +44,10 @@ afterAll(async () => {
await db.destroy();
});
beforeEach(async () => {
await db.stores.dependentFeaturesStore.deleteAll();
});
const addFeatureDependency = async (
childFeature: string,
payload: CreateDependentFeatureSchema,
@ -86,6 +90,12 @@ const getParentOptions = async (childFeature: string, expectedCode = 200) => {
.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 () => {
const parent = uuidv4();
const child = uuidv4();
@ -167,3 +177,20 @@ test('should not allow to add archived parent dependency', async () => {
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);
});

View File

@ -27,4 +27,8 @@ export class FakeDependentFeaturesReadModel
getOrphanParents(parentsAndChildren: string[]): Promise<string[]> {
return Promise.resolve([]);
}
hasAnyDependencies(): Promise<boolean> {
return Promise.resolve(false);
}
}

View File

@ -164,6 +164,7 @@ import {
dependentFeatureSchema,
createDependentFeatureSchema,
parentFeatureOptionsSchema,
dependenciesExistSchema,
} from './spec';
import { IServerOption } from '../types';
import { mapValues, omitKeys } from '../util';
@ -391,6 +392,7 @@ export const schemas: UnleashSchemas = {
createDependentFeatureSchema,
parentFeatureOptionsSchema,
featureDependenciesSchema,
dependenciesExistSchema,
};
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.

View File

@ -35,7 +35,7 @@ const metaRules: Rule[] = [
metaSchema: {
type: 'object',
properties: {
type: { type: 'string', enum: ['object', 'array'] },
type: { type: 'string', enum: ['object', 'array', 'boolean'] },
},
required: ['type'],
},

View 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
>;

View File

@ -164,3 +164,4 @@ export * from './dependent-feature-schema';
export * from './create-dependent-feature-schema';
export * from './parent-feature-options-schema';
export * from './feature-dependencies-schema';
export * from './dependencies-exist-schema';