Skip to content
This repository was archived by the owner on Apr 13, 2025. It is now read-only.

Add config presets #249

Merged
merged 6 commits into from
Oct 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions nodecg-io-core/dashboard/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ class Config extends EventEmitter {
export const config = new Config();

// Update the decrypted copy of the data once the encrypted version changes (if pw available).
// This ensures that the decrypted data is kept uptodate.
// This ensures that the decrypted data is kept up-to-date.
encryptedData.on("change", updateDecryptedData);

/**
* Sets the passed passwort to be used by the crypto module.
* Sets the passed password to be used by the crypto module.
* Will try to decrypt decrypted data to tell whether the password is correct,
* if it is wrong the internal password will be set to undefined.
* Returns whether the password is correct.
Expand Down
1 change: 1 addition & 0 deletions nodecg-io-core/dashboard/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
createInstance,
saveInstanceConfig,
deleteInstance,
selectInstanceConfigPreset,
} from "./serviceInstance";
export {
renderBundleDeps,
Expand Down
5 changes: 5 additions & 0 deletions nodecg-io-core/dashboard/panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
<select id="selectService" class="flex-fill"></select>
</div>

<div id="instancePreset" class="margins flex hidden">
<label for="selectPreset">Load config preset: </label>
<select id="selectPreset" class="flex-fill" onchange="selectInstanceConfigPreset();"></select>
</div>

<div id="instanceNameField" class="margins flex hidden">
<label for="inputInstanceName">Instance Name: </label>
<input id="inputInstanceName" class="flex-fill" type="text" />
Expand Down
78 changes: 63 additions & 15 deletions nodecg-io-core/dashboard/serviceInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { updateOptionsArr, updateOptionsMap } from "./utils/selectUtils";
import { objectDeepCopy } from "./utils/deepCopy";
import { config, sendAuthenticatedMessage } from "./crypto";
import { ObjectMap } from "../extension/service";

const editorDefaultText = "<---- Select a service instance to start editing it in here";
const editorCreateText = "<---- Create a new service instance on the left and then you can edit it in here";
Expand All @@ -23,10 +24,12 @@ document.addEventListener("DOMContentLoaded", () => {
// Inputs
const selectInstance = document.getElementById("selectInstance") as HTMLSelectElement;
const selectService = document.getElementById("selectService") as HTMLSelectElement;
const selectPreset = document.getElementById("selectPreset") as HTMLSelectElement;
const inputInstanceName = document.getElementById("inputInstanceName") as HTMLInputElement;

// Website areas
const instanceServiceSelector = document.getElementById("instanceServiceSelector");
const instancePreset = document.getElementById("instancePreset");
const instanceNameField = document.getElementById("instanceNameField");
const instanceEditButtons = document.getElementById("instanceEditButtons");
const instanceCreateButton = document.getElementById("instanceCreateButton");
Expand Down Expand Up @@ -62,33 +65,59 @@ export function onInstanceSelectChange(value: string): void {
showNotice(undefined);
switch (value) {
case "new":
showInMonaco("text", true, editorCreateText);
setCreateInputs(true, false, true);
showInMonaco(true, editorCreateText);
setCreateInputs(true, false, true, false);
inputInstanceName.value = "";
break;
case "select":
showInMonaco("text", true, editorDefaultText);
setCreateInputs(false, false, true);
showInMonaco(true, editorDefaultText);
setCreateInputs(false, false, true, false);
break;
default:
showConfig(value);
}
}

function showConfig(value: string) {
const inst = config.data?.instances[value];
function showConfig(instName: string) {
const inst = config.data?.instances[instName];
const service = config.data?.services.find((svc) => svc.serviceType === inst?.serviceType);

if (!service) {
showInMonaco("text", true, editorInvalidServiceText);
showInMonaco(true, editorInvalidServiceText);
} else if (service.requiresNoConfig) {
showInMonaco("text", true, editorNotConfigurableText);
showInMonaco(true, editorNotConfigurableText);
} else {
const jsonString = JSON.stringify(inst?.config || {}, null, 4);
showInMonaco("json", false, jsonString, service?.schema);
showInMonaco(false, inst?.config ?? {}, service?.schema);
}

setCreateInputs(false, true, !(service?.requiresNoConfig ?? false));
setCreateInputs(false, true, !(service?.requiresNoConfig ?? false), service?.presets !== undefined);

if (service?.presets) {
renderPresets(service.presets);
}
}

// Preset drop-down
export function selectInstanceConfigPreset(): void {
const selectedPresetName = selectPreset.options[selectPreset.selectedIndex]?.value;
if (!selectedPresetName) {
return;
}

const instName = selectInstance.options[selectInstance.selectedIndex]?.value;
if (!instName) {
return;
}

const instance = config.data?.instances[instName];
if (!instance) {
return;
}

const service = config.data?.services.find((svc) => svc.serviceType === instance.serviceType);
const presetValue = service?.presets?.[selectedPresetName] ?? {};

showInMonaco(false, presetValue, service?.schema);
}

// Save button
Expand Down Expand Up @@ -191,6 +220,17 @@ function renderInstances() {
selectServiceInstance(previousSelected);
}

function renderPresets(presets: ObjectMap<unknown>) {
updateOptionsMap(selectPreset, presets);

// Add "Select..." element that hints the user that he can use this select box
// to choose a preset
const selectHintOption = document.createElement("option");
selectHintOption.innerText = "Select...";
selectPreset.prepend(selectHintOption);
selectPreset.selectedIndex = 0; // Select newly added hint
}

// Util functions

function selectServiceInstance(instanceName: string) {
Expand All @@ -208,7 +248,12 @@ function selectServiceInstance(instanceName: string) {
}

// Hides/unhides parts of the website based on the passed parameters
function setCreateInputs(createMode: boolean, instanceSelected: boolean, showSave: boolean) {
function setCreateInputs(
createMode: boolean,
instanceSelected: boolean,
showSave: boolean,
serviceHasPresets: boolean,
) {
function setVisible(node: HTMLElement | null, visible: boolean) {
if (visible && node?.classList.contains("hidden")) {
node?.classList.remove("hidden");
Expand All @@ -218,6 +263,7 @@ function setCreateInputs(createMode: boolean, instanceSelected: boolean, showSav
}

setVisible(instanceEditButtons, !createMode && instanceSelected);
setVisible(instancePreset, !createMode && instanceSelected && serviceHasPresets);
setVisible(instanceCreateButton, createMode);
setVisible(instanceNameField, createMode);
setVisible(instanceServiceSelector, createMode);
Expand All @@ -231,12 +277,14 @@ export function showNotice(msg: string | undefined): void {
}

function showInMonaco(
type: "text" | "json",
readOnly: boolean,
content: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content: any,
schema?: Record<string, unknown>,
): void {
editor?.updateOptions({ readOnly });
const type = typeof content === "object" ? "json" : "text";
const contentStr = typeof content === "object" ? JSON.stringify(content, null, 4) : content;

// JSON Schema stuff
// Get rid of old models, as they have to be unique and we may add the same again
Expand All @@ -263,5 +311,5 @@ function showInMonaco(
},
);

editor?.setModel(monaco.editor.createModel(content, type, schema ? modelUri : undefined));
editor?.setModel(monaco.editor.createModel(contentStr, type, schema ? modelUri : undefined));
}
6 changes: 6 additions & 0 deletions nodecg-io-core/dashboard/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#bundleControlDiv {
display: grid;
grid-template-columns: auto 1fr;
width: 96.5%;
}

.flex {
Expand All @@ -14,6 +15,7 @@

.flex-fill {
flex: 1;
width: 100%;
}

.flex-column {
Expand All @@ -32,3 +34,7 @@
display: none;
visibility: hidden;
}

select {
text-overflow: ellipsis;
}
7 changes: 7 additions & 0 deletions nodecg-io-core/extension/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ export interface Service<R, C> {
*/
readonly defaultConfig?: R;

/**
* Config presets that the user can choose to load as their config.
* Useful for e.g. detected devices with everything already filled in for that specific device.
* Can also be used to show the user multiple different authentication methods or similar.
*/
presets?: ObjectMap<R>;

/**
* This function validates the passed config after it has been validated against the json schema (if applicable).
* Should make deeper checks like checking validity of auth tokens.
Expand Down
7 changes: 7 additions & 0 deletions nodecg-io-core/extension/serviceBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export abstract class ServiceBundle<R, C> implements Service<R, C> {
*/
public defaultConfig?: R;

/**
* Config presets that the user can choose to load as their config.
* Useful for e.g. detected devices with everything already filled in for that specific device.
* Can also be used to show the user multiple different authentication methods or similar.
*/
public presets?: ObjectMap<R>;

/**
* This constructor creates the service and gets the nodecg-io-core
* @param nodecg the current NodeCG instance
Expand Down
2 changes: 2 additions & 0 deletions nodecg-io-midi-input/extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ module.exports = (nodecg: NodeCG) => {
};

class MidiService extends ServiceBundle<MidiInputServiceConfig, MidiInputServiceClient> {
presets = Object.fromEntries(easymidi.getInputs().map((device) => [device, { device }]));

async validateConfig(config: MidiInputServiceConfig): Promise<Result<void>> {
const devices: Array<string> = new Array<string>();

Expand Down
2 changes: 2 additions & 0 deletions nodecg-io-midi-output/extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ module.exports = (nodecg: NodeCG) => {
};

class MidiService extends ServiceBundle<MidiOutputServiceConfig, MidiOutputServiceClient> {
presets = Object.fromEntries(easymidi.getOutputs().map((device) => [device, { device }]));

async validateConfig(config: MidiOutputServiceConfig): Promise<Result<void>> {
const devices: Array<string> = new Array<string>();

Expand Down
57 changes: 23 additions & 34 deletions nodecg-io-philipshue/extension/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NodeCG } from "nodecg-types/types/server";
import { Result, emptySuccess, success, error, ServiceBundle } from "nodecg-io-core";
import { Result, emptySuccess, success, error, ServiceBundle, ObjectMap } from "nodecg-io-core";
import { v4 as ipv4 } from "is-ip";
import { v3 } from "node-hue-api";
// Only needed for type because of that it is "unused" but still needed
Expand All @@ -11,9 +11,8 @@ const deviceName = "nodecg-io";
const name = "philipshue";

interface PhilipsHueServiceConfig {
discover: boolean;
ipAddr: string;
port: number;
port?: number;
username?: string;
apiKey?: string;
}
Expand All @@ -25,20 +24,20 @@ module.exports = (nodecg: NodeCG) => {
};

class PhilipsHueService extends ServiceBundle<PhilipsHueServiceConfig, PhilipsHueServiceClient> {
presets = {};

constructor(nodecg: NodeCG, name: string, ...pathSegments: string[]) {
super(nodecg, name, ...pathSegments);
this.discoverBridges()
.then((bridgePresets) => (this.presets = bridgePresets))
.catch((err) => this.nodecg.log.error(`Failed to discover local bridges: ${err}`));
}

async validateConfig(config: PhilipsHueServiceConfig): Promise<Result<void>> {
const { discover, port, ipAddr } = config;

if (!config) {
// config could not be found
return error("No config found!");
} else if (!discover) {
// check the ip address if its there
if (ipAddr && !ipv4(ipAddr)) {
return error("Invalid IP address, can handle only IPv4 at the moment!");
}
const { port, ipAddr } = config;

// discover is not set but there is no ip address
return error("Discover isn't true there is no IP address!");
if (!ipv4(ipAddr)) {
return error("Invalid IP address, can handle only IPv4 at the moment!");
} else if (port && !(0 <= port && port <= 65535)) {
// the port is there but the port is wrong
return error("Your port is not between 0 and 65535!");
Expand All @@ -49,16 +48,6 @@ class PhilipsHueService extends ServiceBundle<PhilipsHueServiceConfig, PhilipsHu
}

async createClient(config: PhilipsHueServiceConfig): Promise<Result<PhilipsHueServiceClient>> {
if (config.discover) {
const discIP = await this.discoverBridge();
if (discIP) {
config.ipAddr = discIP;
config.discover = false;
} else {
return error("Could not discover your Hue Bridge, maybe try specifying a specific IP!");
}
}

const { port, username, apiKey, ipAddr } = config;

// check if there is one thing missing
Expand Down Expand Up @@ -97,15 +86,15 @@ class PhilipsHueService extends ServiceBundle<PhilipsHueServiceConfig, PhilipsHu
// Not supported from the client
}

private async discoverBridge() {
const discoveryResults = await discovery.nupnpSearch();
private async discoverBridges(): Promise<ObjectMap<PhilipsHueServiceConfig>> {
const results: { ipaddress: string }[] = await discovery.nupnpSearch();

if (discoveryResults.length === 0) {
this.nodecg.log.error("Failed to resolve any Hue Bridges");
return null;
} else {
// Ignoring that you could have more than one Hue Bridge on a network as this is unlikely in 99.9% of users situations
return discoveryResults[0].ipaddress as string;
}
return Object.fromEntries(
results.map((bridge) => {
const ipAddr = bridge.ipaddress;
const config: PhilipsHueServiceConfig = { ipAddr };
return [ipAddr, config];
}),
);
}
}
10 changes: 3 additions & 7 deletions nodecg-io-philipshue/philipshue-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,13 @@
"type": "object",
"additionalProperties": false,
"properties": {
"discover": {
"type": "boolean",
"description": "If the extension should try to discover the Philips Hue Bridge. **IMPORTANT** ignores the specified IP address and errors if it could not find a bridge, will only be used on first run"
},
"ipAddr": {
"type": "string",
"description": "The IP address of the bridge that you want to connect to, not required/used if you specify discover as true"
"description": "The IP address of the bridge that you want to connect to"
},
"port": {
"type": "number",
"description": "The port that you want to use for connecting to your Hue bridge, not required/used if you specify discover as true"
"description": "The port that you want to use for connecting to your Hue bridge"
},
"apiKey": {
"type": "string",
Expand All @@ -24,5 +20,5 @@
"description": "The username that you want to use/that will be generated on first startup"
}
},
"required": ["discover"]
"required": ["ipAddr"]
}
Loading