0 Design: Library Sidebar
Nick O'Leary edited this page 2017-03-22 14:49:53 +00:00

The library feature in Node-RED comes in two parts:

  1. the flow library is exposed via the drop-down menu : import->library->...
  2. 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:

  1. return a listing of all items of a given type
  2. return the contents of a given type
  3. 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 |
+--------+
  1. library select - so when there are multiple sources, one can be picked.
  2. type select - the type of entity to browse. Will offer up types available for the chosen library.
  3. path/file select boxes - normal file browser type behaviour
  4. 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 with f/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",
                        }
                    ]
                }
            ]
        }
    ]
}