Repository navigation
User task forms and status sites
The cockpit shows your user tasks and your business cases in its lists. What a user sees after clicking one is yours: a form for the task, a page for the case. Your workflow module serves those, and the cockpit loads them into its own screens.
The mechanism is module federation. Your build
produces a remoteEntry.js, the cockpit loads it from your module through its proxy, and pulls the
parts it needs out of it.
-
The four parts
What the cockpit asks for, and the exports it expects to find. -
The build
The plugin configuration, the name which has to match, and the public path. -
The form of a user task
What a form is handed and what it may do with it. -
The status site of a case
The same for a business case, including its user tasks. -
The columns of the two lists
Which columns your module contributes and what a column may say. -
Rendering a cell yourself
When the default cell is not enough. -
Parts of your own
Publishing more than the four, for a cockpit which asks for them. -
When the cockpit cannot load your pages
Reporting work from a system which keeps its own user interface.
Four names, and the cockpit asks for exactly these:
| Exposed module | Named exports the cockpit reads |
|---|---|
UserTaskForm |
UserTaskForm, buildTimestamp
|
WorkflowPage |
WorkflowPage, buildTimestamp
|
UserTaskList |
userTaskListColumns, UserTaskListCell, buildTimestamp
|
WorkflowList |
workflowListColumns, WorkflowListCell, buildTimestamp
|
The names are the contract. A module which exposes UserTaskForm but exports the component under a
different name is loaded and then renders nothing.
buildTimestamp is what says a part has finished loading. Export it from every one of the four, as
a Date, and let your build fill it in. Without it the cockpit keeps waiting for that part.
buildVersion is conventional next to it and useful in your own screens, and the cockpit itself
does not read it.
You do not have to expose all four. A workflow module with no status site exposes three, and the cockpit falls back to its own defaults for what is missing.
The example is development/simulator/src/main/webapp-react in the Git history of the cockpit
repository. It
is a Create React App project with craco on top, and the whole switch
between developing and shipping is one condition in craco.config.js:
webpack: {
configure: {
...(process.env.NODE_ENV !== 'production'
? { entry: './test/index.tsx' } // the dev shell
: { output: { publicPath: '/wm/TestModule/' } }),
},
plugins: {
remove: process.env.NODE_ENV !== 'production'
? []
: [ 'HtmlWebpackPlugin', 'MiniCssExtractPlugin' ],
add: [
new DefinePlugin({
'process.env.BUILD_TIMESTAMP': `'${new Date().toISOString()}'`,
'process.env.BUILD_VERSION': `'${parseVersion()}'`,
}),
...(process.env.NODE_ENV !== 'production'
? []
: [ new ModuleFederationPlugin({
name: "TestModule",
filename: 'remoteEntry.js',
exposes: {
UserTaskList: './src/UserTaskList',
UserTaskForm: './src/UserTaskForm',
WorkflowPage: './src/WorkflowPage',
WorkflowList: './src/WorkflowList',
},
shared: {
react: { singleton: true, requiredVersion: dependencies["react"] },
"react-dom": { singleton: true, requiredVersion: dependencies["react-dom"] },
"react-router-dom": { singleton: true, requiredVersion: dependencies["react-router-dom"] },
},
}) ]),
]
}
},Three details decide whether the cockpit finds anything.
name has to be your workflow module id with every hyphen replaced by an underscore. The cockpit
looks for a global of that name after loading your script, so a module called taxi-ride builds
under taxi_ride.
publicPath has to be the proxy path of your module, /wm/<workflow-module-id>/. The bundle asks
for its own chunks from there, and the cockpit serves it under that path rather than under yours.
The example writes it into the configuration; deriving it from document.currentScript.src at
runtime works too and is one thing less to keep in step.
Share as a singleton what the host already shares. The cockpit's own user interface shares react,
react-dom, react-router-dom, grommet, i18next and react-i18next. A package you declare
which the host does not offer is simply bundled into your module, and a second copy of React is the
one case where that breaks rather than merely costing bytes.
Your configuration then says where the file is:
vanillabp:
workflow-modules:
taxi-ride:
cockpit:
ui-uri-type: WEBPACK_MF_REACT
ui-uri-path: /remoteEntry.jsui-uri-type is a string and the cockpit does not read it. It carries the value to the user
interface, and that one decides what to do with it. VanillaBP knows no names for it. Which names
there are is agreed between your workflow modules and your cockpit user interface.
WEBPACK_MF_REACT above was the value of the user interface which came with the cockpit up to 0.8.
A module which keeps its own pages instead of handing a federated component over reports an address
in ui-uri-path, and the last section of this page is about that.
A value is still required. Write one even if your own user interface could do without it. The day a second value turns up, an empty entry would have to mean one of the two, and nobody can decide afterwards which one.
ui-uri-path is a path and not an address. The cockpit hands it to the user interface the way you
reported it. The user interface puts the proxy route of your module in front of it, which is why
publicPath and this path agree on /wm/<workflow-module-id>/.
import { UserTaskForm } from '@vanillabp/bc-shared';
import { BcUserTask } from '@vanillabp/bc-types';
export const UserTaskForm: UserTaskForm = ({ userTask }) => { ... };One property, the user task. BcUserTask is what the cockpit knows about the task, which is what
your details provider reported, plus a few things the form can do with it: open,
navigateToWorkflow, assign, unassign, claim and unclaim.
Completing the form is not the cockpit's business. Your form calls your own application's API, and
the browser reaches it under /wm/<workflow-module-id>/..., which the cockpit proxies. The session
cookie travels with that request, so your application knows who is filling the form in;
Security says what your side has to do with it.
When your API completed the task, the engine says so, your adapter reports it, and the list updates itself. You do not tell the cockpit anything.
export const WorkflowPage: WorkflowPage = ({ workflow }) => { ... };BcWorkflow carries what was reported about the case, navigateToWorkflow, and getUserTasks,
which fetches the tasks of this case:
const tasks = await workflow.getUserTasks(activeOnly, limitToCurrentUsersPermissions);The second argument is the llatcup question from Security. Pass true and you get the
tasks this user could work on, pass false and you get every task of the case. A page saying where
a case stands usually wants false, a page saying what the reader has to do wants true.
An action on the page, cancelling a case for instance, calls your own API the same way a form does. The cockpit never cancels anything.
A list holds tasks and cases of several workflow modules at once, so each module says which columns it wants. The function is asked once per definition rather than once per row: once per task definition for the task list, once per BPMN process for the case list.
export const userTaskListColumns: ColumnsOfUserTaskFunction = userTask => [
{
title: { en: 'Customer', de: 'Kunde' },
path: 'details.customer.name',
type: 'value',
priority: 3,
width: '10rem',
show: true,
sortable: true,
filterable: true,
resizeable: true,
},
];path reaches into what you reported. details.customer.name is the details map of
Reporting workflows and user tasks, so the column and the
provider method have to agree on the key. path is also what the columns of different definitions
are merged by, and what the cockpit sorts by on the server.
The cockpit sorts and filters by a path below details. only if each part of it holds letters,
digits, _ and -. So details.order-id is fine, while a key with a space or a $ can be shown in
a column but not sorted or filtered by. Give such a column sortable: false and filterable: false.
The other paths a list can be sorted and filtered by are its own fields, like title or dueDate.
title is one text per language. type decides the rendering: value, i18n for a text which is
itself a map of languages, person, date, date-time or time. priority orders the columns,
lowest first. width is a CSS size, and the empty string means "take the rest". show may hide a
column the user can switch on. sortable shows the sort arrows. resizeable lets the user drag the
column's edge.
filterable is declared and set everywhere, and nothing reads it today. A column is not filtered by
writing it.
The cockpit adds its own columns where you named none of them: the selection box, the title, and for user tasks the assignee and the candidates.
Where the value is not what should be shown, export a cell:
export const UserTaskListCell: UserTaskListCell = props => {
const { item, column, defaultCell } = props;
if (column.path === 'details.status') {
return <StatusIcon value={getObjectProperty(item.data, column.path)} />;
}
return defaultCell(props);
};The cell is given the item, the column, the current user, the current language, a translation
function, whether the screen is a phone or a tablet, and defaultCell, which is the cockpit's own
rendering. Handle the columns you care about and pass everything else to defaultCell.
item is a ListItem: the row's data plus its state, which is INITIAL, NEW, UPDATED, ENDED
or REMOVED_FROM_LIST, and whether the user has read it. @vanillabp/bc-shared brings the pieces
for a cell of your own, among them TextListCell, WarningListCell, getObjectProperty and the
two functions picking the colours of a row's state.
A module which exposes no cell gets the default one, and a line in the browser's console says so.
The four are what the cockpit asks for by name. A federated module may expose more, and a cockpit which knows about them can place them in its own screens, a button in the header or an entry in the menu. The part is asked for by the name it is exposed under, and it is handed the workflow module it came from and a function to show a message.
This is how a workflow module contributes something which belongs to no single case, a form starting a new one for instance. The delivered cockpit has no place to put such a part today, so this is for a cockpit of your own, which Building a custom Business Cockpit describes.
Some systems have no federated module to offer. Their user interface was written long before the cockpit and in something other than React, and nobody is going to rebuild it. Such a system can still report its work, and the cockpit then opens its pages instead of loading them:
vanillabp:
workflow-modules:
ticket-system:
cockpit:
ui-uri-type: EXTERNAL
ui-uri-path: https://tickets.example.com/openThe path starts with https://, so the user interface opens it as it is and puts no proxy route in
front of it. EXTERNAL is only a name here. Write the one your cockpit user interface agreed on.
Clicking a task opens that address in a window of its own, and clicking the same task again brings the window that is already open to the front. One address serves every task here, so the user arrives at the other system's own list and looks for the ticket there.
A details provider does better. setUiUriPath says where this one task is handled:
@UserTaskDetailsProvider(taskDefinition = "HANDLE_TICKET")
public UserTaskDetails handleTicket(
final Ticket ticket,
final PrefilledUserTaskDetails details) {
details.setUiUriPath("https://tickets.example.com/ticket/" + ticket.getNumber());
return details;
}PrefilledWorkflowDetails has the same method, for the page which shows the case. What it sets wins
over the configured path, and a provider which sets nothing leaves that path in place.
Two things the cockpit does not do. It does not proxy the other system, so the address has to be one the browser can reach by itself. And it says nothing about the user, so the other system asks who is there in its own way and the user may have to log in a second time.
What the cockpit records stays with the cockpit. Read, claimed and assigned work as they do for any other task, and the other system hears nothing about them. The lists show the columns the cockpit knows for every task, and a column filled with your business data needs a federated module.
