mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge remote-tracking branch 'origin/master' into eliminate-gst-from-app-paths
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
commit
8d87bd0985
@ -1,6 +1,7 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
<component name="InspectionProjectProfileManager">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="ES6PreferShortImport" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
@ -110,6 +110,10 @@ describe("cluster-store", () => {
|
|||||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockFs.restore();
|
||||||
|
});
|
||||||
|
|
||||||
describe("empty config", () => {
|
describe("empty config", () => {
|
||||||
let getCustomKubeConfigDirectory: (directoryName: string) => string;
|
let getCustomKubeConfigDirectory: (directoryName: string) => string;
|
||||||
|
|
||||||
|
|||||||
@ -239,7 +239,7 @@ describe("HotbarStore", () => {
|
|||||||
const hotbarStore = HotbarStore.getInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.add({ name: "hottest", id: "hottest" });
|
hotbarStore.add({ name: "hottest", id: "hottest" });
|
||||||
hotbarStore.activeHotbarId = "hottest";
|
hotbarStore.setActiveHotbar("hottest");
|
||||||
|
|
||||||
const { error } = logger;
|
const { error } = logger;
|
||||||
const mocked = jest.fn();
|
const mocked = jest.fn();
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CatalogCategory, CatalogEntity, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
import { CatalogCategory, CatalogEntity, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||||
import { productName } from "../vars";
|
import { productName } from "../vars";
|
||||||
import { WeblinkStore } from "../weblink-store";
|
import { WeblinkStore } from "../weblink-store";
|
||||||
@ -86,21 +86,6 @@ export class WebLinkCategory extends CatalogCategory {
|
|||||||
kind: "WebLink",
|
kind: "WebLink",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
public static onAdd?: () => void;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.on("catalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
|
|
||||||
ctx.menuItems.push({
|
|
||||||
icon: "public",
|
|
||||||
title: "Add web link",
|
|
||||||
onClick: () => {
|
|
||||||
WebLinkCategory.onAdd();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogCategoryRegistry.add(new WebLinkCategory());
|
catalogCategoryRegistry.add(new WebLinkCategory());
|
||||||
|
|||||||
@ -19,12 +19,11 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import portForwardStoreInjectable from "../port-forward-store.injectable";
|
import { HotbarStore } from "./hotbar-store";
|
||||||
|
|
||||||
const addPortForwardInjectable = getInjectable({
|
|
||||||
instantiate: (di) => di.inject(portForwardStoreInjectable).add,
|
|
||||||
|
|
||||||
|
const hotbarManagerInjectable = getInjectable({
|
||||||
|
instantiate: () => HotbarStore.getInstance(),
|
||||||
lifecycle: lifecycleEnum.singleton,
|
lifecycle: lifecycleEnum.singleton,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default addPortForwardInjectable;
|
export default hotbarManagerInjectable;
|
||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { action, comparer, observable, makeObservable } from "mobx";
|
import { action, comparer, observable, makeObservable, computed } from "mobx";
|
||||||
import { BaseStore } from "./base-store";
|
import { BaseStore } from "./base-store";
|
||||||
import migrations from "../migrations/hotbar-store";
|
import migrations from "../migrations/hotbar-store";
|
||||||
import { toJS } from "./utils";
|
import { toJS } from "./utils";
|
||||||
@ -27,7 +27,7 @@ import { CatalogEntity } from "./catalog";
|
|||||||
import { catalogEntity } from "../main/catalog-sources/general";
|
import { catalogEntity } from "../main/catalog-sources/general";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { broadcastMessage, HotbarTooManyItems } from "./ipc";
|
import { broadcastMessage, HotbarTooManyItems } from "./ipc";
|
||||||
import { defaultHotbarCells, getEmptyHotbar, Hotbar, HotbarCreateOptions } from "./hotbar-types";
|
import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types";
|
||||||
|
|
||||||
export interface HotbarStoreModel {
|
export interface HotbarStoreModel {
|
||||||
hotbars: Hotbar[];
|
hotbars: Hotbar[];
|
||||||
@ -52,22 +52,40 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeHotbarId() {
|
@computed get activeHotbarId() {
|
||||||
return this._activeHotbarId;
|
return this._activeHotbarId;
|
||||||
}
|
}
|
||||||
|
|
||||||
set activeHotbarId(id: string) {
|
/**
|
||||||
if (this.getById(id)) {
|
* If `hotbar` points to a known hotbar, make it active. Otherwise, ignore
|
||||||
this._activeHotbarId = id;
|
* @param hotbar The hotbar instance, or the index, or its ID
|
||||||
|
*/
|
||||||
|
setActiveHotbar(hotbar: Hotbar | number | string) {
|
||||||
|
if (typeof hotbar === "number") {
|
||||||
|
if (hotbar >= 0 && hotbar < this.hotbars.length) {
|
||||||
|
this._activeHotbarId = this.hotbars[hotbar].id;
|
||||||
|
}
|
||||||
|
} else if (typeof hotbar === "string") {
|
||||||
|
if (this.getById(hotbar)) {
|
||||||
|
this._activeHotbarId = hotbar;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.hotbars.indexOf(hotbar) >= 0) {
|
||||||
|
this._activeHotbarId = hotbar.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hotbarIndex(id: string) {
|
private hotbarIndexById(id: string) {
|
||||||
return this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
return this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeHotbarIndex() {
|
private hotbarIndex(hotbar: Hotbar) {
|
||||||
return this.hotbarIndex(this.activeHotbarId);
|
return this.hotbars.indexOf(hotbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get activeHotbarIndex() {
|
||||||
|
return this.hotbarIndexById(this.activeHotbarId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -87,13 +105,11 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
this.hotbars.forEach(ensureExactHotbarItemLength);
|
this.hotbars.forEach(ensureExactHotbarItemLength);
|
||||||
|
|
||||||
if (data.activeHotbarId) {
|
if (data.activeHotbarId) {
|
||||||
if (this.getById(data.activeHotbarId)) {
|
this.setActiveHotbar(data.activeHotbarId);
|
||||||
this.activeHotbarId = data.activeHotbarId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.activeHotbarId) {
|
if (!this.activeHotbarId) {
|
||||||
this.activeHotbarId = this.hotbars[0].id;
|
this.setActiveHotbar(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,8 +134,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
return this.hotbars.find((hotbar) => hotbar.id === id);
|
return this.hotbars.find((hotbar) => hotbar.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
add = action((data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) => {
|
||||||
add(data: HotbarCreateOptions, { setActive = false } = {}) {
|
|
||||||
const hotbar = getEmptyHotbar(data.name, data.id);
|
const hotbar = getEmptyHotbar(data.name, data.id);
|
||||||
|
|
||||||
this.hotbars.push(hotbar);
|
this.hotbars.push(hotbar);
|
||||||
@ -127,29 +142,29 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
if (setActive) {
|
if (setActive) {
|
||||||
this._activeHotbarId = hotbar.id;
|
this._activeHotbarId = hotbar.id;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
@action
|
setHotbarName = action((id: string, name: string) => {
|
||||||
setHotbarName(id: string, name: string) {
|
|
||||||
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||||
|
|
||||||
if(index < 0) {
|
if (index < 0) {
|
||||||
console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id });
|
return void console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id });
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hotbars[index].name = name;
|
this.hotbars[index].name = name;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
remove = action((hotbar: Hotbar) => {
|
||||||
|
if (this.hotbars.length <= 1) {
|
||||||
|
throw new Error("Cannot remove the last hotbar");
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
remove(hotbar: Hotbar) {
|
|
||||||
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
||||||
|
|
||||||
if (this.activeHotbarId === hotbar.id) {
|
if (this.activeHotbarId === hotbar.id) {
|
||||||
this.activeHotbarId = this.hotbars[0].id;
|
this.setActiveHotbar(0);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addToHotbar(item: CatalogEntity, cellIndex?: number) {
|
addToHotbar(item: CatalogEntity, cellIndex?: number) {
|
||||||
@ -263,7 +278,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
index = hotbarStore.hotbars.length - 1;
|
index = hotbarStore.hotbars.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
|
hotbarStore.setActiveHotbar(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
switchToNext() {
|
switchToNext() {
|
||||||
@ -274,7 +289,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
|
hotbarStore.setActiveHotbar(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -284,6 +299,20 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
isAddedToActive(entity: CatalogEntity) {
|
isAddedToActive(entity: CatalogEntity) {
|
||||||
return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid);
|
return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDisplayLabel(hotbar: Hotbar): string {
|
||||||
|
return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayIndex(hotbar: Hotbar): string {
|
||||||
|
const index = this.hotbarIndex(hotbar);
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
return "??";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${index + 1}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -292,12 +321,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
* @param hotbar The hotbar to modify
|
* @param hotbar The hotbar to modify
|
||||||
*/
|
*/
|
||||||
function ensureExactHotbarItemLength(hotbar: Hotbar) {
|
function ensureExactHotbarItemLength(hotbar: Hotbar) {
|
||||||
if (hotbar.items.length === defaultHotbarCells) {
|
// if there are not enough items
|
||||||
// if we already have `defaultHotbarCells` then we are good to stop
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, keep adding empty entries until full
|
|
||||||
while (hotbar.items.length < defaultHotbarCells) {
|
while (hotbar.items.length < defaultHotbarCells) {
|
||||||
hotbar.items.push(null);
|
hotbar.items.push(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,14 +33,18 @@ export interface HotbarItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Hotbar = Required<HotbarCreateOptions>;
|
export type Hotbar = Required<CreateHotbarData>;
|
||||||
|
|
||||||
export interface HotbarCreateOptions {
|
export interface CreateHotbarData {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
items?: Tuple<HotbarItem | null, typeof defaultHotbarCells>;
|
items?: Tuple<HotbarItem | null, typeof defaultHotbarCells>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateHotbarOptions {
|
||||||
|
setActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard
|
export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard
|
||||||
|
|
||||||
export function getEmptyHotbar(name: string, id: string = uuid.v4()): Hotbar {
|
export function getEmptyHotbar(name: string, id: string = uuid.v4()): Hotbar {
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const dialogShowOpenDialogHandler = "dialog:show-open-dialog";
|
export const dialogShowOpenDialogHandler = "dialog:show-open-dialog";
|
||||||
|
export const catalogEntityRunListener = "catalog-entity:run";
|
||||||
|
|
||||||
export * from "./ipc";
|
export * from "./ipc";
|
||||||
export * from "./invalid-kubeconfig";
|
export * from "./invalid-kubeconfig";
|
||||||
|
|||||||
@ -34,7 +34,9 @@ const electronRemote = (() => {
|
|||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
try {
|
try {
|
||||||
return require("@electron/remote");
|
return require("@electron/remote");
|
||||||
} catch {}
|
} catch {
|
||||||
|
// ignore temp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -19,16 +19,19 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import mockFs from "mock-fs";
|
|
||||||
import { watch } from "chokidar";
|
import { watch } from "chokidar";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import type { ExtensionDiscovery } from "./extension-discovery";
|
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import extensionDiscoveryInjectable from "./extension-discovery.injectable";
|
import * as fse from "fs-extra";
|
||||||
import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable";
|
|
||||||
import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable";
|
|
||||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||||
|
import extensionDiscoveryInjectable from "../extension-discovery/extension-discovery.injectable";
|
||||||
|
import type { ExtensionDiscovery } from "../extension-discovery/extension-discovery";
|
||||||
|
import installExtensionInjectable
|
||||||
|
from "../extension-installer/install-extension/install-extension.injectable";
|
||||||
|
import directoryForUserDataInjectable
|
||||||
|
from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
jest.setTimeout(60_000);
|
jest.setTimeout(60_000);
|
||||||
|
|
||||||
@ -36,6 +39,8 @@ jest.mock("../../common/ipc");
|
|||||||
jest.mock("chokidar", () => ({
|
jest.mock("chokidar", () => ({
|
||||||
watch: jest.fn(),
|
watch: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock("fs-extra");
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
app: {
|
app: {
|
||||||
getVersion: () => "99.99.99",
|
getVersion: () => "99.99.99",
|
||||||
@ -54,6 +59,7 @@ jest.mock("electron", () => ({
|
|||||||
|
|
||||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||||
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
||||||
|
const mockedFse = fse as jest.Mocked<typeof fse>;
|
||||||
|
|
||||||
describe("ExtensionDiscovery", () => {
|
describe("ExtensionDiscovery", () => {
|
||||||
let extensionDiscovery: ExtensionDiscovery;
|
let extensionDiscovery: ExtensionDiscovery;
|
||||||
@ -61,20 +67,10 @@ describe("ExtensionDiscovery", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
mockFs({
|
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||||
[`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]:
|
|
||||||
JSON.stringify({
|
|
||||||
name: "my-extension",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
di.override(installExtensionInjectable, () => () => Promise.resolve());
|
di.override(installExtensionInjectable, () => () => Promise.resolve());
|
||||||
|
|
||||||
di.override(
|
mockFs();
|
||||||
extensionPackageRootDirectoryInjectable,
|
|
||||||
() => "some-extension-packages-root",
|
|
||||||
);
|
|
||||||
|
|
||||||
await di.runSetups();
|
await di.runSetups();
|
||||||
|
|
||||||
@ -85,10 +81,20 @@ describe("ExtensionDiscovery", () => {
|
|||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("emits add for added extension", async (done) => {
|
it("emits add for added extension", async (done) => {
|
||||||
let addHandler: (filePath: string) => void;
|
let addHandler: (filePath: string) => void;
|
||||||
|
|
||||||
|
mockedFse.readJson.mockImplementation((p) => {
|
||||||
|
expect(p).toBe(path.join(os.homedir(), ".k8slens/extensions/my-extension/package.json"));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "my-extension",
|
||||||
|
version: "1.0.0",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
mockedFse.pathExists.mockImplementation(() => true);
|
||||||
|
|
||||||
const mockWatchInstance: any = {
|
const mockWatchInstance: any = {
|
||||||
on: jest.fn((event: string, handler: typeof addHandler) => {
|
on: jest.fn((event: string, handler: typeof addHandler) => {
|
||||||
if (event === "add") {
|
if (event === "add") {
|
||||||
@ -99,42 +105,35 @@ describe("ExtensionDiscovery", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedWatch.mockImplementationOnce(() => mockWatchInstance as any);
|
mockedWatch.mockImplementationOnce(() =>
|
||||||
|
(mockWatchInstance) as any,
|
||||||
|
);
|
||||||
|
|
||||||
// Need to force isLoaded to be true so that the file watching is started
|
// Need to force isLoaded to be true so that the file watching is started
|
||||||
extensionDiscovery.isLoaded = true;
|
extensionDiscovery.isLoaded = true;
|
||||||
|
|
||||||
await extensionDiscovery.watchExtensions();
|
await extensionDiscovery.watchExtensions();
|
||||||
|
|
||||||
extensionDiscovery.events.on("add", (extension) => {
|
extensionDiscovery.events.on("add", extension => {
|
||||||
expect(extension).toEqual({
|
expect(extension).toEqual({
|
||||||
absolutePath: expect.any(String),
|
absolutePath: expect.any(String),
|
||||||
id: path.normalize(
|
id: path.normalize("some-directory-for-user-data/node_modules/my-extension/package.json"),
|
||||||
"some-extension-packages-root/node_modules/my-extension/package.json",
|
|
||||||
),
|
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
isCompatible: false,
|
isCompatible: false,
|
||||||
manifest: {
|
manifest: {
|
||||||
name: "my-extension",
|
name: "my-extension",
|
||||||
|
version: "1.0.0",
|
||||||
},
|
},
|
||||||
manifestPath: path.normalize(
|
manifestPath: path.normalize("some-directory-for-user-data/node_modules/my-extension/package.json"),
|
||||||
"some-extension-packages-root/node_modules/my-extension/package.json",
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
addHandler(
|
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json"));
|
||||||
path.join(
|
|
||||||
extensionDiscovery.localFolderPath,
|
|
||||||
"/my-extension/package.json",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't emit add for added file under extension", async (done) => {
|
it("doesn't emit add for added file under extension", async done => {
|
||||||
let addHandler: (filePath: string) => void;
|
let addHandler: (filePath: string) => void;
|
||||||
|
|
||||||
const mockWatchInstance: any = {
|
const mockWatchInstance: any = {
|
||||||
@ -147,7 +146,9 @@ describe("ExtensionDiscovery", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedWatch.mockImplementationOnce(() => mockWatchInstance as any);
|
mockedWatch.mockImplementationOnce(() =>
|
||||||
|
(mockWatchInstance) as any,
|
||||||
|
);
|
||||||
|
|
||||||
// Need to force isLoaded to be true so that the file watching is started
|
// Need to force isLoaded to be true so that the file watching is started
|
||||||
extensionDiscovery.isLoaded = true;
|
extensionDiscovery.isLoaded = true;
|
||||||
@ -158,12 +159,7 @@ describe("ExtensionDiscovery", () => {
|
|||||||
|
|
||||||
extensionDiscovery.events.on("add", onAdd);
|
extensionDiscovery.events.on("add", onAdd);
|
||||||
|
|
||||||
addHandler(
|
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/node_modules/dep/package.json"));
|
||||||
path.join(
|
|
||||||
extensionDiscovery.localFolderPath,
|
|
||||||
"/my-extension/node_modules/dep/package.json",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(onAdd).not.toHaveBeenCalled();
|
expect(onAdd).not.toHaveBeenCalled();
|
||||||
@ -171,3 +167,4 @@ describe("ExtensionDiscovery", () => {
|
|||||||
}, 10);
|
}, 10);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -271,7 +271,6 @@ export class ExtensionLoader {
|
|||||||
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
|
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
|
||||||
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
|
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
|
||||||
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
|
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
|
||||||
registries.CommandRegistry.getInstance().add(extension.commands),
|
|
||||||
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
|
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -302,7 +301,6 @@ export class ExtensionLoader {
|
|||||||
registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems),
|
registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems),
|
||||||
registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts),
|
registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts),
|
||||||
registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems),
|
registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems),
|
||||||
registries.CommandRegistry.getInstance().add(extension.commands),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import type { TopBarRegistration } from "../renderer/components/layout/top-bar/t
|
|||||||
import type { KubernetesCluster } from "../common/catalog-entities";
|
import type { KubernetesCluster } from "../common/catalog-entities";
|
||||||
import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/welcome-menu-items/welcome-menu-registration";
|
import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/welcome-menu-items/welcome-menu-registration";
|
||||||
import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration";
|
import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration";
|
||||||
|
import type { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands";
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: registries.PageRegistration[] = [];
|
globalPages: registries.PageRegistration[] = [];
|
||||||
@ -42,7 +43,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
|
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
|
||||||
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
|
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
|
||||||
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
||||||
commands: registries.CommandRegistration[] = [];
|
commands: CommandRegistration[] = [];
|
||||||
welcomeMenus: WelcomeMenuRegistration[] = [];
|
welcomeMenus: WelcomeMenuRegistration[] = [];
|
||||||
welcomeBanners: WelcomeBannerRegistration[] = [];
|
welcomeBanners: WelcomeBannerRegistration[] = [];
|
||||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export class EntitySettingRegistry extends BaseRegistry<EntitySettingRegistratio
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemsForKind(kind: string, apiVersion: string, source?: string) {
|
getItemsForKind = (kind: string, apiVersion: string, source?: string) => {
|
||||||
let items = this.getItems().filter((item) => {
|
let items = this.getItems().filter((item) => {
|
||||||
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
||||||
});
|
});
|
||||||
@ -66,5 +66,5 @@ export class EntitySettingRegistry extends BaseRegistry<EntitySettingRegistratio
|
|||||||
}
|
}
|
||||||
|
|
||||||
return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50));
|
return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50));
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,6 @@ export * from "./status-bar-registry";
|
|||||||
export * from "./kube-object-detail-registry";
|
export * from "./kube-object-detail-registry";
|
||||||
export * from "./kube-object-menu-registry";
|
export * from "./kube-object-menu-registry";
|
||||||
export * from "./kube-object-status-registry";
|
export * from "./kube-object-status-registry";
|
||||||
export * from "./command-registry";
|
|
||||||
export * from "./entity-setting-registry";
|
export * from "./entity-setting-registry";
|
||||||
export * from "./catalog-entity-detail-registry";
|
export * from "./catalog-entity-detail-registry";
|
||||||
export * from "./workloads-overview-detail-registry";
|
export * from "./workloads-overview-detail-registry";
|
||||||
|
|||||||
@ -18,8 +18,6 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
|
||||||
|
|
||||||
const logger = {
|
const logger = {
|
||||||
silly: jest.fn(),
|
silly: jest.fn(),
|
||||||
debug: jest.fn(),
|
debug: jest.fn(),
|
||||||
@ -47,6 +45,7 @@ jest.mock("winston", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
@ -69,7 +68,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
|
|
||||||
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
|
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
|
||||||
|
|
||||||
const mockOpts = {
|
mockFs({
|
||||||
"minikube-config.yml": JSON.stringify({
|
"minikube-config.yml": JSON.stringify({
|
||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
clusters: [{
|
clusters: [{
|
||||||
@ -91,9 +90,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
kind: "Config",
|
kind: "Config",
|
||||||
preferences: {},
|
preferences: {},
|
||||||
}),
|
}),
|
||||||
};
|
});
|
||||||
|
|
||||||
mockFs(mockOpts);
|
|
||||||
|
|
||||||
await di.runSetups();
|
await di.runSetups();
|
||||||
|
|
||||||
|
|||||||
@ -22,25 +22,33 @@ import electronAppInjectable from "./electron-app/electron-app.injectable";
|
|||||||
import getElectronAppPathInjectable from "./get-electron-app-path.injectable";
|
import getElectronAppPathInjectable from "./get-electron-app-path.injectable";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
import type { App } from "electron";
|
import type { App } from "electron";
|
||||||
|
import registerChannelInjectable from "../register-channel/register-channel.injectable";
|
||||||
|
|
||||||
describe("get-electron-app-path", () => {
|
describe("get-electron-app-path", () => {
|
||||||
let getElectronAppPath: (name: string) => string | null;
|
let getElectronAppPath: (name: string) => string | null;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
const di = getDiForUnitTesting();
|
const di = getDiForUnitTesting({ doGeneralOverrides: false });
|
||||||
|
|
||||||
const appStub = {
|
const appStub = {
|
||||||
|
name: "some-app-name",
|
||||||
|
|
||||||
getPath: (name: string) => {
|
getPath: (name: string) => {
|
||||||
if (name !== "some-existing-name") {
|
if (name !== "some-existing-name") {
|
||||||
throw new Error("irrelevant");
|
throw new Error("irrelevant");
|
||||||
}
|
}
|
||||||
|
|
||||||
return "some-existing-app-path";
|
return "some-existing-app-path";
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||||
|
setPath: (_, __) => undefined,
|
||||||
} as App;
|
} as App;
|
||||||
|
|
||||||
di.override(electronAppInjectable, () => appStub);
|
di.override(electronAppInjectable, () => appStub);
|
||||||
|
di.override(registerChannelInjectable, () => () => undefined);
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
getElectronAppPath = di.inject(getElectronAppPathInjectable);
|
getElectronAppPath = di.inject(getElectronAppPathInjectable);
|
||||||
});
|
});
|
||||||
@ -54,6 +62,6 @@ describe("get-electron-app-path", () => {
|
|||||||
it("given app path does not exist, when called, returns null", () => {
|
it("given app path does not exist, when called, returns null", () => {
|
||||||
const actual = getElectronAppPath("some-non-existing-name");
|
const actual = getElectronAppPath("some-non-existing-name");
|
||||||
|
|
||||||
expect(actual).toBe(null);
|
expect(actual).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -31,6 +31,6 @@ export const getElectronAppPath =
|
|||||||
try {
|
try {
|
||||||
return app.getPath(name);
|
return app.getPath(name);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -26,11 +26,13 @@ import logger from "../logger";
|
|||||||
import { ensureDir, pathExists } from "fs-extra";
|
import { ensureDir, pathExists } from "fs-extra";
|
||||||
import * as lockFile from "proper-lockfile";
|
import * as lockFile from "proper-lockfile";
|
||||||
import { helmCli } from "../helm/helm-cli";
|
import { helmCli } from "../helm/helm-cli";
|
||||||
import { customRequest } from "../../common/request";
|
|
||||||
import { getBundledKubectlVersion } from "../../common/utils/app-version";
|
import { getBundledKubectlVersion } from "../../common/utils/app-version";
|
||||||
import { isDevelopment, isWindows, isTestEnv } from "../../common/vars";
|
import { isDevelopment, isWindows, isTestEnv } from "../../common/vars";
|
||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
import { defaultPackageMirror, packageMirrors } from "../../common/user-store/preferences-helpers";
|
import { defaultPackageMirror, packageMirrors } from "../../common/user-store/preferences-helpers";
|
||||||
|
import got from "got/dist/source";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import stream from "stream";
|
||||||
|
|
||||||
const bundledVersion = getBundledKubectlVersion();
|
const bundledVersion = getBundledKubectlVersion();
|
||||||
const kubectlMap: Map<string, string> = new Map([
|
const kubectlMap: Map<string, string> = new Map([
|
||||||
@ -51,7 +53,7 @@ const kubectlMap: Map<string, string> = new Map([
|
|||||||
["1.21", bundledVersion],
|
["1.21", bundledVersion],
|
||||||
]);
|
]);
|
||||||
let bundledPath: string;
|
let bundledPath: string;
|
||||||
const initScriptVersionString = "# lens-initscript v3\n";
|
const initScriptVersionString = "# lens-initscript v3";
|
||||||
|
|
||||||
export function bundledKubectlPath(): string {
|
export function bundledKubectlPath(): string {
|
||||||
if (bundledPath) { return bundledPath; }
|
if (bundledPath) { return bundledPath; }
|
||||||
@ -308,99 +310,87 @@ export class Kubectl {
|
|||||||
|
|
||||||
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
const downloadStream = got.stream({ url: this.url, decompress: true });
|
||||||
const stream = customRequest({
|
const fileWriteStream = fs.createWriteStream(this.path, { mode: 0o755 });
|
||||||
url: this.url,
|
const pipeline = promisify(stream.pipeline);
|
||||||
gzip: true,
|
|
||||||
});
|
|
||||||
const file = fs.createWriteStream(this.path);
|
|
||||||
|
|
||||||
stream.on("complete", () => {
|
await pipeline(downloadStream, fileWriteStream);
|
||||||
logger.debug("kubectl binary download finished");
|
logger.debug("kubectl binary download finished");
|
||||||
file.end();
|
|
||||||
});
|
|
||||||
stream.on("error", (error) => {
|
|
||||||
logger.error(error);
|
|
||||||
fs.unlink(this.path, () => {
|
|
||||||
// do nothing
|
|
||||||
});
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
file.on("close", () => {
|
|
||||||
logger.debug("kubectl binary download closed");
|
|
||||||
fs.chmod(this.path, 0o755, (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
stream.pipe(file);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async writeInitScripts() {
|
protected async writeInitScripts() {
|
||||||
const kubectlPath = this.dependencies.userStore.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences());
|
const kubectlPath = this.dependencies.userStore.downloadKubectlBinaries
|
||||||
|
? this.dirname
|
||||||
|
: path.dirname(this.getPathFromPreferences());
|
||||||
|
|
||||||
const helmPath = helmCli.getBinaryDir();
|
const helmPath = helmCli.getBinaryDir();
|
||||||
const fsPromises = fs.promises;
|
|
||||||
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
|
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
|
||||||
let bashScript = `${initScriptVersionString}`;
|
const bashScript = [
|
||||||
|
initScriptVersionString,
|
||||||
bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
|
"tempkubeconfig=\"$KUBECONFIG\"",
|
||||||
bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n";
|
"test -f \"/etc/profile\" && . \"/etc/profile\"",
|
||||||
bashScript += "if test -f \"$HOME/.bash_profile\"; then\n";
|
"if test -f \"$HOME/.bash_profile\"; then",
|
||||||
bashScript += " . \"$HOME/.bash_profile\"\n";
|
" . \"$HOME/.bash_profile\"",
|
||||||
bashScript += "elif test -f \"$HOME/.bash_login\"; then\n";
|
"elif test -f \"$HOME/.bash_login\"; then",
|
||||||
bashScript += " . \"$HOME/.bash_login\"\n";
|
" . \"$HOME/.bash_login\"",
|
||||||
bashScript += "elif test -f \"$HOME/.profile\"; then\n";
|
"elif test -f \"$HOME/.profile\"; then",
|
||||||
bashScript += " . \"$HOME/.profile\"\n";
|
" . \"$HOME/.profile\"",
|
||||||
bashScript += "fi\n";
|
"fi",
|
||||||
bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`;
|
`export PATH="${helmPath}:${kubectlPath}:$PATH"`,
|
||||||
bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
|
'export KUBECONFIG="$tempkubeconfig"',
|
||||||
|
`NO_PROXY=",\${NO_PROXY:-localhost},"`,
|
||||||
bashScript += `NO_PROXY=",\${NO_PROXY:-localhost},"\n`;
|
`NO_PROXY="\${NO_PROXY//,localhost,/,}"`,
|
||||||
bashScript += `NO_PROXY="\${NO_PROXY//,localhost,/,}"\n`;
|
`NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"`,
|
||||||
bashScript += `NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"\n`;
|
`NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"`,
|
||||||
bashScript += `NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"\n`;
|
"export NO_PROXY",
|
||||||
bashScript += "export NO_PROXY\n";
|
"unset tempkubeconfig",
|
||||||
bashScript += "unset tempkubeconfig\n";
|
].join("\n");
|
||||||
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });
|
|
||||||
|
|
||||||
const zshScriptPath = path.join(this.dirname, ".zlogin");
|
const zshScriptPath = path.join(this.dirname, ".zlogin");
|
||||||
let zshScript = `${initScriptVersionString}`;
|
const zshScript = [
|
||||||
|
initScriptVersionString,
|
||||||
|
"tempkubeconfig=\"$KUBECONFIG\"",
|
||||||
|
|
||||||
zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
|
// restore previous ZDOTDIR
|
||||||
// restore previous ZDOTDIR
|
"export ZDOTDIR=\"$OLD_ZDOTDIR\"",
|
||||||
zshScript += "export ZDOTDIR=\"$OLD_ZDOTDIR\"\n";
|
|
||||||
// source all the files
|
|
||||||
zshScript += "test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"\n";
|
|
||||||
zshScript += "test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"\n";
|
|
||||||
zshScript += "test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"\n";
|
|
||||||
zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n";
|
|
||||||
|
|
||||||
// voodoo to replace any previous occurrences of kubectl path in the PATH
|
// source all the files
|
||||||
zshScript += `kubectlpath="${kubectlPath}"\n`;
|
"test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"",
|
||||||
zshScript += `helmpath="${helmPath}"\n`;
|
"test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"",
|
||||||
zshScript += "p=\":$kubectlpath:\"\n";
|
"test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"",
|
||||||
zshScript += "d=\":$PATH:\"\n";
|
"test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"",
|
||||||
zshScript += `d=\${d//$p/:}\n`;
|
|
||||||
zshScript += `d=\${d/#:/}\n`;
|
// voodoo to replace any previous occurrences of kubectl path in the PATH
|
||||||
zshScript += `export PATH="$helmpath:$kubectlpath:\${d/%:/}"\n`;
|
`kubectlpath="${kubectlPath}"`,
|
||||||
zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
|
`helmpath="${helmPath}"`,
|
||||||
zshScript += `NO_PROXY=",\${NO_PROXY:-localhost},"\n`;
|
"p=\":$kubectlpath:\"",
|
||||||
zshScript += `NO_PROXY="\${NO_PROXY//,localhost,/,}"\n`;
|
"d=\":$PATH:\"",
|
||||||
zshScript += `NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"\n`;
|
`d=\${d//$p/:}`,
|
||||||
zshScript += `NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"\n`;
|
`d=\${d/#:/}`,
|
||||||
zshScript += "export NO_PROXY\n";
|
`export PATH="$helmpath:$kubectlpath:\${d/%:/}"`,
|
||||||
zshScript += "unset tempkubeconfig\n";
|
"export KUBECONFIG=\"$tempkubeconfig\"",
|
||||||
zshScript += "unset OLD_ZDOTDIR\n";
|
`NO_PROXY=",\${NO_PROXY:-localhost},"`,
|
||||||
await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 });
|
`NO_PROXY="\${NO_PROXY//,localhost,/,}"`,
|
||||||
|
`NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"`,
|
||||||
|
`NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"`,
|
||||||
|
"export NO_PROXY",
|
||||||
|
"unset tempkubeconfig",
|
||||||
|
"unset OLD_ZDOTDIR",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
fs.promises.writeFile(bashScriptPath, bashScript, { mode: 0o644 }),
|
||||||
|
fs.promises.writeFile(zshScriptPath, zshScript, { mode: 0o644 }),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getDownloadMirror(): string {
|
protected getDownloadMirror(): string {
|
||||||
// MacOS packages are only available from default
|
// MacOS packages are only available from default
|
||||||
|
|
||||||
const mirror = packageMirrors.get(this.dependencies.userStore.downloadMirror)
|
const { url } = packageMirrors.get(this.dependencies.userStore.downloadMirror)
|
||||||
?? packageMirrors.get(defaultPackageMirror);
|
?? packageMirrors.get(defaultPackageMirror);
|
||||||
|
|
||||||
return mirror.url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -216,8 +216,16 @@ export function getAppMenu(
|
|||||||
label: "Command Palette...",
|
label: "Command Palette...",
|
||||||
accelerator: "Shift+CmdOrCtrl+P",
|
accelerator: "Shift+CmdOrCtrl+P",
|
||||||
id: "command-palette",
|
id: "command-palette",
|
||||||
click() {
|
click(_m, _b, event) {
|
||||||
broadcastMessage("command-palette:open");
|
/**
|
||||||
|
* Don't broadcast unless it was triggered by menu iteration so that
|
||||||
|
* there aren't double events in renderer
|
||||||
|
*
|
||||||
|
* NOTE: this `?` is required because of a bug in playwright. https://github.com/microsoft/playwright/issues/10554
|
||||||
|
*/
|
||||||
|
if (!event?.triggeredByAccelerator) {
|
||||||
|
broadcastMessage("command-palette:open");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
|
|||||||
@ -186,7 +186,6 @@ export class Router {
|
|||||||
// Port-forward API (the container port and local forwarding port are obtained from the query parameters)
|
// Port-forward API (the container port and local forwarding port are obtained from the query parameters)
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, this.dependencies.routePortForward);
|
this.router.add({ method: "post", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, this.dependencies.routePortForward);
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForward);
|
this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForward);
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forwards` }, PortForwardRoute.routeAllPortForwards);
|
|
||||||
this.router.add({ method: "delete", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForwardStop);
|
this.router.add({ method: "delete", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForwardStop);
|
||||||
|
|
||||||
// Helm API
|
// Helm API
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
import type { LensApiRequest } from "../router";
|
import type { LensApiRequest } from "../router";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { respondJson } from "../utils/http-responses";
|
import { respondJson } from "../utils/http-responses";
|
||||||
import { PortForward, PortForwardArgs } from "./port-forward/port-forward";
|
import { PortForward } from "./port-forward/port-forward";
|
||||||
|
|
||||||
export class PortForwardRoute {
|
export class PortForwardRoute {
|
||||||
static async routeCurrentPortForward(request: LensApiRequest) {
|
static async routeCurrentPortForward(request: LensApiRequest) {
|
||||||
@ -40,31 +40,6 @@ export class PortForwardRoute {
|
|||||||
respondJson(response, { port: portForward?.forwardPort ?? null });
|
respondJson(response, { port: portForward?.forwardPort ?? null });
|
||||||
}
|
}
|
||||||
|
|
||||||
static async routeAllPortForwards(request: LensApiRequest) {
|
|
||||||
const { query, response } = request;
|
|
||||||
const clusterId = query.get("clusterId");
|
|
||||||
|
|
||||||
let portForwards: PortForwardArgs[] = PortForward.portForwards.map(f => (
|
|
||||||
{
|
|
||||||
clusterId: f.clusterId,
|
|
||||||
kind: f.kind,
|
|
||||||
namespace: f.namespace,
|
|
||||||
name: f.name,
|
|
||||||
port: f.port,
|
|
||||||
forwardPort: f.forwardPort,
|
|
||||||
protocol: f.protocol,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (clusterId) {
|
|
||||||
// filter out any not for this cluster
|
|
||||||
portForwards = portForwards.filter(pf => pf.clusterId == clusterId);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
respondJson(response, { portForwards });
|
|
||||||
}
|
|
||||||
|
|
||||||
static async routeCurrentPortForwardStop(request: LensApiRequest) {
|
static async routeCurrentPortForwardStop(request: LensApiRequest) {
|
||||||
const { params, query, response, cluster } = request;
|
const { params, query, response, cluster } = request;
|
||||||
const { namespace, resourceType, resourceName } = params;
|
const { namespace, resourceType, resourceName } = params;
|
||||||
|
|||||||
@ -32,8 +32,10 @@ describe("tray-menu-items", () => {
|
|||||||
let trayMenuItems: IComputedValue<TrayMenuRegistration[]>;
|
let trayMenuItems: IComputedValue<TrayMenuRegistration[]>;
|
||||||
let extensionsStub: ObservableMap<string, LensMainExtension>;
|
let extensionsStub: ObservableMap<string, LensMainExtension>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
di = getDiForUnitTesting();
|
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
extensionsStub = new ObservableMap();
|
extensionsStub = new ObservableMap();
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { computed, observable, makeObservable, action } from "mobx";
|
import { computed, observable, makeObservable, action } from "mobx";
|
||||||
import { ipcRendererOn } from "../../common/ipc";
|
import { catalogEntityRunListener, ipcRendererOn } from "../../common/ipc";
|
||||||
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
|
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
|
||||||
import "../../common/catalog-entities";
|
import "../../common/catalog-entities";
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
@ -32,6 +32,7 @@ import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
|
|||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { CatalogIpcEvents } from "../../common/ipc/catalog";
|
import { CatalogIpcEvents } from "../../common/ipc/catalog";
|
||||||
import { navigate } from "../navigation";
|
import { navigate } from "../navigation";
|
||||||
|
import { isMainFrame } from "process";
|
||||||
|
|
||||||
export type EntityFilter = (entity: CatalogEntity) => any;
|
export type EntityFilter = (entity: CatalogEntity) => any;
|
||||||
export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>;
|
export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>;
|
||||||
@ -85,6 +86,16 @@ export class CatalogEntityRegistry {
|
|||||||
|
|
||||||
// Make sure that we get items ASAP and not the next time one of them changes
|
// Make sure that we get items ASAP and not the next time one of them changes
|
||||||
ipcRenderer.send(CatalogIpcEvents.INIT);
|
ipcRenderer.send(CatalogIpcEvents.INIT);
|
||||||
|
|
||||||
|
if (isMainFrame) {
|
||||||
|
ipcRendererOn(catalogEntityRunListener, (event, id: string) => {
|
||||||
|
const entity = this.getById(id);
|
||||||
|
|
||||||
|
if (entity) {
|
||||||
|
this.onRun(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {
|
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {
|
||||||
|
|||||||
@ -21,13 +21,14 @@
|
|||||||
|
|
||||||
import { when } from "mobx";
|
import { when } from "mobx";
|
||||||
import { catalogCategoryRegistry } from "../../../common/catalog";
|
import { catalogCategoryRegistry } from "../../../common/catalog";
|
||||||
import { catalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../catalog-entity-registry";
|
||||||
import { isActiveRoute } from "../../../renderer/navigation";
|
import { isActiveRoute } from "../../navigation";
|
||||||
|
import type { GeneralEntity } from "../../../common/catalog-entities";
|
||||||
|
|
||||||
export async function setEntityOnRouteMatch() {
|
export async function setEntityOnRouteMatch() {
|
||||||
await when(() => catalogEntityRegistry.entities.size > 0);
|
await when(() => catalogEntityRegistry.entities.size > 0);
|
||||||
|
|
||||||
const entities = catalogEntityRegistry.getItemsForCategory(catalogCategoryRegistry.getByName("General"));
|
const entities: GeneralEntity[] = catalogEntityRegistry.getItemsForCategory(catalogCategoryRegistry.getByName("General"));
|
||||||
const activeEntity = entities.find(entity => isActiveRoute(entity.spec.path));
|
const activeEntity = entities.find(entity => isActiveRoute(entity.spec.path));
|
||||||
|
|
||||||
if (activeEntity) {
|
if (activeEntity) {
|
||||||
@ -41,24 +41,17 @@ import { WeblinkStore } from "../common/weblink-store";
|
|||||||
import { ThemeStore } from "./theme.store";
|
import { ThemeStore } from "./theme.store";
|
||||||
import { SentryInit } from "../common/sentry";
|
import { SentryInit } from "../common/sentry";
|
||||||
import { registerCustomThemes } from "./components/monaco-editor";
|
import { registerCustomThemes } from "./components/monaco-editor";
|
||||||
import { getDi } from "./components/getDi";
|
import { getDi } from "./getDi";
|
||||||
import { DiContextProvider } from "@ogre-tools/injectable-react";
|
import { DiContextProvider } from "@ogre-tools/injectable-react";
|
||||||
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
||||||
|
import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable";
|
||||||
|
import extensionInstallationStateStoreInjectable from "../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||||
import extensionDiscoveryInjectable
|
|
||||||
from "../extensions/extension-discovery/extension-discovery.injectable";
|
|
||||||
import extensionInstallationStateStoreInjectable
|
|
||||||
from "../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
|
||||||
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
|
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
|
||||||
import userStoreInjectable from "../common/user-store/user-store.injectable";
|
import userStoreInjectable from "../common/user-store/user-store.injectable";
|
||||||
|
|
||||||
import initRootFrameInjectable from "./frames/root-frame/init-root-frame/init-root-frame.injectable";
|
import initRootFrameInjectable from "./frames/root-frame/init-root-frame/init-root-frame.injectable";
|
||||||
import initClusterFrameInjectable
|
import initClusterFrameInjectable from "./frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable";
|
||||||
from "./frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable";
|
import commandOverlayInjectable from "./components/command-palette/command-overlay.injectable";
|
||||||
import createTerminalTabInjectable
|
|
||||||
from "./components/dock/create-terminal-tab/create-terminal-tab.injectable";
|
|
||||||
|
|
||||||
if (process.isMainFrame) {
|
if (process.isMainFrame) {
|
||||||
SentryInit();
|
SentryInit();
|
||||||
@ -93,11 +86,6 @@ export async function bootstrap(di: DependencyInjectionContainer) {
|
|||||||
logger.info(`${logPrefix} initializing Registries`);
|
logger.info(`${logPrefix} initializing Registries`);
|
||||||
initializers.initRegistries();
|
initializers.initRegistries();
|
||||||
|
|
||||||
const createTerminalTab = di.inject(createTerminalTabInjectable);
|
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing CommandRegistry`);
|
|
||||||
initializers.initCommandRegistry(createTerminalTab);
|
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing EntitySettingsRegistry`);
|
logger.info(`${logPrefix} initializing EntitySettingsRegistry`);
|
||||||
initializers.initEntitySettingsRegistry();
|
initializers.initEntitySettingsRegistry();
|
||||||
|
|
||||||
@ -117,7 +105,9 @@ export async function bootstrap(di: DependencyInjectionContainer) {
|
|||||||
initializers.initCatalogCategoryRegistryEntries();
|
initializers.initCatalogCategoryRegistryEntries();
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing Catalog`);
|
logger.info(`${logPrefix} initializing Catalog`);
|
||||||
initializers.initCatalog();
|
initializers.initCatalog({
|
||||||
|
openCommandDialog: di.inject(commandOverlayInjectable).open,
|
||||||
|
});
|
||||||
|
|
||||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import { CatalogCategoryRegistry, CatalogEntity, CatalogEntityActionContext, Cat
|
|||||||
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
|
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
|
||||||
import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store";
|
import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store";
|
||||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
|
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
|
||||||
import catalogEntityRegistryInjectable
|
import catalogEntityRegistryInjectable
|
||||||
|
|||||||
@ -66,22 +66,15 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
|
|||||||
@computed get groups() {
|
@computed get groups() {
|
||||||
const groups: Record<string, CustomResourceDefinition[]> = {};
|
const groups: Record<string, CustomResourceDefinition[]> = {};
|
||||||
|
|
||||||
return this.items.reduce((groups, crd) => {
|
for (const crd of this.items) {
|
||||||
const group = crd.getGroup();
|
(groups[crd.getGroup()] ??= []).push(crd);
|
||||||
|
}
|
||||||
|
|
||||||
if (!groups[group]) groups[group] = [];
|
return groups;
|
||||||
groups[group].push(crd);
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}, groups);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getByGroup(group: string, pluralName: string) {
|
getByGroup(group: string, pluralName: string) {
|
||||||
const crdInGroup = this.groups[group];
|
return this.groups[group]?.find(crd => crd.getPluralName() === pluralName);
|
||||||
|
|
||||||
if (!crdInGroup) return null;
|
|
||||||
|
|
||||||
return crdInGroup.find(crd => crd.getPluralName() === pluralName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getByObject(obj: KubeObject) {
|
getByObject(obj: KubeObject) {
|
||||||
|
|||||||
@ -18,13 +18,15 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
|
||||||
import portForwardStoreInjectable from "../port-forward-store.injectable";
|
|
||||||
|
|
||||||
const modifyPortForwardInjectable = getInjectable({
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
instantiate: (di) => di.inject(portForwardStoreInjectable).modify,
|
import { computed } from "mobx";
|
||||||
|
import { crdStore } from "./crd.store";
|
||||||
|
|
||||||
|
const customResourceDefinitionsInjectable = getInjectable({
|
||||||
|
instantiate: () => computed(() => [...crdStore.items]),
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
lifecycle: lifecycleEnum.singleton,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default modifyPortForwardInjectable;
|
export default customResourceDefinitionsInjectable;
|
||||||
@ -30,13 +30,12 @@ import { ConfirmDialog } from "../../confirm-dialog";
|
|||||||
import { Extensions } from "../extensions";
|
import { Extensions } from "../extensions";
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
||||||
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
|
||||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable";
|
import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
import directoryForDownloadsInjectable
|
import directoryForDownloadsInjectable from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable";
|
||||||
from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable";
|
|
||||||
|
|
||||||
mockWindow();
|
mockWindow();
|
||||||
|
|
||||||
|
|||||||
@ -21,16 +21,14 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { boundMethod, cssNames } from "../../utils";
|
import { boundMethod, cssNames } from "../../utils";
|
||||||
import { openPortForward, PortForwardItem } from "../../port-forward";
|
import { openPortForward, PortForwardItem, PortForwardStore } from "../../port-forward";
|
||||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||||
import { MenuItem } from "../menu";
|
import { MenuItem } from "../menu";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import removePortForwardInjectable
|
import portForwardDialogModelInjectable from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable";
|
||||||
from "../../port-forward/port-forward-store/remove-port-forward/remove-port-forward.injectable";
|
import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable";
|
||||||
import portForwardDialogModelInjectable
|
|
||||||
from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable";
|
|
||||||
|
|
||||||
interface Props extends MenuActionsProps {
|
interface Props extends MenuActionsProps {
|
||||||
portForward: PortForwardItem;
|
portForward: PortForwardItem;
|
||||||
@ -38,7 +36,7 @@ interface Props extends MenuActionsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
removePortForward: (item: PortForwardItem) => Promise<void>,
|
portForwardStore: PortForwardStore,
|
||||||
openPortForwardDialog: (item: PortForwardItem) => void
|
openPortForwardDialog: (item: PortForwardItem) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,12 +46,48 @@ class NonInjectedPortForwardMenu extends React.Component<Props & Dependencies> {
|
|||||||
const { portForward } = this.props;
|
const { portForward } = this.props;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.props.removePortForward(portForward);
|
this.portForwardStore.remove(portForward);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}. The port-forward may still be active.`);
|
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}. The port-forward may still be active.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get portForwardStore() {
|
||||||
|
return this.props.portForwardStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private startPortForwarding = async () => {
|
||||||
|
const { portForward } = this.props;
|
||||||
|
|
||||||
|
const pf = await this.portForwardStore.start(portForward);
|
||||||
|
|
||||||
|
if (pf.status === "Disabled") {
|
||||||
|
const { name, kind, forwardPort } = portForward;
|
||||||
|
|
||||||
|
Notifications.error(`Error occurred starting port-forward, the local port ${forwardPort} may not be available or the ${kind} ${name} may not be reachable`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderStartStopMenuItem() {
|
||||||
|
const { portForward, toolbar } = this.props;
|
||||||
|
|
||||||
|
if (portForward.status === "Active") {
|
||||||
|
return (
|
||||||
|
<MenuItem onClick={() => this.portForwardStore.stop(portForward)}>
|
||||||
|
<Icon material="stop" tooltip="Stop port-forward" interactive={toolbar} />
|
||||||
|
<span className="title">Stop</span>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem onClick={this.startPortForwarding}>
|
||||||
|
<Icon material="play_arrow" tooltip="Start port-forward" interactive={toolbar} />
|
||||||
|
<span className="title">Start</span>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderContent() {
|
renderContent() {
|
||||||
const { portForward, toolbar } = this.props;
|
const { portForward, toolbar } = this.props;
|
||||||
|
|
||||||
@ -61,14 +95,17 @@ class NonInjectedPortForwardMenu extends React.Component<Props & Dependencies> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={() => openPortForward(this.props.portForward)}>
|
{ portForward.status === "Active" &&
|
||||||
<Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser" />
|
<MenuItem onClick={() => openPortForward(portForward)}>
|
||||||
<span className="title">Open</span>
|
<Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser" />
|
||||||
</MenuItem>
|
<span className="title">Open</span>
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
<MenuItem onClick={() => this.props.openPortForwardDialog(portForward)}>
|
<MenuItem onClick={() => this.props.openPortForwardDialog(portForward)}>
|
||||||
<Icon material="edit" tooltip="Change port or protocol" interactive={toolbar} />
|
<Icon material="edit" tooltip="Change port or protocol" interactive={toolbar} />
|
||||||
<span className="title">Edit</span>
|
<span className="title">Edit</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{this.renderStartStopMenuItem()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -93,7 +130,7 @@ export const PortForwardMenu = withInjectables<Dependencies, Props>(
|
|||||||
|
|
||||||
{
|
{
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
removePortForward: di.inject(removePortForwardInjectable),
|
portForwardStore: di.inject(portForwardStoreInjectable),
|
||||||
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
|
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -76,7 +76,7 @@ class NonInjectedPortForwards extends React.Component<Props & Dependencies> {
|
|||||||
showDetails = (item: PortForwardItem) => {
|
showDetails = (item: PortForwardItem) => {
|
||||||
navigation.push(portForwardsURL({
|
navigation.push(portForwardsURL({
|
||||||
params: {
|
params: {
|
||||||
forwardport: String(item.getForwardPort()),
|
forwardport: item.getId(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,20 +24,22 @@ import "./service-port-component.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import type { Service, ServicePort } from "../../../common/k8s-api/endpoints";
|
import type { Service, ServicePort } from "../../../common/k8s-api/endpoints";
|
||||||
import { observable, makeObservable, reaction } from "mobx";
|
import { action, makeObservable, observable, reaction } from "mobx";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { aboutPortForwarding, getPortForward, getPortForwards, openPortForward, PortForwardStore, predictProtocol } from "../../port-forward";
|
|
||||||
import type { ForwardedPort } from "../../port-forward";
|
import type { ForwardedPort } from "../../port-forward";
|
||||||
|
import {
|
||||||
|
aboutPortForwarding,
|
||||||
|
notifyErrorPortForwarding, openPortForward,
|
||||||
|
PortForwardStore,
|
||||||
|
predictProtocol,
|
||||||
|
} from "../../port-forward";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable";
|
import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable";
|
||||||
import removePortForwardInjectable from "../../port-forward/port-forward-store/remove-port-forward/remove-port-forward.injectable";
|
import portForwardDialogModelInjectable from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable";
|
||||||
import portForwardDialogModelInjectable
|
import logger from "../../../common/logger";
|
||||||
from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable";
|
|
||||||
import addPortForwardInjectable
|
|
||||||
from "../../port-forward/port-forward-store/add-port-forward/add-port-forward.injectable";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
service: Service;
|
service: Service;
|
||||||
@ -46,9 +48,7 @@ interface Props {
|
|||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
portForwardStore: PortForwardStore
|
portForwardStore: PortForwardStore
|
||||||
removePortForward: (item: ForwardedPort) => Promise<void>
|
openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean, onClose: () => void }) => void
|
||||||
addPortForward: (item: ForwardedPort) => Promise<number>
|
|
||||||
openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean }) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -56,6 +56,7 @@ class NonInjectedServicePortComponent extends React.Component<Props & Dependenci
|
|||||||
@observable waiting = false;
|
@observable waiting = false;
|
||||||
@observable forwardPort = 0;
|
@observable forwardPort = 0;
|
||||||
@observable isPortForwarded = false;
|
@observable isPortForwarded = false;
|
||||||
|
@observable isActive = false;
|
||||||
|
|
||||||
constructor(props: Props & Dependencies) {
|
constructor(props: Props & Dependencies) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -65,13 +66,18 @@ class NonInjectedServicePortComponent extends React.Component<Props & Dependenci
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => [this.props.portForwardStore.portForwards, this.props.service], () => this.checkExistingPortForwarding()),
|
reaction(() => this.props.service, () => this.checkExistingPortForwarding()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get portForwardStore() {
|
||||||
|
return this.props.portForwardStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async checkExistingPortForwarding() {
|
async checkExistingPortForwarding() {
|
||||||
const { service, port } = this.props;
|
const { service, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
let portForward: ForwardedPort = {
|
||||||
kind: "service",
|
kind: "service",
|
||||||
name: service.getName(),
|
name: service.getName(),
|
||||||
namespace: service.getNs(),
|
namespace: service.getNs(),
|
||||||
@ -79,57 +85,66 @@ class NonInjectedServicePortComponent extends React.Component<Props & Dependenci
|
|||||||
forwardPort: this.forwardPort,
|
forwardPort: this.forwardPort,
|
||||||
};
|
};
|
||||||
|
|
||||||
let activePort: number;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
activePort = await getPortForward(portForward) ?? 0;
|
portForward = await this.portForwardStore.getPortForward(portForward);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.isPortForwarded = false;
|
this.isPortForwarded = false;
|
||||||
|
this.isActive = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.forwardPort = activePort;
|
this.forwardPort = portForward.forwardPort;
|
||||||
this.isPortForwarded = activePort ? true : false;
|
this.isPortForwarded = true;
|
||||||
|
this.isActive = portForward.status === "Active";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async portForward() {
|
async portForward() {
|
||||||
const { service, port } = this.props;
|
const { service, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
let portForward: ForwardedPort = {
|
||||||
kind: "service",
|
kind: "service",
|
||||||
name: service.getName(),
|
name: service.getName(),
|
||||||
namespace: service.getNs(),
|
namespace: service.getNs(),
|
||||||
port: port.port,
|
port: port.port,
|
||||||
forwardPort: this.forwardPort,
|
forwardPort: this.forwardPort,
|
||||||
protocol: predictProtocol(port.name),
|
protocol: predictProtocol(port.name),
|
||||||
|
status: "Active",
|
||||||
};
|
};
|
||||||
|
|
||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// determine how many port-forwards are already active
|
// determine how many port-forwards already exist
|
||||||
const { length } = await getPortForwards();
|
const { length } = this.portForwardStore.getPortForwards();
|
||||||
|
|
||||||
this.forwardPort = await this.props.addPortForward(portForward);
|
if (!this.isPortForwarded) {
|
||||||
|
portForward = await this.portForwardStore.add(portForward);
|
||||||
|
} else if (!this.isActive) {
|
||||||
|
portForward = await this.portForwardStore.start(portForward);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.forwardPort) {
|
this.forwardPort = portForward.forwardPort;
|
||||||
portForward.forwardPort = this.forwardPort;
|
|
||||||
|
if (portForward.status === "Active") {
|
||||||
openPortForward(portForward);
|
openPortForward(portForward);
|
||||||
this.isPortForwarded = true;
|
|
||||||
|
|
||||||
// if this is the first port-forward show the about notification
|
// if this is the first port-forward show the about notification
|
||||||
if (!length) {
|
if (!length) {
|
||||||
aboutPortForwarding();
|
aboutPortForwarding();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
notifyErrorPortForwarding(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
logger.error("[SERVICE-PORT-COMPONENT]:", error, portForward);
|
||||||
this.checkExistingPortForwarding();
|
|
||||||
} finally {
|
} finally {
|
||||||
|
this.checkExistingPortForwarding();
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async stopPortForward() {
|
async stopPortForward() {
|
||||||
const { service, port } = this.props;
|
const { service, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
const portForward: ForwardedPort = {
|
||||||
@ -143,12 +158,12 @@ class NonInjectedServicePortComponent extends React.Component<Props & Dependenci
|
|||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.props.removePortForward(portForward);
|
await this.portForwardStore.remove(portForward);
|
||||||
this.isPortForwarded = false;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
|
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
|
||||||
this.checkExistingPortForwarding();
|
|
||||||
} finally {
|
} finally {
|
||||||
|
this.checkExistingPortForwarding();
|
||||||
|
this.forwardPort = 0;
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,7 +171,7 @@ class NonInjectedServicePortComponent extends React.Component<Props & Dependenci
|
|||||||
render() {
|
render() {
|
||||||
const { port, service } = this.props;
|
const { port, service } = this.props;
|
||||||
|
|
||||||
const portForwardAction = async () => {
|
const portForwardAction = action(async () => {
|
||||||
if (this.isPortForwarded) {
|
if (this.isPortForwarded) {
|
||||||
await this.stopPortForward();
|
await this.stopPortForward();
|
||||||
} else {
|
} else {
|
||||||
@ -169,16 +184,16 @@ class NonInjectedServicePortComponent extends React.Component<Props & Dependenci
|
|||||||
protocol: predictProtocol(port.name),
|
protocol: predictProtocol(port.name),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.props.openPortForwardDialog(portForward, { openInBrowser: true });
|
this.props.openPortForwardDialog(portForward, { openInBrowser: true, onClose: () => this.checkExistingPortForwarding() });
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
||||||
<span title="Open in a browser" onClick={() => this.portForward()}>
|
<span title="Open in a browser" onClick={() => this.portForward()}>
|
||||||
{port.toString()}
|
{port.toString()}
|
||||||
</span>
|
</span>
|
||||||
<Button primary onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop" : "Forward..."} </Button>
|
<Button primary onClick={portForwardAction}> {this.isPortForwarded ? (this.isActive ? "Stop/Remove" : "Remove") : "Forward..."} </Button>
|
||||||
{this.waiting && (
|
{this.waiting && (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
)}
|
)}
|
||||||
@ -193,8 +208,6 @@ export const ServicePortComponent = withInjectables<Dependencies, Props>(
|
|||||||
{
|
{
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
portForwardStore: di.inject(portForwardStoreInjectable),
|
portForwardStore: di.inject(portForwardStoreInjectable),
|
||||||
removePortForward: di.inject(removePortForwardInjectable),
|
|
||||||
addPortForward: di.inject(addPortForwardInjectable),
|
|
||||||
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
|
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -34,6 +34,7 @@ $service-status-color-list: (
|
|||||||
|
|
||||||
$port-forward-status-color-list: (
|
$port-forward-status-color-list: (
|
||||||
active: var(--colorOk),
|
active: var(--colorOk),
|
||||||
|
disabled: var(--colorSoftError)
|
||||||
);
|
);
|
||||||
|
|
||||||
@mixin port-forward-status-colors {
|
@mixin port-forward-status-colors {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import { ClusterRoleBindingDialog } from "../dialog";
|
|||||||
import { clusterRolesStore } from "../../+cluster-roles/store";
|
import { clusterRolesStore } from "../../+cluster-roles/store";
|
||||||
import { ClusterRole } from "../../../../../common/k8s-api/endpoints";
|
import { ClusterRole } from "../../../../../common/k8s-api/endpoints";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
|
||||||
import { DiRender, renderFor } from "../../../test-utils/renderFor";
|
import { DiRender, renderFor } from "../../../test-utils/renderFor";
|
||||||
|
|
||||||
jest.mock("../../+cluster-roles/store");
|
jest.mock("../../+cluster-roles/store");
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import React from "react";
|
|||||||
import { clusterRolesStore } from "../../+cluster-roles/store";
|
import { clusterRolesStore } from "../../+cluster-roles/store";
|
||||||
import { ClusterRole } from "../../../../../common/k8s-api/endpoints";
|
import { ClusterRole } from "../../../../../common/k8s-api/endpoints";
|
||||||
import { RoleBindingDialog } from "../dialog";
|
import { RoleBindingDialog } from "../dialog";
|
||||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
|
||||||
import type { DiRender } from "../../../test-utils/renderFor";
|
import type { DiRender } from "../../../test-utils/renderFor";
|
||||||
import { renderFor } from "../../../test-utils/renderFor";
|
import { renderFor } from "../../../test-utils/renderFor";
|
||||||
import directoryForUserDataInjectable
|
import directoryForUserDataInjectable
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import { screen } from "@testing-library/react";
|
|||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { defaultWidth, Welcome } from "../welcome";
|
import { defaultWidth, Welcome } from "../welcome";
|
||||||
import { computed } from "mobx";
|
import { computed } from "mobx";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
import type { DiRender } from "../../test-utils/renderFor";
|
import type { DiRender } from "../../test-utils/renderFor";
|
||||||
import { renderFor } from "../../test-utils/renderFor";
|
import { renderFor } from "../../test-utils/renderFor";
|
||||||
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
@ -46,8 +46,10 @@ describe("<Welcome/>", () => {
|
|||||||
let di: ConfigurableDependencyInjectionContainer;
|
let di: ConfigurableDependencyInjectionContainer;
|
||||||
let welcomeBannersStub: WelcomeBannerRegistration[];
|
let welcomeBannersStub: WelcomeBannerRegistration[];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
di = getDiForUnitTesting();
|
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
render = renderFor(di);
|
render = renderFor(di);
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import "@testing-library/jest-dom/extend-expect";
|
|||||||
import { fireEvent } from "@testing-library/react";
|
import { fireEvent } from "@testing-library/react";
|
||||||
import type { IToleration } from "../../../../common/k8s-api/workload-kube-object";
|
import type { IToleration } from "../../../../common/k8s-api/workload-kube-object";
|
||||||
import { PodTolerations } from "../pod-tolerations";
|
import { PodTolerations } from "../pod-tolerations";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
import directoryForLensLocalStorageInjectable
|
import directoryForLensLocalStorageInjectable
|
||||||
from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||||
|
|||||||
@ -24,20 +24,24 @@ import "./pod-container-port.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import type { Pod } from "../../../common/k8s-api/endpoints";
|
import type { Pod } from "../../../common/k8s-api/endpoints";
|
||||||
import { observable, makeObservable, reaction } from "mobx";
|
import { action, makeObservable, observable, reaction } from "mobx";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { aboutPortForwarding, getPortForward, getPortForwards, openPortForward, PortForwardStore, predictProtocol } from "../../port-forward";
|
|
||||||
import type { ForwardedPort } from "../../port-forward";
|
import type { ForwardedPort } from "../../port-forward";
|
||||||
|
import {
|
||||||
|
aboutPortForwarding,
|
||||||
|
notifyErrorPortForwarding,
|
||||||
|
openPortForward,
|
||||||
|
PortForwardStore,
|
||||||
|
predictProtocol,
|
||||||
|
} from "../../port-forward";
|
||||||
|
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable";
|
import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable";
|
||||||
import removePortForwardInjectable from "../../port-forward/port-forward-store/remove-port-forward/remove-port-forward.injectable";
|
import portForwardDialogModelInjectable from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable";
|
||||||
import portForwardDialogModelInjectable
|
import logger from "../../../common/logger";
|
||||||
from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable";
|
|
||||||
import addPortForwardInjectable
|
|
||||||
from "../../port-forward/port-forward-store/add-port-forward/add-port-forward.injectable";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pod: Pod;
|
pod: Pod;
|
||||||
@ -45,14 +49,12 @@ interface Props {
|
|||||||
name?: string;
|
name?: string;
|
||||||
containerPort: number;
|
containerPort: number;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
portForwardStore: PortForwardStore;
|
portForwardStore: PortForwardStore;
|
||||||
removePortForward: (item: ForwardedPort) => Promise<void>;
|
openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean, onClose: () => void }) => void;
|
||||||
addPortForward: (item: ForwardedPort) => Promise<number>;
|
|
||||||
openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean }) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -60,6 +62,7 @@ class NonInjectedPodContainerPort extends React.Component<Props & Dependencies>
|
|||||||
@observable waiting = false;
|
@observable waiting = false;
|
||||||
@observable forwardPort = 0;
|
@observable forwardPort = 0;
|
||||||
@observable isPortForwarded = false;
|
@observable isPortForwarded = false;
|
||||||
|
@observable isActive = false;
|
||||||
|
|
||||||
constructor(props: Props & Dependencies) {
|
constructor(props: Props & Dependencies) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -69,13 +72,18 @@ class NonInjectedPodContainerPort extends React.Component<Props & Dependencies>
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => [this.props.portForwardStore.portForwards, this.props.pod], () => this.checkExistingPortForwarding()),
|
reaction(() => this.props.pod, () => this.checkExistingPortForwarding()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get portForwardStore() {
|
||||||
|
return this.props.portForwardStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async checkExistingPortForwarding() {
|
async checkExistingPortForwarding() {
|
||||||
const { pod, port } = this.props;
|
const { pod, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
let portForward: ForwardedPort = {
|
||||||
kind: "pod",
|
kind: "pod",
|
||||||
name: pod.getName(),
|
name: pod.getName(),
|
||||||
namespace: pod.getNs(),
|
namespace: pod.getNs(),
|
||||||
@ -83,57 +91,64 @@ class NonInjectedPodContainerPort extends React.Component<Props & Dependencies>
|
|||||||
forwardPort: this.forwardPort,
|
forwardPort: this.forwardPort,
|
||||||
};
|
};
|
||||||
|
|
||||||
let activePort: number;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
activePort = await getPortForward(portForward) ?? 0;
|
portForward = await this.portForwardStore.getPortForward(portForward);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.isPortForwarded = false;
|
this.isPortForwarded = false;
|
||||||
|
this.isActive = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.forwardPort = activePort;
|
this.forwardPort = portForward.forwardPort;
|
||||||
this.isPortForwarded = activePort ? true : false;
|
this.isPortForwarded = true;
|
||||||
|
this.isActive = portForward.status === "Active";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async portForward() {
|
async portForward() {
|
||||||
const { pod, port } = this.props;
|
const { pod, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
let portForward: ForwardedPort = {
|
||||||
kind: "pod",
|
kind: "pod",
|
||||||
name: pod.getName(),
|
name: pod.getName(),
|
||||||
namespace: pod.getNs(),
|
namespace: pod.getNs(),
|
||||||
port: port.containerPort,
|
port: port.containerPort,
|
||||||
forwardPort: this.forwardPort,
|
forwardPort: this.forwardPort,
|
||||||
protocol: predictProtocol(port.name),
|
protocol: predictProtocol(port.name),
|
||||||
|
status: "Active",
|
||||||
};
|
};
|
||||||
|
|
||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// determine how many port-forwards are already active
|
// determine how many port-forwards already exist
|
||||||
const { length } = await getPortForwards();
|
const { length } = this.portForwardStore.getPortForwards();
|
||||||
|
|
||||||
this.forwardPort = await this.props.addPortForward(portForward);
|
if (!this.isPortForwarded) {
|
||||||
|
portForward = await this.portForwardStore.add(portForward);
|
||||||
|
} else if (!this.isActive) {
|
||||||
|
portForward = await this.portForwardStore.start(portForward);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.forwardPort) {
|
if (portForward.status === "Active") {
|
||||||
portForward.forwardPort = this.forwardPort;
|
|
||||||
openPortForward(portForward);
|
openPortForward(portForward);
|
||||||
this.isPortForwarded = true;
|
|
||||||
|
|
||||||
// if this is the first port-forward show the about notification
|
// if this is the first port-forward show the about notification
|
||||||
if (!length) {
|
if (!length) {
|
||||||
aboutPortForwarding();
|
aboutPortForwarding();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
notifyErrorPortForwarding(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
logger.error("[POD-CONTAINER-PORT]:", error, portForward);
|
||||||
this.checkExistingPortForwarding();
|
|
||||||
} finally {
|
} finally {
|
||||||
|
this.checkExistingPortForwarding();
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async stopPortForward() {
|
async stopPortForward() {
|
||||||
const { pod, port } = this.props;
|
const { pod, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
const portForward: ForwardedPort = {
|
||||||
@ -147,12 +162,12 @@ class NonInjectedPodContainerPort extends React.Component<Props & Dependencies>
|
|||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.props.removePortForward(portForward);
|
await this.portForwardStore.remove(portForward);
|
||||||
this.isPortForwarded = false;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
|
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
|
||||||
this.checkExistingPortForwarding();
|
|
||||||
} finally {
|
} finally {
|
||||||
|
this.checkExistingPortForwarding();
|
||||||
|
this.forwardPort = 0;
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,7 +177,7 @@ class NonInjectedPodContainerPort extends React.Component<Props & Dependencies>
|
|||||||
const { name, containerPort, protocol } = port;
|
const { name, containerPort, protocol } = port;
|
||||||
const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`;
|
const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`;
|
||||||
|
|
||||||
const portForwardAction = async () => {
|
const portForwardAction = action(async () => {
|
||||||
if (this.isPortForwarded) {
|
if (this.isPortForwarded) {
|
||||||
await this.stopPortForward();
|
await this.stopPortForward();
|
||||||
} else {
|
} else {
|
||||||
@ -175,16 +190,16 @@ class NonInjectedPodContainerPort extends React.Component<Props & Dependencies>
|
|||||||
protocol: predictProtocol(port.name),
|
protocol: predictProtocol(port.name),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.props.openPortForwardDialog(portForward, { openInBrowser: true });
|
this.props.openPortForwardDialog(portForward, { openInBrowser: true, onClose: () => this.checkExistingPortForwarding() });
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
||||||
<span title="Open in a browser" onClick={() => this.portForward()}>
|
<span title="Open in a browser" onClick={() => this.portForward()}>
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
<Button primary onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop" : "Forward..."} </Button>
|
<Button primary onClick={portForwardAction}> {this.isPortForwarded ? (this.isActive ? "Stop/Remove" : "Remove") : "Forward..."} </Button>
|
||||||
{this.waiting && (
|
{this.waiting && (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
)}
|
)}
|
||||||
@ -199,8 +214,6 @@ export const PodContainerPort = withInjectables<Dependencies, Props>(
|
|||||||
{
|
{
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
portForwardStore: di.inject(portForwardStoreInjectable),
|
portForwardStore: di.inject(portForwardStoreInjectable),
|
||||||
removePortForward: di.inject(removePortForwardInjectable),
|
|
||||||
addPortForward: di.inject(addPortForwardInjectable),
|
|
||||||
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
|
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -19,40 +19,49 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { computed } from "mobx";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import { computed, IComputedValue } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { broadcastMessage, catalogEntityRunListener } from "../../../common/ipc";
|
||||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import { CommandOverlay } from "../command-palette";
|
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||||
import { Select } from "../select";
|
import { Select } from "../select";
|
||||||
|
|
||||||
@observer
|
interface Dependencies {
|
||||||
export class ActivateEntityCommand extends React.Component {
|
closeCommandOverlay: () => void;
|
||||||
@computed get options() {
|
entities: IComputedValue<CatalogEntity[]>;
|
||||||
return catalogEntityRegistry.items.map(entity => ({
|
|
||||||
label: `${entity.kind}: ${entity.getName()}`,
|
|
||||||
value: entity,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect(entity: CatalogEntity): void {
|
|
||||||
catalogEntityRegistry.onRun(entity);
|
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
menuPortalTarget={null}
|
|
||||||
onChange={(v) => this.onSelect(v.value)}
|
|
||||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
|
||||||
menuIsOpen={true}
|
|
||||||
options={this.options}
|
|
||||||
autoFocus={true}
|
|
||||||
escapeClearsValue={false}
|
|
||||||
placeholder="Activate entity ..."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NonInjectedActivateEntityCommand = observer(({ closeCommandOverlay, entities }: Dependencies) => {
|
||||||
|
const options = entities.get().map(entity => ({
|
||||||
|
label: `${entity.kind}: ${entity.getName()}`,
|
||||||
|
value: entity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onSelect = (entity: CatalogEntity): void => {
|
||||||
|
broadcastMessage(catalogEntityRunListener, entity.getId());
|
||||||
|
closeCommandOverlay();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
menuPortalTarget={null}
|
||||||
|
onChange={(v) => onSelect(v.value)}
|
||||||
|
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
||||||
|
menuIsOpen={true}
|
||||||
|
options={options}
|
||||||
|
autoFocus={true}
|
||||||
|
escapeClearsValue={false}
|
||||||
|
placeholder="Activate entity ..."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ActivateEntityCommand = withInjectables<Dependencies>(NonInjectedActivateEntityCommand, {
|
||||||
|
getProps: di => ({
|
||||||
|
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
|
||||||
|
entities: computed(() => [...catalogEntityRegistry.items]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@ -21,21 +21,26 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { CommandOverlay } from "../command-palette";
|
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { isUrl } from "../input/input_validators";
|
import { isUrl } from "../input/input_validators";
|
||||||
import { WeblinkStore } from "../../../common/weblink-store";
|
import { WeblinkStore } from "../../../common/weblink-store";
|
||||||
import { computed, makeObservable, observable } from "mobx";
|
import { computed, makeObservable, observable } from "mobx";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
closeCommandOverlay: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class WeblinkAddCommand extends React.Component {
|
class NonInjectedWeblinkAddCommand extends React.Component<Dependencies> {
|
||||||
@observable url = "";
|
@observable url = "";
|
||||||
@observable nameHidden = true;
|
@observable nameHidden = true;
|
||||||
@observable dirty = false;
|
@observable dirty = false;
|
||||||
|
|
||||||
constructor(props: {}) {
|
constructor(props: Dependencies) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,8 +60,7 @@ export class WeblinkAddCommand extends React.Component {
|
|||||||
name: name || this.url,
|
name: name || this.url,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
});
|
});
|
||||||
|
this.props.closeCommandOverlay();
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get showValidation() {
|
@computed get showValidation() {
|
||||||
@ -100,3 +104,10 @@ export class WeblinkAddCommand extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const WeblinkAddCommand = withInjectables<Dependencies>(NonInjectedWeblinkAddCommand, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@ -22,19 +22,31 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
|
||||||
import { CommandOverlay } from "../command-palette";
|
|
||||||
import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command";
|
import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||||
|
|
||||||
export const ActiveHotbarName = observer(() => {
|
interface Dependencies {
|
||||||
return (
|
openCommandOverlay: (component: React.ReactElement) => void;
|
||||||
<div
|
activeHotbarName: () => string | undefined;
|
||||||
className="flex items-center"
|
}
|
||||||
data-testid="current-hotbar-name"
|
|
||||||
onClick={() => CommandOverlay.open(<HotbarSwitchCommand />)}
|
const NonInjectedActiveHotbarName = observer(({ openCommandOverlay, activeHotbarName }: Dependencies) => (
|
||||||
>
|
<div
|
||||||
<Icon material="bookmarks" className="mr-2" size={14}/>
|
className="flex items-center"
|
||||||
{HotbarStore.getInstance().getActive()?.name}
|
data-testid="current-hotbar-name"
|
||||||
</div>
|
onClick={() => openCommandOverlay(<HotbarSwitchCommand />)}
|
||||||
);
|
>
|
||||||
|
<Icon material="bookmarks" className="mr-2" size={14} />
|
||||||
|
{activeHotbarName()}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const ActiveHotbarName = withInjectables<Dependencies>(NonInjectedActiveHotbarName, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
activeHotbarName: () => di.inject(hotbarManagerInjectable).getActive()?.name,
|
||||||
|
openCommandOverlay: di.inject(commandOverlayInjectable).open,
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -21,21 +21,19 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import { render, fireEvent } from "@testing-library/react";
|
import { fireEvent } from "@testing-library/react";
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { BottomBar } from "./bottom-bar";
|
import { BottomBar } from "./bottom-bar";
|
||||||
import { StatusBarRegistry } from "../../../extensions/registries";
|
import { StatusBarRegistry } from "../../../extensions/registries";
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
|
||||||
import { CommandOverlay } from "../command-palette";
|
|
||||||
import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command";
|
import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command";
|
||||||
import { ActiveHotbarName } from "./active-hotbar-name";
|
import { ActiveHotbarName } from "./active-hotbar-name";
|
||||||
import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing";
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import { DiRender, renderFor } from "../test-utils/renderFor";
|
||||||
|
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
|
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||||
|
import { getEmptyHotbar } from "../../../common/hotbar-types";
|
||||||
|
|
||||||
jest.mock("../command-palette", () => ({
|
|
||||||
CommandOverlay: {
|
|
||||||
open: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
app: {
|
app: {
|
||||||
@ -53,26 +51,36 @@ jest.mock("electron", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const foobarHotbar = getEmptyHotbar("foobar");
|
||||||
|
|
||||||
describe("<BottomBar />", () => {
|
describe("<BottomBar />", () => {
|
||||||
|
let di: DependencyInjectionContainer;
|
||||||
|
let render: DiRender;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
|
|
||||||
|
|
||||||
await dis.runSetups();
|
|
||||||
|
|
||||||
const mockOpts = {
|
const mockOpts = {
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"test-store.json": JSON.stringify({}),
|
"test-store.json": JSON.stringify({}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
|
|
||||||
|
di.override(hotbarManagerInjectable, () => ({
|
||||||
|
getActive: () => foobarHotbar,
|
||||||
|
} as any));
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
StatusBarRegistry.createInstance();
|
StatusBarRegistry.createInstance();
|
||||||
HotbarStore.createInstance();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
StatusBarRegistry.resetInstance();
|
StatusBarRegistry.resetInstance();
|
||||||
HotbarStore.resetInstance();
|
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -82,24 +90,20 @@ describe("<BottomBar />", () => {
|
|||||||
expect(container).toBeInstanceOf(HTMLElement);
|
expect(container).toBeInstanceOf(HTMLElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders w/o errors when .getItems() returns unexpected (not type compliant) data", async () => {
|
it.each([
|
||||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => undefined);
|
undefined,
|
||||||
expect(() => render(<BottomBar />)).not.toThrow();
|
"hello",
|
||||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => "hello");
|
6,
|
||||||
expect(() => render(<BottomBar />)).not.toThrow();
|
null,
|
||||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => 6);
|
[],
|
||||||
expect(() => render(<BottomBar />)).not.toThrow();
|
[{}],
|
||||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => null);
|
{},
|
||||||
expect(() => render(<BottomBar />)).not.toThrow();
|
])("renders w/o errors when .getItems() returns not type compliant (%p)", val => {
|
||||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => []);
|
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => val);
|
||||||
expect(() => render(<BottomBar />)).not.toThrow();
|
|
||||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [{}]);
|
|
||||||
expect(() => render(<BottomBar />)).not.toThrow();
|
|
||||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => { return {};});
|
|
||||||
expect(() => render(<BottomBar />)).not.toThrow();
|
expect(() => render(<BottomBar />)).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders items [{item: React.ReactNode}] (4.0.0-rc.1)", async () => {
|
it("renders items [{item: React.ReactNode}] (4.0.0-rc.1)", () => {
|
||||||
const testId = "testId";
|
const testId = "testId";
|
||||||
const text = "heee";
|
const text = "heee";
|
||||||
|
|
||||||
@ -108,10 +112,10 @@ describe("<BottomBar />", () => {
|
|||||||
]);
|
]);
|
||||||
const { getByTestId } = render(<BottomBar />);
|
const { getByTestId } = render(<BottomBar />);
|
||||||
|
|
||||||
expect(await getByTestId(testId)).toHaveTextContent(text);
|
expect(getByTestId(testId)).toHaveTextContent(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders items [{item: () => React.ReactNode}] (4.0.0-rc.1+)", async () => {
|
it("renders items [{item: () => React.ReactNode}] (4.0.0-rc.1+)", () => {
|
||||||
const testId = "testId";
|
const testId = "testId";
|
||||||
const text = "heee";
|
const text = "heee";
|
||||||
|
|
||||||
@ -120,33 +124,25 @@ describe("<BottomBar />", () => {
|
|||||||
]);
|
]);
|
||||||
const { getByTestId } = render(<BottomBar />);
|
const { getByTestId } = render(<BottomBar />);
|
||||||
|
|
||||||
expect(await getByTestId(testId)).toHaveTextContent(text);
|
expect(getByTestId(testId)).toHaveTextContent(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("show default hotbar name", () => {
|
it("shows active hotbar name", () => {
|
||||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
||||||
{ item: () => <ActiveHotbarName/> },
|
{ item: () => <ActiveHotbarName/> },
|
||||||
]);
|
]);
|
||||||
const { getByTestId } = render(<BottomBar />);
|
const { getByTestId } = render(<BottomBar />);
|
||||||
|
|
||||||
expect(getByTestId("current-hotbar-name")).toHaveTextContent("default");
|
expect(getByTestId("current-hotbar-name")).toHaveTextContent("foobar");
|
||||||
});
|
|
||||||
|
|
||||||
it("show active hotbar name", () => {
|
|
||||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
|
||||||
{ item: () => <ActiveHotbarName/> },
|
|
||||||
]);
|
|
||||||
const { getByTestId } = render(<BottomBar />);
|
|
||||||
|
|
||||||
HotbarStore.getInstance().add({
|
|
||||||
id: "new",
|
|
||||||
name: "new",
|
|
||||||
}, { setActive: true });
|
|
||||||
|
|
||||||
expect(getByTestId("current-hotbar-name")).toHaveTextContent("new");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens command palette on click", () => {
|
it("opens command palette on click", () => {
|
||||||
|
const mockOpen = jest.fn();
|
||||||
|
|
||||||
|
di.override(commandOverlayInjectable, () => ({
|
||||||
|
open: mockOpen,
|
||||||
|
}) as any);
|
||||||
|
|
||||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
||||||
{ item: () => <ActiveHotbarName/> },
|
{ item: () => <ActiveHotbarName/> },
|
||||||
]);
|
]);
|
||||||
@ -155,7 +151,8 @@ describe("<BottomBar />", () => {
|
|||||||
|
|
||||||
fireEvent.click(activeHotbar);
|
fireEvent.click(activeHotbar);
|
||||||
|
|
||||||
expect(CommandOverlay.open).toHaveBeenCalledWith(<HotbarSwitchCommand />);
|
|
||||||
|
expect(mockOpen).toHaveBeenCalledWith(<HotbarSwitchCommand />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sort positioned items properly", () => {
|
it("sort positioned items properly", () => {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ import * as routes from "../../../common/routes";
|
|||||||
import { DeleteClusterDialog } from "../delete-cluster-dialog";
|
import { DeleteClusterDialog } from "../delete-cluster-dialog";
|
||||||
import { reaction } from "mobx";
|
import { reaction } from "mobx";
|
||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync";
|
import { setEntityOnRouteMatch } from "../../api/helpers/general-active-sync";
|
||||||
import { catalogURL, getPreviousTabUrl } from "../../../common/routes";
|
import { catalogURL, getPreviousTabUrl } from "../../../common/routes";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import { TopBar } from "../layout/top-bar/top-bar";
|
import { TopBar } from "../layout/top-bar/top-bar";
|
||||||
|
|||||||
@ -21,68 +21,100 @@
|
|||||||
|
|
||||||
|
|
||||||
import "./command-container.scss";
|
import "./command-container.scss";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Dialog } from "../dialog";
|
import { Dialog } from "../dialog";
|
||||||
import { ipcRendererOn } from "../../../common/ipc";
|
|
||||||
import { CommandDialog } from "./command-dialog";
|
import { CommandDialog } from "./command-dialog";
|
||||||
import type { ClusterId } from "../../../common/cluster-types";
|
import type { ClusterId } from "../../../common/cluster-types";
|
||||||
|
import commandOverlayInjectable, { CommandOverlay } from "./command-overlay.injectable";
|
||||||
|
import { isMac } from "../../../common/vars";
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import { CommandRegistration, CommandRegistry } from "../../../extensions/registries/command-registry";
|
import { broadcastMessage, ipcRendererOn } from "../../../common/ipc";
|
||||||
import { CommandOverlay } from "./command-overlay";
|
import { getMatchedClusterId } from "../../navigation";
|
||||||
|
import type { Disposer } from "../../utils";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import windowAddEventListenerInjectable from "../../window/event-listener.injectable";
|
||||||
|
|
||||||
export interface CommandContainerProps {
|
export interface CommandContainerProps {
|
||||||
clusterId?: ClusterId;
|
clusterId?: ClusterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
addWindowEventListener: <K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions) => Disposer;
|
||||||
|
commandOverlay: CommandOverlay,
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class CommandContainer extends React.Component<CommandContainerProps> {
|
class NonInjectedCommandContainer extends React.Component<CommandContainerProps & Dependencies> {
|
||||||
private escHandler(event: KeyboardEvent) {
|
private escHandler(event: KeyboardEvent) {
|
||||||
|
const { commandOverlay } = this.props;
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
CommandOverlay.close();
|
commandOverlay.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private findCommandById(commandId: string) {
|
handleCommandPalette = () => {
|
||||||
return CommandRegistry.getInstance().getItems().find((command) => command.id === commandId);
|
const { commandOverlay } = this.props;
|
||||||
}
|
const clusterIsActive = getMatchedClusterId() !== undefined;
|
||||||
|
|
||||||
private runCommand(command: CommandRegistration) {
|
if (clusterIsActive) {
|
||||||
command.action({
|
broadcastMessage(`command-palette:${catalogEntityRegistry.activeEntity.getId()}:open`);
|
||||||
entity: catalogEntityRegistry.activeEntity,
|
} else {
|
||||||
});
|
commandOverlay.open(<CommandDialog />);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onKeyboardShortcut(action: () => void) {
|
||||||
|
return ({ key, shiftKey, ctrlKey, altKey, metaKey }: KeyboardEvent) => {
|
||||||
|
const ctrlOrCmd = isMac ? metaKey && !ctrlKey : !metaKey && ctrlKey;
|
||||||
|
|
||||||
|
if (key === "p" && shiftKey && ctrlOrCmd && !altKey) {
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.clusterId) {
|
const { clusterId, addWindowEventListener, commandOverlay } = this.props;
|
||||||
ipcRendererOn(`command-palette:run-action:${this.props.clusterId}`, (event, commandId: string) => {
|
|
||||||
const command = this.findCommandById(commandId);
|
|
||||||
|
|
||||||
if (command) {
|
const action = clusterId
|
||||||
this.runCommand(command);
|
? () => commandOverlay.open(<CommandDialog />)
|
||||||
}
|
: this.handleCommandPalette;
|
||||||
});
|
const ipcChannel = clusterId
|
||||||
} else {
|
? `command-palette:${clusterId}:open`
|
||||||
ipcRendererOn("command-palette:open", () => {
|
: "command-palette:open";
|
||||||
CommandOverlay.open(<CommandDialog />);
|
|
||||||
});
|
disposeOnUnmount(this, [
|
||||||
}
|
ipcRendererOn(ipcChannel, action),
|
||||||
window.addEventListener("keyup", (e) => this.escHandler(e), true);
|
addWindowEventListener("keydown", this.onKeyboardShortcut(action)),
|
||||||
|
addWindowEventListener("keyup", (e) => this.escHandler(e), true),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { commandOverlay } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
isOpen={CommandOverlay.isOpen}
|
isOpen={commandOverlay.isOpen}
|
||||||
animated={true}
|
animated={true}
|
||||||
onClose={CommandOverlay.close}
|
onClose={commandOverlay.close}
|
||||||
modal={false}
|
modal={false}
|
||||||
>
|
>
|
||||||
<div id="command-container">
|
<div id="command-container">
|
||||||
{CommandOverlay.component}
|
{commandOverlay.component}
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CommandContainer = withInjectables<Dependencies, CommandContainerProps>(NonInjectedCommandContainer, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
addWindowEventListener: di.inject(windowAddEventListenerInjectable),
|
||||||
|
commandOverlay: di.inject(commandOverlayInjectable),
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@ -21,108 +21,107 @@
|
|||||||
|
|
||||||
|
|
||||||
import { Select } from "../select";
|
import { Select } from "../select";
|
||||||
import { computed, makeObservable, observable } from "mobx";
|
import type { IComputedValue } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { CommandRegistry } from "../../../extensions/registries/command-registry";
|
import commandOverlayInjectable from "./command-overlay.injectable";
|
||||||
import { CommandOverlay } from "./command-overlay";
|
|
||||||
import { broadcastMessage } from "../../../common/ipc";
|
|
||||||
import { navigate } from "../../navigation";
|
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
|
||||||
import type { CatalogEntity } from "../../../common/catalog";
|
import type { CatalogEntity } from "../../../common/catalog";
|
||||||
import { clusterViewURL } from "../../../common/routes";
|
import { navigate } from "../../navigation";
|
||||||
|
import { broadcastMessage } from "../../../common/ipc";
|
||||||
|
import { IpcRendererNavigationEvents } from "../../navigation/events";
|
||||||
|
import type { RegisteredCommand } from "./registered-commands/commands";
|
||||||
|
import { iter } from "../../utils";
|
||||||
|
import { orderBy } from "lodash";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import registeredCommandsInjectable from "./registered-commands/registered-commands.injectable";
|
||||||
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
|
|
||||||
@observer
|
interface Dependencies {
|
||||||
export class CommandDialog extends React.Component {
|
commands: IComputedValue<Map<string, RegisteredCommand>>;
|
||||||
@observable menuIsOpen = true;
|
activeEntity?: CatalogEntity;
|
||||||
@observable searchValue: any = undefined;
|
closeCommandOverlay: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: {}) {
|
const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeCommandOverlay }: Dependencies) => {
|
||||||
super(props);
|
const [searchValue, setSearchValue] = useState("");
|
||||||
makeObservable(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get activeEntity(): CatalogEntity | undefined {
|
const executeAction = (commandId: string) => {
|
||||||
return catalogEntityRegistry.activeEntity;
|
const command = commands.get().get(commandId);
|
||||||
}
|
|
||||||
|
|
||||||
@computed get options() {
|
|
||||||
const registry = CommandRegistry.getInstance();
|
|
||||||
|
|
||||||
const context = {
|
|
||||||
entity: this.activeEntity,
|
|
||||||
};
|
|
||||||
|
|
||||||
return registry.getItems().filter((command) => {
|
|
||||||
if (command.scope === "entity" && !this.activeEntity) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return command.isActive?.(context) ?? true;
|
|
||||||
} catch(e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.map((command) => ({
|
|
||||||
value: command.id,
|
|
||||||
label: command.title,
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.label > b.label ? 1 : -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onChange(value: string) {
|
|
||||||
const registry = CommandRegistry.getInstance();
|
|
||||||
const command = registry.getItems().find((cmd) => cmd.id === value);
|
|
||||||
|
|
||||||
if (!command) {
|
if (!command) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CommandOverlay.close();
|
closeCommandOverlay();
|
||||||
|
command.action({
|
||||||
|
entity: activeEntity,
|
||||||
|
navigate: (url, opts = {}) => {
|
||||||
|
const { forceRootFrame = false } = opts;
|
||||||
|
|
||||||
if (command.scope === "global") {
|
if (forceRootFrame) {
|
||||||
command.action({
|
broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, url);
|
||||||
entity: this.activeEntity,
|
} else {
|
||||||
});
|
navigate(url);
|
||||||
} else if(this.activeEntity) {
|
}
|
||||||
navigate(clusterViewURL({
|
},
|
||||||
params: {
|
});
|
||||||
clusterId: this.activeEntity.metadata.uid,
|
} catch (error) {
|
||||||
},
|
|
||||||
}));
|
|
||||||
broadcastMessage(`command-palette:run-action:${this.activeEntity.metadata.uid}`, command.id);
|
|
||||||
}
|
|
||||||
} catch(error) {
|
|
||||||
console.error("[COMMAND-DIALOG] failed to execute command", command.id, error);
|
console.error("[COMMAND-DIALOG] failed to execute command", command.id, error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
const context = {
|
||||||
return (
|
entity: activeEntity,
|
||||||
<Select
|
};
|
||||||
menuPortalTarget={null}
|
const activeCommands = iter.filter(commands.get().values(), command => {
|
||||||
onChange={v => this.onChange(v.value)}
|
try {
|
||||||
components={{
|
return command.isActive(context);
|
||||||
DropdownIndicator: null,
|
} catch (error) {
|
||||||
IndicatorSeparator: null,
|
console.error(`[COMMAND-DIALOG]: isActive for ${command.id} threw an error, defaulting to false`, error);
|
||||||
}}
|
}
|
||||||
menuIsOpen={this.menuIsOpen}
|
|
||||||
options={this.options}
|
return false;
|
||||||
autoFocus={true}
|
});
|
||||||
escapeClearsValue={false}
|
const options = Array.from(activeCommands, ({ id, title }) => ({
|
||||||
data-test-id="command-palette-search"
|
value: id,
|
||||||
placeholder="Type a command or search…"
|
label: typeof title === "function"
|
||||||
onInputChange={(newValue, { action }) => {
|
? title(context)
|
||||||
if (action === "input-change") {
|
: title,
|
||||||
this.searchValue = newValue;
|
}));
|
||||||
}
|
|
||||||
}}
|
// Make sure the options are in the correct order
|
||||||
inputValue={this.searchValue}
|
orderBy(options, "label", "asc");
|
||||||
/>
|
|
||||||
);
|
return (
|
||||||
}
|
<Select
|
||||||
}
|
menuPortalTarget={null}
|
||||||
|
onChange={v => executeAction(v.value)}
|
||||||
|
components={{
|
||||||
|
DropdownIndicator: null,
|
||||||
|
IndicatorSeparator: null,
|
||||||
|
}}
|
||||||
|
menuIsOpen
|
||||||
|
options={options}
|
||||||
|
autoFocus={true}
|
||||||
|
escapeClearsValue={false}
|
||||||
|
data-test-id="command-palette-search"
|
||||||
|
placeholder="Type a command or search…"
|
||||||
|
onInputChange={(newValue, { action }) => {
|
||||||
|
if (action === "input-change") {
|
||||||
|
setSearchValue(newValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
inputValue={searchValue}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CommandDialog = withInjectables<Dependencies>(NonInjectedCommandDialog, {
|
||||||
|
getProps: di => ({
|
||||||
|
commands: di.inject(registeredCommandsInjectable),
|
||||||
|
// TODO: replace with injection
|
||||||
|
activeEntity: catalogEntityRegistry.activeEntity,
|
||||||
|
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@ -19,29 +19,37 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export class CommandOverlay {
|
export class CommandOverlay {
|
||||||
static #component = observable.box<React.ReactElement | null>(null, { deep: false });
|
#component = observable.box<React.ReactElement | null>(null, { deep: false });
|
||||||
|
|
||||||
static get isOpen(): boolean {
|
get isOpen(): boolean {
|
||||||
return Boolean(CommandOverlay.#component.get());
|
return Boolean(this.#component.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
static open(component: React.ReactElement) {
|
open = (component: React.ReactElement) => {
|
||||||
if (!React.isValidElement(component)) {
|
if (!React.isValidElement(component)) {
|
||||||
throw new TypeError("CommandOverlay.open must be passed a valid ReactElement");
|
throw new TypeError("CommandOverlay.open must be passed a valid ReactElement");
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandOverlay.#component.set(component);
|
this.#component.set(component);
|
||||||
}
|
};
|
||||||
|
|
||||||
static close() {
|
close = () => {
|
||||||
CommandOverlay.#component.set(null);
|
this.#component.set(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
static get component(): React.ReactElement | null {
|
get component(): React.ReactElement | null {
|
||||||
return CommandOverlay.#component.get();
|
return this.#component.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commandOverlayInjectable = getInjectable({
|
||||||
|
instantiate: () => new CommandOverlay(),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default commandOverlayInjectable;
|
||||||
@ -21,4 +21,4 @@
|
|||||||
|
|
||||||
export * from "./command-container";
|
export * from "./command-container";
|
||||||
export * from "./command-dialog";
|
export * from "./command-dialog";
|
||||||
export * from "./command-overlay";
|
export * from "./command-overlay.injectable";
|
||||||
|
|||||||
@ -19,34 +19,55 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Extensions API -> Commands
|
import type { CatalogEntity } from "../../../../common/catalog";
|
||||||
|
|
||||||
import { BaseRegistry } from "./base-registry";
|
|
||||||
import type { LensExtension } from "../lens-extension";
|
|
||||||
import type { CatalogEntity } from "../../common/catalog";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context given to commands when executed
|
||||||
|
*/
|
||||||
export interface CommandContext {
|
export interface CommandContext {
|
||||||
entity?: CatalogEntity;
|
entity?: CatalogEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CommandActionNavigateOptions {
|
||||||
|
/**
|
||||||
|
* If `true` then the navigate will only navigate on the root frame and not
|
||||||
|
* within a cluster
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
forceRootFrame?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandActionContext extends CommandContext {
|
||||||
|
navigate: (url: string, opts?: CommandActionNavigateOptions) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CommandRegistration {
|
export interface CommandRegistration {
|
||||||
|
/**
|
||||||
|
* The ID of the command, must be globally unique
|
||||||
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
|
||||||
scope: "entity" | "global";
|
/**
|
||||||
action: (context: CommandContext) => void;
|
* The display name of the command in the command pallet
|
||||||
|
*/
|
||||||
|
title: string | ((context: CommandContext) => string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `isActive` instead since there is always an entity active
|
||||||
|
*/
|
||||||
|
scope?: "global" | "entity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function to run when this command is selected
|
||||||
|
*/
|
||||||
|
action: (context: CommandActionContext) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that determines if the command is active.
|
||||||
|
*
|
||||||
|
* @default () => true
|
||||||
|
*/
|
||||||
isActive?: (context: CommandContext) => boolean;
|
isActive?: (context: CommandContext) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommandRegistry extends BaseRegistry<CommandRegistration> {
|
export type RegisteredCommand = Required<Omit<CommandRegistration, "scope">>;
|
||||||
add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) {
|
|
||||||
const itemArray = [items].flat();
|
|
||||||
|
|
||||||
const newIds = itemArray.map((item) => item.id);
|
|
||||||
const currentIds = this.getItems().map((item) => item.id);
|
|
||||||
|
|
||||||
const filteredIds = newIds.filter((id) => !currentIds.includes(id));
|
|
||||||
const filteredItems = itemArray.filter((item) => filteredIds.includes(item.id));
|
|
||||||
|
|
||||||
return super.add(filteredItems, extension);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import * as routes from "../../../../common/routes";
|
||||||
|
import { EntitySettingRegistry, RegisteredEntitySetting } from "../../../../extensions/registries";
|
||||||
|
import { HotbarAddCommand } from "../../hotbar/hotbar-add-command";
|
||||||
|
import { HotbarRemoveCommand } from "../../hotbar/hotbar-remove-command";
|
||||||
|
import { HotbarSwitchCommand } from "../../hotbar/hotbar-switch-command";
|
||||||
|
import { HotbarRenameCommand } from "../../hotbar/hotbar-rename-command";
|
||||||
|
import { ActivateEntityCommand } from "../../activate-entity-command";
|
||||||
|
import type { CommandContext, CommandRegistration } from "./commands";
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import commandOverlayInjectable from "../command-overlay.injectable";
|
||||||
|
import createTerminalTabInjectable
|
||||||
|
from "../../dock/create-terminal-tab/create-terminal-tab.injectable";
|
||||||
|
import type { DockTabCreate } from "../../dock/dock-store/dock.store";
|
||||||
|
|
||||||
|
export function isKubernetesClusterActive(context: CommandContext): boolean {
|
||||||
|
return context.entity?.kind === "KubernetesCluster";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
openCommandDialog: (component: React.ReactElement) => void;
|
||||||
|
getEntitySettingItems: (kind: string, apiVersion: string, source?: string) => RegisteredEntitySetting[];
|
||||||
|
createTerminalTab: () => DockTabCreate
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInternalCommands({ openCommandDialog, getEntitySettingItems, createTerminalTab }: Dependencies): CommandRegistration[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "app.showPreferences",
|
||||||
|
title: "Preferences: Open",
|
||||||
|
action: ({ navigate }) => navigate(routes.preferencesURL(), {
|
||||||
|
forceRootFrame: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewHelmCharts",
|
||||||
|
title: "Cluster: View Helm Charts",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.helmChartsURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewHelmReleases",
|
||||||
|
title: "Cluster: View Helm Releases",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.releaseURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewConfigMaps",
|
||||||
|
title: "Cluster: View ConfigMaps",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.configMapsURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewSecrets",
|
||||||
|
title: "Cluster: View Secrets",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.secretsURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewResourceQuotas",
|
||||||
|
title: "Cluster: View ResourceQuotas",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.resourceQuotaURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewLimitRanges",
|
||||||
|
title: "Cluster: View LimitRanges",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.limitRangeURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewHorizontalPodAutoscalers",
|
||||||
|
title: "Cluster: View HorizontalPodAutoscalers (HPA)",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.hpaURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewPodDisruptionBudget",
|
||||||
|
title: "Cluster: View PodDisruptionBudgets",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.pdbURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewServices",
|
||||||
|
title: "Cluster: View Services",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.servicesURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewEndpoints",
|
||||||
|
title: "Cluster: View Endpoints",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.endpointURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewIngresses",
|
||||||
|
title: "Cluster: View Ingresses",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.ingressURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewNetworkPolicies",
|
||||||
|
title: "Cluster: View NetworkPolicies",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.networkPoliciesURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewNodes",
|
||||||
|
title: "Cluster: View Nodes",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.nodesURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewPods",
|
||||||
|
title: "Cluster: View Pods",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.podsURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewDeployments",
|
||||||
|
title: "Cluster: View Deployments",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.deploymentsURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewDaemonSets",
|
||||||
|
title: "Cluster: View DaemonSets",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.daemonSetsURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewStatefulSets",
|
||||||
|
title: "Cluster: View StatefulSets",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.statefulSetsURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewJobs",
|
||||||
|
title: "Cluster: View Jobs",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.jobsURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewCronJobs",
|
||||||
|
title: "Cluster: View CronJobs",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.cronJobsURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.viewCustomResourceDefinitions",
|
||||||
|
title: "Cluster: View Custom Resource Definitions",
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(routes.crdURL()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "entity.viewSettings",
|
||||||
|
title: ({ entity }) => `${entity.kind}/${entity.getName()}: View Settings`,
|
||||||
|
action: ({ entity, navigate }) => navigate(`/entity/${entity.getId()}/settings`, {
|
||||||
|
forceRootFrame: true,
|
||||||
|
}),
|
||||||
|
isActive: ({ entity }) => {
|
||||||
|
if (!entity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getEntitySettingItems(entity.kind, entity.apiVersion, entity.metadata.source).length > 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cluster.openTerminal",
|
||||||
|
title: "Cluster: Open terminal",
|
||||||
|
action: () => createTerminalTab(),
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hotbar.switchHotbar",
|
||||||
|
title: "Hotbar: Switch ...",
|
||||||
|
action: () => openCommandDialog(<HotbarSwitchCommand />),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hotbar.addHotbar",
|
||||||
|
title: "Hotbar: Add Hotbar ...",
|
||||||
|
action: () => openCommandDialog(<HotbarAddCommand />),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hotbar.removeHotbar",
|
||||||
|
title: "Hotbar: Remove Hotbar ...",
|
||||||
|
action: () => openCommandDialog(<HotbarRemoveCommand />),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hotbar.renameHotbar",
|
||||||
|
title: "Hotbar: Rename Hotbar ...",
|
||||||
|
action: () => openCommandDialog(<HotbarRenameCommand />),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "catalog.searchEntities",
|
||||||
|
title: "Catalog: Activate Entity ...",
|
||||||
|
action: () => openCommandDialog(<ActivateEntityCommand />),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const internalCommandsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => getInternalCommands({
|
||||||
|
openCommandDialog: di.inject(commandOverlayInjectable).open,
|
||||||
|
getEntitySettingItems: EntitySettingRegistry
|
||||||
|
.getInstance()
|
||||||
|
.getItemsForKind,
|
||||||
|
createTerminalTab: di.inject(createTerminalTabInjectable),
|
||||||
|
}),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default internalCommandsInjectable;
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { computed, IComputedValue } from "mobx";
|
||||||
|
import type { CustomResourceDefinition } from "../../../../common/k8s-api/endpoints";
|
||||||
|
import customResourceDefinitionsInjectable from "../../+custom-resources/custom-resources.injectable";
|
||||||
|
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||||
|
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||||
|
import type { CommandRegistration, RegisteredCommand } from "./commands";
|
||||||
|
import internalCommandsInjectable, { isKubernetesClusterActive } from "./internal-commands.injectable";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
extensions: IComputedValue<LensRendererExtension[]>;
|
||||||
|
customResourceDefinitions: IComputedValue<CustomResourceDefinition[]>;
|
||||||
|
internalCommands: CommandRegistration[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const instantiateRegisteredCommands = ({ extensions, customResourceDefinitions, internalCommands }: Dependencies) => computed(() => {
|
||||||
|
const result = new Map<string, RegisteredCommand>();
|
||||||
|
const commands = [
|
||||||
|
...internalCommands,
|
||||||
|
...extensions.get().flatMap(e => e.commands),
|
||||||
|
...customResourceDefinitions.get().map((command): CommandRegistration => ({
|
||||||
|
id: `cluster.view.${command.getResourceKind()}`,
|
||||||
|
title: `Cluster: View ${command.getResourceKind()}`,
|
||||||
|
isActive: isKubernetesClusterActive,
|
||||||
|
action: ({ navigate }) => navigate(command.getResourceUrl()),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { scope, isActive = () => true, ...command } of commands) {
|
||||||
|
if (!result.has(command.id)) {
|
||||||
|
result.set(command.id, { ...command, isActive });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const registeredCommandsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => instantiateRegisteredCommands({
|
||||||
|
extensions: di.inject(rendererExtensionsInjectable),
|
||||||
|
customResourceDefinitions: di.inject(customResourceDefinitionsInjectable),
|
||||||
|
internalCommands: di.inject(internalCommandsInjectable),
|
||||||
|
}),
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default registeredCommandsInjectable;
|
||||||
@ -28,7 +28,7 @@ import { DockStore, DockTab, TabKind } from "../dock-store/dock.store";
|
|||||||
import { noop } from "../../../utils";
|
import { noop } from "../../../utils";
|
||||||
import { ThemeStore } from "../../../theme.store";
|
import { ThemeStore } from "../../../theme.store";
|
||||||
import { UserStore } from "../../../../common/user-store";
|
import { UserStore } from "../../../../common/user-store";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||||
import type { DiRender } from "../../test-utils/renderFor";
|
import type { DiRender } from "../../test-utils/renderFor";
|
||||||
import { renderFor } from "../../test-utils/renderFor";
|
import { renderFor } from "../../test-utils/renderFor";
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import { dockerPod, deploymentPod1 } from "./pod.mock";
|
|||||||
import { ThemeStore } from "../../../theme.store";
|
import { ThemeStore } from "../../../theme.store";
|
||||||
import { UserStore } from "../../../../common/user-store";
|
import { UserStore } from "../../../../common/user-store";
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
import type { DiRender } from "../../test-utils/renderFor";
|
import type { DiRender } from "../../test-utils/renderFor";
|
||||||
import { renderFor } from "../../test-utils/renderFor";
|
import { renderFor } from "../../test-utils/renderFor";
|
||||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { Pod } from "../../../../common/k8s-api/endpoints";
|
|||||||
import { ThemeStore } from "../../../theme.store";
|
import { ThemeStore } from "../../../theme.store";
|
||||||
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
|
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
|
||||||
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable";
|
import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable";
|
||||||
import type { LogTabStore } from "../log-tab-store/log-tab.store";
|
import type { LogTabStore } from "../log-tab-store/log-tab.store";
|
||||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||||
|
|||||||
@ -21,13 +21,17 @@
|
|||||||
|
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { HotbarRemoveCommand } from "../hotbar-remove-command";
|
import { HotbarRemoveCommand } from "../hotbar-remove-command";
|
||||||
import { render, fireEvent } from "@testing-library/react";
|
import { fireEvent } from "@testing-library/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
|
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
|
import hotbarManagerInjectable from "../../../../common/hotbar-store.injectable";
|
||||||
import { ThemeStore } from "../../../theme.store";
|
import { ThemeStore } from "../../../theme.store";
|
||||||
|
import { ConfirmDialog } from "../../confirm-dialog";
|
||||||
|
import type { HotbarStore } from "../../../../common/hotbar-store";
|
||||||
import { UserStore } from "../../../../common/user-store";
|
import { UserStore } from "../../../../common/user-store";
|
||||||
import { Notifications } from "../../notifications";
|
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
|
||||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
|
||||||
const mockHotbars: { [id: string]: any } = {
|
const mockHotbars: { [id: string]: any } = {
|
||||||
@ -38,50 +42,69 @@ const mockHotbars: { [id: string]: any } = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock("../../../../common/hotbar-store", () => ({
|
|
||||||
HotbarStore: {
|
|
||||||
getInstance: () => ({
|
|
||||||
hotbars: [mockHotbars["1"]],
|
|
||||||
getById: (id: string) => mockHotbars[id],
|
|
||||||
remove: () => {},
|
|
||||||
hotbarIndex: () => 0,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("<HotbarRemoveCommand />", () => {
|
describe("<HotbarRemoveCommand />", () => {
|
||||||
beforeEach(async () => {
|
let di: DependencyInjectionContainer;
|
||||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
let render: DiRender;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
mockFs();
|
mockFs();
|
||||||
|
|
||||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||||
|
|
||||||
await di.runSetups();
|
render = renderFor(di);
|
||||||
|
|
||||||
UserStore.createInstance();
|
UserStore.createInstance();
|
||||||
ThemeStore.createInstance();
|
ThemeStore.createInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
UserStore.resetInstance();
|
|
||||||
ThemeStore.resetInstance();
|
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
|
ThemeStore.resetInstance();
|
||||||
|
UserStore.resetInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders w/o errors", () => {
|
it("renders w/o errors", async () => {
|
||||||
const { container } = render(<HotbarRemoveCommand/>);
|
di.override(hotbarManagerInjectable, () => ({
|
||||||
|
hotbars: [mockHotbars["1"]],
|
||||||
|
getById: (id: string) => mockHotbars[id],
|
||||||
|
remove: () => {
|
||||||
|
},
|
||||||
|
hotbarIndex: () => 0,
|
||||||
|
getDisplayLabel: () => "1: Default",
|
||||||
|
}) as any as HotbarStore);
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
|
const { container } = render(<HotbarRemoveCommand />);
|
||||||
|
|
||||||
expect(container).toBeInstanceOf(HTMLElement);
|
expect(container).toBeInstanceOf(HTMLElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("displays error notification if user tries to remove last hotbar", () => {
|
it("calls remove if you click on the entry", async () => {
|
||||||
const spy = jest.spyOn(Notifications, "error");
|
const removeMock = jest.fn();
|
||||||
const { getByText } = render(<HotbarRemoveCommand/>);
|
|
||||||
|
di.override(hotbarManagerInjectable, () => ({
|
||||||
|
hotbars: [mockHotbars["1"]],
|
||||||
|
getById: (id: string) => mockHotbars[id],
|
||||||
|
remove: removeMock,
|
||||||
|
hotbarIndex: () => 0,
|
||||||
|
getDisplayLabel: () => "1: Default",
|
||||||
|
}) as any as HotbarStore);
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
|
const { getByText } = render(
|
||||||
|
<>
|
||||||
|
<HotbarRemoveCommand />
|
||||||
|
<ConfirmDialog />
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.click(getByText("1: Default"));
|
fireEvent.click(getByText("1: Default"));
|
||||||
|
fireEvent.click(getByText("Remove Hotbar"));
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(removeMock).toHaveBeenCalled();
|
||||||
spy.mockRestore();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -21,44 +21,53 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
|
||||||
import { CommandOverlay } from "../command-palette";
|
|
||||||
import { Input, InputValidator } from "../input";
|
import { Input, InputValidator } from "../input";
|
||||||
|
import type { CreateHotbarData, CreateHotbarOptions } from "../../../common/hotbar-types";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||||
|
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
|
||||||
|
import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable";
|
||||||
|
|
||||||
export const uniqueHotbarName: InputValidator = {
|
interface Dependencies {
|
||||||
condition: ({ required }) => required,
|
closeCommandOverlay: () => void;
|
||||||
message: () => "Hotbar with this name already exists",
|
addHotbar: (data: CreateHotbarData, { setActive }?: CreateHotbarOptions) => void;
|
||||||
validate: value => !HotbarStore.getInstance().getByName(value),
|
uniqueHotbarName: InputValidator;
|
||||||
};
|
}
|
||||||
|
|
||||||
@observer
|
const NonInjectedHotbarAddCommand = observer(({ closeCommandOverlay, addHotbar, uniqueHotbarName }: Dependencies) => {
|
||||||
export class HotbarAddCommand extends React.Component {
|
const onSubmit = (name: string) => {
|
||||||
onSubmit = (name: string) => {
|
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HotbarStore.getInstance().add({ name }, { setActive: true });
|
addHotbar({ name }, { setActive: true });
|
||||||
CommandOverlay.close();
|
closeCommandOverlay();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<>
|
||||||
<>
|
<Input
|
||||||
<Input
|
placeholder="Hotbar name"
|
||||||
placeholder="Hotbar name"
|
autoFocus={true}
|
||||||
autoFocus={true}
|
theme="round-black"
|
||||||
theme="round-black"
|
data-test-id="command-palette-hotbar-add-name"
|
||||||
data-test-id="command-palette-hotbar-add-name"
|
validators={uniqueHotbarName}
|
||||||
validators={uniqueHotbarName}
|
onSubmit={onSubmit}
|
||||||
onSubmit={this.onSubmit}
|
dirty={true}
|
||||||
dirty={true}
|
showValidationLine={true}
|
||||||
showValidationLine={true}
|
/>
|
||||||
/>
|
<small className="hint">
|
||||||
<small className="hint">
|
Please provide a new hotbar name (Press "Enter" to confirm or "Escape" to cancel)
|
||||||
Please provide a new hotbar name (Press "Enter" to confirm or "Escape" to cancel)
|
</small>
|
||||||
</small>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
});
|
||||||
}
|
|
||||||
}
|
export const HotbarAddCommand = withInjectables<Dependencies>(NonInjectedHotbarAddCommand, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
|
||||||
|
addHotbar: di.inject(hotbarManagerInjectable).add,
|
||||||
|
uniqueHotbarName: di.inject(uniqueHotbarNameInjectable),
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@ -22,51 +22,44 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Select } from "../select";
|
import { Select } from "../select";
|
||||||
import { computed, makeObservable } from "mobx";
|
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
|
||||||
import { hotbarDisplayLabel } from "./hotbar-display-label";
|
|
||||||
import { CommandOverlay } from "../command-palette";
|
|
||||||
import { ConfirmDialog } from "../confirm-dialog";
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
import { Notifications } from "../notifications";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||||
|
import type { Hotbar } from "../../../common/hotbar-types";
|
||||||
|
|
||||||
@observer
|
interface Dependencies {
|
||||||
export class HotbarRemoveCommand extends React.Component {
|
closeCommandOverlay: () => void;
|
||||||
constructor(props: {}) {
|
hotbarManager: {
|
||||||
super(props);
|
hotbars: Hotbar[];
|
||||||
makeObservable(this);
|
getById: (id: string) => Hotbar | undefined;
|
||||||
}
|
remove: (hotbar: Hotbar) => void;
|
||||||
|
getDisplayLabel: (hotbar: Hotbar) => string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@computed get options() {
|
const NonInjectedHotbarRemoveCommand = observer(({ closeCommandOverlay, hotbarManager }: Dependencies) => {
|
||||||
return HotbarStore.getInstance().hotbars.map((hotbar) => {
|
const options = hotbarManager.hotbars.map(hotbar => ({
|
||||||
return { value: hotbar.id, label: hotbarDisplayLabel(hotbar.id) };
|
value: hotbar.id,
|
||||||
});
|
label: hotbarManager.getDisplayLabel(hotbar),
|
||||||
}
|
}));
|
||||||
|
|
||||||
onChange(id: string): void {
|
const onChange = (id: string): void => {
|
||||||
const hotbarStore = HotbarStore.getInstance();
|
const hotbar = hotbarManager.getById(id);
|
||||||
const hotbar = hotbarStore.getById(id);
|
|
||||||
|
|
||||||
CommandOverlay.close();
|
|
||||||
|
|
||||||
if (!hotbar) {
|
if (!hotbar) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hotbarStore.hotbars.length === 1) {
|
closeCommandOverlay();
|
||||||
Notifications.error("Can't remove the last hotbar");
|
// TODO: make confirm dialog injectable
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfirmDialog.open({
|
ConfirmDialog.open({
|
||||||
okButtonProps: {
|
okButtonProps: {
|
||||||
label: `Remove Hotbar`,
|
label: "Remove Hotbar",
|
||||||
primary: false,
|
primary: false,
|
||||||
accent: true,
|
accent: true,
|
||||||
},
|
},
|
||||||
ok: () => {
|
ok: () => hotbarManager.remove(hotbar),
|
||||||
hotbarStore.remove(hotbar);
|
|
||||||
},
|
|
||||||
message: (
|
message: (
|
||||||
<div className="confirm flex column gaps">
|
<div className="confirm flex column gaps">
|
||||||
<p>
|
<p>
|
||||||
@ -75,19 +68,26 @@ export class HotbarRemoveCommand extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<Select
|
||||||
<Select
|
menuPortalTarget={null}
|
||||||
menuPortalTarget={null}
|
onChange={(v) => onChange(v.value)}
|
||||||
onChange={(v) => this.onChange(v.value)}
|
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
||||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
menuIsOpen={true}
|
||||||
menuIsOpen={true}
|
options={options}
|
||||||
options={this.options}
|
autoFocus={true}
|
||||||
autoFocus={true}
|
escapeClearsValue={false}
|
||||||
escapeClearsValue={false}
|
placeholder="Remove hotbar"
|
||||||
placeholder="Remove hotbar" />
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
export const HotbarRemoveCommand = withInjectables<Dependencies>(NonInjectedHotbarRemoveCommand, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
|
||||||
|
hotbarManager: di.inject(hotbarManagerInjectable),
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@ -19,81 +19,61 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Select } from "../select";
|
import { Select } from "../select";
|
||||||
import { action, computed, makeObservable, observable } from "mobx";
|
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
import { Input, InputValidator } from "../input";
|
||||||
import { hotbarDisplayLabel } from "./hotbar-display-label";
|
import type { Hotbar } from "../../../common/hotbar-types";
|
||||||
import { Input } from "../input";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import { uniqueHotbarName } from "./hotbar-add-command";
|
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||||
import { CommandOverlay } from "../command-palette";
|
import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable";
|
||||||
|
|
||||||
@observer
|
interface Dependencies {
|
||||||
export class HotbarRenameCommand extends React.Component {
|
closeCommandOverlay: () => void;
|
||||||
@observable hotbarId = "";
|
hotbarManager: {
|
||||||
@observable hotbarName = "";
|
hotbars: Hotbar[];
|
||||||
|
getById: (id: string) => Hotbar | undefined;
|
||||||
constructor(props: {}) {
|
setHotbarName: (id: string, name: string) => void;
|
||||||
super(props);
|
getDisplayLabel: (hotbar: Hotbar) => string;
|
||||||
makeObservable(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get options() {
|
|
||||||
return HotbarStore.getInstance().hotbars.map((hotbar) => {
|
|
||||||
return { value: hotbar.id, label: hotbarDisplayLabel(hotbar.id) };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action onSelect = (id: string) => {
|
|
||||||
this.hotbarId = id;
|
|
||||||
this.hotbarName = HotbarStore.getInstance().getById(this.hotbarId).name;
|
|
||||||
};
|
};
|
||||||
|
uniqueHotbarName: InputValidator;
|
||||||
|
}
|
||||||
|
|
||||||
onSubmit = (name: string) => {
|
const NonInjectedHotbarRenameCommand = observer(({ closeCommandOverlay, hotbarManager, uniqueHotbarName }: Dependencies) => {
|
||||||
|
const [hotbarId, setHotbarId] = useState("");
|
||||||
|
const [hotbarName, setHotbarName] = useState("");
|
||||||
|
|
||||||
|
const options = hotbarManager.hotbars.map(hotbar => ({
|
||||||
|
value: hotbar.id,
|
||||||
|
label: hotbarManager.getDisplayLabel(hotbar),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onSelect = (id: string) => {
|
||||||
|
setHotbarId(id);
|
||||||
|
setHotbarName(hotbarManager.getById(id).name);
|
||||||
|
};
|
||||||
|
const onSubmit = (name: string) => {
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hotbarStore = HotbarStore.getInstance();
|
hotbarManager.setHotbarName(hotbarId, name);
|
||||||
const hotbar = HotbarStore.getInstance().getById(this.hotbarId);
|
closeCommandOverlay();
|
||||||
|
|
||||||
if (!hotbar) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hotbarStore.setHotbarName(this.hotbarId, name);
|
|
||||||
CommandOverlay.close();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renderHotbarList() {
|
if (hotbarId) {
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Select
|
|
||||||
menuPortalTarget={null}
|
|
||||||
onChange={(v) => this.onSelect(v.value)}
|
|
||||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
|
||||||
menuIsOpen={true}
|
|
||||||
options={this.options}
|
|
||||||
autoFocus={true}
|
|
||||||
escapeClearsValue={false}
|
|
||||||
placeholder="Rename hotbar"/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNameInput() {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
trim={true}
|
trim={true}
|
||||||
value={this.hotbarName}
|
value={hotbarName}
|
||||||
onChange={v => this.hotbarName = v}
|
onChange={setHotbarName}
|
||||||
placeholder="New hotbar name"
|
placeholder="New hotbar name"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
theme="round-black"
|
theme="round-black"
|
||||||
validators={uniqueHotbarName}
|
validators={uniqueHotbarName}
|
||||||
onSubmit={this.onSubmit}
|
onSubmit={onSubmit}
|
||||||
showValidationLine={true}
|
showValidationLine={true}
|
||||||
/>
|
/>
|
||||||
<small className="hint">
|
<small className="hint">
|
||||||
@ -103,12 +83,25 @@ export class HotbarRenameCommand extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
|
<Select
|
||||||
|
menuPortalTarget={null}
|
||||||
|
onChange={(v) => onSelect(v.value)}
|
||||||
|
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
||||||
|
menuIsOpen={true}
|
||||||
|
options={options}
|
||||||
|
autoFocus={true}
|
||||||
|
escapeClearsValue={false}
|
||||||
|
placeholder="Rename hotbar"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
export const HotbarRenameCommand = withInjectables<Dependencies>(NonInjectedHotbarRenameCommand, {
|
||||||
<>
|
getProps: (di, props) => ({
|
||||||
{!this.hotbarId ? this.renderHotbarList() : this.renderNameInput()}
|
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
|
||||||
</>
|
hotbarManager: di.inject(hotbarManagerInjectable),
|
||||||
);
|
uniqueHotbarName: di.inject(uniqueHotbarNameInjectable),
|
||||||
}
|
...props,
|
||||||
}
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@ -23,38 +23,52 @@ import "./hotbar-selector.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
|
||||||
import { CommandOverlay } from "../command-palette";
|
|
||||||
import { HotbarSwitchCommand } from "./hotbar-switch-command";
|
import { HotbarSwitchCommand } from "./hotbar-switch-command";
|
||||||
import { hotbarDisplayIndex } from "./hotbar-display-label";
|
|
||||||
import { TooltipPosition } from "../tooltip";
|
import { TooltipPosition } from "../tooltip";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import type { Hotbar } from "../../../common/hotbar-types";
|
import type { Hotbar } from "../../../common/hotbar-types";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||||
|
|
||||||
interface Props {
|
export interface HotbarSelectorProps {
|
||||||
hotbar: Hotbar;
|
hotbar: Hotbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HotbarSelector = observer(({ hotbar }: Props) => {
|
interface Dependencies {
|
||||||
const store = HotbarStore.getInstance();
|
hotbarManager: {
|
||||||
|
switchToPrevious: () => void;
|
||||||
|
switchToNext: () => void;
|
||||||
|
getActive: () => Hotbar;
|
||||||
|
getDisplayIndex: (hotbar: Hotbar) => string;
|
||||||
|
};
|
||||||
|
openCommandOverlay: (component: React.ReactElement) => void;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
const NonInjectedHotbarSelector = observer(({ hotbar, hotbarManager, openCommandOverlay }: HotbarSelectorProps & Dependencies) => (
|
||||||
<div className="HotbarSelector flex align-center">
|
<div className="HotbarSelector flex align-center">
|
||||||
<Icon material="play_arrow" className="previous box" onClick={() => store.switchToPrevious()} />
|
<Icon material="play_arrow" className="previous box" onClick={() => hotbarManager.switchToPrevious()} />
|
||||||
<div className="box grow flex align-center">
|
<div className="box grow flex align-center">
|
||||||
<Badge
|
<Badge
|
||||||
id="hotbarIndex"
|
id="hotbarIndex"
|
||||||
small
|
small
|
||||||
label={hotbarDisplayIndex(store.activeHotbarId)}
|
label={hotbarManager.getDisplayIndex(hotbarManager.getActive())}
|
||||||
onClick={() => CommandOverlay.open(<HotbarSwitchCommand />)}
|
onClick={() => openCommandOverlay(<HotbarSwitchCommand />)}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
preferredPositions: [TooltipPosition.TOP, TooltipPosition.TOP_LEFT],
|
preferredPositions: [TooltipPosition.TOP, TooltipPosition.TOP_LEFT],
|
||||||
children: hotbar.name,
|
children: hotbar.name,
|
||||||
}}
|
}}
|
||||||
className="SelectorIndex"
|
className="SelectorIndex"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<Icon material="play_arrow" className="next box" onClick={() => store.switchToNext()} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<Icon material="play_arrow" className="next box" onClick={() => hotbarManager.switchToNext()} />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const HotbarSelector = withInjectables<Dependencies, HotbarSelectorProps>(NonInjectedHotbarSelector, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
hotbarManager: di.inject(hotbarManagerInjectable),
|
||||||
|
openCommandOverlay: di.inject(commandOverlayInjectable).open,
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -22,73 +22,82 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Select } from "../select";
|
import { Select } from "../select";
|
||||||
import { computed, makeObservable } from "mobx";
|
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
import type { CommandOverlay } from "../command-palette";
|
||||||
import { CommandOverlay } from "../command-palette";
|
|
||||||
import { HotbarAddCommand } from "./hotbar-add-command";
|
import { HotbarAddCommand } from "./hotbar-add-command";
|
||||||
import { HotbarRemoveCommand } from "./hotbar-remove-command";
|
import { HotbarRemoveCommand } from "./hotbar-remove-command";
|
||||||
import { hotbarDisplayLabel } from "./hotbar-display-label";
|
|
||||||
import { HotbarRenameCommand } from "./hotbar-rename-command";
|
import { HotbarRenameCommand } from "./hotbar-rename-command";
|
||||||
|
import type { Hotbar } from "../../../common/hotbar-types";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||||
|
|
||||||
@observer
|
const addActionId = "__add__";
|
||||||
export class HotbarSwitchCommand extends React.Component {
|
const removeActionId = "__remove__";
|
||||||
private static addActionId = "__add__";
|
const renameActionId = "__rename__";
|
||||||
private static removeActionId = "__remove__";
|
|
||||||
private static renameActionId = "__rename__";
|
|
||||||
|
|
||||||
constructor(props: {}) {
|
interface HotbarManager {
|
||||||
super(props);
|
hotbars: Hotbar[];
|
||||||
makeObservable(this);
|
setActiveHotbar: (id: string) => void;
|
||||||
}
|
getDisplayLabel: (hotbar: Hotbar) => string;
|
||||||
|
|
||||||
@computed get options() {
|
|
||||||
const hotbarStore = HotbarStore.getInstance();
|
|
||||||
const options = hotbarStore.hotbars.map((hotbar) => {
|
|
||||||
return { value: hotbar.id, label: hotbarDisplayLabel(hotbar.id) };
|
|
||||||
});
|
|
||||||
|
|
||||||
options.push({ value: HotbarSwitchCommand.addActionId, label: "Add hotbar ..." });
|
|
||||||
|
|
||||||
if (hotbarStore.hotbars.length > 1) {
|
|
||||||
options.push({ value: HotbarSwitchCommand.removeActionId, label: "Remove hotbar ..." });
|
|
||||||
}
|
|
||||||
|
|
||||||
options.push({ value: HotbarSwitchCommand.renameActionId, label: "Rename hotbar ..." });
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(idOrAction: string): void {
|
|
||||||
switch (idOrAction) {
|
|
||||||
case HotbarSwitchCommand.addActionId:
|
|
||||||
CommandOverlay.open(<HotbarAddCommand />);
|
|
||||||
|
|
||||||
return;
|
|
||||||
case HotbarSwitchCommand.removeActionId:
|
|
||||||
CommandOverlay.open(<HotbarRemoveCommand />);
|
|
||||||
|
|
||||||
return;
|
|
||||||
case HotbarSwitchCommand.renameActionId:
|
|
||||||
CommandOverlay.open(<HotbarRenameCommand />);
|
|
||||||
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
HotbarStore.getInstance().activeHotbarId = idOrAction;
|
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
menuPortalTarget={null}
|
|
||||||
onChange={(v) => this.onChange(v.value)}
|
|
||||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
|
||||||
menuIsOpen={true}
|
|
||||||
options={this.options}
|
|
||||||
autoFocus={true}
|
|
||||||
escapeClearsValue={false}
|
|
||||||
placeholder="Switch to hotbar" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
hotbarManager: HotbarManager
|
||||||
|
commandOverlay: CommandOverlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHotbarSwitchOptions(hotbarManager: HotbarManager) {
|
||||||
|
const options = hotbarManager.hotbars.map(hotbar => ({
|
||||||
|
value: hotbar.id,
|
||||||
|
label: hotbarManager.getDisplayLabel(hotbar),
|
||||||
|
}));
|
||||||
|
|
||||||
|
options.push({ value: addActionId, label: "Add hotbar ..." });
|
||||||
|
|
||||||
|
if (hotbarManager.hotbars.length > 1) {
|
||||||
|
options.push({ value: removeActionId, label: "Remove hotbar ..." });
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({ value: renameActionId, label: "Rename hotbar ..." });
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NonInjectedHotbarSwitchCommand = observer(({ hotbarManager, commandOverlay }: Dependencies) => {
|
||||||
|
const options = getHotbarSwitchOptions(hotbarManager);
|
||||||
|
|
||||||
|
const onChange = (idOrAction: string): void => {
|
||||||
|
switch (idOrAction) {
|
||||||
|
case addActionId:
|
||||||
|
return commandOverlay.open(<HotbarAddCommand />);
|
||||||
|
case removeActionId:
|
||||||
|
return commandOverlay.open(<HotbarRemoveCommand />);
|
||||||
|
case renameActionId:
|
||||||
|
return commandOverlay.open(<HotbarRenameCommand />);
|
||||||
|
default:
|
||||||
|
hotbarManager.setActiveHotbar(idOrAction);
|
||||||
|
commandOverlay.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
menuPortalTarget={null}
|
||||||
|
onChange={(v) => onChange(v.value)}
|
||||||
|
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
||||||
|
menuIsOpen={true}
|
||||||
|
options={options}
|
||||||
|
autoFocus={true}
|
||||||
|
escapeClearsValue={false}
|
||||||
|
placeholder="Switch to hotbar"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const HotbarSwitchCommand = withInjectables<Dependencies>(NonInjectedHotbarSwitchCommand, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
hotbarManager: di.inject(hotbarManagerInjectable),
|
||||||
|
commandOverlay: di.inject(commandOverlayInjectable),
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@ -103,6 +103,10 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
submitted: false,
|
submitted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
this.setDirtyOnChange.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
setValue(value = "") {
|
setValue(value = "") {
|
||||||
if (value !== this.getValue()) {
|
if (value !== this.getValue()) {
|
||||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(this.input.constructor.prototype, "value").set;
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(this.input.constructor.prototype, "value").set;
|
||||||
|
|||||||
@ -18,13 +18,18 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import portForwardStoreInjectable from "../port-forward-store.injectable";
|
import hotbarManagerInjectable from "../../../../common/hotbar-store.injectable";
|
||||||
|
import type { InputValidator } from "../input_validators";
|
||||||
const removePortForwardInjectable = getInjectable({
|
|
||||||
instantiate: (di) => di.inject(portForwardStoreInjectable).remove,
|
|
||||||
|
|
||||||
|
const uniqueHotbarNameInjectable = getInjectable({
|
||||||
|
instantiate: di => ({
|
||||||
|
condition: ({ required }) => required,
|
||||||
|
message: () => "Hotbar with this name already exists",
|
||||||
|
validate: value => !di.inject(hotbarManagerInjectable).getByName(value),
|
||||||
|
} as InputValidator),
|
||||||
lifecycle: lifecycleEnum.singleton,
|
lifecycle: lifecycleEnum.singleton,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default removePortForwardInjectable;
|
export default uniqueHotbarNameInjectable;
|
||||||
@ -29,7 +29,7 @@ import type { KubeObjectMenuRegistration } from "../../../extensions/registries"
|
|||||||
import { KubeObjectMenuRegistry } from "../../../extensions/registries";
|
import { KubeObjectMenuRegistry } from "../../../extensions/registries";
|
||||||
import { ConfirmDialog } from "../confirm-dialog";
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
import asyncFn, { AsyncFnMock } from "@async-fn/jest";
|
import asyncFn, { AsyncFnMock } from "@async-fn/jest";
|
||||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
|
||||||
import clusterInjectable from "./dependencies/cluster.injectable";
|
import clusterInjectable from "./dependencies/cluster.injectable";
|
||||||
import hideDetailsInjectable from "./dependencies/hide-details.injectable";
|
import hideDetailsInjectable from "./dependencies/hide-details.injectable";
|
||||||
@ -65,8 +65,7 @@ describe("kube-object-menu", () => {
|
|||||||
}) as Cluster);
|
}) as Cluster);
|
||||||
|
|
||||||
di.override(apiManagerInjectable, () => ({
|
di.override(apiManagerInjectable, () => ({
|
||||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
getStore: api => void api,
|
||||||
getStore: api => undefined,
|
|
||||||
}) as ApiManager);
|
}) as ApiManager);
|
||||||
|
|
||||||
di.override(hideDetailsInjectable, () => () => {});
|
di.override(hideDetailsInjectable, () => () => {});
|
||||||
@ -285,5 +284,5 @@ const addDynamicMenuItem = ({
|
|||||||
|
|
||||||
const kubeObjectMenuRegistry = di.inject(kubeObjectMenuRegistryInjectable);
|
const kubeObjectMenuRegistry = di.inject(kubeObjectMenuRegistryInjectable);
|
||||||
|
|
||||||
kubeObjectMenuRegistry.add(dynamicMenuItemStub);
|
kubeObjectMenuRegistry.add([dynamicMenuItemStub]);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import { TopBar } from "./top-bar";
|
|||||||
import { IpcMainWindowEvents } from "../../../../main/window-manager";
|
import { IpcMainWindowEvents } from "../../../../main/window-manager";
|
||||||
import { broadcastMessage } from "../../../../common/ipc";
|
import { broadcastMessage } from "../../../../common/ipc";
|
||||||
import * as vars from "../../../../common/vars";
|
import * as vars from "../../../../common/vars";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
import directoryForUserDataInjectable
|
import directoryForUserDataInjectable
|
||||||
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import React from "react";
|
|||||||
import { fireEvent } from "@testing-library/react";
|
import { fireEvent } from "@testing-library/react";
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { TopBar } from "./top-bar";
|
import { TopBar } from "./top-bar";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
|
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createContainer } from "@ogre-tools/injectable";
|
import { createContainer } from "@ogre-tools/injectable";
|
||||||
import { setLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||||
|
|
||||||
export const getDi = () => {
|
export const getDi = () => {
|
||||||
const di = createContainer(
|
const di = createContainer(
|
||||||
@ -35,10 +35,10 @@ export const getDi = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getRequireContextForRendererCode = () =>
|
const getRequireContextForRendererCode = () =>
|
||||||
require.context("../", true, /\.injectable\.(ts|tsx)$/);
|
require.context("./", true, /\.injectable\.(ts|tsx)$/);
|
||||||
|
|
||||||
const getRequireContextForCommonExtensionCode = () =>
|
|
||||||
require.context("../../extensions", true, /\.injectable\.(ts|tsx)$/);
|
|
||||||
|
|
||||||
const getRequireContextForCommonCode = () =>
|
const getRequireContextForCommonCode = () =>
|
||||||
require.context("../../common", true, /\.injectable\.(ts|tsx)$/);
|
require.context("../common", true, /\.injectable\.(ts|tsx)$/);
|
||||||
|
|
||||||
|
const getRequireContextForCommonExtensionCode = () =>
|
||||||
|
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/);
|
||||||
@ -21,33 +21,26 @@
|
|||||||
|
|
||||||
import glob from "glob";
|
import glob from "glob";
|
||||||
import { memoize } from "lodash/fp";
|
import { memoize } from "lodash/fp";
|
||||||
|
import { createContainer } from "@ogre-tools/injectable";
|
||||||
import {
|
import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||||
createContainer,
|
import getValueFromRegisteredChannelInjectable from "./components/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable";
|
||||||
ConfigurableDependencyInjectionContainer,
|
import writeJsonFileInjectable from "../common/fs/write-json-file/write-json-file.injectable";
|
||||||
} from "@ogre-tools/injectable";
|
import readJsonFileInjectable from "../common/fs/read-json-file/read-json-file.injectable";
|
||||||
import { setLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
|
||||||
import getValueFromRegisteredChannelInjectable from "./app-paths/get-value-from-registered-channel/get-value-from-registered-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";
|
|
||||||
|
|
||||||
export const getDiForUnitTesting = ({ doGeneralOverrides } = { doGeneralOverrides: false }) => {
|
export const getDiForUnitTesting = ({ doGeneralOverrides } = { doGeneralOverrides: false }) => {
|
||||||
const di: ConfigurableDependencyInjectionContainer = createContainer();
|
const di = createContainer();
|
||||||
|
|
||||||
setLegacyGlobalDiForExtensionApi(di);
|
setLegacyGlobalDiForExtensionApi(di);
|
||||||
|
|
||||||
getInjectableFilePaths()
|
for (const filePath of getInjectableFilePaths()) {
|
||||||
.map(key => {
|
const injectableInstance = require(filePath).default;
|
||||||
const injectable = require(key).default;
|
|
||||||
|
|
||||||
return {
|
di.register({
|
||||||
id: key,
|
id: filePath,
|
||||||
...injectable,
|
...injectableInstance,
|
||||||
aliases: [injectable, ...(injectable.aliases || [])],
|
aliases: [injectableInstance, ...(injectableInstance.aliases || [])],
|
||||||
};
|
});
|
||||||
})
|
}
|
||||||
|
|
||||||
.forEach(injectable => di.register(injectable));
|
|
||||||
|
|
||||||
di.preventSideEffects();
|
di.preventSideEffects();
|
||||||
|
|
||||||
@ -67,7 +60,7 @@ export const getDiForUnitTesting = ({ doGeneralOverrides } = { doGeneralOverride
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getInjectableFilePaths = memoize(() => [
|
const getInjectableFilePaths = memoize(() => [
|
||||||
...glob.sync("../**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||||
...glob.sync("../../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||||
...glob.sync("../../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
|
||||||
]);
|
]);
|
||||||
@ -22,32 +22,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import "../../common/catalog-entities/kubernetes-cluster";
|
import "../../common/catalog-entities/kubernetes-cluster";
|
||||||
import { WebLinkCategory } from "../../common/catalog-entities";
|
|
||||||
import { ClusterStore } from "../../common/cluster-store/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store/cluster-store";
|
||||||
import { catalogCategoryRegistry } from "../api/catalog-category-registry";
|
import { catalogCategoryRegistry } from "../api/catalog-category-registry";
|
||||||
import { WeblinkAddCommand } from "../components/catalog-entities/weblink-add-command";
|
import { WeblinkAddCommand } from "../components/catalog-entities/weblink-add-command";
|
||||||
import { CommandOverlay } from "../components/command-palette";
|
|
||||||
import { loadConfigFromString } from "../../common/kube-helpers";
|
import { loadConfigFromString } from "../../common/kube-helpers";
|
||||||
import { DeleteClusterDialog } from "../components/delete-cluster-dialog";
|
import { DeleteClusterDialog } from "../components/delete-cluster-dialog";
|
||||||
|
|
||||||
function initWebLinks() {
|
|
||||||
WebLinkCategory.onAdd = () => CommandOverlay.open(<WeblinkAddCommand />);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initKubernetesClusters() {
|
|
||||||
catalogCategoryRegistry
|
|
||||||
.getForGroupKind("entity.k8slens.dev", "KubernetesCluster")
|
|
||||||
.on("contextMenuOpen", (entity, context) => {
|
|
||||||
if (entity.metadata?.source == "local") {
|
|
||||||
context.menuItems.push({
|
|
||||||
title: "Delete",
|
|
||||||
icon: "delete",
|
|
||||||
onClick: () => onClusterDelete(entity.metadata.uid),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onClusterDelete(clusterId: string) {
|
async function onClusterDelete(clusterId: string) {
|
||||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||||
|
|
||||||
@ -64,7 +44,30 @@ async function onClusterDelete(clusterId: string) {
|
|||||||
DeleteClusterDialog.open({ cluster, config });
|
DeleteClusterDialog.open({ cluster, config });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initCatalog() {
|
interface Dependencies {
|
||||||
initWebLinks();
|
openCommandDialog: (component: React.ReactElement) => void;
|
||||||
initKubernetesClusters();
|
}
|
||||||
|
|
||||||
|
export function initCatalog({ openCommandDialog }: Dependencies) {
|
||||||
|
catalogCategoryRegistry
|
||||||
|
.getForGroupKind("entity.k8slens.dev", "WebLink")
|
||||||
|
.on("catalogAddMenu", ctx => {
|
||||||
|
ctx.menuItems.push({
|
||||||
|
title: "Add web link",
|
||||||
|
icon: "public",
|
||||||
|
onClick: () => openCommandDialog(<WeblinkAddCommand />),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
catalogCategoryRegistry
|
||||||
|
.getForGroupKind("entity.k8slens.dev", "KubernetesCluster")
|
||||||
|
.on("contextMenuOpen", (entity, context) => {
|
||||||
|
if (entity.metadata?.source == "local") {
|
||||||
|
context.menuItems.push({
|
||||||
|
title: "Delete",
|
||||||
|
icon: "delete",
|
||||||
|
onClick: () => onClusterDelete(entity.metadata.uid),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,206 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2021 OpenLens Authors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import * as routes from "../../common/routes";
|
|
||||||
import { CommandRegistry } from "../../extensions/registries";
|
|
||||||
import { getActiveClusterEntity } from "../api/catalog-entity-registry";
|
|
||||||
import { CommandOverlay } from "../components/command-palette";
|
|
||||||
import { HotbarAddCommand } from "../components/hotbar/hotbar-add-command";
|
|
||||||
import { HotbarRemoveCommand } from "../components/hotbar/hotbar-remove-command";
|
|
||||||
import { HotbarSwitchCommand } from "../components/hotbar/hotbar-switch-command";
|
|
||||||
import { navigate } from "../navigation";
|
|
||||||
import { HotbarRenameCommand } from "../components/hotbar/hotbar-rename-command";
|
|
||||||
import { ActivateEntityCommand } from "../components/activate-entity-command";
|
|
||||||
|
|
||||||
export function initCommandRegistry(createTerminalTab: () => void) {
|
|
||||||
CommandRegistry.getInstance()
|
|
||||||
.add([
|
|
||||||
{
|
|
||||||
id: "app.showPreferences",
|
|
||||||
title: "Preferences: Open",
|
|
||||||
scope: "global",
|
|
||||||
action: () => navigate(routes.preferencesURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewHelmCharts",
|
|
||||||
title: "Cluster: View Helm Charts",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.helmChartsURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewHelmReleases",
|
|
||||||
title: "Cluster: View Helm Releases",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.releaseURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewConfigMaps",
|
|
||||||
title: "Cluster: View ConfigMaps",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.configMapsURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewSecrets",
|
|
||||||
title: "Cluster: View Secrets",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.secretsURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewResourceQuotas",
|
|
||||||
title: "Cluster: View ResourceQuotas",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.resourceQuotaURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewLimitRanges",
|
|
||||||
title: "Cluster: View LimitRanges",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.limitRangeURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewHorizontalPodAutoscalers",
|
|
||||||
title: "Cluster: View HorizontalPodAutoscalers (HPA)",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.hpaURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewPodDisruptionBudget",
|
|
||||||
title: "Cluster: View PodDisruptionBudgets",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.pdbURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewServices",
|
|
||||||
title: "Cluster: View Services",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.servicesURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewEndpoints",
|
|
||||||
title: "Cluster: View Endpoints",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.endpointURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewIngresses",
|
|
||||||
title: "Cluster: View Ingresses",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.ingressURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewNetworkPolicies",
|
|
||||||
title: "Cluster: View NetworkPolicies",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.networkPoliciesURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewNodes",
|
|
||||||
title: "Cluster: View Nodes",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.nodesURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewPods",
|
|
||||||
title: "Cluster: View Pods",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.podsURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewDeployments",
|
|
||||||
title: "Cluster: View Deployments",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.deploymentsURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewDaemonSets",
|
|
||||||
title: "Cluster: View DaemonSets",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.daemonSetsURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewStatefulSets",
|
|
||||||
title: "Cluster: View StatefulSets",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.statefulSetsURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewJobs",
|
|
||||||
title: "Cluster: View Jobs",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.jobsURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewCronJobs",
|
|
||||||
title: "Cluster: View CronJobs",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => navigate(routes.cronJobsURL()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.viewCurrentClusterSettings",
|
|
||||||
title: "Cluster: View Settings",
|
|
||||||
scope: "global",
|
|
||||||
action: () => navigate(routes.entitySettingsURL({
|
|
||||||
params: {
|
|
||||||
entityId: getActiveClusterEntity()?.id,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
isActive: (context) => !!context.entity,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cluster.openTerminal",
|
|
||||||
title: "Cluster: Open terminal",
|
|
||||||
scope: "entity",
|
|
||||||
action: () => createTerminalTab(),
|
|
||||||
isActive: (context) => !!context.entity,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "hotbar.switchHotbar",
|
|
||||||
title: "Hotbar: Switch ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<HotbarSwitchCommand />),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "hotbar.addHotbar",
|
|
||||||
title: "Hotbar: Add Hotbar ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<HotbarAddCommand />),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "hotbar.removeHotbar",
|
|
||||||
title: "Hotbar: Remove Hotbar ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<HotbarRemoveCommand />),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "hotbar.renameHotbar",
|
|
||||||
title: "Hotbar: Rename Hotbar ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<HotbarRenameCommand />),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "catalog.searchEntities",
|
|
||||||
title: "Catalog: Activate Entity ...",
|
|
||||||
scope: "global",
|
|
||||||
action: () => CommandOverlay.open(<ActivateEntityCommand />),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
@ -21,7 +21,6 @@
|
|||||||
|
|
||||||
export * from "./catalog-entity-detail-registry";
|
export * from "./catalog-entity-detail-registry";
|
||||||
export * from "./catalog";
|
export * from "./catalog";
|
||||||
export * from "./command-registry";
|
|
||||||
export * from "./entity-settings-registry";
|
export * from "./entity-settings-registry";
|
||||||
export * from "./ipc";
|
export * from "./ipc";
|
||||||
export * from "./kube-object-detail-registry";
|
export * from "./kube-object-detail-registry";
|
||||||
|
|||||||
@ -26,7 +26,6 @@ export function initRegistries() {
|
|||||||
registries.CatalogEntityDetailRegistry.createInstance();
|
registries.CatalogEntityDetailRegistry.createInstance();
|
||||||
registries.ClusterPageMenuRegistry.createInstance();
|
registries.ClusterPageMenuRegistry.createInstance();
|
||||||
registries.ClusterPageRegistry.createInstance();
|
registries.ClusterPageRegistry.createInstance();
|
||||||
registries.CommandRegistry.createInstance();
|
|
||||||
registries.EntitySettingRegistry.createInstance();
|
registries.EntitySettingRegistry.createInstance();
|
||||||
registries.GlobalPageRegistry.createInstance();
|
registries.GlobalPageRegistry.createInstance();
|
||||||
registries.KubeObjectDetailRegistry.createInstance();
|
registries.KubeObjectDetailRegistry.createInstance();
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export function isActiveRoute(route: string | string[] | RouteProps): boolean {
|
|||||||
return !!matchRoute(route);
|
return !!matchRoute(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMatchedClusterId(): string {
|
export function getMatchedClusterId(): string | undefined {
|
||||||
const matched = matchPath<ClusterViewRouteParams>(navigation.location.pathname, {
|
const matched = matchPath<ClusterViewRouteParams>(navigation.location.pathname, {
|
||||||
exact: true,
|
exact: true,
|
||||||
path: clusterViewRoute.path,
|
path: clusterViewRoute.path,
|
||||||
|
|||||||
@ -18,17 +18,20 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
import { noop } from "lodash/fp";
|
||||||
import { action, computed, observable, makeObservable } from "mobx";
|
import { action, computed, observable, makeObservable } from "mobx";
|
||||||
import type { ForwardedPort } from "../port-forward-item";
|
import type { ForwardedPort } from "../port-forward-item";
|
||||||
|
|
||||||
interface PortForwardDialogOpenOptions {
|
interface PortForwardDialogOpenOptions {
|
||||||
openInBrowser: boolean
|
openInBrowser: boolean
|
||||||
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortForwardDialogModel {
|
export class PortForwardDialogModel {
|
||||||
portForward: ForwardedPort = null;
|
portForward: ForwardedPort = null;
|
||||||
useHttps = false;
|
useHttps = false;
|
||||||
openInBrowser = false;
|
openInBrowser = false;
|
||||||
|
onClose = noop;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
@ -46,10 +49,11 @@ export class PortForwardDialogModel {
|
|||||||
return !!this.portForward;
|
return !!this.portForward;
|
||||||
}
|
}
|
||||||
|
|
||||||
open = (portForward: ForwardedPort, options: PortForwardDialogOpenOptions = { openInBrowser: false }) => {
|
open = (portForward: ForwardedPort, options: PortForwardDialogOpenOptions = { openInBrowser: false, onClose: noop }) => {
|
||||||
this.portForward = portForward;
|
this.portForward = portForward;
|
||||||
this.useHttps = portForward.protocol === "https";
|
this.useHttps = portForward.protocol === "https";
|
||||||
this.openInBrowser = options.openInBrowser;
|
this.openInBrowser = options.openInBrowser;
|
||||||
|
this.onClose = options.onClose;
|
||||||
};
|
};
|
||||||
|
|
||||||
close = () => {
|
close = () => {
|
||||||
|
|||||||
@ -27,25 +27,21 @@ import { observer } from "mobx-react";
|
|||||||
import { Dialog, DialogProps } from "../components/dialog";
|
import { Dialog, DialogProps } from "../components/dialog";
|
||||||
import { Wizard, WizardStep } from "../components/wizard";
|
import { Wizard, WizardStep } from "../components/wizard";
|
||||||
import { Input } from "../components/input";
|
import { Input } from "../components/input";
|
||||||
import { Notifications } from "../components/notifications";
|
|
||||||
import { cssNames } from "../utils";
|
import { cssNames } from "../utils";
|
||||||
import { getPortForwards } from "./port-forward-store/port-forward-store";
|
import type { PortForwardStore } from "./port-forward-store/port-forward-store";
|
||||||
import type { ForwardedPort } from "./port-forward-item";
|
|
||||||
import { openPortForward } from "./port-forward-utils";
|
import { openPortForward } from "./port-forward-utils";
|
||||||
import { aboutPortForwarding } from "./port-forward-notify";
|
import { aboutPortForwarding, notifyErrorPortForwarding } from "./port-forward-notify";
|
||||||
import { Checkbox } from "../components/checkbox";
|
import { Checkbox } from "../components/checkbox";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import modifyPortForwardInjectable from "./port-forward-store/modify-port-forward/modify-port-forward.injectable";
|
|
||||||
import type { PortForwardDialogModel } from "./port-forward-dialog-model/port-forward-dialog-model";
|
import type { PortForwardDialogModel } from "./port-forward-dialog-model/port-forward-dialog-model";
|
||||||
import portForwardDialogModelInjectable from "./port-forward-dialog-model/port-forward-dialog-model.injectable";
|
import portForwardDialogModelInjectable from "./port-forward-dialog-model/port-forward-dialog-model.injectable";
|
||||||
import addPortForwardInjectable from "./port-forward-store/add-port-forward/add-port-forward.injectable";
|
import logger from "../../common/logger";
|
||||||
|
import portForwardStoreInjectable from "./port-forward-store/port-forward-store.injectable";
|
||||||
|
|
||||||
interface Props extends Partial<DialogProps> {
|
interface Props extends Partial<DialogProps> {}
|
||||||
}
|
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
modifyPortForward: (item: ForwardedPort, desiredPort: number) => Promise<number>,
|
portForwardStore: PortForwardStore,
|
||||||
addPortForward: (item: ForwardedPort) => Promise<number>,
|
|
||||||
model: PortForwardDialogModel
|
model: PortForwardDialogModel
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,50 +55,58 @@ class NonInjectedPortForwardDialog extends Component<Props & Dependencies> {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get portForwardStore() {
|
||||||
|
return this.props.portForwardStore;
|
||||||
|
}
|
||||||
|
|
||||||
onOpen = async () => {
|
onOpen = async () => {
|
||||||
this.currentPort = +this.props.model.portForward.forwardPort;
|
this.currentPort = +this.props.model.portForward.forwardPort;
|
||||||
this.desiredPort = this.currentPort;
|
this.desiredPort = this.currentPort;
|
||||||
};
|
};
|
||||||
|
|
||||||
onClose = () => {
|
|
||||||
};
|
|
||||||
|
|
||||||
changePort = (value: string) => {
|
changePort = (value: string) => {
|
||||||
this.desiredPort = Number(value);
|
this.desiredPort = Number(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
startPortForward = async () => {
|
startPortForward = async () => {
|
||||||
const portForward = this.props.model.portForward;
|
let { portForward } = this.props.model;
|
||||||
const { currentPort, desiredPort } = this;
|
const { currentPort, desiredPort } = this;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// determine how many port-forwards are already active
|
// determine how many port-forwards already exist
|
||||||
const { length } = await getPortForwards();
|
const { length } = this.portForwardStore.getPortForwards();
|
||||||
|
|
||||||
let port: number;
|
|
||||||
|
|
||||||
portForward.protocol = this.props.model.useHttps ? "https" : "http";
|
portForward.protocol = this.props.model.useHttps ? "https" : "http";
|
||||||
|
|
||||||
if (currentPort) {
|
if (currentPort) {
|
||||||
port = await this.props.modifyPortForward(portForward, desiredPort);
|
const wasRunning = portForward.status === "Active";
|
||||||
|
|
||||||
|
portForward = await this.portForwardStore.modify(portForward, desiredPort);
|
||||||
|
|
||||||
|
if (wasRunning && portForward.status === "Disabled") {
|
||||||
|
notifyErrorPortForwarding(`Error occurred starting port-forward, the local port ${portForward.forwardPort} may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
portForward.forwardPort = desiredPort;
|
portForward.forwardPort = desiredPort;
|
||||||
port = await this.props.addPortForward(portForward);
|
portForward = await this.portForwardStore.add(portForward);
|
||||||
|
|
||||||
// if this is the first port-forward show the about notification
|
if (portForward.status === "Disabled") {
|
||||||
if (!length) {
|
notifyErrorPortForwarding(`Error occurred starting port-forward, the local port ${portForward.forwardPort} may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
||||||
aboutPortForwarding();
|
} else {
|
||||||
|
// if this is the first port-forward show the about notification
|
||||||
|
if (!length) {
|
||||||
|
aboutPortForwarding();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.model.openInBrowser) {
|
if (portForward.status === "Active" && this.props.model.openInBrowser) {
|
||||||
portForward.forwardPort = port;
|
|
||||||
openPortForward(portForward);
|
openPortForward(portForward);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
logger.error(`[PORT-FORWARD-DIALOG]: ${error}`, portForward);
|
||||||
} finally {
|
} finally {
|
||||||
close();
|
this.props.model.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -141,7 +145,7 @@ class NonInjectedPortForwardDialog extends Component<Props & Dependencies> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, modifyPortForward, model, ...dialogProps } = this.props;
|
const { className, portForwardStore, model, ...dialogProps } = this.props;
|
||||||
const resourceName = this.props.model.portForward?.name ?? "";
|
const resourceName = this.props.model.portForward?.name ?? "";
|
||||||
const header = (
|
const header = (
|
||||||
<h5>
|
<h5>
|
||||||
@ -155,14 +159,14 @@ class NonInjectedPortForwardDialog extends Component<Props & Dependencies> {
|
|||||||
isOpen={this.props.model.isOpen}
|
isOpen={this.props.model.isOpen}
|
||||||
className={cssNames("PortForwardDialog", className)}
|
className={cssNames("PortForwardDialog", className)}
|
||||||
onOpen={this.onOpen}
|
onOpen={this.onOpen}
|
||||||
onClose={this.onClose}
|
onClose={model.onClose}
|
||||||
close={this.props.model.close}
|
close={this.props.model.close}
|
||||||
>
|
>
|
||||||
<Wizard header={header} done={this.props.model.close}>
|
<Wizard header={header} done={this.props.model.close}>
|
||||||
<WizardStep
|
<WizardStep
|
||||||
contentClass="flex gaps column"
|
contentClass="flex gaps column"
|
||||||
next={this.startPortForward}
|
next={this.startPortForward}
|
||||||
nextLabel={this.currentPort === 0 ? "Start" : "Restart"}
|
nextLabel={this.currentPort === 0 ? "Start" : "Modify"}
|
||||||
>
|
>
|
||||||
{this.renderContents()}
|
{this.renderContents()}
|
||||||
</WizardStep>
|
</WizardStep>
|
||||||
@ -177,8 +181,7 @@ export const PortForwardDialog = withInjectables<Dependencies, Props>(
|
|||||||
|
|
||||||
{
|
{
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
modifyPortForward: di.inject(modifyPortForwardInjectable),
|
portForwardStore: di.inject(portForwardStoreInjectable),
|
||||||
addPortForward: di.inject(addPortForwardInjectable),
|
|
||||||
model: di.inject(portForwardDialogModelInjectable),
|
model: di.inject(portForwardDialogModelInjectable),
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -23,33 +23,34 @@
|
|||||||
import type { ItemObject } from "../../common/item.store";
|
import type { ItemObject } from "../../common/item.store";
|
||||||
import { autoBind } from "../../common/utils";
|
import { autoBind } from "../../common/utils";
|
||||||
|
|
||||||
|
export type ForwardedPortStatus = "Active" | "Disabled";
|
||||||
export interface ForwardedPort {
|
export interface ForwardedPort {
|
||||||
clusterId?: string;
|
|
||||||
kind: string;
|
kind: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
name: string;
|
name: string;
|
||||||
port: number;
|
port: number;
|
||||||
forwardPort: number;
|
forwardPort: number;
|
||||||
protocol?: string;
|
protocol?: string;
|
||||||
|
status?: ForwardedPortStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortForwardItem implements ItemObject {
|
export class PortForwardItem implements ItemObject {
|
||||||
clusterId: string;
|
|
||||||
kind: string;
|
kind: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
name: string;
|
name: string;
|
||||||
port: number;
|
port: number;
|
||||||
forwardPort: number;
|
forwardPort: number;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
|
status: ForwardedPortStatus;
|
||||||
|
|
||||||
constructor(pf: ForwardedPort) {
|
constructor(pf: ForwardedPort) {
|
||||||
this.clusterId = pf.clusterId;
|
|
||||||
this.kind = pf.kind;
|
this.kind = pf.kind;
|
||||||
this.namespace = pf.namespace;
|
this.namespace = pf.namespace;
|
||||||
this.name = pf.name;
|
this.name = pf.name;
|
||||||
this.port = pf.port;
|
this.port = pf.port;
|
||||||
this.forwardPort = pf.forwardPort;
|
this.forwardPort = pf.forwardPort;
|
||||||
this.protocol = pf.protocol ?? "http";
|
this.protocol = pf.protocol ?? "http";
|
||||||
|
this.status = pf.status ?? "Active";
|
||||||
|
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
}
|
}
|
||||||
@ -62,12 +63,8 @@ export class PortForwardItem implements ItemObject {
|
|||||||
return this.namespace;
|
return this.namespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() {
|
|
||||||
return this.forwardPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
getId() {
|
getId() {
|
||||||
return String(this.forwardPort);
|
return `${this.namespace}-${this.kind}-${this.name}:${this.port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getKind() {
|
getKind() {
|
||||||
@ -87,16 +84,17 @@ export class PortForwardItem implements ItemObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
return "Active"; // to-do allow port-forward-items to be stopped (without removing them)
|
return this.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchFields() {
|
getSearchFields() {
|
||||||
return [
|
return [
|
||||||
this.name,
|
this.name,
|
||||||
this.id,
|
this.namespace,
|
||||||
this.kind,
|
this.kind,
|
||||||
this.port,
|
this.port,
|
||||||
this.forwardPort,
|
this.forwardPort,
|
||||||
|
this.status,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,3 +56,34 @@ export function aboutPortForwarding() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function notifyErrorPortForwarding(msg: string) {
|
||||||
|
const notificationId = `port-forward-error-notification-${getHostedClusterId()}`;
|
||||||
|
|
||||||
|
Notifications.error(
|
||||||
|
(
|
||||||
|
<div className="flex column gaps">
|
||||||
|
<b>Port Forwarding</b>
|
||||||
|
<p>
|
||||||
|
{msg}
|
||||||
|
</p>
|
||||||
|
<div className="flex gaps row align-left box grow">
|
||||||
|
<Button
|
||||||
|
active
|
||||||
|
outlined
|
||||||
|
label="Check Port Forwarding"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(portForwardsURL());
|
||||||
|
notificationsStore.remove(notificationId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: notificationId,
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,21 +19,21 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { action, makeObservable, observable, reaction } from "mobx";
|
||||||
import { makeObservable, observable, reaction } from "mobx";
|
|
||||||
import { ItemStore } from "../../../common/item.store";
|
import { ItemStore } from "../../../common/item.store";
|
||||||
import { autoBind, disposer, getHostedClusterId, StorageHelper } from "../../utils";
|
import { autoBind, disposer, StorageHelper } from "../../utils";
|
||||||
import { ForwardedPort, PortForwardItem } from "../port-forward-item";
|
import { ForwardedPort, PortForwardItem } from "../port-forward-item";
|
||||||
|
import { notifyErrorPortForwarding } from "../port-forward-notify";
|
||||||
import { apiBase } from "../../api";
|
import { apiBase } from "../../api";
|
||||||
import logger from "../../../common/logger";
|
|
||||||
import { waitUntilFree } from "tcp-port-used";
|
import { waitUntilFree } from "tcp-port-used";
|
||||||
|
import logger from "../../../common/logger";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
storage: StorageHelper<ForwardedPort[] | undefined>
|
storage: StorageHelper<ForwardedPort[] | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortForwardStore extends ItemStore<PortForwardItem> {
|
export class PortForwardStore extends ItemStore<PortForwardItem> {
|
||||||
@observable portForwards: PortForwardItem[];
|
@observable portForwards: PortForwardItem[] = [];
|
||||||
|
|
||||||
constructor(private dependencies: Dependencies) {
|
constructor(private dependencies: Dependencies) {
|
||||||
super();
|
super();
|
||||||
@ -50,33 +50,58 @@ export class PortForwardStore extends ItemStore<PortForwardItem> {
|
|||||||
|
|
||||||
if (Array.isArray(savedPortForwards)) {
|
if (Array.isArray(savedPortForwards)) {
|
||||||
logger.info("[PORT-FORWARD-STORE] starting saved port-forwards");
|
logger.info("[PORT-FORWARD-STORE] starting saved port-forwards");
|
||||||
await Promise.all(savedPortForwards.map(this.add));
|
|
||||||
|
// add the disabled ones
|
||||||
|
await Promise.all(
|
||||||
|
savedPortForwards
|
||||||
|
.filter((pf) => pf.status === "Disabled")
|
||||||
|
.map(this.add),
|
||||||
|
);
|
||||||
|
|
||||||
|
// add the active ones and check if they started successfully
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
savedPortForwards
|
||||||
|
.filter((pf) => pf.status === "Active")
|
||||||
|
.map(this.add),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
if (
|
||||||
|
result.status === "rejected" ||
|
||||||
|
result.value.status === "Disabled"
|
||||||
|
) {
|
||||||
|
notifyErrorPortForwarding(
|
||||||
|
"One or more port-forwards could not be started",
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch() {
|
watch() {
|
||||||
return disposer(
|
return disposer(
|
||||||
reaction(() => this.portForwards, () => this.loadAll()),
|
reaction(
|
||||||
|
() => this.portForwards.slice(),
|
||||||
|
() => this.loadAll(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAll() {
|
loadAll() {
|
||||||
return this.loadItems(async () => {
|
return this.loadItems(() => {
|
||||||
const portForwards = await getPortForwards(getHostedClusterId());
|
const portForwards = this.getPortForwards();
|
||||||
|
|
||||||
this.dependencies.storage.set(portForwards);
|
this.dependencies.storage.set(portForwards);
|
||||||
|
|
||||||
this.reset();
|
this.portForwards = [];
|
||||||
portForwards.map(pf => this.portForwards.push(new PortForwardItem(pf)));
|
portForwards.map((pf) => this.portForwards.push(new PortForwardItem(pf)));
|
||||||
|
|
||||||
return this.portForwards;
|
return this.portForwards;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reset = () => {
|
|
||||||
this.portForwards = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
async removeSelectedItems() {
|
async removeSelectedItems() {
|
||||||
return Promise.all(this.selectedItems.map(this.remove));
|
return Promise.all(this.selectedItems.map(this.remove));
|
||||||
}
|
}
|
||||||
@ -91,87 +116,285 @@ export class PortForwardStore extends ItemStore<PortForwardItem> {
|
|||||||
return this.getItems()[index];
|
return this.getItems()[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
add = async (portForward: ForwardedPort): Promise<number> => {
|
/**
|
||||||
|
* add a port-forward to the store and optionally start it
|
||||||
|
* @param portForward the port-forward to add. If the port-forward already exists in the store it will be
|
||||||
|
* returned with its current state. If the forwardPort field is 0 then an arbitrary port will be
|
||||||
|
* used. If the status field is "Active" or not present then an attempt is made to start the port-forward.
|
||||||
|
*
|
||||||
|
* @returns the port-forward with updated status ("Active" if successfully started, "Disabled" otherwise) and
|
||||||
|
* forwardPort
|
||||||
|
*/
|
||||||
|
add = action(async (portForward: ForwardedPort): Promise<ForwardedPort> => {
|
||||||
|
const pf = this.findPortForward(portForward);
|
||||||
|
|
||||||
|
if (pf) {
|
||||||
|
return pf;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.portForwards.push(new PortForwardItem(portForward));
|
||||||
|
|
||||||
|
if (!portForward.status) {
|
||||||
|
portForward.status = "Active";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (portForward.status === "Active") {
|
||||||
|
portForward = await this.start(portForward);
|
||||||
|
}
|
||||||
|
|
||||||
|
return portForward;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* modifies a port-forward in the store, including the forwardPort and protocol
|
||||||
|
* @param portForward the port-forward to modify.
|
||||||
|
*
|
||||||
|
* @returns the port-forward after being modified.
|
||||||
|
*/
|
||||||
|
modify = action(
|
||||||
|
async (
|
||||||
|
portForward: ForwardedPort,
|
||||||
|
desiredPort: number,
|
||||||
|
): Promise<ForwardedPort> => {
|
||||||
|
const pf = this.findPortForward(portForward);
|
||||||
|
|
||||||
|
if (!pf) {
|
||||||
|
throw new Error("port-forward not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pf.status === "Active") {
|
||||||
|
try {
|
||||||
|
await this.stop(pf);
|
||||||
|
} catch {
|
||||||
|
// ignore, assume it is stopped and proceed to restart it
|
||||||
|
}
|
||||||
|
|
||||||
|
pf.forwardPort = desiredPort;
|
||||||
|
pf.protocol = portForward.protocol ?? "http";
|
||||||
|
this.setPortForward(pf);
|
||||||
|
|
||||||
|
return await this.start(pf);
|
||||||
|
}
|
||||||
|
|
||||||
|
pf.forwardPort = desiredPort;
|
||||||
|
this.setPortForward(pf);
|
||||||
|
|
||||||
|
return pf as ForwardedPort;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove and stop an existing port-forward.
|
||||||
|
* @param portForward the port-forward to remove.
|
||||||
|
*/
|
||||||
|
remove = action(async (portForward: ForwardedPort) => {
|
||||||
|
const pf = this.findPortForward(portForward);
|
||||||
|
|
||||||
|
if (!pf) {
|
||||||
|
const error = new Error("port-forward not found");
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
`[PORT-FORWARD-STORE] Error getting port-forward: ${error}`,
|
||||||
|
portForward,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.stop(portForward);
|
||||||
|
} catch (error) {
|
||||||
|
if (pf.status === "Active") {
|
||||||
|
logger.warn(
|
||||||
|
`[PORT-FORWARD-STORE] Error removing port-forward: ${error}`,
|
||||||
|
portForward,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.portForwards.findIndex(portForwardsEqual(portForward));
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.portForwards.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets the list of port-forwards in the store
|
||||||
|
*
|
||||||
|
* @returns the port-forwards
|
||||||
|
*/
|
||||||
|
getPortForwards = (): ForwardedPort[] => {
|
||||||
|
return this.portForwards;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stop an existing port-forward. Its status is set to "Disabled" after successfully stopped.
|
||||||
|
* @param portForward the port-forward to stop.
|
||||||
|
*
|
||||||
|
* @throws if the port-forward could not be stopped. Its status is unchanged
|
||||||
|
*/
|
||||||
|
stop = action(async (portForward: ForwardedPort) => {
|
||||||
|
const pf = this.findPortForward(portForward);
|
||||||
|
|
||||||
|
if (!pf) {
|
||||||
|
logger.warn(
|
||||||
|
"[PORT-FORWARD-STORE] Error getting port-forward: port-forward not found",
|
||||||
|
portForward,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { port, forwardPort } = portForward;
|
const { port, forwardPort } = portForward;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiBase.del(
|
||||||
|
`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}`,
|
||||||
|
{ query: { port, forwardPort }},
|
||||||
|
);
|
||||||
|
await waitUntilFree(+forwardPort, 200, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`[PORT-FORWARD-STORE] Error stopping active port-forward: ${error}`,
|
||||||
|
portForward,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
pf.status = "Disabled";
|
||||||
|
|
||||||
|
this.setPortForward(pf);
|
||||||
|
});
|
||||||
|
|
||||||
|
private findPortForward = (portForward: ForwardedPort) => {
|
||||||
|
return this.portForwards.find(portForwardsEqual(portForward));
|
||||||
|
};
|
||||||
|
|
||||||
|
private setPortForward = action((portForward: ForwardedPort) => {
|
||||||
|
const index = this.portForwards.findIndex(portForwardsEqual(portForward));
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.portForwards[index] = new PortForwardItem(portForward);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start an existing port-forward
|
||||||
|
* @param portForward the port-forward to start. If the forwardPort field is 0 then an arbitrary port will be
|
||||||
|
* used
|
||||||
|
*
|
||||||
|
* @returns the port-forward with updated status ("Active" if successfully started, "Disabled" otherwise) and
|
||||||
|
* forwardPort
|
||||||
|
*
|
||||||
|
* @throws if the port-forward does not already exist in the store
|
||||||
|
*/
|
||||||
|
start = action(async (portForward: ForwardedPort): Promise<ForwardedPort> => {
|
||||||
|
const pf = this.findPortForward(portForward);
|
||||||
|
|
||||||
|
if (!pf) {
|
||||||
|
throw new Error("cannot start non-existent port-forward");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { port, forwardPort } = pf;
|
||||||
let response: PortForwardResult;
|
let response: PortForwardResult;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const protocol = portForward.protocol ?? "http";
|
const protocol = pf.protocol ?? "http";
|
||||||
|
|
||||||
response = await apiBase.post<PortForwardResult>(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}`, { query: { port, forwardPort, protocol }});
|
response = await apiBase.post<PortForwardResult>(
|
||||||
|
`/pods/port-forward/${pf.namespace}/${pf.kind}/${pf.name}`,
|
||||||
|
{ query: { port, forwardPort, protocol }},
|
||||||
|
);
|
||||||
|
|
||||||
// expecting the received port to be the specified port, unless the specified port is 0, which indicates any available port is suitable
|
// expecting the received port to be the specified port, unless the specified port is 0, which indicates any available port is suitable
|
||||||
if (portForward.forwardPort && response?.port && response.port != +portForward.forwardPort) {
|
if (
|
||||||
logger.warn(`[PORT-FORWARD-STORE] specified ${portForward.forwardPort} got ${response.port}`);
|
pf.forwardPort &&
|
||||||
|
response?.port &&
|
||||||
|
response.port != +pf.forwardPort
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
`[PORT-FORWARD-STORE] specified ${pf.forwardPort}, got ${response.port}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pf.forwardPort = response.port;
|
||||||
|
pf.status = "Active";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("[PORT-FORWARD-STORE] Error adding port-forward:", error, portForward);
|
logger.warn(
|
||||||
throw (error);
|
`[PORT-FORWARD-STORE] Error starting port-forward: ${error}`,
|
||||||
|
pf,
|
||||||
|
);
|
||||||
|
pf.status = "Disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reset();
|
this.setPortForward(pf);
|
||||||
|
|
||||||
return response?.port;
|
return pf as ForwardedPort;
|
||||||
};
|
});
|
||||||
|
|
||||||
remove = async (portForward: ForwardedPort) => {
|
/**
|
||||||
const { port, forwardPort } = portForward;
|
* get a port-forward from the store, with up-to-date status
|
||||||
|
* @param portForward the port-forward to get.
|
||||||
|
*
|
||||||
|
* @returns the port-forward with updated status ("Active" if running, "Disabled" if not) and
|
||||||
|
* forwardPort used.
|
||||||
|
*
|
||||||
|
* @throws if the port-forward does not exist in the store
|
||||||
|
*/
|
||||||
|
getPortForward = async (
|
||||||
|
portForward: ForwardedPort,
|
||||||
|
): Promise<ForwardedPort> => {
|
||||||
|
if (!this.findPortForward(portForward)) {
|
||||||
|
throw new Error("port-forward not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let pf: ForwardedPort;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiBase.del(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}`, { query: { port, forwardPort }});
|
// check if the port-forward is active, and if so check if it has the same local port
|
||||||
await waitUntilFree(+forwardPort, 200, 1000);
|
pf = await getActivePortForward(portForward);
|
||||||
|
|
||||||
|
if (pf.forwardPort && pf.forwardPort !== portForward.forwardPort) {
|
||||||
|
logger.warn(
|
||||||
|
`[PORT-FORWARD-STORE] local port, expected ${pf.forwardPort}, got ${portForward.forwardPort}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("[PORT-FORWARD-STORE] Error removing port-forward:", error, portForward);
|
// port is not active
|
||||||
throw (error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reset();
|
return pf;
|
||||||
};
|
|
||||||
|
|
||||||
modify = async (portForward: ForwardedPort, desiredPort: number): Promise<number> => {
|
|
||||||
await this.remove(portForward);
|
|
||||||
|
|
||||||
portForward.forwardPort = desiredPort;
|
|
||||||
|
|
||||||
const port = await this.add(portForward);
|
|
||||||
|
|
||||||
this.reset();
|
|
||||||
|
|
||||||
return port;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PortForwardResult {
|
interface PortForwardResult {
|
||||||
port: number;
|
port: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PortForwardsResult {
|
function portForwardsEqual(portForward: ForwardedPort) {
|
||||||
portForwards: ForwardedPort[];
|
return (pf: ForwardedPort) => (
|
||||||
|
pf.kind == portForward.kind &&
|
||||||
|
pf.name == portForward.name &&
|
||||||
|
pf.namespace == portForward.namespace &&
|
||||||
|
pf.port == portForward.port
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPortForward(portForward: ForwardedPort): Promise<number> {
|
async function getActivePortForward(portForward: ForwardedPort): Promise<ForwardedPort> {
|
||||||
const { port, forwardPort, protocol } = portForward;
|
const { port, forwardPort, protocol } = portForward;
|
||||||
let response: PortForwardResult;
|
let response: PortForwardResult;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await apiBase.get<PortForwardResult>(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}`, { query: { port, forwardPort, protocol }});
|
response = await apiBase.get<PortForwardResult>(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}`, { query: { port, forwardPort, protocol }});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("[PORT-FORWARD-STORE] Error getting port-forward:", error, portForward);
|
logger.warn(`[PORT-FORWARD-STORE] Error getting active port-forward: ${error}`, portForward);
|
||||||
throw (error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response?.port;
|
portForward.status = response?.port ? "Active" : "Disabled";
|
||||||
}
|
portForward.forwardPort = response?.port;
|
||||||
|
|
||||||
|
return portForward;
|
||||||
export async function getPortForwards(clusterId?: string): Promise<ForwardedPort[]> {
|
|
||||||
try {
|
|
||||||
const response = await apiBase.get<PortForwardsResult>("/pods/port-forwards", { query: { clusterId }});
|
|
||||||
|
|
||||||
return response.portForwards;
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn("[PORT-FORWARD-STORE] Error getting all port-forwards:", error);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,6 @@ export function openPortForward(portForward: ForwardedPort) {
|
|||||||
openExternal(browseTo)
|
openExternal(browseTo)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
logger.error(`failed to open in browser: ${error}`, {
|
logger.error(`failed to open in browser: ${error}`, {
|
||||||
clusterId: portForward.clusterId,
|
|
||||||
port: portForward.port,
|
port: portForward.port,
|
||||||
kind: portForward.kind,
|
kind: portForward.kind,
|
||||||
namespace: portForward.namespace,
|
namespace: portForward.namespace,
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
import { SearchStore } from "./search-store";
|
import { SearchStore } from "./search-store";
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
import { getDiForUnitTesting } from "../components/getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
import searchStoreInjectable from "./search-store.injectable";
|
import searchStoreInjectable from "./search-store.injectable";
|
||||||
import directoryForUserDataInjectable
|
import directoryForUserDataInjectable
|
||||||
from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
|||||||
@ -19,18 +19,18 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import type { Disposer } from "../utils";
|
||||||
|
|
||||||
function hotbarIndex(id: string) {
|
function addWindowEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): Disposer {
|
||||||
return HotbarStore.getInstance().hotbarIndex(id) + 1;
|
window.addEventListener(type, listener, options);
|
||||||
|
|
||||||
|
return () => void window.removeEventListener(type, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hotbarDisplayLabel(id: string) : string {
|
const windowAddEventListenerInjectable = getInjectable({
|
||||||
const hotbar = HotbarStore.getInstance().getById(id);
|
instantiate: () => addWindowEventListener,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
return `${hotbarIndex(id)}: ${hotbar.name}`;
|
export default windowAddEventListenerInjectable;
|
||||||
}
|
|
||||||
|
|
||||||
export function hotbarDisplayIndex(id: string) : string {
|
|
||||||
return hotbarIndex(id).toString();
|
|
||||||
}
|
|
||||||
@ -18,7 +18,7 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import { getDiForUnitTesting as getRendererDi } from "../renderer/components/getDiForUnitTesting";
|
import { getDiForUnitTesting as getRendererDi } from "../renderer/getDiForUnitTesting";
|
||||||
import { getDiForUnitTesting as getMainDi } from "../main/getDiForUnitTesting";
|
import { getDiForUnitTesting as getMainDi } from "../main/getDiForUnitTesting";
|
||||||
import { overrideIpcBridge } from "./override-ipc-bridge";
|
import { overrideIpcBridge } from "./override-ipc-bridge";
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user