SimpleSearch JSON Breakdown

The SimpleSearch widget is designed to do simple lookup and display of information from backends implementing the same query style as mod-agreements and mod-licenses.

This page walks through the setup for SimpleSearch, in order to illustrate how a widget is developed.

This page is intended to help illustrate how a Type/Definition interaction looks, in order to aid with the writing and maintaining of new WidgetDefinitions for any WidgetType. If you're looking for specific documentation on the fields available to you in the SimpleSearch type, or how widgets of this type look/work, refer instead to this page.

WidgetType

This is going to dig a bit deeper into the type JSON for SimpleSearch. The WidgetType is quite large, so the following explanation is broken down into component sections which describe what they do and how they can be used to define a WidgetDefinition which conforms to this Type.

Please note that this is still under development and subject to change.

At the top level, the schema for the SimpleSearch WidgetType (which does not include the fields at the level above for name/version) looks like the following:

{
	"$schema": "http://json-schema.org/draft-07/schema#",
	"title": "SimpleSearch widget",
	"type": "object",
	"description": "SimpleSearch widget type",
	"additionalProperties": false,
	"required": ["baseUrl", "results"],
	"properties": { ... }
}

This specifies some simple information about the type, as well as any required information a WidgetDefinition must contain in order to be a valid Definition for this Type. This is where anything the frontend absolutely requires for basic functionality has to be contained.

Examining some of the properties:

"properties": {
	"baseUrl": {
		"type": "string",
		"description": "The base url queries built with this widget will go to"
	},
	"results": {
		"$ref": "#/$defs/results"
	},
	"filters": {
		"$ref": "#/$defs/filters"
	},
	"sort": {
		"$ref": "#/$defs/sort"
	},
	"configurableProperties": {
		"$ref": "#/$defs/configurableProperties"
	}
}

As can be seen, some of the properties may be very simple fields, such as baseUrl, and some of the properties may require more complicated objects. In this case, most of the top-level properties require more detailed information, so to keep the JSON schema as efficient and flat as possible, this information is split out into a "$defs" section.

Looking at the "results" property:

"results": {
	"type": "object",
	"title": "Results",
	"description": "Contains all the information the dashboard needs to fetch and parse results",
	"additionalProperties": false,
	"required": ["columns"],
	"properties": {
		"columns": {
			"type": "array",
			"items": { "$ref": "#/$defs/resultColumn" }
		}
	}
},
"resultColumn": {
	"type": "object",
	"title": "Result column",
	"description": "Describes the columns to be made available in the output",
	"additionalProperties": false,
	"required": ["accessPath", "name", "valueType"],
	"properties": {
		"accessPath": {
			"type": "string",
			"description": "a string defining the path to the specified object property"
		},
		"label": {
			"type": "string",
			"description": "an optional string prescribing the display label of the field"
		},
		"name": {
			"type": "string",
			"description": "a string defining the name of the property for this column"
		},
		"valueType": {
			"type": "string",
			"enum": ["String", "Integer", "Float", "Boolean", "Date"],
			"description": "a string defining the type of property we are displaying"
		}
	}
}

All of this information is to specify that the shape for this property must look like the following:

results: {
	columns: [
		{
			accessPath: "path.to.property.on.object",
			name: "name of property",
			label: "display label of field, can be overwritten",
			valueType: "String"
		},
		...
	]
}

(here we've only filled in one index of the columns array, normally we'd expect more options).

The frontend can then take this information and, as part of the configuration form, offer the options to users as potential fields they can display as part of their results table. Based on the valueType, we can display the results using a formatter, like FormattedUTCDate for Dates, or ✓/✖ for Boolean values.

SimpleSearch also has similar fields for `filters` and `sort`, determining the fields which a user can define filters on, and sort on. The valueType in the filter column will be used to decide what kind of field entry the user will see in the widget creation form. For a Date they will see a FOLIO datepicker, for a String they will see a TextField etc.

Finally looking at some of the other options the SimpleSearch WidgetType gives us for the Definitions:

