mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Full dependency inversion of <Dock> and all current tab kinds (#4757)
Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Co-authored-by: Sebastian Malton <sebastian@malton.name> Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
152 files changed
+3423
-2617
No files matched your search
+21
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { TabId } from "../dock/store";
|
||||
import createResourceTabStoreInjectable from "./store.injectable";
|
||||
|
||||
const clearCreateResourceTabDataInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const createResourceTabStore = di.inject(createResourceTabStoreInjectable);
|
||||
|
||||
return (tabId: TabId): void => {
|
||||
createResourceTabStore.clearData(tabId);
|
||||
};
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default clearCreateResourceTabDataInjectable;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import dockStoreInjectable from "../dock/store.injectable";
|
||||
import { DockTabCreateSpecific, TabKind } from "../dock/store";
|
||||
|
||||
const createResourceTabInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const dockStore = di.inject(dockStoreInjectable);
|
||||
|
||||
return (tabParams: DockTabCreateSpecific = {}) =>
|
||||
dockStore.createTab({
|
||||
title: "Create resource",
|
||||
...tabParams,
|
||||
kind: TabKind.CREATE_RESOURCE,
|
||||
});
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createResourceTabInjectable;
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import type { GroupSelectOption, SelectOption } from "../../select";
|
||||
import userCreateResourceTemplatesInjectable from "./user-templates.injectable";
|
||||
import lensCreateResourceTemplatesInjectable from "./lens-templates.injectable";
|
||||
|
||||
export type RawTemplates = [group: string, items: [file: string, contents: string][]];
|
||||
|
||||
const createResourceTemplatesInjectable = getInjectable({
|
||||
instantiate: async (di) => {
|
||||
const lensResourceTemplates = await di.inject(lensCreateResourceTemplatesInjectable);
|
||||
const userResourceTemplates = di.inject(userCreateResourceTemplatesInjectable);
|
||||
|
||||
return computed(() => {
|
||||
const res = [
|
||||
...userResourceTemplates.get(),
|
||||
lensResourceTemplates,
|
||||
];
|
||||
|
||||
return res.map(([group, items]) => ({
|
||||
label: group,
|
||||
options: items.map(([label, value]) => ({ label, value })),
|
||||
}) as GroupSelectOption<SelectOption<string>>);
|
||||
});
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createResourceTemplatesInjectable;
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
const extensionMatchers = [
|
||||
/\.yaml$/,
|
||||
/\.yml$/,
|
||||
/\.json$/,
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a fileName matches a yaml or json file name structure
|
||||
* @param fileName The fileName to check
|
||||
*/
|
||||
export function hasCorrectExtension(fileName: string): boolean {
|
||||
return extensionMatchers.some(matcher => matcher.test(fileName));
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import path from "path";
|
||||
import { hasCorrectExtension } from "./has-correct-extension";
|
||||
import "../../../../common/vars";
|
||||
import readFileInjectable from "../../../../common/fs/read-file.injectable";
|
||||
import readDirInjectable from "../../../../common/fs/read-dir.injectable";
|
||||
import type { RawTemplates } from "./create-resource-templates.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
readDir: (dirPath: string) => Promise<string[]>;
|
||||
readFile: (filePath: string, encoding: "utf-8") => Promise<string>;
|
||||
}
|
||||
|
||||
async function getTemplates({ readDir, readFile }: Dependencies) {
|
||||
const templatesFolder = path.resolve(__static, "../templates/create-resource");
|
||||
|
||||
/**
|
||||
* Mapping between file names and their contents
|
||||
*/
|
||||
const templates: [file: string, contents: string][] = [];
|
||||
|
||||
for (const dirEntry of await readDir(templatesFolder)) {
|
||||
if (hasCorrectExtension(dirEntry)) {
|
||||
templates.push([path.parse(dirEntry).name, await readFile(path.join(templatesFolder, dirEntry), "utf-8")]);
|
||||
}
|
||||
}
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
const lensCreateResourceTemplatesInjectable = getInjectable({
|
||||
instantiate: async (di): Promise<RawTemplates> => {
|
||||
const templates = await getTemplates({
|
||||
readFile: di.inject(readFileInjectable),
|
||||
readDir: di.inject(readDirInjectable),
|
||||
});
|
||||
|
||||
return ["lens", templates];
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default lensCreateResourceTemplatesInjectable;
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { CreateResourceTabStore } from "./store";
|
||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||
|
||||
const createResourceTabStoreInjectable = getInjectable({
|
||||
instantiate: (di) => new CreateResourceTabStore({
|
||||
createStorage: di.inject(createStorageInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createResourceTabStoreInjectable;
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { StorageHelper } from "../../../utils";
|
||||
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
|
||||
|
||||
interface Dependencies {
|
||||
createStorage:<T> (storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>;
|
||||
}
|
||||
|
||||
export class CreateResourceTabStore extends DockTabStore<string> {
|
||||
constructor(protected dependencies: Dependencies) {
|
||||
super(dependencies, {
|
||||
storageKey: "create_resource",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { computed, IComputedValue, observable } from "mobx";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { delay, getOrInsert, waitForPath } from "../../../utils";
|
||||
import { watch } from "chokidar";
|
||||
import { readFile } from "fs/promises";
|
||||
import logger from "../../../../common/logger";
|
||||
import { hasCorrectExtension } from "./has-correct-extension";
|
||||
import type { RawTemplates } from "./create-resource-templates.injectable";
|
||||
|
||||
const userTemplatesFolder = path.join(os.homedir(), ".k8slens", "templates");
|
||||
|
||||
function groupTemplates(templates: Map<string, string>): RawTemplates[] {
|
||||
const res = new Map<string, [string, string][]>();
|
||||
|
||||
for (const [filePath, contents] of templates) {
|
||||
const rawRelative = path.dirname(path.relative(userTemplatesFolder, filePath));
|
||||
const title = rawRelative === "."
|
||||
? "ungrouped"
|
||||
: rawRelative;
|
||||
|
||||
getOrInsert(res, title, []).push([path.parse(filePath).name, contents]);
|
||||
}
|
||||
|
||||
return [...res.entries()];
|
||||
}
|
||||
|
||||
function watchUserCreateResourceTemplates(): IComputedValue<RawTemplates[]> {
|
||||
/**
|
||||
* Map between filePaths and template contents
|
||||
*/
|
||||
const templates = observable.map<string, string>();
|
||||
|
||||
const onAddOrChange = async (filePath: string) => {
|
||||
if (!hasCorrectExtension(filePath)) {
|
||||
// ignore non yaml or json files
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const contents = await readFile(filePath, "utf-8");
|
||||
|
||||
templates.set(filePath, contents);
|
||||
} catch (error) {
|
||||
if (error?.code === "ENOENT") {
|
||||
// ignore, file disappeared
|
||||
} else {
|
||||
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while reading ${filePath}`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
const onUnlink = (filePath: string) => {
|
||||
templates.delete(filePath);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
for (let i = 1;; i *= 2) {
|
||||
try {
|
||||
await waitForPath(userTemplatesFolder);
|
||||
break;
|
||||
} catch (error) {
|
||||
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while waiting for ${userTemplatesFolder} to exist, waiting and trying again`, error);
|
||||
await delay(i * 1000); // exponential backoff in seconds
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: There is technically a race condition here of the form "time-of-check to time-of-use"
|
||||
*/
|
||||
watch(userTemplatesFolder, {
|
||||
disableGlobbing: true,
|
||||
ignorePermissionErrors: true,
|
||||
usePolling: false,
|
||||
awaitWriteFinish: {
|
||||
pollInterval: 100,
|
||||
stabilityThreshold: 1000,
|
||||
},
|
||||
ignoreInitial: false,
|
||||
atomic: 150, // for "atomic writes"
|
||||
})
|
||||
.on("add", onAddOrChange)
|
||||
.on("change", onAddOrChange)
|
||||
.on("unlink", onUnlink)
|
||||
.on("error", error => {
|
||||
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while watching files under ${userTemplatesFolder}`, error);
|
||||
});
|
||||
})();
|
||||
|
||||
return computed(() => groupTemplates(templates));
|
||||
}
|
||||
|
||||
const userCreateResourceTemplatesInjectable = getInjectable({
|
||||
instantiate: () => watchUserCreateResourceTemplates(),
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default userCreateResourceTemplatesInjectable;
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { GroupSelectOption, Select, SelectOption } from "../../select";
|
||||
import yaml from "js-yaml";
|
||||
import { IComputedValue, makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import type { CreateResourceTabStore } from "./store";
|
||||
import type { DockTab } from "../dock/store";
|
||||
import { EditorPanel } from "../editor-panel";
|
||||
import { InfoPanel } from "../info-panel";
|
||||
import * as resourceApplierApi from "../../../../common/k8s-api/endpoints/resource-applier.api";
|
||||
import { Notifications } from "../../notifications";
|
||||
import logger from "../../../../common/logger";
|
||||
import type { KubeJsonApiData } from "../../../../common/k8s-api/kube-json-api";
|
||||
import { getDetailsUrl } from "../../kube-detail-params";
|
||||
import { apiManager } from "../../../../common/k8s-api/api-manager";
|
||||
import { prevDefault } from "../../../utils";
|
||||
import { navigate } from "../../../navigation";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import createResourceTabStoreInjectable from "./store.injectable";
|
||||
import createResourceTemplatesInjectable from "./create-resource-templates.injectable";
|
||||
import { Spinner } from "../../spinner";
|
||||
|
||||
interface Props {
|
||||
tab: DockTab;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
createResourceTemplates: IComputedValue<GroupSelectOption<SelectOption>[]>;
|
||||
createResourceTabStore: CreateResourceTabStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
|
||||
@observable error = "";
|
||||
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this.props.createResourceTabStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
onChange = (value: string) => {
|
||||
this.error = ""; // reset first, validation goes later
|
||||
this.props.createResourceTabStore.setData(this.tabId, value);
|
||||
};
|
||||
|
||||
onError = (error: Error | string) => {
|
||||
this.error = error.toString();
|
||||
};
|
||||
|
||||
onSelectTemplate = (item: SelectOption<string>) => {
|
||||
this.props.createResourceTabStore.setData(this.tabId, item.value);
|
||||
};
|
||||
|
||||
create = async (): Promise<void> => {
|
||||
if (this.error || !this.data.trim()) {
|
||||
// do not save when field is empty or there is an error
|
||||
return;
|
||||
}
|
||||
|
||||
// skip empty documents
|
||||
const resources = yaml.loadAll(this.data).filter(Boolean);
|
||||
|
||||
if (resources.length === 0) {
|
||||
return void logger.info("Nothing to create");
|
||||
}
|
||||
|
||||
const creatingResources = resources.map(async (resource: string) => {
|
||||
try {
|
||||
const data = await resourceApplierApi.update(resource) as KubeJsonApiData;
|
||||
const { kind, apiVersion, metadata: { name, namespace }} = data;
|
||||
|
||||
const showDetails = () => {
|
||||
const resourceLink = apiManager.lookupApiLink({ kind, apiVersion, name, namespace });
|
||||
|
||||
navigate(getDetailsUrl(resourceLink));
|
||||
close();
|
||||
};
|
||||
|
||||
const close = Notifications.ok(
|
||||
<p>
|
||||
{kind} <a onClick={prevDefault(showDetails)}>{name}</a> successfully created.
|
||||
</p>,
|
||||
);
|
||||
} catch (error) {
|
||||
Notifications.error(error?.toString() ?? "Unknown error occured");
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(creatingResources);
|
||||
};
|
||||
|
||||
renderControls() {
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<Select
|
||||
autoConvertOptions={false}
|
||||
controlShouldRenderValue={false} // always keep initial placeholder
|
||||
className="TemplateSelect"
|
||||
placeholder="Select Template ..."
|
||||
options={this.props.createResourceTemplates.get()}
|
||||
menuPlacement="top"
|
||||
themeName="outlined"
|
||||
onChange={ this.onSelectTemplate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tabId, data, error } = this;
|
||||
|
||||
return (
|
||||
<div className="CreateResource flex column">
|
||||
<InfoPanel
|
||||
tabId={tabId}
|
||||
error={error}
|
||||
controls={this.renderControls()}
|
||||
submit={this.create}
|
||||
submitLabel="Create"
|
||||
showNotifications={false}
|
||||
/>
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={data}
|
||||
onChange={this.onChange}
|
||||
onError={this.onError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const CreateResource = withInjectables<Dependencies, Props>(NonInjectedCreateResource, {
|
||||
getPlaceholder: () => <Spinner center />,
|
||||
|
||||
getProps: async (di, props) => ({
|
||||
createResourceTabStore: di.inject(createResourceTabStoreInjectable),
|
||||
createResourceTemplates: await di.inject(createResourceTemplatesInjectable),
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
Reference in new issue
Block a user