janus-idp / backstage-showcase

This repo is moving to https://github.com/redhat-developer/red-hat-developer-hub
https://janus-idp.io
Apache License 2.0
112 stars 152 forks source link

Support to register scaffolderPlugin.provide(createScaffolderFieldExtension( for dynamic plugin #1146

Closed cmoulliard closed 6 months ago

cmoulliard commented 8 months ago

Support to register scaffolderPlugin.provide

It is not possible today to register using the dynamic configuration, a plugin packaging ScaffolderFieldExtension. Such fields which are React field components are used part of a backstage template and are registered as such

// https://github.com/q-shift/backstage-plugins/blob/main/plugins/quarkus/src/plugin.ts#L7-L11

import {scaffolderPlugin} from '@backstage/plugin-scaffolder';
import {createScaffolderFieldExtension, FieldExtensionComponent} from '@backstage/plugin-scaffolder-react';
import {QuarkusExtensionList} from './scaffolder/QuarkusExtensionList';

export const QuarkusExtensionListField: FieldExtensionComponent<string, string> = scaffolderPlugin.provide(
    createScaffolderFieldExtension({
        name: 'QuarkusExtensionList',
        component: QuarkusExtensionList,
    }),
davidfestal commented 8 months ago

cc @tumido

cmoulliard commented 8 months ago

@tumido Have you been able to have a look ? Can I help you ?

tumido commented 7 months ago

Yes, this is currently not possible. I'm not sure if we want to evolve and enhance the current dynamic frontend system while there's parallel work ongoing in the upstream.

  1. We would need to add support for collecting, parsing and loading these as dynamic extensions, that can be easily done on the chrome/core app, similarly to what we do for dynamic pages etc.
  2. We would need to take a look into how the Scaffolder frontend is being rendered. I haven't looked into that at all, so it may even require us forking the Scaffolder frontend. That would totally change the scope of the work here.
davidfestal commented 7 months ago

I'm not sure if we want to evolve and enhance the current dynamic frontend system while there's parallel work ongoing in the upstream.

I don't think it makes sense to add more features in the current dynamic frontend system which will risk making it more difficult to converge with the parallel upstream work. If it doesn't prevent the core feature of the Quarkus plugin to work, then let's postpone the scaffolder field extension for later.

davidfestal commented 7 months ago

@christophe-f what do you think ?

durandom commented 7 months ago
  1. is there a hacky way to get the FieldExtension into the showcase app? Like hardcoding it or wrapping it in a feature flag, which could be enable by a check if the quarkus plugin is enabled. Pretty much like what you did with the Quarkus Tab
  2. how many of those quarkus extensions would be listed and are they always installation dependent? I.e. could the interim solution be a static dropdown that's populated in the template?

@cmoulliard ^^

cmoulliard commented 7 months ago

2. how many of those quarkus extensions would be listed and are they always installation dependent?

Until now we have developed 3 fields and soon we will have 4. They are needed when a user scaffold a project using a template including them

cmoulliard commented 7 months ago

2. I.e. could the interim solution be a static dropdown that's populated in the template?

This is doable but as the list of the quarkus versions change when new quarkus are out (like also the extensions liust), then it will be needed to find a way to populate the list using values passed as app-config.yaml field/parameter (aka similar to what users do with their git servers, etc)

cmoulliard commented 7 months ago

There is something that I dont understand. Even if this is not well documented, backstage-next offers a way to configure some additional fields as you can see here: https://github.com/backstage/backstage/blob/master/packages/app-next/app-config.yaml#L149-L156

  - scaffolder.page:
       config:
         groups:
         - title: Recommended
           filter:
             entity.metadata.tags: recommended
   - scaffolder.page/fields: '@backstage/plugin-scaffolder#LowerCaseValuePickerFieldExtension'
   - scaffolder.page/layout: '@backstage/plugin-scaffolder#TwoColumnLayout'

Question: Does it work or not ? Is it something that we could use with backstage-showcase ?

davidfestal commented 7 months ago

Surely, that's the plan, but this backstage-next things were not available when the dynamic frontend plugins were introduced into the Janus showcase, which explains why we had to start with something Janus-specific which covered the main needs on the Janus side.

The plan is now to:

That's precisely why we want to avoid, as much as possible, investing more in the Janus-specific implementation.

cmoulliard commented 7 months ago
  • dynamic frontend plugin system

Is there a great example well documented explaining how to create a new front plugin, build it and configure it ?

christophe-f commented 7 months ago

Is there a great example well documented explaining how to create a new front plugin, build it and configure it ?

It's currently in progress.

christophe-f commented 7 months ago

This is definitely something that we need to provide. Platform engineers will need to add custom FieldExtension.

cmoulliard commented 7 months ago

I started to experiment/play with new frontend using app-next package of backstage = 1.25 where I included the scaffolderPlugin within the App.tsx file

const app = createApp({
  features: [
    graphiqlPlugin,
    pagesPlugin,
    techRadarPlugin,
    techdocsPlugin,
    userSettingsPlugin,
    homePlugin,
    appVisualizerPlugin,
    scaffolderPlugin,
...

and declared (based on existing example) such a field declaration within the app-config.yaml file but that fails

    - scaffolder.page/fields: '@backstage/plugin-scaffolder#LowerCaseValuePickerFieldExtension'

Error reported "Invalid extension configuration at app.extensions[19][scaffolder.page/fields], value must be a boolean or object"

Raised by this code:

// backstage/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts)
  if (typeof value !== 'object' || Array.isArray(value)) {
    // We don't mention null here - we don't want people to explicitly enter
    // - entity.card.about: null
    throw new Error(errorMsg('value must be a boolean or object', id));
  }
cmoulliard commented 7 months ago

I digged into the code and found how backstage is able to discover the custom template fields ;-)

Such a mechanism is taking place here within the scaffolderpage

/**
 * The Router and main entrypoint to the Scaffolder plugin.
 *
 * @public
 */
export const ScaffolderPage = scaffolderPlugin.provide(
  createRoutableExtension({
    name: 'ScaffolderPage',
    component: () => import('./components/Router').then(m => m.Router),
    mountPoint: rootRouteRef,
  }),
);

which creates a routableExtension using the Router.tsx component. The router.tsx contains a const instantiated from a function useCustomFieldExtensions() using either the react router dom => outlet() or props.children.

// https://github.com/backstage/backstage/blob/master/plugins/scaffolder/src/components/Router/Router.tsx#L110-L112
...
  const outlet = useOutlet() || props.children;
  const customFieldExtensions =
    useCustomFieldExtensions<FieldExtensionOptions>(outlet);
...

and

// https://github.com/backstage/backstage/blob/master/plugins/scaffolder-react/src/hooks/useCustomFieldExtensions.ts#L32-L39

import { useElementFilter } from '@backstage/core-plugin-api';
import { FieldExtensionOptions } from '../extensions';
import {
  FIELD_EXTENSION_KEY,
  FIELD_EXTENSION_WRAPPER_KEY,
} from '../extensions/keys';

/**
 * Hook that returns all custom field extensions from the current outlet.
 * @public
 */
export const useCustomFieldExtensions = <
  TComponentDataType = FieldExtensionOptions,
>(
  outlet: React.ReactNode,
) => {
  return useElementFilter(outlet, elements =>
    elements
      .selectByComponentData({
        key: FIELD_EXTENSION_WRAPPER_KEY,
      })
      .findComponentData<TComponentDataType>({
        key: FIELD_EXTENSION_KEY,
      }),
  );
}

Do you think that we could pass the props.children (or register the componentData within the outlet. How ? IDK) to the router using a new parameter created within the janus-idp front-end yaml file and containing ?

@gashcrumb @tumido