janus-idp / backstage-showcase

Enterprise-ready Backstage distribution

Home Page:https://showcase.janus-idp.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

cmoulliard opened this issue · comments

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

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

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.

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.

@christophe-f what do you think ?

  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 ^^

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

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)

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 ?

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:

  • Contribute (with some adaptations) the dynamic frontend plugin system that has been done on Janus side to upstream, and integrate it with this new upstream frontend system
  • Switcg the Janus implementation to use this new upstream dynamic frontend plugin system.

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

  • dynamic frontend plugin system

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

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.

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

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

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 ?

  • customField(s) name => to be loaded from plugin A
  • customField(s) name => to be loaded from plugin T
  • etc

@gashcrumb @tumido