mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add CRDs to Command Palette and fix bugs
- Deprecate the "scope" on command registrations as it is buggy and generally unfixable - Switch to mounting CommandContainer in each new frame so that there is no need to broadcast anything other than opening palette - Add CRD entries to the command registry within each cluster - Add navigate to CommandActionContext to simplify handling of root frame scoped URLs - Switch to using DI for some of the deps Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
4f75acf2b4
commit
abe90ad92a
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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 } from "mobx";
|
||||
import { crdStore } from "../../../../renderer/components/+custom-resources/crd.store";
|
||||
|
||||
const customResourceDefinitionsInjectable = getInjectable({
|
||||
instantiate: () => computed(() => [...crdStore.items]),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default customResourceDefinitionsInjectable;
|
||||
@ -201,3 +201,18 @@ export function every<T>(src: Iterable<T>, fn: (val: T) => any): boolean {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a new iterator that drains the first and then the second
|
||||
* @param first The first iterable to iterate through
|
||||
* @param second The second iterable to iterate through
|
||||
*/
|
||||
export function* chain<T>(first: Iterable<T>, second: Iterable<T>): IterableIterator<T> {
|
||||
for (const t of first) {
|
||||
yield t;
|
||||
}
|
||||
|
||||
for (const t of second) {
|
||||
yield t;
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import mockFs from "mock-fs";
|
||||
import { watch } from "chokidar";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
import path from "path";
|
||||
@ -30,6 +29,7 @@ import { AppPaths } from "../../common/app-paths";
|
||||
import type { ExtensionLoader } from "../extension-loader";
|
||||
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import * as fse from "fs-extra";
|
||||
|
||||
jest.setTimeout(60_000);
|
||||
|
||||
@ -43,6 +43,7 @@ jest.mock("../extension-installer", () => ({
|
||||
installPackage: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock("fs-extra");
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
@ -63,6 +64,7 @@ AppPaths.init();
|
||||
|
||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
||||
const mockedFse = fse as jest.Mocked<typeof fse>;
|
||||
|
||||
describe("ExtensionDiscovery", () => {
|
||||
let extensionLoader: ExtensionLoader;
|
||||
@ -77,22 +79,20 @@ describe("ExtensionDiscovery", () => {
|
||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
});
|
||||
|
||||
describe("with mockFs", () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]: JSON.stringify({
|
||||
name: "my-extension",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("emits add for added extension", async (done) => {
|
||||
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 = {
|
||||
on: jest.fn((event: string, handler: typeof addHandler) => {
|
||||
if (event === "add") {
|
||||
@ -106,7 +106,6 @@ describe("ExtensionDiscovery", () => {
|
||||
mockedWatch.mockImplementationOnce(() =>
|
||||
(mockWatchInstance) as any,
|
||||
);
|
||||
|
||||
const extensionDiscovery = ExtensionDiscovery.createInstance(
|
||||
extensionLoader,
|
||||
);
|
||||
@ -125,6 +124,7 @@ describe("ExtensionDiscovery", () => {
|
||||
isCompatible: false,
|
||||
manifest: {
|
||||
name: "my-extension",
|
||||
version: "1.0.0",
|
||||
},
|
||||
manifestPath: path.normalize("node_modules/my-extension/package.json"),
|
||||
});
|
||||
@ -133,7 +133,6 @@ describe("ExtensionDiscovery", () => {
|
||||
|
||||
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json"));
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't emit add for added file under extension", async done => {
|
||||
let addHandler: (filePath: string) => void;
|
||||
@ -172,3 +171,4 @@ describe("ExtensionDiscovery", () => {
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -262,7 +262,6 @@ export class ExtensionLoader {
|
||||
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
|
||||
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
|
||||
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
|
||||
registries.CommandRegistry.getInstance().add(extension.commands),
|
||||
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
|
||||
];
|
||||
|
||||
@ -293,7 +292,6 @@ export class ExtensionLoader {
|
||||
registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems),
|
||||
registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts),
|
||||
registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems),
|
||||
registries.CommandRegistry.getInstance().add(extension.commands),
|
||||
];
|
||||
|
||||
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 { 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 { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands";
|
||||
|
||||
export class LensRendererExtension extends LensExtension {
|
||||
globalPages: registries.PageRegistration[] = [];
|
||||
@ -42,7 +43,7 @@ export class LensRendererExtension extends LensExtension {
|
||||
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
|
||||
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
|
||||
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
||||
commands: registries.CommandRegistration[] = [];
|
||||
commands: CommandRegistration[] = [];
|
||||
welcomeMenus: WelcomeMenuRegistration[] = [];
|
||||
welcomeBanners: WelcomeBannerRegistration[] = [];
|
||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||
|
||||
@ -28,7 +28,6 @@ export * from "./status-bar-registry";
|
||||
export * from "./kube-object-detail-registry";
|
||||
export * from "./kube-object-menu-registry";
|
||||
export * from "./kube-object-status-registry";
|
||||
export * from "./command-registry";
|
||||
export * from "./entity-setting-registry";
|
||||
export * from "./catalog-entity-detail-registry";
|
||||
export * from "./workloads-overview-detail-registry";
|
||||
|
||||
@ -81,7 +81,7 @@ describe("kubeconfig manager tests", () => {
|
||||
let contextHandler: ContextHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockOpts = {
|
||||
mockFs({
|
||||
"minikube-config.yml": JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
clusters: [{
|
||||
@ -103,9 +103,7 @@ describe("kubeconfig manager tests", () => {
|
||||
kind: "Config",
|
||||
preferences: {},
|
||||
}),
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
});
|
||||
|
||||
cluster = new Cluster({
|
||||
id: "foo",
|
||||
|
||||
@ -24,11 +24,15 @@ import { createContainer } from "@ogre-tools/injectable";
|
||||
export const getDi = () =>
|
||||
createContainer(
|
||||
getRequireContextForMainCode,
|
||||
getRequireContextForCommonCode,
|
||||
getRequireContextForCommonExtensionCode,
|
||||
);
|
||||
|
||||
const getRequireContextForMainCode = () =>
|
||||
require.context("./", true, /\.injectable\.(ts|tsx)$/);
|
||||
|
||||
const getRequireContextForCommonCode = () =>
|
||||
require.context("../common", true, /\.injectable\.(ts|tsx)$/);
|
||||
|
||||
const getRequireContextForCommonExtensionCode = () =>
|
||||
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/);
|
||||
|
||||
@ -216,8 +216,14 @@ export function getAppMenu(
|
||||
label: "Command Palette...",
|
||||
accelerator: "Shift+CmdOrCtrl+P",
|
||||
id: "command-palette",
|
||||
click() {
|
||||
click(_m, _b, event) {
|
||||
/**
|
||||
* Don't broadcast unless it was triggered by menu iteration so that
|
||||
* there aren't double events in renderer
|
||||
*/
|
||||
if (!event?.triggeredByAccelerator) {
|
||||
broadcastMessage("command-palette:open");
|
||||
}
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
|
||||
@ -32,6 +32,7 @@ import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { CatalogIpcEvents } from "../../common/ipc/catalog";
|
||||
import { navigate } from "../navigation";
|
||||
import { isMainFrame } from "process";
|
||||
|
||||
export type EntityFilter = (entity: CatalogEntity) => any;
|
||||
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
|
||||
ipcRenderer.send(CatalogIpcEvents.INIT);
|
||||
|
||||
if (isMainFrame) {
|
||||
ipcRendererOn("catalog-entity:run", (event, id: string) => {
|
||||
const entity = this.getById(id);
|
||||
|
||||
if (entity) {
|
||||
this.onRun(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {
|
||||
|
||||
@ -21,13 +21,14 @@
|
||||
|
||||
import { when } from "mobx";
|
||||
import { catalogCategoryRegistry } from "../../../common/catalog";
|
||||
import { catalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry";
|
||||
import { isActiveRoute } from "../../../renderer/navigation";
|
||||
import { catalogEntityRegistry } from "../catalog-entity-registry";
|
||||
import { isActiveRoute } from "../../navigation";
|
||||
import type { GeneralEntity } from "../../../common/catalog-entities";
|
||||
|
||||
export async function setEntityOnRouteMatch() {
|
||||
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));
|
||||
|
||||
if (activeEntity) {
|
||||
@ -49,7 +49,7 @@ import { SentryInit } from "../common/sentry";
|
||||
import { TerminalStore } from "./components/dock/terminal.store";
|
||||
import { AppPaths } from "../common/app-paths";
|
||||
import { registerCustomThemes } from "./components/monaco-editor";
|
||||
import { getDi } from "./components/getDi";
|
||||
import { getDi } from "./getDi";
|
||||
import { DiContextProvider } from "@ogre-tools/injectable-react";
|
||||
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
||||
@ -102,9 +102,6 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
||||
logger.info(`${logPrefix} initializing Registries`);
|
||||
initializers.initRegistries();
|
||||
|
||||
logger.info(`${logPrefix} initializing CommandRegistry`);
|
||||
initializers.initCommandRegistry();
|
||||
|
||||
logger.info(`${logPrefix} initializing EntitySettingsRegistry`);
|
||||
initializers.initEntitySettingsRegistry();
|
||||
|
||||
|
||||
@ -66,22 +66,15 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
|
||||
@computed get groups() {
|
||||
const groups: Record<string, CustomResourceDefinition[]> = {};
|
||||
|
||||
return this.items.reduce((groups, crd) => {
|
||||
const group = crd.getGroup();
|
||||
|
||||
if (!groups[group]) groups[group] = [];
|
||||
groups[group].push(crd);
|
||||
for (const crd of this.items) {
|
||||
(groups[crd.getGroup()] ??= []).push(crd);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, groups);
|
||||
}
|
||||
|
||||
getByGroup(group: string, pluralName: string) {
|
||||
const crdInGroup = this.groups[group];
|
||||
|
||||
if (!crdInGroup) return null;
|
||||
|
||||
return crdInGroup.find(crd => crd.getPluralName() === pluralName);
|
||||
return this.groups[group]?.find(crd => crd.getPluralName() === pluralName);
|
||||
}
|
||||
|
||||
getByObject(obj: KubeObject) {
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
import { computed } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { CommandOverlay } from "../command-palette";
|
||||
@ -37,7 +38,7 @@ export class ActivateEntityCommand extends React.Component {
|
||||
}
|
||||
|
||||
onSelect(entity: CatalogEntity): void {
|
||||
catalogEntityRegistry.onRun(entity);
|
||||
broadcastMessage("catalog-entity:run", entity.getId());
|
||||
CommandOverlay.close();
|
||||
}
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ import * as routes from "../../../common/routes";
|
||||
import { DeleteClusterDialog } from "../delete-cluster-dialog";
|
||||
import { reaction } from "mobx";
|
||||
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 { TopBar } from "../layout/top-bar/top-bar";
|
||||
|
||||
|
||||
@ -21,20 +21,30 @@
|
||||
|
||||
|
||||
import "./command-container.scss";
|
||||
import { observer } from "mobx-react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { Dialog } from "../dialog";
|
||||
import { ipcRendererOn } from "../../../common/ipc";
|
||||
import { CommandDialog } from "./command-dialog";
|
||||
import type { ClusterId } from "../../../common/cluster-types";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { CommandRegistration, CommandRegistry } from "../../../extensions/registries/command-registry";
|
||||
import { CommandOverlay } from "./command-overlay";
|
||||
import { isMac } from "../../../common/vars";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { broadcastMessage, ipcRendererOn } from "../../../common/ipc";
|
||||
import { getMatchedClusterId } from "../../navigation";
|
||||
import type { Disposer } from "../../utils";
|
||||
|
||||
export interface CommandContainerProps {
|
||||
clusterId?: ClusterId;
|
||||
}
|
||||
|
||||
function addWindowEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): Disposer {
|
||||
window.addEventListener(type, listener, options);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(type, listener);
|
||||
};
|
||||
}
|
||||
|
||||
@observer
|
||||
export class CommandContainer extends React.Component<CommandContainerProps> {
|
||||
private escHandler(event: KeyboardEvent) {
|
||||
@ -44,31 +54,39 @@ export class CommandContainer extends React.Component<CommandContainerProps> {
|
||||
}
|
||||
}
|
||||
|
||||
private findCommandById(commandId: string) {
|
||||
return CommandRegistry.getInstance().getItems().find((command) => command.id === commandId);
|
||||
}
|
||||
handleCommandPalette = () => {
|
||||
const clusterIsActive = getMatchedClusterId() !== undefined;
|
||||
|
||||
private runCommand(command: CommandRegistration) {
|
||||
command.action({
|
||||
entity: catalogEntityRegistry.activeEntity,
|
||||
});
|
||||
if (clusterIsActive) {
|
||||
broadcastMessage(`command-palette:${catalogEntityRegistry.activeEntity.getId()}:open`);
|
||||
} 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() {
|
||||
if (this.props.clusterId) {
|
||||
ipcRendererOn(`command-palette:run-action:${this.props.clusterId}`, (event, commandId: string) => {
|
||||
const command = this.findCommandById(commandId);
|
||||
const action = this.props.clusterId
|
||||
? () => CommandOverlay.open(<CommandDialog />)
|
||||
: this.handleCommandPalette;
|
||||
const ipcChannel = this.props.clusterId
|
||||
? `command-palette:${this.props.clusterId}:open`
|
||||
: "command-palette:open";
|
||||
|
||||
if (command) {
|
||||
this.runCommand(command);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ipcRendererOn("command-palette:open", () => {
|
||||
CommandOverlay.open(<CommandDialog />);
|
||||
});
|
||||
}
|
||||
window.addEventListener("keyup", (e) => this.escHandler(e), true);
|
||||
disposeOnUnmount(this, [
|
||||
ipcRendererOn(ipcChannel, action),
|
||||
addWindowEventListener("keydown", this.onKeyboardShortcut(action)),
|
||||
addWindowEventListener("keyup", (e) => this.escHandler(e), true),
|
||||
]);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@ -21,61 +21,31 @@
|
||||
|
||||
|
||||
import { Select } from "../select";
|
||||
import { computed, makeObservable, observable } from "mobx";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { CommandRegistry } from "../../../extensions/registries/command-registry";
|
||||
import React, { useState } from "react";
|
||||
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 { 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
|
||||
export class CommandDialog extends React.Component {
|
||||
@observable menuIsOpen = true;
|
||||
@observable searchValue: any = undefined;
|
||||
interface Dependencies {
|
||||
commands: IComputedValue<Map<string, RegisteredCommand>>;
|
||||
activeEntity?: CatalogEntity;
|
||||
}
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
const NonInjectedCommandDialog = observer(({ commands, activeEntity }: Dependencies) => {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
|
||||
@computed get activeEntity(): CatalogEntity | undefined {
|
||||
return catalogEntityRegistry.activeEntity;
|
||||
}
|
||||
|
||||
@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);
|
||||
const executeAction = (commandId: string) => {
|
||||
const command = commands.get().get(commandId);
|
||||
|
||||
if (!command) {
|
||||
return;
|
||||
@ -83,46 +53,73 @@ export class CommandDialog extends React.Component {
|
||||
|
||||
try {
|
||||
CommandOverlay.close();
|
||||
|
||||
if (command.scope === "global") {
|
||||
command.action({
|
||||
entity: this.activeEntity,
|
||||
});
|
||||
} else if(this.activeEntity) {
|
||||
navigate(clusterViewURL({
|
||||
params: {
|
||||
clusterId: this.activeEntity.metadata.uid,
|
||||
},
|
||||
}));
|
||||
broadcastMessage(`command-palette:run-action:${this.activeEntity.metadata.uid}`, command.id);
|
||||
entity: activeEntity,
|
||||
navigate: (url, opts = {}) => {
|
||||
const { forceRootFrame = false } = opts;
|
||||
|
||||
if (forceRootFrame) {
|
||||
broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, url);
|
||||
} else {
|
||||
navigate(url);
|
||||
}
|
||||
} catch(error) {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[COMMAND-DIALOG] failed to execute command", command.id, error);
|
||||
}
|
||||
};
|
||||
|
||||
const context = {
|
||||
entity: activeEntity,
|
||||
};
|
||||
const activeCommands = iter.filter(commands.get().values(), command => {
|
||||
try {
|
||||
return command.isActive(context);
|
||||
} catch (error) {
|
||||
console.error(`[COMMAND-DIALOG]: isActive for ${command.id} threw an error, defaulting to false`, error);
|
||||
}
|
||||
|
||||
render() {
|
||||
return false;
|
||||
});
|
||||
const options = Array.from(activeCommands, ({ id, title }) => ({
|
||||
value: id,
|
||||
label: typeof title === "function"
|
||||
? title(context)
|
||||
: title,
|
||||
}));
|
||||
|
||||
// Make sure the options are in the correct order
|
||||
orderBy(options, "label", "asc");
|
||||
|
||||
return (
|
||||
<Select
|
||||
menuPortalTarget={null}
|
||||
onChange={v => this.onChange(v.value)}
|
||||
onChange={v => executeAction(v.value)}
|
||||
components={{
|
||||
DropdownIndicator: null,
|
||||
IndicatorSeparator: null,
|
||||
}}
|
||||
menuIsOpen={this.menuIsOpen}
|
||||
options={this.options}
|
||||
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") {
|
||||
this.searchValue = newValue;
|
||||
setSearchValue(newValue);
|
||||
}
|
||||
}}
|
||||
inputValue={this.searchValue}
|
||||
inputValue={searchValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const CommandDialog = withInjectables<Dependencies>(NonInjectedCommandDialog, {
|
||||
getProps: di => ({
|
||||
commands: di.inject(registeredCommandsInjectable),
|
||||
// TODO: replace with injection
|
||||
activeEntity: catalogEntityRegistry.activeEntity,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -19,34 +19,55 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Extensions API -> Commands
|
||||
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
import type { LensExtension } from "../lens-extension";
|
||||
import type { CatalogEntity } from "../../common/catalog";
|
||||
import type { CatalogEntity } from "../../../../common/catalog";
|
||||
|
||||
/**
|
||||
* The context given to commands when executed
|
||||
*/
|
||||
export interface CommandContext {
|
||||
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 {
|
||||
/**
|
||||
* The ID of the command, must be globally unique
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
export class CommandRegistry extends BaseRegistry<CommandRegistration> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
export type RegisteredCommand = Required<Omit<CommandRegistration, "scope">>;
|
||||
@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 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 } from "../../../../extensions/registries";
|
||||
import { CommandOverlay } from "../../../components/command-palette";
|
||||
import { createTerminalTab } from "../../../components/dock/terminal.store";
|
||||
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 { HotbarRenameCommand } from "../../../components/hotbar/hotbar-rename-command";
|
||||
import { ActivateEntityCommand } from "../../../components/activate-entity-command";
|
||||
import type { CommandContext, CommandRegistration } from "./commands";
|
||||
|
||||
export function isKubernetesClusterActive(context: CommandContext): boolean {
|
||||
return context.entity?.kind === "KubernetesCluster";
|
||||
}
|
||||
|
||||
export const internalCommands: CommandRegistration[] = [
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO: replace with injection
|
||||
const entries = EntitySettingRegistry.getInstance()
|
||||
.getItemsForKind(entity.kind, entity.apiVersion, entity.metadata.source);
|
||||
|
||||
return entries.length > 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "cluster.openTerminal",
|
||||
title: "Cluster: Open terminal",
|
||||
action: () => createTerminalTab(),
|
||||
isActive: isKubernetesClusterActive,
|
||||
},
|
||||
{
|
||||
id: "hotbar.switchHotbar",
|
||||
title: "Hotbar: Switch ...",
|
||||
action: () => CommandOverlay.open(<HotbarSwitchCommand />),
|
||||
},
|
||||
{
|
||||
id: "hotbar.addHotbar",
|
||||
title: "Hotbar: Add Hotbar ...",
|
||||
action: () => CommandOverlay.open(<HotbarAddCommand />),
|
||||
},
|
||||
{
|
||||
id: "hotbar.removeHotbar",
|
||||
title: "Hotbar: Remove Hotbar ...",
|
||||
action: () => CommandOverlay.open(<HotbarRemoveCommand />),
|
||||
},
|
||||
{
|
||||
id: "hotbar.renameHotbar",
|
||||
title: "Hotbar: Rename Hotbar ...",
|
||||
action: () => CommandOverlay.open(<HotbarRenameCommand />),
|
||||
},
|
||||
{
|
||||
id: "catalog.searchEntities",
|
||||
title: "Catalog: Activate Entity ...",
|
||||
action: () => CommandOverlay.open(<ActivateEntityCommand />),
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 "../../../../common/k8s-api/endpoints/custom-resources/custom-resources.injectable";
|
||||
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||
import { iter } from "../../../utils";
|
||||
import type { RegisteredCommand } from "./commands";
|
||||
import { internalCommands, isKubernetesClusterActive } from "./internal-commands";
|
||||
|
||||
interface Dependencies {
|
||||
extensions: IComputedValue<LensRendererExtension[]>;
|
||||
customResourceDefinitions: IComputedValue<CustomResourceDefinition[]>;
|
||||
}
|
||||
|
||||
const instantiateRegisteredCommands = ({ extensions, customResourceDefinitions }: Dependencies) => computed(() => {
|
||||
const result = new Map<string, RegisteredCommand>();
|
||||
const commands = iter.chain(
|
||||
internalCommands,
|
||||
iter.chain(
|
||||
iter.flatMap(extensions.get(), extension => extension.commands),
|
||||
iter.map(customResourceDefinitions.get(), command => ({
|
||||
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),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default registeredCommandsInjectable;
|
||||
@ -103,6 +103,10 @@ export class Input extends React.Component<InputProps, State> {
|
||||
submitted: false,
|
||||
};
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.setDirtyOnChange.cancel();
|
||||
}
|
||||
|
||||
setValue(value = "") {
|
||||
if (value !== this.getValue()) {
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(this.input.constructor.prototype, "value").set;
|
||||
|
||||
@ -280,5 +280,5 @@ const addDynamicMenuItem = ({
|
||||
|
||||
const kubeObjectMenuRegistry = di.inject(kubeObjectMenuRegistryInjectable);
|
||||
|
||||
kubeObjectMenuRegistry.add(dynamicMenuItemStub);
|
||||
kubeObjectMenuRegistry.add([dynamicMenuItemStub]);
|
||||
};
|
||||
|
||||
@ -24,11 +24,15 @@ import { createContainer } from "@ogre-tools/injectable";
|
||||
export const getDi = () =>
|
||||
createContainer(
|
||||
getRequireContextForRendererCode,
|
||||
getRequireContextForCommonCode,
|
||||
getRequireContextForCommonExtensionCode,
|
||||
);
|
||||
|
||||
const getRequireContextForRendererCode = () =>
|
||||
require.context("../", true, /\.injectable\.(ts|tsx)$/);
|
||||
require.context("./", true, /\.injectable\.(ts|tsx)$/);
|
||||
|
||||
const getRequireContextForCommonCode = () =>
|
||||
require.context("../common", true, /\.injectable\.(ts|tsx)$/);
|
||||
|
||||
const getRequireContextForCommonExtensionCode = () =>
|
||||
require.context("../../extensions", true, /\.injectable\.(ts|tsx)$/);
|
||||
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/);
|
||||
@ -1,207 +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 { createTerminalTab } from "../components/dock/terminal.store";
|
||||
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() {
|
||||
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";
|
||||
export * from "./command-registry";
|
||||
export * from "./entity-settings-registry";
|
||||
export * from "./ipc";
|
||||
export * from "./kube-object-detail-registry";
|
||||
|
||||
@ -26,7 +26,6 @@ export function initRegistries() {
|
||||
registries.CatalogEntityDetailRegistry.createInstance();
|
||||
registries.ClusterPageMenuRegistry.createInstance();
|
||||
registries.ClusterPageRegistry.createInstance();
|
||||
registries.CommandRegistry.createInstance();
|
||||
registries.EntitySettingRegistry.createInstance();
|
||||
registries.GlobalPageRegistry.createInstance();
|
||||
registries.KubeObjectDetailRegistry.createInstance();
|
||||
|
||||
@ -54,7 +54,7 @@ export function isActiveRoute(route: string | string[] | RouteProps): boolean {
|
||||
return !!matchRoute(route);
|
||||
}
|
||||
|
||||
export function getMatchedClusterId(): string {
|
||||
export function getMatchedClusterId(): string | undefined {
|
||||
const matched = matchPath<ClusterViewRouteParams>(navigation.location.pathname, {
|
||||
exact: true,
|
||||
path: clusterViewRoute.path,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user