diff --git a/src/renderer/components/dock/create-resource.store.ts b/src/renderer/components/dock/create-resource.store.ts index 28a0041bb0..48a5ad9891 100644 --- a/src/renderer/components/dock/create-resource.store.ts +++ b/src/renderer/components/dock/create-resource.store.ts @@ -23,10 +23,10 @@ import fs from "fs-extra"; import path from "path"; import os from "os"; import { autoBind } from "../../utils"; -import { DockTabsStore } from "./dock-tabs.store"; +import { DockTabStore } from "./dock-tab.store"; import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store"; -export class CreateResourceStore extends DockTabsStore { +export class CreateResourceStore extends DockTabStore { constructor() { super({ storageKey: "create_resource" @@ -43,8 +43,8 @@ export class CreateResourceStore extends DockTabsStore { return path.resolve(os.homedir(), "~/.k8slens/templates"); } - async getBundledTemplate(fileName: string, ext = "yaml") { - return import(`../../../../templates/create-resource/${fileName}.${ext}`); + async getTemplate(fileName: string, ext = "yaml") { + return import(`../../../../templates/create-resource/${fileName}.${ext}?raw`); } } diff --git a/src/renderer/components/dock/create-resource.tsx b/src/renderer/components/dock/create-resource.tsx index dd9da94fb4..069910ff8b 100644 --- a/src/renderer/components/dock/create-resource.tsx +++ b/src/renderer/components/dock/create-resource.tsx @@ -51,7 +51,7 @@ export class CreateResource extends React.Component { } // TODO - @computed get selectTemplateOptions(): SelectOption[] { + @computed get templateOptions(): SelectOption[] { return []; } @@ -127,7 +127,7 @@ export class CreateResource extends React.Component { autoConvertOptions={false} className="SelectResourceTemplate" placeholder="Select Template ..." - options={this.selectTemplateOptions} + options={this.templateOptions} menuPlacement="top" themeName="outlined" onChange={v => this.onSelectTemplate(v)} diff --git a/src/renderer/components/dock/dock-tabs.store.ts b/src/renderer/components/dock/dock-tab.store.ts similarity index 90% rename from src/renderer/components/dock/dock-tabs.store.ts rename to src/renderer/components/dock/dock-tab.store.ts index 9db08222b3..01f62e7d5b 100644 --- a/src/renderer/components/dock/dock-tabs.store.ts +++ b/src/renderer/components/dock/dock-tab.store.ts @@ -28,18 +28,20 @@ export interface DockTabStoreOptions { storageKey?: string; // save data to persistent storage under the key } -export class DockTabsStore { +export class DockTabStore { @observable private _data: Record = {}; @observable.ref private storage?: StorageHelper>; // available only with `options.storageKey` + @observable dataReady = false; // dock-tab's data ready to interact, e.g. start editing resource + protected watchers = observable.map(); protected dispose = disposer(); @computed get data() { if (this.options.storageKey) { return this.storage.get(); - } else { - return this._data; } + + return this._data; } set data(value: Record) { @@ -60,7 +62,7 @@ export class DockTabsStore { }; if (this.options.storageKey) { - this.storage = createStorage(this.options.storageKey, {}); // starts preloading json file via Node.fs right away + this.storage = createStorage(this.options.storageKey, this._data); } if (this.options.autoInit) { @@ -69,13 +71,15 @@ export class DockTabsStore { } get whenReady(): Promise { - return Promise.allSettled([ + return Promise.all([ dockStore.whenReady, this.storage?.whenReady, ]); } protected init() { + this.dataReady = true; + this.dispose.push( autorun(() => { const docTabIds = dockStore.tabs.map(tab => tab.id); @@ -110,16 +114,12 @@ export class DockTabsStore { return reaction(() => this.data[tabId], callback, opts); } - isReady(tabId: TabId): boolean { - return Boolean(this.getData(tabId) !== undefined); - } - - getData(tabId: TabId) { - return this.data[tabId]; + getData(tabId: TabId): T { + return this.data[tabId] ?? {} as T; } setData(tabId: TabId, data: T) { - this.data[tabId] = data; + this.data[tabId] = { ...data }; } clearData(tabId: TabId) { @@ -131,6 +131,7 @@ export class DockTabsStore { } destroy() { + this.dataReady = false; this.dispose(); this.disposeWatchers(); this.reset(); diff --git a/src/renderer/components/dock/dock.store.ts b/src/renderer/components/dock/dock.store.ts index 6f1e378932..264648e98f 100644 --- a/src/renderer/components/dock/dock.store.ts +++ b/src/renderer/components/dock/dock.store.ts @@ -96,7 +96,7 @@ export interface DockTabChangeEvent { export interface DockTabChangeEventOptions extends IReactionOptions { kind?: TabKind; // filter: matching by dockStore.selectedTab.kind - isVisible?: boolean; // filter: dock and selected tab should be visible + dockIsVisible?: boolean; // filter: dock and selected tab should be visible (default: true) } export class DockStore implements DockStorageState { @@ -190,11 +190,11 @@ export class DockStore implements DockStorageState { } onTabChange(callback?: (evt: DockTabChangeEvent) => void, options: DockTabChangeEventOptions = {}) { - const { kind, isVisible, ...reactionOpts } = options; + const { kind, dockIsVisible = true, ...reactionOpts } = options; return reaction(() => this.selectedTab, ((tab, prevTab) => { if (kind && kind !== tab.kind) return; - if (isVisible && !dockStore.isOpen) return; + if (dockIsVisible && !dockStore.isOpen) return; callback({ tab, prevTab, diff --git a/src/renderer/components/dock/edit-resource.store.ts b/src/renderer/components/dock/edit-resource.store.ts index 181c704eaa..3614995436 100644 --- a/src/renderer/components/dock/edit-resource.store.ts +++ b/src/renderer/components/dock/edit-resource.store.ts @@ -19,8 +19,9 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autoBind, noop } from "../../utils"; -import { DockTabsStore } from "./dock-tabs.store"; +import { makeObservable } from "mobx"; +import { autoBind } from "../../utils"; +import { DockTabStore } from "./dock-tab.store"; import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { apiManager } from "../../../common/k8s-api/api-manager"; @@ -31,39 +32,23 @@ export interface EditingResource { draft?: string; // edited draft in yaml } -export class EditResourceStore extends DockTabsStore { +export class EditResourceStore extends DockTabStore { constructor() { super({ storageKey: "edit_resource_store", }); autoBind(this); + makeObservable(this); } - protected async init() { - super.init(); + async loadResource(tabId: TabId) { + const resource = this.getResource(tabId); + const store = this.getStore(tabId); + const data = this.getData(tabId); - this.dispose.push( - dockStore.onTabChange(({ selectedTabId }) => { - const { resource } = this.getData(selectedTabId); - const store = apiManager.getStore(resource); - - if (!store?.getByPath(resource)) { - store?.loadFromPath(resource).catch(noop); // preload resource by uri - } - }, { - fireImmediately: true, - }) - ); - } - - protected finalizeDataForSave({ draft, ...data }: EditingResource): EditingResource { - return data; // skip saving draft to local-storage - } - - isReady(tabId: TabId) { - const tabDataReady = super.isReady(tabId); - - return Boolean(tabDataReady && this.getResource(tabId)); // ready to edit resource + if (!resource && store && data) { + await store.loadFromPath(data.resource); // preloading resource for the first time + } } getStore(tabId: TabId): KubeObjectStore | undefined { @@ -74,8 +59,8 @@ export class EditResourceStore extends DockTabsStore { return this.getStore(tabId)?.getByPath(this.getResourcePath(tabId)); } - getResourcePath(tabId: TabId): string | undefined { - return this.getData(tabId)?.resource; + getResourcePath(tabId: TabId): string { + return this.getData(tabId)?.resource ?? ""; } getTabByResource(object: KubeObject): DockTab { diff --git a/src/renderer/components/dock/edit-resource.tsx b/src/renderer/components/dock/edit-resource.tsx index 8c67780cf2..eaf06764e0 100644 --- a/src/renderer/components/dock/edit-resource.tsx +++ b/src/renderer/components/dock/edit-resource.tsx @@ -22,8 +22,8 @@ import "./edit-resource.scss"; import React from "react"; -import { action, computed, makeObservable, observable } from "mobx"; -import { observer } from "mobx-react"; +import { action, autorun, makeObservable, observable } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; import jsYaml from "js-yaml"; import type { DockTab } from "./dock.store"; import { cssNames } from "../../utils"; @@ -46,32 +46,32 @@ export class EditResource extends React.Component { constructor(props: Props) { super(props); makeObservable(this); + + disposeOnUnmount(this, [ + autorun(() => editResourceStore.loadResource(this.tabId)), + ]); } get tabId() { return this.props.tab.id; } - get isReadyForEditing() { - return editResourceStore.isReady(this.tabId); - } - get resource(): KubeObject | undefined { return editResourceStore.getResource(this.tabId); } - @computed get draft(): string { - if (!this.isReadyForEditing) { - return ""; // wait until tab's data and kube-object resource are loaded - } - + get draft(): string { const { draft } = editResourceStore.getData(this.tabId); if (typeof draft === "string") { - return draft; + return draft; // get previously edited draft } - return jsYaml.safeDump(this.resource.toPlainObject()); // dump resource first time + if (this.resource) { + return jsYaml.safeDump(this.resource.toPlainObject()); // dump resource first time + } + + return ""; } @action @@ -113,12 +113,12 @@ export class EditResource extends React.Component { }; render() { - const { tabId, error, draft, isReadyForEditing, resource } = this; - - if (!isReadyForEditing) { + if (!editResourceStore.dataReady || !this.resource) { return ; } + const { tabId, error, draft, resource } = this; + return (
{ +export class InstallChartStore extends DockTabStore { details = observable.map(); versions = observable.map(); @@ -48,22 +47,8 @@ export class InstallChartStore extends DockTabsStore { makeObservable(this); } - protected init() { - super.init(); - - this.dispose.push( - dockStore.onTabChange(({ selectedTabId }) => { - this.loadData(selectedTabId).catch(err => Notifications.error(String(err))); - }, { - kind: TabKind.INSTALL_CHART, - fireImmediately: true, - isVisible: true, - }), - ); - } - @action - async loadData(tabId: string) { + async loadData(tabId: TabId) { const promises = []; if (!this.getData(tabId).values) { diff --git a/src/renderer/components/dock/install-chart.tsx b/src/renderer/components/dock/install-chart.tsx index 73b38a7a0e..5e3060bc62 100644 --- a/src/renderer/components/dock/install-chart.tsx +++ b/src/renderer/components/dock/install-chart.tsx @@ -22,8 +22,8 @@ import "./install-chart.scss"; import React, { Component } from "react"; -import { makeObservable, observable } from "mobx"; -import { observer } from "mobx-react"; +import { autorun, computed, makeObservable, observable } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; import { dockStore, DockTab } from "./dock.store"; import { InfoPanel } from "./info-panel"; import { Badge } from "../badge"; @@ -53,20 +53,20 @@ export class InstallChart extends Component { constructor(props: Props) { super(props); makeObservable(this); - } - get values() { - return this.chartData.values; - } - - get chartData() { - return installChartStore.getData(this.tabId); + disposeOnUnmount(this, [ + autorun(() => installChartStore.loadData(this.tabId)), + ]); } get tabId() { return this.props.tab.id; } + get chartData(): IChartInstallData | undefined { + return installChartStore.getData(this.tabId); + } + get versions() { return installChartStore.versions.get(this.tabId); } @@ -75,6 +75,10 @@ export class InstallChart extends Component { return installChartStore.details.get(this.tabId); } + @computed get isReady() { + return [installChartStore.dataReady, this.versions].every(Boolean); + } + viewRelease = () => { const { release } = this.releaseDetails; @@ -127,42 +131,45 @@ export class InstallChart extends Component { ); }; - render() { - const { tabId, chartData, values, versions, install } = this; + renderReleaseDetails(): React.ReactNode { + return ( +
+

+ +

+

Installation complete!

+
+
+ this.showNotes = false} + logs={this.releaseDetails.log} + /> +
+ ); + } - if (chartData?.values === undefined || !versions) { + render() { + if (!this.isReady) { return ; } if (this.releaseDetails) { - return ( -
-

- -

-

Installation complete!

-
-
- this.showNotes = false} - logs={this.releaseDetails.log} - /> -
- ); + return this.renderReleaseDetails(); } + const { tabId, chartData, install, versions } = this; const { repo, name, version, namespace, releaseName } = chartData; const panelControls = (
@@ -208,7 +215,7 @@ export class InstallChart extends Component { /> this.error = error} /> diff --git a/src/renderer/components/dock/log-tab.store.ts b/src/renderer/components/dock/log-tab.store.ts index 55bbb549f8..e9614c740f 100644 --- a/src/renderer/components/dock/log-tab.store.ts +++ b/src/renderer/components/dock/log-tab.store.ts @@ -25,7 +25,7 @@ import { podsStore } from "../+workloads-pods/pods.store"; import { IPodContainer, Pod } from "../../../common/k8s-api/endpoints"; import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object"; -import { DockTabsStore } from "./dock-tabs.store"; +import { DockTabStore } from "./dock-tab.store"; import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store"; export interface LogTabData { @@ -45,7 +45,7 @@ interface WorkloadLogsTabData { workload: WorkloadKubeObject } -export class LogTabStore extends DockTabsStore { +export class LogTabStore extends DockTabStore { constructor() { super({ storageKey: "pod_logs" diff --git a/src/renderer/components/dock/upgrade-chart.store.ts b/src/renderer/components/dock/upgrade-chart.store.ts index d58727ee7d..5ee6fa1416 100644 --- a/src/renderer/components/dock/upgrade-chart.store.ts +++ b/src/renderer/components/dock/upgrade-chart.store.ts @@ -19,19 +19,22 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { action, makeObservable, observable, when } from "mobx"; +import { action, makeObservable, observable } from "mobx"; import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store"; -import { DockTabsStore } from "./dock-tabs.store"; +import { DockTabStore } from "./dock-tab.store"; import { getReleaseValues, HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; import { releaseStore } from "../+apps-releases/release.store"; +import type { IChartVersion } from "../+apps-helm-charts/helm-chart.store"; +import { helmChartStore } from "../+apps-helm-charts/helm-chart.store"; export interface IChartUpgradeData { releaseName: string; releaseNamespace: string; } -export class UpgradeChartStore extends DockTabsStore { +export class UpgradeChartStore extends DockTabStore { public values = observable.map(); + public versions = observable.array(); constructor() { super({ @@ -41,64 +44,65 @@ export class UpgradeChartStore extends DockTabsStore { makeObservable(this); } - get whenReady() { - return Promise.all([ - super.whenReady, - when(() => releaseStore.isLoaded), - ]); - } - protected init() { super.init(); - - this.dispose.push( - dockStore.onTabChange(({ selectedTabId }) => { - if (!this.isLoaded(selectedTabId)) { - this.loadData(selectedTabId); // preload just once - } - }, { - kind: TabKind.UPGRADE_CHART, - isVisible: true, - fireImmediately: true, - }), - ); + releaseStore.loadFromContextNamespaces(); } - isLoading(tabId = dockStore.selectedTabId) { - const values = this.values.get(tabId); - - return !releaseStore.isLoaded || values === undefined; - } - - isLoaded(tabId: TabId) { + hasValues(tabId: TabId) { return this.values.has(tabId); } - @action - private async loadData(tabId: TabId) { - const values = this.values.get(tabId); - + async loadData(tabId: TabId) { await Promise.all([ - !releaseStore.isLoaded && releaseStore.loadFromContextNamespaces(), - !values && this.reloadValues(tabId) + this.loadValues(tabId), + this.loadVersions(tabId), ]); } - @action - private async reloadValues(tabId: TabId) { - this.values.delete(tabId); - const { releaseName, releaseNamespace } = this.getData(tabId); - const values = await getReleaseValues(releaseName, releaseNamespace, true); + getRelease(tabId: TabId) { + const releaseName = this.getData(tabId)?.releaseName; - this.values.set(tabId, values); + if (!releaseName) return null; + + return releaseStore.getByName(releaseName); } - getTabByRelease(releaseName: string): DockTab { - const entry = Object - .entries(this.data) - .find(([, data]) => data.releaseName === releaseName); + @action + async loadVersions(tabId: TabId) { + try { + const { releaseName } = this.getData(tabId); - return dockStore.getTabById(entry?.[0]); + this.versions.clear(); + const versions = await helmChartStore.getVersions(releaseName); + + this.versions.replace(versions); + } catch (error) { + console.error(`[UPGRADE-CHART]: loading versions has failed: ${error}`); + } + } + + @action + async loadValues(tabId: TabId, { forceReload = false } = {}) { + if (this.hasValues(tabId) && !forceReload) { + return; + } + + try { + this.values.delete(tabId); + const { releaseName, releaseNamespace } = this.getData(tabId); + const values = await getReleaseValues(releaseName, releaseNamespace, true); + + this.values.set(tabId, values); + } catch (error) { + console.error(`[UPGRADE-CHART]: loading values has failed: ${error}`); + } + } + + getTabByRelease(releaseName: string): DockTab | null { + const entry = Object.entries(this.data).find(([, data]) => data.releaseName === releaseName); + + return entry ? dockStore.getTabById(entry[0]) : null; } } diff --git a/src/renderer/components/dock/upgrade-chart.tsx b/src/renderer/components/dock/upgrade-chart.tsx index 28ca5e36c0..85cd8ccbe9 100644 --- a/src/renderer/components/dock/upgrade-chart.tsx +++ b/src/renderer/components/dock/upgrade-chart.tsx @@ -22,7 +22,7 @@ import "./upgrade-chart.scss"; import React from "react"; -import { makeObservable, observable, reaction } from "mobx"; +import { autorun, computed, makeObservable, observable } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { cssNames } from "../../utils"; import type { DockTab } from "./dock.store"; @@ -32,9 +32,9 @@ import { Spinner } from "../spinner"; import { releaseStore } from "../+apps-releases/release.store"; import { Badge } from "../badge"; import { MonacoEditor } from "../monaco-editor"; -import { helmChartStore, IChartVersion } from "../+apps-helm-charts/helm-chart.store"; -import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; import { Select, SelectOption } from "../select"; +import type { IChartVersion } from "../+apps-helm-charts/helm-chart.store"; +import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; interface Props { className?: string; @@ -44,19 +44,14 @@ interface Props { @observer export class UpgradeChart extends React.Component { @observable error: string; - @observable versions = observable.array(); - @observable version: IChartVersion; + @observable selectedVersion: IChartVersion; constructor(props: Props) { super(props); makeObservable(this); - } - - componentDidMount() { - this.loadVersions(); disposeOnUnmount(this, [ - reaction(() => this.release, () => this.loadVersions()) + autorun(() => upgradeChartStore.loadData(this.tabId)), ]); } @@ -64,28 +59,26 @@ export class UpgradeChart extends React.Component { return this.props.tab.id; } - get release(): HelmRelease { - const tabData = upgradeChartStore.getData(this.tabId); + get release(): HelmRelease | null { + return upgradeChartStore.getRelease(this.tabId); + } - if (!tabData) return null; + get isReady(){ + return [ + upgradeChartStore.dataReady, + this.release, + this.selectedVersion, + ].every(Boolean); + } - return releaseStore.getByName(tabData.releaseName); + @computed get versionOptions(): SelectOption[] { + return upgradeChartStore.versions.map(version => ({ value: version })); } get value() { return upgradeChartStore.values.get(this.tabId); } - async loadVersions() { - if (!this.release) return; - this.version = null; - this.versions.clear(); - const versions = await helmChartStore.getVersions(this.release.getChart()); - - this.versions.replace(versions); - this.version = this.versions[0]; - } - onChange = (value: string) => { upgradeChartStore.values.set(this.tabId, value); }; @@ -96,7 +89,7 @@ export class UpgradeChart extends React.Component { upgrade = async () => { if (this.error) return null; - const { version, repo } = this.version; + const { version, repo } = this.selectedVersion; const releaseName = this.release.getName(); const releaseNs = this.release.getNs(); @@ -121,13 +114,14 @@ export class UpgradeChart extends React.Component { }; render() { - const { tabId, release, value, error, versions, version } = this; - const { className } = this.props; - - if (!release || upgradeChartStore.isLoading() || !version) { + if (!this.isReady) { return ; } + + const { className } = this.props; + const { tabId, release, value, error, versionOptions, selectedVersion } = this; const currentVersion = release.getVersion(); + const controlsAndInfo = (
Release @@ -138,10 +132,10 @@ export class UpgradeChart extends React.Component { className="chart-version" menuPlacement="top" themeName="outlined" - value={version} - options={versions} + value={selectedVersion} + options={versionOptions} formatOptionLabel={this.formatVersionLabel} - onChange={({ value }: SelectOption) => this.version = value} + onChange={({ value }: SelectOption) => this.selectedVersion = value} />
); diff --git a/src/renderer/components/monaco-editor/monaco-editor.tsx b/src/renderer/components/monaco-editor/monaco-editor.tsx index c5f1586649..f38031b1de 100644 --- a/src/renderer/components/monaco-editor/monaco-editor.tsx +++ b/src/renderer/components/monaco-editor/monaco-editor.tsx @@ -21,7 +21,7 @@ import "./monaco-editor.scss"; import React from "react"; -import { action, computed, makeObservable, observable, reaction, when } from "mobx"; +import { action, computed, makeObservable, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import { editor, Uri } from "monaco-editor"; import { ThemeStore } from "../../theme.store"; @@ -65,7 +65,6 @@ export const defaultEditorProps: Partial = { } }; -// FIXME: load resource templates via webpack's dynamic import for create-resource dock tab // FIXME: apply changes of props.options and globalOptions.editor to active editor @observer @@ -86,14 +85,6 @@ export class MonacoEditor extends React.Component { makeObservable(this); } - get whenEditorReady() { - return when(() => Boolean(this.containerElem && this.editor)); - } - - get whenUnmounting() { - return when(() => this.unmounting); - } - /** * Monitor editor's dom container element box-size and sync with monaco's dimensions * @private @@ -141,7 +132,7 @@ export class MonacoEditor extends React.Component { } console.info(`[MONACO]: editor did mounted`); } catch (error) { - console.error(`[MONACO]: mounting failed`, { error, editor: this }); + console.error(`[MONACO]: mounting failed`, { error, editor, component: this }); } } diff --git a/webpack.renderer.ts b/webpack.renderer.ts index 345fc95b80..0b6b5bf74c 100755 --- a/webpack.renderer.ts +++ b/webpack.renderer.ts @@ -88,6 +88,13 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura }, module: { rules: [ + { + // Allow to load raw file contents with dynamic imports, e.g. import("../some-path.txt?raw") + // See also: https://webpack.js.org/guides/asset-modules/ + use: "raw-loader", + resourceQuery: /raw/, + enforce: "post", + }, { test: /\.node$/, use: "node-loader"