Skip to main content

frontend

Our frontend (https://github.com/krateoplatformops/frontend) can be defined as a data-driven meta-framework, but what does that mean exactly? Essentially, we created an architecture that ensures a consistent user experience by design. This is achieved by pre-packaging graphic elements and layouts that can be used and composed to build the portal pages.

It's important to note that the Krateo portal should not be seen as a black box, but rather as a central point for collecting valuable information for the platform's users, which is distributed across different systems.

Another important requirement is that the portal must be easily extendable without requiring any coding. All efforts should be focused on understanding how to display services in the catalog, rather than on maintaining forms for collecting information.

In summary, the Krateo frontend queries the backend (Kubernetes) via snowplow to know which layouts and graphic elements must processed with client-side runtime rendering.

Widgets

In Krateo Composable Portal everything is based on the concept of widgets and their composition, a widget is a k8s CRD that maps to a UI element in the frontend (eg a Button) or to a configuration used by other widget (eg a Route)

see all widgets

Anatomy of a widget

A widget source of truth is a JSON schema that is used to generate a CRD, this allow each widget to have it's own Kind and schema validation at the moment of apply example: src/widgets/Button/Button.schema.json

widgetData

Every widget has a widgetData property that contains data used to control how the widget looks like or behave in the Frontend Composable Portal, in this example we are defining a label, an icon (using fontawesome naming convention) and a type that control the the visual style of the button, in the button API references can be seen all possible values.

Let's explore a basic Button widget

# button.yaml
kind: Button
apiVersion: widgets.templates.krateo.io/v1beta1
metadata:
name: button-1
namespace: krateo-system
spec:
widgetData:
label: This is a button
icon: fa-sun
type: primary

widgetDataTemplate

Every widget supports the property spec.widgetDataTemplate that allows overriding a specific value defined in spec.widgetData, this is useful to inject dynamic content inside a widget.

 widgetDataTemplate:
- forPath: data
expression: ${ .namespaces }

widgetDataTemplate accepts an array of objects with forPath and expression keys.

forPath is used to chose what key in widgetData to override, it uses dot notation to reference nested data eg parentProperty.childProperty

expression is a jq expression that uses the result of the jq expression as the data to be injected in the specified path

Simple example

In the example below, the label of the button will be the date when the widget is loaded, as the data from widgetDataTemplate is substituted dynamically at the moment of loading a widget

kind: Button
apiVersion: widgets.templates.krateo.io/v1beta1
metadata:
name: button-post-nginx
namespace: krateo-system
spec:
widgetData:
label: button 1
icon: fa-rocket
type: primary
widgetDataTemplate:
- forPath: label
expression: ${ now | strftime("%Y-%m-%d") }

Complete example

kind: Table
apiVersion: widgets.templates.krateo.io/v1beta1
metadata:
name: table-of-namespaces
namespace: krateo-system
spec:
widgetData:
pageSize: 10
data: []
columns:
- valueKey: name
title: Cluster Namespaces

widgetDataTemplate:
- forPath: data
expression: ${ .namespaces }
apiRef:
name: cluster-namespaces
namespace: krateo-system


---
apiVersion: templates.krateo.io/v1
kind: RESTAction
metadata:
name: cluster-namespaces
namespace: krateo-system
spec:
api:
- name: namespaces
path: "/api/v1/namespaces"
filter: "[.items[] | {name: .metadata.name}]"

In the example above, we declared a table with a single column name to display all namespaces of the cluster. The data is loaded directly from the k8s api server

Hoes does it work?

widgetDataTemplate:
- forPath: data
expression: ${ .namespaces }

What is .namespaces?

In the expression .namespace reference the result of an api called namespaces.

The Table widget has a field spec.apiRef that references a RESTAction by name (cluster-namespaces), an api with name namespaces is declared in the RESTAction's spec.api array

By this chain of references Widget -> apiRef -> RESTAction -> api widgetDataTemplate is able to refecence an api by name

apiVersion: templates.krateo.io/v1
kind: RESTAction
metadata:
name: cluster-namespaces
namespace: krateo-system
spec:
api:
- name: namespaces
path: "/api/v1/namespaces"
filter: "[.items[] | {name: .metadata.name}]"

As shown above, the endpoint called is /api/v1/namespaces which call the k8s api server, if this were an absolute URL it could reference external APIs, see the RESTActions documentation for more details and learning how to authenticate to external APIs.

actions

Actions are a way to declare widget behavious and user interactions.

The currencly supported actions are:

  • rest
  • navigate
  • openDrawer
  • openModal

Widgets can define actions inside widgetData

Rest Action

Used to trigger an HTTP request to a specified resource (mathing the resourceRefId)

PropertyTypeRequiredDescriptionAdditional Info
payloadKeystringNoKey used to nest the payload in the request body
idstringNoUnique identifier for the action
resourceRefIdstringNoThe identifier of the k8s custom resource that should be represented
requireConfirmationbooleanNoWhether user confirmation is required before triggering the action
onSuccessNavigateTostringNoURL to navigate to after successful execution
onEventNavigateToobjectNoConditional navigation triggered by a specific eventadditionalProperties: false
onEventNavigateTo.eventReasonstringYesIdentifier of the awaited event reason
onEventNavigateTo.urlstringYesURL to navigate to when the event is received
onEventNavigateTo.timeoutintegerNoThe timeout in seconds to wait for the eventDefault: 50
loadingstringNoDefines the loading indicator behavior for the actionEnum: ["global", "inline", "none"]
typestringNoType of action to executeEnum: ["rest"]
payloadobjectNoStatic payload sent with the requestadditionalProperties: true
payloadToOverridearrayNoList of payload fields to override dynamicallyArray of objects
payloadToOverride.namestringYesName of the field to override
payloadToOverride.valuestringYesValue to use for overriding the field

Example

This is an example of a button that when clicked, creates a new nginx pod named my-nginx

kind: Button
apiVersion: widgets.templates.krateo.io/v1beta1
metadata:
name: button-post-nginx
namespace: krateo-system
spec:
widgetData:
label: button 1
icon: fa-rocket
type: primary
clickActionId: action-1
actions:
rest:
- id: action-1
resourceRefId: resource-ref-1
type: rest
payload:
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod-789
spec:
containers:
- image: 'nginx:latest'
name: nginx
ports:
- containerPort: 80

resourcesRefs:
- id: resource-ref-1
apiVersion: v1
resource: pods
name: my-nginx
namespace: krateo-system
verb: POST

Navigate to a different URL

PropertyTypeRequiredDescriptionAdditional Info
idstringNoUnique identifier for the action
typestringNoType of navigation actionEnum: ["navigate"]
namestringNoName of the navigation action
resourceRefIdstringNoThe identifier of the k8s custom resource that should be represented
requireConfirmationbooleanNoWhether user confirmation is required before navigating
loadingstringNoDefines the loading indicator behavior during navigationEnum: ["global", "inline", "none"]

OpenDrawer action

Display another widget, referenced by resourceRefId inside a drawer (side panel)

PropertyTypeRequiredDescriptionAdditional Info
idstringNoUnique identifier for the drawer action
typestringNoType of drawer actionEnum: ["openDrawer"]
resourceRefIdstringNoThe identifier of the k8s custom resource that should be represented
requireConfirmationbooleanNoWhether user confirmation is required before opening
loadingstringNoDefines the loading indicator behavior for the drawerEnum: ["global", "inline", "none"]
sizestringNoDrawer size to be displayedEnum: ["default", "large"]
titlestringNoTitle shown in the drawer header

OpenModal action

Display another widget, referenced by resourceRefId inside a modal

PropertyTypeRequiredDescriptionAdditional Info
idstringNoUnique identifier for the modal action
typestringNoType of modal actionEnum: ["openModal"]
namestringNoName of the modal action
resourceRefIdstringNoThe identifier of the k8s custom resource that should be represented
requireConfirmationbooleanNoWhether user confirmation is required before opening
loadingstringNoDefines the loading indicator behavior for the modalEnum: ["global", "inline", "none"]
titlestringNoTitle shown in the modal header

composing widgets

In order to compose complex and more powetful UIs, widgets needs a way to reference other widgets and RESTActions, this is possible via the spec.resourcesRefs property

resourcesRefs

kind: Row
apiVersion: widgets.templates.krateo.io/v1beta1
metadata:
name: my-row
namespace: krateo-system
spec:
widgetData:
items:
- resourceRefId: pie-chart-inside-column
size: 6
- resourceRefId: table-of-pods
size: 18
resourcesRefs:
- id: table-of-pods
apiVersion: widgets.templates.krateo.io/v1beta1
name: table-of-pods
namespace: krateo-system
resource: tables
verb: GET
- id: pie-chart-inside-column
apiVersion: widgets.templates.krateo.io/v1beta1
name: pie-chart-inside-column
namespace: krateo-system
resource: piecharts
verb: GET

In the example above we can see resourcesRefs declaring a list of other resources and a user-defined ID. A widget of kind Row use a matching ID to reference and display other resource, in this example it will display the items in order or declaration, pie-chart-inside-column on top and table-of-pods below regardless of the order of the resourcesRefs.

resourcesRefsTemplate

Similar to widgetDataTemplate, resourcesRefsTemplate allows to populate resourcesRefs with dynamic data coming from an api

kind: Row
apiVersion: widgets.templates.krateo.io/v1beta1
metadata:
name: templates-row
namespace: my-namespace
spec:
apiRef:
name: templates-panels
namespace: my-namespace
widgetData:
items: []
widgetDataTemplate:
- forPath: items
expression: >
${ [ .templatespanels[] | { resourceRefId: .metadata.name, size: 12 } ] }
resourcesRefs: []
resourcesRefsTemplate:
- iterator: ${ .templatespanels }
template:
id: ${ .metadata.name }
apiVersion: ${ .apiVersion }
resource: panels
namespace: ${ .metadata.namespace }
name: ${ .metadata.name }
verb: GET

In the example above resourcesRefsTemplate declares an iterator, that loop over the result of an api called templatespanels and populate resourcesRefs with it. if resourcesRefs has some manually filled items they will be merged with the result of resourcesRefsTemplate

As a quick recap of what is happing:

  • the widget references a RESTAction with name templates-panels in apiRef
  • templates-panels RESTAction declares an api called templatespanels
  • resourcesRefsTemplate's iterator uss the result of templatespanels to populate them items that will be part of resourcesRefs

Widgets API reference

An api reference listing all widgets and their widgetData is available at widgets-api-reference.md