diff --git a/src/renderer/components/dock/create-resource.tsx b/src/renderer/components/dock/create-resource.tsx index 2a0ab5893d..ca6aaa1675 100644 --- a/src/renderer/components/dock/create-resource.tsx +++ b/src/renderer/components/dock/create-resource.tsx @@ -25,14 +25,15 @@ import yaml from "js-yaml"; import { computed, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { createResourceStore } from "./create-resource.store"; -import { InfoPanel, InfoPanelProps } from "./info-panel"; +import { InfoPanel } from "./info-panel"; import * as resourceApplierApi from "../../../common/k8s-api/endpoints/resource-applier.api"; import { Notifications } from "../notifications"; import { TabKind } from "./dock.store"; import { dockViewsManager } from "./dock.views-manager"; import logger from "../../../common/logger"; +import { DockTabContent, DockTabContentProps } from "./dock-tab-content"; -interface Props extends InfoPanelProps { +interface Props extends DockTabContentProps { } type SelectOptionTemplate = SelectOption; @@ -44,7 +45,7 @@ interface SelectOptionTemplateValue { } @observer -export class CreateResourceInfoPanel extends React.Component { +export class CreateResource extends React.Component { constructor(props: Props) { super(props); makeObservable(this); @@ -133,26 +134,27 @@ export class CreateResourceInfoPanel extends React.Component { }; render() { + const { tabId } = this; + return ( - + createResourceStore.setData(tabId, value)} + > + + ); } } dockViewsManager.register(TabKind.CREATE_RESOURCE, { - InfoPanel: CreateResourceInfoPanel, - editor: { - getValue(tabId) { - return createResourceStore.getData(tabId); - }, - setValue(tabId, value) { - createResourceStore.setData(tabId, value); - }, - } + Content: CreateResource, }); diff --git a/src/renderer/components/dock/dock-tab-content.module.css b/src/renderer/components/dock/dock-tab-content.module.css index e74d3bce76..aee271b1db 100644 --- a/src/renderer/components/dock/dock-tab-content.module.css +++ b/src/renderer/components/dock/dock-tab-content.module.css @@ -25,4 +25,8 @@ height: 100%; display: flex; flex-direction: column; + + .editor { + flex: 1; + } } diff --git a/src/renderer/components/dock/dock-tab-content.tsx b/src/renderer/components/dock/dock-tab-content.tsx index 3e76e410ef..268468669a 100644 --- a/src/renderer/components/dock/dock-tab-content.tsx +++ b/src/renderer/components/dock/dock-tab-content.tsx @@ -22,107 +22,58 @@ import styles from "./dock-tab-content.module.css"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; -import { action, computed, makeObservable, observable, reaction } from "mobx"; -import type { DockTab, TabId } from "./dock.store"; -import { dockStore } from "./dock.store"; +import { makeObservable, observable, reaction } from "mobx"; +import { dockStore, TabId } from "./dock.store"; import { cssNames } from "../../utils"; -import { DockTabComponents, dockViewsManager } from "./dock.views-manager"; import { MonacoEditor } from "../monaco-editor"; import throttle from "lodash/throttle"; export interface DockTabContentProps extends React.HTMLAttributes { + tabId: TabId; className?: string; - tab?: DockTab; - bindContainerRef?(elem: HTMLElement): void; + withEditor?: boolean; + editorValue?: string; + editorOnChange?(value: string): void; } @observer export class DockTabContent extends React.Component { - @observable.ref editor: MonacoEditor; + @observable.ref editor?: MonacoEditor; + @observable error = ""; constructor(props: DockTabContentProps) { super(props); makeObservable(this); disposeOnUnmount(this, [ - reaction(() => this.tabId, this.onTabChange, { delay: 100 }), - // keep focus on editor's area when just opened - reaction(() => dockStore.isOpen, isOpen => isOpen && this.focusEditor()), + reaction(() => dockStore.isOpen, isOpen => isOpen && this.editor?.focus()), // focus to editor on dock's resize or turning into fullscreen mode - dockStore.onResize(throttle(() => this.focusEditor(), 250)), + dockStore.onResize(throttle(() => this.editor?.focus(), 250)), ]); } - @computed get tabId(): TabId | undefined { - return this.props.tab?.id; - } - - @computed get tabComponents(): DockTabComponents | undefined { - return dockViewsManager.get(this.props.tab?.kind); - } - - @observable error = ""; - - @computed get editorIsVisible() { - return Boolean(this.tabComponents?.editor); - } - - focusEditor() { - if (!this.editorIsVisible) return; - - this.editor.focus(); - } - - @action - onTabChange = () => { - this.error = ""; // reset any errors from another tab - this.focusEditor(); // focus to editor if available via registered tab views - }; - - /** - * 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: { editor } } = this; - - return ( - { - this.error = ""; - editor?.setValue(tabId, value); - }} - onError={error => { - this.error = this.editorIsVisible ? error : ""; - }} - ref={editor => this.editor = editor} - /> - ); - } - render() { - const { bindContainerRef, className, tab } = this.props; + const { className, tabId, withEditor, editorValue, editorOnChange } = this.props; - if (!tab) return null; - const { InfoPanel, Content } = this.tabComponents; + if (!tabId) return null; return ( -
- {InfoPanel && } - {Content && } - {this.renderEditor()} +
{this.props.children} + + {withEditor && ( + this.error = error} + ref={monaco => this.editor = monaco} + /> + )}
); } diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx index e53df01581..411d533eaf 100644 --- a/src/renderer/components/dock/dock.tsx +++ b/src/renderer/components/dock/dock.tsx @@ -31,7 +31,7 @@ import { createResourceTab } from "./create-resource.store"; import { DockTabs } from "./dock-tabs"; import { dockStore, DockTab } from "./dock.store"; import { createTerminalTab } from "./terminal.store"; -import { DockTabContent } from "./dock-tab-content"; +import { dockViewsManager } from "./dock.views-manager"; interface Props { className?: string; @@ -78,6 +78,7 @@ export class Dock extends React.Component { render() { const { className } = this.props; const { isOpen, toggle, tabs, toggleFillSize, selectedTab, hasTabs, fullSize, height } = dockStore; + const { Content: DockTabContent } = dockViewsManager.get(selectedTab?.kind) ?? {}; return (
{
- + {selectedTab && }
); diff --git a/src/renderer/components/dock/dock.views-manager.ts b/src/renderer/components/dock/dock.views-manager.ts index 94e490c003..537ac00aa4 100644 --- a/src/renderer/components/dock/dock.views-manager.ts +++ b/src/renderer/components/dock/dock.views-manager.ts @@ -21,17 +21,11 @@ import type React from "react"; import { observable } from "mobx"; -import type { TabId, TabKind } from "./dock.store"; +import type { 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; - } } const dockViews = observable.map([], { diff --git a/src/renderer/components/dock/edit-resource.tsx b/src/renderer/components/dock/edit-resource.tsx index d3ee4bdb21..5341efa268 100644 --- a/src/renderer/components/dock/edit-resource.tsx +++ b/src/renderer/components/dock/edit-resource.tsx @@ -24,18 +24,19 @@ import yaml from "js-yaml"; import { makeObservable, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { editResourceStore } from "./edit-resource.store"; -import { InfoPanel, InfoPanelProps } from "./info-panel"; +import { InfoPanel } from "./info-panel"; import { Badge } from "../badge"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { TabKind } from "./dock.store"; import { dockViewsManager } from "./dock.views-manager"; import { createPatch } from "rfc6902"; +import { DockTabContent, DockTabContentProps } from "./dock-tab-content"; -interface Props extends InfoPanelProps { +interface Props extends DockTabContentProps { } @observer -export class EditResourceInfoPanel extends React.Component { +export class EditResource extends React.Component { @observable.ref resource?: KubeObject; constructor(props: Props) { @@ -104,36 +105,36 @@ export class EditResourceInfoPanel extends React.Component { }; render() { - const { resource } = this; + const { resource, tabId } = this; + const tabData = editResourceStore.getData(tabId); if (!resource) return null; return ( - - Kind: - Name: - Namespace: - - )} - /> + tabData.draft = v} + > + + Kind: + Name: + Namespace: + + )} + /> + ); } } dockViewsManager.register(TabKind.EDIT_RESOURCE, { - InfoPanel: EditResourceInfoPanel, - editor: { - getValue(tabId) { - return editResourceStore.getData(tabId)?.draft ?? ""; - }, - setValue(tabId, value) { - editResourceStore.getData(tabId).draft = value; - }, - } + Content: EditResource, }); diff --git a/src/renderer/components/dock/install-chart.tsx b/src/renderer/components/dock/install-chart.tsx index cb8ca7716e..f85672e92c 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, InfoPanelProps } from "./info-panel"; +import { InfoPanel } from "./info-panel"; import { Badge } from "../badge"; import { NamespaceSelect } from "../+namespaces/namespace-select"; import { prevDefault } from "../../utils"; @@ -40,12 +40,13 @@ import { Input } from "../input"; import { navigate } from "../../navigation"; import { releaseURL } from "../../../common/routes"; import { dockViewsManager } from "./dock.views-manager"; +import { DockTabContent, DockTabContentProps } from "./dock-tab-content"; -interface Props extends InfoPanelProps { +interface Props extends DockTabContentProps { } @observer -export class InstallChartInfoPanel extends Component { +export class InstallChart extends Component { @observable showNotes = false; constructor(props: Props) { @@ -159,7 +160,7 @@ export class InstallChartInfoPanel extends Component { return this.renderReleaseDetails(); } - const { chartData, install, versions } = this; + const { chartData, install, versions, tabId } = this; const { repo, name, version, namespace, releaseName } = chartData; const panelControls = ( @@ -194,27 +195,26 @@ export class InstallChartInfoPanel extends Component { ); return ( - + installChartStore.getData(tabId).values = v} + > + + ); } } dockViewsManager.register(TabKind.INSTALL_CHART, { - InfoPanel: InstallChartInfoPanel, - editor: { - getValue(tabId) { - return installChartStore.getData(tabId).values; - }, - setValue(tabId, value) { - installChartStore.getData(tabId).values = value; - }, - } + Content: InstallChart, }); diff --git a/src/renderer/components/dock/logs.tsx b/src/renderer/components/dock/logs.tsx index d7578531e1..eadeb12687 100644 --- a/src/renderer/components/dock/logs.tsx +++ b/src/renderer/components/dock/logs.tsx @@ -52,12 +52,12 @@ export class Logs extends React.Component { componentDidMount() { disposeOnUnmount(this, - reaction(() => this.props.tab.id, this.reload, { fireImmediately: true }), + reaction(() => this.props.tabId, this.reload, { fireImmediately: true }), ); } get tabId() { - return this.props.tab.id; + return this.props.tabId; } load = async () => { diff --git a/src/renderer/components/dock/upgrade-chart.tsx b/src/renderer/components/dock/upgrade-chart.tsx index aea7f0c2da..217795b126 100644 --- a/src/renderer/components/dock/upgrade-chart.tsx +++ b/src/renderer/components/dock/upgrade-chart.tsx @@ -22,7 +22,7 @@ import React from "react"; import { action, computed, makeObservable, observable } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; -import { InfoPanel, InfoPanelProps } from "./info-panel"; +import { InfoPanel } from "./info-panel"; import { Badge } from "../badge"; import { Select, SelectOption } from "../select"; import { dockStore, TabId, TabKind } from "./dock.store"; @@ -30,12 +30,14 @@ import type { IChartVersion } from "../+apps-helm-charts/helm-chart.store"; import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; import { upgradeChartStore } from "./upgrade-chart.store"; import { dockViewsManager } from "./dock.views-manager"; +import { DockTabContent, DockTabContentProps } from "./dock-tab-content"; +import { Spinner } from "../spinner"; -interface Props extends InfoPanelProps { +interface Props extends DockTabContentProps { } @observer -export class UpgradeChartInfoPanel extends React.Component { +export class UpgradeChart extends React.Component { @observable selectedVersion: IChartVersion; constructor(props: Props) { @@ -88,46 +90,45 @@ export class UpgradeChartInfoPanel extends React.Component { render() { if (!upgradeChartStore.isReady(this.tabId)) { - return null; + return ; } - const { release, selectedVersion, chartVersions } = this; + const { release, selectedVersion, chartVersions, tabId } = this; return ( - - Release - Namespace - Version - Upgrade version - ) => this.selectedVersion = value} + /> + + } + /> + ); } } dockViewsManager.register(TabKind.UPGRADE_CHART, { - InfoPanel: UpgradeChartInfoPanel, - editor: { - getValue(tabId): string { - return upgradeChartStore.values.get(tabId); - }, - setValue(tabId, value) { - upgradeChartStore.values.set(tabId, value); - }, - } + Content: UpgradeChart, }); diff --git a/src/renderer/components/monaco-editor/monaco-editor.tsx b/src/renderer/components/monaco-editor/monaco-editor.tsx index d867ac1d56..636d252d02 100644 --- a/src/renderer/components/monaco-editor/monaco-editor.tsx +++ b/src/renderer/components/monaco-editor/monaco-editor.tsx @@ -33,8 +33,10 @@ import debounce from "lodash/debounce"; registerCustomThemes(); // setup +export type MonacoEditorId = string; + export interface MonacoEditorProps { - id?: string; // associating editor's ID with created model.uri + id?: MonacoEditorId; // associating editor's ID with created model.uri value?: string; className?: string; autoFocus?: boolean; @@ -59,7 +61,11 @@ export const defaultEditorProps: Partial = { @observer export class MonacoEditor extends React.Component { static defaultProps = defaultEditorProps as object; - static viewStates = new WeakMap(); + static viewStates = new WeakMap(); + + static createUri(id: MonacoEditorId): Uri { + return Uri.file(`/monaco-editor/${id}`); + } public staticId = `editor-id#${Math.round(1e7 * Math.random())}`; public dispose = disposer(); @@ -68,13 +74,24 @@ export class MonacoEditor extends React.Component { @observable.ref editor: editor.IStandaloneCodeEditor; @observable dimensions: { width?: number, height?: number } = {}; @observable unmounting = false; - validationErrors = observable.array(); constructor(props: MonacoEditorProps) { super(props); makeObservable(this); } + @computed get model(): editor.ITextModel { + const uri = MonacoEditor.createUri(this.props.id ?? this.staticId); + const model = editor.getModels().find(model => model.uri.toString() == uri.toString()); + + if (model) { + return model; // already exists + } + + const { language, value } = this.props; + return editor.createModel(value, language, uri); + } + @computed get options(): editor.IStandaloneEditorConstructionOptions { return toJS({ ...UserStore.getInstance().editorConfiguration, @@ -105,20 +122,31 @@ export class MonacoEditor extends React.Component { onModelChange = (model: editor.ITextModel, oldModel?: editor.ITextModel) => { console.info("[MONACO]: model change", { model, oldModel }); - this.saveViewState(oldModel); // save current view-model state in the editor + this.saveViewState(oldModel); this.editor.setModel(model); - this.editor.restoreViewState(this.getViewState(model)); // restore cursor position, selection, etc. + this.restoreViewState(model); this.editor.layout(); this.editor.focus(); // keep focus in editor, e.g. when clicking between dock-tabs this.validateLazy(); }; - @computed get editorId(): string { - return this.props.id ?? this.staticId; + /** + * Save current view-model state in the editor. + * This will allow restore cursor position, selected text, etc. + * @param {editor.ITextModel} model + */ + saveViewState(model = this.model) { + if (!model) return; + + MonacoEditor.viewStates.set(model.uri, this.editor.saveViewState()); } - @computed get model(): editor.ITextModel { - return this.getModelById(this.editorId); + restoreViewState(model = this.model) { + if (!model) return; + + const viewState = MonacoEditor.viewStates.get(model.uri); + + this.editor.restoreViewState(viewState); } componentDidMount() { @@ -132,6 +160,7 @@ export class MonacoEditor extends React.Component { componentWillUnmount() { this.unmounting = true; + this.saveViewState(); this.destroy(); } @@ -150,8 +179,10 @@ export class MonacoEditor extends React.Component { readOnly, ...this.options, }); + console.info(`[MONACO]: editor created for language=${language}, theme=${theme}`); this.validateLazy(); // validate initial value + this.restoreViewState(); // restore previous state if any if (this.props.autoFocus) { this.editor.focus(); @@ -204,33 +235,6 @@ export class MonacoEditor extends React.Component { this.editor?.layout({ width, height }); } - createUri(id: string): Uri { - return Uri.file(`/monaco-editor/${id}`); - } - - getViewState(model = this.model): editor.ICodeEditorViewState { - return MonacoEditor.viewStates.get(model) ?? null; - } - - saveViewState(model = this.model) { - if (!model) return; - MonacoEditor.viewStates.set(model, this.editor.saveViewState()); - } - - getModelById(id: string): editor.ITextModel | null { - const uri = this.createUri(id); - const model = editor.getModels().find(model => String(model.uri) === String(uri)); - - if (model) { - return model; // model with corresponding props.id exists - } - - // creating new temporary model if not exists regarding to props.ID - const { language, value } = this.props; - - return editor.createModel(value, language, uri); - } - setValue(value = ""): void { if (value == this.getValue()) return; @@ -256,15 +260,11 @@ export class MonacoEditor extends React.Component { validators.push(syntaxValidator); } - // console.info('[MONACO]: validating', { value, validators }); - this.validationErrors.clear(); // reset first - for (const validate of validators) { try { await validate(value); } catch (error) { error = String(error); - this.validationErrors.push(error); this.props.onError?.(error); // emit error outside via callback } }