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

Merge branch 'master' into fix/consistent-inputs

This commit is contained in:
Alex Andreev 2022-02-01 15:34:25 +03:00
commit 135717b052
280 changed files with 4847 additions and 4072 deletions

View File

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

View File

@ -1,3 +1,3 @@
disturl "https://atom.io/download/electron"
target "13.6.1"
target "14.2.4"
runtime "electron"

View File

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

View File

@ -57,6 +57,14 @@ Will register a new view for the KubernetesCluster category, and because the pri
The default list view has a priority of 50 and and custom views with priority (defaulting to 50) >= 50 will be displayed afterwards.
#### Styling Custom Views
By default, custom view blocks are styled with [Flexbox](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox). Some details comes from this.
- To set fixed height of a custom block, use `max-height` css rule.
- To set flexible height, use `height`.
- Otherwise, custom view will have height of it's contents.
## Entities
An entity is the data within the catalog.

View File

@ -11,6 +11,7 @@
*/
import type { ElectronApplication, Page } from "playwright";
import * as utils from "../helpers/utils";
import { isWindows } from "../../src/common/vars";
describe("preferences page tests", () => {
let window: Page, cleanup: () => Promise<void>;
@ -33,7 +34,8 @@ describe("preferences page tests", () => {
await cleanup();
}, 10*60*1000);
it('shows "preferences" and can navigate through the tabs', async () => {
// skip on windows due to suspected playwright issue with Electron 14
utils.itIf(!isWindows)('shows "preferences" and can navigate through the tabs', async () => {
const pages = [
{
id: "application",

View File

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

View File

@ -5,6 +5,7 @@
import type { ElectronApplication, Page } from "playwright";
import * as utils from "../helpers/utils";
import { isWindows } from "../../src/common/vars";
describe("Lens command palette", () => {
let window: Page, cleanup: () => Promise<void>, app: ElectronApplication;
@ -19,7 +20,8 @@ describe("Lens command palette", () => {
}, 10*60*1000);
describe("menu", () => {
it("opens command dialog from menu", async () => {
// skip on windows due to suspected playwright issue with Electron 14
utils.itIf(!isWindows)("opens command dialog from menu", async () => {
await app.evaluate(async ({ app }) => {
await app.applicationMenu
.getMenuItemById("view")

View File

@ -40,7 +40,7 @@ async function getMainWindow(app: ElectronApplication, timeout = 50_000): Promis
throw new Error(`Lens did not open the main window within ${timeout}ms`);
}
export async function start() {
async function attemptStart() {
const CICD = path.join(os.tmpdir(), "lens-integration-testing", uuid.v4());
// Make sure that the directory is clear
@ -76,6 +76,19 @@ export async function start() {
}
}
export async function start() {
// this is an attempted workaround for an issue with playwright not always getting the main window when using Electron 14.2.4 (observed on windows)
for (let i = 0; ; i++) {
try {
return await attemptStart();
} catch (error) {
if (i === 4) {
throw error;
}
}
}
}
export async function clickWelcomeButton(window: Page) {
await window.click("[data-testid=welcome-menu-container] li a");
}

View File

@ -191,7 +191,6 @@
}
},
"dependencies": {
"@electron/remote": "^1.2.2",
"@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.1",
@ -324,8 +323,8 @@
"@types/webpack-dev-server": "^3.11.6",
"@types/webpack-env": "^1.16.3",
"@types/webpack-node-externals": "^1.7.1",
"@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",
@ -334,23 +333,23 @@
"css-loader": "^5.2.7",
"deepdash": "^5.3.9",
"dompurify": "^2.3.4",
"electron": "^13.6.1",
"electron": "^14.2.4",
"electron-builder": "^22.14.5",
"electron-notarize": "^0.3.0",
"esbuild": "^0.13.15",
"esbuild-loader": "^2.16.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",
"file-loader": "^6.2.0",
"flex.box": "^3.4.4",
"fork-ts-checker-webpack-plugin": "^5.2.1",
"hoist-non-react-statics": "^3.3.2",
"html-webpack-plugin": "^4.5.2",
"ignore-loader": "^0.1.2",
"include-media": "^1.4.9",
"jest": "26.6.3",
"jest-canvas-mock": "^2.3.1",

View File

@ -4,45 +4,61 @@
*/
import { anyObject } from "jest-mock-extended";
import { merge } from "lodash";
import mockFs from "mock-fs";
import logger from "../../main/logger";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { HotbarStore } from "../hotbar-store";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import directoryForUserDataInjectable
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
jest.mock("../../main/catalog/catalog-entity-registry", () => ({
catalogEntityRegistry: {
items: [
{
getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "1dfa26e2ebab15780a3547e9c7fa785c",
name: "mycluster",
source: "local",
labels: {},
},
}),
getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
},
{
metadata: {
uid: "55b42c3c7ba3b04193416cda405269a5",
name: "my_shiny_cluster",
source: "remote",
labels: {},
},
}),
getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
},
{
metadata: {
uid: "catalog-entity",
name: "Catalog",
source: "app",
labels: {},
},
},
}),
],
},
}));
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return merge(data, {
return {
getName: jest.fn(() => data.metadata?.name),
getId: jest.fn(() => data.metadata?.uid),
getSource: jest.fn(() => data.metadata?.source ?? "unknown"),
@ -52,7 +68,8 @@ function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKi
metadata: {},
spec: {},
status: {},
}) as CatalogEntity;
...data,
} as CatalogEntity;
}
const testCluster = getMockCatalogEntity({

View File

@ -5,12 +5,12 @@
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store/cluster-store";
import { broadcastMessage, requestMain } from "../ipc";
import { broadcastMessage } from "../ipc";
import { app } from "electron";
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc";
export interface KubernetesClusterPrometheusMetrics {
address?: {
@ -67,22 +67,22 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
async connect(): Promise<void> {
if (app) {
await ClusterStore.getInstance().getById(this.metadata.uid)?.activate();
await ClusterStore.getInstance().getById(this.getId())?.activate();
} else {
await requestMain(clusterActivateHandler, this.metadata.uid, false);
await requestClusterActivation(this.getId(), false);
}
}
async disconnect(): Promise<void> {
if (app) {
ClusterStore.getInstance().getById(this.metadata.uid)?.disconnect();
ClusterStore.getInstance().getById(this.getId())?.disconnect();
} else {
await requestMain(clusterDisconnectHandler, this.metadata.uid, false);
await requestClusterDisconnection(this.getId(), false);
}
}
async onRun(context: CatalogEntityActionContext) {
context.navigate(`/cluster/${this.metadata.uid}`);
context.navigate(`/cluster/${this.getId()}`);
}
onDetailsOpen(): void {
@ -100,7 +100,7 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
icon: "settings",
onClick: () => broadcastMessage(
IpcRendererNavigationEvents.NAVIGATE_IN_APP,
`/entity/${this.metadata.uid}/settings`,
`/entity/${this.getId()}/settings`,
),
});
}
@ -111,14 +111,14 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
context.menuItems.push({
title: "Disconnect",
icon: "link_off",
onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid),
onClick: () => requestClusterDisconnection(this.getId()),
});
break;
case LensKubernetesClusterStatus.DISCONNECTED:
context.menuItems.push({
title: "Connect",
icon: "link",
onClick: () => context.navigate(`/cluster/${this.metadata.uid}`),
onClick: () => context.navigate(`/cluster/${this.getId()}`),
});
break;
}

View File

@ -38,9 +38,9 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
context.menuItems.push({
title: "Delete",
icon: "delete",
onClick: async () => WeblinkStore.getInstance().removeById(this.metadata.uid),
onClick: async () => WeblinkStore.getInstance().removeById(this.getId()),
confirm: {
message: `Remove Web Link "${this.metadata.name}" from ${productName}?`,
message: `Remove Web Link "${this.getName()}" from ${productName}?`,
},
});
}

View File

@ -315,11 +315,24 @@ export abstract class CatalogEntity<
@observable status: Status;
@observable spec: Spec;
constructor(data: CatalogEntityData<Metadata, Status, Spec>) {
constructor({ metadata, status, spec }: CatalogEntityData<Metadata, Status, Spec>) {
makeObservable(this);
this.metadata = data.metadata;
this.status = data.status;
this.spec = data.spec;
if (!metadata || typeof metadata !== "object") {
throw new TypeError("CatalogEntity's metadata must be a defined object");
}
if (!status || typeof status !== "object") {
throw new TypeError("CatalogEntity's status must be a defined object");
}
if (!spec || typeof spec !== "object") {
throw new TypeError("CatalogEntity's spec must be a defined object");
}
this.metadata = metadata;
this.status = status;
this.spec = spec;
}
/**

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { ipcMain, ipcRenderer, webFrame } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
@ -11,16 +11,16 @@ import { Cluster } from "../cluster/cluster";
import migrations from "../../migrations/cluster-store";
import logger from "../../main/logger";
import { appEventBus } from "../app-event-bus/event-bus";
import { ipcMainHandle, requestMain } from "../ipc";
import { ipcMainHandle } from "../ipc";
import { disposer, toJS } from "../utils";
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
import { requestInitialClusterStates } from "../../renderer/ipc";
import { clusterStates } from "../ipc/cluster";
export interface ClusterStoreModel {
clusters?: ClusterModel[];
}
const initialStates = "cluster:states";
interface Dependencies {
createCluster: (model: ClusterModel) => Cluster
}
@ -49,18 +49,18 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
async loadInitialOnRenderer() {
logger.info("[CLUSTER-STORE] requesting initial state sync");
for (const { id, state } of await requestMain(initialStates)) {
for (const { id, state } of await requestInitialClusterStates()) {
this.getById(id)?.setState(state);
}
}
provideInitialFromMain() {
ipcMainHandle(initialStates, () => {
return this.clustersList.map(cluster => ({
ipcMainHandle(clusterStates, () => (
this.clustersList.map(cluster => ({
id: cluster.id,
state: cluster.getState(),
}));
});
}))
));
}
protected pushStateToViewsAutomatically() {

View File

@ -5,7 +5,7 @@
import { ipcMain } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../ipc";
import { broadcastMessage } from "../ipc";
import type { ContextHandler } from "../../main/context-handler/context-handler";
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import type { Kubectl } from "../../main/kubectl/kubectl";
@ -20,6 +20,7 @@ import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, C
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
import { disposer, toJS } from "../utils";
import type { Response } from "request";
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
interface Dependencies {
directoryForKubeConfigs: string,
@ -641,7 +642,7 @@ export class Cluster implements ClusterModel, ClusterState {
const { response } = error as HttpError & { response: Response };
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body });
broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id);
broadcastMessage(clusterListNamespaceForbiddenChannel, this.id);
}
return namespaceList;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { EnsureOptions, WriteOptions } from "fs-extra";
import path from "path";
import type { JsonValue } from "type-fest";
import fsInjectable from "./fs.injectable";
interface Dependencies {
writeJson: (file: string, object: any, options?: WriteOptions | BufferEncoding | string) => Promise<void>;
ensureDir: (dir: string, options?: EnsureOptions | number) => Promise<void>;
}
const writeJsonFile = ({ writeJson, ensureDir }: Dependencies) => async (filePath: string, content: JsonValue) => {
await ensureDir(path.dirname(filePath), { mode: 0o755 });
await writeJson(filePath, content, {
encoding: "utf-8",
spaces: 2,
});
};
const writeJsonFileInjectable = getInjectable({
instantiate: (di) => {
const { writeJson, ensureDir } = di.inject(fsInjectable);
return writeJsonFile({
writeJson,
ensureDir,
});
},
lifecycle: lifecycleEnum.singleton,
});
export default writeJsonFileInjectable;

View File

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

View File

@ -10,8 +10,9 @@ import { toJS } from "./utils";
import { CatalogEntity } from "./catalog";
import { catalogEntity } from "../main/catalog-sources/general";
import logger from "../main/logger";
import { broadcastMessage, HotbarTooManyItems } from "./ipc";
import { broadcastMessage } from "./ipc";
import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types";
import { hotbarTooManyItemsChannel } from "./ipc/hotbar";
export interface HotbarStoreModel {
hotbars: Hotbar[];
@ -153,28 +154,28 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
@action
addToHotbar(item: CatalogEntity, cellIndex?: number) {
const hotbar = this.getActive();
const uid = item.metadata?.uid;
const name = item.metadata?.name;
const uid = item.getId();
const name = item.getName();
if (typeof uid !== "string") {
throw new TypeError("CatalogEntity.metadata.uid must be a string");
throw new TypeError("CatalogEntity's ID must be a string");
}
if (typeof name !== "string") {
throw new TypeError("CatalogEntity.metadata.name must be a string");
throw new TypeError("CatalogEntity's NAME must be a string");
}
const newItem = { entity: {
uid,
name,
source: item.metadata.source,
}};
if (this.isAddedToActive(item)) {
return;
}
const entity = {
uid,
name,
source: item.metadata.source,
};
const newItem = { entity };
if (cellIndex === undefined) {
// Add item to empty cell
const emptyCellIndex = hotbar.items.indexOf(null);
@ -182,7 +183,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
if (emptyCellIndex != -1) {
hotbar.items[emptyCellIndex] = newItem;
} else {
broadcastMessage(HotbarTooManyItems);
broadcastMessage(hotbarTooManyItemsChannel);
}
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
hotbar.items[cellIndex] = newItem;
@ -277,11 +278,14 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}
/**
* Checks if entity already pinned to hotbar
* @returns boolean
* Checks if entity already pinned to the active hotbar
*/
isAddedToActive(entity: CatalogEntity) {
return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid);
isAddedToActive(entity: CatalogEntity | null | undefined): boolean {
if (!entity) {
return false;
}
return this.getActive().items.findIndex(item => item?.entity.uid === entity.getId()) >= 0;
}
getDisplayLabel(hotbar: Hotbar): string {

View File

@ -3,14 +3,17 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export enum CatalogIpcEvents {
/**
* This is broadcast on whenever there is an update to any catalog item
*/
ITEMS = "catalog:items",
/**
* This is used to activate a specific entity in the renderer main frame
*/
export const catalogEntityRunListener = "catalog-entity:run";
/**
* This can be sent from renderer to main to initialize a broadcast of ITEMS
*/
INIT = "catalog:init",
}
/**
* This is broadcast on whenever there is an update to any catalog item
*/
export const catalogItemsChannel = "catalog:items";
/**
* This can be sent from renderer to main to initialize a broadcast of ITEMS
*/
export const catalogInitChannel = "catalog:init";

View File

@ -1,16 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* This channel is broadcast on whenever the cluster fails to list namespaces
* during a refresh and no `accessibleNamespaces` have been set.
*/
export const ClusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden";
export type ListNamespaceForbiddenArgs = [clusterId: string];
export function isListNamespaceForbiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs {
return args.length === 1 && typeof args[0] === "string";
}

View File

@ -13,3 +13,16 @@ export const clusterSetDeletingHandler = "cluster:deleting:set";
export const clusterClearDeletingHandler = "cluster:deleting:clear";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
export const clusterStates = "cluster:states";
/**
* This channel is broadcast on whenever the cluster fails to list namespaces
* during a refresh and no `accessibleNamespaces` have been set.
*/
export const clusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden";
export type ListNamespaceForbiddenArgs = [clusterId: string];
export function isListNamespaceForbiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs {
return args.length === 1 && typeof args[0] === "string";
}

View File

@ -3,6 +3,4 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import * as dialog from "./dialog";
export { dialog };
export const openFilePickingDialogChannel = "dialog:open:file-picking";

View File

@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const extensionDiscoveryStateChannel = "extension-discovery:state";
export const bundledExtensionsLoaded = "extension-loader:bundled-extensions-loaded";
export const extensionLoaderFromMainChannel = "extension-loader:main:state";
export const extensionLoaderFromRendererChannel = "extension-loader:renderer:state";

View File

@ -1,5 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const BundledExtensionsLoaded = "extension-loader:bundled-extensions-loaded";

View File

@ -3,4 +3,4 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const HotbarTooManyItems = "hotbar:too-many-items";
export const hotbarTooManyItemsChannel = "hotbar:too-many-items";

View File

@ -3,13 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const dialogShowOpenDialogHandler = "dialog:show-open-dialog";
export const catalogEntityRunListener = "catalog-entity:run";
export * from "./ipc";
export * from "./invalid-kubeconfig";
export * from "./update-available.ipc";
export * from "./cluster.ipc";
export * from "./update-available";
export * from "./type-enforced-ipc";
export * from "./hotbar";
export * from "./extension-loader.ipc";

View File

@ -12,25 +12,8 @@ import { toJS } from "../utils/toJS";
import logger from "../../main/logger";
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
import type { Disposer } from "../utils";
import type remote from "@electron/remote";
const electronRemote = (() => {
if (ipcRenderer) {
try {
return require("@electron/remote");
} catch {
// ignore temp
}
}
return null;
})();
const subFramesChannel = "ipc:get-sub-frames";
export async function requestMain(channel: string, ...args: any[]) {
return ipcRenderer.invoke(channel, ...args.map(sanitizePayload));
}
export const broadcastMainChannel = "ipc:broadcast-main";
export function ipcMainHandle(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) {
ipcMain.handle(channel, async (event, ...args) => {
@ -42,51 +25,55 @@ function getSubFrames(): ClusterFrameInfo[] {
return Array.from(clusterFrameMap.values());
}
export function broadcastMessage(channel: string, ...args: any[]) {
const subFramesP = ipcRenderer
? requestMain(subFramesChannel)
: Promise.resolve(getSubFrames());
export async function broadcastMessage(channel: string, ...args: any[]): Promise<void> {
if (ipcRenderer) {
return ipcRenderer.invoke(broadcastMainChannel, channel, ...args.map(sanitizePayload));
}
subFramesP
.then(subFrames => {
const views: undefined | ReturnType<typeof webContents.getAllWebContents> | ReturnType<typeof remote.webContents.getAllWebContents> = (webContents || electronRemote?.webContents)?.getAllWebContents();
if (!webContents) {
return;
}
if (!views || !Array.isArray(views) || views.length === 0) return;
args = args.map(sanitizePayload);
ipcMain.listeners(channel).forEach((func) => func({
processId: undefined, frameId: undefined, sender: undefined, senderFrame: undefined,
}, ...args));
ipcRenderer?.send(channel, ...args);
ipcMain?.emit(channel, ...args);
const subFrames = getSubFrames();
const views = webContents.getAllWebContents();
for (const view of views) {
let viewType = "unknown";
if (!views || !Array.isArray(views) || views.length === 0) return;
// There will be a uncaught exception if the view is destroyed.
try {
viewType = view.getType();
} catch {
// We can ignore the view destroyed exception as viewType is only used for logging.
}
args = args.map(sanitizePayload);
// Send message to views.
try {
logger.silly(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args });
view.send(channel, ...args);
} catch (error) {
logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"`, { error: String(error) });
}
for (const view of views) {
let viewType = "unknown";
// Send message to subFrames of views.
for (const frameInfo of subFrames) {
logger.silly(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args });
// There will be a uncaught exception if the view is destroyed.
try {
viewType = view.getType();
} catch {
// We can ignore the view destroyed exception as viewType is only used for logging.
}
try {
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
} catch (error) {
logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"'s subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { error: String(error) });
}
}
// Send message to views.
try {
logger.silly(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args });
view.send(channel, ...args);
} catch (error) {
logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"`, { error });
}
// Send message to subFrames of views.
for (const frameInfo of subFrames) {
logger.silly(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args });
try {
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
} catch (error) {
logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"'s subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { error: String(error) });
}
});
}
}
}
export function ipcMainOn(channel: string, listener: (event: Electron.IpcMainEvent, ...args: any[]) => any): Disposer {
@ -101,10 +88,6 @@ export function ipcRendererOn(channel: string, listener: (event: Electron.IpcRen
return () => ipcRenderer.off(channel, listener);
}
export function bindBroadcastHandlers() {
ipcMainHandle(subFramesChannel, () => getSubFrames());
}
/**
* Sanitizing data for IPC-messaging before send.
* Removes possible observable values to avoid exceptions like "can't clone object".

39
src/common/ipc/window.ts Normal file
View File

@ -0,0 +1,39 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const windowActionHandleChannel = "window:window-action";
export const windowOpenAppMenuAsContextMenuChannel = "window:open-app-context-menu";
export const windowLocationChangedChannel = "window:location-changed";
/**
* The supported actions on the current window. The argument for `windowActionHandleChannel`
*/
export enum WindowAction {
/**
* Request that the current window goes back one step of browser history
*/
GO_BACK = "back",
/**
* Request that the current window goes forward one step of browser history
*/
GO_FORWARD = "forward",
/**
* Request that the current window is minimized
*/
MINIMIZE = "minimize",
/**
* Request that the current window is maximized if it isn't, or unmaximized
* if it is
*/
TOGGLE_MAXIMIZE = "toggle-maximize",
/**
* Request that the current window is closed
*/
CLOSE = "close",
}

View File

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

View File

@ -9,11 +9,10 @@ import { ResourceApplier } from "../../main/resource-applier";
import type { KubernetesCluster } from "../catalog-entities";
import logger from "../../main/logger";
import { app } from "electron";
import { requestMain } from "../ipc";
import { clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store/cluster-store";
import yaml from "js-yaml";
import { productName } from "../vars";
import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc";
export class ResourceStack {
constructor(protected cluster: KubernetesCluster, protected name: string) {}
@ -41,7 +40,7 @@ export class ResourceStack {
}
protected async applyResources(resources: string[], extraArgs?: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid);
const clusterModel = ClusterStore.getInstance().getById(this.cluster.getId());
if (!clusterModel) {
throw new Error(`cluster not found`);
@ -54,7 +53,7 @@ export class ResourceStack {
if (app) {
return await new ResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs);
} else {
const response = await requestMain(clusterKubectlApplyAllHandler, this.cluster.metadata.uid, resources, kubectlArgs);
const response = await requestKubectlApplyAll(this.cluster.getId(), resources, kubectlArgs);
if (response.stderr) {
throw new Error(response.stderr);
@ -65,7 +64,7 @@ export class ResourceStack {
}
protected async deleteResources(resources: string[], extraArgs?: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid);
const clusterModel = ClusterStore.getInstance().getById(this.cluster.getId());
if (!clusterModel) {
throw new Error(`cluster not found`);
@ -78,7 +77,7 @@ export class ResourceStack {
if (app) {
return await new ResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs);
} else {
const response = await requestMain(clusterKubectlDeleteAllHandler, this.cluster.metadata.uid, resources, kubectlArgs);
const response = await requestKubectlDeleteAll(this.cluster.getId(), resources, kubectlArgs);
if (response.stderr) {
throw new Error(response.stderr);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { isWindows } from "../vars";
const isWindowsInjectable = getInjectable({
instantiate: () => isWindows,
lifecycle: lifecycleEnum.singleton,
});
export default isWindowsInjectable;

View File

@ -8,8 +8,7 @@ import { Console } from "console";
import { stdout, stderr } from "process";
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
import { runInAction } from "mobx";
import updateExtensionsStateInjectable
from "../extension-loader/update-extensions-state/update-extensions-state.injectable";
import updateExtensionsStateInjectable from "../extension-loader/update-extensions-state/update-extensions-state.injectable";
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
import mockFs from "mock-fs";
@ -24,7 +23,7 @@ jest.mock(
() => ({
ipcRenderer: {
invoke: jest.fn(async (channel: string) => {
if (channel === "extensions:main") {
if (channel === "extension-loader:main:state") {
return [
[
manifestPath,
@ -61,7 +60,7 @@ jest.mock(
}),
on: jest.fn(
(channel: string, listener: (event: any, ...args: any[]) => void) => {
if (channel === "extensions:main") {
if (channel === "extension-loader:main:state") {
// First initialize with extensions 1 and 2
// and then broadcast event to remove extension 2 and add extension number 3
setTimeout(() => {

View File

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

View File

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

View File

@ -2,13 +2,12 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type { StatusBarRegistration } from "../../renderer/components/cluster-manager/status-bar-registration";
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../../renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration";
export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../renderer/components/+preferences/app-preferences/app-preference-registration";
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry";
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry";
export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry";
export type { StatusBarRegistration } from "../registries/status-bar-registry";
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler";
export type { CustomCategoryViewProps, CustomCategoryViewComponents, CustomCategoryViewRegistration } from "../../renderer/components/+catalog/custom-views";

View File

@ -10,12 +10,7 @@ import fse from "fs-extra";
import { makeObservable, observable, reaction, when } from "mobx";
import os from "os";
import path from "path";
import {
broadcastMessage,
ipcMainHandle,
ipcRendererOn,
requestMain,
} from "../../common/ipc";
import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc";
import { toJS } from "../../common/utils";
import logger from "../../main/logger";
import type { ExtensionsStore } from "../extensions-store/extensions-store";
@ -24,6 +19,8 @@ import type { LensExtensionId, LensExtensionManifest } from "../lens-extension";
import { isProduction } from "../../common/vars";
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
import type { PackageJson } from "type-fest";
import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling";
import { requestInitialExtensionDiscovery } from "../../renderer/ipc";
interface Dependencies {
extensionLoader: ExtensionLoader;
@ -96,9 +93,6 @@ export class ExtensionDiscovery {
return when(() => this.isLoaded);
}
// IPC channel to broadcast changes to extension-discovery from main
protected static readonly extensionDiscoveryChannel = "extension-discovery:main";
public events = new EventEmitter();
constructor(protected dependencies : Dependencies) {
@ -141,14 +135,14 @@ export class ExtensionDiscovery {
this.isLoaded = isLoaded;
};
requestMain(ExtensionDiscovery.extensionDiscoveryChannel).then(onMessage);
ipcRendererOn(ExtensionDiscovery.extensionDiscoveryChannel, (_event, message: ExtensionDiscoveryChannelMessage) => {
requestInitialExtensionDiscovery().then(onMessage);
ipcRendererOn(extensionDiscoveryStateChannel, (_event, message: ExtensionDiscoveryChannelMessage) => {
onMessage(message);
});
}
async initMain(): Promise<void> {
ipcMainHandle(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON());
ipcMainHandle(extensionDiscoveryStateChannel, () => this.toJSON());
reaction(() => this.toJSON(), () => {
this.broadcast();
@ -492,6 +486,6 @@ export class ExtensionDiscovery {
}
broadcast(): void {
broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON());
broadcastMessage(extensionDiscoveryStateChannel, this.toJSON());
}
}

View File

@ -8,7 +8,7 @@ import { EventEmitter } from "events";
import { isEqual } from "lodash";
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
import path from "path";
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc";
import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc";
import { Disposer, toJS } from "../../common/utils";
import logger from "../../main/logger";
import type { KubernetesCluster } from "../common-api/catalog";
@ -17,6 +17,8 @@ import type { LensExtension, LensExtensionConstructor, LensExtensionId } from ".
import type { LensRendererExtension } from "../lens-renderer-extension";
import * as registries from "../registries";
import type { LensExtensionState } from "../extensions-store/extensions-store";
import { extensionLoaderFromMainChannel, extensionLoaderFromRendererChannel } from "../../common/ipc/extension-handling";
import { requestExtensionLoaderInitialState } from "../../renderer/ipc";
const logModule = "[EXTENSIONS-LOADER]";
@ -49,12 +51,6 @@ export class ExtensionLoader {
*/
protected instancesByName = observable.map<string, LensExtension>();
// IPC channel to broadcast changes to extensions from main
protected static readonly extensionsMainChannel = "extensions:main";
// IPC channel to broadcast changes to extensions from renderer
protected static readonly extensionsRendererChannel = "extensions:renderer";
// emits event "remove" of type LensExtension when the extension is removed
private events = new EventEmitter();
@ -196,11 +192,11 @@ export class ExtensionLoader {
this.isLoaded = true;
this.loadOnMain();
ipcMainHandle(ExtensionLoader.extensionsMainChannel, () => {
ipcMainHandle(extensionLoaderFromMainChannel, () => {
return Array.from(this.toJSON());
});
ipcMainOn(ExtensionLoader.extensionsRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
ipcMainOn(extensionLoaderFromRendererChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
this.syncExtensions(extensions);
});
}
@ -220,16 +216,16 @@ export class ExtensionLoader {
});
};
requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler);
ipcRendererOn(ExtensionLoader.extensionsMainChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
requestExtensionLoaderInitialState().then(extensionListHandler);
ipcRendererOn(extensionLoaderFromMainChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
extensionListHandler(extensions);
});
}
broadcastExtensions() {
const channel = ipcRenderer
? ExtensionLoader.extensionsRendererChannel
: ExtensionLoader.extensionsMainChannel;
? extensionLoaderFromRendererChannel
: extensionLoaderFromMainChannel;
broadcastMessage(channel, Array.from(this.extensions));
}
@ -253,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),
];
@ -281,7 +276,6 @@ export class ExtensionLoader {
const removeItems = [
registries.ClusterPageRegistry.getInstance().add(extension.clusterPages, extension),
registries.ClusterPageMenuRegistry.getInstance().add(extension.clusterPageMenus, extension),
registries.KubeObjectMenuRegistry.getInstance().add(extension.kubeObjectMenuItems),
registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems),
registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts),
registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems),

View File

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

View File

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

View File

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

View File

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

View File

@ -51,8 +51,7 @@ describe("create clusters", () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
const mockOpts = {
mockFs({
"minikube-config.yml": JSON.stringify({
apiVersion: "v1",
clusters: [{
@ -74,9 +73,7 @@ describe("create clusters", () => {
kind: "Config",
preferences: {},
}),
};
mockFs(mockOpts);
});
await di.runSetups();

View File

@ -10,15 +10,15 @@ import "../common/catalog-entities/kubernetes-cluster";
import { disposer, toJS } from "../common/utils";
import { debounce } from "lodash";
import type { CatalogEntity } from "../common/catalog";
import { CatalogIpcEvents } from "../common/ipc/catalog";
import { catalogInitChannel, catalogItemsChannel } from "../common/ipc/catalog";
const broadcaster = debounce((items: CatalogEntity[]) => {
broadcastMessage(CatalogIpcEvents.ITEMS, items);
broadcastMessage(catalogItemsChannel, items);
}, 1_000, { leading: true, trailing: true });
export function pushCatalogToRenderer(catalog: CatalogEntityRegistry) {
return disposer(
ipcMainOn(CatalogIpcEvents.INIT, () => broadcaster(toJS(catalog.items))),
ipcMainOn(catalogInitChannel, () => broadcaster(toJS(catalog.items))),
reaction(() => toJS(catalog.items), (items) => {
broadcaster(items);
}, {

View File

@ -36,7 +36,7 @@ export class CatalogEntityRegistry {
}
getById<T extends CatalogEntity>(id: string): T | undefined {
return this.items.find((entity) => entity.metadata.uid === id) as T | undefined;
return this.items.find(entity => entity.getId() === id) as T | undefined;
}
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "../common/cluster-ipc";
import "../common/ipc/cluster";
import type http from "http";
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
import { Cluster } from "../common/cluster/cluster";
@ -85,7 +85,7 @@ export class ClusterManager extends Singleton {
}
protected updateEntityFromCluster(cluster: Cluster) {
const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id);
const index = catalogEntityRegistry.items.findIndex((entity) => entity.getId() === cluster.id);
if (index === -1) {
return;
@ -169,11 +169,11 @@ export class ClusterManager extends Singleton {
@action
protected syncClustersFromCatalog(entities: KubernetesCluster[]) {
for (const entity of entities) {
const cluster = this.store.getById(entity.metadata.uid);
const cluster = this.store.getById(entity.getId());
if (!cluster) {
const model = {
id: entity.metadata.uid,
id: entity.getId(),
kubeConfigPath: entity.spec.kubeconfigPath,
contextName: entity.spec.kubeconfigContext,
accessibleNamespaces: entity.spec.accessibleNamespaces ?? [],

View File

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

View File

@ -6,7 +6,6 @@
// Main process
import { injectSystemCAs } from "../common/system-ca";
import { initialize as initializeRemote } from "@electron/remote/main";
import * as Mobx from "mobx";
import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsMainApi from "../extensions/main-api";
@ -24,7 +23,7 @@ import type { InstalledExtension } from "../extensions/extension-discovery/exten
import type { LensExtensionId } from "../extensions/lens-extension";
import { installDeveloperTools } from "./developer-tools";
import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
import { bindBroadcastHandlers, ipcMainOn } from "../common/ipc";
import { ipcMainOn } from "../common/ipc";
import { startUpdateChecking } from "./app-updater";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { pushCatalogToRenderer } from "./catalog-pusher";
@ -81,9 +80,6 @@ di.runSetups().then(() => {
app.disableHardwareAcceleration();
}
logger.debug("[APP-MAIN] initializing remote");
initializeRemote();
logger.debug("[APP-MAIN] configuring packages");
configurePackages();
@ -131,8 +127,6 @@ di.runSetups().then(() => {
logger.info("🐚 Syncing shell environment");
await shellSync();
bindBroadcastHandlers();
powerMonitor.on("shutdown", () => app.exit());
registerFileProtocol("static", __static);

View File

@ -3,23 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { BrowserWindow, dialog, IpcMainInvokeEvent, Menu } from "electron";
import { BrowserWindow, IpcMainInvokeEvent, Menu } from "electron";
import { clusterFrameMap } from "../../../common/cluster-frames";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/cluster-ipc";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/ipc/cluster";
import type { ClusterId } from "../../../common/cluster-types";
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
import { appEventBus } from "../../../common/app-event-bus/event-bus";
import { dialogShowOpenDialogHandler, ipcMainHandle, ipcMainOn } from "../../../common/ipc";
import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../common/ipc";
import { catalogEntityRegistry } from "../../catalog";
import { pushCatalogToRenderer } from "../../catalog-pusher";
import { ClusterManager } from "../../cluster-manager";
import { ResourceApplier } from "../../resource-applier";
import { IpcMainWindowEvents, WindowManager } from "../../window-manager";
import { WindowManager } from "../../window-manager";
import path from "path";
import { remove } from "fs-extra";
import { getAppMenu } from "../../menu/menu";
import type { MenuRegistration } from "../../menu/menu-registration";
import type { IComputedValue } from "mobx";
import { onLocationChange, handleWindowAction } from "../../ipc/window";
import { openFilePickingDialogChannel } from "../../../common/ipc/dialog";
import { showOpenDialog } from "../../ipc/dialog";
import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel } from "../../../common/ipc/window";
interface Dependencies {
electronMenuItems: IComputedValue<MenuRegistration[]>,
@ -136,21 +140,22 @@ export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalSt
}
});
ipcMainHandle(dialogShowOpenDialogHandler, async (event, dialogOpts: Electron.OpenDialogOptions) => {
await WindowManager.getInstance().ensureMainWindow();
ipcMainHandle(windowActionHandleChannel, (event, action) => handleWindowAction(action));
return dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), dialogOpts);
});
ipcMainOn(windowLocationChangedChannel, () => onLocationChange());
ipcMainOn(IpcMainWindowEvents.OPEN_CONTEXT_MENU, async (event) => {
ipcMainHandle(openFilePickingDialogChannel, (event, opts) => showOpenDialog(opts));
ipcMainHandle(broadcastMainChannel, (event, channel, ...args) => broadcastMessage(channel, ...args));
ipcMainOn(windowOpenAppMenuAsContextMenuChannel, async (event) => {
const menu = Menu.buildFromTemplate(getAppMenu(WindowManager.getInstance(), electronMenuItems.get()));
const options = {
menu.popup({
...BrowserWindow.fromWebContents(event.sender),
// Center of the topbar menu icon
x: 20,
y: 20,
} as Electron.PopupOptions;
menu.popup(options);
});
});
};

12
src/main/ipc/dialog.ts Normal file
View File

@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { BrowserWindow, dialog, OpenDialogOptions } from "electron";
export async function showOpenDialog(dialogOptions: OpenDialogOptions): Promise<{ canceled: boolean; filePaths: string[]; }> {
const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), dialogOptions);
return { canceled, filePaths };
}

71
src/main/ipc/window.ts Normal file
View File

@ -0,0 +1,71 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { BrowserWindow, webContents } from "electron";
import { broadcastMessage } from "../../common/ipc";
import { WindowAction } from "../../common/ipc/window";
export function handleWindowAction(action: WindowAction) {
const window = BrowserWindow.getFocusedWindow();
if (!window) return;
switch (action) {
case WindowAction.GO_BACK: {
window.webContents.goBack();
break;
}
case WindowAction.GO_FORWARD: {
window.webContents.goForward();
break;
}
case WindowAction.MINIMIZE: {
window.minimize();
break;
}
case WindowAction.TOGGLE_MAXIMIZE: {
if (window.isMaximized()) {
window.unmaximize();
} else {
window.maximize();
}
break;
}
case WindowAction.CLOSE: {
window.close();
break;
}
default:
throw new Error(`Attemped window action ${action} is unknown`);
}
}
export function onLocationChange(): void {
const getAllWebContents = webContents.getAllWebContents();
const canGoBack = getAllWebContents.some((webContent) => {
if (webContent.getType() === "window") {
return webContent.canGoBack();
}
return false;
});
const canGoForward = getAllWebContents.some((webContent) => {
if (webContent.getType() === "window") {
return webContent.canGoForward();
}
return false;
});
broadcastMessage("history:can-go-back", canGoBack);
broadcastMessage("history:can-go-forward", canGoForward);
}

View File

@ -8,17 +8,14 @@ import { makeObservable, observable } from "mobx";
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron";
import windowStateKeeper from "electron-window-state";
import { appEventBus } from "../common/app-event-bus/event-bus";
import { BundledExtensionsLoaded, ipcMainOn } from "../common/ipc";
import { ipcMainOn } from "../common/ipc";
import { delay, iter, Singleton } from "../common/utils";
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import logger from "./logger";
import { isMac, productName } from "../common/vars";
import { LensProxy } from "./lens-proxy";
export const enum IpcMainWindowEvents {
OPEN_CONTEXT_MENU = "window:open-context-menu",
}
import { bundledExtensionsLoaded } from "../common/ipc/extension-handling";
function isHideable(window: BrowserWindow | null): boolean {
return Boolean(window && !window.isDestroyed());
@ -75,9 +72,9 @@ export class WindowManager extends Singleton {
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: true,
enableRemoteModule: true,
webviewTag: true,
contextIsolation: false,
nativeWindowOpen: false,
},
});
this.windowState.manage(this.mainWindow);
@ -135,7 +132,8 @@ export class WindowManager extends Singleton {
// Always disable Node.js integration for all webviews
webPreferences.nodeIntegration = false;
}).setWindowOpenHandler((details) => {
})
.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
return { action: "deny" };
@ -165,7 +163,7 @@ export class WindowManager extends Singleton {
if (!this.mainWindow) {
viewHasLoaded = new Promise<void>(resolve => {
ipcMain.once(BundledExtensionsLoaded, () => resolve());
ipcMain.once(bundledExtensionsLoaded, () => resolve());
});
await this.initMainWindow(showSplash);
}
@ -249,9 +247,9 @@ export class WindowManager extends Singleton {
show: false,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false,
nodeIntegrationInSubFrames: true,
nativeWindowOpen: true,
},
});
await this.splashWindow.loadURL("static://splash.html");

View File

@ -16,7 +16,7 @@ export default {
for (const hotbar of hotbars) {
for (let i = 0; i < hotbar.items.length; i += 1) {
const item = hotbar.items[i];
const entity = catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item?.entity.uid);
const entity = catalogEntityRegistry.items.find((entity) => entity.getId() === item?.entity.uid);
if (!entity) {
// Clear disabled item

View File

@ -4,7 +4,7 @@
*/
import { computed, observable, makeObservable, action } from "mobx";
import { catalogEntityRunListener, ipcRendererOn } from "../../common/ipc";
import { ipcRendererOn } from "../../common/ipc";
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
import "../../common/catalog-entities";
import type { Cluster } from "../../common/cluster/cluster";
@ -14,7 +14,7 @@ import { once } from "lodash";
import logger from "../../common/logger";
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
import { ipcRenderer } from "electron";
import { CatalogIpcEvents } from "../../common/ipc/catalog";
import { catalogInitChannel, catalogItemsChannel, catalogEntityRunListener } from "../../common/ipc/catalog";
import { navigate } from "../navigation";
import { isMainFrame } from "process";
@ -79,12 +79,12 @@ export class CatalogEntityRegistry {
}
init() {
ipcRendererOn(CatalogIpcEvents.ITEMS, (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
ipcRendererOn(catalogItemsChannel, (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
this.updateItems(items);
});
// Make sure that we get items ASAP and not the next time one of them changes
ipcRenderer.send(CatalogIpcEvents.INIT);
ipcRenderer.send(catalogInitChannel);
if (isMainFrame) {
ipcRendererOn(catalogEntityRunListener, (event, id: string) => {
@ -120,7 +120,7 @@ export class CatalogEntityRegistry {
const entity = this.categoryRegistry.getEntityForData(item);
if (entity) {
this._entities.set(entity.metadata.uid, entity);
this._entities.set(entity.getId(), entity);
} else {
this.rawEntities.push(item);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,100 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./catalog.module.scss";
import React from "react";
import { action, computed } from "mobx";
import { CatalogEntity } from "../../api/catalog-entity";
import type { ItemObject } from "../../../common/item.store";
import { Badge } from "../badge";
import { navigation } from "../../navigation";
import { searchUrlParam } from "../input";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import type { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
constructor(public entity: T, private registry: CatalogEntityRegistry) {
if (!(entity instanceof CatalogEntity)) {
throw Object.assign(new TypeError("CatalogEntityItem cannot wrap a non-CatalogEntity type"), { typeof: typeof entity, prototype: Object.getPrototypeOf(entity) });
}
}
get kind() {
return this.entity.kind;
}
get apiVersion() {
return this.entity.apiVersion;
}
get name() {
return this.entity.metadata.name;
}
getName() {
return this.entity.metadata.name;
}
get id() {
return this.entity.metadata.uid;
}
getId() {
return this.id;
}
@computed get phase() {
return this.entity.status.phase;
}
get enabled() {
return this.entity.status.enabled ?? true;
}
get labels() {
return KubeObject.stringifyLabels(this.entity.metadata.labels);
}
getLabelBadges(onClick?: React.MouseEventHandler<any>) {
return this.labels
.map(label => (
<Badge
scrollable
className={styles.badge}
key={label}
label={label}
title={label}
onClick={(event) => {
navigation.searchParams.set(searchUrlParam.name, label);
onClick?.(event);
event.stopPropagation();
}}
expandable={false}
/>
));
}
get source() {
return this.entity.metadata.source || "unknown";
}
get searchFields() {
return [
this.name,
this.id,
this.phase,
`source=${this.source}`,
...this.labels,
];
}
onRun() {
this.registry.onRun(this.entity);
}
@action
async onContextMenuOpen(ctx: any) {
return this.entity.onContextMenuOpen(ctx);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -81,14 +81,14 @@ export class EntitySettings extends React.Component<Props> {
<>
<div className="flex items-center pb-8">
<Avatar
title={this.entity.metadata.name}
colorHash={`${this.entity.metadata.name}-${this.entity.metadata.source}`}
title={this.entity.getName()}
colorHash={`${this.entity.getName()}-${this.entity.metadata.source}`}
src={this.entity.spec.icon?.src}
className={styles.settingsAvatar}
size={40}
/>
<div className={styles.entityName}>
{this.entity.metadata.name}
{this.entity.getName()}
</div>
</div>
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>

View File

@ -27,12 +27,11 @@ import enableExtensionInjectable from "./enable-extension/enable-extension.injec
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension/confirm-uninstall-extension.injectable";
import installFromInputInjectable from "./install-from-input/install-from-input.injectable";
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog/install-from-select-file-dialog.injectable";
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
import type { LensExtensionId } from "../../../extensions/lens-extension";
import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable";
import { supportedExtensionFormats } from "./supported-extension-formats";
import extensionInstallationStateStoreInjectable
from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
interface Dependencies {
@ -107,22 +106,15 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
}
}
export const Extensions = withInjectables<Dependencies>(
NonInjectedExtensions,
{
getProps: (di) => ({
userExtensions: di.inject(userExtensionsInjectable),
enableExtension: di.inject(enableExtensionInjectable),
disableExtension: di.inject(disableExtensionInjectable),
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
installFromInput: di.inject(installFromInputInjectable),
installOnDrop: di.inject(installOnDropInjectable),
installFromSelectFileDialog: di.inject(
installFromSelectFileDialogInjectable,
),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
}),
},
);
export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
getProps: (di) => ({
userExtensions: di.inject(userExtensionsInjectable),
enableExtension: di.inject(enableExtensionInjectable),
disableExtension: di.inject(disableExtensionInjectable),
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
installFromInput: di.inject(installFromInputInjectable),
installOnDrop: di.inject(installOnDropInjectable),
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
}),
});

View File

@ -0,0 +1,39 @@
/**
* 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 { 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";
interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void>
directoryForDownloads: string
}
const installFromSelectFileDialog = ({ attemptInstalls, directoryForDownloads }: Dependencies) => async () => {
const { canceled, filePaths } = await requestOpenFilePickingDialog({
defaultPath: directoryForDownloads,
properties: ["openFile", "multiSelections"],
message: `Select extensions to install (formats: ${supportedExtensionFormats.join(", ")}), `,
buttonLabel: "Use configuration",
filters: [{ name: "tarball", extensions: supportedExtensionFormats }],
});
if (!canceled) {
await attemptInstalls(filePaths);
}
};
const installFromSelectFileDialogInjectable = getInjectable({
instantiate: (di) => installFromSelectFileDialog({
attemptInstalls: di.inject(attemptInstallsInjectable),
directoryForDownloads: di.inject(directoryForDownloadsInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default installFromSelectFileDialogInjectable;

View File

@ -1,20 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { installFromSelectFileDialog } from "./install-from-select-file-dialog";
import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable";
import directoryForDownloadsInjectable from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable";
const installFromSelectFileDialogInjectable = getInjectable({
instantiate: (di) =>
installFromSelectFileDialog({
attemptInstalls: di.inject(attemptInstallsInjectable),
directoryForDownloads: di.inject(directoryForDownloadsInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default installFromSelectFileDialogInjectable;

View File

@ -1,29 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { dialog } from "../../../remote-helpers";
import { supportedExtensionFormats } from "../supported-extension-formats";
interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void>
directoryForDownloads: string
}
export const installFromSelectFileDialog =
({ attemptInstalls, directoryForDownloads }: Dependencies) =>
async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
defaultPath: directoryForDownloads,
properties: ["openFile", "multiSelections"],
message: `Select extensions to install (formats: ${supportedExtensionFormats.join(
", ",
)}), `,
buttonLabel: "Use configuration",
filters: [{ name: "tarball", extensions: supportedExtensionFormats }],
});
if (!canceled) {
await attemptInstalls(filePaths);
}
};

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import { SubTitle } from "../layout/sub-title";
import { Icon } from "../icon";
import { Notifications } from "../notifications";
import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager";
import { dialog } from "../../remote-helpers";
import { requestOpenFilePickingDialog } from "../../ipc";
interface Props extends Partial<DialogProps> {
onAddRepo: Function
@ -73,7 +73,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
}
async selectFileDialog(type: FileType, fileFilter: FileFilter) {
const { canceled, filePaths } = await dialog.showOpenDialog({
const { canceled, filePaths } = await requestOpenFilePickingDialog({
defaultPath: this.getFilePath(type),
properties: ["openFile", "showHiddenFiles"],
message: `Select file`,

View File

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

View File

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

View File

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

View File

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

View File

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

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