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.
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 (default200
)displayTitle
: if false, title and description are not shown even if defined (defaulttrue
)
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 componentui: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.
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).
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 thefetch:template
step, which specifies the directory name where the component will be created in the repository; - the
rootDirectory
parameter in thewitboostMeshComponent: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 thewitboostMeshComponent:publish:*
step to be the system repository, and then setting the two parameters above (targetPath
androotDirectory
) to the same value, which will be the name of the directory; - as multiple repositories: by setting the
repoUrl
parameter in thewitboostMeshComponent:publish:*
step to eb the new component's repository URL. In this case, thetargetPath
androotDirectory
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.)
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 }}'