Creating Custom Components¶
About Custom Components¶
Functions, Widgets, Card Components, DataTypes, and Plugins have overlapping functionality and architecture. These constructs are used to add extra functionality to Arches upon data entry or data visualization.
At a high level, if you’re trying to add a custom interface for entering business data, you’ll use a Widget. If you need to enter and display a type of data unknown to Arches, you’ll need a new DataType to go with your Widget. If you need sophisticated side effects or to perform additional computation based on the data entered, it’s time for a Function.
Note
To develop these components, you’ll need familiarity with Django templates, intermediate skills in the Python programming language, and some basic familiarity with the Knockout.js front-end framework.
Functions¶
Functions are the most powerful extension to Arches. Functions associated with a Resource are called during various CRUD operations, and have access to any server-side model. Proficient Python/Django developers will find few limitations extending an Arches Project with Functions.
Function must be created, registered, and then associated with a Resource Model.
Creating a Function¶
A Function comprises three separate files, which should be seen as front-end/back-end complements. On the front-end, you will need a component made from a Django HTML template and JavaScript pair, which should share the same basename.
In your Project, these files must be placed like so:
/myproject/myproject/media/js/views/components/functions/spatial_join.js
/myproject/myproject/templates/views/components/functions/spatial_join.htm
The third file is a Python file which contains a dictionary telling Arches some important details about your Function, as well as its main logic.
/myproject/myproject/functions/spatial_join.py
Note
As in the example above, its advisable that all of your files share the same basename. (If your Function is moved into a Package, this is necessary.) A new Project should have an example function in it whose files you can copy to begin this process.
Defining the Function’s Details¶
The first step in creating a function is defining the details
that
are in the top of your Function’s .py
file.
details = {
'name': 'Sample Function',
'type': 'node',
'description': 'Just a sample demonstrating node group selection',
'defaultconfig': {"selected_nodegroup":""},
'classname': 'SampleFunction',
'component': 'views/components/functions/sample-function'
}
- name
Required Name is used to unregister a function, and shows up in the
fn list
command.- type
Required As of version 4.2, this should always be set to
node
- description
Optional Add a description of what your Function does.
- defaultconfig
Required A JSON object with any configuration needed to serve your function’s logic
- classname
Required The name of the python class that holds this Function’s logic.
- component
Required Canonical path to html/js component.
More about the defaultconfig
field¶
Any configuration information you need your Function to access can be
stored here. If your function needs to calculate something based on
the value of an existing Node, you can refer to it here. Or, if you
want your Function to e-mail an administrator whenever a specific node
is changed, both the Node ID and the email address to be used are good
candidates for storage in the defaultconfig
dictionary.
The defaultconfig
field serves both as a default, and as your
user-defined schema for your function’s configuration data. Your
front-end component for the function will likely collect some of this
configuration data from the user and store it in the config
attribute of the pertinent FunctionXGraph
.
Writing your Function Logic¶
In your Function’s Python code, you have access to all your server-side models. You’re basically able to extend Arches in any way you please. You may want to review the Data Model documentation.
Function Hooks¶
Your function needs to extend the BaseFunction
class. Depending on
what you are trying to do, you will need to implement the get
,
save
, delete
, on_import
, and/or after_function_save
methods.
class MyFunction(BaseFunction):
def get(self):
raise NotImplementedError
def save(self, tile, request):
raise NotImplementedError
def delete(self, tile, request):
raise NotImplementedError
def on_import(self, tile):
raise NotImplementedError
def after_function_save(self, functionxgraph, request):
raise NotImplementedError
Note
Not all of these methods are called in the current Arches
software. You can also leave any of them unimplemented, and the
BaseFunction
class will raise a NotImplementedError
for
you. Arches is designed to gracefully ignore these exceptions for
functions.
A detailed description of current functionality is below.
save
and delete
¶
The Tile
object will look up all its Graph’s associated Functions
upon being saved. Before writing to the database, it calls each
function’s save
method, passing itself along with the Django
Request
object. This is likely where the bulk of your function’s
logic will reside.
The Tile
object similarly calls each of its graph’s
functions’ delete
methods with the same parameters. Here, you can
execute any cleanup or other desired side effects of a Tile’s
deletion. Your delete
implementation will have the same signature
as save
.
after_function_save
¶
The Graph view passes a FunctionXGraph object to
after_function_save
, along with the request.
The FunctionXGraph object has a config
attribute which stores that
instance’s version of the defaultconfig
dictionary. This is a good
opportunity, for example, to programmatically manipulate the
Function’s configuration based on the Graph or any other server-side
object.
You can also write any general logic that you’d like to fire upon the assignment of a Function to a Resource.
on_import
¶
The import module calls on_import if the file format is a JSON-formatted Arches file, and passes an associated Tile object.
CSV imports do not call this hook.
The UI Component¶
Having implemented your function’s logic, it’s time to develop the front-end components required to associate it with Resources and provide any configuration data.
The component you develop here will be rendered in the Resource Manager when you associate the function with a Resource, and this is where you’ll put any forms or other UI artifacts used to configure the Function.
Developing your Function’s UI component is very similar to developing
Widgets. More specific guidelines are in progress, but for now,
refer to the sample code in your project’s
templates/views/components/functions/
directory, and gain a little
more insight from the templates/views/components/widgets/
directory. The complementary JavaScript examples will be located in
media/js/views/components/functions/
and
media/js/views/components/widgets
directories.
Registering Functions¶
First, list the names of functions you already have registered:
(ENV)$ python manage.py fn list
Now you can register your new function with
(ENV)$ python manage.py fn register --source <path to your function's .py file>
For example:
(ENV)$ python manage.py fn register --source /Documents/projects/mynewproject/mynewproject/functions/sample_function.py
Now navigate to the Function Manager in the Arches Designer to confirm that your new function is there and functional. If it’s not, you may want to unregister your function, make additional changes, and re-register it. To unregister your function, simply run
(ENV)$ python manage.py fn unregister --name 'Sample Function'
All commands are listed in Command Line Reference - Function Commands.
Widgets¶
Widgets allow you to customize how data of a certain DataType is entered into Arches, and further customize how that data is presented in Reports. You might have several Widgets for a given DataType, depending on how you want the Report to look or to match the context of a certain Resource.
Widgets are primarily a UI artifact, though they are closely tied to their underlying DataType.
To develop a custom Widget, you’ll need to write three separate files, and place them in the appropriate directories. For the appearance and behavior of the Widget, you’ll need a component made of a Django template and JavaScript file placed like so:
project_name/templates/views/components/widgets/sample-widget.htm
project_name/media/js/views/components/widgets/sample-widget.js
To register and configure the Widget, you’ll need a JSON configuration file:
project_name/widgets/sample-widget.json
Configuring your Widget¶
To start, here is a sample Widget JSON file:
{
"name": "sample-widget",
"component": "views/components/widgets/sample-widget",
"defaultconfig": {
"x_placeholder":"Longitude",
"y_placeholder":"Latitude"
},
"helptext": null,
"datatype": "sample-datatype"
}
The most important field here is the datatype
field. This controls
where your Widget will appear in the Arches Resource Designer. Nodes
each have a DataType, and Widgets matching that DataType will be
available when you’re designing your Cards. The value must match an
existing DataType within Arches.
You can also populate the defaultconfig
field with any
configuration data you wish, to be used in your Widget’s front-end
component.
Designing Your Widget¶
Your Widget’s template needs to include three Django template “blocks” for rendering the Widget in different contexts within Arches. These blocks are called form, config_form, and report. As you might guess from their names, form is rendered when your Widget appears on a Card for business data entry, config_form is rendered when you configure the Widget on a card when designing a Resource, and report controls how data from your Widget is presented in a Report.
Here is an example:
{% extends "views/components/widgets/base.htm" %}
{% load i18n %}
{% block form %}
<div class="row widget-wrapper">
<div class="form-group">
<label class="control-label widget-input-label" for="" data-bind="text:label"></label>
<div class="col-xs-12">
<input type="number" data-bind="textInput: x_value, attr: {placeholder: x_placeholder}" class="form-control input-lg widget-input" style="margin-bottom: 5px">
</div>
<div class="col-xs-12">
<input type="number" data-bind="textInput: y_value, attr: {placeholder: y_placeholder}" class="form-control input-lg widget-input" style="margin-bottom: 5px">
</div>
<div class="col-xs-12">
<input type="text" data-bind="textInput: srid" class="form-control input-lg widget-input" style="margin-bottom: 5px">
</div>
<div class="col-xs-12">
<label class="control-label widget-input-label" for="">Preview</label>
<input disabled type="text" data-bind="textInput: preview" class="form-control input-lg widget-input">
</div>
</div>
</div>
{% endblock form %}
{% block config_form %}
<div class="control-label">
{% trans "X Coordinate Placeholder" %}
</div>
<div class="col-xs-12 crud-widget-container">
<input type="" placeholder="{% trans "Placeholder" %}" id="" class="form-control input-md widget-input" data-bind="textInput: x_placeholder">
</div>
<div class="control-label">
{% trans "Y Coordinate Placeholder" %}
</div>
<div class="col-xs-12 crud-widget-container">
<input type="" placeholder="{% trans "Placeholder" %}" id="" class="form-control input-md widget-input" data-bind="textInput: y_placeholder">
</div>
{% endblock config_form %}
{% block report %}
<dt data-bind="text: label"></dt>
<dd>
<div style='margin-bottom:2px' data-bind="text: value"></div>
</dd>
{% endblock report %}
To pull it all together, you’ll need to write a complementary
JavaScript file. The Arches UI uses Knockout.js, and the best way to
develop your Widget in a compatible way is to write a Knockout
component with a viewModel
corresponding to your Widget’s view
(the Django template).
Here is an example, continuing with our sample-widget
:
define(['knockout', 'underscore', 'viewmodels/widget'], function (ko, _, WidgetViewModel) {
/**
* registers a text-widget component for use in forms
* @function external:"ko.components".text-widget
* @param {object} params
* @param {string} params.value - the value being managed
* @param {function} params.config - observable containing config object
* @param {string} params.config().label - label to use alongside the text input
* @param {string} params.config().placeholder - default text to show in the text input
*/
return ko.components.register('sample-widget', {
viewModel: function(params) {
params.configKeys = ['x_placeholder','y_placeholder'];
WidgetViewModel.apply(this, [params]);
var self = this;
if (this.value()) {
var coords = this.value().split('POINT(')[1].replace(')','').split(' ')
var srid = this.value().split(';')[0].split('=')[1]
this.x_value = ko.observable(coords[0]);
this.y_value = ko.observable(coords[1]);
this.srid = ko.observable('4326');
} else {
this.x_value = ko.observable();
this.y_value = ko.observable();
this.srid = ko.observable('4326');
};
this.preview = ko.pureComputed(function() {
var res = "SRID=" + this.srid() + ";POINT(" + this.x_value() + " " + this.y_value() + ")"
this.value(res);
return res;
}, this);
},
template: { require: 'text!templates/views/components/widgets/sample-widget.htm' }
});
});
Registering your Widget¶
After placing your Django template and JavaScript files in their respective directories, you are now ready to register your Widget:
python manage.py widget register --source /Documents/projects/mynewproject/mynewproject/widgets/sample-widget.json
The command will confirm your Widget has been registered, and you can also see it with:
python manage.py widget list
If you make an update to your Widget, you can load the changes to Arches with:
python manage.py widget update --source /Documents/projects/mynewproject/mynewproject/widgets/sample-widget.json
All the Widget commands are detailed in Command Line Reference - Widget Commands.
DataTypes¶
A DataType defines a type of business data. DataTypes are associated with Nodes and Widgets. When you are designing your Cards, the Widgets with the same DataType as the Node you are collecting data for will be available. In your Branches, each Node with a DataType will honor the DataType configuration you specify when you create it.
The simplest (non-configurable, non-searchable) DataTypes consist of a single Python file. If you want to provide Node-specific configuration to your DataType (such as whether to expose a Node with that DataType to Advanced Search or how the data is rendered), you’ll also develop a UI component comprising a Django template and JavaScript file.
In your Project, these files must be placed accordingly:
Optional Configuration Component:
/myproject/myproject/media/js/views/components/datatypes/sample_datatype.js
/myproject/myproject/templates/views/components/datatypes/sample_datatype.htm
DataType File:
/myproject/myproject/datatypes/sample_datatype.py
To begin, let’s examine the sample-datatype
included with Arches:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | from arches.app.datatypes.base import BaseDataType
from arches.app.models import models
from arches.app.models.system_settings import settings
sample_widget = models.Widget.objects.get(name="sample-widget")
details = {
"datatype": "sample-datatype",
"iconclass": "fa fa-file-code-o",
"modulename": "datatypes.py",
"classname": "SampleDataType",
"defaultwidget": sample_widget,
"defaultconfig": {"placeholder_text": ""},
"configcomponent": "views/components/datatypes/sample-datatype",
"configname": "sample-datatype-config",
"isgeometric": False,
"issearchable": False,
}
class SampleDataType(BaseDataType):
def validate(self, value, row_number=None, source=None):
errors = []
try:
value.upper()
except:
errors.append(
{
"type": "ERROR",
"message": "datatype: {0} value: {1} {2} {3} - {4}. {5}".format(
self.datatype_model.datatype, value, row_number, source, "this is not a string", "This data was not imported.",
),
}
)
return errors
def append_to_document(self, document, nodevalue, nodeid, tile):
document["strings"].append({"string": nodevalue, "nodegroup_id": tile.nodegroup_id})
def transform_export_values(self, value, *args, **kwargs):
if value != None:
return value.encode("utf8")
def get_search_terms(self, nodevalue, nodeid=None):
terms = []
if nodevalue is not None:
if settings.WORDS_PER_SEARCH_TERM == None or (len(nodevalue.split(" ")) < settings.WORDS_PER_SEARCH_TERM):
terms.append(nodevalue)
return terms
def append_search_filters(self, value, node, query, request):
try:
if value["val"] != "":
match_type = "phrase_prefix" if "~" in value["op"] else "phrase"
match_query = Match(field="tiles.data.%s" % (str(node.pk)), query=value["val"], type=match_type,)
if "!" in value["op"]:
query.must_not(match_query)
query.filter(Exists(field="tiles.data.%s" % (str(node.pk))))
else:
query.must(match_query)
except KeyError, e:
pass
|
Writing Your DataType¶
Your DataType needs, at minimum, to implement the validate
method. You’re also likely to implement the
transform_import_values
or transform_export_values
methods. Depending on whether your DataType is spatial, you may need
to implement some other methods as well. If you want to expose Nodes
of your DataType to Advanced Search, you’ll also need to implement the
append_search_filters
method.
You can get a pretty good idea of what methods you need to implement
by looking at the BaseDataType
class in the Arches source code
located at arches/app/datatypes/base.py
and below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | import json
from django.core.urlresolvers import reverse
from arches.app.models import models
class BaseDataType(object):
def __init__(self, model=None):
self.datatype_model = model
def validate(self, value, row_number=None, source=None):
return []
def append_to_document(self, document, nodevalue, nodeid, tile):
"""
Assigns a given node value to the corresponding key in a document in
in preparation to index the document
"""
pass
def after_update_all(self):
"""
Refreshes mv_geojson_geoms materialized view after save.
"""
pass
def transform_import_values(self, value, nodeid):
"""
Transforms values from probably string/wkt representation to specified
datatype in arches
"""
return value
def transform_export_values(self, value, *args, **kwargs):
"""
Transforms values from probably string/wkt representation to specified
datatype in arches
"""
return value
def get_bounds(self, tile, node):
"""
Gets the bounds of a geometry if the datatype is spatial
"""
return None
def get_layer_config(self, node=None):
"""
Gets the layer config to generate a map layer (use if spatial)
"""
return None
def should_cache(self, node=None):
"""
Tells the system if the tileserver should cache for a given node
"""
return False
def should_manage_cache(self, node=None):
"""
Tells the system if the tileserver should clear cache on edits for a
given node
"""
return False
def get_map_layer(self, node=None):
"""
Gets the array of map layers to add to the map for a given node
should be a dictionary including (as in map_layers table):
nodeid, name, layerdefinitions, isoverlay, icon
"""
return None
def clean(self, tile, nodeid):
"""
Converts '' values to null when saving a tile.
"""
if tile.data[nodeid] == '':
tile.data[nodeid] = None
def get_map_source(self, node=None, preview=False):
"""
Gets the map source definition to add to the map for a given node
should be a dictionary including (as in map_sources table):
name, source (json)
"""
tileserver_url = reverse('tileserver')
if node is None:
return None
source_config = {
"type": "vector",
"tiles": ["%s/%s/{z}/{x}/{y}.pbf" % (tileserver_url, node.nodeid)]
}
count = None
if preview == True:
count = models.TileModel.objects.filter(data__has_key=str(node.nodeid)).count()
if count == 0:
source_config = {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"total": 1
},
"geometry": {
"type": "Point",
"coordinates": [
-122.4810791015625,
37.93553306183642
]
}
},
{
"type": "Feature",
"properties": {
"total": 100
},
"geometry": {
"type": "Point",
"coordinates": [
-58.30078125,
-18.075412438417395
]
}
},
{
"type": "Feature",
"properties": {
"total": 1
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-179.82421875,
44.213709909702054
],
[
-154.16015625,
32.69486597787505
],
[
-171.5625,
18.812717856407776
],
[
-145.72265625,
2.986927393334876
],
[
-158.37890625,
-30.145127183376115
]
]
}
},
{
"type": "Feature",
"properties": {
"total": 1
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-50.9765625,
22.59372606392931
],
[
-23.37890625,
22.59372606392931
],
[
-23.37890625,
42.94033923363181
],
[
-50.9765625,
42.94033923363181
],
[
-50.9765625,
22.59372606392931
]
]
]
}
},
{
"type": "Feature",
"properties": {
"total": 1
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-27.59765625,
-14.434680215297268
],
[
-24.43359375,
-32.10118973232094
],
[
0.87890625,
-31.653381399663985
],
[
2.28515625,
-12.554563528593656
],
[
-14.23828125,
-0.3515602939922709
],
[
-27.59765625,
-14.434680215297268
]
]
]
}
}
]
}
}
return {
"nodeid": node.nodeid,
"name": "resources-%s" % node.nodeid,
"source": json.dumps(source_config),
"count": count
}
def get_pref_label(self, nodevalue):
"""
Gets the prefLabel of a concept value
"""
return None
def get_display_value(self, tile, node):
"""
Returns a list of concept values for a given node
"""
return unicode(tile.data[str(node.nodeid)])
def get_search_terms(self, nodevalue, nodeid=None):
"""
Returns a nodevalue if it qualifies as a search term
"""
return []
def append_search_filters(self, value, node, query, request):
"""
Allows for modification of an elasticsearch bool query for use in
advanced search
"""
pass
def handle_request(self, current_tile, request, node):
"""
Updates files
"""
pass
|
the validate
method¶
Here, you write logic that the Tile model will use to accept or reject a Node’s data before saving. This is the core implementation of what your DataType is and is not.
The validate
method returns an array of errors. If the array is
empty, the data is considered valid. You can populate the errors array
with any number of dictionaries with a type
key and a message
key. The value for type
will generally be ERROR
, but you can
provide other kinds of messages.
the append_search_filters
method¶
In this method, you’ll create an ElasticSearch query Nodes matching this datatype based on input from the user in the Advanced Search screen. (You design this input form in your DataType’s front-end component.)
Arches has its own ElasticSearch query DSL builder class.
You’ll want to review that code for an idea of what to do. The search
view passes your DataType a Bool() query from this class, which you
call directly. You can invoke its must
, filter
, should
, or
must-not
methods and pass complex queries you build with the DSL
builder’s Match
class or similar. You’ll execute this search
directly in your append_search_filters
method.
In-depth documentation of this part is planned, but for now, look at the core datatypes located in Arches’ source code for examples of the approaches you can take here.
Note
If you’re an accomplished Django developer, it should also be
possible to use Elastic’s own Python DSL builder in your Project
to build the complex search logic you’ll pass to Arches’ Bool()
search, but this has not been tested.
Configuring your DataType¶
You’ll need to populate the details
dictionary to configure your
new DataType.
details = {
"datatype": "sample-datatype",
"iconclass": "fa fa-file-code-o",
"modulename": "datatypes.py",
"classname": "SampleDataType",
"defaultwidget": sample_widget,
"defaultconfig": {"placeholder_text": ""},
"configcomponent": "views/components/datatypes/sample-datatype",
"configname": "sample-datatype-config",
"isgeometric": False,
"issearchable": False,
- datatype
Required The name of your datatype. The convention in Arches is to use kebab-case here.
- iconclass
Required The FontAwesome icon class your DataType should use. Browse them here.
- modulename
Required This should always be set to
datatypes.py
unless you’ve developed your own Python module to hold your many DataTypes, in which case you’ll know what to put here.- classname
Required The name of the Python class implementing your datatype, located in your DataType’s Python file below these details.
- defaultwidget
Required The default Widget to be used for this DataType.
- defaultconfig
Optional You can provide user-defined default configuration here.
- configcomponent
Optional If you develop a configuration component, put the fully-qualified name of the view here. Example:
views/components/datatypes/sample-datatype
- configname
Optional The name of the Knockout component you have registered in your UI component’s JavaScript file.
- isgeometric
Required Used by the Arches UI to determine whether to create a Map Layer based on the DataType, and also for caching. If you’re developing such a DataType, set this to True.
- issearchable
Optional Determines if the datatype participates in advanced search. The default is false.
Important
configcomponent
and configname
are required together.
Developing the Configuration Component¶
Your component JavaScript file should register a Knockout component
with your DataType’s configname
. This component should be an
object with two keys: viewModel
, and template
The value for viewModel
should be a function where you put the
logic for your template. You’ll be setting up Knockout observable and
computed values tied to any form elements you’ve developed to collect
Advanced Search or Node-level configuration information from the user.
The value for template
should be another object with the key
require
, and the value should be
text!datatype-config-templates/<your-datatype-name>
. Arches will
know what to do with this – it comes from the value you supplied in
your Python file’s details
dictionary for configcomponent
.
Pulling it all together, here’s the JavaScript portion of Arches’
date
DataType.
define(['knockout'], function (ko) {
var name = 'date-datatype-config';
ko.components.register(name, {
viewModel: function(params) {
var self = this;
this.search = params.search;
if (this.search) {
var filter = params.filterValue();
this.viewMode = 'days';
this.op = ko.observable(filter.op || '');
this.searchValue = ko.observable(filter.val || '');
this.filterValue = ko.computed(function () {
return {
op: self.op(),
val: self.searchValue()
}
}).extend({ throttle: 750 });
params.filterValue(this.filterValue());
this.filterValue.subscribe(function (val) {
params.filterValue(val);
});
}
},
template: { require: 'text!datatype-config-templates/date' }
});
return name;
});
Advanced Search Rendering¶
If you’re supporting Advanced Search functionality for Nodes with your
DataType, your Django template will include a search
block,
conditionally rendered by Knockout.js if the search view is
active. Here’s the one from the boolean
datatype:
<!-- ko if: $data.search -->
{% block search %}
<div class="col-sm-12">
<select class="resources" data-bind="value: searchValue, chosen: {width: '100%', disable_search_threshold: 15}, options: [{id:'t', name:trueLabel}, {id:'f', name:falseLabel}], optionsText: 'name', optionsValue: 'id'">
</select>
</div>
{% endblock search %}
<!-- /ko -->
Note the <!-- ko if: $data.search -->
directive opening and
closing the search block. This is not an HTML comment – it’s
Knockout.js-flavored markup for the conditional rendering.
Arches’ built-in date
DataType does not use the Django template
block
directive, but only implements advanced search, and contains
a more sophisticated example of the component logic needed:
{% load i18n %}
<!-- ko if: $data.search -->
<div class="col-md-4 col-lg-3">
<select class="resources" tabindex="-1" style="display: none;" data-bind="value: op, chosen: {width: '100%', disable_search_threshold: 15}">
<option value="eq"> = </option>
<option value="gt"> > </option>
<option value="lt"> < </option>
<option value="gte"> >= </option>
<option value="lte"> <= </option>
</select>
</div>
<div class="col-md-8 col-lg-9">
<input type="" placeholder="{% trans "Date" %}" class="form-control input-md widget-input" data-bind="value: searchValue, datepicker: {format: 'YYYY-MM-DD', viewMode: viewMode, minDate: false, maxDate: false}">
</div>
<!-- /ko -->
Node-specific Configuration¶
This section of your template should be enclosed in Knockout-flavored
markup something like: <!-- ko if: $data.graph -->
, and in your
Knockout function you should follow the convention and end up with
something like if (this.graph) {
Here, you put form elements corresponding to any configuration you’ve
implemented in your DataType. These should correspond to keys in your
DataType’s defaultconfig
.
Arches’ boolean
DataType has the following defaultconfig
:
{'falseLabel': 'No', 'trueLabel': 'Yes'}
You can see the corresponding data bindings in the Django template:
<!-- ko if: $data.graph -->
<div class="control-label">
{% trans "Label 'True'" %}
</div>
<div class="col-xs-12 pad-no crud-widget-container">
<input type="" id="" class="form-control input-md widget-input" data-bind="value: trueLabel, valueUpdate: 'keyup'">
</div>
<div class="control-label">
{% trans "Label 'False'" %}
</div>
<div class="col-xs-12 pad-no crud-widget-container">
<input type="" id="" class="form-control input-md widget-input" data-bind="value: falseLabel, valueUpdate: 'keyup'">
</div>
<!-- /ko -->
And finally, here is the boolean
DataType’s JavaScript file in its entirety:
define(['knockout'], function (ko) {
var name = 'boolean-datatype-config';
ko.components.register(name, {
viewModel: function(params) {
var self = this;
this.search = params.search;
this.graph = params.graph;
this.trueLabel = params.config ? params.config.trueLabel : params.node.config.trueLabel;
this.falseLabel = params.config ? params.config.falseLabel : params.node.config.falseLabel;
if (this.search) {
var filter = params.filterValue();
this.searchValue = ko.observable(filter.val || '');
this.filterValue = ko.computed(function () {
return {
val: self.searchValue()
}
});
params.filterValue(this.filterValue());
this.filterValue.subscribe(function (val) {
params.filterValue(val);
});
}
},
template: { require: 'text!datatype-config-templates/boolean' }
});
return name;
});
Registering your DataType¶
These commands are identical to working with Widgets, but you use the
word datatype
instead. Please refer to Command Line Reference - Widget Commands.
Card Components¶
Beginning in Arches 4.3, Cards are rendered using Card Components, allowing them to be composed and nested arbitrarily in various contexts within the Arches UI. Arches comes with a default Card Component that should suit most needs, but you can also create and register custom Card Components to extend the front-end behavior of Arches.
Developing Card Components is very similar to developing Widgets. A Card Component consists of a Django template and Knockout.js JavaScript file. To register your component, you’ll also need a JSON file specifying its initial configuration.
To develop your new card, you’ll place files like so in your project:
project_name/templates/views/components/cards/my-new-card.htm
project_name/media/js/views/components/cards/my-new-card.js
To register and configure the Component, you’ll need a JSON configuration file:
project_name/cards/my-new-card.json
Creating a Card Component¶
The default template and Knockout files illustrate everything a Card Component needs, and you’ll be extending this functionality. Your template will provide conditional markup for various contexts (‘editor-tree’, ‘designer-tree’, ‘permissions-tree’, ‘form’, and ‘report’), render all the card’s Widgets, and display other information.
Here’s the template for the default Card Component:
{% load i18n %}
<!-- ko foreach: { data: [$data], as: 'self' } -->
<!-- ko if: state === 'editor-tree' -->
<li role="treeitem card-treeitem" class="jstree-node" data-bind="css: {'jstree-open': (card.tiles().length > 0 && card.expanded()), 'jstree-closed' : (card.tiles().length > 0 && !card.expanded()), 'jstree-leaf': card.tiles().length === 0}, scrollTo: card.scrollTo, container: '.resource-editor-tree'">
<i class="jstree-icon jstree-ocl" role="presentation" data-bind="click: function(){card.expanded(!card.expanded())}"></i>
<a class="jstree-anchor" href="#" tabindex="-1" data-bind="css:{'filtered': card.highlight(), 'jstree-clicked': card.selected, 'child-selected': card.isChildSelected()}, click: function () { card.canAdd() ? card.selected(true) : card.tiles()[0].selected(true) },">
<i class="fa fa-file-o" role="presentation" data-bind="css:{'filtered': card.highlight(), 'has-provisional-edits fa-file': card.doesChildHaveProvisionalEdits()}"></i>
<span style="padding-right: 5px;" data-bind="text: card.model.name"></span>
<!-- ko if: card.canAdd() -->
<i class="fa fa-plus-circle add-new-tile" role="presentation" data-bind="css:{'jstree-clicked': card.selected}" data-toggle="tooltip" data-original-title="{% trans "Add New" %}"></i>
<!-- /ko -->
</a>
<ul class="jstree-children" aria-expanded="true">
<div data-bind="sortable: {
data: card.tiles,
beforeMove: self.beforeMove,
afterMove: card.reorderTiles
}">
<li role="treeitem" class="jstree-node" data-bind="css: {'jstree-open': (cards.length > 0 && expanded), 'jstree-closed' : (cards.length > 0 && !expanded()), 'jstree-leaf': cards.length === 0}">
<i class="jstree-icon jstree-ocl" role="presentation" data-bind="click: function(){expanded(!expanded())}"></i>
<a class="jstree-anchor" href="#" tabindex="-1" data-bind="click: function () { self.form.selection($data) }, css:{'jstree-clicked': selected, 'child-selected': isChildSelected(), 'filtered-leaf': card.highlight()}">
<i class="fa fa-file" role="presentation" data-bind="css:{'has-provisional-edits': doesChildHaveProvisionalEdits() || $data.hasprovisionaledits()}"></i>
<strong style="margin-right: 10px;">
<!-- ko if: card.widgets().length > 0 -->
<span data-bind="text: card.widgets()[0].label || card.model.name"></span>:
<div style="display: inline;" data-bind="component: {
name: self.form.widgetLookup[card.widgets()[0].widget_id()].name,
params: {
tile: $data,
node: self.form.nodeLookup[card.widgets()[0].node_id()],
config: self.form.widgetLookup[card.widgets()[0].widget_id()].config,
label: self.form.widgetLookup[card.widgets()[0].widget_id()].label,
value: $data.data[card.widgets()[0].node_id()],
type: 'resource-editor',
state: 'display_value'
}
}"></div>
<!-- /ko -->
<!-- ko if: card.widgets().length === 0 -->
<span data-bind="text: card.model.name"></span>
<!-- /ko -->
</strong>
</a>
<!-- ko if: cards.length > 0 -->
<ul class="jstree-children" aria-expanded="true" data-bind="foreach: {
data: cards,
as: 'card'
}">
<!-- ko component: {
name: self.form.cardComponentLookup[card.model.component_id()].componentname,
params: {
state: 'editor-tree',
card: card,
tile: null,
loading: self.loading,
form: self.form
}
} --> <!-- /ko -->
</ul>
<!-- /ko -->
</li>
</div>
<!-- /ko -->
</ul>
</li>
<!-- /ko -->
<!-- ko if: state === 'designer-tree' -->
<li role="treeitem card-treeitem" class="jstree-node" data-bind="css: {'jstree-open': ((card.cards().length > 0 || card.widgets().length > 0) && card.expanded()), 'jstree-closed' : ((card.cards().length > 0 || card.widgets().length > 0) && !card.expanded()), 'jstree-leaf': card.cards().length === 0 && card.widgets().length === 0}, scrollTo: card.scrollTo, container: '.designer-card-tree'">
<i class="jstree-icon jstree-ocl" role="presentation" data-bind="click: function(){card.expanded(!card.expanded())}"></i>
<a class="jstree-anchor" href="#" tabindex="-1" data-bind="css:{'filtered': card.highlight(), 'jstree-clicked': card.selected, 'child-selected': card.isChildSelected()}, click: function () { card.selected(true) },">
<i class="fa fa-file-o" role="presentation"></i>
<span style="padding-right: 5px;" data-bind="text: card.model.name"></span>
</a>
<!-- ko if: card.cards().length > 0 || card.widgets().length > 0 -->
<ul class="jstree-children card-designer-tree" aria-expanded="true">
<div data-bind="sortable: {
data: card.widgets,
as: 'widget',
beforeMove: self.beforeMove,
afterMove: function() { card.model.save() }
}">
<li role="treeitem" class="jstree-node jstree-leaf" data-bind="css: {
'jstree-last': $index() === (card.widgets().length - 1) && card.cards().length === 0
}">
<i class="jstree-icon jstree-ocl" role="presentation"></i>
<a class="jstree-anchor" href="#" tabindex="-1" data-bind="click: function() { widget.selected(true) }, css:{'jstree-clicked': widget.selected, 'hover': widget.hovered}, event: { mouseover: function(){ widget.hovered(true) }, mouseout: function(){ widget.hovered(null) } }">
<i data-bind="css: widget.datatype.iconclass" role="presentation"></i>
<strong style="margin-right: 10px;" >
<span data-bind="text: !!(widget.label()) ? widget.label() : widget.node.name"></span>
</strong>
</a>
</li>
</div>
<div data-bind="sortable: {
data: card.cards,
as: 'childCard',
beforeMove: self.beforeMove,
afterMove: function() {
card.reorderCards();
}
}">
<div data-bind="css: {
'jstree-last': ($index() === (card.cards().length - 1))
}">
<!-- ko component: {
name: self.form.cardComponentLookup[childCard.model.component_id()].componentname,
params: {
state: 'designer-tree',
card: childCard,
tile: null,
loading: self.loading,
form: self.form
}
} --> <!-- /ko -->
</div>
</div>
</ul>
<!-- /ko -->
</li>
<!-- /ko -->
<!-- ko if: state === 'permissions-tree' -->
<li role="treeitem card-treeitem" class="jstree-node" data-bind="css: {'jstree-open': ((card.cards().length > 0 || card.widgets().length > 0) && card.expanded()), 'jstree-closed' : ((card.cards().length > 0 || card.widgets().length > 0) && !card.expanded()), 'jstree-leaf': card.cards().length === 0 && card.widgets().length === 0}">
<i class="jstree-icon jstree-ocl" role="presentation" data-bind="click: function(){card.expanded(!card.expanded())}"></i>
<a class="jstree-anchor permissions-card" href="#" tabindex="-1" data-bind="css:{'jstree-clicked': card.selected, 'child-selected': card.isChildSelected()}, click: function () { card.selected(true) },">
<i class="fa fa-file-o" role="presentation"></i>
<span style="padding-right: 5px;" data-bind="text: card.model.name, css:{'filtered': card.highlight()}">
</span>
<span class="node-permissions">
<!--ko if: card.perms -->
<!-- ko foreach: card.perms() -->
<i class="node-permission-icon" data-bind="css: $data.icon"></i>
<!-- /ko -->
<!-- /ko -->
</span>
</a>
<!-- ko if: card.cards().length > 0 || card.widgets().length > 0 -->
<ul class="jstree-children" aria-expanded="true">
<div data-bind="sortable: {
data: card.widgets,
as: 'widget',
beforeMove: self.beforeMove,
afterMove: function() { card.model.save() }
}">
<li role="treeitem" class="jstree-node jstree-leaf" data-bind="css: {
'jstree-last': $index() === (card.widgets().length - 1) && card.cards().length === 0
}">
<i class="jstree-icon jstree-ocl" role="presentation"></i>
<a class="jstree-anchor permissions-widget" href="#" tabindex="-1">
<i class="fa fa-file" role="presentation" ></i>
<strong style="margin-right: 10px;" >
<span data-bind="text: !!(widget.label()) ? widget.label() : widget.node.name"></span>
</strong>
</a>
</li>
</div>
<div data-bind="foreach: {
data: card.cards,
as: 'card'
}">
<!-- ko component: {
name: self.form.cardComponentLookup[card.model.component_id()].componentname,
params: {
state: 'permissions-tree',
card: card,
tile: null,
loading: self.loading,
form: self.form,
multiselect: true
}
} --> <!-- /ko -->
</div>
</ul>
<!-- /ko -->
</li>
<!-- /ko -->
<!-- ko if: state === 'form' -->
<div class="card-component">
<!--ko if: reviewer && provisionalTileViewModel.selectedProvisionalEdit() -->
<div class="edit-message-container">
<span>{% trans 'Currently showing edits by' %}</span>
<span class="edit-message-container-user" data-bind="text: provisionalTileViewModel.selectedProvisionalEdit().username() + '.'"></span>
<!--ko if: !provisionalTileViewModel.tileIsFullyProvisional() -->
<a class="reset-authoritative" href='' data-bind="click: function(){provisionalTileViewModel.resetAuthoritative();}">{% trans 'Return to approved edits' %}</a>
<!--/ko-->
<!--ko if: provisionalTileViewModel.selectedProvisionalEdit().isfullyprovisional -->
<span>{% trans ' This is a new contribution by a provisional editor.' %}</span>
<!--/ko-->
</div>
<!--/ko-->
<!--ko if: reviewer && provisionalTileViewModel.provisionaledits().length > 0 && !provisionalTileViewModel.selectedProvisionalEdit()-->
<div class="edit-message-container approved">
<div>{% trans 'Currently showing the most recent approved edits' %}</div>
</div>
<!--/ko-->
<div class="new-provisional-edit-card-container">
<!--ko if: reviewer && provisionalTileViewModel.provisionaledits().length > 0 -->
<!--ko if: !provisionalTileViewModel.tileIsFullyProvisional() -->
<div class='new-provisional-edits-list'>
<div class='new-provisional-edits-header'>
<div class='new-provisional-edits-title'>{% trans 'Provisional Edits' %}</div>
<div class="btn btn-shim btn-danger btn-labeled btn-xs fa fa-trash new-provisional-edits-delete-all" style="padding: 3px;" data-bind="click: function(){provisionalTileViewModel.deleteAllProvisionalEdits()}">{% trans 'Delete all edits' %}</div>
</div>
<!--ko foreach: { data: provisionalTileViewModel.provisionaledits(), as: 'pe' } -->
<div class='new-provisional-edit-entry' data-bind="css: {'selected': pe === $parent.provisionalTileViewModel.selectedProvisionalEdit()}, click: function(){$parent.provisionalTileViewModel.selectProvisionalEdit(pe)}">
<div class='title'>
<div class='field'>
<span data-bind="text : pe.username"></span>
</div>
<a href='' class='field fa fa-times-circle new-delete-provisional-edit' data-bind="click : function(){$parent.provisionalTileViewModel.rejectProvisionalEdit(pe)}"></a>
</div>
<div class="field timestamp">
<span data-bind="text : pe.displaydate">@</span>
<span data-bind="text : pe.displaytimestamp"></span>
</div>
</div>
<!-- /ko -->
</div>
<!--/ko-->
<!--/ko-->
<div class="card">
<h4 data-bind="text: card.model.name"></h4>
<h5 data-bind="text: card.model.instructions"></h5>
<!-- ko if: card.widgets().length > 0 -->
<form class="widgets" style="margin-bottom: 20px;">
<div data-bind="foreach: {
data:card.widgets, as: 'widget'
}">
<div data-bind='component: {
name: self.form.widgetLookup[widget.widget_id()].name,
params: {
formData: self.tile.formData,
tile: self.tile,
form: self.form,
config: widget.configJSON,
label: widget.label(),
value: self.tile.data[widget.node_id()],
node: self.form.nodeLookup[widget.node_id()],
expanded: self.expanded,
graph: self.form.graph,
type: "resource-editor"
}
}, css:{ "active": widget.selected, "hover": widget.hovered, "widget-preview": self.preview
}, click: function(data, e) { if (!widget.selected() && self.preview) {widget.selected(true);}
}, event: { mouseover: function(){ if (self.preview){widget.hovered(true) } }, mouseout: function(){ if (self.preview){widget.hovered(null)} } }'></div>
</div>
</form>
<!-- /ko -->
<!-- ko if: card.widgets().length === 0 -->
<ul class="card-summary-section" data-bind="css: {disabled: !tile.tileid}">
<!-- ko foreach: { data: tile.cards, as: 'card' } -->
<li class="card-summary">
<a href="javascript:void(0)" data-bind="click: function () {
if (card.parent.tileid) {
card.canAdd() ? card.selected(true) : card.tiles()[0].selected(true);
}
}">
<h4 class="card-summary-name">
<span data-bind="text: card.model.name"></span>
<!-- ko if: card.canAdd() && card.parent.tileid -->
<i class="fa fa-plus-circle card-summary-add"></i>
<!-- /ko -->
</h4>
</a>
<ul class="tile-summary-item" data-bind="foreach: {
data: card.tiles,
as: 'tile'
}">
<li class="tile-summary">
<a href="#" data-bind="click: function () { tile.selected(true) }">
<!-- ko if: card.widgets().length > 0 -->
<span data-bind="text: card.widgets()[0].label || card.model.name" class="tile-summary-label"></span>:
<div style="display: inline;" data-bind="component: {
name: self.form.widgetLookup[card.widgets()[0].widget_id()].name,
params: {
tile: tile,
node: self.form.nodeLookup[card.widgets()[0].node_id()],
config: self.form.widgetLookup[card.widgets()[0].widget_id()].config,
label: self.form.widgetLookup[card.widgets()[0].widget_id()].label,
value: tile.data[card.widgets()[0].node_id()],
type: 'resource-editor',
state: 'display_value'
}
}"></div>
<!-- /ko -->
<!-- ko if: card.widgets().length === 0 -->
<span data-bind="text: card.model.name"></span>
<!-- /ko -->
</a>
</li>
</ul>
</li>
<!-- /ko -->
</ul>
<!-- /ko -->
<div class="install-buttons">
<!-- ko if: tile.tileid -->
<button class="btn btn-shim btn-warning btn-labeled btn-lg fa fa-trash" data-bind="click: function () { self.form.deleteTile(tile); }">{% trans 'Delete this record' %}</button>
<!-- /ko -->
<!-- ko if: tile.dirty() -->
<!-- ko if: provisionalTileViewModel && !provisionalTileViewModel.tileIsFullyProvisional() -->
<button class="btn btn-shim btn-danger btn-labeled btn-lg fa fa-times" data-bind="click: tile.reset">{% trans 'Cancel edit' %}</button>
<!-- /ko -->
<!-- ko if: tile.tileid -->
<button class="btn btn-shim btn-mint btn-labeled btn-lg fa fa-plus" data-bind="click: function () { self.form.saveTile(tile); }">{% trans 'Save edit' %}</button>
<!-- /ko -->
<!-- /ko -->
<!-- ko if: !tile.tileid -->
<button class="btn btn-shim btn-mint btn-labeled btn-lg fa fa-plus" data-bind="click: function () { self.form.saveTile(tile); }">{% trans 'Add' %}</button>
<!-- /ko -->
</div>
</div>
</div>
</div>
<!-- /ko -->
<!-- ko if: state === 'report' -->
<div class="rp-card-section">
<span class="rp-tile-title" data-bind="text: card.model.get('name')"></span>
<!-- ko foreach: { data: card.tiles, as: 'tile' } -->
<div class="rp-card-section">
<!-- ko if: card.model.get('widgets')().length > 0 -->
<div class="rp-report-tile" data-bind="attr: { id: tile.tileid }">
<dl class="dl-horizontal">
<!-- ko foreach: { data: card.model.get('widgets'), as: 'widget' } -->
<!-- ko component: {
name: widget.widgetLookup[widget.get("widget_id")()].name,
params: {
config: configJSON,
label: widget.get("label")(),
node: widget.node,
value: tile.data[widget.node.nodeid],
state: "report"
}
} --><!-- /ko -->
<!-- /ko -->
</dl>
</div>
<!-- /ko -->
<div class="rp-report-container-tile" data-bind="visible: card.cards().length > 0">
<!-- ko foreach: { data: tile.cards, as: 'card' } -->
<!-- ko component: {
name: card.model.cardComponentLookup[card.model.component_id()].componentname,
params: {
state: 'report',
card: card
}
} --> <!-- /ko -->
<!-- /ko -->
</div>
</div>
<!-- /ko -->
<!-- ko if: card.tiles().length === 0 -->
<div class="row rp-report-tile rp-no-data">
<!-- ko ifnot: card.model.get('cardid') -->
{% trans "Sorry, you don't have access to this information" %}
<!-- /ko -->
<!-- ko if: card.model.get('cardid') -->
{% trans "No data added yet for" %} "<span data-bind="text: card.model.get('name')"></span>"
<!-- /ko -->
</div>
<!-- /ko -->
</div>
<!-- /ko -->
<!-- /ko -->
And here’s the Knockout file:
define(['knockout', 'bindings/scrollTo'], function(ko) {
var viewModel = function(params) {
this.state = params.state || 'form';
this.preview = params.preview;
this.loading = params.loading || ko.observable(false);
this.card = params.card;
this.tile = params.tile;
if (this.preview) {
if (!this.card.newTile) {
this.card.newTile = this.card.getNewTile();
}
this.tile = this.card.newTile;
}
this.form = params.form;
this.provisionalTileViewModel = params.provisionalTileViewModel;
this.reviewer = params.reviewer;
this.expanded = ko.observable(true);
this.beforeMove = function(e) {
e.cancelDrop = (e.sourceParent!==e.targetParent);
};
};
return ko.components.register('default-card', {
viewModel: viewModel,
template: {
require: 'text!templates/views/components/cards/default.htm'
}
});
});
Registering your Card Component¶
To register your Component, you’ll need a JSON configuration file looking a lot like this sample:
{
"name": "My New Card",
"componentid": "eea17d6c-0c32-4536-8a01-392df734de1c",
"component": "/views/components/cards/my-new-card",
"componentname": "my-new-card",
"description": "An awesome new card that does wonderful things.",
"defaultconfig": {}
}
- componentid
Optional A UUID4 for your Component. Feel free to generate one in advance if that fits your workflow; if not, Arches will generate one for you and print it to STDOUT when you register the Component.
- name
Required The name of your new Card Component, visible in the drop-down list of card components in the Arches Designer.
- description
Required A brief description of your component.
- component
Required The path to the component view you have developed. Example:
views/components/cards/sample-datatype
- componentname
Required Set this to the last part of component above.
- defaultconfig
Required You can provide user-defined default configuration here. Make it a JSON dictionary of keys and values. An empty dictionary is acceptable.
Card Commands¶
To register your Card Component, use this command:
python manage.py card_component register --source /Documents/projects/mynewproject/mynewproject/cards/new-card-component.json
The command will confirm your Component has been registered, and you can also see it with:
python manage.py card_component list
If you make an update to your Card Component, you can load the changes to Arches with:
python manage.py card_component update --source /Documents/projects/mynewproject/mynewproject/cards/new-card-component.json
All the Card Component commands are detailed in Command Line Reference - Card Component Commands.
Plugins¶
Plugins allow a developer to create an independent page in Arches that is accessible from the main navigation menu. For example, you may need a customized way of visualize your resource data. A plugin would enable you to design such an interface. Plugins, like widgets and card components rely only on front-end code. Ajax queries, generally calls to the API, must be used to access any server side data.
Registering your Plugin¶
To register your Plugin, you’ll need a JSON configuration file looking a lot like this sample:
{
"pluginid": "b122ede7-24a6-4fc5-a3cc-f95bfa28b1cf",
"name": "Sample Plugin",
"icon": "fa fa-share-alt",
"component": "views/components/plugins/sample-plugin",
"componentname": "sample-plugin",
"config": {},
"slug": "sample-plugin",
"sortorder": 0
}
- pluginid
Optional A UUID4 for your Plugin. Feel free to generate one in advance if that fits your workflow; if not, Arches will generate one for you and print it to STDOUT when you register the Plugin.
- name
Required The name of your new Plugin, visible when a user hovers over the main navigation menu
- icon
Required The icon visible in the main navigation menu.
- component
Required The path to the component view you have developed. Example:
views/components/plugins/sample-plugin
- componentname
Required Set this to the last part of component above.
- config
Required You can provide user-defined default configuration here. Make it a JSON dictionary of keys and values. An empty dictionary is acceptable.
- slug
Required The string that will be used in the url to access your plugin
- sortorder
Required The order in which your plugin will be listed if there are multiple plugins
Plugin Commands¶
To register your Plugin, use this command:
python manage.py plugin register --source /Documents/projects/mynewproject/mynewproject/plugins/sample-plugin.json
The command will confirm your Plugin has been registered, and you can also see it with:
python manage.py plugin list
If you make an update to your Plugin, you can load the changes to Arches with:
python manage.py plugin update --source /Documents/projects/mynewproject/mynewproject/plugins/sample-plugin.json