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
4224937260
@ -37,9 +37,9 @@ export default class ExampleMainExtension extends Main.LensExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### App Menus
|
### Menus
|
||||||
|
|
||||||
This extension can register custom app menus that will be displayed on OS native menus.
|
This extension can register custom app and tray menus that will be displayed on OS native menus.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@ -56,6 +56,29 @@ export default class ExampleMainExtension extends Main.LensExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
trayMenus = [
|
||||||
|
{
|
||||||
|
label: "My links",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Lens",
|
||||||
|
click() {
|
||||||
|
Main.Navigation.navigate("https://k8slens.dev");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Lens Github",
|
||||||
|
click() {
|
||||||
|
Main.Navigation.navigate("https://github.com/lensapp/lens");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
The Main Extension API is the interface to Lens's main process.
|
The Main Extension API is the interface to Lens's main process.
|
||||||
Lens runs in both main and renderer processes.
|
Lens runs in both main and renderer processes.
|
||||||
The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items and [protocol handlers](protocol-handlers.md), and run custom code in Lens's main process.
|
The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items and [protocol handlers](protocol-handlers.md), and run custom code in Lens's main process.
|
||||||
It also provides convenient methods for navigating to built-in Lens pages and extension pages, as well as adding and removing sources of catalog entities.
|
It also provides convenient methods for navigating to built-in Lens pages and extension pages, as well as adding and removing sources of catalog entities.
|
||||||
|
|
||||||
## `Main.LensExtension` Class
|
## `Main.LensExtension` Class
|
||||||
|
|
||||||
@ -45,7 +45,6 @@ For more details on accessing Lens state data, please see the [Stores](../stores
|
|||||||
### `appMenus`
|
### `appMenus`
|
||||||
|
|
||||||
The Main Extension API allows you to customize the UI application menu.
|
The Main Extension API allows you to customize the UI application menu.
|
||||||
Note that this is the only UI feature that the Main Extension API allows you to customize.
|
|
||||||
The following example demonstrates adding an item to the **Help** menu.
|
The following example demonstrates adding an item to the **Help** menu.
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
@ -65,7 +64,7 @@ export default class SamplePageMainExtension extends Main.LensExtension {
|
|||||||
```
|
```
|
||||||
|
|
||||||
`appMenus` is an array of objects that satisfy the `MenuRegistration` interface.
|
`appMenus` is an array of objects that satisfy the `MenuRegistration` interface.
|
||||||
`MenuRegistration` extends React's `MenuItemConstructorOptions` interface.
|
`MenuRegistration` extends Electron's `MenuItemConstructorOptions` interface.
|
||||||
The properties of the appMenus array objects are defined as follows:
|
The properties of the appMenus array objects are defined as follows:
|
||||||
|
|
||||||
* `parentId` is the name of the menu where your new menu item will be listed.
|
* `parentId` is the name of the menu where your new menu item will be listed.
|
||||||
@ -96,6 +95,35 @@ export default class SamplePageMainExtension extends Main.LensExtension {
|
|||||||
When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
|
When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
|
||||||
This page would be defined in your extension's `Renderer.LensExtension` implementation (See [`Renderer.LensExtension`](renderer-extension.md)).
|
This page would be defined in your extension's `Renderer.LensExtension` implementation (See [`Renderer.LensExtension`](renderer-extension.md)).
|
||||||
|
|
||||||
|
### `trayMenus`
|
||||||
|
|
||||||
|
`trayMenus` is an array of `TrayMenuRegistration` objects. Most importantly you can define a `label` and a `click` handler. Other properties are `submenu`, `enabled`, `toolTip`, `id` and `type`.
|
||||||
|
|
||||||
|
``` typescript
|
||||||
|
interface TrayMenuRegistration {
|
||||||
|
label?: string;
|
||||||
|
click?: (menuItem: TrayMenuRegistration) => void;
|
||||||
|
id?: string;
|
||||||
|
type?: "normal" | "separator" | "submenu"
|
||||||
|
toolTip?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
submenu?: TrayMenuRegistration[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The following example demonstrates how tray menus can be added from extension:
|
||||||
|
|
||||||
|
``` typescript
|
||||||
|
import { Main } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
export default class SampleTrayMenuMainExtension extends Main.LensExtension {
|
||||||
|
trayMenus = [{
|
||||||
|
label: "menu from the extension",
|
||||||
|
click: () => { console.log("tray menu clicked!") }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### `addCatalogSource()` and `removeCatalogSource()` Methods
|
### `addCatalogSource()` and `removeCatalogSource()` Methods
|
||||||
|
|
||||||
The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities).
|
The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities).
|
||||||
|
|||||||
@ -272,10 +272,7 @@ export class ExtensionLoader {
|
|||||||
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.CommandRegistry.getInstance().add(extension.commands),
|
||||||
registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus),
|
|
||||||
registries.WelcomeBannerRegistry.getInstance().add(extension.welcomeBanners),
|
|
||||||
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
|
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
|
||||||
registries.TopBarRegistry.getInstance().add(extension.topBarItems),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
||||||
|
|||||||
@ -25,9 +25,10 @@ import { catalogEntityRegistry } from "../main/catalog";
|
|||||||
import type { CatalogEntity } from "../common/catalog";
|
import type { CatalogEntity } from "../common/catalog";
|
||||||
import type { IObservableArray } from "mobx";
|
import type { IObservableArray } from "mobx";
|
||||||
import type { MenuRegistration } from "../main/menu/menu-registration";
|
import type { MenuRegistration } from "../main/menu/menu-registration";
|
||||||
|
import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration";
|
||||||
export class LensMainExtension extends LensExtension {
|
export class LensMainExtension extends LensExtension {
|
||||||
appMenus: MenuRegistration[] = [];
|
appMenus: MenuRegistration[] = [];
|
||||||
|
trayMenus: TrayMenuRegistration[] = [];
|
||||||
|
|
||||||
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
|
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
|
||||||
return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId);
|
return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId);
|
||||||
|
|||||||
@ -26,7 +26,10 @@ import type { CatalogEntity } from "../common/catalog";
|
|||||||
import type { Disposer } from "../common/utils";
|
import type { Disposer } from "../common/utils";
|
||||||
import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry";
|
import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry";
|
||||||
import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry";
|
import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry";
|
||||||
|
import type { TopBarRegistration } from "../renderer/components/layout/top-bar/top-bar-registration";
|
||||||
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 { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration";
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: registries.PageRegistration[] = [];
|
globalPages: registries.PageRegistration[] = [];
|
||||||
@ -40,10 +43,10 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
|
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
|
||||||
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
||||||
commands: registries.CommandRegistration[] = [];
|
commands: registries.CommandRegistration[] = [];
|
||||||
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
welcomeMenus: WelcomeMenuRegistration[] = [];
|
||||||
welcomeBanners: registries.WelcomeBannerRegistration[] = [];
|
welcomeBanners: WelcomeBannerRegistration[] = [];
|
||||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||||
topBarItems: registries.TopBarRegistration[] = [];
|
topBarItems: TopBarRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||||
const { navigate } = await import("../renderer/navigation");
|
const { navigate } = await import("../renderer/navigation");
|
||||||
|
|||||||
@ -30,9 +30,6 @@ export * from "./kube-object-menu-registry";
|
|||||||
export * from "./kube-object-status-registry";
|
export * from "./kube-object-status-registry";
|
||||||
export * from "./command-registry";
|
export * from "./command-registry";
|
||||||
export * from "./entity-setting-registry";
|
export * from "./entity-setting-registry";
|
||||||
export * from "./welcome-menu-registry";
|
|
||||||
export * from "./welcome-banner-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";
|
||||||
export * from "./topbar-registry";
|
|
||||||
export * from "./protocol-handler";
|
export * from "./protocol-handler";
|
||||||
|
|||||||
33
src/extensions/renderer-extensions.injectable.ts
Normal file
33
src/extensions/renderer-extensions.injectable.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 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 type { IComputedValue } from "mobx";
|
||||||
|
import extensionsInjectable from "./extensions.injectable";
|
||||||
|
import type { LensRendererExtension } from "./lens-renderer-extension";
|
||||||
|
|
||||||
|
const rendererExtensionsInjectable = getInjectable({
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|
||||||
|
instantiate: (di) =>
|
||||||
|
di.inject(extensionsInjectable) as IComputedValue<LensRendererExtension[]>,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rendererExtensionsInjectable;
|
||||||
@ -55,28 +55,23 @@ import { WeblinkStore } from "../common/weblink-store";
|
|||||||
import { SentryInit } from "../common/sentry";
|
import { SentryInit } from "../common/sentry";
|
||||||
import { ensureDir } from "fs-extra";
|
import { ensureDir } from "fs-extra";
|
||||||
import { initMenu } from "./menu/menu";
|
import { initMenu } from "./menu/menu";
|
||||||
import { initTray } from "./tray";
|
|
||||||
import { kubeApiRequest } from "./proxy-functions";
|
import { kubeApiRequest } from "./proxy-functions";
|
||||||
|
import { initTray } from "./tray/tray";
|
||||||
import { ShellSession } from "./shell-session/shell-session";
|
import { ShellSession } from "./shell-session/shell-session";
|
||||||
import { getDi } from "./getDi";
|
import { getDi } from "./getDi";
|
||||||
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
||||||
import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
|
import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||||
import extensionDiscoveryInjectable
|
import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable";
|
||||||
from "../extensions/extension-discovery/extension-discovery.injectable";
|
import directoryForExesInjectable from "../common/app-paths/directory-for-exes/directory-for-exes.injectable";
|
||||||
import directoryForExesInjectable
|
import initIpcMainHandlersInjectable from "./initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable";
|
||||||
from "../common/app-paths/directory-for-exes/directory-for-exes.injectable";
|
|
||||||
import initIpcMainHandlersInjectable
|
|
||||||
from "./initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable";
|
|
||||||
import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable";
|
import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable";
|
||||||
import directoryForKubeConfigsInjectable
|
import directoryForKubeConfigsInjectable from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||||
from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
import kubeconfigSyncManagerInjectable from "./catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable";
|
||||||
import kubeconfigSyncManagerInjectable
|
|
||||||
from "./catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable";
|
|
||||||
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
|
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
|
||||||
import routerInjectable from "./router/router.injectable";
|
import routerInjectable from "./router/router.injectable";
|
||||||
import shellApiRequestInjectable
|
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
|
||||||
from "./proxy-functions/shell-api-request/shell-api-request.injectable";
|
|
||||||
import userStoreInjectable from "../common/user-store/user-store.injectable";
|
import userStoreInjectable from "../common/user-store/user-store.injectable";
|
||||||
|
import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable";
|
||||||
|
|
||||||
const di = getDi();
|
const di = getDi();
|
||||||
|
|
||||||
@ -255,10 +250,11 @@ di.runSetups().then(() => {
|
|||||||
const windowManager = WindowManager.createInstance();
|
const windowManager = WindowManager.createInstance();
|
||||||
|
|
||||||
const menuItems = di.inject(electronMenuItemsInjectable);
|
const menuItems = di.inject(electronMenuItemsInjectable);
|
||||||
|
const trayMenuItems = di.inject(trayMenuItemsInjectable);
|
||||||
|
|
||||||
onQuitCleanup.push(
|
onQuitCleanup.push(
|
||||||
initMenu(windowManager, menuItems),
|
initMenu(windowManager, menuItems),
|
||||||
initTray(windowManager),
|
initTray(windowManager, trayMenuItems),
|
||||||
() => ShellSession.cleanup(),
|
() => ShellSession.cleanup(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -29,8 +29,7 @@ const electronMenuItemsInjectable = getInjectable({
|
|||||||
const extensions = di.inject(mainExtensionsInjectable);
|
const extensions = di.inject(mainExtensionsInjectable);
|
||||||
|
|
||||||
return computed(() =>
|
return computed(() =>
|
||||||
extensions.get().flatMap((extension) => extension.appMenus),
|
extensions.get().flatMap((extension) => extension.appMenus));
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "cluster":
|
case "cluster":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
|
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`);
|
||||||
case "workloadMemoryUsage":
|
case "workloadMemoryUsage":
|
||||||
return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`;
|
return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`;
|
||||||
case "memoryRequests":
|
case "memoryRequests":
|
||||||
@ -50,7 +50,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "memoryAllocatableCapacity":
|
case "memoryAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
|
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
|
||||||
case "cpuUsage":
|
case "cpuUsage":
|
||||||
return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
|
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||||
case "cpuRequests":
|
case "cpuRequests":
|
||||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
|
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
|
||||||
case "cpuLimits":
|
case "cpuLimits":
|
||||||
@ -66,31 +66,31 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "podAllocatableCapacity":
|
case "podAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
|
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
|
||||||
case "fsSize":
|
case "fsSize":
|
||||||
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||||
case "fsUsage":
|
case "fsUsage":
|
||||||
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "nodes":
|
case "nodes":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
|
return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
case "workloadMemoryUsage":
|
case "workloadMemoryUsage":
|
||||||
return `sum(container_memory_working_set_bytes{container!=""}) by (node)`;
|
return `sum(container_memory_working_set_bytes{container!="POD", container!=""}) by (node)`;
|
||||||
case "memoryCapacity":
|
case "memoryCapacity":
|
||||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||||
case "memoryAllocatableCapacity":
|
case "memoryAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
|
||||||
case "cpuUsage":
|
case "cpuUsage":
|
||||||
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`;
|
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
case "cpuCapacity":
|
case "cpuCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||||
case "cpuAllocatableCapacity":
|
case "cpuAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||||
case "fsSize":
|
case "fsSize":
|
||||||
return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`;
|
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
case "fsUsage":
|
case "fsUsage":
|
||||||
return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`;
|
return `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "pods":
|
case "pods":
|
||||||
|
|||||||
36
src/main/tray/tray-menu-items.injectable.ts
Normal file
36
src/main/tray/tray-menu-items.injectable.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 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 mainExtensionsInjectable from "../../extensions/main-extensions.injectable";
|
||||||
|
|
||||||
|
const trayItemsInjectable = getInjectable({
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const extensions = di.inject(mainExtensionsInjectable);
|
||||||
|
|
||||||
|
return computed(() =>
|
||||||
|
extensions.get().flatMap(extension => extension.trayMenus));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default trayItemsInjectable;
|
||||||
136
src/main/tray/tray-menu-items.test.ts
Normal file
136
src/main/tray/tray-menu-items.test.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* 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 type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
|
import { LensMainExtension } from "../../extensions/lens-main-extension";
|
||||||
|
import trayItemsInjectable from "./tray-menu-items.injectable";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
|
import { computed, ObservableMap, runInAction } from "mobx";
|
||||||
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
import mainExtensionsInjectable from "../../extensions/main-extensions.injectable";
|
||||||
|
import type { TrayMenuRegistration } from "./tray-menu-registration";
|
||||||
|
|
||||||
|
describe("tray-menu-items", () => {
|
||||||
|
let di: ConfigurableDependencyInjectionContainer;
|
||||||
|
let trayMenuItems: IComputedValue<TrayMenuRegistration[]>;
|
||||||
|
let extensionsStub: ObservableMap<string, LensMainExtension>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = getDiForUnitTesting();
|
||||||
|
|
||||||
|
extensionsStub = new ObservableMap();
|
||||||
|
|
||||||
|
di.override(
|
||||||
|
mainExtensionsInjectable,
|
||||||
|
() => computed(() => [...extensionsStub.values()]),
|
||||||
|
);
|
||||||
|
|
||||||
|
trayMenuItems = di.inject(trayItemsInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not have any items yet", () => {
|
||||||
|
expect(trayMenuItems.get()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when extension is enabled", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const someExtension = new SomeTestExtension({
|
||||||
|
id: "some-extension-id",
|
||||||
|
trayMenus: [{ label: "tray-menu-from-some-extension" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
extensionsStub.set("some-extension-id", someExtension);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has tray menu items", () => {
|
||||||
|
expect(trayMenuItems.get()).toEqual([
|
||||||
|
{
|
||||||
|
label: "tray-menu-from-some-extension",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when disabling extension, does not have tray menu items", () => {
|
||||||
|
runInAction(() => {
|
||||||
|
extensionsStub.delete("some-extension-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(trayMenuItems.get()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when other extension is enabled", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const someOtherExtension = new SomeTestExtension({
|
||||||
|
id: "some-extension-id",
|
||||||
|
trayMenus: [{ label: "some-label-from-second-extension" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
extensionsStub.set("some-other-extension-id", someOtherExtension);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has tray menu items for both extensions", () => {
|
||||||
|
expect(trayMenuItems.get()).toEqual([
|
||||||
|
{
|
||||||
|
label: "tray-menu-from-some-extension",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: "some-label-from-second-extension",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when extension is disabled, still returns tray menu items for extensions that are enabled", () => {
|
||||||
|
runInAction(() => {
|
||||||
|
extensionsStub.delete("some-other-extension-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(trayMenuItems.get()).toEqual([
|
||||||
|
{
|
||||||
|
label: "tray-menu-from-some-extension",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class SomeTestExtension extends LensMainExtension {
|
||||||
|
constructor({ id, trayMenus }: {
|
||||||
|
id: string;
|
||||||
|
trayMenus: TrayMenuRegistration[];
|
||||||
|
}) {
|
||||||
|
super({
|
||||||
|
id,
|
||||||
|
absolutePath: "irrelevant",
|
||||||
|
isBundled: false,
|
||||||
|
isCompatible: false,
|
||||||
|
isEnabled: false,
|
||||||
|
manifest: { name: id, version: "some-version" },
|
||||||
|
manifestPath: "irrelevant",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.trayMenus = trayMenus;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,17 +19,12 @@
|
|||||||
* 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 { catalogURL } from "../../common/routes";
|
export interface TrayMenuRegistration {
|
||||||
import { WelcomeMenuRegistry } from "../../extensions/registries";
|
label?: string;
|
||||||
import { navigate } from "../navigation";
|
click?: (menuItem: TrayMenuRegistration) => void;
|
||||||
|
id?: string;
|
||||||
export function initWelcomeMenuRegistry() {
|
type?: "normal" | "separator" | "submenu"
|
||||||
WelcomeMenuRegistry.getInstance()
|
toolTip?: string;
|
||||||
.add([
|
enabled?: boolean;
|
||||||
{
|
submenu?: TrayMenuRegistration[]
|
||||||
title: "Browse Clusters in Catalog",
|
|
||||||
icon: "view_list",
|
|
||||||
click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }} )),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
@ -20,16 +20,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import packageInfo from "../../package.json";
|
import packageInfo from "../../../package.json";
|
||||||
import { Menu, Tray } from "electron";
|
import { Menu, Tray } from "electron";
|
||||||
import { autorun } from "mobx";
|
import { autorun, IComputedValue } from "mobx";
|
||||||
import { showAbout } from "./menu/menu";
|
import { showAbout } from "../menu/menu";
|
||||||
import { checkForUpdates, isAutoUpdateEnabled } from "./app-updater";
|
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
|
||||||
import type { WindowManager } from "./window-manager";
|
import type { WindowManager } from "../window-manager";
|
||||||
import logger from "./logger";
|
import logger from "../logger";
|
||||||
import { isDevelopment, isWindows, productName } from "../common/vars";
|
import { isDevelopment, isWindows, productName } from "../../common/vars";
|
||||||
import { exitApp } from "./exit-app";
|
import { exitApp } from "../exit-app";
|
||||||
import { preferencesURL } from "../common/routes";
|
import { preferencesURL } from "../../common/routes";
|
||||||
|
import { toJS } from "../../common/utils";
|
||||||
|
import type { TrayMenuRegistration } from "./tray-menu-registration";
|
||||||
|
|
||||||
const TRAY_LOG_PREFIX = "[TRAY]";
|
const TRAY_LOG_PREFIX = "[TRAY]";
|
||||||
|
|
||||||
@ -44,7 +46,10 @@ export function getTrayIcon(): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initTray(windowManager: WindowManager) {
|
export function initTray(
|
||||||
|
windowManager: WindowManager,
|
||||||
|
trayMenuItems: IComputedValue<TrayMenuRegistration[]>,
|
||||||
|
) {
|
||||||
const icon = getTrayIcon();
|
const icon = getTrayIcon();
|
||||||
|
|
||||||
tray = new Tray(icon);
|
tray = new Tray(icon);
|
||||||
@ -62,7 +67,7 @@ export function initTray(windowManager: WindowManager) {
|
|||||||
const disposers = [
|
const disposers = [
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
try {
|
try {
|
||||||
const menu = createTrayMenu(windowManager);
|
const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()));
|
||||||
|
|
||||||
tray.setContextMenu(menu);
|
tray.setContextMenu(menu);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -78,8 +83,21 @@ export function initTray(windowManager: WindowManager) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTrayMenu(windowManager: WindowManager): Menu {
|
function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions {
|
||||||
const template: Electron.MenuItemConstructorOptions[] = [
|
return {
|
||||||
|
...trayItem,
|
||||||
|
submenu: trayItem.submenu ? trayItem.submenu.map(getMenuItemConstructorOptions) : undefined,
|
||||||
|
click: trayItem.click ? () => {
|
||||||
|
trayItem.click(trayItem);
|
||||||
|
} : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTrayMenu(
|
||||||
|
windowManager: WindowManager,
|
||||||
|
extensionTrayItems: TrayMenuRegistration[],
|
||||||
|
): Menu {
|
||||||
|
let template: Electron.MenuItemConstructorOptions[] = [
|
||||||
{
|
{
|
||||||
label: `Open ${productName}`,
|
label: `Open ${productName}`,
|
||||||
click() {
|
click() {
|
||||||
@ -108,6 +126,8 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template = template.concat(extensionTrayItems.map(getMenuItemConstructorOptions));
|
||||||
|
|
||||||
return Menu.buildFromTemplate(template.concat([
|
return Menu.buildFromTemplate(template.concat([
|
||||||
{
|
{
|
||||||
label: `About ${productName}`,
|
label: `About ${productName}`,
|
||||||
@ -107,9 +107,6 @@ export async function bootstrap(di: DependencyInjectionContainer) {
|
|||||||
logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`);
|
logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`);
|
||||||
initializers.initKubeObjectDetailRegistry();
|
initializers.initKubeObjectDetailRegistry();
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing WelcomeMenuRegistry`);
|
|
||||||
initializers.initWelcomeMenuRegistry();
|
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`);
|
logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`);
|
||||||
initializers.initWorkloadsOverviewDetailRegistry();
|
initializers.initWorkloadsOverviewDetailRegistry();
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { ThemeStore } from "../../theme.store";
|
|||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { isWindows } from "../../../common/vars";
|
import { isWindows } from "../../../common/vars";
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
import { Switch } from "../switch";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
|
import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
|
||||||
import { action } from "mobx";
|
import { action } from "mobx";
|
||||||
@ -86,16 +86,12 @@ export const Application = observer(() => {
|
|||||||
|
|
||||||
<section id="terminalSelection">
|
<section id="terminalSelection">
|
||||||
<SubTitle title="Terminal copy & paste" />
|
<SubTitle title="Terminal copy & paste" />
|
||||||
<FormSwitch
|
<Switch
|
||||||
label="Copy on select and paste on right-click"
|
checked={userStore.terminalCopyOnSelect}
|
||||||
control={
|
onChange={() => userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect}
|
||||||
<Switcher
|
>
|
||||||
checked={userStore.terminalCopyOnSelect}
|
Copy on select and paste on right-click
|
||||||
onChange={v => userStore.terminalCopyOnSelect = v.target.checked}
|
</Switch>
|
||||||
name="terminalCopyOnSelect"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
@ -135,16 +131,9 @@ export const Application = observer(() => {
|
|||||||
|
|
||||||
<section id="other">
|
<section id="other">
|
||||||
<SubTitle title="Start-up"/>
|
<SubTitle title="Start-up"/>
|
||||||
<FormSwitch
|
<Switch checked={userStore.openAtLogin} onChange={() => userStore.openAtLogin = !userStore.openAtLogin}>
|
||||||
control={
|
Automatically start Lens on login
|
||||||
<Switcher
|
</Switch>
|
||||||
checked={userStore.openAtLogin}
|
|
||||||
onChange={v => userStore.openAtLogin = v.target.checked}
|
|
||||||
name="startup"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Automatically start Lens on login"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
import { Switch } from "../switch";
|
||||||
import { Select } from "../select";
|
import { Select } from "../select";
|
||||||
import { SubTitle } from "../layout/sub-title";
|
import { SubTitle } from "../layout/sub-title";
|
||||||
import { SubHeader } from "../layout/sub-header";
|
import { SubHeader } from "../layout/sub-header";
|
||||||
@ -45,15 +45,12 @@ export const Editor = observer(() => {
|
|||||||
<section>
|
<section>
|
||||||
<div className="flex gaps justify-space-between">
|
<div className="flex gaps justify-space-between">
|
||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
<FormSwitch
|
<Switch
|
||||||
label={<SubHeader compact>Show minimap</SubHeader>}
|
checked={editorConfiguration.minimap.enabled}
|
||||||
control={
|
onChange={() => editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled}
|
||||||
<Switcher
|
>
|
||||||
checked={editorConfiguration.minimap.enabled}
|
Show minimap
|
||||||
onChange={(evt, checked) => editorConfiguration.minimap.enabled = checked}
|
</Switch>
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
<SubHeader compact>Position</SubHeader>
|
<SubHeader compact>Position</SubHeader>
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { SubTitle } from "../layout/sub-title";
|
|||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { bundledKubectlPath } from "../../../main/kubectl/kubectl";
|
import { bundledKubectlPath } from "../../../main/kubectl/kubectl";
|
||||||
import { SelectOption, Select } from "../select";
|
import { SelectOption, Select } from "../select";
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
import { Switch } from "../switch";
|
||||||
import { packageMirrors } from "../../../common/user-store/preferences-helpers";
|
import { packageMirrors } from "../../../common/user-store/preferences-helpers";
|
||||||
import directoryForBinariesInjectable
|
import directoryForBinariesInjectable
|
||||||
from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
|
from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
|
||||||
@ -54,16 +54,12 @@ const NonInjectedKubectlBinaries: React.FC<Dependencies> = (({ defaultPathForKub
|
|||||||
<>
|
<>
|
||||||
<section>
|
<section>
|
||||||
<SubTitle title="Kubectl binary download"/>
|
<SubTitle title="Kubectl binary download"/>
|
||||||
<FormSwitch
|
<Switch
|
||||||
control={
|
checked={userStore.downloadKubectlBinaries}
|
||||||
<Switcher
|
onChange={() => userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries}
|
||||||
checked={userStore.downloadKubectlBinaries}
|
>
|
||||||
onChange={v => userStore.downloadKubectlBinaries = v.target.checked}
|
Download kubectl binaries matching the Kubernetes cluster version
|
||||||
name="kubectl-download"
|
</Switch>
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Download kubectl binaries matching the Kubernetes cluster version"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@ -24,10 +24,11 @@ import React from "react";
|
|||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { SubTitle } from "../layout/sub-title";
|
import { SubTitle } from "../layout/sub-title";
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
import { Switch } from "../switch";
|
||||||
|
|
||||||
export const LensProxy = observer(() => {
|
export const LensProxy = observer(() => {
|
||||||
const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || "");
|
const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || "");
|
||||||
|
const store = UserStore.getInstance();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="proxy">
|
<section id="proxy">
|
||||||
@ -50,16 +51,9 @@ export const LensProxy = observer(() => {
|
|||||||
|
|
||||||
<section className="small">
|
<section className="small">
|
||||||
<SubTitle title="Certificate Trust"/>
|
<SubTitle title="Certificate Trust"/>
|
||||||
<FormSwitch
|
<Switch checked={store.allowUntrustedCAs} onChange={() => store.allowUntrustedCAs = !store.allowUntrustedCAs}>
|
||||||
control={
|
Allow untrusted Certificate Authorities
|
||||||
<Switcher
|
</Switch>
|
||||||
checked={UserStore.getInstance().allowUntrustedCAs}
|
|
||||||
onChange={v => UserStore.getInstance().allowUntrustedCAs = v.target.checked}
|
|
||||||
name="startup"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Allow untrusted Certificate Authorities"
|
|
||||||
/>
|
|
||||||
<small className="hint">
|
<small className="hint">
|
||||||
This will make Lens to trust ANY certificate authority without any validations.{" "}
|
This will make Lens to trust ANY certificate authority without any validations.{" "}
|
||||||
Needed with some corporate proxies that do certificate re-writing.{" "}
|
Needed with some corporate proxies that do certificate re-writing.{" "}
|
||||||
|
|||||||
@ -20,45 +20,55 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { screen } from "@testing-library/react";
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { Welcome } from "../welcome";
|
import { defaultWidth, Welcome } from "../welcome";
|
||||||
import { TopBarRegistry, WelcomeMenuRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries";
|
import { computed } from "mobx";
|
||||||
import { defaultWidth } from "../welcome";
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import type { DiRender } from "../../test-utils/renderFor";
|
||||||
|
import { renderFor } from "../../test-utils/renderFor";
|
||||||
|
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
|
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||||
|
import { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||||
|
import type { WelcomeBannerRegistration } from "../welcome-banner-items/welcome-banner-registration";
|
||||||
|
|
||||||
jest.mock(
|
jest.mock("electron", () => ({
|
||||||
"electron",
|
ipcRenderer: {
|
||||||
() => ({
|
on: jest.fn(),
|
||||||
ipcRenderer: {
|
},
|
||||||
on: jest.fn(),
|
app: {
|
||||||
},
|
getPath: () => "tmp",
|
||||||
app: {
|
},
|
||||||
getPath: () => "tmp",
|
}));
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
describe("<Welcome/>", () => {
|
describe("<Welcome/>", () => {
|
||||||
beforeEach(() => {
|
let render: DiRender;
|
||||||
TopBarRegistry.createInstance();
|
let di: ConfigurableDependencyInjectionContainer;
|
||||||
WelcomeMenuRegistry.createInstance();
|
let welcomeBannersStub: WelcomeBannerRegistration[];
|
||||||
WelcomeBannerRegistry.createInstance();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
beforeEach(() => {
|
||||||
TopBarRegistry.resetInstance();
|
di = getDiForUnitTesting();
|
||||||
WelcomeMenuRegistry.resetInstance();
|
|
||||||
WelcomeBannerRegistry.resetInstance();
|
render = renderFor(di);
|
||||||
|
|
||||||
|
welcomeBannersStub = [];
|
||||||
|
|
||||||
|
di.override(rendererExtensionsInjectable, () =>
|
||||||
|
computed(() => [
|
||||||
|
new TestExtension({
|
||||||
|
id: "some-id",
|
||||||
|
welcomeBanners: welcomeBannersStub,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders <Banner /> registered in WelcomeBannerRegistry and hide logo", async () => {
|
it("renders <Banner /> registered in WelcomeBannerRegistry and hide logo", async () => {
|
||||||
const testId = "testId";
|
const testId = "testId";
|
||||||
|
|
||||||
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
welcomeBannersStub.push({
|
||||||
{
|
Banner: () => <div data-testid={testId} />,
|
||||||
Banner: () => <div data-testid={testId} />,
|
});
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { container } = render(<Welcome />);
|
const { container } = render(<Welcome />);
|
||||||
|
|
||||||
@ -67,16 +77,15 @@ describe("<Welcome/>", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => {
|
it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => {
|
||||||
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
welcomeBannersStub.push({
|
||||||
{
|
width: 100,
|
||||||
width: 100,
|
Banner: () => <div />,
|
||||||
Banner: () => <div />,
|
});
|
||||||
},
|
|
||||||
{
|
welcomeBannersStub.push({
|
||||||
width: 800,
|
width: 800,
|
||||||
Banner: () => <div />,
|
Banner: () => <div />,
|
||||||
},
|
});
|
||||||
]);
|
|
||||||
|
|
||||||
render(<Welcome />);
|
render(<Welcome />);
|
||||||
|
|
||||||
@ -92,3 +101,25 @@ describe("<Welcome/>", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class TestExtension extends LensRendererExtension {
|
||||||
|
constructor({
|
||||||
|
id,
|
||||||
|
welcomeBanners,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
welcomeBanners: WelcomeBannerRegistration[];
|
||||||
|
}) {
|
||||||
|
super({
|
||||||
|
id,
|
||||||
|
absolutePath: "irrelevant",
|
||||||
|
isBundled: false,
|
||||||
|
isCompatible: false,
|
||||||
|
isEnabled: false,
|
||||||
|
manifest: { name: id, version: "some-version" },
|
||||||
|
manifestPath: "irrelevant",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.welcomeBanners = welcomeBanners;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* 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 rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
|
||||||
|
const welcomeBannerItemsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => {
|
||||||
|
const extensions = di.inject(rendererExtensionsInjectable);
|
||||||
|
|
||||||
|
return computed(() => [
|
||||||
|
...extensions.get().flatMap((extension) => extension.welcomeBanners),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default welcomeBannerItemsInjectable;
|
||||||
@ -19,8 +19,6 @@
|
|||||||
* 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 { BaseRegistry } from "./base-registry";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WelcomeBannerRegistration is for an extension to register
|
* WelcomeBannerRegistration is for an extension to register
|
||||||
* Provide a Banner component to be renderered in the welcome screen.
|
* Provide a Banner component to be renderered in the welcome screen.
|
||||||
@ -35,5 +33,3 @@ export interface WelcomeBannerRegistration {
|
|||||||
*/
|
*/
|
||||||
width?: number
|
width?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WelcomeBannerRegistry extends BaseRegistry<WelcomeBannerRegistration> { }
|
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 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 { computed, IComputedValue } from "mobx";
|
||||||
|
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||||
|
import { navigate } from "../../../navigation";
|
||||||
|
import { catalogURL } from "../../../../common/routes";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
extensions: IComputedValue<LensRendererExtension[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getWelcomeMenuItems = ({ extensions }: Dependencies) => {
|
||||||
|
const browseClusters = {
|
||||||
|
title: "Browse Clusters in Catalog",
|
||||||
|
icon: "view_list",
|
||||||
|
click: () =>
|
||||||
|
navigate(
|
||||||
|
catalogURL({
|
||||||
|
params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return computed(() => [
|
||||||
|
browseClusters,
|
||||||
|
...extensions.get().flatMap((extension) => extension.welcomeMenus),
|
||||||
|
]);
|
||||||
|
};
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* 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 rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||||
|
import { getWelcomeMenuItems } from "./get-welcome-menu-items";
|
||||||
|
|
||||||
|
const welcomeMenuItemsInjectable = getInjectable({
|
||||||
|
instantiate: (di) =>
|
||||||
|
getWelcomeMenuItems({
|
||||||
|
extensions: di.inject(rendererExtensionsInjectable),
|
||||||
|
}),
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default welcomeMenuItemsInjectable;
|
||||||
@ -19,12 +19,8 @@
|
|||||||
* 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 { BaseRegistry } from "./base-registry";
|
|
||||||
|
|
||||||
export interface WelcomeMenuRegistration {
|
export interface WelcomeMenuRegistration {
|
||||||
title: string | (() => string);
|
title: string | (() => string);
|
||||||
icon: string;
|
icon: string;
|
||||||
click: () => void | Promise<void>;
|
click: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WelcomeMenuRegistry extends BaseRegistry<WelcomeMenuRegistration> {}
|
|
||||||
@ -22,78 +22,129 @@
|
|||||||
import "./welcome.scss";
|
import "./welcome.scss";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
import Carousel from "react-material-ui-carousel";
|
import Carousel from "react-material-ui-carousel";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { productName, slackUrl } from "../../../common/vars";
|
import { productName, slackUrl } from "../../../common/vars";
|
||||||
import { WelcomeMenuRegistry } from "../../../extensions/registries";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import { WelcomeBannerRegistry } from "../../../extensions/registries";
|
import welcomeMenuItemsInjectable from "./welcome-menu-items/welcome-menu-items.injectable";
|
||||||
|
import type { WelcomeMenuRegistration } from "./welcome-menu-items/welcome-menu-registration";
|
||||||
|
import welcomeBannerItemsInjectable from "./welcome-banner-items/welcome-banner-items.injectable";
|
||||||
|
import type { WelcomeBannerRegistration } from "./welcome-banner-items/welcome-banner-registration";
|
||||||
|
|
||||||
export const defaultWidth = 320;
|
export const defaultWidth = 320;
|
||||||
|
|
||||||
@observer
|
interface Dependencies {
|
||||||
export class Welcome extends React.Component {
|
welcomeMenuItems: IComputedValue<WelcomeMenuRegistration[]>
|
||||||
render() {
|
welcomeBannerItems: IComputedValue<WelcomeBannerRegistration[]>
|
||||||
const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems();
|
}
|
||||||
|
|
||||||
// if there is banner with specified width, use it to calculate the width of the container
|
const NonInjectedWelcome: React.FC<Dependencies> = ({ welcomeMenuItems, welcomeBannerItems }) => {
|
||||||
const maxWidth = welcomeBanner.reduce((acc, curr) => {
|
const welcomeBanners = welcomeBannerItems.get();
|
||||||
const currWidth = curr.width ?? 0;
|
|
||||||
|
|
||||||
if (acc > currWidth) {
|
// if there is banner with specified width, use it to calculate the width of the container
|
||||||
return acc;
|
const maxWidth = welcomeBanners.reduce((acc, curr) => {
|
||||||
}
|
const currWidth = curr.width ?? 0;
|
||||||
|
|
||||||
return currWidth;
|
if (acc > currWidth) {
|
||||||
}, defaultWidth);
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return currWidth;
|
||||||
<div className="flex justify-center Welcome align-center">
|
}, defaultWidth);
|
||||||
<div style={{ width: `${maxWidth}px` }} data-testid="welcome-banner-container">
|
|
||||||
{welcomeBanner.length > 0 ? (
|
return (
|
||||||
<Carousel
|
<div className="flex justify-center Welcome align-center">
|
||||||
stopAutoPlayOnHover={true}
|
<div
|
||||||
indicators={welcomeBanner.length > 1}
|
style={{ width: `${maxWidth}px` }}
|
||||||
autoPlay={true}
|
data-testid="welcome-banner-container"
|
||||||
navButtonsAlwaysInvisible={true}
|
>
|
||||||
indicatorIconButtonProps={{
|
{welcomeBanners.length > 0 ? (
|
||||||
style: {
|
<Carousel
|
||||||
color: "var(--iconActiveBackground)",
|
stopAutoPlayOnHover={true}
|
||||||
},
|
indicators={welcomeBanners.length > 1}
|
||||||
}}
|
autoPlay={true}
|
||||||
activeIndicatorIconButtonProps={{
|
navButtonsAlwaysInvisible={true}
|
||||||
style: {
|
indicatorIconButtonProps={{
|
||||||
color: "var(--iconActiveColor)",
|
style: {
|
||||||
},
|
color: "var(--iconActiveBackground)",
|
||||||
}}
|
},
|
||||||
interval={8000}
|
}}
|
||||||
|
activeIndicatorIconButtonProps={{
|
||||||
|
style: {
|
||||||
|
color: "var(--iconActiveColor)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
interval={8000}
|
||||||
|
>
|
||||||
|
{welcomeBanners.map((item, index) => (
|
||||||
|
<item.Banner key={index} />
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
) : (
|
||||||
|
<Icon svg="logo-lens" className="logo" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div
|
||||||
|
style={{ width: `${defaultWidth}px` }}
|
||||||
|
data-testid="welcome-text-container"
|
||||||
|
>
|
||||||
|
<h2>Welcome to {productName} 5!</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To get you started we have auto-detected your clusters in your
|
||||||
|
kubeconfig file and added them to the catalog, your centralized
|
||||||
|
view for managing all your cloud-native resources.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
If you have any questions or feedback, please join our{" "}
|
||||||
|
<a
|
||||||
|
href={slackUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="link"
|
||||||
|
>
|
||||||
|
Lens Community slack channel
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
className="block"
|
||||||
|
style={{ width: `${defaultWidth}px` }}
|
||||||
|
data-testid="welcome-menu-container"
|
||||||
>
|
>
|
||||||
{welcomeBanner.map((item, index) =>
|
{welcomeMenuItems.get().map((item, index) => (
|
||||||
<item.Banner key={index} />,
|
<li
|
||||||
)}
|
key={index}
|
||||||
</Carousel>
|
className="flex grid-12"
|
||||||
) : <Icon svg="logo-lens" className="logo" />}
|
onClick={() => item.click()}
|
||||||
|
>
|
||||||
<div className="flex justify-center">
|
<Icon material={item.icon} className="box col-1" />
|
||||||
<div style={{ width: `${defaultWidth}px` }} data-testid="welcome-text-container">
|
<a className="box col-10">
|
||||||
<h2>Welcome to {productName} 5!</h2>
|
{typeof item.title === "string"
|
||||||
|
? item.title
|
||||||
<p>
|
: item.title()}
|
||||||
To get you started we have auto-detected your clusters in your kubeconfig file and added them to the catalog, your centralized view for managing all your cloud-native resources.
|
</a>
|
||||||
<br /><br />
|
<Icon material="navigate_next" className="box col-1" />
|
||||||
If you have any questions or feedback, please join our <a href={slackUrl} target="_blank" rel="noreferrer" className="link">Lens Community slack channel</a>.
|
</li>
|
||||||
</p>
|
))}
|
||||||
|
</ul>
|
||||||
<ul className="block" style={{ width: `${defaultWidth}px` }} data-testid="welcome-menu-container">
|
|
||||||
{WelcomeMenuRegistry.getInstance().getItems().map((item, index) => (
|
|
||||||
<li key={index} className="flex grid-12" onClick={() => item.click()}>
|
|
||||||
<Icon material={item.icon} className="box col-1" /> <a className="box col-10">{typeof item.title === "string" ? item.title : item.title()}</a> <Icon material="navigate_next" className="box col-1" />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const Welcome = withInjectables<Dependencies>(
|
||||||
|
observer(NonInjectedWelcome),
|
||||||
|
|
||||||
|
{
|
||||||
|
getProps: (di) => ({
|
||||||
|
welcomeMenuItems: di.inject(welcomeMenuItemsInjectable),
|
||||||
|
welcomeBannerItems: di.inject(welcomeBannerItemsInjectable),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -91,6 +91,15 @@ html, body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#terminal-init {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
|||||||
@ -39,11 +39,10 @@ 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 "../../../main/catalog-sources/helpers/general-active-sync";
|
||||||
import { TopBar } from "../layout/topbar";
|
|
||||||
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 catalogPreviousActiveTabStorageInjectable
|
import { TopBar } from "../layout/top-bar/top-bar";
|
||||||
from "../+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
|
import catalogPreviousActiveTabStorageInjectable from "../+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
catalogPreviousActiveTabStorage: { get: () => string }
|
catalogPreviousActiveTabStorage: { get: () => string }
|
||||||
|
|||||||
@ -38,17 +38,9 @@ interface Dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Terminal {
|
export class Terminal {
|
||||||
public static readonly spawningPool = (() => {
|
public static get spawningPool() {
|
||||||
// terminal element must be in DOM before attaching via xterm.open(elem)
|
return document.getElementById("terminal-init");
|
||||||
// https://xtermjs.org/docs/api/terminal/classes/terminal/#open
|
}
|
||||||
const pool = document.createElement("div");
|
|
||||||
|
|
||||||
pool.className = "terminal-init";
|
|
||||||
pool.style.cssText = "position: absolute; top: 0; left: 0; height: 0; visibility: hidden; overflow: hidden";
|
|
||||||
document.body.appendChild(pool);
|
|
||||||
|
|
||||||
return pool;
|
|
||||||
})();
|
|
||||||
|
|
||||||
static async preloadFonts() {
|
static async preloadFonts() {
|
||||||
const fontPath = require("../../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires
|
const fontPath = require("../../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires
|
||||||
|
|||||||
@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
exports[`kube-object-menu given kube object renders 1`] = `
|
exports[`kube-object-menu given kube object renders 1`] = `
|
||||||
<body>
|
<body>
|
||||||
<div
|
|
||||||
class="terminal-init"
|
|
||||||
style="position: absolute; top: 0px; left: 0px; height: 0px; visibility: hidden; overflow: hidden;"
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<ul
|
<ul
|
||||||
@ -22,8 +18,8 @@ exports[`kube-object-menu given kube object renders 1`] = `
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="Icon material interactive focusable"
|
class="Icon material interactive focusable"
|
||||||
id="tooltip_target_2"
|
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
tooltip="Delete"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="icon"
|
class="icon"
|
||||||
@ -31,7 +27,6 @@ exports[`kube-object-menu given kube object renders 1`] = `
|
|||||||
>
|
>
|
||||||
delete
|
delete
|
||||||
</span>
|
</span>
|
||||||
<div />
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="title"
|
class="title"
|
||||||
@ -46,20 +41,11 @@ exports[`kube-object-menu given kube object renders 1`] = `
|
|||||||
class="Animate opacity-scale Dialog flex center ConfirmDialog modal"
|
class="Animate opacity-scale Dialog flex center ConfirmDialog modal"
|
||||||
style="--enter-duration: 100ms; --leave-duration: 100ms;"
|
style="--enter-duration: 100ms; --leave-duration: 100ms;"
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
class="Tooltip narrow invisible formatter"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`kube-object-menu given kube object when removing kube object renders 1`] = `
|
exports[`kube-object-menu given kube object when removing kube object renders 1`] = `
|
||||||
<body>
|
<body>
|
||||||
<div
|
|
||||||
class="terminal-init"
|
|
||||||
style="position: absolute; top: 0px; left: 0px; height: 0px; visibility: hidden; overflow: hidden;"
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<ul
|
<ul
|
||||||
@ -76,8 +62,8 @@ exports[`kube-object-menu given kube object when removing kube object renders 1`
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="Icon material interactive focusable"
|
class="Icon material interactive focusable"
|
||||||
id="tooltip_target_4"
|
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
tooltip="Delete"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="icon"
|
class="icon"
|
||||||
@ -85,7 +71,6 @@ exports[`kube-object-menu given kube object when removing kube object renders 1`
|
|||||||
>
|
>
|
||||||
delete
|
delete
|
||||||
</span>
|
</span>
|
||||||
<div />
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="title"
|
class="title"
|
||||||
@ -153,20 +138,11 @@ exports[`kube-object-menu given kube object when removing kube object renders 1`
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="Tooltip narrow invisible formatter"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`kube-object-menu given kube object with namespace when removing kube object, renders confirmation dialog with namespace 1`] = `
|
exports[`kube-object-menu given kube object with namespace when removing kube object, renders confirmation dialog with namespace 1`] = `
|
||||||
<body>
|
<body>
|
||||||
<div
|
|
||||||
class="terminal-init"
|
|
||||||
style="position: absolute; top: 0px; left: 0px; height: 0px; visibility: hidden; overflow: hidden;"
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<ul
|
<ul
|
||||||
@ -183,8 +159,8 @@ exports[`kube-object-menu given kube object with namespace when removing kube ob
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="Icon material interactive focusable"
|
class="Icon material interactive focusable"
|
||||||
id="tooltip_target_12"
|
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
tooltip="Delete"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="icon"
|
class="icon"
|
||||||
@ -192,7 +168,6 @@ exports[`kube-object-menu given kube object with namespace when removing kube ob
|
|||||||
>
|
>
|
||||||
delete
|
delete
|
||||||
</span>
|
</span>
|
||||||
<div />
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="title"
|
class="title"
|
||||||
@ -260,20 +235,11 @@ exports[`kube-object-menu given kube object with namespace when removing kube ob
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="Tooltip narrow invisible formatter"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`kube-object-menu given kube object without namespace when removing kube object, renders confirmation dialog without namespace 1`] = `
|
exports[`kube-object-menu given kube object without namespace when removing kube object, renders confirmation dialog without namespace 1`] = `
|
||||||
<body>
|
<body>
|
||||||
<div
|
|
||||||
class="terminal-init"
|
|
||||||
style="position: absolute; top: 0px; left: 0px; height: 0px; visibility: hidden; overflow: hidden;"
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<ul
|
<ul
|
||||||
@ -290,8 +256,8 @@ exports[`kube-object-menu given kube object without namespace when removing kube
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="Icon material interactive focusable"
|
class="Icon material interactive focusable"
|
||||||
id="tooltip_target_17"
|
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
tooltip="Delete"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="icon"
|
class="icon"
|
||||||
@ -299,7 +265,6 @@ exports[`kube-object-menu given kube object without namespace when removing kube
|
|||||||
>
|
>
|
||||||
delete
|
delete
|
||||||
</span>
|
</span>
|
||||||
<div />
|
|
||||||
</i>
|
</i>
|
||||||
<span
|
<span
|
||||||
class="title"
|
class="title"
|
||||||
@ -367,20 +332,11 @@ exports[`kube-object-menu given kube object without namespace when removing kube
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="Tooltip narrow invisible formatter"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`kube-object-menu given no kube object, renders 1`] = `
|
exports[`kube-object-menu given no kube object, renders 1`] = `
|
||||||
<body>
|
<body>
|
||||||
<div
|
|
||||||
class="terminal-init"
|
|
||||||
style="position: absolute; top: 0px; left: 0px; height: 0px; visibility: hidden; overflow: hidden;"
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<ul
|
<ul
|
||||||
class="Animate opacity Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
|
class="Animate opacity Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
|
||||||
|
|||||||
@ -42,6 +42,9 @@ import type { ApiManager } from "../../../common/k8s-api/api-manager";
|
|||||||
import apiManagerInjectable from "./dependencies/api-manager.injectable";
|
import apiManagerInjectable from "./dependencies/api-manager.injectable";
|
||||||
import { KubeObjectMenu } from "./index";
|
import { KubeObjectMenu } from "./index";
|
||||||
|
|
||||||
|
// TODO: Make tooltips free of side effects by making it deterministic
|
||||||
|
jest.mock("../tooltip");
|
||||||
|
|
||||||
describe("kube-object-menu", () => {
|
describe("kube-object-menu", () => {
|
||||||
let di: ConfigurableDependencyInjectionContainer;
|
let di: ConfigurableDependencyInjectionContainer;
|
||||||
let render: DiRender;
|
let render: DiRender;
|
||||||
@ -66,8 +69,7 @@ describe("kube-object-menu", () => {
|
|||||||
getStore: api => undefined,
|
getStore: api => undefined,
|
||||||
}) as ApiManager);
|
}) as ApiManager);
|
||||||
|
|
||||||
di.override(hideDetailsInjectable, () => () => {
|
di.override(hideDetailsInjectable, () => () => {});
|
||||||
});
|
|
||||||
|
|
||||||
di.override(editResourceTabInjectable, () => () => ({
|
di.override(editResourceTabInjectable, () => () => ({
|
||||||
id: "irrelevant",
|
id: "irrelevant",
|
||||||
|
|||||||
52
src/renderer/components/layout/close-button.module.scss
Normal file
52
src/renderer/components/layout/close-button.module.scss
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--textColorDimmed);
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #72767d25;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--textColorAccent);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.esc {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: var(--margin);
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
color: var(--textColorDimmed);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
41
src/renderer/components/layout/close-button.tsx
Normal file
41
src/renderer/components/layout/close-button.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 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 styles from "./close-button.module.scss";
|
||||||
|
|
||||||
|
import React, { HTMLAttributes } from "react";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CloseButton(props: Props) {
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<div className={styles.closeButton} role="button" aria-label="Close">
|
||||||
|
<Icon material="close" className={styles.icon}/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.esc} aria-hidden="true">
|
||||||
|
ESC
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -138,41 +138,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .toolsRegion {
|
> .toolsRegion {
|
||||||
.fixedTools {
|
width: 45px;
|
||||||
position: fixed;
|
|
||||||
top: 60px;
|
|
||||||
|
|
||||||
.closeBtn {
|
|
||||||
width: 35px;
|
|
||||||
height: 35px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border: 2px solid var(--textColorDimmed);
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #72767d4d;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Icon {
|
|
||||||
color: var(--textColorTertiary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.esc {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 4px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--textColorDimmed);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,8 +25,8 @@ import React from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
import { Icon } from "../icon";
|
|
||||||
import { catalogURL } from "../../../common/routes";
|
import { catalogURL } from "../../../common/routes";
|
||||||
|
import { CloseButton } from "./close-button";
|
||||||
|
|
||||||
export interface SettingLayoutProps extends React.DOMAttributes<any> {
|
export interface SettingLayoutProps extends React.DOMAttributes<any> {
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
@ -104,13 +104,8 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
|
|||||||
<div className="toolsRegion">
|
<div className="toolsRegion">
|
||||||
{
|
{
|
||||||
this.props.provideBackButtonNavigation && (
|
this.props.provideBackButtonNavigation && (
|
||||||
<div className="fixedTools">
|
<div className="fixed top-[60px]">
|
||||||
<div className="closeBtn" role="button" aria-label="Close" onClick={back}>
|
<CloseButton onClick={back}/>
|
||||||
<Icon material="close" />
|
|
||||||
</div>
|
|
||||||
<div className="esc" aria-hidden="true">
|
|
||||||
ESC
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* 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 rendererExtensionsInjectable from "../../../../../extensions/renderer-extensions.injectable";
|
||||||
|
|
||||||
|
const topBarItemsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => {
|
||||||
|
const extensions = di.inject(rendererExtensionsInjectable);
|
||||||
|
|
||||||
|
return computed(() =>
|
||||||
|
extensions.get().flatMap((extension) => extension.topBarItems),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default topBarItemsInjectable;
|
||||||
@ -18,10 +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 type React from "react";
|
|
||||||
import { BaseRegistry } from "./base-registry";
|
|
||||||
|
|
||||||
interface TopBarComponents {
|
interface TopBarComponents {
|
||||||
Item: React.ComponentType;
|
Item: React.ComponentType;
|
||||||
}
|
}
|
||||||
@ -29,6 +25,3 @@ interface TopBarComponents {
|
|||||||
export interface TopBarRegistration {
|
export interface TopBarRegistration {
|
||||||
components: TopBarComponents;
|
components: TopBarComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TopBarRegistry extends BaseRegistry<TopBarRegistration> {
|
|
||||||
}
|
|
||||||
@ -20,23 +20,32 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
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 { TopBar } from "../topbar";
|
import { TopBar } from "./top-bar";
|
||||||
import { TopBarRegistry } from "../../../../extensions/registries";
|
|
||||||
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 { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
|
import directoryForUserDataInjectable
|
||||||
|
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
const mockConfig = vars as { isWindows: boolean, isLinux: boolean };
|
const mockConfig = vars as { isWindows: boolean; isLinux: boolean };
|
||||||
|
|
||||||
jest.mock("../../../../common/ipc");
|
jest.mock("../../../../common/ipc");
|
||||||
|
|
||||||
jest.mock("../../../../common/vars", () => {
|
jest.mock("../../../../common/vars", () => {
|
||||||
|
const SemVer = require("semver").SemVer;
|
||||||
|
|
||||||
|
const versionStub = new SemVer("1.0.0");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
isWindows: null,
|
isWindows: null,
|
||||||
isLinux: null,
|
isLinux: null,
|
||||||
|
appSemVer: versionStub,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -57,20 +66,30 @@ jest.mock("@electron/remote", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("<Tobar/> in Windows and Linux", () => {
|
describe("<TopBar/> in Windows and Linux", () => {
|
||||||
beforeEach(() => {
|
let render: DiRender;
|
||||||
TopBarRegistry.createInstance();
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
mockFs();
|
||||||
|
|
||||||
|
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
TopBarRegistry.resetInstance();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows window controls on Windows", () => {
|
it("shows window controls on Windows", () => {
|
||||||
mockConfig.isWindows = true;
|
mockConfig.isWindows = true;
|
||||||
mockConfig.isLinux = false;
|
mockConfig.isLinux = false;
|
||||||
|
|
||||||
const { getByTestId } = render(<TopBar/>);
|
const { getByTestId } = render(<TopBar />);
|
||||||
|
|
||||||
expect(getByTestId("window-menu")).toBeInTheDocument();
|
expect(getByTestId("window-menu")).toBeInTheDocument();
|
||||||
expect(getByTestId("window-minimize")).toBeInTheDocument();
|
expect(getByTestId("window-minimize")).toBeInTheDocument();
|
||||||
@ -82,7 +101,7 @@ describe("<Tobar/> in Windows and Linux", () => {
|
|||||||
mockConfig.isWindows = false;
|
mockConfig.isWindows = false;
|
||||||
mockConfig.isLinux = true;
|
mockConfig.isLinux = true;
|
||||||
|
|
||||||
const { getByTestId } = render(<TopBar/>);
|
const { getByTestId } = render(<TopBar />);
|
||||||
|
|
||||||
expect(getByTestId("window-menu")).toBeInTheDocument();
|
expect(getByTestId("window-menu")).toBeInTheDocument();
|
||||||
expect(getByTestId("window-minimize")).toBeInTheDocument();
|
expect(getByTestId("window-minimize")).toBeInTheDocument();
|
||||||
@ -93,7 +112,7 @@ describe("<Tobar/> in Windows and Linux", () => {
|
|||||||
it("triggers ipc events on click", () => {
|
it("triggers ipc events on click", () => {
|
||||||
mockConfig.isWindows = true;
|
mockConfig.isWindows = true;
|
||||||
|
|
||||||
const { getByTestId } = render(<TopBar/>);
|
const { getByTestId } = render(<TopBar />);
|
||||||
|
|
||||||
const menu = getByTestId("window-menu");
|
const menu = getByTestId("window-menu");
|
||||||
const minimize = getByTestId("window-minimize");
|
const minimize = getByTestId("window-minimize");
|
||||||
@ -20,14 +20,26 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
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 { TopBar } from "../topbar";
|
import { TopBar } from "./top-bar";
|
||||||
import { TopBarRegistry } from "../../../../extensions/registries";
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
|
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
|
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import directoryForUserDataInjectable
|
||||||
|
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
jest.mock("../../../../common/vars", () => {
|
jest.mock("../../../../common/vars", () => {
|
||||||
|
const SemVer = require("semver").SemVer;
|
||||||
|
|
||||||
|
const versionStub = new SemVer("1.0.0");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isMac: true,
|
isMac: true,
|
||||||
|
appSemVer: versionStub,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -47,9 +59,6 @@ jest.mock(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
app: {
|
|
||||||
getPath: () => "tmp",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -76,12 +85,23 @@ jest.mock("@electron/remote", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("<TopBar/>", () => {
|
describe("<TopBar/>", () => {
|
||||||
beforeEach(() => {
|
let di: ConfigurableDependencyInjectionContainer;
|
||||||
TopBarRegistry.createInstance();
|
let render: DiRender;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
mockFs();
|
||||||
|
|
||||||
|
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
TopBarRegistry.resetInstance();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders w/o errors", () => {
|
it("renders w/o errors", () => {
|
||||||
@ -129,13 +149,13 @@ describe("<TopBar/>", () => {
|
|||||||
const testId = "testId";
|
const testId = "testId";
|
||||||
const text = "an item";
|
const text = "an item";
|
||||||
|
|
||||||
TopBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
di.override(topBarItemsInjectable, () => computed(() => [
|
||||||
{
|
{
|
||||||
components: {
|
components: {
|
||||||
Item: () => <span data-testid={testId}>{text}</span>,
|
Item: () => <span data-testid={testId}>{text}</span>,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]));
|
||||||
|
|
||||||
const { getByTestId } = render(<TopBar/>);
|
const { getByTestId } = render(<TopBar/>);
|
||||||
|
|
||||||
@ -19,22 +19,28 @@
|
|||||||
* 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 styles from "./topbar.module.scss";
|
import styles from "./top-bar.module.scss";
|
||||||
import React, { useEffect, useMemo, useRef } from "react";
|
import React, { useEffect, useMemo, useRef } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { TopBarRegistry } from "../../../extensions/registries";
|
import type { IComputedValue } from "mobx";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../../icon";
|
||||||
import { webContents, getCurrentWindow } from "@electron/remote";
|
import { webContents, getCurrentWindow } from "@electron/remote";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { broadcastMessage, ipcRendererOn } from "../../../common/ipc";
|
import { broadcastMessage, ipcRendererOn } from "../../../../common/ipc";
|
||||||
import { watchHistoryState } from "../../remote-helpers/history-updater";
|
import { watchHistoryState } from "../../../remote-helpers/history-updater";
|
||||||
import { isActiveRoute, navigate } from "../../navigation";
|
import { isActiveRoute, navigate } from "../../../navigation";
|
||||||
import { catalogRoute, catalogURL } from "../../../common/routes";
|
import { catalogRoute, catalogURL } from "../../../../common/routes";
|
||||||
import { IpcMainWindowEvents } from "../../../main/window-manager";
|
import { IpcMainWindowEvents } from "../../../../main/window-manager";
|
||||||
import { isLinux, isWindows } from "../../../common/vars";
|
import { isLinux, isWindows } from "../../../../common/vars";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../../utils";
|
||||||
|
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import type { TopBarRegistration } from "./top-bar-registration";
|
||||||
|
|
||||||
interface Props extends React.HTMLAttributes<any> {
|
interface Props extends React.HTMLAttributes<any> {}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
items: IComputedValue<TopBarRegistration[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevEnabled = observable.box(false);
|
const prevEnabled = observable.box(false);
|
||||||
@ -48,34 +54,10 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => {
|
|||||||
nextEnabled.set(state);
|
nextEnabled.set(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TopBar = observer(({ children, ...rest }: Props) => {
|
const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) => {
|
||||||
const elem = useRef<HTMLDivElement>();
|
const elem = useRef<HTMLDivElement>();
|
||||||
const window = useMemo(() => getCurrentWindow(), []);
|
const window = useMemo(() => getCurrentWindow(), []);
|
||||||
|
|
||||||
const renderRegisteredItems = () => {
|
|
||||||
const items = TopBarRegistry.getInstance().getItems();
|
|
||||||
|
|
||||||
if (!Array.isArray(items)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{items.map((registration, index) => {
|
|
||||||
if (!registration?.components?.Item) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={index}>
|
|
||||||
<registration.components.Item />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openContextMenu = () => {
|
const openContextMenu = () => {
|
||||||
broadcastMessage(IpcMainWindowEvents.OPEN_CONTEXT_MENU);
|
broadcastMessage(IpcMainWindowEvents.OPEN_CONTEXT_MENU);
|
||||||
};
|
};
|
||||||
@ -156,7 +138,7 @@ export const TopBar = observer(({ children, ...rest }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
{renderRegisteredItems()}
|
{renderRegisteredItems(items.get())}
|
||||||
{children}
|
{children}
|
||||||
{(isWindows || isLinux) && (
|
{(isWindows || isLinux) && (
|
||||||
<div className={cssNames(styles.windowButtons, { [styles.linuxButtons]: isLinux })}>
|
<div className={cssNames(styles.windowButtons, { [styles.linuxButtons]: isLinux })}>
|
||||||
@ -174,3 +156,29 @@ export const TopBar = observer(({ children, ...rest }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const renderRegisteredItems = (items: TopBarRegistration[]) => (
|
||||||
|
<div>
|
||||||
|
{items.map((registration, index) => {
|
||||||
|
if (!registration?.components?.Item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
<registration.components.Item />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const TopBar = withInjectables(observer(NonInjectedTopBar), {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
items: di.inject(topBarItemsInjectable),
|
||||||
|
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
@ -228,10 +228,15 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Select {
|
.Select {
|
||||||
|
&__value-container {
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&__control {
|
&__control {
|
||||||
box-shadow: 0 0 0 1px var(--inputControlBorder);
|
box-shadow: 0 0 0 1px var(--inputControlBorder);
|
||||||
background: var(--inputControlBackground);
|
background: var(--inputControlBackground);
|
||||||
border-radius: 5px;
|
border-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__single-value {
|
&__single-value {
|
||||||
|
|||||||
67
src/renderer/components/switch/__tests__/switch.test.tsx
Normal file
67
src/renderer/components/switch/__tests__/switch.test.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* 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 { fireEvent, render } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
|
import { Switch } from "..";
|
||||||
|
|
||||||
|
describe("<Switch/>", () => {
|
||||||
|
it("renders w/o errors", () => {
|
||||||
|
const { container } = render(<Switch />);
|
||||||
|
|
||||||
|
expect(container).toBeInstanceOf(HTMLElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("render label text", () => {
|
||||||
|
const { getByLabelText } = render(<Switch>Test label</Switch>);
|
||||||
|
|
||||||
|
expect(getByLabelText("Test label")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes disabled and checked attributes to input", () => {
|
||||||
|
const { container } = render(<Switch checked disabled/>);
|
||||||
|
const checkbox = container.querySelector("input[type=checkbox]");
|
||||||
|
|
||||||
|
expect(checkbox).toHaveAttribute("disabled");
|
||||||
|
expect(checkbox).toHaveAttribute("checked");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("onClick event fired", () => {
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const { getByTestId } = render(<Switch onClick={onClick}/>);
|
||||||
|
const switcher = getByTestId("switch");
|
||||||
|
|
||||||
|
fireEvent.click(switcher);
|
||||||
|
|
||||||
|
expect(onClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("onClick event not fired for disabled item", () => {
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const { getByTestId } = render(<Switch onClick={onClick} disabled/>);
|
||||||
|
const switcher = getByTestId("switch");
|
||||||
|
|
||||||
|
fireEvent.click(switcher);
|
||||||
|
|
||||||
|
expect(onClick).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -35,6 +35,9 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use <Switch/> instead from "../switch.tsx".
|
||||||
|
*/
|
||||||
export function FormSwitch(props: FormControlLabelProps) {
|
export function FormSwitch(props: FormControlLabelProps) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
|
|||||||
@ -21,3 +21,4 @@
|
|||||||
|
|
||||||
export * from "./switcher";
|
export * from "./switcher";
|
||||||
export * from "./form-switcher";
|
export * from "./form-switcher";
|
||||||
|
export * from "./switch";
|
||||||
|
|||||||
121
src/renderer/components/switch/switch.module.scss
Normal file
121
src/renderer/components/switch/switch.module.scss
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.Switch {
|
||||||
|
--thumb-size: 2rem;
|
||||||
|
--thumb-color: hsl(0 0% 100%);
|
||||||
|
--thumb-color-highlight: hsl(0 0% 100% / 25%);
|
||||||
|
|
||||||
|
--track-size: calc(var(--thumb-size) * 2);
|
||||||
|
--track-padding: 2px;
|
||||||
|
--track-color-inactive: hsl(80 0% 35%);
|
||||||
|
--track-color-active: hsl(110, 60%, 60%);
|
||||||
|
|
||||||
|
--thumb-position: 0%;
|
||||||
|
--thumb-transition-duration: .25s;
|
||||||
|
|
||||||
|
--hover-highlight-size: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2ch;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
color: var(--textColorAccent);
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
& > input {
|
||||||
|
padding: var(--track-padding);
|
||||||
|
background: var(--track-color-inactive);
|
||||||
|
inline-size: var(--track-size);
|
||||||
|
block-size: var(--thumb-size);
|
||||||
|
border-radius: var(--track-size);
|
||||||
|
|
||||||
|
appearance: none;
|
||||||
|
pointer-events: none;
|
||||||
|
border: none;
|
||||||
|
outline-offset: 5px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid: [track] 1fr / [track] 1fr;
|
||||||
|
|
||||||
|
transition: background-color .25s ease;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
grid-area: track;
|
||||||
|
inline-size: var(--thumb-size);
|
||||||
|
block-size: var(--thumb-size);
|
||||||
|
background: var(--thumb-color);
|
||||||
|
box-shadow: 0 0 0 var(--hover-highlight-size) var(--thumb-color-highlight);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translateX(var(--thumb-position));
|
||||||
|
transition:
|
||||||
|
transform var(--thumb-transition-duration) ease,
|
||||||
|
box-shadow .25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:disabled):hover::before {
|
||||||
|
--hover-highlight-size: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
background: var(--track-color-active);
|
||||||
|
--thumb-position: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
--track-color-inactive: hsl(80 0% 30%);
|
||||||
|
--thumb-color: transparent;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 40%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
box-shadow: 0 0 0 2px var(--blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include theme-light {
|
||||||
|
.Switch {
|
||||||
|
--thumb-color-highlight: hsl(0 0% 0% / 25%);
|
||||||
|
|
||||||
|
& > input {
|
||||||
|
&:disabled {
|
||||||
|
--track-color-inactive: hsl(80 0% 80%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/renderer/components/switch/switch.tsx
Normal file
38
src/renderer/components/switch/switch.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* 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 styles from "./switch.module.scss";
|
||||||
|
|
||||||
|
import React, { ChangeEvent, HTMLProps } from "react";
|
||||||
|
import { cssNames } from "../../utils";
|
||||||
|
|
||||||
|
interface Props extends Omit<HTMLProps<HTMLInputElement>, "onChange"> {
|
||||||
|
onChange?: (checked: boolean, event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Switch({ children, disabled, onChange, ...props }: Props) {
|
||||||
|
return (
|
||||||
|
<label className={cssNames(styles.Switch, { [styles.disabled]: disabled })} data-testid="switch">
|
||||||
|
{children}
|
||||||
|
<input type="checkbox" role="switch" disabled={disabled} onChange={(event) => onChange?.(props.checked, event)} {...props}/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -31,6 +31,9 @@ interface Props extends SwitchProps {
|
|||||||
classes: Styles;
|
classes: Styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use <Switch/> instead from "../switch.tsx".
|
||||||
|
*/
|
||||||
export const Switcher = withStyles((theme: Theme) =>
|
export const Switcher = withStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
root: {
|
root: {
|
||||||
|
|||||||
@ -27,7 +27,6 @@ export * from "./ipc";
|
|||||||
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 "./registries";
|
export * from "./registries";
|
||||||
export * from "./welcome-menu-registry";
|
|
||||||
export * from "./workloads-overview-detail-registry";
|
export * from "./workloads-overview-detail-registry";
|
||||||
export * from "./catalog-category-registry";
|
export * from "./catalog-category-registry";
|
||||||
export * from "./status-bar-registry";
|
export * from "./status-bar-registry";
|
||||||
|
|||||||
@ -33,8 +33,5 @@ export function initRegistries() {
|
|||||||
registries.KubeObjectMenuRegistry.createInstance();
|
registries.KubeObjectMenuRegistry.createInstance();
|
||||||
registries.KubeObjectStatusRegistry.createInstance();
|
registries.KubeObjectStatusRegistry.createInstance();
|
||||||
registries.StatusBarRegistry.createInstance();
|
registries.StatusBarRegistry.createInstance();
|
||||||
registries.WelcomeMenuRegistry.createInstance();
|
|
||||||
registries.WelcomeBannerRegistry.createInstance();
|
|
||||||
registries.WorkloadsOverviewDetailRegistry.createInstance();
|
registries.WorkloadsOverviewDetailRegistry.createInstance();
|
||||||
registries.TopBarRegistry.createInstance();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<div id="terminal-init"></div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -26,7 +26,14 @@ module.exports = {
|
|||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["Roboto", "Helvetica", "Arial", "sans-serif"],
|
sans: ["Roboto", "Helvetica", "Arial", "sans-serif"],
|
||||||
},
|
},
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
textAccent: "var(--textColorAccent)",
|
||||||
|
textPrimary: "var(--textColorPrimary)",
|
||||||
|
textTertiary: "var(--textColorTertiary)",
|
||||||
|
textDimmed: "var(--textColorDimmed)",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
extend: {},
|
extend: {},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user