feat(scaffolder): remove sections or fields based on a feature flag

Signed-off-by: djamaile <rdjamaile@gmail.com>
This commit is contained in:
djamaile
2022-02-18 16:21:37 +01:00
parent 9c17a6cdb9
commit 2f2543592c
3 changed files with 171 additions and 3 deletions
@@ -24,7 +24,11 @@ import {
Stepper,
Typography,
} from '@material-ui/core';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import {
errorApiRef,
useApi,
featureFlagsApiRef,
} from '@backstage/core-plugin-api';
import { FormProps, IChangeEvent, UiSchema, withTheme } from '@rjsf/core';
import { Theme as MuiTheme } from '@rjsf/material-ui';
import React, { useState } from 'react';
@@ -116,13 +120,53 @@ export const MultistepJsonForm = ({
const [activeStep, setActiveStep] = useState(0);
const [disableButtons, setDisableButtons] = useState(false);
const errorApi = useApi(errorApiRef);
const featureFlagApi = useApi(featureFlagsApiRef);
const featureFlagKey = 'backstage:featureFlag';
const filterOutProperties = (schema: JsonObject): JsonObject => {
const removedPropertiesKeys: Array<string> = [];
if (schema.properties) {
schema.properties = Object.fromEntries(
Object.entries(schema.properties).filter(([key, value]) => {
if (value[featureFlagKey]) {
if (featureFlagApi.isActive(value[featureFlagKey])) {
return true;
}
removedPropertiesKeys.push(key);
return false;
}
return true;
}),
);
// remove the feature flag property keys from required if they are not active
if (Array.isArray(schema.required) && removedPropertiesKeys.length > 0) {
for (const property of removedPropertiesKeys) {
const index = schema.required.findIndex(r => r === property);
if (index > -1) {
schema.required.splice(index, 1);
}
}
}
}
return schema;
};
const updatedSteps = steps.filter(s => {
const featureFlag = s.schema[featureFlagKey] as string;
if (featureFlag && !featureFlagApi.isActive(featureFlag)) {
return null;
}
// filter out properties accordingly to the feature flag settings;
s.schema = filterOutProperties(s.schema);
return s;
});
const handleReset = () => {
setActiveStep(0);
onReset();
};
const handleNext = () => {
setActiveStep(Math.min(activeStep + 1, steps.length));
setActiveStep(Math.min(activeStep + 1, updatedSteps.length));
};
const handleBack = () => setActiveStep(Math.max(activeStep - 1, 0));
const handleCreate = async () => {
@@ -138,7 +182,7 @@ export const MultistepJsonForm = ({
return (
<>
<Stepper activeStep={activeStep} orientation="vertical">
{steps.map(({ title, schema, ...formProps }, index) => {
{updatedSteps.map(({ title, schema, ...formProps }, index) => {
return (
<StepUI key={title}>
<StepLabel
@@ -26,6 +26,10 @@ import { MemoryRouter, Route } from 'react-router';
import { ScaffolderApi, scaffolderApiRef } from '../../api';
import { rootRouteRef } from '../../routes';
import { TemplatePage } from './TemplatePage';
import {
featureFlagsApiRef,
FeatureFlagsApi,
} from '@backstage/core-plugin-api';
import { ApiProvider } from '@backstage/core-app-api';
import { errorApiRef } from '@backstage/core-plugin-api';
@@ -48,11 +52,63 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
listActions: jest.fn(),
};
const featureFlagsApiMock: jest.Mocked<FeatureFlagsApi> = {
isActive: jest.fn(),
registerFlag: jest.fn(),
getRegisteredFlags: jest.fn(),
save: jest.fn(),
};
const errorApiMock = { post: jest.fn(), error$: jest.fn() };
const schemaMockValue = {
title: 'my-schema',
steps: [
{
title: 'Fill in some steps',
schema: {
title: 'Fill in some steps',
'backstage:featureFlag': 'experimental-feature',
properties: {
name: {
title: 'Name',
type: 'string',
'backstage:featureFlag': 'should-show-some-stuff-first-option',
},
description: {
title: 'Description',
type: 'string',
description: 'A description for the component',
},
owner: {
title: 'Owner',
type: 'string',
description: 'Owner of the component',
},
},
type: 'object',
},
},
{
title: 'Send data',
schema: {
title: 'Send data',
properties: {
user: {
title: 'User',
type: 'string',
},
},
type: 'object',
},
},
],
};
const apis = TestApiRegistry.from(
[scaffolderApiRef, scaffolderApiMock],
[errorApiRef, errorApiMock],
[featureFlagsApiRef, featureFlagsApiMock],
);
describe('TemplatePage', () => {
@@ -197,4 +253,32 @@ describe('TemplatePage', () => {
fireEvent.click(await findByText('Next step'));
expect(await findByText('Reset')).toBeInTheDocument();
});
it('should display a section or property based on a feature flag', async () => {
featureFlagsApiMock.isActive.mockImplementation(flag => {
if (flag === 'experimental-feature') {
return true;
}
return false;
});
scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue(
schemaMockValue,
);
const { queryByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
</ApiProvider>,
{
mountedRoutes: {
'/create/actions': rootRouteRef,
},
},
);
expect(await queryByText('Name')).not.toBeInTheDocument();
expect(await queryByText('Description')).toBeInTheDocument();
expect(await queryByText('Owner')).toBeInTheDocument();
expect(await queryByText('Send data')).toBeInTheDocument();
});
});