1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

refactoring dock-tab stores -- part 2

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-10-04 16:07:49 +03:00
parent 5c41b8612e
commit 5038267bbc
13 changed files with 188 additions and 214 deletions

View File

@ -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<string> {
export class CreateResourceStore extends DockTabStore<string> {
constructor() {
super({
storageKey: "create_resource"
@ -43,8 +43,8 @@ export class CreateResourceStore extends DockTabsStore<string> {
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`);
}
}

View File

@ -51,7 +51,7 @@ export class CreateResource extends React.Component<Props> {
}
// TODO
@computed get selectTemplateOptions(): SelectOption<string>[] {
@computed get templateOptions(): SelectOption<string>[] {
return [];
}
@ -127,7 +127,7 @@ export class CreateResource extends React.Component<Props> {
autoConvertOptions={false}
className="SelectResourceTemplate"
placeholder="Select Template ..."
options={this.selectTemplateOptions}
options={this.templateOptions}
menuPlacement="top"
themeName="outlined"
onChange={v => this.onSelectTemplate(v)}

View File

@ -28,18 +28,20 @@ export interface DockTabStoreOptions {
storageKey?: string; // save data to persistent storage under the key
}
export class DockTabsStore<T extends {}> {
export class DockTabStore<T extends {}> {
@observable private _data: Record<TabId, T> = {};
@observable.ref private storage?: StorageHelper<Record<TabId, T>>; // available only with `options.storageKey`
@observable dataReady = false; // dock-tab's data ready to interact, e.g. start editing resource
protected watchers = observable.map<TabId, IReactionDisposer | Disposer>();
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<TabId, T>) {
@ -60,7 +62,7 @@ export class DockTabsStore<T extends {}> {
};
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<T extends {}> {
}
get whenReady(): Promise<any> {
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<T extends {}> {
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<T extends {}> {
}
destroy() {
this.dataReady = false;
this.dispose();
this.disposeWatchers();
this.reset();

View File

@ -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,

View File

@ -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<EditingResource> {
export class EditResourceStore extends DockTabStore<EditingResource> {
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<KubeObject> | undefined {
@ -74,8 +59,8 @@ export class EditResourceStore extends DockTabsStore<EditingResource> {
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 {

View File

@ -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<Props> {
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<Props> {
};
render() {
const { tabId, error, draft, isReadyForEditing, resource } = this;
if (!isReadyForEditing) {
if (!editResourceStore.dataReady || !this.resource) {
return <Spinner center/>;
}
const { tabId, error, draft, resource } = this;
return (
<div className={cssNames("EditResource flex column", this.props.className)}>
<InfoPanel

View File

@ -21,10 +21,9 @@
import { action, makeObservable, observable } from "mobx";
import { dockStore, DockTabCreateSpecific, TabId, TabKind } from "./dock.store";
import { DockTabsStore } from "./dock-tabs.store";
import { DockTabStore } from "./dock-tab.store";
import { getChartDetails, getChartValues, HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
import type { IReleaseUpdateDetails } from "../../../common/k8s-api/endpoints/helm-releases.api";
import { Notifications } from "../notifications";
export interface IChartInstallData {
name: string;
@ -37,7 +36,7 @@ export interface IChartInstallData {
lastVersion?: boolean;
}
export class InstallChartStore extends DockTabsStore<IChartInstallData> {
export class InstallChartStore extends DockTabStore<IChartInstallData> {
details = observable.map<TabId, IReleaseUpdateDetails>();
versions = observable.map<TabId, string[]>();
@ -48,22 +47,8 @@ export class InstallChartStore extends DockTabsStore<IChartInstallData> {
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) {

View File

@ -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<Props> {
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<Props> {
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<Props> {
);
};
render() {
const { tabId, chartData, values, versions, install } = this;
renderReleaseDetails(): React.ReactNode {
return (
<div className="InstallChartDone flex column gaps align-center justify-center">
<p>
<Icon material="check" big sticker/>
</p>
<p>Installation complete!</p>
<div className="flex gaps align-center">
<Button
autoFocus primary
label="View Helm Release"
onClick={prevDefault(this.viewRelease)}
/>
<Button
plain active
label="Show Notes"
onClick={() => this.showNotes = true}
/>
</div>
<LogsDialog
title="Helm Chart Install"
isOpen={this.showNotes}
close={() => this.showNotes = false}
logs={this.releaseDetails.log}
/>
</div>
);
}
if (chartData?.values === undefined || !versions) {
render() {
if (!this.isReady) {
return <Spinner center/>;
}
if (this.releaseDetails) {
return (
<div className="InstallChartDone flex column gaps align-center justify-center">
<p>
<Icon material="check" big sticker/>
</p>
<p>Installation complete!</p>
<div className="flex gaps align-center">
<Button
autoFocus primary
label="View Helm Release"
onClick={prevDefault(this.viewRelease)}
/>
<Button
plain active
label="Show Notes"
onClick={() => this.showNotes = true}
/>
</div>
<LogsDialog
title="Helm Chart Install"
isOpen={this.showNotes}
close={() => this.showNotes = false}
logs={this.releaseDetails.log}
/>
</div>
);
return this.renderReleaseDetails();
}
const { tabId, chartData, install, versions } = this;
const { repo, name, version, namespace, releaseName } = chartData;
const panelControls = (
<div className="install-controls flex gaps align-center">
@ -208,7 +215,7 @@ export class InstallChart extends Component<Props> {
/>
<MonacoEditor
id={tabId}
value={values}
value={chartData.values}
onChange={this.onValuesChange}
onError={error => this.error = error}
/>

View File

@ -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<LogTabData> {
export class LogTabStore extends DockTabStore<LogTabData> {
constructor() {
super({
storageKey: "pod_logs"

View File

@ -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<IChartUpgradeData> {
export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
public values = observable.map<TabId, string>();
public versions = observable.array<IChartVersion>();
constructor() {
super({
@ -41,64 +44,65 @@ export class UpgradeChartStore extends DockTabsStore<IChartUpgradeData> {
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;
}
}

View File

@ -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<Props> {
@observable error: string;
@observable versions = observable.array<IChartVersion>();
@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<Props> {
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<IChartVersion>[] {
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<Props> {
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<Props> {
};
render() {
const { tabId, release, value, error, versions, version } = this;
const { className } = this.props;
if (!release || upgradeChartStore.isLoading() || !version) {
if (!this.isReady) {
return <Spinner center/>;
}
const { className } = this.props;
const { tabId, release, value, error, versionOptions, selectedVersion } = this;
const currentVersion = release.getVersion();
const controlsAndInfo = (
<div className="upgrade flex gaps align-center">
<span>Release</span> <Badge label={release.getName()}/>
@ -138,10 +132,10 @@ export class UpgradeChart extends React.Component<Props> {
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}
/>
</div>
);

View File

@ -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<MonacoEditorProps> = {
}
};
// 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<MonacoEditorProps> {
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<MonacoEditorProps> {
}
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 });
}
}

View File

@ -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"