This repository consists of two parts:
- A "Panel UI" that, given some JSON configuration, displays buttons that can make HTTP Requests or run CLI commands on a server implementing the QSP Command Protocol. It can be viewed here.
- The
qsp.jsserver. It is a standalone reference implementation of:- The QSP Command Protocol
- The QSP Proxy Protocol
- A simple file server
The Panel UI always shows a Configuration section with Import and Export buttons. These buttons allow you to import and export a configuration file. Note that configuration files are stored in the browser's local storage, so your configuration should remain even on page refreshes.
The JSON configuration consists of a PanelConfig object. It uses a root key for the main Panel. Each Panel has a title string, an optional list of buttons, and an optional list of sub-panel children.
For example, this configuration will display a Main Panel, with Sub Panel A containing A-1 and A-2 buttons followed by Sub Panel B containing B-1 and B-2 buttons.
{
"root": {
"title": "Main Panel",
"children": [
{
"title": "Sub Panel A",
"buttons": [
{ "text": "A-1" },
{ "text": "A-2" }
]
},
{
"title": "Sub Panel B",
"buttons": [
{ "text": "B-1" },
{ "text": "B-2" }
]
}
]
}
}Note how children and buttons are optional - the Main Panel has no buttons, and both sub-panels have no children panels.
Buttons, by default, do nothing when pressed - so these buttons will simply display their text.
Buttons can make HTTP(S) requests when pressed - simply add a request property with an HttpCallRequest object.
Note that only method and url are required - other properties are optional.
For example:
{
"root": {
"title": "Main Panel",
"buttons": [
{
"text": "GET Readme",
"request": {
"method": "GET",
"url": "http://192.168.1.64:1234/README.md"
}
},
{
"text": "POST To Endpoint",
"request": {
"method": "POST",
"url": "http://192.168.1.64:1234/endpoint",
"headers": { "key": "value" },
"body": "POST bodies can go here (optionally)."
}
}
]
}
}When held, buttons will repeatedly request. Initially, they wait repeatInitial milliseconds before repeating, and every subsequent repetition occurs repeat milliseconds apart.
For example: this button, when held, waits 2 seconds initially, then begins making repeated calls every 500 milliseconds.
{
"root": {
"title": "Main Panel",
"buttons": [{
"text": "GET Readme",
"repeatInitial": 2000,
"repeat": 500,
"request": {
"method": "GET",
"url": "http://192.168.1.64:1234/README.md"
}
}]
}
}Buttons can also display the received response text with the showOutput property. When laying buttons out, you can control their column with the optional column property.
In the following example, B is displayed in the same column as A (underneath it) by using column, and when pressed will display the contents of the README.md when pressed (presuming a static file server at 192.168.1.64:1234).
{
"root": {
"title": "Main Panel",
"buttons": [
{
"text": "A"
},
{
"text": "B",
"showOutput": true,
"column": 1,
"request": {
"method": "GET",
"url": "http://192.168.1.64:1234/README.md"
}
}
]
}
}Buttons also have a set property that contains an arbitrary map of key-strings to value-strings. When reading any string-based property, the application replaces all instances of ${key} with the value of set[key].
In the following example, the button will make a GET to http://192.168.1.64:1234/README.md when pressed.
{
"root": {
"title": "Main Panel",
"buttons": [{
"text": "README",
"showOutput": true,
"set": {
"ip": "192.168.1.64",
"file": "README"
},
"request": {
"method": "GET",
"url": "http://${ip}:1234/${file}.md"
}
}]
}
}Note: If the actual text is desired, simply add a backslash between the $ and the {. For example, hello $\{world} will expand to the text: hello ${world}. More slashes can be added - one slash will always be removed.
Additionally, a setList property allows substitution of numeric keys. The following example behaves the same as the previous example, but uses setList:
{
"root": {
"title": "Main Panel",
"buttons": [{
"text": "README (List)",
"showOutput": true,
"setList": ["192.168.1.64", "README"],
"request": {
"method": "GET",
"url": "http://${0}:1234/${1}.md"
}
}]
}
}In addition to the key-value pairs in set and setList, there is a list of global variables that are can be referenced (and which can be overridden if a user uses the same key-name):
GLOBAL_PROTOCOL- The protocol of this page (ex:http:orhttps:)GLOBAL_HOST- The current page's host (ex:abc.example.com:1234)GLOBAL_HOSTNAME- The current page's host name (ex:abc.example.com)GLOBAL_PORT- The current page's port number (ex:1234)GLOBAL_PATHNAME- The current page's path (ex:/a/b/index.html)
Finally, transform functions can be used by prefixing the key with a function name. There are two functions:
${urlencode:x}- UseencodeURIComponentto encode the text inx.${jsonString:x}- UseJSON.stringifyto encode the text inx.
For example, the following button makes a request to http://192.168.1.64:1234/README.md?a=%7B%22key%22%3A%22a%5C%22b%22%7D:
{
"root": {
"title": "Main Panel",
"buttons": [{
"text": "README (Query JSON)",
"showOutput": true,
"set": {
"value": "a\"b",
"data": "{\"key\":${jsonString:value}}"
},
"request": {
"method": "GET",
"url": "http://192.168.1.64:1234/README.md?a=${urlencode:data}"
}
}]
}
}Notice how even set values are expanded - allowing set.data to reference set.value. In the above example:
${value}expands toa"b${jsonString:value}expands to"a\""(a JSON-safe quote-surrounded string)${urlencode:value}expands toa%22b(a URL-param-safe string)
By combining these two (as done above), we can embed JSON data as a URL parameter.
The root configuration object also accepts a templates object containing buttons that are not to be displayed.
Instead, these buttons can provide "default" values to other buttons who reference them with the is property. For example, both buttons in the following example GET the same README.md file:
{
"templates": {
"ParentA": {
"text": "unused (Parent A)",
"showOutput": true,
"set": { "ip": "192.168.1.64" }
},
"ParentB": {
"text": "unused (Parent B)",
"showOutput": true,
"request": {
"method": "GET",
"url": "http://192.168.1.64:1234/README.md"
}
}
},
"root": {
"title": "Main Panel",
"buttons": [
{
"text": "A",
"is": "ParentA",
"set": { "file": "README" },
"request": {
"method": "GET",
"url": "http://${ip}:1234/${file}.md"
}
},
{
"text": "B",
"is": "ParentB"
}
]
}
}Note the following:
- All properties are "inherited" - for example, both buttons act as if
showOutputistrue, since they both "inherit" that override from each of their parents. - Even
requestis inherited, as shown by B. setkeys are inherited per-key. Above,fileis found inA, butipis inherited fromParentA.- Buttons in the
templatesobject can also use theisproperty, creating multi-level inheritance chains.
Users of the Panel UI can update the set map from within the UI. To enable this, add an arguments list to a button. Each item in the list is an ArgumentSchema with a key (matching a set key), user-displayed info, and an optional list of accepted values.
In the following example, users can update host with a text field, and choose from two files (README.md or index.js) to download.
{
"root": {
"title": "Main Panel",
"buttons": [{
"text": "Download",
"showOutput": true,
"set": {
"host": "192.168.1.64:1234",
"file": "README.md"
},
"arguments": [
{
"key": "host",
"info": "The address to download from."
},
{
"key": "file",
"info": "The filename to download.",
"values": ["README.md", "index.js"]
}
],
"request": {
"method": "GET",
"url": "http://${host}/${file}"
}
}]
}
}Note: user input is not escaped in any way - all input is directly copied into set. This means users can potentially input values with ${replacement} text.
By adding a proxyUrl property to a button, the entire HttpCallRequest in the button's request will be serialized and POST-ed to ${proxyUrl}?do=proxy. Assuming the server located at proxyUrl implements the QSP Proxy Protocol, the server will place the request itself and return the result.
The qsp.js server is a reference implementation of the QSP Proxy Protocol.
By adding a command name to the button along with a commandUrl property, the following will occur:
- A list of supported commands will be fetched from
${commandUrl}?do=getCommands. - A command whose name matches
commandwill be found from that list. - The matching command will provide
argumentsfor this button. - Upon pressing the button, the corresponding binary
runnerwill be run on the server.
Additionally, the button can set isPersist to true or false (default). If it is true, the command will stream its output into a new popup window (good for long-running commands).
For an example of using command, see the Full CLI Command Example section below.
The qsp.js file contains a reference implementation of the QSP Command and Proxy protocols, in addition to serving as a simple static file server.
It can be run with the -h to display usage information, including how to run it with a configuration file.
The qsp.js file configuration is a QspServerConfig object. It contains a list of commands, each of which are a QspServerConfigCommand object.
Each command consists of a name (for reference by Panel UI buttons' command property), a list of arguments (identical in structure to Panel UI button arguments), and a runner which describes the CLI binary to run (and its arguments).
The runner is a list of strings. The first represents the binary name (ex: "ls"), and all following strings represent arguments passed to that binary. Each string can contain substitutions with a similar replacement syntax as the Panel UI - for example, ${replace} is replaced with the value of the argument whose key is replace. It supports a different set of string transform functions:
${relativePath:x}- assumesxis a path or URL, and converts it to a path that must be within the current working directory.
Check out this repository. In the newly checked out folder, create a qsp-config.js file with the following contents:
/** @type {import('./types').QspServerConfig} */
export const config = {
commands: [
{
name: 'list',
arguments: [{
key: 'type',
info: 'The kind of output to provide',
values: ['l', 'lah']
}],
runner: ['ls', '-${type}']
},
{
name: 'ping-command',
arguments: [{
key: 'host',
info: 'The host to ping.'
}],
runner: ['ping', '${host}', '-c', '10']
}
]
};Additionally, create a panel-config.js with the following contents:
/** @type {import('./types').PanelConfig} */
export const config = {
"root": {
"title": "Main Panel",
"buttons": [
{
"text": "LS",
"command": "list",
"commandUrl": "http://localhost:1234"
},
{
"text": "Ping Btn",
"command": "ping-command",
"commandUrl": "http://localhost:1234",
"isPersisted": true
}
]
}
}Then, run the reference server with:
./qsp.js -c ./qsp-config.js -p 1234 -l _LOCALNext, open the Panel UI here: http://localhost:1234/_LOCAL/index.html
In that UI, use Import to import the panel-config.js you created above.
This creates two buttons, one of which runs ls and one which runs ping. Notice:
- The
"isPersisted": trueflag in the Panel UI configuration means the output will stream into a new window as it is produced. This is ideal for long-running commands likeping. - The LS button in the Panel UI does not display an input popup when pressed. Since it has a single input that uses
values(and no other inputs), the button itself becomes a dropdown. Choosing an item from the dropdown runs the command without needing an additional popup. This is ideal for a simple command with a few variants.