diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts
index 25afaa76fe..8b9b132114 100644
--- a/src/extensions/lens-renderer-extension.ts
+++ b/src/extensions/lens-renderer-extension.ts
@@ -2,6 +2,7 @@ import type { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPage
import type { Cluster } from "../main/cluster";
import { LensExtension } from "./lens-extension";
import { getExtensionPageUrl } from "./registries/page-registry";
+import { CommandRegistration } from "./registries/command-registry";
export class LensRendererExtension extends LensExtension {
globalPages: PageRegistration[] = [];
@@ -14,6 +15,7 @@ export class LensRendererExtension extends LensExtension {
statusBarItems: StatusBarRegistration[] = [];
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
+ commands: CommandRegistration[] = [];
async navigate
(pageId?: string, params?: P) {
const { navigate } = await import("../renderer/navigation");
diff --git a/src/extensions/registries/command-registry.ts b/src/extensions/registries/command-registry.ts
new file mode 100644
index 0000000000..359702c29f
--- /dev/null
+++ b/src/extensions/registries/command-registry.ts
@@ -0,0 +1,22 @@
+// Extensions API -> Commands
+
+import type { Cluster } from "../../main/cluster";
+import type { Workspace } from "../../common/workspace-store";
+import { BaseRegistry } from "./base-registry";
+
+export type CommandContext = {
+ cluster?: Cluster;
+ workspace?: Workspace;
+};
+
+export interface CommandRegistration {
+ id: string;
+ title: string;
+ action: (context: CommandContext) => void;
+ isActive?: (context: CommandContext) => boolean;
+}
+
+export class CommandRegistry extends BaseRegistry {
+}
+
+export const commandRegistry = new CommandRegistry();
diff --git a/src/renderer/components/+preferences/preferences.route.ts b/src/renderer/components/+preferences/preferences.route.ts
index 7c880bd5fc..6937ecb0dd 100644
--- a/src/renderer/components/+preferences/preferences.route.ts
+++ b/src/renderer/components/+preferences/preferences.route.ts
@@ -1,8 +1,16 @@
import type { RouteProps } from "react-router";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
import { buildURL } from "../../../common/utils/buildUrl";
+import { navigate } from "../../navigation";
export const preferencesRoute: RouteProps = {
path: "/preferences"
};
export const preferencesURL = buildURL(preferencesRoute.path);
+
+commandRegistry.add({
+ id: "app.showPreferences",
+ title: "Preferences: Open",
+ action: () => navigate(preferencesURL.toString())
+});
diff --git a/src/renderer/components/+workspaces/workspaces.tsx b/src/renderer/components/+workspaces/workspaces.tsx
index 932306adab..fa5897f5c8 100644
--- a/src/renderer/components/+workspaces/workspaces.tsx
+++ b/src/renderer/components/+workspaces/workspaces.tsx
@@ -12,6 +12,8 @@ import { cssNames, prevDefault } from "../../utils";
import { Button } from "../button";
import { isRequired, InputValidator } from "../input/input_validators";
import { clusterStore } from "../../../common/cluster-store";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { navigate } from "../../navigation";
@observer
export class Workspaces extends React.Component {
@@ -214,3 +216,9 @@ export class Workspaces extends React.Component {
);
}
}
+
+commandRegistry.add({
+ id: "workspace.showList",
+ title: "Workspace: Open list",
+ action: () => navigate("/workspaces")
+});
diff --git a/src/renderer/components/command-palette/command-dialog.scss b/src/renderer/components/command-palette/command-dialog.scss
new file mode 100644
index 0000000000..0d03c4d0e9
--- /dev/null
+++ b/src/renderer/components/command-palette/command-dialog.scss
@@ -0,0 +1,28 @@
+#command-dialog {
+ position: absolute;
+ top: 20px;
+ width: 40%;
+ height: none !important;
+ left: 0;
+ right: 0;
+ margin-left: auto;
+ margin-right: auto;
+ z-index: 1000;
+ background-color: var(--dockInfoBackground);
+
+ ul {
+ margin-top: 10px;
+ li {
+ padding-left: 4px;
+ margin-bottom: 10px;
+ a {
+ text-decoration: none;
+ border-bottom: none;
+ }
+ }
+ li:hover {
+
+ cursor: pointer;
+ }
+ }
+}
diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx
new file mode 100644
index 0000000000..55605f9f8b
--- /dev/null
+++ b/src/renderer/components/command-palette/command-dialog.tsx
@@ -0,0 +1,103 @@
+
+import "./command-dialog.scss";
+import { Select } from "../select";
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import React from "react";
+import { commandRegistry } from "../../../extensions/registries/command-registry";
+import { Dialog } from "../dialog";
+import { isMac } from "../../../common/vars";
+import { clusterStore } from "../../../common/cluster-store";
+import { workspaceStore } from "../../../common/workspace-store";
+
+@observer
+export class CommandDialog extends React.Component {
+ @observable visible = false;
+ @observable menuIsOpen = true;
+
+ private escHandler(event: KeyboardEvent) {
+ if (event.key === "Escape") {
+ event.stopPropagation();
+ this.closeDialog();
+ }
+ }
+
+ @action
+ private shortcutHandler(event: KeyboardEvent) {
+ console.log(event);
+
+ if (isMac) {
+ if (event.shiftKey && event.metaKey && event.key === "p") {
+ this.visible = true;
+ this.menuIsOpen = true;
+ event.stopPropagation();
+ }
+ } else {
+ if (event.shiftKey && event.ctrlKey && event.key === "p") {
+ this.visible = true;
+ this.menuIsOpen = true;
+ event.stopPropagation();
+ }
+ }
+ }
+
+ private closeDialog() {
+ this.menuIsOpen = false;
+ setTimeout(() => {
+ this.visible = false;
+ }, 1000);
+ }
+
+ componentDidMount() {
+ window.addEventListener("keyup", (e) => this.escHandler(e), true);
+ window.addEventListener("keydown", (e) => this.shortcutHandler(e), true);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("keyup", this.escHandler);
+ }
+
+ @computed get options() {
+ return commandRegistry.getItems().map((command) => {
+ return { value: command.id, label: command.title };
+ });
+ }
+
+ private onChange(value: string) {
+ const command = commandRegistry.getItems().find((cmd) => cmd.id === value);
+
+ console.log(value, command);
+
+ if (!command) {
+ return;
+ }
+
+ try {
+ command.action({
+ cluster: clusterStore.active,
+ workspace: workspaceStore.currentWorkspace
+ });
+ } catch(error) {
+ console.error("failed to execute command", command.id, error);
+ } finally {
+ this.closeDialog();
+ }
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/src/renderer/lens-app.tsx b/src/renderer/lens-app.tsx
index 6d77aa12e0..fa47d2f71c 100644
--- a/src/renderer/lens-app.tsx
+++ b/src/renderer/lens-app.tsx
@@ -11,6 +11,7 @@ import { Notifications } from "./components/notifications";
import { ConfirmDialog } from "./components/confirm-dialog";
import { extensionLoader } from "../extensions/extension-loader";
import { broadcastMessage } from "../common/ipc";
+import { CommandDialog } from "./components/command-palette/command-dialog";
@observer
export class LensApp extends React.Component {
@@ -36,6 +37,7 @@ export class LensApp extends React.Component {
+
);
}