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:
commit
c0559bf772
106
.eslintrc.js
106
.eslintrc.js
@ -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",
|
||||
|
||||
@ -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 {};
|
||||
@ -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'");
|
||||
|
||||
14
package.json
14
package.json
@ -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",
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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);
|
||||
38
src/common/fs/write-json-file.injectable.ts
Normal file
38
src/common/fs/write-json-file.injectable.ts
Normal 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;
|
||||
@ -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 });
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/common/utils/wait-for-path.ts
Normal file
55
src/common/utils/wait-for-path.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
13
src/common/vars/is-windows.injectable.ts
Normal file
13
src/common/vars/is-windows.injectable.ts
Normal 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;
|
||||
@ -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>;
|
||||
@ -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;
|
||||
};
|
||||
@ -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";
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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[] = [];
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -74,7 +74,7 @@ export class WindowManager extends Singleton {
|
||||
nodeIntegrationInSubFrames: true,
|
||||
webviewTag: true,
|
||||
contextIsolation: false,
|
||||
nativeWindowOpen: true,
|
||||
nativeWindowOpen: false,
|
||||
},
|
||||
});
|
||||
this.windowState.manage(this.mainWindow);
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()) || []);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
@ -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");
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -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> {
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
};
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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,
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
.CreateResource {
|
||||
}
|
||||
@ -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,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { TabId } from "../dock/store";
|
||||
import createResourceTabStoreInjectable from "./store.injectable";
|
||||
|
||||
const clearCreateResourceTabDataInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const createResourceTabStore = di.inject(createResourceTabStoreInjectable);
|
||||
|
||||
return (tabId: TabId): void => {
|
||||
createResourceTabStore.clearData(tabId);
|
||||
};
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default clearCreateResourceTabDataInjectable;
|
||||
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import dockStoreInjectable from "../dock/store.injectable";
|
||||
import { DockTabCreateSpecific, TabKind } from "../dock/store";
|
||||
|
||||
const createResourceTabInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const dockStore = di.inject(dockStoreInjectable);
|
||||
|
||||
return (tabParams: DockTabCreateSpecific = {}) =>
|
||||
dockStore.createTab({
|
||||
title: "Create resource",
|
||||
...tabParams,
|
||||
kind: TabKind.CREATE_RESOURCE,
|
||||
});
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createResourceTabInjectable;
|
||||
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import type { GroupSelectOption, SelectOption } from "../../select";
|
||||
import userCreateResourceTemplatesInjectable from "./user-templates.injectable";
|
||||
import lensCreateResourceTemplatesInjectable from "./lens-templates.injectable";
|
||||
|
||||
export type RawTemplates = [group: string, items: [file: string, contents: string][]];
|
||||
|
||||
const createResourceTemplatesInjectable = getInjectable({
|
||||
instantiate: async (di) => {
|
||||
const lensResourceTemplates = await di.inject(lensCreateResourceTemplatesInjectable);
|
||||
const userResourceTemplates = di.inject(userCreateResourceTemplatesInjectable);
|
||||
|
||||
return computed(() => {
|
||||
const res = [
|
||||
...userResourceTemplates.get(),
|
||||
lensResourceTemplates,
|
||||
];
|
||||
|
||||
return res.map(([group, items]) => ({
|
||||
label: group,
|
||||
options: items.map(([label, value]) => ({ label, value })),
|
||||
}) as GroupSelectOption<SelectOption<string>>);
|
||||
});
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createResourceTemplatesInjectable;
|
||||
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
const extensionMatchers = [
|
||||
/\.yaml$/,
|
||||
/\.yml$/,
|
||||
/\.json$/,
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a fileName matches a yaml or json file name structure
|
||||
* @param fileName The fileName to check
|
||||
*/
|
||||
export function hasCorrectExtension(fileName: string): boolean {
|
||||
return extensionMatchers.some(matcher => matcher.test(fileName));
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import path from "path";
|
||||
import { hasCorrectExtension } from "./has-correct-extension";
|
||||
import "../../../../common/vars";
|
||||
import readFileInjectable from "../../../../common/fs/read-file.injectable";
|
||||
import readDirInjectable from "../../../../common/fs/read-dir.injectable";
|
||||
import type { RawTemplates } from "./create-resource-templates.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
readDir: (dirPath: string) => Promise<string[]>;
|
||||
readFile: (filePath: string, encoding: "utf-8") => Promise<string>;
|
||||
}
|
||||
|
||||
async function getTemplates({ readDir, readFile }: Dependencies) {
|
||||
const templatesFolder = path.resolve(__static, "../templates/create-resource");
|
||||
|
||||
/**
|
||||
* Mapping between file names and their contents
|
||||
*/
|
||||
const templates: [file: string, contents: string][] = [];
|
||||
|
||||
for (const dirEntry of await readDir(templatesFolder)) {
|
||||
if (hasCorrectExtension(dirEntry)) {
|
||||
templates.push([path.parse(dirEntry).name, await readFile(path.join(templatesFolder, dirEntry), "utf-8")]);
|
||||
}
|
||||
}
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
const lensCreateResourceTemplatesInjectable = getInjectable({
|
||||
instantiate: async (di): Promise<RawTemplates> => {
|
||||
const templates = await getTemplates({
|
||||
readFile: di.inject(readFileInjectable),
|
||||
readDir: di.inject(readDirInjectable),
|
||||
});
|
||||
|
||||
return ["lens", templates];
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default lensCreateResourceTemplatesInjectable;
|
||||
@ -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;
|
||||
19
src/renderer/components/dock/create-resource/store.ts
Normal file
19
src/renderer/components/dock/create-resource/store.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
154
src/renderer/components/dock/create-resource/view.tsx
Normal file
154
src/renderer/components/dock/create-resource/view.tsx
Normal 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,
|
||||
}),
|
||||
});
|
||||
@ -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;
|
||||
@ -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,
|
||||
});
|
||||
@ -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;
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
@ -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
Loading…
Reference in New Issue
Block a user