Skip to main content

Template Steps Customization

The Scaffolder Wizard allows you to define a set of input fields displayed to the user, by defining them inside the template.yaml.

To let users fill out the information required by your template, you will need to use value pickers. When you define a new field inside template.yaml, you are also defining the value picker that will help the user to fill the field. Value pickers are then rendered to HTML input fields, using React Json Schema library to render a form from a YAML file, providing an wide set of pickers that may fit your use case. On top of React Json Schema, Witboost provides an additional set of pickers tailored to the platform, allowing to perform more complex operations and enrich your templates leveraging the potential of the platform and its catalog.

Please refer to the Picker section for more information about each Witboost picker.

note

Some of the pickers listed below have an additional property called ui:style that you can use to customize the appearance of the component. By adding some CSS rules to it, you can change the appearance of the single pickers.

Use this feature with care, since it could break the page behaviour.

Most of the pickers listed below can also be hidden by setting their property ui:widget to hidden.

Layout

Object Layout

To group together pickers, you can use an object that will hold all of their values together. When defining an object, you can just list all of its children as properties:

ObjectExample:
type: object
title: My Object
description: an example object
required:
- name
- dataType
properties:
name:
type: string
title: Column Name
dataType:
type: string
default: TEXT
title: Column Data Type
enum:
- TEXT
- NUMBER
- DATE
- BOOLEAN

If you need just to group elements together, without showing a title or a description, you can just set the ui:option called displayTitle to false:

ObjectExample:
type: object
title: My Object
description: an example object
ui:options:
displayTitle: false
required:
- name
- dataType
properties:
name:
type: string
title: Column Name
dataType:
type: string
default: TEXT
title: Column Data Type
enum:
- TEXT
- NUMBER
- DATE
- BOOLEAN

Horizontal Layout

If you want to display different pickers inside an object horizontally instead of vertically, you can simply add the field ui:ObjectFieldTemplate: HorizontalTemplate. In this way, all the pickers inside said object will be displayed one after the other horizontally.

You can also specify additional options using the ui:options value:

  • elementsPerRow: how many elements per row should be displayed (this is honored if the overall width is less than the screen resolution)
  • minElementWidth: the minimum width of every element in the layout (default 200)
  • displayTitle: if false, title and description are not shown even if defined (default true)

As an example, you can display different pickers horizontally with 8 elements per row by defining:

HorizontalExample:
type: object
ui:ObjectFieldTemplate: HorizontalTemplate
ui:options:
elementsPerRow: 8
properties: ...

It could happen that fewer than 8 elements are displayed if the screen does not have 8 * minElementWidth pixels available. To fix this, you can try reducing the minimum elements width to 150:

HorizontalExample:
type: object
ui:ObjectFieldTemplate: HorizontalTemplate
ui:options:
elementsPerRow: 8
minElementWidth: 150
required:
- name
- dataType
properties: ...

You can also use the minElementWidth the other way around: by increasing it over 200, you can make elements go to a new line if there is not enough space for them.

Table Layout

Sometimes, you would like to display pickers that collect inputs needed to fill a table-like structure (e.g. when asking the user to insert a schema for a table). In this case, you usually would like to have an array of elements, each containing multiple values, but rendering that to the user in a friendly way can be very difficult.

To improve this, you can define a layout for such arrays by leveraging two custom layouts:

  • ui:ArrayFieldTemplate: ArrayTableTemplate that should be added to the array component
  • ui:ObjectFieldTemplate: TableRowTemplate that should be added to the items of the array

The resulting definition would be something like:

