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.errorMessage + ? html` + + ${msg("Invalid Configuration")} + ` + : html` + + ${msg("Valid Configuration")} + `} +
+ + this.textareaElem?.value} + > +
+ + ${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 - * + * + * ``` + * Or: + * ```ts + * value}> * ``` * * @event on-copied @@ -17,6 +21,9 @@ export class CopyButton extends LitElement { @property({ type: String }) value?: string; + @property({ type: Function }) + getValue?: () => string; + @state() private isCopied: boolean = false; @@ -33,18 +40,22 @@ export class CopyButton extends LitElement { render() { return html` - ${this.isCopied ? msg("Copied") : msg("Copy")} `; } private onClick() { - CopyButton.copyToClipboard(this.value!); + const value = (this.getValue ? this.getValue() : this.value) || ""; + CopyButton.copyToClipboard(value); this.isCopied = true; - this.dispatchEvent(new CustomEvent("on-copied", { detail: this.value })); + this.dispatchEvent(new CustomEvent("on-copied", { detail: value })); this.timeoutId = window.setTimeout(() => { this.isCopied = false; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index fc50acba..b2494df8 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -9,6 +9,9 @@ import("./account-settings").then(({ AccountSettings }) => { import("./archive-invite-form").then(({ ArchiveInviteForm }) => { customElements.define("btrix-archive-invite-form", ArchiveInviteForm); }); +import("./config-editor").then(({ ConfigEditor }) => { + customElements.define("btrix-config-editor", ConfigEditor); +}); import("./archives-list").then(({ ArchivesList }) => { customElements.define("btrix-archives-list", ArchivesList); }); diff --git a/frontend/src/pages/archive/crawl-templates-detail.ts b/frontend/src/pages/archive/crawl-templates-detail.ts index 97ac9ead..6c39235d 100644 --- a/frontend/src/pages/archive/crawl-templates-detail.ts +++ b/frontend/src/pages/archive/crawl-templates-detail.ts @@ -3,6 +3,7 @@ import { state, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { msg, localized, str } from "@lit/localize"; import cronstrue from "cronstrue"; // TODO localize +import { parse as yamlToJson, stringify as jsonToYaml } from "yaml"; import type { AuthState } from "../../utils/AuthService"; import LiteElement, { html } from "../../utils/LiteElement"; @@ -36,13 +37,11 @@ export class CrawlTemplatesDetail extends LiteElement { private showAllSeedURLs: boolean = false; @state() - private isSeedsJsonView: boolean = false; + private isConfigCodeView: boolean = false; + /** YAML or stringified JSON config */ @state() - private seedsJson: string = ""; - - @state() - private invalidSeedsJsonMessage: string = ""; + private configCode: string = ""; @state() private isSubmittingUpdate: boolean = false; @@ -70,9 +69,9 @@ export class CrawlTemplatesDetail extends LiteElement { (seed: any) => typeof seed !== "string" ); if (isComplexConfig) { - this.isSeedsJsonView = true; + this.isConfigCodeView = true; } - this.seedsJson = JSON.stringify(this.crawlTemplate.config, null, 2); + this.configCode = jsonToYaml(this.crawlTemplate.config); } catch (e: any) { this.notify({ message: @@ -385,7 +384,7 @@ export class CrawlTemplatesDetail extends LiteElement { >${msg("Actions")} -