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

Merge remote-tracking branch 'origin/master' into webpack5-upgrade-attempt

# Conflicts:
#	package.json
#	yarn.lock
This commit is contained in:
Roman 2022-01-31 18:21:45 +02:00
commit c0559bf772
216 changed files with 6497 additions and 4395 deletions

View File

@ -11,17 +11,12 @@ module.exports = {
"**/dist/**/*",
"**/static/**/*",
"**/site/**/*",
"extensions/*/*.tgz",
],
settings: {
react: {
version: packageJson.devDependencies.react || "detect",
},
// the package eslint-import-resolver-typescript is required for this line which fixes errors when using .d.ts files
"import/resolver": {
"typescript": {
"alwaysTryTypes": true,
},
},
},
overrides: [
{
@ -95,95 +90,9 @@ module.exports = {
{
files: [
"**/*.ts",
],
parser: "@typescript-eslint/parser",
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
plugins: [
"header",
"unused-imports",
],
parserOptions: {
ecmaVersion: 2018,
sourceType: "module",
},
rules: {
"no-constant-condition": ["error", { "checkLoops": false }],
"header/header": [2, "./license-header"],
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-unused-vars": "off",
"space-before-function-paren": "off",
"@typescript-eslint/space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",
"asyncArrow": "always",
}],
"unused-imports/no-unused-imports-ts": process.env.PROD === "true" ? "error" : "warn",
"unused-imports/no-unused-vars-ts": [
"warn", {
"vars": "all",
"args": "after-used",
"ignoreRestSiblings": true,
},
],
"comman-dangle": "off",
"@typescript-eslint/comma-dangle": ["error", "always-multiline"],
"comma-spacing": "off",
"@typescript-eslint/comma-spacing": "error",
"indent": ["error", 2, {
"SwitchCase": 1,
}],
"quotes": ["error", "double", {
"avoidEscape": true,
"allowTemplateLiterals": true,
}],
"object-curly-spacing": "off",
"@typescript-eslint/object-curly-spacing": ["error", "always", {
"objectsInObjects": false,
"arraysInObjects": true,
}],
"react/prop-types": "off",
"semi": "off",
"@typescript-eslint/semi": ["error"],
"linebreak-style": ["error", "unix"],
"eol-last": ["error", "always"],
"object-shorthand": "error",
"prefer-template": "error",
"template-curly-spacing": "error",
"no-unused-expressions": "off",
"@typescript-eslint/no-unused-expressions": "error",
"padding-line-between-statements": [
"error",
{ "blankLine": "always", "prev": "*", "next": "return" },
{ "blankLine": "always", "prev": "*", "next": "block-like" },
{ "blankLine": "always", "prev": "*", "next": "function" },
{ "blankLine": "always", "prev": "*", "next": "class" },
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] },
],
"no-template-curly-in-string": "error",
},
},
{
files: [
"**/*.tsx",
],
parser: "@typescript-eslint/parser",
plugins: [
"header",
"unused-imports",
],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
@ -191,13 +100,19 @@ module.exports = {
"plugin:import/recommended",
"plugin:import/typescript",
],
plugins: [
"header",
"unused-imports",
"react-hooks",
],
parserOptions: {
ecmaVersion: 2018,
sourceType: "module",
jsx: true,
},
rules: {
"no-constant-condition": ["error", { "checkLoops": false }],
"no-constant-condition": ["error", {
"checkLoops": false,
}],
"header/header": [2, "./license-header"],
"react/prop-types": "off",
"no-invalid-this": "off",
@ -211,9 +126,10 @@ module.exports = {
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off",
"react/display-name": "off",
"@typescript-eslint/no-unused-vars": "off",
"react/display-name": "off",
"space-before-function-paren": "off",
"@typescript-eslint/space-before-function-paren": ["error", {
"anonymous": "always",

View File

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

View File

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

View File

@ -320,8 +320,8 @@
"@types/webpack": "^5.28.0",
"@types/webpack-env": "^1.16.3",
"@types/webpack-node-externals": "^2.5.3",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.7.0",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"ansi_up": "^5.1.0",
"chart.js": "^2.9.4",
"circular-dependency-plugin": "^5.2.2",
@ -335,17 +335,17 @@
"electron-notarize": "^0.3.0",
"esbuild": "^0.13.15",
"esbuild-loader": "^2.18.0",
"eslint": "^7.32.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint": "^8.7.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-react": "^7.27.1",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unused-imports": "^1.1.5",
"eslint-plugin-unused-imports": "^2.0.0",
"flex.box": "^3.4.4",
"fork-ts-checker-webpack-plugin": "^6.5.0",
"hoist-non-react-statics": "^3.3.2",
"html-webpack-plugin": "^5.5.0",
"ignore-loader": "^0.1.2",
"include-media": "^1.4.9",
"jest": "26.6.3",
"jest-canvas-mock": "^2.3.1",

View File

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

View File

@ -2,12 +2,12 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* 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 fsInjectable from "./fs.injectable";
const kubeObjectMenuRegistryInjectable = getInjectable({
instantiate: () => KubeObjectMenuRegistry.getInstance(),
const readFileInjectable = getInjectable({
instantiate: (di) => di.inject(fsInjectable).readFile,
lifecycle: lifecycleEnum.singleton,
});
export default kubeObjectMenuRegistryInjectable;
export default readFileInjectable;

View File

@ -2,15 +2,11 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* 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 fsInjectable from "../fs.injectable";
import fsInjectable from "./fs.injectable";
const readJsonFileInjectable = getInjectable({
instantiate: (di) => readJsonFile({
fs: di.inject(fsInjectable),
}),
instantiate: (di) => di.inject(fsInjectable).readJson,
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

@ -111,7 +111,7 @@ export abstract class ItemStore<Item extends ItemObject> {
}
}
protected async loadItem(...args: any[]): Promise<Item>
protected async loadItem(...args: any[]): Promise<Item>;
@action
protected async loadItem(request: () => Promise<Item>, sortItems = true) {
const item = await Promise.resolve(request()).catch(() => null);

View File

@ -6,7 +6,7 @@
import moment from "moment-timezone";
import path from "path";
import os from "os";
import { getAppVersion, ObservableToggleSet } from "../utils";
import { getAppVersion } from "../utils";
import type { editor } from "monaco-editor";
import merge from "lodash/merge";
import { SemVer } from "semver";
@ -236,10 +236,10 @@ const terminalCopyOnSelect: PreferenceDescription<boolean> = {
},
};
const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string, ObservableToggleSet<string>>> = {
const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string, Set<string>>> = {
fromStore(val) {
return new Map(
(val ?? []).map(([tableId, columnIds]) => [tableId, new ObservableToggleSet(columnIds)]),
(val ?? []).map(([tableId, columnIds]) => [tableId, new Set(columnIds)]),
);
},
toStore(val) {

View File

@ -11,7 +11,7 @@ import migrations, { fileNameMigration } from "../../migrations/user-store";
import { getAppVersion } from "../utils/app-version";
import { kubeConfigDefaultPath } from "../kube-helpers";
import { appEventBus } from "../app-event-bus/event-bus";
import { ObservableToggleSet, toJS } from "../../renderer/utils";
import { getOrInsertSet, toggle, toJS } from "../../renderer/utils";
import { DESCRIPTORS, EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel, TerminalConfig } from "./preferences-helpers";
import logger from "../../main/logger";
@ -71,7 +71,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
* The column IDs under each configurable table ID that have been configured
* to not be shown
*/
hiddenTableColumns = observable.map<string, ObservableToggleSet<string>>();
hiddenTableColumns = observable.map<string, Set<string>>();
/**
* Monaco editor configs
@ -133,16 +133,11 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
return columnIds.some(columnId => config.has(columnId));
}
@action
/**
* Toggles the hidden configuration of a table's column
*/
toggleTableColumnVisibility(tableId: string, columnId: string) {
if (!this.hiddenTableColumns.get(tableId)) {
this.hiddenTableColumns.set(tableId, new ObservableToggleSet());
}
this.hiddenTableColumns.get(tableId).toggle(columnId);
toggle(getOrInsertSet(this.hiddenTableColumns, tableId), columnId);
}
@action

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

@ -3,6 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { runInAction } from "mobx";
/**
* Get the value behind `key`. If it was not present, first insert `value`
* @param map The map to interact with
@ -26,6 +28,14 @@ export function getOrInsertMap<K, MK, MV>(map: Map<K, Map<MK, MV>>, key: K): Map
return getOrInsert(map, key, new Map<MK, MV>());
}
/**
* Like `getOrInsert` but specifically for when `V` is `Set<any>` so that
* the typings are inferred.
*/
export function getOrInsertSet<K, SK>(map: Map<K, Set<SK>>, key: K): Set<SK> {
return getOrInsert(map, key, new Set<SK>());
}
/**
* Like `getOrInsert` but with delayed creation of the item
*/
@ -36,3 +46,17 @@ export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, value: () => V): V
return map.get(key);
}
/**
* If `key` is in `set`, remove it otherwise add it.
* @param set The set to manipulate
* @param key The key to toggle the "is in"-ness of
*/
export function toggle<K>(set: Set<K>, key: K): void {
runInAction(() => {
// Returns true if value was already in Set; otherwise false.
if (!set.delete(key)) {
set.add(key);
}
});
}

View File

@ -3,12 +3,14 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, ObservableMap } from "mobx";
import { action, ObservableMap, runInAction } from "mobx";
export function multiSet<T, V>(map: Map<T, V>, newEntries: [T, V][]): void {
for (const [key, val] of newEntries) {
map.set(key, val);
}
runInAction(() => {
for (const [key, val] of newEntries) {
map.set(key, val);
}
});
}
export class ExtendedMap<K, V> extends Map<K, V> {

View File

@ -10,13 +10,6 @@ export function noop<T extends any[]>(...args: T): void {
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 "./autobind";
export * from "./camelCase";
@ -45,10 +38,10 @@ export * from "./singleton";
export * from "./sort-compare";
export * from "./splitArray";
export * from "./tar";
export * from "./toggle-set";
export * from "./toJS";
export * from "./type-narrowing";
export * from "./types";
export * from "./wait-for-path";
import * as iter from "./iter";
import * as array from "./array";

View File

@ -1,24 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { ObservableSet } from "mobx";
export class ToggleSet<T> extends Set<T> {
public toggle(value: T): void {
if (!this.delete(value)) {
// Set.prototype.delete returns false if `value` was not in the set
this.add(value);
}
}
}
export class ObservableToggleSet<T> extends ObservableSet<T> {
public toggle(value: T): void {
if (!this.delete(value)) {
// Set.prototype.delete returns false if `value` was not in the set
this.add(value);
}
}
}

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.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { writeJsonFile } from "./write-json-file";
import fsInjectable from "../fs.injectable";
import { isLinux } from "../vars";
const writeJsonFileInjectable = getInjectable({
instantiate: (di) => writeJsonFile({ fs: di.inject(fsInjectable) }),
const isLinuxInjectable = getInjectable({
instantiate: () => isLinux,
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

@ -2,13 +2,12 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* 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 { 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 { 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 { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry";
export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry";
export type { StatusBarRegistration } from "../registries/status-bar-registry";
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler";
export type { CustomCategoryViewProps, CustomCategoryViewComponents, CustomCategoryViewRegistration } from "../../renderer/components/+catalog/custom-views";

View File

@ -249,7 +249,6 @@ export class ExtensionLoader {
const removeItems = [
registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension),
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
];
@ -277,7 +276,6 @@ export class ExtensionLoader {
const removeItems = [
registries.ClusterPageRegistry.getInstance().add(extension.clusterPages, extension),
registries.ClusterPageMenuRegistry.getInstance().add(extension.clusterPageMenus, extension),
registries.KubeObjectMenuRegistry.getInstance().add(extension.kubeObjectMenuItems),
registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems),
registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts),
registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems),

View File

@ -42,10 +42,9 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
return isBundled || Boolean(this.state.get(id)?.enabled);
}
@action
mergeState = (extensionsState: Record<LensExtensionId, LensExtensionState>) => {
mergeState = action((extensionsState: Record<LensExtensionId, LensExtensionState>) => {
this.state.merge(extensionsState);
};
});
@action
protected fromStore({ extensions }: LensExtensionsStoreModel) {

View File

@ -18,6 +18,8 @@ import type { CommandRegistration } from "../renderer/components/command-palette
import type { AppPreferenceRegistration } from "../renderer/components/+preferences/app-preferences/app-preference-registration";
import type { AdditionalCategoryColumnRegistration } from "../renderer/components/+catalog/custom-category-columns";
import type { CustomCategoryViewRegistration } from "../renderer/components/+catalog/custom-views";
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 {
globalPages: registries.PageRegistration[] = [];
@ -26,9 +28,9 @@ export class LensRendererExtension extends LensExtension {
kubeObjectStatusTexts: registries.KubeObjectStatusRegistration[] = [];
appPreferences: AppPreferenceRegistration[] = [];
entitySettings: registries.EntitySettingRegistration[] = [];
statusBarItems: registries.StatusBarRegistration[] = [];
statusBarItems: StatusBarRegistration[] = [];
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
commands: CommandRegistration[] = [];
welcomeMenus: WelcomeMenuRegistration[] = [];

View File

@ -7,9 +7,7 @@
export * from "./page-registry";
export * from "./page-menu-registry";
export * from "./status-bar-registry";
export * from "./kube-object-detail-registry";
export * from "./kube-object-menu-registry";
export * from "./kube-object-status-registry";
export * from "./entity-setting-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.
*/
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 terminalStoreInjectable from "../../renderer/components/dock/terminal-store/terminal-store.injectable";
import createTerminalTabInjectable from "../../renderer/components/dock/terminal/create-terminal-tab.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 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 { 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
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 const createTerminalTab = asLegacyGlobalFunctionForExtensionApi(createTerminalTabInjectable);
export const TerminalStore = asLegacyGlobalSingletonForExtensionApi(TerminalStoreClass, terminalStoreInjectable);
export const terminalStore = asLegacyGlobalObjectForExtensionApi(terminalStoreInjectable);
export const logTabStore = asLegacyGlobalObjectForExtensionApi(logTabStoreInjectable);
export const terminalStore = asLegacyGlobalObjectForExtensionApiWithModifications(terminalStoreInjectable, {
sendCommand: () => asLegacyGlobalFunctionForExtensionApi(sendCommandInjectable),
});
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 appNameInjectable from "./app-paths/app-name/app-name.injectable";
import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable";
import writeJsonFileInjectable from "../common/fs/write-json-file/write-json-file.injectable";
import readJsonFileInjectable from "../common/fs/read-json-file/read-json-file.injectable";
import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
export const getDiForUnitTesting = (
{ doGeneralOverrides } = { doGeneralOverrides: false },

View File

@ -74,7 +74,7 @@ export class WindowManager extends Singleton {
nodeIntegrationInSubFrames: true,
webviewTag: true,
contextIsolation: false,
nativeWindowOpen: true,
nativeWindowOpen: false,
},
});
this.windowState.manage(this.mainWindow);

View File

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

View File

@ -70,8 +70,7 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
].filter(Boolean);
}
@action
refreshContexts = debounce(() => {
readonly refreshContexts = debounce(action(() => {
const { config, error } = loadConfigFromString(this.customConfig.trim() || "{}");
this.kubeContexts.replace(getContexts(config));
@ -83,10 +82,9 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
if (config.contexts.length === 0) {
this.errors.push('No contexts defined, either missing the "contexts" field, or it is empty.');
}
}, 500);
}), 500);
@action
addClusters = async () => {
addClusters = action(async () => {
this.isWaiting = true;
appEventBus.emit({ name: "cluster-add", action: "click" });
@ -102,7 +100,7 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
} catch (error) {
Notifications.error(`Failed to add clusters: ${error}`);
}
};
});
render() {
return (

View File

@ -18,8 +18,7 @@ import { Select, SelectOption } from "../select";
import { Badge } from "../badge";
import { Tooltip, withStyles } from "@material-ui/core";
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 {
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 { HelmChartDetails } from "./helm-chart-details";
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 type { HelmChartsRouteParams } from "../../../common/routes";

View File

@ -27,7 +27,7 @@ import { getDetailsUrl } from "../../kube-detail-params";
import { Checkbox } from "../../checkbox";
import { MonacoEditor } from "../../monaco-editor";
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 releaseInjectable from "./release.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 { Icon } from "../icon";
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 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 removableReleasesInjectable from "./removable-releases.injectable";
import type { RemovableHelmRelease } from "./removable-releases";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import releasesInjectable from "./releases.injectable";
import { Spinner } from "../spinner";
enum columnId {
name = "name",
@ -48,7 +46,6 @@ interface Dependencies {
selectNamespace: (namespace: string) => void
}
@observer
class NonInjectedHelmReleases extends Component<Dependencies & Props> {
componentDidMount() {
const { match: { params: { namespace }}} = this.props;
@ -89,12 +86,8 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
}
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 releasesArePending = this.props.releasesArePending;
// TODO: Implement ItemListLayout without stateful stores
const legacyReleaseStore = {
@ -103,7 +96,11 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
},
loadAll: () => Promise.resolve(),
isLoaded: true,
get isLoaded() {
return !releasesArePending.get();
},
failedLoading: false,
getTotalCount: () => releases.get().length,

View File

@ -57,8 +57,7 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
}
}
@action
updateCategoryItems = (category: CatalogCategory) => {
updateCategoryItems = action((category: CatalogCategory) => {
if (category instanceof EventEmitter) {
const menuItems: CatalogEntityAddMenu[] = [];
const context: CatalogEntityAddMenuContext = {
@ -69,7 +68,7 @@ export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
category.emit("catalogAddMenu", context);
this.menuItems.set(category.getId(), menuItems);
}
};
});
getCategoryFilteredItems = (category: CatalogCategory) => {
return category.filteredItems(this.menuItems.get(category.getId()) || []);

View File

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

View File

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

View File

@ -143,8 +143,7 @@ class NonInjectedCatalog extends React.Component<Props & Dependencies> {
return catalogCategoryRegistry.items;
}
@action
onTabChange = (tabId: string | null) => {
onTabChange = action((tabId: string | null) => {
const activeCategory = this.categories.find(category => category.getId() === tabId);
if (activeCategory) {
@ -152,7 +151,7 @@ class NonInjectedCatalog extends React.Component<Props & Dependencies> {
} else {
navigate(catalogURL({ params: { group: browseCatalogTab }}));
}
};
});
renderNavigation() {
return (

View File

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

View File

@ -12,7 +12,7 @@ import { DrawerItem, DrawerTitle } from "../drawer";
import { Input } from "../input";
import { Button } from "../button";
import { Notifications } from "../notifications";
import { base64, ObservableToggleSet } from "../../utils";
import { base64, toggle } from "../../utils";
import { Icon } from "../icon";
import { secretsStore } from "./secrets.store";
import type { KubeObjectDetailsProps } from "../kube-object-details";
@ -27,7 +27,7 @@ interface Props extends KubeObjectDetailsProps<Secret> {
export class SecretDetails extends React.Component<Props> {
@observable isSaving = false;
@observable data: { [name: string]: string } = {};
revealSecret = new ObservableToggleSet<string>();
revealSecret = new Set<string>();
constructor(props: Props) {
super(props);
@ -99,7 +99,7 @@ export class SecretDetails extends React.Component<Props> {
<Icon
material={revealSecret ? "visibility" : "visibility_off"}
tooltip={revealSecret ? "Hide" : "Show"}
onClick={() => this.revealSecret.toggle(name)}
onClick={() => toggle(this.revealSecret, name)}
/>
)}
</div>

View File

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

View File

@ -4,7 +4,7 @@
*/
import { action, comparer, computed, IReactionDisposer, makeObservable, reaction } from "mobx";
import { autoBind, noop, StorageHelper, ToggleSet } from "../../../utils";
import { autoBind, noop, StorageHelper, toggle } from "../../../utils";
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../../../common/k8s-api/kube-object.store";
import { Namespace, namespacesApi } from "../../../../common/k8s-api/endpoints/namespaces.api";
@ -175,10 +175,10 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
*/
@action
toggleContext(namespaces: string | string[]) {
const nextState = new ToggleSet(this.contextNamespaces);
const nextState = new Set(this.contextNamespaces);
for (const namespace of [namespaces].flat()) {
nextState.toggle(namespace);
toggle(nextState, namespace);
}
this.dependencies.storage.set([...nextState]);
@ -191,9 +191,9 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
* @param namespace The name of a namespace
*/
toggleSingle(namespace: string) {
const nextState = new ToggleSet(this.contextNamespaces);
const nextState = new Set(this.contextNamespaces);
nextState.toggle(namespace);
toggle(nextState, namespace);
this.dependencies.storage.set([...nextState]);
}

View File

@ -8,7 +8,7 @@ import "./port-forwards.scss";
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
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 { PortForwardMenu } from "./port-forward-menu";
import { PortForwardsRouteParams, portForwardsURL } from "../../../common/routes";

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import fse from "fs-extra";
import { action, computed, makeObservable, observable, reaction } from "mobx";
import { computed, makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { Notice } from "../+extensions/notice";
@ -93,7 +93,6 @@ export class KubeconfigSyncs extends React.Component {
return Array.from(this.syncs.entries(), ([filePath, value]) => ({ filePath, ...value }));
}
@action
onPick = async (filePaths: string[]) => multiSet(this.syncs, await getAllEntries(filePaths));
getIconName(entry: Entry) {

View File

@ -115,8 +115,7 @@ export class ClusterRoleBindingDialog extends React.Component<Props> {
return this.serviceAccountOptions.filter(({ value }) => this.selectedAccounts.has(value));
}
@action
onOpen = () => {
onOpen = action(() => {
const binding = this.clusterRoleBinding;
if (!binding) {
@ -137,16 +136,15 @@ export class ClusterRoleBindingDialog extends React.Component<Props> {
);
this.selectedUsers.replace(uSubjects.map(user => user.name));
this.selectedGroups.replace(gSubjects.map(group => group.name));
};
});
@action
reset = () => {
reset = action(() => {
this.selectedRoleRef = undefined;
this.bindingName = "";
this.selectedAccounts.clear();
this.selectedUsers.clear();
this.selectedGroups.clear();
};
});
createBindings = async () => {
const { selectedRoleRef, selectedBindings, bindingName } = this;

View File

@ -116,8 +116,7 @@ export class RoleBindingDialog extends React.Component<Props> {
return this.serviceAccountOptions.filter(({ value }) => this.selectedAccounts.has(value));
}
@action
onOpen = () => {
onOpen = action(() => {
const binding = this.roleBinding;
if (!binding) {
@ -140,17 +139,16 @@ export class RoleBindingDialog extends React.Component<Props> {
);
this.selectedUsers.replace(uSubjects.map(user => user.name));
this.selectedGroups.replace(gSubjects.map(group => group.name));
};
});
@action
reset = () => {
reset = action(() => {
this.selectedRoleRef = undefined;
this.bindingName = "";
this.bindingNamespace = "";
this.selectedAccounts.clear();
this.selectedUsers.clear();
this.selectedGroups.clear();
};
});
createBindings = async () => {
const { selectedRoleRef, bindingNamespace: namespace, selectedBindings } = this;

View File

@ -28,27 +28,30 @@ export class ServiceAccountsDetails extends React.Component<Props> {
@observable secrets: Secret[];
@observable imagePullSecrets: Secret[];
@disposeOnUnmount
loadSecrets = autorun(async () => {
this.secrets = null;
this.imagePullSecrets = null;
const { object: serviceAccount } = this.props;
componentDidMount(): void {
disposeOnUnmount(this, [
autorun(async () => {
this.secrets = null;
this.imagePullSecrets = null;
const { object: serviceAccount } = this.props;
if (!serviceAccount) {
return;
}
const namespace = serviceAccount.getNs();
const secrets = serviceAccount.getSecrets().map(({ name }) => {
return secretsStore.load({ name, namespace });
});
if (!serviceAccount) {
return;
}
const namespace = serviceAccount.getNs();
const secrets = serviceAccount.getSecrets().map(({ name }) => {
return secretsStore.load({ name, namespace });
});
this.secrets = await Promise.all(secrets);
const imagePullSecrets = serviceAccount.getImagePullSecrets().map(async ({ name }) => {
return secretsStore.load({ name, namespace }).catch(() => this.generateDummySecretObject(name));
});
this.secrets = await Promise.all(secrets);
const imagePullSecrets = serviceAccount.getImagePullSecrets().map(async ({ name }) => {
return secretsStore.load({ name, namespace }).catch(() => this.generateDummySecretObject(name));
});
this.imagePullSecrets = await Promise.all(imagePullSecrets);
});
this.imagePullSecrets = await Promise.all(imagePullSecrets);
}),
]);
}
constructor(props: Props) {
super(props);

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 React from "react";
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 { 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 { serviceAccountsStore } from "./store";
import type { ServiceAccountsRouteParams } from "../../../../common/routes";
@ -58,9 +53,6 @@ export class ServiceAccounts extends React.Component<Props> {
account.getNs(),
account.getAge(),
]}
renderItemMenu={(item: ServiceAccount) => {
return <ServiceAccountMenu object={item}/>;
}}
addRemoveButtons={{
onAdd: () => CreateServiceAccountDialog.open(),
addTooltip: "Create new Service Account",
@ -72,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 { observer } from "mobx-react";
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 { jobStore } from "../+workloads-jobs/job.store";
import { eventStore } from "../+events/event.store";
import type { KubeObjectMenuProps } from "../kube-object-menu";
import { KubeObjectListLayout } from "../kube-object-list-layout";
import { CronJobTriggerDialog } from "./cronjob-trigger-dialog";
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 moment from "moment";
@ -82,61 +75,8 @@ export class CronJobs extends React.Component<Props> {
cronJob.getLastScheduleTime(),
cronJob.getAge(),
]}
renderItemMenu={(item: CronJob) => {
return <CronJobMenu object={item}/>;
}}
/>
);
}
}
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 { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router";
import { Deployment, deploymentApi } 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 type { Deployment } from "../../../common/k8s-api/endpoints";
import { deploymentStore } from "./deployments.store";
import { eventStore } from "../+events/event.store";
import { KubeObjectListLayout } from "../kube-object-list-layout";
@ -21,7 +16,6 @@ import { cssNames } from "../../utils";
import kebabCase from "lodash/kebabCase";
import orderBy from "lodash/orderBy";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { Notifications } from "../notifications";
import type { DeploymentsRouteParams } from "../../../common/routes";
enum columnId {
@ -91,43 +85,7 @@ export class Deployments extends React.Component<Props> {
deployment.getAge(),
this.renderConditions(deployment),
]}
renderItemMenu={item => <DeploymentMenu object={item} />}
/>
);
}
}
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

@ -20,19 +20,22 @@ interface Props {
export class PodDetailsSecrets extends Component<Props> {
@observable secrets: Map<string, Secret> = observable.map<string, Secret>();
@disposeOnUnmount
secretsLoader = autorun(async () => {
const { pod } = this.props;
componentDidMount(): void {
disposeOnUnmount(this, [
autorun(async () => {
const { pod } = this.props;
const secrets = await Promise.all(
pod.getSecrets().map(secretName => secretsApi.get({
name: secretName,
namespace: pod.getNs(),
})),
);
const secrets = await Promise.all(
pod.getSecrets().map(secretName => secretsApi.get({
name: secretName,
namespace: pod.getNs(),
})),
);
secrets.forEach(secret => secret && this.secrets.set(secret.getName(), secret));
});
secrets.forEach(secret => secret && this.secrets.set(secret.getName(), secret));
}),
]);
}
constructor(props: Props) {
super(props);

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 { 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 { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { RouteComponentProps } from "react-router";
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 { eventStore } from "../+events/event.store";
@ -70,23 +65,8 @@ export class ReplicaSets extends React.Component<Props> {
replicaSet.getReady(),
replicaSet.getAge(),
]}
renderItemMenu={(item: ReplicaSet) => {
return <ReplicaSetMenu object={item}/>;
}}
/>
);
}
}
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 { statefulSetStore } from "./statefulset.store";
import { eventStore } from "../+events/event.store";
import type { KubeObjectMenuProps } from "../kube-object-menu";
import { KubeObjectListLayout } from "../kube-object-list-layout";
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";
enum columnId {
@ -72,21 +68,7 @@ export class StatefulSets extends React.Component<Props> {
<KubeObjectStatusIcon key="icon" object={statefulSet}/>,
statefulSet.getAge(),
]}
renderItemMenu={item => <StatefulSetMenu object={item} />}
/>
);
}
}
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

@ -0,0 +1,28 @@
/**
* 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 rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
import type { StatusBarRegistration } from "./status-bar-registration";
const bottomBarItemsInjectable = getInjectable({
instantiate: (di) => {
const extensions = di.inject(rendererExtensionsInjectable);
return computed(() =>
extensions
.get()
.flatMap((extension) => extension.statusBarItems)
.sort(leftItemsBeforeRight),
);
},
lifecycle: lifecycleEnum.singleton,
});
export default bottomBarItemsInjectable;
const leftItemsBeforeRight = (firstItem: StatusBarRegistration, secondItem: StatusBarRegistration) =>
firstItem.components?.position?.localeCompare(secondItem.components?.position);

View File

@ -4,10 +4,15 @@
*/
import React from "react";
import { render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { BottomBar } from "./bottom-bar";
import { StatusBarRegistry } from "../../../extensions/registries";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import { computed, IObservableArray, observable, runInAction } from "mobx";
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
jest.mock("electron", () => ({
app: {
@ -15,13 +20,43 @@ jest.mock("electron", () => ({
},
}));
describe("<BottomBar />", () => {
beforeEach(() => {
StatusBarRegistry.createInstance();
});
class SomeTestExtension extends LensRendererExtension {
constructor(statusBarItems: IObservableArray<any>) {
super({
id: "some-id",
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: { name: "some-id", version: "some-version" },
manifestPath: "irrelevant",
});
afterEach(() => {
StatusBarRegistry.resetInstance();
this.statusBarItems = statusBarItems;
}
}
describe("<BottomBar />", () => {
let render: DiRender;
let statusBarItems: IObservableArray<any>;
beforeEach(async () => {
statusBarItems = observable.array([]);
const someTestExtension = new SomeTestExtension(statusBarItems);
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(rendererExtensionsInjectable, () => {
return computed(() => [someTestExtension]);
});
render = renderFor(di);
await di.runSetups();
});
it("renders w/o errors", () => {
@ -30,6 +65,7 @@ describe("<BottomBar />", () => {
expect(container).toBeInstanceOf(HTMLElement);
});
it.each([
undefined,
"hello",
@ -39,7 +75,10 @@ describe("<BottomBar />", () => {
[{}],
{},
])("renders w/o errors when .getItems() returns not type compliant (%p)", val => {
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => val);
runInAction(() => {
statusBarItems.replace([val]);
});
expect(() => render(<BottomBar />)).not.toThrow();
});
@ -47,9 +86,12 @@ describe("<BottomBar />", () => {
const testId = "testId";
const text = "heee";
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{ item: <span data-testid={testId} >{text}</span> },
]);
runInAction(() => {
statusBarItems.replace([
{ item: <span data-testid={testId} >{text}</span> },
]);
});
const { getByTestId } = render(<BottomBar />);
expect(getByTestId(testId)).toHaveTextContent(text);
@ -59,9 +101,12 @@ describe("<BottomBar />", () => {
const testId = "testId";
const text = "heee";
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{ item: () => <span data-testid={testId} >{text}</span> },
]);
runInAction(() => {
statusBarItems.replace([
{ item: () => <span data-testid={testId} >{text}</span> },
]);
});
const { getByTestId } = render(<BottomBar />);
expect(getByTestId(testId)).toHaveTextContent(text);
@ -69,31 +114,33 @@ describe("<BottomBar />", () => {
it("sort positioned items properly", () => {
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{
components: {
Item: () => <div data-testid="sortedElem">right</div>,
runInAction(() => {
statusBarItems.replace([
{
components: {
Item: () => <div data-testid="sortedElem">right</div>,
},
},
},
{
components: {
Item: () => <div data-testid="sortedElem">right</div>,
position: "right",
{
components: {
Item: () => <div data-testid="sortedElem">right</div>,
position: "right",
},
},
},
{
components: {
Item: () => <div data-testid="sortedElem">left</div>,
position: "left",
{
components: {
Item: () => <div data-testid="sortedElem">left</div>,
position: "left",
},
},
},
{
components: {
Item: () => <div data-testid="sortedElem">left</div>,
position: "left",
{
components: {
Item: () => <div data-testid="sortedElem">left</div>,
position: "left",
},
},
},
]);
]);
});
const { getAllByTestId } = render(<BottomBar />);
const elems = getAllByTestId("sortedElem");

View File

@ -7,11 +7,18 @@ import styles from "./bottom-bar.module.scss";
import React from "react";
import { observer } from "mobx-react";
import { StatusBarRegistration, StatusBarRegistry } from "../../../extensions/registries";
import { cssNames } from "../../utils";
import { withInjectables } from "@ogre-tools/injectable-react";
import bottomBarItemsInjectable from "./bottom-bar-items.injectable";
import type { IComputedValue } from "mobx";
import type { StatusBarRegistration } from "./status-bar-registration";
interface Dependencies {
items: IComputedValue<StatusBarRegistration[]>
}
@observer
export class BottomBar extends React.Component {
class NonInjectedBottomBar extends React.Component<Dependencies> {
renderRegisteredItem(registration: StatusBarRegistration) {
const { item } = registration;
@ -23,19 +30,9 @@ export class BottomBar extends React.Component {
}
renderRegisteredItems() {
const items = StatusBarRegistry.getInstance().getItems();
if (!Array.isArray(items)) {
return null;
}
items.sort(function sortLeftPositionFirst(a, b) {
return a.components?.position?.localeCompare(b.components?.position);
});
return (
<>
{items.map((registration, index) => {
{this.props.items.get().map((registration, index) => {
if (!registration?.item && !registration?.components?.Item) {
return null;
}
@ -64,3 +61,14 @@ export class BottomBar extends React.Component {
);
}
}
export const BottomBar = withInjectables<Dependencies>(
NonInjectedBottomBar,
{
getProps: (di, props) => ({
items: di.inject(bottomBarItemsInjectable),
...props,
}),
},
);

View File

@ -2,12 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Extensions API -> Status bar customizations
import type React from "react";
import { BaseRegistry } from "./base-registry";
interface StatusBarComponents {
Item?: React.ComponentType;
/**
@ -28,6 +22,3 @@ export interface StatusBarRegistration extends StatusBarRegistrationV2 {
*/
item?: React.ReactNode;
}
export class StatusBarRegistry extends BaseRegistry<StatusBarRegistration> {
}

View File

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

View File

@ -48,9 +48,6 @@ export class Dialog extends React.PureComponent<DialogProps, DialogState> {
pinned: false,
};
@disposeOnUnmount
closeOnNavigate = reaction(() => navigation.toString(), () => this.close());
public state: DialogState = {
isOpen: this.props.isOpen,
};
@ -64,7 +61,13 @@ export class Dialog extends React.PureComponent<DialogProps, DialogState> {
}
componentDidMount() {
if (this.isOpen) this.onOpen();
if (this.isOpen) {
this.onOpen();
}
disposeOnUnmount(this, [
reaction(() => navigation.toString(), () => this.close()),
]);
}
componentDidUpdate(prevProps: DialogProps) {

View File

@ -8,12 +8,12 @@ import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import fse from "fs-extra";
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 { ThemeStore } from "../../../theme.store";
import { UserStore } from "../../../../common/user-store";
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 { renderFor } from "../../test-utils/renderFor";
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.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import { CreateResourceStore } from "./create-resource.store";
import { CreateResourceTabStore } from "./store";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const createResourceStoreInjectable = getInjectable({
instantiate: (di) => new CreateResourceStore({
dockStore: di.inject(dockStoreInjectable),
const createResourceTabStoreInjectable = getInjectable({
instantiate: (di) => new CreateResourceTabStore({
createStorage: di.inject(createStorageInjectable),
}),
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 { DockTabStore, DockTabStoreOptions } from "./dock-tab.store";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const createDockTabStoreInjectable = getInjectable({
instantiate: (di) => {
const dependencies = {
dockStore: di.inject(dockStoreInjectable),
createStorage: di.inject(createStorageInjectable),
};

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