"configurableProperties": {
	"type": "object",
	"title": "Other properties",
	"description": "A collection of other properties which can be configured/made configurable",
	"additionalProperties": false,
	"properties": {
		"urlLink": {
			"$ref": "#/$defs/urlLink"
		},
		"numberOfRows": {
			"$ref": "#/$defs/numberOfRows"
		}
	}
}

These properties provide a place to hold other configurable fields, where we know things such as what type of data we're expecting. The urlLink will be used to display a link at the bottom of the simpleSearch widget, which the user can configure to take them to the same query in the SASQ screen of whatever app is relevant. The numberOfRows is how we specify a maximum number of rows for the fetch to return. See below for how this would be configured in a definition.

WidgetDefinition

A WidgetDefinition ready to be accepted into the database for this type might look like the following:

{
	"type": {
		"name": "SimpleSearch",
		"version": "1.0"
	},
	"version": "1.0",
	"name":"ERM Agreements",
	"definition": {
		...
	}
}

(This top level shape will be shared by definitions for ALL widget types)

Notice that text fields are used here for the "type" field. These will be used to attempt to resolve which type this definition conforms to (if, after resolution, we do not find the relevant type, or the "definition" field fails validation then this will error out).

There is also a "version" field. This will be used to resolve definitions in case breaking changes are made to a widgetDefinition, eg columns removed or API paths changing. The idea is that widgets created using the old version of the widgetDefinition will not immediately fail (the appropriate action to take in this scenario, perhaps rendering a red "attention needed" widget in place of out of date ones or a simple warning MessageBanner, will be later determined by user use cases and stories).

The definition section is what contains the meat of the WidgetDefinition, the actual JSON conforming to the Type.

{
	"baseUrl":"/erm/sas",
	"results": {
		"columns": [
			{
				"name":"agreementName",
				"label": "Agreement name",
				"accessPath":"name",
				"valueType": "String"
			},
			{
				"name":"startDate",
				"label": "Start date",
				"accessPath":"startDate",
				"valueType": "Date"
			},
			{
				"name":"endDate",
				"label": "End date",
				"accessPath":"endDate",
				"valueType": "Date"
			},
			{
				"name":"agreementStatus",
				"label": "Status",
				"accessPath":"agreementStatus.value",
				"valueType": "String"
			}
		]
	},
	"filters": {
		"columns": [
			{
				"name":"agreementName",
				"label": "Agreement name",
				"filterPath":"name",
				"valueType": "String",
				"comparators": ["=~", "!~"]
			},
			{
				"name":"agreementStatus",
				"label": "Agreement status",
				"filterPath":"agreementStatus.value",
				"valueType": "Enum",
				"enumValues": [
					{"value": "active", "label": "Active"},
					{"value": "closed", "label": "Closed"},
					{"value": "in_negotiation", "label": "In negotiation"}
				],
				"comparators": ["==", "!="]
			},
			{
				"name":"startDate",
				"label": "Start date",
				"filterPath":"startDate",
				"valueType": "Date",
				"comparators": ["==", "!=", ">", ">=", "<", "<="]
			},
			{
				"name":"endDate",
				"label": "End date",
				"filterPath":"endDate",
				"valueType": "Date",
				"comparators": ["==", "!=", ">", ">=", "<", "<=", "isNull", "isNotNull"]
			},
			{
				"name":"internalContact",
				"label": "Internal contact",
				"filterPath":"contacts.user",
				"valueType": "UUID",
				"comparators": ["==", "!="]
			}
		]
	},
	"sort": {
		"columns": [
			{
				"name":"id",
				"sortPath":"id",
				"sortTypes": ["asc"]
			},
			{
				"name":"agreementName",
				"sortPath":"name",
				"sortTypes": ["asc", "desc"]
			},
			{
				"name":"startDate",
				"sortPath":"startDate",
				"sortTypes": ["asc", "desc"]
			},
			{
				"name":"endDate",
				"sortPath":"endDate",
				"sortTypes": ["asc", "desc"]
			}
		]
	},
	"configurableProperties": {
		"urlLink": {
			"configurable": true
		},
		"numberOfRows": {
			"configurable": false,
			"defValue": 5
		}
	}
}

