From 118d07cfb94df6e7b41c9245c2365a7c1bcf21c7 Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 9 Oct 2021 20:10:27 +0300 Subject: [PATCH] fix: provide validation error to via MonacoEditor.props.onError() Signed-off-by: Roman --- .../components/dock/create-resource.scss | 23 ------- .../components/dock/create-resource.tsx | 28 ++++---- .../components/dock/dock-tab-content.tsx | 43 ++++++------ .../components/dock/dock.views-manager.ts | 3 +- .../components/dock/edit-resource.scss | 23 ------- .../components/dock/edit-resource.tsx | 48 ++++++------- src/renderer/components/dock/info-panel.tsx | 49 ++++++-------- .../components/dock/install-chart.scss | 2 +- .../components/dock/install-chart.tsx | 32 +++++---- src/renderer/components/dock/logs.tsx | 2 +- .../components/dock/upgrade-chart.scss | 23 ------- .../components/dock/upgrade-chart.tsx | 67 ++++++++----------- .../monaco-editor/monaco-editor.tsx | 48 +++++-------- .../monaco-editor/monaco-validators.ts | 12 ++-- 14 files changed, 148 insertions(+), 255 deletions(-) delete mode 100644 src/renderer/components/dock/create-resource.scss delete mode 100644 src/renderer/components/dock/edit-resource.scss delete mode 100644 src/renderer/components/dock/upgrade-chart.scss diff --git a/src/renderer/components/dock/create-resource.scss b/src/renderer/components/dock/create-resource.scss deleted file mode 100644 index a14b187a80..0000000000 --- a/src/renderer/components/dock/create-resource.scss +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -.CreateResource { -} diff --git a/src/renderer/components/dock/create-resource.tsx b/src/renderer/components/dock/create-resource.tsx index c4ea7f9202..dd278c3f02 100644 --- a/src/renderer/components/dock/create-resource.tsx +++ b/src/renderer/components/dock/create-resource.tsx @@ -19,22 +19,20 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./create-resource.scss"; import React from "react"; import { GroupSelectOption, Select, SelectOption } from "../select"; import jsYaml from "js-yaml"; import { computed, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { createResourceStore } from "./create-resource.store"; -import { InfoPanel } from "./info-panel"; +import { InfoPanel, InfoPanelProps } from "./info-panel"; import { resourceApplierApi } from "../../../common/k8s-api/endpoints/resource-applier.api"; import type { JsonApiErrorParsed } from "../../../common/k8s-api/json-api"; import { Notifications } from "../notifications"; import { TabKind } from "./dock.store"; -import type { DockTabContentProps } from "./dock-tab-content"; import { dockViewsManager } from "./dock.views-manager"; -interface Props extends DockTabContentProps { +interface Props extends InfoPanelProps { } type SelectOptionTemplate = SelectOption; @@ -46,14 +44,14 @@ interface SelectOptionTemplateValue { } @observer -export class CreateResource extends React.Component { +export class CreateResourceInfoPanel extends React.Component { constructor(props: Props) { super(props); makeObservable(this); } get tabId() { - return this.props.tab.id; + return this.props.tabId; } get draft() { @@ -136,21 +134,19 @@ export class CreateResource extends React.Component { render() { return ( -
- -
+ ); } } dockViewsManager.register(TabKind.CREATE_RESOURCE, { - Content: CreateResource, + InfoPanel: CreateResourceInfoPanel, editor: { getValue(tabId) { return createResourceStore.getData(tabId); diff --git a/src/renderer/components/dock/dock-tab-content.tsx b/src/renderer/components/dock/dock-tab-content.tsx index e35cb1defa..3a41107be3 100644 --- a/src/renderer/components/dock/dock-tab-content.tsx +++ b/src/renderer/components/dock/dock-tab-content.tsx @@ -21,12 +21,12 @@ import "./dock-tab-content.scss"; import React from "react"; -import { observer } from "mobx-react"; -import { computed, makeObservable } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; +import { computed, makeObservable, observable, reaction } from "mobx"; import type { DockTab, TabId } from "./dock.store"; import { cssNames } from "../../utils"; import { DockTabComponents, dockViewsManager } from "./dock.views-manager"; -import { MonacoEditor, onMonacoContentChangeCallback } from "../monaco-editor"; +import { MonacoEditor } from "../monaco-editor"; export interface DockTabContentProps extends React.HTMLAttributes { className?: string; @@ -39,6 +39,10 @@ export class DockTabContent extends React.Component { constructor(props: DockTabContentProps) { super(props); makeObservable(this); + + disposeOnUnmount(this, [ + reaction(() => this.tabId, () => this.error = ""), // reset + ]); } @computed get tabId(): TabId { @@ -49,46 +53,45 @@ export class DockTabContent extends React.Component { return dockViewsManager.get(this.props.tab.kind); } + @observable error = ""; + /** * Always keep editor in DOM while (while is open/rendered). * This allows to restore editor's model-view state (cursor pos, selection, etc.) * while switching between different tab.kind-s (e.g. "terminal" tab doesn't have editor) */ renderEditor() { - const { tabId, tabComponents } = this; - const isHidden = !tabComponents.editor; + const { tabId, tabComponents: { editor } } = this; + const isHidden = !editor; return ( { + this.error = ""; + editor?.setValue(tabId, value); + }} + onError={error => { + this.error = isHidden ? "" : error; + }} /> ); } - onEditorChange: onMonacoContentChangeCallback = (value) => { - this.tabComponents.editor?.setValue(this.tabId, value); - }; - - onEditorError = (error: string) => { - this.tabComponents.editor?.onError?.(this.tabId, error); - }; - - // TODO: provide error to dock's info-panel render() { - const { children: bottomContent, bindContainerRef, className, tab } = this.props; + const { bindContainerRef, className, tab } = this.props; if (!tab) return null; - const { Content } = this.tabComponents; + const { InfoPanel, Content } = this.tabComponents; return (
+ {InfoPanel && } {Content && } {this.renderEditor()} - {bottomContent} + {this.props.children}
); } diff --git a/src/renderer/components/dock/dock.views-manager.ts b/src/renderer/components/dock/dock.views-manager.ts index acabe0ab30..f1eef8dede 100644 --- a/src/renderer/components/dock/dock.views-manager.ts +++ b/src/renderer/components/dock/dock.views-manager.ts @@ -23,13 +23,14 @@ import type React from "react"; import { observable } from "mobx"; import type { TabId, TabKind } from "./dock.store"; import type { DockTabContentProps } from "./dock-tab-content"; +import type { InfoPanelProps } from "./info-panel"; export interface DockTabComponents { + InfoPanel?: React.ComponentType; Content?: React.ComponentType; editor?: { getValue(tabId: TabId): string | undefined; setValue(tabId: TabId, value: string): void; - onError?(tabId: TabId, error: string | Error): void; } } diff --git a/src/renderer/components/dock/edit-resource.scss b/src/renderer/components/dock/edit-resource.scss deleted file mode 100644 index 0911cc8649..0000000000 --- a/src/renderer/components/dock/edit-resource.scss +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -.EditResource { -} \ No newline at end of file diff --git a/src/renderer/components/dock/edit-resource.tsx b/src/renderer/components/dock/edit-resource.tsx index fdb6277d77..aa9374d16f 100644 --- a/src/renderer/components/dock/edit-resource.tsx +++ b/src/renderer/components/dock/edit-resource.tsx @@ -19,27 +19,23 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./edit-resource.scss"; - import React from "react"; import { observer } from "mobx-react"; import jsYaml from "js-yaml"; import { editResourceStore } from "./edit-resource.store"; -import { InfoPanel } from "./info-panel"; +import { InfoPanel, InfoPanelProps } from "./info-panel"; import { Badge } from "../badge"; -import { Spinner } from "../spinner"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; -import type { DockTabContentProps } from "./dock-tab-content"; import { TabKind } from "./dock.store"; import { dockViewsManager } from "./dock.views-manager"; -interface Props extends DockTabContentProps { +interface Props extends InfoPanelProps { } @observer -export class EditResource extends React.Component { +export class EditResourceInfoPanel extends React.Component { get tabId() { - return this.props.tab.id; + return this.props.tabId; } get resource(): KubeObject | undefined { @@ -74,34 +70,30 @@ export class EditResource extends React.Component { }; render() { - const { tabId, resource } = this; + const { resource } = this; - if (!editResourceStore.dataReady || !resource) { - return ; - } + if (!resource) return null; return ( -
- - Kind: - Name: - Namespace: -
- )} - /> - + + Kind: + Name: + Namespace: + + )} + /> ); } } dockViewsManager.register(TabKind.EDIT_RESOURCE, { - Content: EditResource, + InfoPanel: EditResourceInfoPanel, editor: { getValue(tabId) { return editResourceStore.getData(tabId).draft; diff --git a/src/renderer/components/dock/info-panel.tsx b/src/renderer/components/dock/info-panel.tsx index 42a6ce8df8..2fa761f1fc 100644 --- a/src/renderer/components/dock/info-panel.tsx +++ b/src/renderer/components/dock/info-panel.tsx @@ -20,9 +20,8 @@ */ import "./info-panel.scss"; - import React, { Component, ReactNode } from "react"; -import { computed, observable, reaction, makeObservable } from "mobx"; +import { makeObservable, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { cssNames } from "../../utils"; import { Button } from "../button"; @@ -31,14 +30,11 @@ import { Spinner } from "../spinner"; import { dockStore, TabId } from "./dock.store"; import { Notifications } from "../notifications"; -interface Props extends OptionalProps { +export interface InfoPanelProps { tabId: TabId; submit?: () => Promise; -} - -interface OptionalProps { className?: string; - error?: string; + error?: React.ReactNode; controls?: ReactNode; submitLabel?: ReactNode; submittingMessage?: ReactNode; @@ -50,22 +46,23 @@ interface OptionalProps { showStatusPanel?: boolean; } -@observer -export class InfoPanel extends Component { - static defaultProps: OptionalProps = { - submitLabel: "Submit", - submittingMessage: "Submitting..", - showButtons: true, - showSubmitClose: true, - showInlineInfo: true, - showNotifications: true, - showStatusPanel: true, - }; +const defaultProps: Partial = { + submitLabel: "Submit", + submittingMessage: "Submitting..", + showButtons: true, + showSubmitClose: true, + showInlineInfo: true, + showNotifications: true, + showStatusPanel: true, +}; + +@observer +export class InfoPanel extends Component { + static defaultProps = defaultProps as object; - @observable error = ""; @observable waiting = false; - constructor(props: Props) { + constructor(props: InfoPanelProps) { super(props); makeObservable(this); } @@ -78,10 +75,6 @@ export class InfoPanel extends Component { ]); } - @computed get errorInfo() { - return this.props.error; - } - submit = async () => { const { showNotifications } = this.props; @@ -107,14 +100,16 @@ export class InfoPanel extends Component { dockStore.closeTab(this.props.tabId); }; - renderErrorIcon() { - if (!this.props.showInlineInfo || !this.errorInfo) { + renderErrorIcon(): React.ReactNode { + const { error } = this.props; + + if (!error || !this.props.showInlineInfo) { return null; } return (
- +
); } diff --git a/src/renderer/components/dock/install-chart.scss b/src/renderer/components/dock/install-chart.scss index d6d444a452..7994caa7eb 100644 --- a/src/renderer/components/dock/install-chart.scss +++ b/src/renderer/components/dock/install-chart.scss @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -.InstallChart { +.InstallChartInfoPanel { .Select { &.NamespaceSelect { min-width: 130px; diff --git a/src/renderer/components/dock/install-chart.tsx b/src/renderer/components/dock/install-chart.tsx index 6c6193a820..cb8ca7716e 100644 --- a/src/renderer/components/dock/install-chart.tsx +++ b/src/renderer/components/dock/install-chart.tsx @@ -25,7 +25,7 @@ import React, { Component } from "react"; import { computed, makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; import { dockStore, TabKind } from "./dock.store"; -import { InfoPanel } from "./info-panel"; +import { InfoPanel, InfoPanelProps } from "./info-panel"; import { Badge } from "../badge"; import { NamespaceSelect } from "../+namespaces/namespace-select"; import { prevDefault } from "../../utils"; @@ -39,14 +39,13 @@ import { Select, SelectOption } from "../select"; import { Input } from "../input"; import { navigate } from "../../navigation"; import { releaseURL } from "../../../common/routes"; -import type { DockTabContentProps } from "./dock-tab-content"; import { dockViewsManager } from "./dock.views-manager"; -interface Props extends DockTabContentProps { +interface Props extends InfoPanelProps { } @observer -export class InstallChart extends Component { +export class InstallChartInfoPanel extends Component { @observable showNotes = false; constructor(props: Props) { @@ -55,7 +54,7 @@ export class InstallChart extends Component { } get tabId() { - return this.props.tab.id; + return this.props.tabId; } get chartData(): IChartInstallData | undefined { @@ -160,7 +159,7 @@ export class InstallChart extends Component { return this.renderReleaseDetails(); } - const { tabId, chartData, install, versions } = this; + const { chartData, install, versions } = this; const { repo, name, version, namespace, releaseName } = chartData; const panelControls = ( @@ -195,22 +194,21 @@ export class InstallChart extends Component { ); return ( -
- -
+ ); } } dockViewsManager.register(TabKind.INSTALL_CHART, { - Content: InstallChart, + InfoPanel: InstallChartInfoPanel, editor: { getValue(tabId) { return installChartStore.getData(tabId).values; diff --git a/src/renderer/components/dock/logs.tsx b/src/renderer/components/dock/logs.tsx index cac18bec63..d8bc4850a7 100644 --- a/src/renderer/components/dock/logs.tsx +++ b/src/renderer/components/dock/logs.tsx @@ -125,7 +125,7 @@ export class Logs extends React.Component { return ( { @observable selectedVersion: IChartVersion; get tabId() { - return this.props.tab.id; + return this.props.tabId; } get release(): HelmRelease | null { @@ -90,46 +86,39 @@ export class UpgradeChart extends React.Component { }; render() { - const { tabId, release, versions, selectedVersion } = this; - - if (!release) { - return ; - } - + const { release, versions, selectedVersion } = this; const currentVersion = release.getVersion(); return ( -
- - Release - Namespace - Version - Upgrade version - ) => this.selectedVersion = value} + /> +
+ } + /> ); } } dockViewsManager.register(TabKind.UPGRADE_CHART, { - Content: UpgradeChart, + InfoPanel: UpgradeChart, editor: { getValue(tabId): string { return upgradeChartStore.values.get(tabId); diff --git a/src/renderer/components/monaco-editor/monaco-editor.tsx b/src/renderer/components/monaco-editor/monaco-editor.tsx index f0922d057e..b2c27dfba6 100644 --- a/src/renderer/components/monaco-editor/monaco-editor.tsx +++ b/src/renderer/components/monaco-editor/monaco-editor.tsx @@ -39,24 +39,12 @@ export interface MonacoEditorProps { theme?: "vs" /* default, light theme */ | "vs-dark" | "hc-black" | string; language?: "yaml" | "json"; // configure bundled list of languages in via MonacoWebpackPlugin({languages: []}) options?: Partial; // customize editor's initialization options - onChange?: onMonacoContentChangeCallback; - onError?: onMonacoErrorCallback; // provide syntax validation errors, etc. + onChange?(value: string, evt: editor.IModelContentChangedEvent): void; // catch latest value updates + onError?(error?: string): void; // provide syntax validation errors, etc. onDidLayoutChange?(info: editor.EditorLayoutInfo): void; onDidContentSizeChange?(evt: editor.IContentSizeChangedEvent): void; } -// `props.onChange` called via editor's api value changes / user input, but when `props.value` changes -export interface onMonacoContentChangeCallback { - (value: string, data: { - model: editor.ITextModel, // current model - event: editor.IModelContentChangedEvent; - }): void; -} - -export interface onMonacoErrorCallback { - (error: string): void; // TODO: provide validation or some another occurred error -} - export const defaultEditorProps: Partial = { language: "yaml", get theme(): MonacoEditorProps["theme"] { @@ -119,6 +107,7 @@ export class MonacoEditor extends React.Component { this.editor.restoreViewState(this.getViewState(model)); // restore cursor position, selection, etc. this.editor.layout(); this.editor.focus(); // keep focus in editor, e.g. when clicking between dock-tabs + this.validateLazy(); }; @computed get editorId(): string { @@ -132,13 +121,9 @@ export class MonacoEditor extends React.Component { componentDidMount() { try { this.createEditor(); - - if (this.props.autoFocus) { - this.editor.focus(); - } console.info(`[MONACO]: editor did mounted`); } catch (error) { - console.error(`[MONACO]: mounting failed`, { error, editor, component: this }); + console.error(`[MONACO]: mounting failed: ${error}`, this); } } @@ -162,6 +147,11 @@ export class MonacoEditor extends React.Component { ...this.options, }); console.info(`[MONACO]: editor created for language=${language}, theme=${theme}`); + this.validateLazy(); // validate initial value + + if (this.props.autoFocus) { + this.editor.focus(); + } const onDidLayoutChangeDisposer = this.editor.onDidLayoutChange(layoutInfo => { this.props.onDidLayoutChange?.(layoutInfo); @@ -172,11 +162,8 @@ export class MonacoEditor extends React.Component { const value = this.editor.getValue(); // console.info("[MONACO]: value changed", { value, event }); + this.props.onChange?.(value, event); this.validateLazy(value); - this.props.onChange?.(value, { - model: this.model, - event, - }); }); const onContentSizeChangeDisposer = this.editor.onDidContentSizeChange((params) => { @@ -244,18 +231,15 @@ export class MonacoEditor extends React.Component { if (value == this.getValue()) return; this.editor.setValue(value); - this.validateLazy(value); + this.validate(value); } getValue(opts?: { preserveBOM: boolean; lineEnding: string; }): string { return this.editor?.getValue(opts) ?? ""; } - // avoid excessive validations during typing - validateLazy = debounce((value: string) => this.validate(value), 250); - @action - async validate(value: string = this.getValue()): Promise { + validate = async (value = this.getValue()): Promise => { const { language } = this.props; const validators: MonacoValidator[] = []; const syntaxValidator: MonacoValidator = monacoValidators[language]; @@ -264,7 +248,8 @@ export class MonacoEditor extends React.Component { validators.push(syntaxValidator); } - this.validationErrors.clear(); + // console.info('[MONACO]: validating', { value, validators }); + this.validationErrors.clear(); // reset first for (const validate of validators) { try { @@ -275,7 +260,10 @@ export class MonacoEditor extends React.Component { this.props.onError?.(error); // emit error outside via callback } } - } + }; + + // avoid excessive validations during typing + validateLazy = debounce(this.validate, 250); bindRef = (elem: HTMLElement) => this.containerElem = elem; diff --git a/src/renderer/components/monaco-editor/monaco-validators.ts b/src/renderer/components/monaco-editor/monaco-validators.ts index a6ef78894a..743553d5df 100644 --- a/src/renderer/components/monaco-editor/monaco-validators.ts +++ b/src/renderer/components/monaco-editor/monaco-validators.ts @@ -24,21 +24,21 @@ export interface MonacoValidator { (value: string): Promise; } -export const yamlValidator: MonacoValidator = async (value: string) => { +export async function yamlValidator(value: string) { try { - await yaml.safeLoad(value); + await yaml.load(value); } catch (error) { throw String(error as YAMLException); } -}; +} -export const jsonValidator: MonacoValidator = async (value: string) => { +export async function jsonValidator(value: string) { try { - await yaml.safeLoad(value, { json: true }); + JSON.parse(value); } catch (error) { throw String(error); } -}; +} export const monacoValidators = { yaml: yamlValidator,