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

Commit f9db3d0

Browse files
authored
Merge pull request #249 from codeoverflow-org/feat/109-service-preset-list
Add config presets
2 parents 317aeaa + e6fbad0 commit f9db3d0

File tree

16 files changed

+197
-82
lines changed

16 files changed

+197
-82
lines changed

nodecg-io-core/dashboard/crypto.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ class Config extends EventEmitter {
3939
export const config = new Config();
4040

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

4545
/**
46-
* Sets the passed passwort to be used by the crypto module.
46+
* Sets the passed password to be used by the crypto module.
4747
* Will try to decrypt decrypted data to tell whether the password is correct,
4848
* if it is wrong the internal password will be set to undefined.
4949
* Returns whether the password is correct.

nodecg-io-core/dashboard/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export {
66
createInstance,
77
saveInstanceConfig,
88
deleteInstance,
9+
selectInstanceConfigPreset,
910
} from "./serviceInstance";
1011
export {
1112
renderBundleDeps,

nodecg-io-core/dashboard/panel.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@
4242
<select id="selectService" class="flex-fill"></select>
4343
</div>
4444

45+
<div id="instancePreset" class="margins flex hidden">
46+
<label for="selectPreset">Load config preset: </label>
47+
<select id="selectPreset" class="flex-fill" onchange="selectInstanceConfigPreset();"></select>
48+
</div>
49+
4550
<div id="instanceNameField" class="margins flex hidden">
4651
<label for="inputInstanceName">Instance Name: </label>
4752
<input id="inputInstanceName" class="flex-fill" type="text" />

nodecg-io-core/dashboard/serviceInstance.ts

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { updateOptionsArr, updateOptionsMap } from "./utils/selectUtils";
88
import { objectDeepCopy } from "./utils/deepCopy";
99
import { config, sendAuthenticatedMessage } from "./crypto";
10+
import { ObjectMap } from "../extension/service";
1011

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

2830
// Website areas
2931
const instanceServiceSelector = document.getElementById("instanceServiceSelector");
32+
const instancePreset = document.getElementById("instancePreset");
3033
const instanceNameField = document.getElementById("instanceNameField");
3134
const instanceEditButtons = document.getElementById("instanceEditButtons");
3235
const instanceCreateButton = document.getElementById("instanceCreateButton");
@@ -62,33 +65,59 @@ export function onInstanceSelectChange(value: string): void {
6265
showNotice(undefined);
6366
switch (value) {
6467
case "new":
65-
showInMonaco("text", true, editorCreateText);
66-
setCreateInputs(true, false, true);
68+
showInMonaco(true, editorCreateText);
69+
setCreateInputs(true, false, true, false);
6770
inputInstanceName.value = "";
6871
break;
6972
case "select":
70-
showInMonaco("text", true, editorDefaultText);
71-
setCreateInputs(false, false, true);
73+
showInMonaco(true, editorDefaultText);
74+
setCreateInputs(false, false, true, false);
7275
break;
7376
default:
7477
showConfig(value);
7578
}
7679
}
7780

78-
function showConfig(value: string) {
79-
const inst = config.data?.instances[value];
81+
function showConfig(instName: string) {
82+
const inst = config.data?.instances[instName];
8083
const service = config.data?.services.find((svc) => svc.serviceType === inst?.serviceType);
8184

8285
if (!service) {
83-
showInMonaco("text", true, editorInvalidServiceText);
86+
showInMonaco(true, editorInvalidServiceText);
8487
} else if (service.requiresNoConfig) {
85-
showInMonaco("text", true, editorNotConfigurableText);
88+
showInMonaco(true, editorNotConfigurableText);
8689
} else {
87-
const jsonString = JSON.stringify(inst?.config || {}, null, 4);
88-
showInMonaco("json", false, jsonString, service?.schema);
90+
showInMonaco(false, inst?.config ?? {}, service?.schema);
8991
}
9092

91-
setCreateInputs(false, true, !(service?.requiresNoConfig ?? false));
93+
setCreateInputs(false, true, !(service?.requiresNoConfig ?? false), service?.presets !== undefined);
94+
95+
if (service?.presets) {
96+
renderPresets(service.presets);
97+
}
98+
}
99+
100+
// Preset drop-down
101+
export function selectInstanceConfigPreset(): void {
102+
const selectedPresetName = selectPreset.options[selectPreset.selectedIndex]?.value;
103+
if (!selectedPresetName) {
104+
return;
105+
}
106+
107+
const instName = selectInstance.options[selectInstance.selectedIndex]?.value;
108+
if (!instName) {
109+
return;
110+
}
111+
112+
const instance = config.data?.instances[instName];
113+
if (!instance) {
114+
return;
115+
}
116+
117+
const service = config.data?.services.find((svc) => svc.serviceType === instance.serviceType);
118+
const presetValue = service?.presets?.[selectedPresetName] ?? {};
119+
120+
showInMonaco(false, presetValue, service?.schema);
92121
}
93122

94123
// Save button
@@ -191,6 +220,17 @@ function renderInstances() {
191220
selectServiceInstance(previousSelected);
192221
}
193222

223+
function renderPresets(presets: ObjectMap<unknown>) {
224+
updateOptionsMap(selectPreset, presets);
225+
226+
// Add "Select..." element that hints the user that he can use this select box
227+
// to choose a preset
228+
const selectHintOption = document.createElement("option");
229+
selectHintOption.innerText = "Select...";
230+
selectPreset.prepend(selectHintOption);
231+
selectPreset.selectedIndex = 0; // Select newly added hint
232+
}
233+
194234
// Util functions
195235

196236
function selectServiceInstance(instanceName: string) {
@@ -208,7 +248,12 @@ function selectServiceInstance(instanceName: string) {
208248
}
209249

210250
// Hides/unhides parts of the website based on the passed parameters
211-
function setCreateInputs(createMode: boolean, instanceSelected: boolean, showSave: boolean) {
251+
function setCreateInputs(
252+
createMode: boolean,
253+
instanceSelected: boolean,
254+
showSave: boolean,
255+
serviceHasPresets: boolean,
256+
) {
212257
function setVisible(node: HTMLElement | null, visible: boolean) {
213258
if (visible && node?.classList.contains("hidden")) {
214259
node?.classList.remove("hidden");
@@ -218,6 +263,7 @@ function setCreateInputs(createMode: boolean, instanceSelected: boolean, showSav
218263
}
219264

220265
setVisible(instanceEditButtons, !createMode && instanceSelected);
266+
setVisible(instancePreset, !createMode && instanceSelected && serviceHasPresets);
221267
setVisible(instanceCreateButton, createMode);
222268
setVisible(instanceNameField, createMode);
223269
setVisible(instanceServiceSelector, createMode);
@@ -231,12 +277,14 @@ export function showNotice(msg: string | undefined): void {
231277
}
232278

233279
function showInMonaco(
234-
type: "text" | "json",
235280
readOnly: boolean,
236-
content: string,
281+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
282+
content: any,
237283
schema?: Record<string, unknown>,
238284
): void {
239285
editor?.updateOptions({ readOnly });
286+
const type = typeof content === "object" ? "json" : "text";
287+
const contentStr = typeof content === "object" ? JSON.stringify(content, null, 4) : content;
240288

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

266-
editor?.setModel(monaco.editor.createModel(content, type, schema ? modelUri : undefined));
314+
editor?.setModel(monaco.editor.createModel(contentStr, type, schema ? modelUri : undefined));
267315
}

nodecg-io-core/dashboard/styles.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#bundleControlDiv {
77
display: grid;
88
grid-template-columns: auto 1fr;
9+
width: 96.5%;
910
}
1011

1112
.flex {
@@ -14,6 +15,7 @@
1415

1516
.flex-fill {
1617
flex: 1;
18+
width: 100%;
1719
}
1820

1921
.flex-column {
@@ -32,3 +34,7 @@
3234
display: none;
3335
visibility: hidden;
3436
}
37+
38+
select {
39+
text-overflow: ellipsis;
40+
}

nodecg-io-core/extension/service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ export interface Service<R, C> {
3737
*/
3838
readonly defaultConfig?: R;
3939

40+
/**
41+
* Config presets that the user can choose to load as their config.
42+
* Useful for e.g. detected devices with everything already filled in for that specific device.
43+
* Can also be used to show the user multiple different authentication methods or similar.
44+
*/
45+
presets?: ObjectMap<R>;
46+
4047
/**
4148
* This function validates the passed config after it has been validated against the json schema (if applicable).
4249
* Should make deeper checks like checking validity of auth tokens.

nodecg-io-core/extension/serviceBundle.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export abstract class ServiceBundle<R, C> implements Service<R, C> {
2525
*/
2626
public defaultConfig?: R;
2727

28+
/**
29+
* Config presets that the user can choose to load as their config.
30+
* Useful for e.g. detected devices with everything already filled in for that specific device.
31+
* Can also be used to show the user multiple different authentication methods or similar.
32+
*/
33+
public presets?: ObjectMap<R>;
34+
2835
/**
2936
* This constructor creates the service and gets the nodecg-io-core
3037
* @param nodecg the current NodeCG instance

nodecg-io-midi-input/extension/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ module.exports = (nodecg: NodeCG) => {
1414
};
1515

1616
class MidiService extends ServiceBundle<MidiInputServiceConfig, MidiInputServiceClient> {
17+
presets = Object.fromEntries(easymidi.getInputs().map((device) => [device, { device }]));
18+
1719
async validateConfig(config: MidiInputServiceConfig): Promise<Result<void>> {
1820
const devices: Array<string> = new Array<string>();
1921

nodecg-io-midi-output/extension/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ module.exports = (nodecg: NodeCG) => {
1414
};
1515

1616
class MidiService extends ServiceBundle<MidiOutputServiceConfig, MidiOutputServiceClient> {
17+
presets = Object.fromEntries(easymidi.getOutputs().map((device) => [device, { device }]));
18+
1719
async validateConfig(config: MidiOutputServiceConfig): Promise<Result<void>> {
1820
const devices: Array<string> = new Array<string>();
1921

nodecg-io-philipshue/extension/index.ts

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NodeCG } from "nodecg-types/types/server";
2-
import { Result, emptySuccess, success, error, ServiceBundle } from "nodecg-io-core";
2+
import { Result, emptySuccess, success, error, ServiceBundle, ObjectMap } from "nodecg-io-core";
33
import { v4 as ipv4 } from "is-ip";
44
import { v3 } from "node-hue-api";
55
// Only needed for type because of that it is "unused" but still needed
@@ -11,9 +11,8 @@ const deviceName = "nodecg-io";
1111
const name = "philipshue";
1212

1313
interface PhilipsHueServiceConfig {
14-
discover: boolean;
1514
ipAddr: string;
16-
port: number;
15+
port?: number;
1716
username?: string;
1817
apiKey?: string;
1918
}
@@ -25,20 +24,20 @@ module.exports = (nodecg: NodeCG) => {
2524
};
2625

2726
class PhilipsHueService extends ServiceBundle<PhilipsHueServiceConfig, PhilipsHueServiceClient> {
27+
presets = {};
28+
29+
constructor(nodecg: NodeCG, name: string, ...pathSegments: string[]) {
30+
super(nodecg, name, ...pathSegments);
31+
this.discoverBridges()
32+
.then((bridgePresets) => (this.presets = bridgePresets))
33+
.catch((err) => this.nodecg.log.error(`Failed to discover local bridges: ${err}`));
34+
}
35+
2836
async validateConfig(config: PhilipsHueServiceConfig): Promise<Result<void>> {
29-
const { discover, port, ipAddr } = config;
30-
31-
if (!config) {
32-
// config could not be found
33-
return error("No config found!");
34-
} else if (!discover) {
35-
// check the ip address if its there
36-
if (ipAddr && !ipv4(ipAddr)) {
37-
return error("Invalid IP address, can handle only IPv4 at the moment!");
38-
}
37+
const { port, ipAddr } = config;
3938

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

5150
async createClient(config: PhilipsHueServiceConfig): Promise<Result<PhilipsHueServiceClient>> {
52-
if (config.discover) {
53-
const discIP = await this.discoverBridge();
54-
if (discIP) {
55-
config.ipAddr = discIP;
56-
config.discover = false;
57-
} else {
58-
return error("Could not discover your Hue Bridge, maybe try specifying a specific IP!");
59-
}
60-
}
61-
6251
const { port, username, apiKey, ipAddr } = config;
6352

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

100-
private async discoverBridge() {
101-
const discoveryResults = await discovery.nupnpSearch();
89+
private async discoverBridges(): Promise<ObjectMap<PhilipsHueServiceConfig>> {
90+
const results: { ipaddress: string }[] = await discovery.nupnpSearch();
10291

103-
if (discoveryResults.length === 0) {
104-
this.nodecg.log.error("Failed to resolve any Hue Bridges");
105-
return null;
106-
} else {
107-
// Ignoring that you could have more than one Hue Bridge on a network as this is unlikely in 99.9% of users situations
108-
return discoveryResults[0].ipaddress as string;
109-
}
92+
return Object.fromEntries(
93+
results.map((bridge) => {
94+
const ipAddr = bridge.ipaddress;
95+
const config: PhilipsHueServiceConfig = { ipAddr };
96+
return [ipAddr, config];
97+
}),
98+
);
11099
}
111100
}

0 commit comments

Comments
 (0)