SchemaExample:
title: Schema Example
description: A Schema Example
type: array
ui:ArrayFieldTemplate: ArrayTableTemplate
ui:options:
maxDescriptionRows: 2
default: []
items:
type: object
ui:ObjectFieldTemplate: TableRowTemplate
required:
- name
- dataType
properties:
name:
type: string
title: Column Name
dataType:
type: string
default: TEXT
title: Column Data Type
enum:
- TEXT
- NUMBER
- DATE
- BOOLEAN
constraint:
type: string
title: Constraint
enum:
- PRIMARY_KEY
- NOT_NULL
- UNIQUE
- NO CONSTRAINT
dataLength:
type: integer
title: Column DataLength
precision:
type: integer
title: Column Precision
minimum: 1
scale:
type: integer
title: Column Scale
minimum: 1

When displaying items in a table, the description of the single fields are moved to the column headers. Since column headers could be stretched in case of long descriptions, you can configure how many lines of description are displayed at maximum by leveraging the ui:option called maxDescriptionRows (default 3).

Conditional Fields

If you need to display a picker only when a condition is met, you can leverage the React Json Form "if-then-else" functionality.

As an example, think about a case where you have a selector for the data type, and in case the selected value is a string, you want the user to insert also its length. You can achieve this behaviour leveraging the allOf feature:

The allOf property must be declared either at the root properties field or in any defined object field where a properties field is declared.

The required property inside the dependent property is necessary to prevent the conditional properties to render when the dependent property has no value, unless there is a conditional property with the same name for each value of the enum of the dependent property.

properties:
dataType:
type: string
title: data type
enum:
- array
- binary
- boolean
- date
- float
- int
- string
- varchar
allOf:
- if:
properties:
dataType:
anyOf:
- const: varchar
- const: string
required: [dataType]
then:
properties:
length:
title: Length
type: number
description: Maximum length of the string

You can also add multiple allOf clauses based on the selected values:

dataType:
type: string
title: data type
enum:
- array
- binary
- boolean
- date
- float
- int
- string
- varchar
allOf:
- if:
properties:
dataType:
anyOf:
- const: varchar
- const: string
required: [dataType]
then:
properties:
length:
title: Length
type: number
description: Maximum length of the string
- if:
properties:
dataType:
const: float
required: [dataType]
then:
properties:
columnScale:
title: Scale
type: number
description: The scale of the floating point number

Validation

Since there are default validations (like checking if the ID of the DP already exists), in order to achieve the best user experience, you need to define name, domain, and identifier in the same page of the template (also there needs to be a dataproduct or parentRef field if you are creating a component definition).

Target repositories

At one point in the creation time, you will need to define on which (remote) location the template is going to be created. Currently, you need to provide either totally empty repository (non-initialized one, this means also without the README file) or non existing ones (which will be created for you).

tip

When using GitLab and providing ExistingGroup/NonExistingGroupOne/NonExistingGroupTwo in the User/Group field, the NonExistingGroupOne and NonExistingGroupTwo will be created automatically (if the token provided has the corresponding rights).

Monorepo or multiple repositories

When creating a new component, you can choose to create it in a monorepo or in a separate repository:

  • when using the monorepo, only one repository is created at system creation time, and all the components are created inside it as sub-folders;
  • when using multiple repositories, a new repository is created for each system and component.

To choose between the two different approaches, you need to configure the component templates by adding the following parameters in the steps:

  • the targetPath parameter in the fetch:template step, which specifies the directory name where the component will be created in the repository;
  • the rootDirectory parameter in the witboostMeshComponent:publish:* step (where * can be github, gitlab, etc), that contains again the directory name where the component will be created.

If the two parameters above are set to '.' (dot), the component will be created in the root of the repository. So, you can choose how the components are organized in the repository:

  • as a monorepo: by setting the repoUrl parameter in the witboostMeshComponent:publish:* step to be the system repository, and then setting the two parameters above (targetPath and rootDirectory) to the same value, which will be the name of the directory;
  • as multiple repositories: by setting the repoUrl parameter in the witboostMeshComponent:publish:* step to eb the new component's repository URL. In this case, the targetPath and rootDirectory parameters should be set to '.' (dot).

As an example, you can define a template that creates a component in a a new repository as follows:

