Table of Contents
The library feature in Node-RED comes in two parts:
- the flow library is exposed via the drop-down menu :
import->library->...
- within nodes that allow part of their config to be saved to a type-specific library, such as Function and Template
The current UX is fairly bad for the flow library. The drop-down menu is hard to navigate, closes too easily and doesn't work well beyond one or two levels of hierarchy.
The purpose of this work is to overhaul the library experience, bringing together the two different types of library into one unified design.
Some notes on what is needed:
- a single sidebar tab for the library that provides a file-browser like interface
- support for different types of library entities (flows, functions, templates, etc)
- allow the library to not only get its content from the 'local library' we have today, but also allow custom library sources be provided
- custom library sources should either be identified in settings.js (a la palette catalogues), or allow the user to provide a library url to use (a la Eclipse p2 repo model).
HTTP Admin API
We don't currently document the library API as part of the admin http api. That gives us some small amount of freedom to redesign it.
The current library http API is inconsistent; flows
have a slightly different behaviour to other library types. This change will ensure the API is consistent for all types.
GET /library/[type]
This returns a full listing of the given types in the library.
The format of the response is a recursive structure of entry objects with path
and name
elements. If it represents a directory, it also has a children
property that is an array of entry objects:
{
"path": "the full path this level represents",
"name": "the name of this entity"
"children": [
{
"path": ...
"name": ...
"children": [ ]
}
]
}
GET /library/[type]/[path]
path
must resolve to an entry in the library.
The response is an object with the following properties:
{
path: "/full/path/to/the/entryName,
name: "entryName"
meta: {
a: "optional meta data about the entry"
},
body: "the main body of the entry"
}
POST /library/[type]/[path]
Saves the provided entry to the library. The format is the same as that for the GET request above. The path
entry is taken from the url, not the request body.
Storage API
The underlying Storage API needs to change to accommodate these changes.
The problem is with Storage.getLibraryEntry(type,name)
.
In its current form, if name
is a directory, it is expected to return a listing of the files and directories, including any metadata about the files. This is a problem as that metadata (such as outputs
count on Functions) is contained inside the file; in order to return the listing, every file must be read in part to retrieve its metadata.
We need three functions:
- return a listing of all items of a given type
- return the contents of a given type
- save a type
That means adding a new function that returns the full listing.
To ensure backwards compatibility, the storage layer will inspect to see what functions the plugin provides and drive them appropriately.
Storage.getLibraryEntries(type)
New: Returns the full listing of the give types in the library in the format described above.
Storage.getLibraryEntry(type,path)
Changed: Get a type-specific library entry. Unlike the current version, path
is expected to be a specific entry - not a directory.
It will return an object:
{
path: "/full/path/to/the/entryName,
name: "entryName"
meta: {
a: "optional meta data about the entry"
},
body: "the main body of the entry"
}
Storage.saveLibraryEntry(type,name,meta,body)
Unchanged: save a type-specific library entry
Library Sidebar
Some ascii art for the sidebar layout.
+-+-------------------------+
| | source libraries |
+-+-------------------------+
+-+-------------------------+
| | types |
+-+-------------------------+
+---------------------------+
| path1 |
| path2 |
| path3 |
| path4 |
| |
+---------------------------+
| |
| file1 |
| file2 |
| file3 |
| file4 |
| |
| |
| |
| |
| |
+---------------------------+
+--------+
| import |
+--------+
- library select - so when there are multiple sources, one can be picked.
- type select - the type of entity to browse. Will offer up types available for the chosen library.
- path/file select boxes - normal file browser type behaviour
- import button - when clicked will import the selected entry.
ToDo: should there be a preview pane? Maybe the path/file boxes should collapse into one tree and use the second box for preview.
The 'import' button behaviour will depend on the current editor state:
- in 'normal' mode, import will import the selected flow, or the node of the appropriate type.
- if a node is being edited, the 'type' select will be restricted to the required type and clicking import will copy it over to the node edit dialog
Todo: what is the export to library
workflow.
External Library API
A common request is to be able to have a shared library between a team of developers. The current model works if they are sharing a node-red instance (as the current library is local to the runtime), but doesn't help where you have assets that should be shared across instances.
So... what's the right way to allow other libraries to be plugged in?
IGNORE STUFF BELOW HERE
/library/flows
For flows
type, if the path is empty, it returns a complete listing of all flow
library entries. The response object is a simple recursive structure; at the top level, two properties might exist:
f
(if it exists) points at an array of filenames that exist at that level.d
points at an object whose properties are pathnames and values are objects withf
/d
properties.
Given the structure:
/
├── one
│ └── example1.json
└── two
└── three
└── example2.json
The following representation is returned:
{
"d": {
"one": {
"f": [
"example1"
]
},
"two": {
"d": {
"three": {
"f": [
"example2"
]
}
}
}
}
}
This was done because, unlike the other types that are exposed via a file-browser-like UI, the flows are displayed in the drop-down menu which needs to know about everything at the start to build the menu structure.
A goal of this work is to make this all consistent!
This reveals two different approaches of what gets returned:
- a complete summary of the available entries : this allows for a more responsive UI (no network requests whilst browsing the tree). It also allows for a UI that can search/filter the full list of available entries
- a directory-by-directory approach : lots more requests, can't search the full list
This suggests the desired consistent approach is:
/library/[type]
: return a full listing/library/[type]/path
: if it resolves to an entry, return the entry. Otherwise return the full listing rooted at this point
The format of the listing response should be made clearer.
{
"path": "the full path this level represents",
"name": "the name of this entity"
"children": [
{
"path": ...
"name": ...
"children": [ ]
}
]
}
If an entry has no children
property, it is a file. Otherwise it is a directory.
The previous example becomes:
{
"path": "/",
"name": "/",
"children": [
{
"path": "/one",
"name": "one",
"children": [
{
"path": "/one/example1",
"name": "example1"
}
]
},
{
"path": "/two",
"name": "two",
"children": [
{
"path": "/two/three",
"name": "three",
"children": [
{
"path": "/two/three/example2",
"name": "example2",
}
]
}
]
}
]
}