diff --git a/nodecg-io-core/dashboard/serviceInstance.ts b/nodecg-io-core/dashboard/serviceInstance.ts index 7060665d9..ff57bfdf3 100644 --- a/nodecg-io-core/dashboard/serviceInstance.ts +++ b/nodecg-io-core/dashboard/serviceInstance.ts @@ -10,6 +10,8 @@ import { config, sendAuthenticatedMessage } from "./crypto"; 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"; +const editorInvalidServiceText = "!!!!! Service of this instance couldn't be found."; +const editorNotConfigurableText = "----- This service cannot be configured."; document.addEventListener("DOMContentLoaded", () => { config.onChange(() => { @@ -59,18 +61,12 @@ export function onInstanceSelectChange(value: string): void { showNotice(undefined); switch (value) { case "new": - editor?.updateOptions({ - readOnly: true, - }); - editor?.setModel(monaco.editor.createModel(editorCreateText, "text")); + showInMonaco("text", true, editorCreateText); setCreateInputs(true, false); inputInstanceName.value = ""; break; case "select": - editor?.updateOptions({ - readOnly: true, - }); - editor?.setModel(monaco.editor.createModel(editorDefaultText, "text")); + showInMonaco("text", true, editorDefaultText); setCreateInputs(false, false); break; default: @@ -82,30 +78,15 @@ function showConfig(value: string) { const inst = config.data?.instances[value]; const service = config.data?.services.find((svc) => svc.serviceType === inst?.serviceType); - editor?.updateOptions({ - readOnly: false, - }); - - // Get rid of old models, as they have to be unique and we may add the same again - monaco.editor.getModels().forEach((m) => m.dispose()); + if (!service) { + showInMonaco("text", true, editorInvalidServiceText); + } else if (service.requiresNoConfig) { + showInMonaco("text", true, editorNotConfigurableText); + } else { + const jsonString = JSON.stringify(inst?.config || {}, null, 4); + showInMonaco("json", false, jsonString, service?.schema); + } - // This model uri can be completely made up as long the uri in the schema matches with the one in the language model. - const modelUri = monaco.Uri.parse(`mem://nodecg-io/${inst?.serviceType}.json`); - monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - validate: service?.schema !== undefined, - schemas: - service?.schema !== undefined - ? [ - { - uri: modelUri.toString(), - fileMatch: [modelUri.toString()], - schema: objectDeepCopy(service?.schema), - }, - ] - : [], - }); - const model = monaco.editor.createModel(JSON.stringify(inst?.config || {}, null, 4), "json", modelUri); - editor?.setModel(model); setCreateInputs(false, true); } @@ -246,3 +227,39 @@ export function showNotice(msg: string | undefined): void { spanInstanceNotice.innerText = msg !== undefined ? msg : ""; } } + +function showInMonaco( + type: "text" | "json", + readOnly: boolean, + content: string, + schema?: Record, +): void { + editor?.updateOptions({ readOnly }); + + // JSON Schema stuff + // Get rid of old models, as they have to be unique and we may add the same again + monaco.editor.getModels().forEach((m) => m.dispose()); + + // This model uri can be completely made up as long the uri in the schema matches with the one in the language model. + const modelUri = monaco.Uri.parse(`mem://nodecg-io/selectedServiceSchema.json`); + + monaco.languages.json.jsonDefaults.setDiagnosticsOptions( + schema + ? { + validate: true, + schemas: [ + { + uri: modelUri.toString(), + fileMatch: [modelUri.toString()], + schema: objectDeepCopy(schema), + }, + ], + } + : { + validate: false, // if not set we disable validation again. + schemas: [], + }, + ); + + editor?.setModel(monaco.editor.createModel(content, type)); +} diff --git a/nodecg-io-core/extension/index.ts b/nodecg-io-core/extension/index.ts index 3ec9b6d5f..624abaa25 100644 --- a/nodecg-io-core/extension/index.ts +++ b/nodecg-io-core/extension/index.ts @@ -22,7 +22,7 @@ module.exports = (nodecg: NodeCG): NodeCGIOCore => { const serviceManager = new ServiceManager(nodecg); const bundleManager = new BundleManager(nodecg); const instanceManager = new InstanceManager(nodecg, serviceManager, bundleManager); - const persistenceManager = new PersistenceManager(nodecg, instanceManager, bundleManager); + const persistenceManager = new PersistenceManager(nodecg, serviceManager, instanceManager, bundleManager); new MessageManager( nodecg, diff --git a/nodecg-io-core/extension/instanceManager.ts b/nodecg-io-core/extension/instanceManager.ts index cce2a7348..3924e1e16 100644 --- a/nodecg-io-core/extension/instanceManager.ts +++ b/nodecg-io-core/extension/instanceManager.ts @@ -65,16 +65,23 @@ export class InstanceManager extends EventEmitter { const service = svcResult.result; // Create actual instance and save it - this.serviceInstances[instanceName] = { + const inst = { serviceType: service.serviceType, config: service.defaultConfig, client: undefined, }; + this.serviceInstances[instanceName] = inst; this.emit("change"); this.nodecg.log.info( `Service instance "${instanceName}" of service "${service.serviceType}" has been successfully created.`, ); + + // Service requires no config, we can create it right now. + if (service.requiresNoConfig) { + this.updateInstanceClient(inst, instanceName, service); + } + return emptySuccess(); } @@ -145,7 +152,7 @@ export class InstanceManager extends EventEmitter { return error("The service of this instance couldn't be found."); } - if (validation) { + if (validation || !service.result.requiresNoConfig) { const schemaValid = this.ajv.validate(service.result.schema, config); if (!schemaValid) { return error("Config invalid: " + this.ajv.errorsText()); diff --git a/nodecg-io-core/extension/persistenceManager.ts b/nodecg-io-core/extension/persistenceManager.ts index 56543909a..22e7ee96b 100644 --- a/nodecg-io-core/extension/persistenceManager.ts +++ b/nodecg-io-core/extension/persistenceManager.ts @@ -4,6 +4,7 @@ import { BundleManager } from "./bundleManager"; import * as crypto from "crypto-js"; import { emptySuccess, error, Result, success } from "./utils/result"; import { ObjectMap, ServiceDependency, ServiceInstance } from "./types"; +import { ServiceManager } from "./serviceManager"; /** * Models all the data that needs to be persistent in a plain manner. @@ -58,6 +59,7 @@ export class PersistenceManager { constructor( private readonly nodecg: NodeCG, + private readonly services: ServiceManager, private readonly instances: InstanceManager, private readonly bundles: BundleManager, ) { @@ -158,6 +160,11 @@ export class PersistenceManager { continue; } + const svc = this.services.getService(inst.serviceType); + if (!svc.failed && svc.result.requiresNoConfig) { + continue; + } + // Re-set config of this instance. // We can skip the validation here because the config was already validated when it was initially set, // before getting saved to disk. diff --git a/nodecg-io-core/extension/serviceBundle.ts b/nodecg-io-core/extension/serviceBundle.ts index 9178d8d54..0d8a3f26d 100644 --- a/nodecg-io-core/extension/serviceBundle.ts +++ b/nodecg-io-core/extension/serviceBundle.ts @@ -96,10 +96,17 @@ export abstract class ServiceBundle implements Service { * It gets rid of the handlers by stopping the client and creating a new one, to which then only the * now wanted handlers get registered (e.g. if a bundle doesn't uses this service anymore but another still does). * Not ideal, but if your service can't implement removeHandlers for some reason it is still better than - * having dangling handlers that still fire eventho they shouldn't. + * having dangling handlers that still fire events eventho they shouldn't. */ reCreateClientToRemoveHandlers = false; + /** + * This flag says that this service cannot be configured and doesn't need any config passed to {@link createClient}. + * If this is set {@link validateConfig} will never be called. + * @default false + */ + requiresNoConfig = false; + private readSchema(pathSegments: string[]): unknown { const joinedPath = path.resolve(...pathSegments); try { diff --git a/nodecg-io-core/extension/types.d.ts b/nodecg-io-core/extension/types.d.ts index ab6565fcf..371750fc3 100644 --- a/nodecg-io-core/extension/types.d.ts +++ b/nodecg-io-core/extension/types.d.ts @@ -75,14 +75,22 @@ export interface Service> { readonly removeHandlers?(client: C): void; /** - * This flag can be enabled by services if they can't implement removeHandlers but also have some handlers that + * This flag can be enabled by services if they can't implement {@link removeHandlers} but also have some handlers that * should be reset if a bundleDependency has been changed. * It gets rid of the handlers by stopping the client and creating a new one, to which then only the * now wanted handlers get registered (e.g. if a bundle doesn't uses this service anymore but another still does). * Not ideal, but if your service can't implement removeHandlers for some reason it is still better than * having dangling handlers that still fire eventho they shouldn't. + * @default false */ reCreateClientToRemoveHandlers: boolean; + + /** + * This flag says that this service cannot be configured and doesn't need any config passed to {@link createClient}. + * If this is set {@link validateConfig} will never be called. + * @default false + */ + requiresNoConfig: boolean; } /** diff --git a/nodecg-io-curseforge/extension/index.ts b/nodecg-io-curseforge/extension/index.ts index 2aaf1cd0a..3e9dfdecc 100644 --- a/nodecg-io-curseforge/extension/index.ts +++ b/nodecg-io-curseforge/extension/index.ts @@ -53,4 +53,6 @@ class CurseforgeService extends ServiceBundle { stopClient(_: CurseForgeClient): void { this.nodecg.log.info("Successfully stopped CurseForge client."); } + + requiresNoConfig = true; }