steps:
- id: template
name: Fetch Skeleton + Template
action: fetch:template
input:
url: ./skeleton
targetPath: '.'
values: ...

- id: publish
name: Publish
action: witboostMeshComponent:publish:github
input:
allowedHosts: ['github.com']
repoUrl: ${{ parameters.repoUrl }}
rootDirectory: '.'
parentRef: '${{ parameters.parentRef }}'

where the repoUrl parameter is set to the new component's repository URL.

While to create it in an existing monorepo:

steps:
- id: template
name: Fetch Skeleton + Template
action: fetch:template
input:
url: ./skeleton
targetPath: '${{ parameters.rootDirectory }}'
values: ...

- id: publish
name: Publish
action: witboostMeshComponent:publish:github
input:
allowedHosts: ['github.com']
repoUrl: ${{ parameters.repoUrl }}
rootDirectory: ${{ parameters.rootDirectory }}
parentRef: '${{ parameters.parentRef }}'

where:

  • rootDirectory parameter is set to the name of the directory where the component will be created in the monorepo;
  • repoUrl parameter is set to the parent system repository URL.

Automatic repository selection

If you want to simplify the repository selection for the user, you can avoid adding repository pickers, and auto-generate the repository URL based on the system name and the component name. This is extremely useful when you want to avoid the user to select the repository name, in order to prevent errors or to enforce a specific naming convention.

We will report here some examples of this feature, for monorepos and multiple repositories, and fore some of the most common repository hosts. For all the implementations, will assume that:

  • the system reference is stored in the parentRef parameter
  • the domain reference is stored in the domain parameter
  • the component name is stored in the name parameter
  • the catalog-info.yaml file is stored in the root of the repository
  • the repository owner is ORG_NAME (usually this will be the user or the organization name)

Gitlab new repository

For Gitlab, the component repository will be created in nested groups in the form ORG_NAME/group/sub-group/${domain}/${system}/${component}.

steps:
- id: template
name: Fetch Skeleton + Template
action: fetch:template
input:
url: ./skeleton
targetPath: '.'
values: ...

- id: publish
name: Publish
action: witboostMeshComponent:publish:gitlab
input:
allowedHosts: ['gitlab.com']
description: 'This is ${{ parameters.name }}'
repoUrl: 'gitlab.com?owner=ORG_NAME%2Fgroup%2Fsub-group%2F${{ parameters.domain | replace(r/domain:| |-/, "") }}%2F${{ parameters.parentRef.split(".")[1] | replace(r/ /g, "-") }}&repo=${{ parameters.name.split(" ") | join("-") | lower }}'
rootDirectory: '.'
parentRef: '${{ parameters.parentRef }}'

- id: register
name: Register
action: catalog:register
input:
catalogInfoUrl: 'https://gitlab.com/ORG_NAME/group/sub-group/${{ parameters.domain | replace(r/domain:| |-/, "") }}/${{ parameters.parentRef.split(".")[1] | replace(r/ /g, "-") }}/${{ parameters.name.split(" ") | join("-") | lower }}/-/blob/master/catalog-info.yaml'

Note that the repoUrl parameter is set to the new component's repository URL, and the rootDirectory parameter is set to '.'. Also, the domain, parentRef, and name parameters are pre-processed to remove spaces and special characters, and to join the words together.

Gitlab monorepo

For Github, the component will be created in the system repo (in the form ORG_NAME/group/sub-group/${domain}/${system}) in a directory called ${component}. The relative template.yaml steps section would be:

steps:
- id: template
name: Fetch Skeleton + Template
action: fetch:template
input:
url: ./skeleton
targetPath: '${{ parameters.name.split(" ") | join("-") | lower }}'
values:
...

