diff --git a/frontend/package.json b/frontend/package.json
index 156644fb..94471e30 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -19,7 +19,8 @@
"lodash": "^4.17.21",
"path-parser": "^6.1.0",
"pretty-ms": "^7.0.1",
- "tailwindcss": "^3.0.15"
+ "tailwindcss": "^3.0.15",
+ "yaml": "^2.0.0-11"
},
"scripts": {
"test": "web-test-runner \"src/**/*.test.{ts,js}\" --node-resolve --playwright --browsers chromium",
diff --git a/frontend/src/components/config-editor.ts b/frontend/src/components/config-editor.ts
new file mode 100644
index 00000000..963002d2
--- /dev/null
+++ b/frontend/src/components/config-editor.ts
@@ -0,0 +1,185 @@
+import { property, state, query } from "lit/decorators.js";
+import { msg, localized } from "@lit/localize";
+import {
+ parse as yamlToJson,
+ stringify as yamlStringify,
+ YAMLParseError,
+} from "yaml";
+
+import LiteElement, { html } from "../utils/LiteElement";
+
+/**
+ * Usage example:
+ * ```ts
+ *
+ *
+ * ```
+ *
+ * @event on-change
+ */
+@localized()
+export class ConfigEditor extends LiteElement {
+ @property({ type: String })
+ value = "";
+
+ @state()
+ errorMessage = "";
+
+ @query("#config-editor-textarea")
+ textareaElem?: HTMLTextAreaElement;
+
+ render() {
+ return html`
+
+
+
+ ${this.renderTextArea()}
+
+
+ ${this.errorMessage
+ ? html`
+ ${this.errorMessage}
+ `
+ : ""}
+
+
+ `;
+ }
+
+ private renderTextArea() {
+ const lineCount = this.value.split("\n").length;
+
+ return html`
+
+
+ ${[...new Array(lineCount)].map((line, i) => html`${i + 1}
`)}
+
+
+
+
+
+ `;
+ }
+
+ private handleParseError(error: Error) {
+ if (error instanceof YAMLParseError) {
+ const errorMessage = error.message.replace("YAMLParseError: ", "");
+ this.errorMessage = errorMessage;
+ } else {
+ this.errorMessage = msg("Invalid YAML or JSON");
+ console.debug(error);
+ }
+ }
+
+ private checkValidity(value: string) {
+ yamlToJson(value);
+ }
+
+ private onBlur(value: string) {
+ if (!value) {
+ this.textareaElem?.setCustomValidity(msg("Please fill out this field"));
+ this.textareaElem?.reportValidity();
+ }
+ }
+
+ private onChange(value: string) {
+ try {
+ this.checkValidity(value);
+ this.textareaElem?.setCustomValidity("");
+ this.errorMessage = "";
+ this.dispatchEvent(
+ new CustomEvent("on-change", {
+ detail: {
+ value: value,
+ },
+ })
+ );
+ } catch (e: any) {
+ this.textareaElem?.setCustomValidity(msg("Please fix errors"));
+ this.handleParseError(e);
+ }
+
+ this.textareaElem?.reportValidity();
+ }
+
+ /**
+ * Stop propgation of sl-select events.
+ * Prevents bug where sl-dialog closes when dropdown closes
+ * https://github.com/shoelace-style/shoelace/issues/170
+ */
+ private stopProp(e: CustomEvent) {
+ e.stopPropagation();
+ }
+}
diff --git a/frontend/src/components/copy-button.ts b/frontend/src/components/copy-button.ts
index 0f5ad8ba..77856b75 100644
--- a/frontend/src/components/copy-button.ts
+++ b/frontend/src/components/copy-button.ts
@@ -7,7 +7,11 @@ import { msg, localized } from "@lit/localize";
*
* Usage example:
* ```ts
- *