mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
dock-tabs: create-resoure refactoring & fixes
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
7355f61465
commit
c4aa88183f
@ -22,29 +22,102 @@
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import { autoBind } from "../../utils";
|
|
||||||
import { DockTabStore } from "./dock-tab.store";
|
import { DockTabStore } from "./dock-tab.store";
|
||||||
import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store";
|
import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store";
|
||||||
|
import { action, makeObservable, observable } from "mobx";
|
||||||
|
|
||||||
|
export interface TemplatesGroup {
|
||||||
|
label: string;
|
||||||
|
templates: Record<string/*filename*/, string /*contents*/>;
|
||||||
|
}
|
||||||
|
|
||||||
export class CreateResourceStore extends DockTabStore<string> {
|
export class CreateResourceStore extends DockTabStore<string> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({ storageKey: "create_resource" });
|
||||||
storageKey: "create_resource"
|
makeObservable(this);
|
||||||
});
|
|
||||||
autoBind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async init() {
|
readonly templateGroups: Record<string, TemplatesGroup> = observable({
|
||||||
super.init();
|
[this.appTemplatesFolder]: {
|
||||||
await fs.ensureDir(this.userTemplatesFolder);
|
label: "Lens Templates",
|
||||||
|
templates: {} as Record<string/*filename*/, string /*contents*/>,
|
||||||
|
},
|
||||||
|
[this.userTemplatesFolder]: {
|
||||||
|
label: "Custom templates",
|
||||||
|
templates: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
get appTemplatesFolder(): string {
|
||||||
|
// production: declare files to copy in "package.json" -> "build.extraResources"
|
||||||
|
return path.resolve(__static, "../templates/create-resource");
|
||||||
}
|
}
|
||||||
|
|
||||||
get userTemplatesFolder(): string {
|
get userTemplatesFolder(): string {
|
||||||
return path.resolve(os.homedir(), "~/.k8slens/templates");
|
return path.resolve(os.homedir(), "~/.k8slens/templates");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTemplate(fileName: string, ext = "yaml") {
|
protected async init() {
|
||||||
return import(`../../../../templates/create-resource/${fileName}.${ext}?raw`);
|
super.init();
|
||||||
|
|
||||||
|
// scan all available templates immediately
|
||||||
|
await this.scanTemplates(this.appTemplatesFolder);
|
||||||
|
await this.scanTemplates(this.userTemplatesFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
private async scanTemplates(sourceFolder: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const templatesGroup = this.templateGroups[sourceFolder];
|
||||||
|
|
||||||
|
if (!templatesGroup) return; // exit: unknown templates source folder
|
||||||
|
|
||||||
|
await fs.ensureDir(sourceFolder);
|
||||||
|
const fileNames = await fs.readdir(sourceFolder);
|
||||||
|
|
||||||
|
templatesGroup.templates = Object.fromEntries(
|
||||||
|
fileNames.map(fileName => [fileName, "" /* empty: content not loaded yet */])
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CREATE-RESOURCE]: scanning template folders error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async loadTemplate(init: { fileName: string, sourceFolder: string }): Promise<string> {
|
||||||
|
const { fileName, sourceFolder } = init;
|
||||||
|
|
||||||
|
const templatesGroup = this.templateGroups[sourceFolder];
|
||||||
|
|
||||||
|
if (!templatesGroup) {
|
||||||
|
return ""; // unknown group, exit
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = templatesGroup.templates[fileName];
|
||||||
|
|
||||||
|
if (template) return template; // return cache, preloaded already
|
||||||
|
|
||||||
|
try {
|
||||||
|
const templatePath = path.resolve(sourceFolder, fileName);
|
||||||
|
const pathExists = await fs.pathExists(templatePath);
|
||||||
|
|
||||||
|
if (!pathExists) return ""; // template file not exists, skip
|
||||||
|
|
||||||
|
const textContent = await fs.readFile(templatePath, { encoding: "utf-8" });
|
||||||
|
|
||||||
|
templatesGroup.templates[fileName] = textContent; // save cache
|
||||||
|
|
||||||
|
return textContent;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CREATE-RESOURCE]: reading "${sourceFolder}/${fileName}" has failed: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// bundled app templates via webpack.renderer.ts, "?raw"-query loads content as text
|
||||||
|
async getBundledTemplate(fileName: string, ext = "yaml"): Promise<string> {
|
||||||
|
return import(`${this.appTemplatesFolder}/${fileName}.${ext}?raw`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import "./create-resource.scss";
|
import "./create-resource.scss";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Select, SelectOption } from "../select";
|
import { GroupSelectOption, Select, SelectOption } from "../select";
|
||||||
import jsYaml from "js-yaml";
|
import jsYaml from "js-yaml";
|
||||||
import { computed, makeObservable, observable } from "mobx";
|
import { computed, makeObservable, observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
@ -39,34 +39,31 @@ interface Props {
|
|||||||
tab: DockTab;
|
tab: DockTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelectOptionTemplate = SelectOption<SelectOptionTemplateValue>;
|
||||||
|
|
||||||
|
interface SelectOptionTemplateValue {
|
||||||
|
sourceFolder: string;
|
||||||
|
fileName: string;
|
||||||
|
content?: string; // available after loading (template selection)
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class CreateResource extends React.Component<Props> {
|
export class CreateResource extends React.Component<Props> {
|
||||||
@observable currentTemplates: Map<string, SelectOption> = new Map();
|
|
||||||
@observable error = "";
|
|
||||||
@observable templates = observable.map<string/*filename*/, string>();
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
@observable error = "";
|
||||||
@computed get templateOptions(): SelectOption<string>[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get tabId() {
|
get tabId() {
|
||||||
return this.props.tab.id;
|
return this.props.tab.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get data() {
|
get draft() {
|
||||||
return createResourceStore.getData(this.tabId);
|
return createResourceStore.getData(this.tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectedTemplate() {
|
|
||||||
return this.currentTemplates.get(this.tabId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange = (value: string) => {
|
onChange = (value: string) => {
|
||||||
createResourceStore.setData(this.tabId, value);
|
createResourceStore.setData(this.tabId, value);
|
||||||
};
|
};
|
||||||
@ -75,24 +72,14 @@ export class CreateResource extends React.Component<Props> {
|
|||||||
this.error = error;
|
this.error = error;
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME
|
|
||||||
onSelectTemplate = (item: SelectOption) => {
|
|
||||||
console.log(`SELECTED TEMPLATE: ${this.tabId}`, item);
|
|
||||||
// this.currentTemplates.set(this.tabId, item);
|
|
||||||
//
|
|
||||||
// fs.readFile(item.value, "utf8").then(templateFileContent => {
|
|
||||||
// createResourceStore.setData(this.tabId, templateFileContent);
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
|
|
||||||
create = async () => {
|
create = async () => {
|
||||||
if (this.error || !this.data.trim()) {
|
if (this.error || !this.draft) {
|
||||||
// do not save when field is empty or there is an error
|
// do not save when field is empty or there is an error
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip empty documents if "---" pasted at the beginning or end
|
// skip empty documents if "---" pasted at the beginning or end
|
||||||
const resources = jsYaml.safeLoadAll(this.data).filter(Boolean);
|
const resources = jsYaml.safeLoadAll(this.draft).filter(Boolean);
|
||||||
const createdResources: string[] = [];
|
const createdResources: string[] = [];
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
@ -125,20 +112,41 @@ export class CreateResource extends React.Component<Props> {
|
|||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
<Select
|
<Select
|
||||||
autoConvertOptions={false}
|
autoConvertOptions={false}
|
||||||
|
controlShouldRenderValue={false} // always keep initial placeholder
|
||||||
className="SelectResourceTemplate"
|
className="SelectResourceTemplate"
|
||||||
placeholder="Select Template ..."
|
placeholder="Select Template ..."
|
||||||
options={this.templateOptions}
|
|
||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
themeName="outlined"
|
themeName="outlined"
|
||||||
|
options={this.templateOptions}
|
||||||
onChange={v => this.onSelectTemplate(v)}
|
onChange={v => this.onSelectTemplate(v)}
|
||||||
value={this.selectedTemplate}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get templateOptions(): GroupSelectOption<SelectOptionTemplate>[] {
|
||||||
|
return Object.entries(createResourceStore.templateGroups).map(([sourceFolder, group]) => {
|
||||||
|
return {
|
||||||
|
label: group.label,
|
||||||
|
options: Object.entries(group.templates).map(([fileName]) => {
|
||||||
|
return {
|
||||||
|
label: fileName,
|
||||||
|
value: { fileName, sourceFolder },
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectTemplate = async ({ value }: SelectOptionTemplate) => {
|
||||||
|
const { fileName, sourceFolder, content } = value;
|
||||||
|
const templateContent = content ?? await createResourceStore.loadTemplate({ fileName, sourceFolder });
|
||||||
|
|
||||||
|
createResourceStore.setData(this.tabId, templateContent); // update draft
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { tabId, data, error, create } = this;
|
const { tabId, draft, error, create } = this;
|
||||||
const { className } = this.props;
|
const { className } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -153,7 +161,7 @@ export class CreateResource extends React.Component<Props> {
|
|||||||
/>
|
/>
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
id={tabId}
|
id={tabId}
|
||||||
value={data}
|
value={draft}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onError={this.onError}
|
onError={this.onError}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -65,8 +65,6 @@ export const defaultEditorProps: Partial<MonacoEditorProps> = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: apply changes of props.options and globalOptions.editor to active editor
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class MonacoEditor extends React.Component<MonacoEditorProps> {
|
export class MonacoEditor extends React.Component<MonacoEditorProps> {
|
||||||
static defaultProps = defaultEditorProps as object;
|
static defaultProps = defaultEditorProps as object;
|
||||||
@ -186,6 +184,10 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
|
|||||||
|
|
||||||
this.disposeOnUnmount.push(
|
this.disposeOnUnmount.push(
|
||||||
reaction(() => this.model, this.onModelChange),
|
reaction(() => this.model, this.onModelChange),
|
||||||
|
reaction(() => this.props.theme, editor.setTheme),
|
||||||
|
reaction(() => this.props.value, value => this.setValue(value)),
|
||||||
|
reaction(() => toJS(this.props.options), opts => this.editor.updateOptions(opts)),
|
||||||
|
|
||||||
() => onDidLayoutChangeDisposer.dispose(),
|
() => onDidLayoutChangeDisposer.dispose(),
|
||||||
() => onValueChangeDisposer.dispose(),
|
() => onValueChangeDisposer.dispose(),
|
||||||
() => onContentSizeChangeDisposer.dispose(),
|
() => onContentSizeChangeDisposer.dispose(),
|
||||||
@ -237,6 +239,8 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setValue(value: string) {
|
setValue(value: string) {
|
||||||
|
if (value == this.getValue()) return;
|
||||||
|
|
||||||
this.editor.setValue(value);
|
this.editor.setValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user