- id: publish
name: Publish
action: witboostMeshComponent:publish:gitlab
input:
allowedHosts: ['gitlab.com']
description: 'This is ${{ parameters.name }}'
repoUrl: 'gitlab.com?owner=ORG_NAME%2Fgroup%2Fsub-group%2F${{ parameters.domain | replace(r/domain:| |-/, "") }}&repo=${{ parameters.parentRef.split(".")[1] | replace(r/ /g, "-") }}
rootDirectory: '${{ parameters.name.split(" ") | join("-") | lower }}'
parentRef: '${{ parameters.parentRef }}'

- id: register
name: Register
action: catalog:register
input:
catalogInfoUrl: 'https://gitlab.com/ORG_NAME/group/sub-group/${{ parameters.domain | replace(r/domain:| |-/, "") }}/${{ parameters.parentRef.split(".")[1] | replace(r/ /g, "-") }}/-/blob/master/${{ parameters.name.split(" ") | join("-") | lower }}/catalog-info.yaml'

Note that the repoUrl parameter is set to the parent system repository URL, and the rootDirectory parameter is set to the name of the directory where the component will be created in the monorepo. Also, the domain, parentRef, and name parameters are pre-processed to remove spaces and special characters, and to join the words together. In the catalogInfoUrl parameter, the same value used for the rootDirectory is used to define the path to the catalog-info.yaml file.

Github new repository

For Github, the component repository name will be in the form witboost-${domain}-${system}-${component}. The relative template.yaml steps section would be:

steps:
- id: template
name: Fetch Skeleton + Template
action: fetch:template
input:
url: ./skeleton
targetPath: '.'
values: ...

- id: publish
name: Publish
action: witboostMeshComponent:publish:github
input:
allowedHosts: ['github.com']
description: 'This is ${{ parameters.name }}'
repoUrl: 'github.com?owner=ORG_NAME&repo=witboost-${{ parameters.domain | replace(r/domain:| |-/, "") }}-${{ parameters.parentRef.split(".")[1] | replace(r/ |-/g, "") }}-${{ parameters.name.split(" ") | join("") | lower }}'
rootDirectory: '.'
parentRef: '${{ parameters.parentRef }}'

- id: register
name: Register
action: catalog:register
input:
catalogInfoUrl: 'https://github.com/ORG_NAME/witboost-${{ parameters.domain | replace(r/domain:| |-/, "") }}-${{ parameters.parentRef.split(".")[1] | replace(r/ |-/g, "") }}-${{ parameters.name.split(" ") | join("") | lower }}/blob/master/catalog-info.yaml'

Note that the repoUrl parameter is set to the new component's repository URL, and the rootDirectory parameter is set to '.'. Also, the domain, parentRef, and name parameters are pre-processed to remove spaces and special characters, and to join the words together.

Github monorepo

For Github, the component will be created in the system repo (in the form witboost-${domain}-${system}) in a directory called ${component}. The relative template.yaml steps section would be:

steps:
- id: template
name: Fetch Skeleton + Template
action: fetch:template
input:
url: ./skeleton
targetPath: '${{ parameters.name.split(" ") | join("-") | lower }}'
values: ...

- id: publish
name: Publish
action: witboostMeshComponent:publish:github
input:
allowedHosts: ['github.com']
description: 'This is ${{ parameters.name }}'
repoUrl: 'github.com?owner=ORG_NAME&repo=witboost-${{ parameters.domain | replace(r/domain:| |-/, "") }}-${{ parameters.parentRef.split(".")[1] | replace(r/ /g, "-") }}'
rootDirectory: '${{ parameters.name.split(" ") | join("-") | lower }}'
parentRef: '${{ parameters.parentRef }}'

- id: register
name: Register
action: catalog:register
input:
catalogInfoUrl: 'https://github.com/ORG_NAME/witboost-${{ parameters.domain | replace(r/domain:| |-/, "") }}-${{ parameters.parentRef.split(".")[1] | replace(r/ /g, "-") }}/blob/master/${{ parameters.name.split(" ") | join("-") | lower }}/catalog-info.yaml'

