Configuration
The configuration service evaluates a series of conditional configuration blocks and determines which blocks to keep in composing a final configuration document for the user interface. The configuration document consists of blocks that define JSON structures that are to be merged into a final JSON configuration that powers the user interface.
The configuration service executes two phases - evaluate
and merge
. It essentially looks like this:
[block1] --> --> [config1] -->
[block2] -->
[block3] --> evaluate merge --> [final configuration]
[block4] --> --> [config4] -->
[block5] --> --> [config5] -->
In the evaluate
phase, the conditional blocks are evaluated to determine whether they should be kept. An evaluator is used and the current runtime state is matched against the given condition. If true, the block portion is carried forward.
In the merge
phase, the carried forward blocks are merged into a final configuration document.
Each conditional block takes on a structure like this:
{
"evaluator": "{evaluatorId}",
"condition": {condition},
"config": {
...
}
}
The config
section consists of any arbitrary JSON. Upon merge, the config
section is merged together with any other config
sections. The merge logic is pretty simple.
- Objects are composed together key by key. Keys that already exist are overwritten by later
config
entries.null
entries result in key removal. - Arrays are additive and append on merge
- All other types are overwrite in nature. The last
config
entry with the key wins.null
removes the key.
Once the merge completes, a final configuration JSON document is assembled. This document is for all intents and purposes normalized. The user interface looks to this document to tell it how to render and what to do. It defines things like:
- What actions to include on the Document Actions page
- What buttons to include in the Selection... dropdown when navigating documents
- What menu options to show on the left-hand side when viewing a project
- What dashlets to show on the platform dashlets page
All of these decisions take into account the context of what is being looked at (i.e. what URI is being looked at and what observables are populated), who is looking at it (i.e. who is logged in) and what their authorities and permissions are. Evaluators play an important role here. Each evaluator is essentially a small bit of very fast executing JavaScript that is responsible for making very quick decisions regarding the above.
The Config Service is built to be fast. It can consider a very large set of blocks and deduce a normalized configuration in a matter of milliseconds.
An Example
To get a sense of how this works, imagine we have three config blocks like this:
Block #1
{
"evaluator": "document-is-file",
"config": {
"document-actions": [{
"id": "custom-preview-in-website"
}]
}
}
Block #2
{
"evaluator": "document-has-type",
"condition": "my:article",
"config": {
"document-actions": [{
"id": "custom-copy-text-to-clipboard"
}]
}
}
Block #3
{
"evaluator": "and",
"condition": [{
"evaluator": "document-is-file"
}, {
"evaluator": "document-has-attachment",
"condition": "scan"
}]
"config": {
"document-actions": [{
"id": "custom-download-scan"
}]
}
}
Each of these blocks provides conditions by which the document-actions
array will be constructed. If all three were to evaluate to true, the resulting config document would look like this:
{
"config": {
"document-actions": [{
"id": "custom-preview-in-website"
}, {
"id": "custom-copy-text-to-clipboard"
}, {
"id": "custom-download-scan"
}]
}
}
However, all three conditions will evaluate separately and will only be true depending on where the user is, who they are and what they're working with.
- If the user were looking at a file, block #1 would be included since it has a
document-is-file
evaluator. Block
#2 might be included if the document is of type my:article
. And block #3 would only be included if the document were both a file and also had a scan
attachment.
- If the user were looking at a folder, only block #2 has a chance of being shown. And it would only show if the folder had the
my:article
node type. Which is possible, depending on how you model your content.
This is a contrived example but one which demonstrates how configuration blocks assemble based on runtime information about what is being viewed. In this case, the document-actions
configuration changes. This configuration drives what menu options appear in the Cloud CMS user interface under the Actions dropdown for a file or a folder.
Replacing Blocks
In addition to the base properties of evaluator
, condition
and config
, you can also use the replace
setting to inform a block that it should override or completely replace any aggregated configuration that was composed prior to loading the current block.
In the example above, suppose that Block #3 had replace set high like this:
{
"evaluator": "and",
"condition": [{
"evaluator": "document-is-file"
}, {
"evaluator": "document-has-attachment",
"condition": "scan"
}]
"config": {
"document-actions": [{
"id": "custom-download-scan"
}]
},
"replace": true
}
If Block #3 loads last, then it will completely overwrite Block #1 and Block #2. In a sense, Block #1 and Block #2 would be thrown out and Block #3 would be the only block loaded.
The replace
true option is useful for overrides where overlaid configurations wish to completely tear down what was configured in the core and override with something entirely custom.
Note that the replace
field is specified at the top level. It indicates that the entire configuration should be replaced.
Updating and Removing Sections
When multiple blocks are loaded for the same configuration space, they are merged together as per the example above. The merge process merges arrays and objects within the JSON. It is sensitive to ordering and allows for replacement if the replace
boolean is used.
The key
field places a special role for elements within the JSON config. When a key
field is encountered, it is expected to uniquely identify the JSON sub-configuration.
For example, the core product ships with a default configuration that looks a bit like this...
{
"config": {
"blocks": [
{
"evaluator": "context-platform",
"config": {
"context": {
"items": [
{
"key": "platform/dashboard",
...
},
{
"key": "platform/resources",
...
},
{
"key": "platform/collaborate",
...
},
{
"key": "platform/developers",
...
},
{
"key": "platform/manage",
...
}
]
}
}
}
]
}
}
Where the key
field identifies each of the configuration sections under the items
array. The ...
indicators simply serve to show where additional configuration would appear.
You can use this key
field to include another configuration block that updates or merges in more configuration. For example, we could add a color
field to the platform/developers
section like this:
{
"config": {
"blocks": [
{
"evaluator": "context-platform",
"config": {
"context": {
"items": [
{
"key": "platform/developers",
"color": "green"
}
]
}
}
}
]
}
}
This is a contrived example once again (setting color
to green
doesn't actually do anything) but it does demonstrate how you can inject that property onto an existing configuration. Other properties on the platform/developers
will be unaffected.
Furthermore, you can remove sections by using the special remove
field. Suppose we wanted to remove the platform/developers
section altogether. We could do something like this:
{
"config": {
"blocks": [
{
"evaluator": "context-platform",
"config": {
"context": {
"items": [
{
"key": "platform/developers",
"remove": true
}
]
}
}
}
]
}
}
This configuration block will cause the platform/developers
element to be spliced out of (or removed from) the array. Other items in the array will not be affected.