There is a considerable amount of information in the widget definition. The key aspects are:

Results

This section is all about display in the final table. To that end the value types allowed are all from the following list 

["String", "Integer", "Float", "Boolean", "Date", "Link"]

Notice for example that the valueType for agreementStatus is "String", whereas later on the same path is described as an "Enum". The filters are described in more detail below, but it is important to note that a valueType String means that the resulting field will be displayed as simple text in the resulting table.

Likewise "Boolean" and "Date" types are used to drive display of either a FormattedUTCDate, or a tick/cross for true/false, and "Link" is used to attempt to generate

These columns are then used to populate a component which offers the ability to pick one or several of these to form the columns for our display table in the output. The label will be the default label displayed for that column, although this can be overwritten.

Filters

This section drives both how the filter sections are created dynamically as well as how the filter part of the final query is built. Looking first at the agreementStatus entry:

{
	"name":"agreementStatus",
	"label": "Agreement status",
	"filterPath":"agreementStatus.value",
	"valueType": "Enum",
	"enumValues": [
		{"value": "active", "label": "Active"},
		{"value": "closed", "label": "Closed"},
		{"value": "in_negotiation", "label": "In negotiation"}
	],
	"comparators": ["==", "!="]
}

As before there is a name and label, used to help the user while they build their widget configuration in the form. In this case it is specified that the agreement status has type "Enum". This is because agreementStatus is not a string, but actually a closed list of possible values. This setup will mean that the user will see a dropdown list of the specified values.

At some point a more complicated WidgetType (or version of SimpleSearch) maybe offered that can look up these values at the point where the user is building their widget.

Since ultimately this is saved as a JSON string, it wouldn't break anything to specify this as a "String" type. However that would necessitate that users know each of the possible values for this field, as well as any differences between the filter value and how it's displayed to them usually (i.e. the UI label used for the value). Equally it wouldn't break anything to put type Date, but it would then render a datePicker component and return strings of form "2021-03-19".

For example, it would be possible to specify a valueType "Enum" for n set Dates in a date field, say: 

"enumValues": [
	{"value": "2020-12-25", "label": "Christmas 2020"},
	{"value": "2019-12-25", "label": "Christmas 2019"},
	{"value": "2018-12-25", "label": "Christmas 2018"},
],

This illustrates that the actual type of the underlying data isn't what is being specified here, instead these properties all relate to the configurable form and display in the UI.

The final part of this section is the comparators. For SimpleSearch these can be chosen from the list (although this is at present not enforced):

["==", "!=", "=~", "!~", ">", ">=", "<", "<=", "isNull", "isNotNull"]

since those are the comparators understood by KIWT style endpoints. Obviously not all of these make sense in all circumstances, but the Definition does not care, and will dutifully offer all of the comparators specified without question, even though "status <= 'abc'" is clearly nonsense.

Sort

The sort block is the simplest of the three, 

{
	"name":"endDate",
	"sortPath":"endDate",
	"sortTypes": ["asc", "desc"]
}

This simply needs the path, and what sort options (in this case ascending/descending) are being offered. If there is only one sortType for a single sortEntry then no options are offered to the user in the form and that single value is used instead.

Configurable Properties

These are a bit different from the above, instead being for individual configurable fields with specific purposes. The way that SimpleSearch type is set up is that these can either be set to be user-configurable or not in the WidgetDefinition.

"configurableProperties": {
	"urlLink": {
		"configurable": true
	},
	"numberOfRows": {
		"configurable": false,
		"defValue": 5
	}
}

Here, the urlLink is set to be configurable, whereas numberOfRows is non-configruable, and has a defaultValue. This will remove that entry on the dynamic widget form. The Type schema is set up to enforce defValue if configurable is set to false. In the case where a defValue is set and configurable is set to true this will manifest as a default value in the initial form creation.