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

Merge branch 'master' into improve-dock-tabs-ux

# Conflicts:
#	src/renderer/components/dock/dock-tabs.tsx
This commit is contained in:
DmitriyNoa 2022-01-31 16:24:10 +01:00
commit 6746ae489f
177 changed files with 3748 additions and 2903 deletions

View File

@ -2,6 +2,4 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
export default {};
.EditResource {
}

View File

@ -354,8 +354,7 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
} }
}, 10*60*1000); }, 10*60*1000);
// TODO: Make re-rendering of KubeObjectListLayout not cause namespaceSelector to be closed it("show logs and highlight the log search entries", async () => {
xit("show logs and highlight the log search entries", async () => {
await frame.click(`a[href="/workloads"]`); await frame.click(`a[href="/workloads"]`);
await frame.click(`a[href="/pods"]`); await frame.click(`a[href="/pods"]`);
@ -400,8 +399,7 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
await frame.waitForSelector("div.TableCell >> text='kube-system'"); await frame.waitForSelector("div.TableCell >> text='kube-system'");
}, 10*60*1000); }, 10*60*1000);
// TODO: Make re-rendering of KubeObjectListLayout not cause namespaceSelector to be closed it(`should create the ${TEST_NAMESPACE} and a pod in the namespace`, async () => {
xit(`should create the ${TEST_NAMESPACE} and a pod in the namespace`, async () => {
await frame.click('a[href="/namespaces"]'); await frame.click('a[href="/namespaces"]');
await frame.click("button.add-button"); await frame.click("button.add-button");
await frame.waitForSelector("div.AddNamespaceDialog >> text='Create Namespace'"); await frame.waitForSelector("div.AddNamespaceDialog >> text='Create Namespace'");

View File

@ -349,6 +349,7 @@
"fork-ts-checker-webpack-plugin": "^5.2.1", "fork-ts-checker-webpack-plugin": "^5.2.1",
"hoist-non-react-statics": "^3.3.2", "hoist-non-react-statics": "^3.3.2",
"html-webpack-plugin": "^4.5.2", "html-webpack-plugin": "^4.5.2",
"ignore-loader": "^0.1.2",
"include-media": "^1.4.9", "include-media": "^1.4.9",
"jest": "26.6.3", "jest": "26.6.3",
"jest-canvas-mock": "^2.3.1", "jest-canvas-mock": "^2.3.1",

View File

@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import logTabStoreInjectable from "./tab-store.injectable"; import fsInjectable from "./fs.injectable";
const updateTabNameInjectable = getInjectable({ const readDirInjectable = getInjectable({
instantiate: (di) => di.inject(logTabStoreInjectable).updateTabName, instantiate: (di) => di.inject(fsInjectable).readdir,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });
export default updateTabNameInjectable; export default readDirInjectable;

View File

@ -2,12 +2,12 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { KubeObjectMenuRegistry } from "../../../../../extensions/registries";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable";
const kubeObjectMenuRegistryInjectable = getInjectable({ const readFileInjectable = getInjectable({
instantiate: () => KubeObjectMenuRegistry.getInstance(), instantiate: (di) => di.inject(fsInjectable).readFile,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });
export default kubeObjectMenuRegistryInjectable; export default readFileInjectable;

View File

@ -2,15 +2,11 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { readJsonFile } from "./read-json-file";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import fsInjectable from "../fs.injectable"; import fsInjectable from "./fs.injectable";
const readJsonFileInjectable = getInjectable({ const readJsonFileInjectable = getInjectable({
instantiate: (di) => readJsonFile({ instantiate: (di) => di.inject(fsInjectable).readJson,
fs: di.inject(fsInjectable),
}),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -1,16 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { JsonObject } from "type-fest";
interface Dependencies {
fs: {
readJson: (filePath: string) => Promise<JsonObject>;
};
}
export const readJsonFile =
({ fs }: Dependencies) =>
(filePath: string) =>
fs.readJson(filePath);

View File

@ -0,0 +1,38 @@
/**
* 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 { EnsureOptions, WriteOptions } from "fs-extra";
import path from "path";
import type { JsonValue } from "type-fest";
import fsInjectable from "./fs.injectable";
interface Dependencies {
writeJson: (file: string, object: any, options?: WriteOptions | BufferEncoding | string) => Promise<void>;
ensureDir: (dir: string, options?: EnsureOptions | number) => Promise<void>;
}
const writeJsonFile = ({ writeJson, ensureDir }: Dependencies) => async (filePath: string, content: JsonValue) => {
await ensureDir(path.dirname(filePath), { mode: 0o755 });
await writeJson(filePath, content, {
encoding: "utf-8",
spaces: 2,
});
};
const writeJsonFileInjectable = getInjectable({
instantiate: (di) => {
const { writeJson, ensureDir } = di.inject(fsInjectable);
return writeJsonFile({
writeJson,
ensureDir,
});
},
lifecycle: lifecycleEnum.singleton,
});
export default writeJsonFileInjectable;

View File

@ -1,31 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
import type { JsonObject } from "type-fest";
interface Dependencies {
fs: {
ensureDir: (
directoryName: string,
options: { mode: number }
) => Promise<void>;
writeJson: (
filePath: string,
contentObject: JsonObject,
options: { spaces: number }
) => Promise<void>;
};
}
export const writeJsonFile =
({ fs }: Dependencies) =>
async (filePath: string, contentObject: JsonObject) => {
const directoryName = path.dirname(filePath);
await fs.ensureDir(directoryName, { mode: 0o755 });
await fs.writeJson(filePath, contentObject, { spaces: 2 });
};

View File

@ -1,19 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { bind } from "../index";
describe("bind", () => {
it("should work correctly", () => {
function foobar(bound: number, nonBound: number): number {
expect(typeof bound).toBe("number");
expect(typeof nonBound).toBe("number");
return bound + nonBound;
}
const foobarBound = bind(foobar, null, 5);
expect(foobarBound(10)).toBe(15);
});
});

View File

@ -10,13 +10,6 @@ export function noop<T extends any[]>(...args: T): void {
return void args; return void args;
} }
/**
* A typecorrect version of <function>.bind()
*/
export function bind<BoundArgs extends any[], NonBoundArgs extends any[], ReturnType>(fn: (...args: [...BoundArgs, ...NonBoundArgs]) => ReturnType, thisArg: any, ...boundArgs: BoundArgs): (...args: NonBoundArgs) => ReturnType {
return fn.bind(thisArg, ...boundArgs);
}
export * from "./app-version"; export * from "./app-version";
export * from "./autobind"; export * from "./autobind";
export * from "./camelCase"; export * from "./camelCase";
@ -49,6 +42,7 @@ export * from "./toggle-set";
export * from "./toJS"; export * from "./toJS";
export * from "./type-narrowing"; export * from "./type-narrowing";
export * from "./types"; export * from "./types";
export * from "./wait-for-path";
import * as iter from "./iter"; import * as iter from "./iter";
import * as array from "./array"; import * as array from "./array";

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { FSWatcher } from "chokidar";
import path from "path";
/**
* Wait for `filePath` and all parent directories to exist.
* @param pathname The file path to wait until it exists
*
* NOTE: There is technically a race condition in this function of the form
* "time-of-check to time-of-use" because we have to wait for each parent
* directory to exist first.
*/
export async function waitForPath(pathname: string): Promise<void> {
const dirOfPath = path.dirname(pathname);
if (dirOfPath === pathname) {
// The root of this filesystem, assume it exists
return;
} else {
await waitForPath(dirOfPath);
}
return new Promise((resolve, reject) => {
const watcher = new FSWatcher({
depth: 0,
disableGlobbing: true,
});
const onAddOrAddDir = (filePath: string) => {
if (filePath === pathname) {
watcher.unwatch(dirOfPath);
watcher
.close()
.then(() => resolve())
.catch(reject);
}
};
const onError = (error: any) => {
watcher.unwatch(dirOfPath);
watcher
.close()
.then(() => reject(error))
.catch(() => reject(error));
};
watcher
.on("add", onAddOrAddDir)
.on("addDir", onAddOrAddDir)
.on("error", onError)
.add(dirOfPath);
});
}

View File

@ -3,12 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { writeJsonFile } from "./write-json-file"; import { isLinux } from "../vars";
import fsInjectable from "../fs.injectable";
const writeJsonFileInjectable = getInjectable({ const isLinuxInjectable = getInjectable({
instantiate: (di) => writeJsonFile({ fs: di.inject(fsInjectable) }), instantiate: () => isLinux,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });
export default writeJsonFileInjectable; export default isLinuxInjectable;

View File

@ -0,0 +1,13 @@
/**
* 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 { isWindows } from "../vars";
const isWindowsInjectable = getInjectable({
instantiate: () => isWindows,
lifecycle: lifecycleEnum.singleton,
});
export default isWindowsInjectable;

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
type TentativeTuple<T> = T extends object ? [T] : [undefined?];
type MapInjectables<T> = {
[Key in keyof T]: T[Key] extends () => infer Res ? Res : never;
};
export const asLegacyGlobalObjectForExtensionApiWithModifications = <
TInjectable extends Injectable<unknown, unknown, TInstantiationParameter>,
TInstantiationParameter,
OtherFields extends Record<string, () => any>,
>(
injectableKey: TInjectable,
otherFields: OtherFields,
...instantiationParameter: TentativeTuple<TInstantiationParameter>
) =>
new Proxy(
{},
{
get(target, propertyName) {
if (propertyName === "$$typeof") {
return undefined;
}
const instance: any = getLegacyGlobalDiForExtensionApi().inject(
injectableKey,
...instantiationParameter,
);
const propertyValue = instance[propertyName] ?? otherFields[propertyName as any]();
if (typeof propertyValue === "function") {
return function (...args: any[]) {
return propertyValue.apply(instance, args);
};
}
return propertyValue;
},
},
) as ReturnType<TInjectable["instantiate"]> & MapInjectables<OtherFields>;

View File

@ -1,43 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
type TentativeTuple<T> = T extends object ? [T] : [undefined?];
export const asLegacyGlobalSingletonForExtensionApi = <
TClass extends abstract new (...args: any[]) => any,
TInjectable extends Injectable<unknown, unknown, TInstantiationParameter>,
TInstantiationParameter,
>(
Class: TClass,
injectableKey: TInjectable,
...instantiationParameter: TentativeTuple<TInstantiationParameter>
) =>
new Proxy(Class, {
construct: () => {
throw new Error("A legacy singleton class must be created by createInstance()");
},
get: (target: any, propertyName) => {
if (propertyName === "getInstance" || propertyName === "createInstance") {
return () =>
getLegacyGlobalDiForExtensionApi().inject(
injectableKey,
...instantiationParameter,
);
}
if (propertyName === "resetInstance") {
return () => getLegacyGlobalDiForExtensionApi().purge(injectableKey);
}
return target[propertyName];
},
}) as InstanceType<TClass> & {
getInstance: () => InstanceType<TClass>;
createInstance: () => InstanceType<TClass>;
resetInstance: () => void;
};

View File

@ -3,9 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
export type { StatusBarRegistration } from "../../renderer/components/cluster-manager/status-bar-registration"; export type { StatusBarRegistration } from "../../renderer/components/cluster-manager/status-bar-registration";
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../../renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration";
export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../renderer/components/+preferences/app-preferences/app-preference-registration"; export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../renderer/components/+preferences/app-preferences/app-preference-registration";
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry"; export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry";
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry"; export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry"; export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry";
export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry"; export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry";

View File

@ -276,7 +276,6 @@ export class ExtensionLoader {
const removeItems = [ const removeItems = [
registries.ClusterPageRegistry.getInstance().add(extension.clusterPages, extension), registries.ClusterPageRegistry.getInstance().add(extension.clusterPages, extension),
registries.ClusterPageMenuRegistry.getInstance().add(extension.clusterPageMenus, extension), registries.ClusterPageMenuRegistry.getInstance().add(extension.clusterPageMenus, extension),
registries.KubeObjectMenuRegistry.getInstance().add(extension.kubeObjectMenuItems),
registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems), registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems),
registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts), registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts),
registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems), registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems),

View File

@ -19,6 +19,7 @@ import type { AppPreferenceRegistration } from "../renderer/components/+preferen
import type { AdditionalCategoryColumnRegistration } from "../renderer/components/+catalog/custom-category-columns"; import type { AdditionalCategoryColumnRegistration } from "../renderer/components/+catalog/custom-category-columns";
import type { CustomCategoryViewRegistration } from "../renderer/components/+catalog/custom-views"; import type { CustomCategoryViewRegistration } from "../renderer/components/+catalog/custom-views";
import type { StatusBarRegistration } from "../renderer/components/cluster-manager/status-bar-registration"; import type { StatusBarRegistration } from "../renderer/components/cluster-manager/status-bar-registration";
import type { KubeObjectMenuRegistration } from "../renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration";
export class LensRendererExtension extends LensExtension { export class LensRendererExtension extends LensExtension {
globalPages: registries.PageRegistration[] = []; globalPages: registries.PageRegistration[] = [];
@ -29,7 +30,7 @@ export class LensRendererExtension extends LensExtension {
entitySettings: registries.EntitySettingRegistration[] = []; entitySettings: registries.EntitySettingRegistration[] = [];
statusBarItems: StatusBarRegistration[] = []; statusBarItems: StatusBarRegistration[] = [];
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = []; kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = []; kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = []; kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
commands: CommandRegistration[] = []; commands: CommandRegistration[] = [];
welcomeMenus: WelcomeMenuRegistration[] = []; welcomeMenus: WelcomeMenuRegistration[] = [];

View File

@ -8,7 +8,6 @@
export * from "./page-registry"; export * from "./page-registry";
export * from "./page-menu-registry"; export * from "./page-menu-registry";
export * from "./kube-object-detail-registry"; export * from "./kube-object-detail-registry";
export * from "./kube-object-menu-registry";
export * from "./kube-object-status-registry"; export * from "./kube-object-status-registry";
export * from "./entity-setting-registry"; export * from "./entity-setting-registry";
export * from "./catalog-entity-detail-registry"; export * from "./catalog-entity-detail-registry";

View File

@ -3,14 +3,18 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api"; import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import createTerminalTabInjectable from "../../renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable"; import createTerminalTabInjectable from "../../renderer/components/dock/terminal/create-terminal-tab.injectable";
import terminalStoreInjectable from "../../renderer/components/dock/terminal-store/terminal-store.injectable"; import terminalStoreInjectable from "../../renderer/components/dock/terminal/store.injectable";
import { asLegacyGlobalObjectForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; import { asLegacyGlobalObjectForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import logTabStoreInjectable from "../../renderer/components/dock/logs/tab-store.injectable"; import logTabStoreInjectable from "../../renderer/components/dock/logs/tab-store.injectable";
import { asLegacyGlobalSingletonForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-singleton-for-extension-api";
import { TerminalStore as TerminalStoreClass } from "../../renderer/components/dock/terminal-store/terminal.store";
import commandOverlayInjectable from "../../renderer/components/command-palette/command-overlay.injectable"; import commandOverlayInjectable from "../../renderer/components/command-palette/command-overlay.injectable";
import { asLegacyGlobalObjectForExtensionApiWithModifications } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications";
import createPodLogsTabInjectable from "../../renderer/components/dock/logs/create-pod-logs-tab.injectable";
import createWorkloadLogsTabInjectable from "../../renderer/components/dock/logs/create-workload-logs-tab.injectable";
import sendCommandInjectable from "../../renderer/components/dock/terminal/send-command.injectable";
import { podsStore } from "../../renderer/components/+workloads-pods/pods.store";
import renameTabInjectable from "../../renderer/components/dock/dock/rename-tab.injectable";
// layouts // layouts
export * from "../../renderer/components/layout/main-layout"; export * from "../../renderer/components/layout/main-layout";
@ -71,7 +75,30 @@ export * from "../../renderer/components/+events/kube-event-details";
export * from "../../renderer/components/status-brick"; export * from "../../renderer/components/status-brick";
export const createTerminalTab = asLegacyGlobalFunctionForExtensionApi(createTerminalTabInjectable); export const createTerminalTab = asLegacyGlobalFunctionForExtensionApi(createTerminalTabInjectable);
export const TerminalStore = asLegacyGlobalSingletonForExtensionApi(TerminalStoreClass, terminalStoreInjectable); export const terminalStore = asLegacyGlobalObjectForExtensionApiWithModifications(terminalStoreInjectable, {
export const terminalStore = asLegacyGlobalObjectForExtensionApi(terminalStoreInjectable); sendCommand: () => asLegacyGlobalFunctionForExtensionApi(sendCommandInjectable),
export const logTabStore = asLegacyGlobalObjectForExtensionApi(logTabStoreInjectable); });
export const logTabStore = asLegacyGlobalObjectForExtensionApiWithModifications(logTabStoreInjectable, {
createPodTab: () => asLegacyGlobalFunctionForExtensionApi(createPodLogsTabInjectable),
createWorkloadTab: () => asLegacyGlobalFunctionForExtensionApi(createWorkloadLogsTabInjectable),
renameTab: () => (tabId: string): void => {
const renameTab = asLegacyGlobalFunctionForExtensionApi(renameTabInjectable);
const tabData = logTabStore.getData(tabId);
const pod = podsStore.getById(tabData.selectedPodId);
renameTab(tabId, `Pod ${pod.getName()}`);
},
tabs: () => undefined,
});
export class TerminalStore {
static getInstance() {
return terminalStore;
}
static createInstance() {
return terminalStore;
}
static resetInstance() {
console.warn("TerminalStore.resetInstance() does nothing");
}
}

View File

@ -12,8 +12,8 @@ import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-
import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable"; import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable";
import appNameInjectable from "./app-paths/app-name/app-name.injectable"; import appNameInjectable from "./app-paths/app-name/app-name.injectable";
import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable"; import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable";
import writeJsonFileInjectable from "../common/fs/write-json-file/write-json-file.injectable"; import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
import readJsonFileInjectable from "../common/fs/read-json-file/read-json-file.injectable"; import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
export const getDiForUnitTesting = ( export const getDiForUnitTesting = (
{ doGeneralOverrides } = { doGeneralOverrides: false }, { doGeneralOverrides } = { doGeneralOverrides: false },

View File

@ -73,9 +73,6 @@ export async function bootstrap(di: DependencyInjectionContainer) {
logger.info(`${logPrefix} initializing EntitySettingsRegistry`); logger.info(`${logPrefix} initializing EntitySettingsRegistry`);
initializers.initEntitySettingsRegistry(); initializers.initEntitySettingsRegistry();
logger.info(`${logPrefix} initializing KubeObjectMenuRegistry`);
initializers.initKubeObjectMenuRegistry();
logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`); logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`);
initializers.initKubeObjectDetailRegistry(); initializers.initKubeObjectDetailRegistry();

View File

@ -18,7 +18,7 @@ import { Select, SelectOption } from "../select";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { Tooltip, withStyles } from "@material-ui/core"; import { Tooltip, withStyles } from "@material-ui/core";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import createInstallChartTabInjectable from "../dock/create-install-chart-tab/create-install-chart-tab.injectable"; import createInstallChartTabInjectable from "../dock/install-chart/create-install-chart-tab.injectable";
interface Props { interface Props {
chart: HelmChart; chart: HelmChart;

View File

@ -12,7 +12,7 @@ import { helmChartStore } from "./helm-chart.store";
import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api"; import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
import { HelmChartDetails } from "./helm-chart-details"; import { HelmChartDetails } from "./helm-chart-details";
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { ItemListLayout } from "../item-object-list/item-list-layout"; import { ItemListLayout } from "../item-object-list/list-layout";
import { helmChartsURL } from "../../../common/routes"; import { helmChartsURL } from "../../../common/routes";
import type { HelmChartsRouteParams } from "../../../common/routes"; import type { HelmChartsRouteParams } from "../../../common/routes";

View File

@ -27,7 +27,7 @@ import { getDetailsUrl } from "../../kube-detail-params";
import { Checkbox } from "../../checkbox"; import { Checkbox } from "../../checkbox";
import { MonacoEditor } from "../../monaco-editor"; import { MonacoEditor } from "../../monaco-editor";
import { IAsyncComputed, withInjectables } from "@ogre-tools/injectable-react"; import { IAsyncComputed, withInjectables } from "@ogre-tools/injectable-react";
import createUpgradeChartTabInjectable from "../../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable"; import createUpgradeChartTabInjectable from "../../dock/upgrade-chart/create-upgrade-chart-tab.injectable";
import updateReleaseInjectable from "../update-release/update-release.injectable"; import updateReleaseInjectable from "../update-release/update-release.injectable";
import releaseInjectable from "./release.injectable"; import releaseInjectable from "./release.injectable";
import releaseDetailsInjectable from "./release-details.injectable"; import releaseDetailsInjectable from "./release-details.injectable";

View File

@ -10,7 +10,7 @@ import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import createUpgradeChartTabInjectable from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable"; import createUpgradeChartTabInjectable from "../dock/upgrade-chart/create-upgrade-chart-tab.injectable";
import releaseRollbackDialogModelInjectable from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable"; import releaseRollbackDialogModelInjectable from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable";
import deleteReleaseInjectable from "./delete-release/delete-release.injectable"; import deleteReleaseInjectable from "./delete-release/delete-release.injectable";

View File

@ -23,10 +23,8 @@ import { ReleaseRollbackDialog } from "./release-rollback-dialog";
import { ReleaseDetails } from "./release-details/release-details"; import { ReleaseDetails } from "./release-details/release-details";
import removableReleasesInjectable from "./removable-releases.injectable"; import removableReleasesInjectable from "./removable-releases.injectable";
import type { RemovableHelmRelease } from "./removable-releases"; import type { RemovableHelmRelease } from "./removable-releases";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import releasesInjectable from "./releases.injectable"; import releasesInjectable from "./releases.injectable";
import { Spinner } from "../spinner";
enum columnId { enum columnId {
name = "name", name = "name",
@ -48,7 +46,6 @@ interface Dependencies {
selectNamespace: (namespace: string) => void selectNamespace: (namespace: string) => void
} }
@observer
class NonInjectedHelmReleases extends Component<Dependencies & Props> { class NonInjectedHelmReleases extends Component<Dependencies & Props> {
componentDidMount() { componentDidMount() {
const { match: { params: { namespace }}} = this.props; const { match: { params: { namespace }}} = this.props;
@ -89,12 +86,8 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
} }
render() { render() {
if (this.props.releasesArePending.get()) {
// TODO: Make Spinner "center" work properly
return <div className="flex center" style={{ height: "100%" }}><Spinner /></div>;
}
const releases = this.props.releases; const releases = this.props.releases;
const releasesArePending = this.props.releasesArePending;
// TODO: Implement ItemListLayout without stateful stores // TODO: Implement ItemListLayout without stateful stores
const legacyReleaseStore = { const legacyReleaseStore = {
@ -103,7 +96,11 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
}, },
loadAll: () => Promise.resolve(), loadAll: () => Promise.resolve(),
isLoaded: true,
get isLoaded() {
return !releasesArePending.get();
},
failedLoading: false, failedLoading: false,
getTotalCount: () => releases.get().length, getTotalCount: () => releases.get().length,

View File

@ -22,6 +22,8 @@
.content { .content {
min-height: 26px; min-height: 26px;
line-height: 1.3;
padding: 2px var(--padding) 2px 0;
&:hover { &:hover {
background-color: var(--sidebarItemHoverBackground); background-color: var(--sidebarItemHoverBackground);
@ -39,6 +41,8 @@
.iconContainer { .iconContainer {
margin-left: 28px; margin-left: 28px;
margin-top: 2px;
align-self: flex-start;
} }
} }

View File

@ -20,6 +20,7 @@
padding: 0 var(--padding); padding: 0 var(--padding);
padding-bottom: 0; padding-bottom: 0;
padding-right: 24px; // + reserved space for .pinIcon padding-right: 24px; // + reserved space for .pinIcon
flex-grow: 2.5!important;
> span { > span {
overflow: hidden; overflow: hidden;

View File

@ -6,7 +6,6 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { orderBy } from "lodash"; import { orderBy } from "lodash";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import type { CatalogCategory, CatalogEntity } from "../../../common/catalog"; import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
import { bind } from "../../utils";
import type { ItemListLayoutProps } from "../item-object-list"; import type { ItemListLayoutProps } from "../item-object-list";
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns"; import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
import categoryColumnsInjectable from "./custom-category-columns.injectable"; import categoryColumnsInjectable from "./custom-category-columns.injectable";
@ -50,7 +49,7 @@ function getBrowseAllColumns(): RegisteredAdditionalCategoryColumn[] {
]; ];
} }
function getCategoryColumns({ extensionColumns }: Dependencies, { activeCategory }: GetCategoryColumnsParams): CategoryColumns { const getCategoryColumns = ({ extensionColumns }: Dependencies) => ({ activeCategory }: GetCategoryColumnsParams): CategoryColumns => {
const allRegistrations = orderBy( const allRegistrations = orderBy(
activeCategory activeCategory
? getSpecificCategoryColumns(activeCategory, extensionColumns) ? getSpecificCategoryColumns(activeCategory, extensionColumns)
@ -83,12 +82,13 @@ function getCategoryColumns({ extensionColumns }: Dependencies, { activeCategory
renderTableContents: entity => tableRowRenderers.map(fn => fn(entity)), renderTableContents: entity => tableRowRenderers.map(fn => fn(entity)),
searchFilters, searchFilters,
}; };
} };
const getCategoryColumnsInjectable = getInjectable({ const getCategoryColumnsInjectable = getInjectable({
instantiate: (di) => bind(getCategoryColumns, null, { instantiate: (di) => getCategoryColumns({
extensionColumns: di.inject(categoryColumnsInjectable), extensionColumns: di.inject(categoryColumnsInjectable),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -7,14 +7,13 @@ import { requestOpenFilePickingDialog } from "../../ipc";
import { supportedExtensionFormats } from "./supported-extension-formats"; import { supportedExtensionFormats } from "./supported-extension-formats";
import attemptInstallsInjectable from "./attempt-installs/attempt-installs.injectable"; import attemptInstallsInjectable from "./attempt-installs/attempt-installs.injectable";
import directoryForDownloadsInjectable from "../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable"; import directoryForDownloadsInjectable from "../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable";
import { bind } from "../../utils";
interface Dependencies { interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void> attemptInstalls: (filePaths: string[]) => Promise<void>
directoryForDownloads: string directoryForDownloads: string
} }
async function installFromSelectFileDialog({ attemptInstalls, directoryForDownloads }: Dependencies) { const installFromSelectFileDialog = ({ attemptInstalls, directoryForDownloads }: Dependencies) => async () => {
const { canceled, filePaths } = await requestOpenFilePickingDialog({ const { canceled, filePaths } = await requestOpenFilePickingDialog({
defaultPath: directoryForDownloads, defaultPath: directoryForDownloads,
properties: ["openFile", "multiSelections"], properties: ["openFile", "multiSelections"],
@ -26,13 +25,14 @@ async function installFromSelectFileDialog({ attemptInstalls, directoryForDownlo
if (!canceled) { if (!canceled) {
await attemptInstalls(filePaths); await attemptInstalls(filePaths);
} }
} };
const installFromSelectFileDialogInjectable = getInjectable({ const installFromSelectFileDialogInjectable = getInjectable({
instantiate: (di) => bind(installFromSelectFileDialog, null, { instantiate: (di) => installFromSelectFileDialog({
attemptInstalls: di.inject(attemptInstallsInjectable), attemptInstalls: di.inject(attemptInstallsInjectable),
directoryForDownloads: di.inject(directoryForDownloadsInjectable), directoryForDownloads: di.inject(directoryForDownloadsInjectable),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -8,7 +8,7 @@ import "./port-forwards.scss";
import React from "react"; import React from "react";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import type { RouteComponentProps } from "react-router-dom"; import type { RouteComponentProps } from "react-router-dom";
import { ItemListLayout } from "../item-object-list/item-list-layout"; import { ItemListLayout } from "../item-object-list/list-layout";
import type { PortForwardItem, PortForwardStore } from "../../port-forward"; import type { PortForwardItem, PortForwardStore } from "../../port-forward";
import { PortForwardMenu } from "./port-forward-menu"; import { PortForwardMenu } from "./port-forward-menu";
import { PortForwardsRouteParams, portForwardsURL } from "../../../common/routes"; import { PortForwardsRouteParams, portForwardsURL } from "../../../common/routes";

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { KubeObjectMenuProps } from "../../kube-object-menu";
import type { ServiceAccount } from "../../../../common/k8s-api/endpoints";
import { MenuItem } from "../../menu";
import { openServiceAccountKubeConfig } from "../../kubeconfig-dialog";
import { Icon } from "../../icon";
export function ServiceAccountMenu(props: KubeObjectMenuProps<ServiceAccount>) {
const { object, toolbar } = props;
return (
<MenuItem onClick={() => openServiceAccountKubeConfig(object)}>
<Icon material="insert_drive_file" tooltip="Kubeconfig File" interactive={toolbar} />
<span className="title">Kubeconfig</span>
</MenuItem>
);
}

View File

@ -8,13 +8,8 @@ import "./view.scss";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React from "react"; import React from "react";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import type { ServiceAccount } from "../../../../common/k8s-api/endpoints/service-accounts.api";
import { Icon } from "../../icon";
import { KubeObjectListLayout } from "../../kube-object-list-layout"; import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import type { KubeObjectMenuProps } from "../../kube-object-menu";
import { openServiceAccountKubeConfig } from "../../kubeconfig-dialog";
import { MenuItem } from "../../menu";
import { CreateServiceAccountDialog } from "./create-dialog"; import { CreateServiceAccountDialog } from "./create-dialog";
import { serviceAccountsStore } from "./store"; import { serviceAccountsStore } from "./store";
import type { ServiceAccountsRouteParams } from "../../../../common/routes"; import type { ServiceAccountsRouteParams } from "../../../../common/routes";
@ -69,13 +64,3 @@ export class ServiceAccounts extends React.Component<Props> {
} }
} }
export function ServiceAccountMenu(props: KubeObjectMenuProps<ServiceAccount>) {
const { object, toolbar } = props;
return (
<MenuItem onClick={() => openServiceAccountKubeConfig(object)}>
<Icon material="insert_drive_file" tooltip="Kubeconfig File" interactive={toolbar} />
<span className="title">Kubeconfig</span>
</MenuItem>
);
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { KubeObjectMenuProps } from "../kube-object-menu";
import { CronJob, cronJobApi } from "../../../common/k8s-api/endpoints";
import { MenuItem } from "../menu";
import { CronJobTriggerDialog } from "./cronjob-trigger-dialog";
import { Icon } from "../icon";
import { ConfirmDialog } from "../confirm-dialog";
import { Notifications } from "../notifications";
export function CronJobMenu(props: KubeObjectMenuProps<CronJob>) {
const { object, toolbar } = props;
return (
<>
<MenuItem onClick={() => CronJobTriggerDialog.open(object)}>
<Icon material="play_circle_filled" tooltip="Trigger" interactive={toolbar}/>
<span className="title">Trigger</span>
</MenuItem>
{object.isSuspend() ?
<MenuItem onClick={() => ConfirmDialog.open({
ok: async () => {
try {
await cronJobApi.resume({ namespace: object.getNs(), name: object.getName() });
} catch (err) {
Notifications.error(err);
}
},
labelOk: `Resume`,
message: (
<p>
Resume CronJob <b>{object.getName()}</b>?
</p>),
})}>
<Icon material="play_circle_outline" tooltip="Resume" interactive={toolbar}/>
<span className="title">Resume</span>
</MenuItem>
: <MenuItem onClick={() => ConfirmDialog.open({
ok: async () => {
try {
await cronJobApi.suspend({ namespace: object.getNs(), name: object.getName() });
} catch (err) {
Notifications.error(err);
}
},
labelOk: `Suspend`,
message: (
<p>
Suspend CronJob <b>{object.getName()}</b>?
</p>),
})}>
<Icon material="pause_circle_filled" tooltip="Suspend" interactive={toolbar}/>
<span className="title">Suspend</span>
</MenuItem>
}
</>
);
}

View File

@ -8,18 +8,11 @@ import "./cronjobs.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import { CronJob, cronJobApi } from "../../../common/k8s-api/endpoints/cron-job.api";
import { MenuItem } from "../menu";
import { Icon } from "../icon";
import { cronJobStore } from "./cronjob.store"; import { cronJobStore } from "./cronjob.store";
import { jobStore } from "../+workloads-jobs/job.store"; import { jobStore } from "../+workloads-jobs/job.store";
import { eventStore } from "../+events/event.store"; import { eventStore } from "../+events/event.store";
import type { KubeObjectMenuProps } from "../kube-object-menu";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import { CronJobTriggerDialog } from "./cronjob-trigger-dialog";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { ConfirmDialog } from "../confirm-dialog/confirm-dialog";
import { Notifications } from "../notifications/notifications";
import type { CronJobsRouteParams } from "../../../common/routes"; import type { CronJobsRouteParams } from "../../../common/routes";
import moment from "moment"; import moment from "moment";
@ -87,53 +80,3 @@ export class CronJobs extends React.Component<Props> {
} }
} }
export function CronJobMenu(props: KubeObjectMenuProps<CronJob>) {
const { object, toolbar } = props;
return (
<>
<MenuItem onClick={() => CronJobTriggerDialog.open(object)}>
<Icon material="play_circle_filled" tooltip="Trigger" interactive={toolbar}/>
<span className="title">Trigger</span>
</MenuItem>
{object.isSuspend() ?
<MenuItem onClick={() => ConfirmDialog.open({
ok: async () => {
try {
await cronJobApi.resume({ namespace: object.getNs(), name: object.getName() });
} catch (err) {
Notifications.error(err);
}
},
labelOk: `Resume`,
message: (
<p>
Resume CronJob <b>{object.getName()}</b>?
</p>),
})}>
<Icon material="play_circle_outline" tooltip="Resume" interactive={toolbar}/>
<span className="title">Resume</span>
</MenuItem>
: <MenuItem onClick={() => ConfirmDialog.open({
ok: async () => {
try {
await cronJobApi.suspend({ namespace: object.getNs(), name: object.getName() });
} catch (err) {
Notifications.error(err);
}
},
labelOk: `Suspend`,
message: (
<p>
Suspend CronJob <b>{object.getName()}</b>?
</p>),
})}>
<Icon material="pause_circle_filled" tooltip="Suspend" interactive={toolbar}/>
<span className="title">Suspend</span>
</MenuItem>
}
</>
);
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { KubeObjectMenuProps } from "../kube-object-menu";
import { Deployment, deploymentApi } from "../../../common/k8s-api/endpoints";
import { MenuItem } from "../menu";
import { DeploymentScaleDialog } from "./deployment-scale-dialog";
import { Icon } from "../icon";
import { ConfirmDialog } from "../confirm-dialog";
import { Notifications } from "../notifications";
export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
const { object, toolbar } = props;
return (
<>
<MenuItem onClick={() => DeploymentScaleDialog.open(object)}>
<Icon material="open_with" tooltip="Scale" interactive={toolbar}/>
<span className="title">Scale</span>
</MenuItem>
<MenuItem onClick={() => ConfirmDialog.open({
ok: async () =>
{
try {
await deploymentApi.restart({
namespace: object.getNs(),
name: object.getName(),
});
} catch (err) {
Notifications.error(err);
}
},
labelOk: `Restart`,
message: (
<p>
Are you sure you want to restart deployment <b>{object.getName()}</b>?
</p>
),
})}>
<Icon material="autorenew" tooltip="Restart" interactive={toolbar}/>
<span className="title">Restart</span>
</MenuItem>
</>
);
}

View File

@ -8,12 +8,7 @@ import "./deployments.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import { Deployment, deploymentApi } from "../../../common/k8s-api/endpoints"; import type { Deployment } from "../../../common/k8s-api/endpoints";
import type { KubeObjectMenuProps } from "../kube-object-menu";
import { MenuItem } from "../menu";
import { Icon } from "../icon";
import { DeploymentScaleDialog } from "./deployment-scale-dialog";
import { ConfirmDialog } from "../confirm-dialog";
import { deploymentStore } from "./deployments.store"; import { deploymentStore } from "./deployments.store";
import { eventStore } from "../+events/event.store"; import { eventStore } from "../+events/event.store";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
@ -21,7 +16,6 @@ import { cssNames } from "../../utils";
import kebabCase from "lodash/kebabCase"; import kebabCase from "lodash/kebabCase";
import orderBy from "lodash/orderBy"; import orderBy from "lodash/orderBy";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { Notifications } from "../notifications";
import type { DeploymentsRouteParams } from "../../../common/routes"; import type { DeploymentsRouteParams } from "../../../common/routes";
enum columnId { enum columnId {
@ -95,38 +89,3 @@ export class Deployments extends React.Component<Props> {
); );
} }
} }
export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
const { object, toolbar } = props;
return (
<>
<MenuItem onClick={() => DeploymentScaleDialog.open(object)}>
<Icon material="open_with" tooltip="Scale" interactive={toolbar}/>
<span className="title">Scale</span>
</MenuItem>
<MenuItem onClick={() => ConfirmDialog.open({
ok: async () =>
{
try {
await deploymentApi.restart({
namespace: object.getNs(),
name: object.getName(),
});
} catch (err) {
Notifications.error(err);
}
},
labelOk: `Restart`,
message: (
<p>
Are you sure you want to restart deployment <b>{object.getName()}</b>?
</p>
),
})}>
<Icon material="autorenew" tooltip="Restart" interactive={toolbar}/>
<span className="title">Restart</span>
</MenuItem>
</>
);
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { KubeObjectMenuProps } from "../kube-object-menu";
import type { ReplicaSet } from "../../../common/k8s-api/endpoints";
import { MenuItem } from "../menu";
import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog";
import { Icon } from "../icon";
export function ReplicaSetMenu(props: KubeObjectMenuProps<ReplicaSet>) {
const { object, toolbar } = props;
return (
<>
<MenuItem onClick={() => ReplicaSetScaleDialog.open(object)}>
<Icon material="open_with" tooltip="Scale" interactive={toolbar}/>
<span className="title">Scale</span>
</MenuItem>
</>
);
}

View File

@ -7,15 +7,10 @@ import "./replicasets.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { ReplicaSet } from "../../../common/k8s-api/endpoints";
import type { KubeObjectMenuProps } from "../kube-object-menu";
import { replicaSetStore } from "./replicasets.store"; import { replicaSetStore } from "./replicasets.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import { MenuItem } from "../menu/menu";
import { Icon } from "../icon/icon";
import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog";
import type { ReplicaSetsRouteParams } from "../../../common/routes"; import type { ReplicaSetsRouteParams } from "../../../common/routes";
import { eventStore } from "../+events/event.store"; import { eventStore } from "../+events/event.store";
@ -75,15 +70,3 @@ export class ReplicaSets extends React.Component<Props> {
} }
} }
export function ReplicaSetMenu(props: KubeObjectMenuProps<ReplicaSet>) {
const { object, toolbar } = props;
return (
<>
<MenuItem onClick={() => ReplicaSetScaleDialog.open(object)}>
<Icon material="open_with" tooltip="Scale" interactive={toolbar}/>
<span className="title">Scale</span>
</MenuItem>
</>
);
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { KubeObjectMenuProps } from "../kube-object-menu";
import type { StatefulSet } from "../../../common/k8s-api/endpoints";
import { MenuItem } from "../menu";
import { StatefulSetScaleDialog } from "./statefulset-scale-dialog";
import { Icon } from "../icon";
export function StatefulSetMenu(props: KubeObjectMenuProps<StatefulSet>) {
const { object, toolbar } = props;
return (
<>
<MenuItem onClick={() => StatefulSetScaleDialog.open(object)}>
<Icon material="open_with" tooltip="Scale" interactive={toolbar}/>
<span className="title">Scale</span>
</MenuItem>
</>
);
}

View File

@ -12,12 +12,8 @@ import type { StatefulSet } from "../../../common/k8s-api/endpoints";
import { podsStore } from "../+workloads-pods/pods.store"; import { podsStore } from "../+workloads-pods/pods.store";
import { statefulSetStore } from "./statefulset.store"; import { statefulSetStore } from "./statefulset.store";
import { eventStore } from "../+events/event.store"; import { eventStore } from "../+events/event.store";
import type { KubeObjectMenuProps } from "../kube-object-menu";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { StatefulSetScaleDialog } from "./statefulset-scale-dialog";
import { MenuItem } from "../menu/menu";
import { Icon } from "../icon/icon";
import type { StatefulSetsRouteParams } from "../../../common/routes"; import type { StatefulSetsRouteParams } from "../../../common/routes";
enum columnId { enum columnId {
@ -76,16 +72,3 @@ export class StatefulSets extends React.Component<Props> {
); );
} }
} }
export function StatefulSetMenu(props: KubeObjectMenuProps<StatefulSet>) {
const { object, toolbar } = props;
return (
<>
<MenuItem onClick={() => StatefulSetScaleDialog.open(object)}>
<Icon material="open_with" tooltip="Scale" interactive={toolbar}/>
<span className="title">Scale</span>
</MenuItem>
</>
);
}

View File

@ -14,9 +14,8 @@ import { ActivateEntityCommand } from "../../activate-entity-command";
import type { CommandContext, CommandRegistration } from "./commands"; import type { CommandContext, CommandRegistration } from "./commands";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import commandOverlayInjectable from "../command-overlay.injectable"; import commandOverlayInjectable from "../command-overlay.injectable";
import createTerminalTabInjectable import createTerminalTabInjectable from "../../dock/terminal/create-terminal-tab.injectable";
from "../../dock/create-terminal-tab/create-terminal-tab.injectable"; import type { DockTabCreate } from "../../dock/dock/store";
import type { DockTabCreate } from "../../dock/dock-store/dock.store";
export function isKubernetesClusterActive(context: CommandContext): boolean { export function isKubernetesClusterActive(context: CommandContext): boolean {
return context.entity?.kind === "KubernetesCluster"; return context.entity?.kind === "KubernetesCluster";

View File

@ -8,12 +8,12 @@ import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import fse from "fs-extra"; import fse from "fs-extra";
import { DockTabs } from "../dock-tabs"; import { DockTabs } from "../dock-tabs";
import { DockStore, DockTab, TabKind } from "../dock-store/dock.store"; import { DockStore, DockTab, TabKind } from "../dock/store";
import { noop } from "../../../utils"; import { noop } from "../../../utils";
import { ThemeStore } from "../../../theme.store"; import { ThemeStore } from "../../../theme.store";
import { UserStore } from "../../../../common/user-store"; import { UserStore } from "../../../../common/user-store";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import dockStoreInjectable from "../dock-store/dock-store.injectable"; import dockStoreInjectable from "../dock/store.injectable";
import type { DiRender } from "../../test-utils/renderFor"; import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor"; import { renderFor } from "../../test-utils/renderFor";
import directoryForUserDataInjectable import directoryForUserDataInjectable

View File

@ -1,19 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { createInstallChartTab } from "./create-install-chart-tab";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import installChartStoreInjectable from "../install-chart-store/install-chart-store.injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const createInstallChartTabInjectable = getInjectable({
instantiate: (di) => createInstallChartTab({
installChartStore: di.inject(installChartStoreInjectable),
createDockTab: di.inject(dockStoreInjectable).createTab,
}),
lifecycle: lifecycleEnum.singleton,
});
export default createInstallChartTabInjectable;

View File

@ -1,44 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
import {
DockTab,
DockTabCreate,
DockTabCreateSpecific,
TabKind,
} from "../dock-store/dock.store";
import type { InstallChartStore } from "../install-chart-store/install-chart.store";
interface Dependencies {
createDockTab: (rawTab: DockTabCreate, addNumber: boolean) => DockTab;
installChartStore: InstallChartStore;
}
export const createInstallChartTab =
({ createDockTab, installChartStore }: Dependencies) =>
(chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => {
const { name, repo, version } = chart;
const tab = createDockTab(
{
title: `Helm Install: ${repo}/${name}`,
...tabParams,
kind: TabKind.INSTALL_CHART,
},
false,
);
installChartStore.setData(tab.id, {
name,
repo,
version,
namespace: "default",
releaseName: "",
description: "",
});
return tab;
};

View File

@ -1,70 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import fs from "fs-extra";
import path from "path";
import os from "os";
import groupBy from "lodash/groupBy";
import filehound from "filehound";
import { watch } from "chokidar";
import { autoBind, StorageHelper } from "../../../utils";
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
import type { DockStore } from "../dock-store/dock.store";
interface Dependencies {
dockStore: DockStore,
createStorage:<T> (storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
}
export class CreateResourceStore extends DockTabStore<string> {
constructor(protected dependencies: Dependencies) {
super(dependencies, {
storageKey: "create_resource",
});
autoBind(this);
fs.ensureDirSync(this.userTemplatesFolder);
}
get lensTemplatesFolder():string {
return path.resolve(__static, "../templates/create-resource");
}
get userTemplatesFolder():string {
return path.join(os.homedir(), ".k8slens", "templates");
}
async getTemplates(templatesPath: string, defaultGroup: string) {
const templates = await filehound.create().path(templatesPath).ext(["yaml", "json"]).depth(1).find();
return templates ? this.groupTemplates(templates, templatesPath, defaultGroup) : {};
}
groupTemplates(templates: string[], templatesPath: string, defaultGroup: string) {
return groupBy(templates, (v:string) =>
path.relative(templatesPath, v).split(path.sep).length>1
? path.parse(path.relative(templatesPath, v)).dir
: defaultGroup);
}
async getMergedTemplates() {
const userTemplates = await this.getTemplates(this.userTemplatesFolder, "ungrouped");
const lensTemplates = await this.getTemplates(this.lensTemplatesFolder, "lens");
return { ...userTemplates, ...lensTemplates };
}
async watchUserTemplates(callback: ()=> void){
watch(this.userTemplatesFolder, {
depth: 1,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 500,
},
}).on("all", () => {
callback();
});
}
}

View File

@ -1,17 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { createResourceTab } from "./create-resource-tab";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const createResourceTabInjectable = getInjectable({
instantiate: (di) => createResourceTab({
dockStore: di.inject(dockStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createResourceTabInjectable;

View File

@ -1,18 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store";
interface Dependencies {
dockStore: DockStore
}
export const createResourceTab =
({ dockStore }: Dependencies) =>
(tabParams: DockTabCreateSpecific = {}) =>
dockStore.createTab({
title: "Create resource",
...tabParams,
kind: TabKind.CREATE_RESOURCE,
});

View File

@ -1,7 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.CreateResource {
}

View File

@ -1,184 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./create-resource.scss";
import React from "react";
import path from "path";
import fs from "fs-extra";
import { GroupSelectOption, Select, SelectOption } from "../select";
import yaml from "js-yaml";
import { makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import type { CreateResourceStore } from "./create-resource-store/create-resource.store";
import type { DockTab } from "./dock-store/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 createResourceStoreInjectable
from "./create-resource-store/create-resource-store.injectable";
interface Props {
tab: DockTab;
}
interface Dependencies {
createResourceStore: CreateResourceStore
}
@observer
class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
@observable currentTemplates: Map<string, SelectOption> = new Map();
@observable error = "";
@observable templates: GroupSelectOption<SelectOption>[] = [];
constructor(props: Props & Dependencies) {
super(props);
makeObservable(this);
}
componentDidMount() {
this.props.createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v));
this.props.createResourceStore.watchUserTemplates(() => this.props.createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v)));
}
updateGroupSelectOptions(templates: Record<string, string[]>) {
this.templates = Object.entries(templates)
.map(([name, grouping]) => this.convertToGroup(name, grouping));
}
convertToGroup(group: string, items: string[]): GroupSelectOption {
const options = items.map(v => ({ label: path.parse(v).name, value: v }));
return { label: group, options };
}
get tabId() {
return this.props.tab.id;
}
get data() {
return this.props.createResourceStore.getData(this.tabId);
}
get currentTemplate() {
return this.currentTemplates.get(this.tabId) ?? null;
}
onChange = (value: string) => {
this.error = ""; // reset first, validation goes later
this.props.createResourceStore.setData(this.tabId, value);
};
onError = (error: Error | string) => {
this.error = error.toString();
};
onSelectTemplate = (item: SelectOption) => {
this.currentTemplates.set(this.tabId, item);
fs.readFile(item.value, "utf8").then(v => {
this.props.createResourceStore.setData(this.tabId, v);
});
};
create = async (): Promise<undefined> => {
if (this.error || !this.data.trim()) {
// do not save when field is empty or there is an error
return null;
}
// 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: KubeJsonApiData = await resourceApplierApi.update(resource);
const { kind, apiVersion, metadata: { name, namespace }} = data;
const resourceLink = apiManager.lookupApiLink({ kind, apiVersion, name, namespace });
const showDetails = () => {
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);
return undefined;
};
renderControls() {
return (
<div className="flex gaps align-center">
<Select
autoConvertOptions={false}
controlShouldRenderValue={false} // always keep initial placeholder
className="TemplateSelect"
placeholder="Select Template ..."
options={this.templates}
menuPlacement="top"
themeName="outlined"
onChange={v => this.onSelectTemplate(v)}
value={this.currentTemplate}
/>
</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,
{
getProps: (di, props) => ({
createResourceStore: di.inject(createResourceStoreInjectable),
...props,
}),
},
);

View File

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

View File

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

View File

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

View File

@ -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));
}

View File

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

View File

@ -3,17 +3,15 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable"; import { CreateResourceTabStore } from "./store";
import { CreateResourceStore } from "./create-resource.store";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const createResourceStoreInjectable = getInjectable({ const createResourceTabStoreInjectable = getInjectable({
instantiate: (di) => new CreateResourceStore({ instantiate: (di) => new CreateResourceTabStore({
dockStore: di.inject(dockStoreInjectable),
createStorage: di.inject(createStorageInjectable), createStorage: di.inject(createStorageInjectable),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });
export default createResourceStoreInjectable; export default createResourceTabStoreInjectable;

View File

@ -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",
});
}
}

View File

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

View File

@ -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,
}),
});

View File

@ -1,17 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { createTerminalTab } from "./create-terminal-tab";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const createTerminalTabInjectable = getInjectable({
instantiate: (di) => createTerminalTab({
dockStore: di.inject(dockStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createTerminalTabInjectable;

View File

@ -1,22 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
DockStore,
DockTabCreateSpecific,
TabKind,
} from "../dock-store/dock.store";
interface Dependencies {
dockStore: DockStore;
}
export const createTerminalTab =
({ dockStore }: Dependencies) =>
(tabParams: DockTabCreateSpecific = {}) =>
dockStore.createTab({
title: `Terminal`,
...tabParams,
kind: TabKind.TERMINAL,
});

View File

@ -1,19 +0,0 @@
/**
* 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 { createUpgradeChartTab } from "./create-upgrade-chart-tab";
import upgradeChartStoreInjectable from "../upgrade-chart-store/upgrade-chart-store.injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const createUpgradeChartTabInjectable = getInjectable({
instantiate: (di) => createUpgradeChartTab({
upgradeChartStore: di.inject(upgradeChartStoreInjectable),
dockStore: di.inject(dockStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createUpgradeChartTabInjectable;

View File

@ -1,41 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store";
import type { UpgradeChartStore } from "../upgrade-chart-store/upgrade-chart.store";
interface Dependencies {
upgradeChartStore: UpgradeChartStore;
dockStore: DockStore
}
export const createUpgradeChartTab =
({ upgradeChartStore, dockStore }: Dependencies) =>
(release: HelmRelease, tabParams: DockTabCreateSpecific = {}) => {
let tab = upgradeChartStore.getTabByRelease(release.getName());
if (tab) {
dockStore.open();
dockStore.selectTab(tab.id);
}
if (!tab) {
tab = dockStore.createTab(
{
title: `Helm Upgrade: ${release.getName()}`,
...tabParams,
kind: TabKind.UPGRADE_CHART,
},
false,
);
upgradeChartStore.setData(tab.id, {
releaseName: release.getName(),
releaseNamespace: release.getNs(),
});
}
return tab;
};

View File

@ -1,18 +0,0 @@
/**
* 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 { DockStore } from "./dock.store";
import dockStorageInjectable from "./dock-storage/dock-storage.injectable";
const dockStoreInjectable = getInjectable({
instantiate: (di) =>
new DockStore({
storage: di.inject(dockStorageInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default dockStoreInjectable;

View File

@ -4,13 +4,11 @@
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { DockTabStore, DockTabStoreOptions } from "./dock-tab.store"; import { DockTabStore, DockTabStoreOptions } from "./dock-tab.store";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const createDockTabStoreInjectable = getInjectable({ const createDockTabStoreInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) => {
const dependencies = { const dependencies = {
dockStore: di.inject(dockStoreInjectable),
createStorage: di.inject(createStorageInjectable), createStorage: di.inject(createStorageInjectable),
}; };

View File

@ -3,9 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { autorun, observable, reaction } from "mobx"; import { action, observable, reaction } from "mobx";
import { autoBind, StorageHelper, toJS } from "../../../utils"; import { autoBind, StorageHelper, toJS } from "../../../utils";
import type { DockStore, TabId } from "../dock-store/dock.store"; import type { TabId } from "../dock/store";
export interface DockTabStoreOptions { export interface DockTabStoreOptions {
autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true
@ -14,16 +14,15 @@ export interface DockTabStoreOptions {
export type DockTabStorageState<T> = Record<TabId, T>; export type DockTabStorageState<T> = Record<TabId, T>;
interface Dependencies { interface DockTabStoreDependencies {
dockStore: DockStore
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>> createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
} }
export class DockTabStore<T> { export class DockTabStore<T> {
protected storage?: StorageHelper<DockTabStorageState<T>>; protected storage?: StorageHelper<DockTabStorageState<T>>;
protected data = observable.map<TabId, T>(); private data = observable.map<TabId, T>();
constructor(protected dependencies: Dependencies, protected options: DockTabStoreOptions) { constructor(protected dependencies: DockTabStoreDependencies, protected options: DockTabStoreOptions) {
autoBind(this); autoBind(this);
this.options = { this.options = {
@ -48,17 +47,6 @@ export class DockTabStore<T> {
reaction(() => this.toJSON(), data => this.storage.set(data)); reaction(() => this.toJSON(), data => this.storage.set(data));
}); });
} }
// clear data for closed tabs
autorun(() => {
const currentTabs = this.dependencies.dockStore.tabs.map(tab => tab.id);
Array.from(this.data.keys()).forEach(tabId => {
if (!currentTabs.includes(tabId)) {
this.clearData(tabId);
}
});
});
} }
protected finalizeDataForSave(data: T): T { protected finalizeDataForSave(data: T): T {
@ -75,8 +63,22 @@ export class DockTabStore<T> {
return Object.fromEntries<T>(deepCopy); return Object.fromEntries<T>(deepCopy);
} }
protected getAllData() {
return this.data.toJSON();
}
findTabIdFromData(inspecter: (val: T) => any): TabId | undefined {
for (const [tabId, data] of this.data) {
if (inspecter(data)) {
return tabId;
}
}
return undefined;
}
isReady(tabId: TabId): boolean { isReady(tabId: TabId): boolean {
return Boolean(this.getData(tabId) !== undefined); return this.getData(tabId) !== undefined;
} }
getData(tabId: TabId) { getData(tabId: TabId) {
@ -91,8 +93,11 @@ export class DockTabStore<T> {
this.data.delete(tabId); this.data.delete(tabId);
} }
@action
reset() { reset() {
this.data.clear(); for (const tabId of this.data.keys()) {
this.clearData(tabId);
}
this.storage?.reset(); this.storage?.reset();
} }
} }

View File

@ -8,14 +8,14 @@ import "./dock-tab.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { boundMethod, cssNames, prevDefault, isMiddleClick } from "../../utils"; import { boundMethod, cssNames, prevDefault, isMiddleClick } from "../../utils";
import type { DockStore, DockTab as DockTabModel } from "./dock-store/dock.store"; import type { DockStore, DockTab as DockTabModel } from "./dock/store";
import { Tab, TabProps } from "../tabs"; import { Tab, TabProps } from "../tabs";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Menu, MenuItem } from "../menu"; import { Menu, MenuItem } from "../menu";
import { observable, makeObservable } from "mobx"; import { observable, makeObservable } from "mobx";
import { isMac } from "../../../common/vars"; import { isMac } from "../../../common/vars";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock-store/dock-store.injectable"; import dockStoreInjectable from "./dock/store.injectable";
export interface DockTabProps extends TabProps<DockTabModel> { export interface DockTabProps extends TabProps<DockTabModel> {
moreActions?: React.ReactNode; moreActions?: React.ReactNode;

View File

@ -8,9 +8,9 @@ import { reaction } from "mobx";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Tabs } from "../tabs/tabs"; import { Tabs } from "../tabs/tabs";
import { DockTab } from "./dock-tab"; import { DockTab } from "./dock-tab";
import type { DockTab as DockTabModel } from "./dock-store/dock.store"; import type { DockTab as DockTabModel } from "./dock/store";
import { TabKind, DockStore } from "./dock-store/dock.store"; import { TabKind, DockStore } from "./dock/store";
import { TerminalTab } from "./terminal-tab"; import { TerminalTab } from "./terminal/dock-tab";
interface Props { interface Props {
tabs: DockTabModel[] tabs: DockTabModel[]

View File

@ -13,18 +13,19 @@ import { Icon } from "../icon";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
import { MenuActions } from "../menu/menu-actions"; import { MenuActions } from "../menu/menu-actions";
import { ResizeDirection, ResizingAnchor } from "../resizing-anchor"; import { ResizeDirection, ResizingAnchor } from "../resizing-anchor";
import { CreateResource } from "./create-resource"; import { CreateResource } from "./create-resource/view";
import { DockTabs } from "./dock-tabs"; import { DockTabs } from "./dock-tabs";
import { DockStore, DockTab, TabKind } from "./dock-store/dock.store"; import { DockStore, DockTab, TabKind } from "./dock/store";
import { EditResource } from "./edit-resource"; import { EditResource } from "./edit-resource/view";
import { InstallChart } from "./install-chart"; import { InstallChart } from "./install-chart/view";
import { LogsDockTab } from "./logs/dock-tab"; import { LogsDockTab } from "./logs/view";
import { TerminalWindow } from "./terminal-window"; import { TerminalWindow } from "./terminal/view";
import { UpgradeChart } from "./upgrade-chart"; import { UpgradeChart } from "./upgrade-chart/view";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import createResourceTabInjectable from "./create-resource-tab/create-resource-tab.injectable"; import createResourceTabInjectable from "./create-resource/create-resource-tab.injectable";
import dockStoreInjectable from "./dock-store/dock-store.injectable"; import dockStoreInjectable from "./dock/store.injectable";
import createTerminalTabInjectable from "./create-terminal-tab/create-terminal-tab.injectable"; import createTerminalTabInjectable from "./terminal/create-terminal-tab.injectable";
import { ErrorBoundary } from "../error-boundary";
interface Props { interface Props {
className?: string; className?: string;
@ -188,7 +189,9 @@ class NonInjectedDock extends React.Component<Props & Dependencies> {
)} )}
</div> </div>
</div> </div>
{this.renderTabContent()} <ErrorBoundary>
{this.renderTabContent()}
</ErrorBoundary>
</div> </div>
); );
} }

View File

@ -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 "./store";
import dockStoreInjectable from "./store.injectable";
const closeDockTabInjectable = getInjectable({
instantiate: (di) => {
const dockStore = di.inject(dockStoreInjectable);
return (tabId: TabId): void => {
dockStore.closeTab(tabId);
};
},
lifecycle: lifecycleEnum.singleton,
});
export default closeDockTabInjectable;

View File

@ -0,0 +1,20 @@
/**
* 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 "./store.injectable";
import type { DockTab, DockTabCreate } from "./store";
const createDockTabInjectable = getInjectable({
instantiate: (di) => {
const dockStore = di.inject(dockStoreInjectable);
return (rawTabDesc: DockTabCreate, addNumber?: boolean): DockTab =>
dockStore.createTab(rawTabDesc, addNumber);
},
lifecycle: lifecycleEnum.singleton,
});
export default createDockTabInjectable;

View File

@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import createStorageInjectable from "../../../../utils/create-storage/create-storage.injectable"; import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
import { DockStorageState, TabKind } from "../dock.store"; import { DockStorageState, TabKind } from "./store";
const dockStorageInjectable = getInjectable({ const dockStorageInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) => {

View File

@ -0,0 +1,22 @@
/**
* 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 "./store.injectable";
import type { TabId } from "./store";
const renameTabInjectable = getInjectable({
instantiate: (di) => {
const dockStore = di.inject(dockStoreInjectable);
return (tabId: TabId, title: string): void => {
dockStore.renameTab(tabId, title);
};
},
lifecycle: lifecycleEnum.singleton,
});
export default renameTabInjectable;

View File

@ -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 "./store";
import dockStoreInjectable from "./store.injectable";
const selectDockTabInjectable = getInjectable({
instantiate: (di) => {
const dockStore = di.inject(dockStoreInjectable);
return (tabId: TabId): void => {
dockStore.selectTab(tabId);
};
},
lifecycle: lifecycleEnum.singleton,
});
export default selectDockTabInjectable;

View File

@ -0,0 +1,35 @@
/**
* 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 { DockStore, TabKind } from "./store";
import dockStorageInjectable from "./dock-storage.injectable";
import clearLogTabDataInjectable from "../logs/clear-log-tab-data.injectable";
import clearUpgradeChartTabDataInjectable from "../upgrade-chart/clear-upgrade-chart-tab-data.injectable";
import clearCreateResourceTabDataInjectable from "../create-resource/clear-create-resource-tab-data.injectable";
import clearEditResourceTabDataInjectable from "../edit-resource/clear-edit-resource-tab-data.injectable";
import clearTerminalTabDataInjectable from "../terminal/clear-terminal-tab-data.injectable";
import clearInstallChartTabDataInjectable from "../install-chart/clear-install-chart-tab-data.injectable";
import isLogsTabDataValidInjectable from "../logs/is-logs-tab-data-valid.injectable";
const dockStoreInjectable = getInjectable({
instantiate: (di) => new DockStore({
storage: di.inject(dockStorageInjectable),
tabDataClearers: {
[TabKind.POD_LOGS]: di.inject(clearLogTabDataInjectable),
[TabKind.UPGRADE_CHART]: di.inject(clearUpgradeChartTabDataInjectable),
[TabKind.CREATE_RESOURCE]: di.inject(clearCreateResourceTabDataInjectable),
[TabKind.EDIT_RESOURCE]: di.inject(clearEditResourceTabDataInjectable),
[TabKind.INSTALL_CHART]: di.inject(clearInstallChartTabDataInjectable),
[TabKind.TERMINAL]: di.inject(clearTerminalTabDataInjectable),
},
tabDataValidator: {
[TabKind.POD_LOGS]: di.inject(isLogsTabDataValidInjectable),
},
}),
lifecycle: lifecycleEnum.singleton,
});
export default dockStoreInjectable;

View File

@ -98,11 +98,13 @@ export interface DockTabCloseEvent {
} }
interface Dependencies { interface Dependencies {
storage: StorageHelper<DockStorageState> readonly storage: StorageHelper<DockStorageState>
readonly tabDataClearers: Record<TabKind, (tabId: TabId) => void>;
readonly tabDataValidator: Partial<Record<TabKind, (tabId: TabId) => boolean>>;
} }
export class DockStore implements DockStorageState { export class DockStore implements DockStorageState {
constructor(private dependencies: Dependencies) { constructor(private readonly dependencies: Dependencies) {
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
this.init(); this.init();
@ -171,6 +173,16 @@ export class DockStore implements DockStorageState {
private init() { private init() {
// adjust terminal height if window size changes // adjust terminal height if window size changes
window.addEventListener("resize", throttle(this.adjustHeight, 250)); window.addEventListener("resize", throttle(this.adjustHeight, 250));
this.whenReady.then(action(() => {
for (const tab of this.tabs) {
const validator = this.dependencies.tabDataValidator[tab.kind];
if (validator && !validator(tab.id)) {
this.closeTab(tab.id);
}
}
}));
} }
get maxHeight() { get maxHeight() {
@ -321,6 +333,7 @@ export class DockStore implements DockStorageState {
} }
this.tabs = this.tabs.filter(tab => tab.id !== tabId); this.tabs = this.tabs.filter(tab => tab.id !== tabId);
this.dependencies.tabDataClearers[tab.kind](tab.id);
if (this.selectedTabId === tab.id) { if (this.selectedTabId === tab.id) {
if (this.tabs.length) { if (this.tabs.length) {
@ -334,6 +347,7 @@ export class DockStore implements DockStorageState {
} }
} }
@action
closeTabs(tabs: DockTab[]) { closeTabs(tabs: DockTab[]) {
tabs.forEach(tab => this.closeTab(tab.id)); tabs.forEach(tab => this.closeTab(tab.id));
} }

View File

@ -1,20 +0,0 @@
/**
* 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/dock-store.injectable";
import { EditResourceStore } from "./edit-resource.store";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const editResourceStoreInjectable = getInjectable({
instantiate: (di) =>
new EditResourceStore({
dockStore: di.inject(dockStoreInjectable),
createStorage: di.inject(createStorageInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default editResourceStoreInjectable;

View File

@ -1,109 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { autoBind, noop, StorageHelper } from "../../../utils";
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
import { autorun, IReactionDisposer } from "mobx";
import type { DockStore, DockTab, TabId } from "../dock-store/dock.store";
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { apiManager } from "../../../../common/k8s-api/api-manager";
import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
export interface EditingResource {
resource: string; // resource path, e.g. /api/v1/namespaces/default
draft?: string; // edited draft in yaml
firstDraft?: string;
}
interface Dependencies {
dockStore: DockStore
createStorage:<T> (storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
}
export class EditResourceStore extends DockTabStore<EditingResource> {
private watchers = new Map<TabId, IReactionDisposer>();
constructor(protected dependencies: Dependencies) {
super(dependencies, {
storageKey: "edit_resource_store",
});
autoBind(this);
}
protected async init() {
super.init();
await this.storage.whenReady;
autorun(() => {
Array.from(this.data).forEach(([tabId, { resource }]) => {
if (this.watchers.get(tabId)) {
return;
}
this.watchers.set(tabId, autorun(() => {
const store = apiManager.getStore(resource);
if (store) {
const isActiveTab = this.dependencies.dockStore.isOpen && this.dependencies.dockStore.selectedTabId === tabId;
const obj = store.getByPath(resource);
// preload resource for editing
if (!obj && !store.isLoaded && !store.isLoading && isActiveTab) {
store.loadFromPath(resource).catch(noop);
}
// auto-close tab when resource removed from store
else if (!obj && store.isLoaded) {
this.dependencies.dockStore.closeTab(tabId);
}
}
}, {
delay: 100, // make sure all kube-object stores are initialized
}));
});
});
}
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
}
getStore(tabId: TabId): KubeObjectStore<KubeObject> | undefined {
return apiManager.getStore(this.getResourcePath(tabId));
}
getResource(tabId: TabId): KubeObject | undefined {
return this.getStore(tabId)?.getByPath(this.getResourcePath(tabId));
}
getResourcePath(tabId: TabId): string | undefined {
return this.getData(tabId)?.resource;
}
getTabByResource(object: KubeObject): DockTab {
const [tabId] = Array.from(this.data).find(([, { resource }]) => {
return object.selfLink === resource;
}) || [];
return this.dependencies.dockStore.getTabById(tabId);
}
clearInitialDraft(tabId: TabId): void {
delete this.getData(tabId)?.firstDraft;
}
reset() {
super.reset();
Array.from(this.watchers).forEach(([tabId, dispose]) => {
this.watchers.delete(tabId);
dispose();
});
}
}

View File

@ -1,19 +0,0 @@
/**
* 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 { editResourceTab } from "./edit-resource-tab";
import editResourceStoreInjectable from "../edit-resource-store/edit-resource-store.injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const editResourceTabInjectable = getInjectable({
instantiate: (di) => editResourceTab({
dockStore: di.inject(dockStoreInjectable),
editResourceStore: di.inject(editResourceStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default editResourceTabInjectable;

View File

@ -1,41 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store";
import type { EditResourceStore } from "../edit-resource-store/edit-resource.store";
interface Dependencies {
dockStore: DockStore;
editResourceStore: EditResourceStore;
}
export const editResourceTab =
({ dockStore, editResourceStore }: Dependencies) =>
(object: KubeObject, tabParams: DockTabCreateSpecific = {}) => {
// use existing tab if already opened
let tab = editResourceStore.getTabByResource(object);
if (tab) {
dockStore.open();
dockStore.selectTab(tab.id);
}
// or create new tab
if (!tab) {
tab = dockStore.createTab(
{
title: `${object.kind}: ${object.getName()}`,
...tabParams,
kind: TabKind.EDIT_RESOURCE,
},
false,
);
editResourceStore.setData(tab.id, {
resource: object.selfLink,
});
}
return tab;
};

View File

@ -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 editResourceTabStoreInjectable from "./store.injectable";
const clearEditResourceTabDataInjectable = getInjectable({
instantiate: (di) => {
const editResourceTabStore = di.inject(editResourceTabStoreInjectable);
return (tabId: TabId) => {
editResourceTabStore.clearData(tabId);
};
},
lifecycle: lifecycleEnum.singleton,
});
export default clearEditResourceTabDataInjectable;

View File

@ -0,0 +1,56 @@
/**
* 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 editResourceTabStoreInjectable from "./store.injectable";
import dockStoreInjectable from "../dock/store.injectable";
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { DockStore, DockTabCreateSpecific, TabId, TabKind } from "../dock/store";
import type { EditResourceTabStore } from "./store";
import { runInAction } from "mobx";
interface Dependencies {
dockStore: DockStore;
editResourceStore: EditResourceTabStore;
}
const createEditResourceTab = ({ dockStore, editResourceStore }: Dependencies) => (object: KubeObject, tabParams: DockTabCreateSpecific = {}): TabId => {
// use existing tab if already opened
const tabId = editResourceStore.getTabIdByResource(object);
if (tabId) {
dockStore.open();
dockStore.selectTab(tabId);
return tabId;
}
return runInAction(() => {
const tab = dockStore.createTab(
{
title: `${object.kind}: ${object.getName()}`,
...tabParams,
kind: TabKind.EDIT_RESOURCE,
},
false,
);
editResourceStore.setData(tab.id, {
resource: object.selfLink,
});
return tab.id;
});
};
const createEditResourceTabInjectable = getInjectable({
instantiate: (di) => createEditResourceTab({
dockStore: di.inject(dockStoreInjectable),
editResourceStore: di.inject(editResourceTabStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createEditResourceTabInjectable;

View File

@ -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 { EditResourceTabStore } from "./store";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const editResourceTabStoreInjectable = getInjectable({
instantiate: (di) => new EditResourceTabStore({
createStorage: di.inject(createStorageInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default editResourceTabStoreInjectable;

View File

@ -0,0 +1,57 @@
/**
* 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";
import type { TabId } from "../dock/store";
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { apiManager } from "../../../../common/k8s-api/api-manager";
import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
export interface EditingResource {
resource: string; // resource path, e.g. /api/v1/namespaces/default
draft?: string; // edited draft in yaml
firstDraft?: string;
}
interface Dependencies {
createStorage:<T> (storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
}
export class EditResourceTabStore extends DockTabStore<EditingResource> {
constructor(protected dependencies: Dependencies) {
super(dependencies, {
storageKey: "edit_resource_store",
});
}
protected finalizeDataForSave({ draft, ...data }: EditingResource): EditingResource {
return data; // skip saving draft to local-storage
}
isReady(tabId: TabId) {
return super.isReady(tabId) && Boolean(this.getResource(tabId)); // ready to edit resource
}
getStore(tabId: TabId): KubeObjectStore<KubeObject> | undefined {
return apiManager.getStore(this.getResourcePath(tabId));
}
getResource(tabId: TabId): KubeObject | undefined {
return this.getStore(tabId)?.getByPath(this.getResourcePath(tabId));
}
getResourcePath(tabId: TabId): string | undefined {
return this.getData(tabId)?.resource;
}
getTabIdByResource(object: KubeObject): TabId {
return this.findTabIdFromData(({ resource }) => object.selfLink === resource);
}
clearInitialDraft(tabId: TabId): void {
delete this.getData(tabId)?.firstDraft;
}
}

View File

@ -3,29 +3,30 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import "./edit-resource.scss";
import React from "react"; import React from "react";
import { computed, makeObservable, observable } from "mobx"; import { autorun, computed, makeObservable, observable } from "mobx";
import { observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import yaml from "js-yaml"; import yaml from "js-yaml";
import type { DockTab } from "./dock-store/dock.store"; import type { DockTab, TabId } from "../dock/store";
import type { EditResourceStore } from "./edit-resource-store/edit-resource.store"; import type { EditResourceTabStore } from "./store";
import { InfoPanel } from "./info-panel"; import { InfoPanel } from "../info-panel";
import { Badge } from "../badge"; import { Badge } from "../../badge";
import { EditorPanel } from "./editor-panel"; import { EditorPanel } from "../editor-panel";
import { Spinner } from "../spinner"; import { Spinner } from "../../spinner";
import type { KubeObject } from "../../../common/k8s-api/kube-object"; import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { createPatch } from "rfc6902"; import { createPatch } from "rfc6902";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import editResourceStoreInjectable from "./edit-resource-store/edit-resource-store.injectable"; import editResourceTabStoreInjectable from "./store.injectable";
import { noop } from "../../../utils";
import closeDockTabInjectable from "../dock/close-dock-tab.injectable";
interface Props { interface Props {
tab: DockTab; tab: DockTab;
} }
interface Dependencies { interface Dependencies {
editResourceStore: EditResourceStore editResourceStore: EditResourceTabStore;
closeTab: (tabId: TabId) => void;
} }
@observer @observer
@ -37,6 +38,26 @@ class NonInjectedEditResource extends React.Component<Props & Dependencies> {
makeObservable(this); makeObservable(this);
} }
componentDidMount(): void {
disposeOnUnmount(this, [
autorun(() => {
const store = this.props.editResourceStore.getStore(this.props.tab.id);
const tabData = this.props.editResourceStore.getData(this.props.tab.id);
const obj = this.resource;
if (!obj) {
if (store.isLoaded) {
// auto-close tab when resource removed from store
this.props.closeTab(this.props.tab.id);
} else if (!store.isLoading) {
// preload resource for editing
store.loadFromPath(tabData.resource).catch(noop);
}
}
}),
]);
}
get tabId() { get tabId() {
return this.props.tab.id; return this.props.tab.id;
} }
@ -132,13 +153,10 @@ class NonInjectedEditResource extends React.Component<Props & Dependencies> {
} }
} }
export const EditResource = withInjectables<Dependencies, Props>( export const EditResource = withInjectables<Dependencies, Props>(NonInjectedEditResource, {
NonInjectedEditResource, getProps: (di, props) => ({
editResourceStore: di.inject(editResourceTabStoreInjectable),
{ closeTab: di.inject(closeDockTabInjectable),
getProps: (di, props) => ({ ...props,
editResourceStore: di.inject(editResourceStoreInjectable), }),
...props, });
}),
},
);

View File

@ -8,11 +8,11 @@ import throttle from "lodash/throttle";
import React from "react"; import React from "react";
import { makeObservable, observable, reaction } from "mobx"; import { makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import type { DockStore, TabId } from "./dock-store/dock.store"; import type { DockStore, TabId } from "./dock/store";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { MonacoEditor, MonacoEditorProps } from "../monaco-editor"; import { MonacoEditor, MonacoEditorProps } from "../monaco-editor";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock-store/dock-store.injectable"; import dockStoreInjectable from "./dock/store.injectable";
export interface EditorPanelProps { export interface EditorPanelProps {
tabId: TabId; tabId: TabId;

View File

@ -12,14 +12,14 @@ import { cssNames } from "../../utils";
import { Button } from "../button"; import { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import type { DockStore, TabId } from "./dock-store/dock.store"; import type { DockStore, TabId } from "./dock/store";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock-store/dock-store.injectable"; import dockStoreInjectable from "./dock/store.injectable";
interface Props extends OptionalProps { interface Props extends OptionalProps {
tabId: TabId; tabId: TabId;
submit?: () => Promise<ReactNode | string>; submit?: () => Promise<ReactNode | string | void>;
} }
interface OptionalProps { interface OptionalProps {
@ -80,7 +80,7 @@ class NonInjectedInfoPanel extends Component<Props & Dependencies> {
try { try {
const result = await this.props.submit(); const result = await this.props.submit();
if (showNotifications) Notifications.ok(result); if (showNotifications && result) Notifications.ok(result);
} catch (error) { } catch (error) {
if (showNotifications) Notifications.error(error.toString()); if (showNotifications) Notifications.error(error.toString());
} finally { } finally {

View File

@ -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 installChartTabStoreInjectable from "./store.injectable";
const clearInstallChartTabDataInjectable = getInjectable({
instantiate: (di) => {
const installChartTabStore = di.inject(installChartTabStoreInjectable);
return (tabId: TabId) => {
installChartTabStore.clearData(tabId);
};
},
lifecycle: lifecycleEnum.singleton,
});
export default clearInstallChartTabDataInjectable;

View File

@ -0,0 +1,55 @@
/**
* 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 installChartTabStoreInjectable from "./store.injectable";
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
import {
DockTab,
DockTabCreate,
DockTabCreateSpecific,
TabKind,
} from "../dock/store";
import type { InstallChartTabStore } from "./store";
import createDockTabInjectable from "../dock/create-dock-tab.injectable";
interface Dependencies {
createDockTab: (rawTab: DockTabCreate, addNumber: boolean) => DockTab;
installChartStore: InstallChartTabStore;
}
const createInstallChartTab = ({ createDockTab, installChartStore }: Dependencies) => (chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => {
const { name, repo, version } = chart;
const tab = createDockTab(
{
title: `Helm Install: ${repo}/${name}`,
...tabParams,
kind: TabKind.INSTALL_CHART,
},
false,
);
installChartStore.setData(tab.id, {
name,
repo,
version,
namespace: "default",
releaseName: "",
description: "",
});
return tab;
};
const createInstallChartTabInjectable = getInjectable({
instantiate: (di) => createInstallChartTab({
installChartStore: di.inject(installChartTabStoreInjectable),
createDockTab: di.inject(createDockTabInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createInstallChartTabInjectable;

View File

@ -3,18 +3,16 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { InstallChartStore } from "./install-chart.store"; import { InstallChartTabStore } from "./store";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable"; import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable";
import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api"; import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const installChartStoreInjectable = getInjectable({ const installChartTabStoreInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) => {
const createDockTabStore = di.inject(createDockTabStoreInjectable); const createDockTabStore = di.inject(createDockTabStoreInjectable);
return new InstallChartStore({ return new InstallChartTabStore({
dockStore: di.inject(dockStoreInjectable),
createStorage: di.inject(createStorageInjectable), createStorage: di.inject(createStorageInjectable),
versionsStore: createDockTabStore<string[]>(), versionsStore: createDockTabStore<string[]>(),
detailsStore: createDockTabStore<IReleaseUpdateDetails>(), detailsStore: createDockTabStore<IReleaseUpdateDetails>(),
@ -23,4 +21,4 @@ const installChartStoreInjectable = getInjectable({
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });
export default installChartStoreInjectable; export default installChartTabStoreInjectable;

View File

@ -3,12 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { action, autorun, makeObservable } from "mobx"; import { action, makeObservable, when } from "mobx";
import { DockStore, TabId, TabKind } from "../dock-store/dock.store"; import type { TabId } from "../dock/store";
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
import { getChartDetails, getChartValues } from "../../../../common/k8s-api/endpoints/helm-charts.api"; import { getChartDetails, getChartValues } from "../../../../common/k8s-api/endpoints/helm-charts.api";
import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api"; import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { Notifications } from "../../notifications";
import type { StorageHelper } from "../../../utils"; import type { StorageHelper } from "../../../utils";
export interface IChartInstallData { export interface IChartInstallData {
@ -23,29 +22,18 @@ export interface IChartInstallData {
} }
interface Dependencies { interface Dependencies {
dockStore: DockStore, createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>;
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>> versionsStore: DockTabStore<string[]>;
detailsStore: DockTabStore<IReleaseUpdateDetails>;
versionsStore: DockTabStore<string[]>,
detailsStore: DockTabStore<IReleaseUpdateDetails>
} }
export class InstallChartStore extends DockTabStore<IChartInstallData> { export class InstallChartTabStore extends DockTabStore<IChartInstallData> {
constructor(protected dependencies: Dependencies) { constructor(protected dependencies: Dependencies) {
super( super(
dependencies, dependencies,
{ storageKey: "install_charts" }, { storageKey: "install_charts" },
); );
makeObservable(this); makeObservable(this);
autorun(() => {
const { selectedTab, isOpen } = dependencies.dockStore;
if (selectedTab?.kind === TabKind.INSTALL_CHART && isOpen) {
this.loadData(selectedTab.id)
.catch(err => Notifications.error(String(err)));
}
}, { delay: 250 });
} }
get versions() { get versions() {
@ -60,6 +48,8 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
async loadData(tabId: string) { async loadData(tabId: string) {
const promises = []; const promises = [];
await when(() => this.isReady(tabId));
if (!this.getData(tabId).values) { if (!this.getData(tabId).values) {
promises.push(this.loadValues(tabId)); promises.push(this.loadValues(tabId));
} }
@ -94,8 +84,4 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
return this.loadValues(tabId, attempt + 1); return this.loadValues(tabId, attempt + 1);
} }
} }
setData(tabId: TabId, data: IChartInstallData){
super.setData(tabId, data);
}
} }

View File

@ -8,29 +8,27 @@ import "./install-chart.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import { action, makeObservable, observable } from "mobx"; import { action, makeObservable, observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { DockStore, DockTab } from "./dock-store/dock.store"; import type { DockStore, DockTab } from "../dock/store";
import { InfoPanel } from "./info-panel"; import { InfoPanel } from "../info-panel";
import { Badge } from "../badge"; import { Badge } from "../../badge";
import { NamespaceSelect } from "../+namespaces/namespace-select"; import { NamespaceSelect } from "../../+namespaces/namespace-select";
import { prevDefault } from "../../utils"; import { prevDefault } from "../../../utils";
import type { IChartInstallData, InstallChartStore } from "./install-chart-store/install-chart.store"; import type { IChartInstallData, InstallChartTabStore } from "./store";
import { Spinner } from "../spinner"; import { Spinner } from "../../spinner";
import { Icon } from "../icon"; import { Icon } from "../../icon";
import { Button } from "../button"; import { Button } from "../../button";
import { LogsDialog } from "../dialog/logs-dialog"; import { LogsDialog } from "../../dialog/logs-dialog";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../../select";
import { Input } from "../input"; import { Input } from "../../input";
import { EditorPanel } from "./editor-panel"; import { EditorPanel } from "../editor-panel";
import { navigate } from "../../navigation"; import { navigate } from "../../../navigation";
import { releaseURL } from "../../../common/routes"; import { releaseURL } from "../../../../common/routes";
import type { import type { IReleaseCreatePayload, IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
IReleaseCreatePayload,
IReleaseUpdateDetails,
} from "../../../common/k8s-api/endpoints/helm-releases.api";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import installChartStoreInjectable from "./install-chart-store/install-chart-store.injectable"; import installChartTabStoreInjectable from "./store.injectable";
import dockStoreInjectable from "./dock-store/dock-store.injectable"; import dockStoreInjectable from "../dock/store.injectable";
import createReleaseInjectable from "../+apps-releases/create-release/create-release.injectable"; import createReleaseInjectable from "../../+apps-releases/create-release/create-release.injectable";
import { Notifications } from "../../notifications";
interface Props { interface Props {
tab: DockTab; tab: DockTab;
@ -38,7 +36,7 @@ interface Props {
interface Dependencies { interface Dependencies {
createRelease: (payload: IReleaseCreatePayload) => Promise<IReleaseUpdateDetails> createRelease: (payload: IReleaseCreatePayload) => Promise<IReleaseUpdateDetails>
installChartStore: InstallChartStore installChartStore: InstallChartTabStore
dockStore: DockStore dockStore: DockStore
} }
@ -52,6 +50,11 @@ class NonInjectedInstallChart extends Component<Props & Dependencies> {
makeObservable(this); makeObservable(this);
} }
componentDidMount(): void {
this.props.installChartStore.loadData(this.tabId)
.catch(err => Notifications.error(String(err)));
}
get chartData() { get chartData() {
return this.props.installChartStore.getData(this.tabId); return this.props.installChartStore.getData(this.tabId);
} }
@ -221,7 +224,7 @@ export const InstallChart = withInjectables<Dependencies, Props>(
{ {
getProps: (di, props) => ({ getProps: (di, props) => ({
createRelease: di.inject(createReleaseInjectable), createRelease: di.inject(createReleaseInjectable),
installChartStore: di.inject(installChartStoreInjectable), installChartStore: di.inject(installChartTabStoreInjectable),
dockStore: di.inject(dockStoreInjectable), dockStore: di.inject(dockStoreInjectable),
...props, ...props,
}), }),

View File

@ -8,7 +8,7 @@ import "@testing-library/jest-dom/extend-expect";
import * as selectEvent from "react-select-event"; import * as selectEvent from "react-select-event";
import { Pod } from "../../../../../common/k8s-api/endpoints"; import { Pod } from "../../../../../common/k8s-api/endpoints";
import { LogResourceSelector } from "../resource-selector"; import { LogResourceSelector } from "../resource-selector";
import { dockerPod, deploymentPod1 } from "./pod.mock"; import { dockerPod, deploymentPod1, deploymentPod2 } from "./pod.mock";
import { ThemeStore } from "../../../../theme.store"; import { ThemeStore } from "../../../../theme.store";
import { UserStore } from "../../../../../common/user-store"; import { UserStore } from "../../../../../common/user-store";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
@ -18,7 +18,9 @@ import { renderFor } from "../../../test-utils/renderFor";
import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import callForLogsInjectable from "../call-for-logs.injectable"; import callForLogsInjectable from "../call-for-logs.injectable";
import { LogTabViewModel, LogTabViewModelDependencies } from "../logs-view-model"; import { LogTabViewModel, LogTabViewModelDependencies } from "../logs-view-model";
import type { TabId } from "../../dock-store/dock.store"; import type { TabId } from "../../dock/store";
import userEvent from "@testing-library/user-event";
import { SearchStore } from "../../../../search-store/search-store";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: { app: {
@ -36,10 +38,6 @@ jest.mock("electron", () => ({
}, },
})); }));
const getComponent = (model: LogTabViewModel) => (
<LogResourceSelector model={model} />
);
function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependencies>): LogTabViewModel { function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependencies>): LogTabViewModel {
return new LogTabViewModel(tabId, { return new LogTabViewModel(tabId, {
getLogs: jest.fn(), getLogs: jest.fn(),
@ -49,34 +47,74 @@ function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependen
setLogTabData: jest.fn(), setLogTabData: jest.fn(),
loadLogs: jest.fn(), loadLogs: jest.fn(),
reloadLogs: jest.fn(), reloadLogs: jest.fn(),
updateTabName: jest.fn(), renameTab: jest.fn(),
stopLoadingLogs: jest.fn(), stopLoadingLogs: jest.fn(),
getPodById: jest.fn(),
getPodsByOwnerId: jest.fn(),
searchStore: new SearchStore(),
areLogsPresent: jest.fn(),
...deps, ...deps,
}); });
} }
const getOnePodViewModel = (tabId: TabId): LogTabViewModel => { const getOnePodViewModel = (tabId: TabId, deps: Partial<LogTabViewModelDependencies> = {}): LogTabViewModel => {
const selectedPod = new Pod(dockerPod); const selectedPod = new Pod(dockerPod);
return mockLogTabViewModel(tabId, { return mockLogTabViewModel(tabId, {
getLogTabData: () => ({ getLogTabData: () => ({
pods: [selectedPod], selectedPodId: selectedPod.getId(),
selectedPod, selectedContainer: selectedPod.getContainers()[0].name,
selectedContainer: selectedPod.getContainers()[0], namespace: selectedPod.getNs(),
showPrevious: false,
showTimestamps: false,
}), }),
getPodById: (id) => {
if (id === selectedPod.getId()) {
return selectedPod;
}
return undefined;
},
...deps,
}); });
}; };
const getFewPodsTabData = (tabId: TabId): LogTabViewModel => { const getFewPodsTabData = (tabId: TabId, deps: Partial<LogTabViewModelDependencies> = {}): LogTabViewModel => {
const selectedPod = new Pod(deploymentPod1); const selectedPod = new Pod(deploymentPod1);
const anotherPod = new Pod(dockerPod); const anotherPod = new Pod(deploymentPod2);
return mockLogTabViewModel(tabId, { return mockLogTabViewModel(tabId, {
getLogTabData: () => ({ getLogTabData: () => ({
pods: [selectedPod, anotherPod], owner: {
selectedPod, uid: "uuid",
selectedContainer: selectedPod.getContainers()[0], kind: "Deployment",
name: "super-deployment",
},
selectedPodId: selectedPod.getId(),
selectedContainer: selectedPod.getContainers()[0].name,
namespace: selectedPod.getNs(),
showPrevious: false,
showTimestamps: false,
}), }),
getPodById: (id) => {
if (id === selectedPod.getId()) {
return selectedPod;
}
if (id === anotherPod.getId()) {
return anotherPod;
}
return undefined;
},
getPodsByOwnerId: (id) => {
if (id === "uuid") {
return [selectedPod, anotherPod];
}
return [];
},
...deps,
}); });
}; };
@ -109,14 +147,14 @@ describe("<LogResourceSelector />", () => {
it("renders w/o errors", () => { it("renders w/o errors", () => {
const model = getOnePodViewModel("foobar"); const model = getOnePodViewModel("foobar");
const { container } = render(getComponent(model)); const { container } = render(<LogResourceSelector model={model} />);
expect(container).toBeInstanceOf(HTMLElement); expect(container).toBeInstanceOf(HTMLElement);
}); });
it("renders proper namespace", async () => { it("renders proper namespace", async () => {
const model = getOnePodViewModel("foobar"); const model = getOnePodViewModel("foobar");
const { findByTestId } = render(getComponent(model)); const { findByTestId } = render(<LogResourceSelector model={model} />);
const ns = await findByTestId("namespace-badge"); const ns = await findByTestId("namespace-badge");
expect(ns).toHaveTextContent("default"); expect(ns).toHaveTextContent("default");
@ -124,7 +162,7 @@ describe("<LogResourceSelector />", () => {
it("renders proper selected items within dropdowns", async () => { it("renders proper selected items within dropdowns", async () => {
const model = getOnePodViewModel("foobar"); const model = getOnePodViewModel("foobar");
const { findByText } = render(getComponent(model)); const { findByText } = render(<LogResourceSelector model={model} />);
expect(await findByText("dockerExporter")).toBeInTheDocument(); expect(await findByText("dockerExporter")).toBeInTheDocument();
expect(await findByText("docker-exporter")).toBeInTheDocument(); expect(await findByText("docker-exporter")).toBeInTheDocument();
@ -132,33 +170,40 @@ describe("<LogResourceSelector />", () => {
it("renders sibling pods in dropdown", async () => { it("renders sibling pods in dropdown", async () => {
const model = getFewPodsTabData("foobar"); const model = getFewPodsTabData("foobar");
const { container, findByText } = render(getComponent(model)); const { container, findByText } = render(<LogResourceSelector model={model} />);
selectEvent.openMenu(container.querySelector(".pod-selector")); selectEvent.openMenu(container.querySelector(".pod-selector"));
expect(await findByText("deploymentPod2", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument();
expect(await findByText("dockerExporter", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument();
expect(await findByText("deploymentPod1", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument(); expect(await findByText("deploymentPod1", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument();
}); });
it("renders sibling containers in dropdown", async () => { it("renders sibling containers in dropdown", async () => {
const model = getFewPodsTabData("foobar"); const model = getFewPodsTabData("foobar");
const { findByText, container } = render(getComponent(model)); const { findByText, container } = render(<LogResourceSelector model={model} />);
const containerSelector: HTMLElement = container.querySelector(".container-selector");
selectEvent.openMenu(containerSelector);
selectEvent.openMenu(container.querySelector(".container-selector"));
expect(await findByText("node-exporter-1")).toBeInTheDocument(); expect(await findByText("node-exporter-1")).toBeInTheDocument();
expect(await findByText("init-node-exporter")).toBeInTheDocument(); expect(await findByText("init-node-exporter")).toBeInTheDocument();
expect(await findByText("init-node-exporter-1")).toBeInTheDocument(); expect(await findByText("init-node-exporter-1")).toBeInTheDocument();
}); });
it("renders pod owner as dropdown title", async () => { it("renders pod owner as badge", async () => {
const model = getFewPodsTabData("foobar"); const model = getFewPodsTabData("foobar");
const { findByText, container } = render(getComponent(model)); const { findByText } = render(<LogResourceSelector model={model} />);
const podSelector: HTMLElement = container.querySelector(".pod-selector");
selectEvent.openMenu(podSelector); expect(await findByText("super-deployment", {
exact: false,
})).toBeInTheDocument();
});
expect(await findByText("super-deployment")).toBeInTheDocument(); it("updates tab name if selected pod changes", async () => {
const renameTab = jest.fn();
const model = getFewPodsTabData("foobar", { renameTab });
const { findByText, container } = render(<LogResourceSelector model={model} />);
selectEvent.openMenu(container.querySelector(".pod-selector"));
userEvent.click(await findByText("deploymentPod2", { selector: ".pod-selector-menu .Select__option" }));
expect(renameTab).toBeCalledWith("foobar", "Pod deploymentPod2");
}); });
}); });

View File

@ -0,0 +1,147 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { screen } from "@testing-library/react";
import { Pod } from "../../../../../common/k8s-api/endpoints";
import { dockerPod } from "./pod.mock";
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
import type { DiRender } from "../../../test-utils/renderFor";
import { renderFor } from "../../../test-utils/renderFor";
import { LogTabViewModel, LogTabViewModelDependencies } from "../logs-view-model";
import type { TabId } from "../../dock/store";
import { LogSearch } from "../search";
import userEvent from "@testing-library/user-event";
import { SearchStore } from "../../../../search-store/search-store";
function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependencies>): LogTabViewModel {
return new LogTabViewModel(tabId, {
getLogs: jest.fn(),
getLogsWithoutTimestamps: jest.fn(),
getTimestampSplitLogs: jest.fn(),
getLogTabData: jest.fn(),
setLogTabData: jest.fn(),
loadLogs: jest.fn(),
reloadLogs: jest.fn(),
renameTab: jest.fn(),
stopLoadingLogs: jest.fn(),
getPodById: jest.fn(),
getPodsByOwnerId: jest.fn(),
areLogsPresent: jest.fn(),
searchStore: new SearchStore(),
...deps,
});
}
const getOnePodViewModel = (tabId: TabId, deps: Partial<LogTabViewModelDependencies> = {}): LogTabViewModel => {
const selectedPod = new Pod(dockerPod);
return mockLogTabViewModel(tabId, {
getLogTabData: () => ({
selectedPodId: selectedPod.getId(),
selectedContainer: selectedPod.getContainers()[0].name,
namespace: selectedPod.getNs(),
showPrevious: false,
showTimestamps: false,
}),
getPodById: (id) => {
if (id === selectedPod.getId()) {
return selectedPod;
}
return undefined;
},
...deps,
});
};
describe("LogSearch tests", () => {
let render: DiRender;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
await di.runSetups();
});
it("renders w/o errors", () => {
const model = getOnePodViewModel("foobar");
const { container } = render(
<LogSearch
model={model}
scrollToOverlay={jest.fn()}
/>,
);
expect(container).toBeInstanceOf(HTMLElement);
});
it("should scroll to new active overlay when clicking the previous button", async () => {
const scrollToOverlay = jest.fn();
const model = getOnePodViewModel("foobar", {
getLogsWithoutTimestamps: () => [
"hello",
"world",
],
});
render(
<LogSearch
model={model}
scrollToOverlay={scrollToOverlay}
/>,
);
userEvent.click(await screen.findByPlaceholderText("Search..."));
userEvent.keyboard("o");
userEvent.click(await screen.findByText("keyboard_arrow_up"));
expect(scrollToOverlay).toBeCalled();
});
it("should scroll to new active overlay when clicking the next button", async () => {
const scrollToOverlay = jest.fn();
const model = getOnePodViewModel("foobar", {
getLogsWithoutTimestamps: () => [
"hello",
"world",
],
});
render(
<LogSearch
model={model}
scrollToOverlay={scrollToOverlay}
/>,
);
userEvent.click(await screen.findByPlaceholderText("Search..."));
userEvent.keyboard("o");
userEvent.click(await screen.findByText("keyboard_arrow_down"));
expect(scrollToOverlay).toBeCalled();
});
it("next and previous should be disabled initially", async () => {
const scrollToOverlay = jest.fn();
const model = getOnePodViewModel("foobar", {
getLogsWithoutTimestamps: () => [
"hello",
"world",
],
});
render(
<LogSearch
model={model}
scrollToOverlay={scrollToOverlay}
/>,
);
userEvent.click(await screen.findByText("keyboard_arrow_down"));
userEvent.click(await screen.findByText("keyboard_arrow_up"));
expect(scrollToOverlay).not.toBeCalled();
});
});

View File

@ -1,146 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { podsStore } from "../../../+workloads-pods/pods.store";
import { UserStore } from "../../../../../common/user-store";
import { Pod } from "../../../../../common/k8s-api/endpoints";
import { ThemeStore } from "../../../../theme.store";
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
import { mockWindow } from "../../../../../../__mocks__/windowMock";
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
import logTabStoreInjectable from "../tab-store.injectable";
import type { LogTabStore } from "../tab.store";
import dockStoreInjectable from "../../dock-store/dock-store.injectable";
import type { DockStore } from "../../dock-store/dock.store";
import directoryForUserDataInjectable
from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import mockFs from "mock-fs";
mockWindow();
podsStore.items.push(new Pod(dockerPod));
podsStore.items.push(new Pod(deploymentPod1));
podsStore.items.push(new Pod(deploymentPod2));
describe("log tab store", () => {
let logTabStore: LogTabStore;
let dockStore: DockStore;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs();
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
await di.runSetups();
dockStore = di.inject(dockStoreInjectable);
logTabStore = di.inject(logTabStoreInjectable);
UserStore.createInstance();
ThemeStore.createInstance();
});
afterEach(() => {
UserStore.resetInstance();
ThemeStore.resetInstance();
mockFs.restore();
});
it("creates log tab without sibling pods", () => {
const selectedPod = new Pod(dockerPod);
const selectedContainer = selectedPod.getAllContainers()[0];
logTabStore.createPodTab({
selectedPod,
selectedContainer,
});
expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
pods: [selectedPod],
selectedPod,
selectedContainer,
showTimestamps: false,
previous: false,
});
});
it("creates log tab with sibling pods", () => {
const selectedPod = new Pod(deploymentPod1);
const siblingPod = new Pod(deploymentPod2);
const selectedContainer = selectedPod.getInitContainers()[0];
logTabStore.createPodTab({
selectedPod,
selectedContainer,
});
expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
pods: [selectedPod, siblingPod],
selectedPod,
selectedContainer,
showTimestamps: false,
previous: false,
});
});
it("removes item from pods list if pod deleted from store", () => {
const selectedPod = new Pod(deploymentPod1);
const selectedContainer = selectedPod.getInitContainers()[0];
logTabStore.createPodTab({
selectedPod,
selectedContainer,
});
podsStore.items.pop();
expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
pods: [selectedPod],
selectedPod,
selectedContainer,
showTimestamps: false,
previous: false,
});
});
it("adds item into pods list if new sibling pod added to store", () => {
const selectedPod = new Pod(deploymentPod1);
const selectedContainer = selectedPod.getInitContainers()[0];
logTabStore.createPodTab({
selectedPod,
selectedContainer,
});
podsStore.items.push(new Pod(deploymentPod3));
expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
pods: [selectedPod, deploymentPod3],
selectedPod,
selectedContainer,
showTimestamps: false,
previous: false,
});
});
// FIXME: this is failed when it's not .only == depends on something above
it.only("closes tab if no pods left in store", async () => {
const selectedPod = new Pod(deploymentPod1);
const selectedContainer = selectedPod.getInitContainers()[0];
const id = logTabStore.createPodTab({
selectedPod,
selectedContainer,
});
podsStore.items.clear();
expect(logTabStore.getData(dockStore.selectedTabId)).toBeUndefined();
expect(logTabStore.getData(id)).toBeUndefined();
expect(dockStore.getTabById(id)).toBeUndefined();
});
});

View File

@ -0,0 +1,19 @@
/**
* 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 logStoreInjectable from "./store.injectable";
const areLogsPresentInjectable = getInjectable({
instantiate: (di) => {
const logStore = di.inject(logStoreInjectable);
return (tabId: TabId) => logStore.areLogsPresent(tabId);
},
lifecycle: lifecycleEnum.singleton,
});
export default areLogsPresentInjectable;

View File

@ -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 logTabStoreInjectable from "./tab-store.injectable";
const clearLogTabDataInjectable = getInjectable({
instantiate: (di) => {
const logTabStore = di.inject(logTabStoreInjectable);
return (tabId: TabId): void => {
logTabStore.clearData(tabId);
};
},
lifecycle: lifecycleEnum.singleton,
});
export default clearLogTabDataInjectable;

Some files were not shown because too many files have changed in this diff Show More