Merge pull request #9665 from djamaile/feature-flag-scaffolding

feat(scaffolder): remove sections or fields based on a feature flag
This commit is contained in:
Fredrik Adelöw
2022-02-21 21:53:40 +01:00
committed by GitHub
3 changed files with 171 additions and 10 deletions
@@ -24,12 +24,17 @@ 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';
import { transformSchemaToProps } from './schema';
import { Content, StructuredMetadataTable } from '@backstage/core-components';
import cloneDeep from 'lodash/cloneDeep';
import * as fieldOverrides from './FieldOverrides';
const Form = withTheme(MuiTheme);
@@ -104,18 +109,50 @@ export function getReviewData(formData: Record<string, any>, steps: Step[]) {
return reviewData;
}
export const MultistepJsonForm = ({
steps,
formData,
onChange,
onReset,
onFinish,
fields,
widgets,
}: Props) => {
export const MultistepJsonForm = (props: Props) => {
const { formData, onChange, onReset, onFinish, fields, widgets } = props;
const [activeStep, setActiveStep] = useState(0);
const [disableButtons, setDisableButtons] = useState(false);
const errorApi = useApi(errorApiRef);
const featureFlagApi = useApi(featureFlagsApiRef);
const featureFlagKey = 'backstage:featureFlag';
const filterOutProperties = (step: Step): Step => {
const filteredStep = cloneDeep(step);
const removedPropertyKeys: Array<string> = [];
if (filteredStep.schema.properties) {
filteredStep.schema.properties = Object.fromEntries(
Object.entries(filteredStep.schema.properties).filter(
([key, value]) => {
if (value[featureFlagKey]) {
if (featureFlagApi.isActive(value[featureFlagKey])) {
return true;
}
removedPropertyKeys.push(key);
return false;
}
return true;
},
),
);
// remove the feature flag property key from required if they are not active
filteredStep.schema.required = Array.isArray(filteredStep.schema.required)
? filteredStep.schema.required?.filter(
r => !removedPropertyKeys.includes(r as string),
)
: filteredStep.schema.required;
}
return filteredStep;
};
const steps = props.steps
.filter(step => {
const featureFlag = step.schema[featureFlagKey];
return (
typeof featureFlag !== 'string' || featureFlagApi.isActive(featureFlag)
);
})
.map(filterOutProperties);
const handleReset = () => {
setActiveStep(0);
@@ -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();
});
});