chore: hooks -> decorators

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-10-14 09:06:16 +02:00
parent aee334c14c
commit 88746203aa
15 changed files with 91 additions and 85 deletions
@@ -475,7 +475,8 @@ export async function createRouter(
description: schema.description,
schema,
})),
EXPERIMENTAL_formHooks: template.spec.EXPERIMENTAL_formHooks,
EXPERIMENTAL_formDecorators:
template.spec.EXPERIMENTAL_formDecorators,
});
},
)
@@ -173,9 +173,9 @@
}
}
},
"EXPERIMENTAL_formHooks": {
"EXPERIMENTAL_formDecorators": {
"type": "object",
"description": "A list of hooks and their inputs that the form should trigger before submitting the job",
"description": "A list of decorators and their inputs that the form should trigger before submitting the job",
"properties": {
"id": {
"type": "string",
@@ -59,7 +59,7 @@ export interface TemplateEntityV1beta3 extends Entity {
/**
* Form hooks to be run
*/
EXPERIMENTAL_formHooks?: { id: string; input?: JsonObject }[];
EXPERIMENTAL_formDecorators?: { id: string; input?: JsonObject }[];
/**
* This is a JSONSchema or an array of JSONSchema's which is used to render a form in the frontend
+1 -1
View File
@@ -538,7 +538,7 @@ export type TemplateParameterSchema = {
description?: string;
schema: JsonObject;
}>;
EXPERIMENTAL_formHooks?: {
EXPERIMENTAL_formDecorators?: {
id: string;
input?: JsonObject;
}[];
@@ -35,7 +35,7 @@ import { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
import { useTemplateTimeSavedMinutes } from '../../hooks/useTemplateTimeSaved';
import { JsonValue } from '@backstage/types';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { useFormHooks } from '../../../../../scaffolder/src/alpha/hooks/useFormHooks';
import { useFormDecorators } from '../../../../../scaffolder/src/alpha/hooks/useFormDecorators';
const useStyles = makeStyles({
markdown: {
@@ -89,24 +89,24 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => {
const sortedManifest = useFilteredSchemaProperties(manifest);
const minutesSaved = useTemplateTimeSavedMinutes(templateRef);
const { setSecrets } = useTemplateSecrets();
const formHooks = useFormHooks();
const formDecorators = useFormDecorators();
const workflowOnCreate = useCallback(
async (formState: Record<string, JsonValue>) => {
if (manifest?.EXPERIMENTAL_formHooks && formHooks?.size) {
// for each of the form hooks, go and call the hook with the context
if (manifest?.EXPERIMENTAL_formDecorators && formDecorators?.size) {
// for each of the form decorators, go and call the decorator with the context
await Promise.all(
manifest.EXPERIMENTAL_formHooks.map(async hook => {
const formHook = formHooks.get(hook.id);
if (!formHook) {
manifest.EXPERIMENTAL_formDecorators.map(async decorator => {
const formDecorator = formDecorators.get(decorator.id);
if (!formDecorator) {
// eslint-disable-next-line no-console
console.error('Failed to find form hook', hook.id);
console.error('Failed to find form decorator', decorator.id);
return;
}
await formHook.fn({
await formDecorator.fn({
setSecrets,
input: hook.input,
input: decorator.input,
});
}),
);
@@ -121,8 +121,8 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => {
});
},
[
manifest?.EXPERIMENTAL_formHooks,
formHooks,
manifest?.EXPERIMENTAL_formDecorators,
formDecorators,
onCreate,
analytics,
templateName,
@@ -13,21 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createScaffolderFormHook } from './createScaffolderFormHook';
import { createScaffolderFormDecorator } from './createScaffolderFormDecorator';
describe('createScaffolderFormHook', () => {
it('should return a hook', () => {
const hook = createScaffolderFormHook({
describe('createScaffolderFormDecorator', () => {
it('should return a decorator', () => {
const decorator = createScaffolderFormDecorator({
id: 'test',
deps: {},
fn: async () => {},
});
expect(hook).toMatchInlineSnapshot();
expect(decorator).toMatchInlineSnapshot();
});
it('should allow passing schema and be typesafe', () => {
const hook = createScaffolderFormHook({
const decorator = createScaffolderFormDecorator({
id: 'test',
deps: {},
schema: {
@@ -46,6 +46,6 @@ describe('createScaffolderFormHook', () => {
},
});
expect(hook).toMatchInlineSnapshot();
expect(decorator).toMatchInlineSnapshot();
});
});
@@ -16,12 +16,12 @@
import { AnyApiRef } from '@backstage/core-plugin-api';
import { z } from 'zod';
export type ScaffolderFormHookContext<TInput> = {
export type ScaffolderFormDecoratorContext<TInput> = {
input: TInput;
setSecrets: (input: Record<string, string>) => void;
};
export type ScaffolderFormHook<
export type ScaffolderFormDecorator<
TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType } = {},
TDeps extends { [key in string]: AnyApiRef } = { [key in string]: AnyApiRef },
TInput extends {} = {
@@ -35,7 +35,7 @@ export type ScaffolderFormHook<
};
deps?: TDeps;
fn: (
ctx: ScaffolderFormHookContext<TInput>,
ctx: ScaffolderFormDecoratorContext<TInput>,
deps: TDeps extends { [key in string]: AnyApiRef }
? { [key in keyof TDeps]: TDeps[key]['T'] }
: never,
@@ -43,11 +43,11 @@ export type ScaffolderFormHook<
};
/**
* Method for creating hooks which can be used to collect
* Method for creating decorators which can be used to collect
* secrets from the user before submitting to the backend.
* @public
*/
export function createScaffolderFormHook<
export function createScaffolderFormDecorator<
TDeps extends { [key in string]: AnyApiRef },
TInputSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType },
TInput extends {} = {
@@ -60,12 +60,12 @@ export function createScaffolderFormHook<
};
deps?: TDeps;
fn: (
ctx: ScaffolderFormHookContext<TInput>,
ctx: ScaffolderFormDecoratorContext<TInput>,
deps: TDeps extends { [key in string]: AnyApiRef }
? { [key in keyof TDeps]: TDeps[key]['T'] }
: never,
) => Promise<void>;
}): ScaffolderFormHook<TInputSchema, TDeps, TInput> {
}): ScaffolderFormDecorator<TInputSchema, TDeps, TInput> {
return {
...options,
version: 'v1',
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './createScaffolderFormHook';
export * from './createScaffolderFormDecorator';
+1 -1
View File
@@ -33,5 +33,5 @@ export type TemplateParameterSchema = {
description?: string;
schema: JsonObject;
}>;
EXPERIMENTAL_formHooks?: { id: string; input?: JsonObject }[];
EXPERIMENTAL_formDecorators?: { id: string; input?: JsonObject }[];
};
@@ -14,21 +14,23 @@
* limitations under the License.
*/
import { ScaffolderFormHooksApi } from './types';
import { ScaffolderFormHook } from '@backstage/plugin-scaffolder-react/alpha';
import { ScaffolderFormDecoratorsApi } from './types';
import { ScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha';
export class DefaultScaffolderFormHooksApi implements ScaffolderFormHooksApi {
export class DefaultScaffolderFormDecoratorsApi
implements ScaffolderFormDecoratorsApi
{
constructor(
private readonly options: {
hooks: Array<ScaffolderFormHook>;
decorators: Array<ScaffolderFormDecorator>;
},
) {}
static create(options: { hooks: ScaffolderFormHook[] }) {
return new DefaultScaffolderFormHooksApi(options);
static create(options: { decorators: ScaffolderFormDecorator[] }) {
return new DefaultScaffolderFormDecoratorsApi(options);
}
async getFormHooks(): Promise<ScaffolderFormHook[]> {
return this.options.hooks;
async getFormDecorators(): Promise<ScaffolderFormDecorator[]> {
return this.options.decorators;
}
}
+3 -3
View File
@@ -15,12 +15,12 @@
*/
import { createApiRef } from '@backstage/frontend-plugin-api';
import { ScaffolderFormFieldsApi, ScaffolderFormHooksApi } from './types';
import { ScaffolderFormFieldsApi, ScaffolderFormDecoratorsApi } from './types';
export const formFieldsApiRef = createApiRef<ScaffolderFormFieldsApi>({
id: 'plugin.scaffolder.form-fields',
});
export const formHooksApiRef = createApiRef<ScaffolderFormHooksApi>({
id: 'plugin.scaffolder.form-hooks',
export const formDecoratorsApiRef = createApiRef<ScaffolderFormDecoratorsApi>({
id: 'plugin.scaffolder.form-decorators',
});
+3 -3
View File
@@ -16,13 +16,13 @@
import {
FormFieldExtensionData,
ScaffolderFormHook,
ScaffolderFormDecorator,
} from '@backstage/plugin-scaffolder-react/alpha';
export interface ScaffolderFormFieldsApi {
getFormFields(): Promise<FormFieldExtensionData[]>;
}
export interface ScaffolderFormHooksApi {
getFormHooks(): Promise<ScaffolderFormHook[]>;
export interface ScaffolderFormDecoratorsApi {
getFormDecorators(): Promise<ScaffolderFormDecorator[]>;
}
@@ -36,10 +36,7 @@ import {
} from '@backstage/plugin-scaffolder-react';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import {
ScaffolderFormHook,
Workflow,
} from '@backstage/plugin-scaffolder-react/alpha';
import { Workflow } from '@backstage/plugin-scaffolder-react/alpha';
import { JsonValue } from '@backstage/types';
import { Header, Page } from '@backstage/core-components';
@@ -68,7 +65,6 @@ export type TemplateWizardPageProps = {
title?: string;
subtitle?: string;
};
EXPERIMENTAL_formHooks?: ScaffolderFormHook[];
};
export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
@@ -13,24 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DefaultScaffolderFormHooksApi } from '../api/FormHooksApi';
import { createScaffolderFormHook } from '@backstage/plugin-scaffolder-react/alpha';
import { DefaultScaffolderFormDecoratorsApi } from '../api/FormDecoratorsApi';
import { createScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha';
import { createApiRef } from '@backstage/core-plugin-api';
import { TestApiProvider, withLogCollector } from '@backstage/test-utils';
import { renderHook } from '@testing-library/react';
import { useFormHooks } from './useFormHooks';
import { useFormDecorators } from './useFormDecorators';
import React from 'react';
import { formHooksApiRef } from '../api/ref';
import { formDecoratorsApiRef } from '../api/ref';
describe('useFormHooks', () => {
describe('useFormDecorators', () => {
const mockApiRef = createApiRef<{
test: (input: string) => void;
}>({ id: 'test' });
const mockApiImplementation = { test: jest.fn() };
const mockHook = createScaffolderFormHook({
const mockDecorator = createScaffolderFormDecorator({
id: 'test',
deps: { mockApiRef },
schema: {
@@ -43,16 +43,18 @@ describe('useFormHooks', () => {
},
});
it('should wrap up the form hooks', async () => {
const renderedHook = renderHook(() => useFormHooks(), {
it('should wrap up the form decorators', async () => {
const renderedHook = renderHook(() => useFormDecorators(), {
wrapper: ({ children }) => (
<TestApiProvider
apis={[
[mockApiRef, mockApiImplementation],
[
formHooksApiRef,
// @ts-expect-error - todo
DefaultScaffolderFormHooksApi.create({ hooks: [mockHook] }),
formDecoratorsApiRef,
DefaultScaffolderFormDecoratorsApi.create({
// @ts-expect-error - todo
decorators: [mockDecorator],
}),
],
]}
>
@@ -65,10 +67,10 @@ describe('useFormHooks', () => {
expect(result.size).toBe(1);
const testHook = result.get('test')!;
expect(testHook).toBeDefined();
const testDecorator = result.get('test')!;
expect(testDecorator).toBeDefined();
await testHook.fn({
await testDecorator.fn({
setSecrets: () => {},
input: { test: 'input value' },
});
@@ -78,14 +80,16 @@ describe('useFormHooks', () => {
it('should skip failing deps', async () => {
const { error } = await withLogCollector(async () => {
const renderedHook = renderHook(() => useFormHooks(), {
const renderedHook = renderHook(() => useFormDecorators(), {
wrapper: ({ children }) => (
<TestApiProvider
apis={[
[
formHooksApiRef,
// @ts-expect-error - todo
DefaultScaffolderFormHooksApi.create({ hooks: [mockHook] }),
formDecoratorsApiRef,
DefaultScaffolderFormDecoratorsApi.create({
// @ts-expect-error - todo
decorators: [mockDecorator],
}),
],
]}
>
@@ -14,40 +14,43 @@
* limitations under the License.
*/
import { useApi, useApiHolder } from '@backstage/core-plugin-api';
import { formHooksApiRef } from '../api/ref';
import { formDecoratorsApiRef } from '../api/ref';
import useAsync from 'react-use/esm/useAsync';
import { useMemo } from 'react';
import { ScaffolderFormHookContext } from '@backstage/plugin-scaffolder-react/alpha';
import { ScaffolderFormDecoratorContext } from '@backstage/plugin-scaffolder-react/alpha';
/** @internal */
type BoundFieldHook = {
fn: (ctx: ScaffolderFormHookContext<any>) => Promise<void>;
type BoundFieldDecorator = {
fn: (ctx: ScaffolderFormDecoratorContext<any>) => Promise<void>;
};
export const useFormHooks = () => {
const formHooksApi = useApi(formHooksApiRef);
const { value: hooks } = useAsync(() => formHooksApi.getFormHooks(), []);
export const useFormDecorators = () => {
const formDecoratorsApi = useApi(formDecoratorsApiRef);
const { value: decorators } = useAsync(
() => formDecoratorsApi.getFormDecorators(),
[],
);
const apiHolder = useApiHolder();
return useMemo(() => {
const hooksMap = new Map<string, BoundFieldHook>();
const decoratorsMap = new Map<string, BoundFieldDecorator>();
for (const hook of hooks ?? []) {
for (const decorator of decorators ?? []) {
try {
const resolvedDeps = Object.entries(hook.deps ?? {}).map(
const resolvedDeps = Object.entries(decorator.deps ?? {}).map(
([key, value]) => {
const api = apiHolder.get(value);
if (!api) {
throw new Error(
`Failed to resolve apiRef ${value.id} for form hook ${hook.id} it will be disabled`,
`Failed to resolve apiRef ${value.id} for form decorator ${decorator.id} it will be disabled`,
);
}
return [key, api];
},
);
hooksMap.set(hook.id, {
fn: ctx => hook.fn(ctx, Object.fromEntries(resolvedDeps)),
decoratorsMap.set(decorator.id, {
fn: ctx => decorator.fn(ctx, Object.fromEntries(resolvedDeps)),
});
} catch (ex) {
// eslint-disable-next-line no-console
@@ -55,6 +58,6 @@ export const useFormHooks = () => {
return undefined;
}
}
return hooksMap;
}, [apiHolder, hooks]);
return decoratorsMap;
}, [apiHolder, decorators]);
};