Note that the repoUrl parameter is set to the parent system repository URL, and the rootDirectory parameter is set to the name of the directory where the component will be created in the monorepo. Also, the domain, parentRef, and name parameters are pre-processed to remove spaces and special characters, and to join the words together. In the catalogInfoUrl parameter, the same value used for the rootDirectory is used to define the path to the catalog-info.yaml file.

Documentation

You can document any type of entity (Templates, Systems, ...) following this procedure.

Create a mkdocs.yml file in the root of your repository with the following content:

site_name: 'example-docs'
nav:
- Home: index.md

plugins:
- techdocs-core

Update your component's entity description by adding the following lines to its catalog-info.yaml in the root of its repository:

metadata:
annotations:
backstage.io/techdocs-ref: dir:.

The backstage.io/techdocs-ref annotation is used by TechDocs to download the documentation source files for generating an entity's TechDocs site.

Create a /docs folder in the root of your repository with at least an index.md file in it. (If you add more markdown files, make sure to update the nav in the mkdocs.yml file to get proper navigation for your documentation.)

note

Although docs is a popular directory name for storing documentation, it can be renamed to something else and can be configured by mkdocs.yml. See https://www.mkdocs.org/user-guide/configuration/#docs_dir

The docs/index.md can for example have the following content:


# example docs

This is a basic example of documentation.

Custom functions

Inside a template, you can use all filters and functions provided by the Nunjucks templating engine.

In addition to the default functions, Witboost provides some custom functions and filters that can be used in the templates. The custom functions are:

  • concat: concatenates two arrays or strings into a single array; it can be used as {{ concat(array1, array2) }}. The two parameters can be single values, arrays, or a mix of them (even null or undefined values).

The concat function can be useful if you have two different ways of defining the same array property, and you want to merge them into a single array. For example, in an Access Request Template, you could let the users decide if they need to select the identities from groups/users of from the owner of an existing system. You can simply have two properties in the template, and then merge them into a single array using the concat function:

apiVersion: witboost.com/v1
kind: AccessControlRequestTemplate
metadata:
name: access-request-template-users-and-systems
title: Access Request Template
description: Template definition for requesting access to a resource
spec:
type: grant
parameters:
- title: Access Request
required:
- motivation
properties:
type:
title: Select access target
description: Choose if the access should be given to a User/Group or a Data Product
type: string
default: Data Product
enum:
- Data Product
- Users/Groups
allOf:
- if:
properties:
type:
const: Data Product
then:
required:
- dataproduct
- owner
properties:
dataproduct:
title: Data Product
description: Data Product this component belongs to
ui:field: EntitySearchPicker
ui:options:
multiSelection: false
entities:
- type: System
displayField: '{{spec.mesh.name}}'
returnField: full
userFilters:
- search
- domain
- type
columns:
- name: name
path: '{{spec.mesh.name}}'
- name: owner
path: '{{spec.owner}}'
owner:
title: Owner
type: string
description: Data Product owner which will be granted access
ui:field: EntitySelectionPicker
ui:fieldName: dataproduct
ui:property: spec.owner
ui:options:
allowArbitraryValues: false
- if:
properties:
type:
const: Users/Groups
then:
required:
- users
properties:
users:
title: Identities
type: array
description: Select users/groups that you are requesting access
ui:field: IdentitiesPicker
ui:options:
maxIdentities: 5
showOnlyUserMemberGroups: true
allowedKinds:
- user
- group
- if: true
then:
properties:
motivation:
title: Motivation
type: string
description: Motivate your request
ui:options:
multiline: true
rows: 6

steps:
- id: send_request
name: Send Request
action: access-request:send
input:
identities: ${{ concat(parameters.owner, parameters.users) }}
fields:
owner: '${{ parameters.owner }}'
users: '${{ parameters.users }}'
motivation: '${{ parameters.motivation }}'
displayFields:
- title: Motivation
text: '${{ parameters.motivation }}'