mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into enhancement/group-app-preferences-by-extension
This commit is contained in:
commit
89d90127dd
13
.eslintrc.js
13
.eslintrc.js
@ -54,6 +54,7 @@ module.exports = {
|
||||
"react-hooks",
|
||||
],
|
||||
rules: {
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"header/header": [2, "./license-header"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"comma-spacing": "error",
|
||||
@ -107,7 +108,10 @@ module.exports = {
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
plugins: [
|
||||
"header",
|
||||
@ -118,7 +122,7 @@ module.exports = {
|
||||
sourceType: "module",
|
||||
},
|
||||
rules: {
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"header/header": [2, "./license-header"],
|
||||
"no-invalid-this": "off",
|
||||
"@typescript-eslint/no-invalid-this": ["error"],
|
||||
@ -191,8 +195,11 @@ module.exports = {
|
||||
"unused-imports",
|
||||
],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
@ -200,8 +207,9 @@ module.exports = {
|
||||
jsx: true,
|
||||
},
|
||||
rules: {
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"header/header": [2, "./license-header"],
|
||||
"react/prop-types": "off",
|
||||
"no-invalid-this": "off",
|
||||
"@typescript-eslint/no-invalid-this": ["error"],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
@ -246,7 +254,6 @@ module.exports = {
|
||||
"objectsInObjects": false,
|
||||
"arraysInObjects": true,
|
||||
}],
|
||||
"react/prop-types": "off",
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": ["error"],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
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.
|
||||
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
|
||||
|
||||
@ -45,7 +45,6 @@ For more details on accessing Lens state data, please see the [Stores](../stores
|
||||
### `appMenus`
|
||||
|
||||
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.
|
||||
|
||||
``` typescript
|
||||
@ -65,7 +64,7 @@ export default class SamplePageMainExtension extends Main.LensExtension {
|
||||
```
|
||||
|
||||
`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:
|
||||
|
||||
* `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"`.
|
||||
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
|
||||
|
||||
The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities).
|
||||
|
||||
@ -19,17 +19,18 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { catalogURL } from "../../common/routes";
|
||||
import { WelcomeMenuRegistry } from "../../extensions/registries";
|
||||
import { navigate } from "../navigation";
|
||||
|
||||
export function initWelcomeMenuRegistry() {
|
||||
WelcomeMenuRegistry.getInstance()
|
||||
.add([
|
||||
{
|
||||
title: "Browse Clusters in Catalog",
|
||||
icon: "view_list",
|
||||
click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }} )),
|
||||
module.exports = {
|
||||
"overrides": [
|
||||
{
|
||||
files: [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
rules: {
|
||||
"import/no-unresolved": ["error", {
|
||||
ignore: ["@k8slens/extensions"],
|
||||
}],
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -62,8 +62,7 @@
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
||||
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts",
|
||||
"src/(.*)": "<rootDir>/__mocks__/windowMock.ts"
|
||||
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts"
|
||||
},
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/dist",
|
||||
@ -200,6 +199,7 @@
|
||||
"@ogre-tools/injectable-react": "2.0.0",
|
||||
"@sentry/electron": "^2.5.4",
|
||||
"@sentry/integrations": "^6.15.0",
|
||||
"@types/circular-dependency-plugin": "5.0.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"auto-bind": "^4.0.0",
|
||||
"autobind-decorator": "^2.4.0",
|
||||
@ -214,7 +214,7 @@
|
||||
"filehound": "^1.17.5",
|
||||
"fs-extra": "^9.0.1",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"got": "^11.8.2",
|
||||
"got": "^11.8.3",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"handlebars": "^4.7.7",
|
||||
"http-proxy": "^1.18.1",
|
||||
@ -333,7 +333,7 @@
|
||||
"concurrently": "^5.3.0",
|
||||
"css-loader": "^5.2.7",
|
||||
"deepdash": "^5.3.9",
|
||||
"dompurify": "^2.3.3",
|
||||
"dompurify": "^2.3.4",
|
||||
"electron": "^13.6.1",
|
||||
"electron-builder": "^22.14.5",
|
||||
"electron-notarize": "^0.3.0",
|
||||
@ -341,6 +341,7 @@
|
||||
"esbuild-loader": "^2.16.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-react": "^7.27.1",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-unused-imports": "^1.1.5",
|
||||
|
||||
@ -472,8 +472,8 @@ describe("pre 2.6.0 config with a cluster icon", () => {
|
||||
it("moves the icon into preferences", async () => {
|
||||
const storedClusterData = ClusterStore.getInstance().clustersList[0];
|
||||
|
||||
expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
|
||||
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
|
||||
expect(Object.prototype.hasOwnProperty.call(storedClusterData, "icon")).toBe(false);
|
||||
expect(Object.prototype.hasOwnProperty.call(storedClusterData.preferences, "icon")).toBe(true);
|
||||
expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -42,9 +42,9 @@ import { Console } from "console";
|
||||
import { SemVer } from "semver";
|
||||
import electron from "electron";
|
||||
import { stdout, stderr } from "process";
|
||||
import { ThemeStore } from "../../renderer/theme.store";
|
||||
import type { ClusterStoreModel } from "../cluster-store";
|
||||
import { AppPaths } from "../app-paths";
|
||||
import { defaultTheme } from "../vars";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
AppPaths.init();
|
||||
@ -75,7 +75,7 @@ describe("user store tests", () => {
|
||||
us.httpsProxy = "abcd://defg";
|
||||
|
||||
expect(us.httpsProxy).toBe("abcd://defg");
|
||||
expect(us.colorTheme).toBe(ThemeStore.defaultTheme);
|
||||
expect(us.colorTheme).toBe(defaultTheme);
|
||||
|
||||
us.colorTheme = "light";
|
||||
expect(us.colorTheme).toBe("light");
|
||||
@ -86,7 +86,7 @@ describe("user store tests", () => {
|
||||
|
||||
us.colorTheme = "some other theme";
|
||||
us.resetTheme();
|
||||
expect(us.colorTheme).toBe(ThemeStore.defaultTheme);
|
||||
expect(us.colorTheme).toBe(defaultTheme);
|
||||
});
|
||||
|
||||
it("correctly calculates if the last seen version is an old release", () => {
|
||||
|
||||
@ -23,7 +23,8 @@ import { app, ipcMain, ipcRenderer } from "electron";
|
||||
import { observable, when } from "mobx";
|
||||
import path from "path";
|
||||
import logger from "./logger";
|
||||
import { fromEntries, toJS } from "./utils";
|
||||
import { fromEntries } from "./utils/objects";
|
||||
import { toJS } from "./utils/toJS";
|
||||
import { isWindows } from "./vars";
|
||||
|
||||
export type PathName = Parameters<typeof app["getPath"]>[0];
|
||||
|
||||
@ -20,11 +20,10 @@
|
||||
*/
|
||||
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog";
|
||||
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
|
||||
import { ClusterStore } from "../cluster-store";
|
||||
import { broadcastMessage, requestMain } from "../ipc";
|
||||
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
|
||||
import { app } from "electron";
|
||||
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
|
||||
|
||||
@ -30,7 +30,15 @@ import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
|
||||
import type { Disposer } from "../utils";
|
||||
import type remote from "@electron/remote";
|
||||
|
||||
const electronRemote = ipcMain ? null : require("@electron/remote");
|
||||
const electronRemote = (() => {
|
||||
if (ipcRenderer) {
|
||||
try {
|
||||
return require("@electron/remote");
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
const subFramesChannel = "ipc:get-sub-frames";
|
||||
|
||||
|
||||
@ -19,10 +19,10 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { CustomResourceDefinition } from "../endpoints";
|
||||
import { CustomResourceDefinition, CustomResourceDefinitionSpec } from "../endpoints";
|
||||
|
||||
describe("Crds", () => {
|
||||
describe("getVersion", () => {
|
||||
describe("getVersion()", () => {
|
||||
it("should throw if none of the versions are served", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "apiextensions.k8s.io/v1",
|
||||
@ -136,7 +136,7 @@ describe("Crds", () => {
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the version name from the version field", () => {
|
||||
it("should get the version name from the version field, ignoring versions on v1beta", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "apiextensions.k8s.io/v1beta1",
|
||||
kind: "CustomResourceDefinition",
|
||||
@ -147,7 +147,14 @@ describe("Crds", () => {
|
||||
},
|
||||
spec: {
|
||||
version: "abc",
|
||||
},
|
||||
versions: [
|
||||
{
|
||||
name: "foobar",
|
||||
served: true,
|
||||
storage: true,
|
||||
},
|
||||
],
|
||||
} as CustomResourceDefinitionSpec,
|
||||
});
|
||||
|
||||
expect(crd.getVersion()).toBe("abc");
|
||||
|
||||
@ -164,14 +164,14 @@ describe("KubeObject", () => {
|
||||
|
||||
describe("isJsonApiDataList", () => {
|
||||
function isAny(val: unknown): val is any {
|
||||
return !Boolean(void val);
|
||||
return true;
|
||||
}
|
||||
|
||||
function isNotAny(val: unknown): val is any {
|
||||
return Boolean(void val);
|
||||
return false;
|
||||
}
|
||||
|
||||
function isBoolean(val: unknown): val is Boolean {
|
||||
function isBoolean(val: unknown): val is boolean {
|
||||
return typeof val === "boolean";
|
||||
}
|
||||
|
||||
|
||||
@ -48,34 +48,36 @@ export interface CRDVersion {
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinition {
|
||||
spec: {
|
||||
group: string;
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||
*/
|
||||
version?: string;
|
||||
names: {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
listKind: string;
|
||||
};
|
||||
scope: "Namespaced" | "Cluster" | string;
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||
*/
|
||||
validation?: object;
|
||||
versions?: CRDVersion[];
|
||||
conversion: {
|
||||
strategy?: string;
|
||||
webhook?: any;
|
||||
};
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||
*/
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
||||
export interface CustomResourceDefinitionSpec {
|
||||
group: string;
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
version?: string;
|
||||
names: {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
listKind: string;
|
||||
};
|
||||
scope: "Namespaced" | "Cluster";
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
validation?: object;
|
||||
versions?: CRDVersion[];
|
||||
conversion: {
|
||||
strategy?: string;
|
||||
webhook?: any;
|
||||
};
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinition {
|
||||
spec: CustomResourceDefinitionSpec;
|
||||
status: {
|
||||
conditions: {
|
||||
lastTransitionTime: string;
|
||||
@ -150,27 +152,32 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
}
|
||||
|
||||
getPreferedVersion(): CRDVersion {
|
||||
// Prefer the modern `versions` over the legacy `version`
|
||||
if (this.spec.versions) {
|
||||
for (const version of this.spec.versions) {
|
||||
if (version.storage) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
} else if (this.spec.version) {
|
||||
const { additionalPrinterColumns: apc } = this.spec;
|
||||
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
|
||||
const { apiVersion } = this;
|
||||
|
||||
return {
|
||||
name: this.spec.version,
|
||||
served: true,
|
||||
storage: true,
|
||||
schema: this.spec.validation,
|
||||
additionalPrinterColumns,
|
||||
};
|
||||
switch (apiVersion) {
|
||||
case "apiextensions.k8s.io/v1":
|
||||
for (const version of this.spec.versions) {
|
||||
if (version.storage) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "apiextensions.k8s.io/v1beta1": {
|
||||
const { additionalPrinterColumns: apc } = this.spec;
|
||||
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
|
||||
|
||||
return {
|
||||
name: this.spec.version,
|
||||
served: true,
|
||||
storage: true,
|
||||
schema: this.spec.validation,
|
||||
additionalPrinterColumns,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
|
||||
throw new Error(`Unknown apiVersion=${apiVersion}: Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
@ -197,7 +204,7 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
|
||||
|
||||
return columns
|
||||
.filter(column => column.name != "Age" && (ignorePriority || !column.priority));
|
||||
.filter(column => column.name.toLowerCase() != "age" && (ignorePriority || !column.priority));
|
||||
}
|
||||
|
||||
getValidation() {
|
||||
|
||||
@ -187,7 +187,7 @@ export class Ingress extends KubeObject {
|
||||
const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort;
|
||||
|
||||
if (rules && rules.length > 0) {
|
||||
if (rules.some(rule => rule.hasOwnProperty("http"))) {
|
||||
if (rules.some(rule => Object.prototype.hasOwnProperty.call(rule, "http"))) {
|
||||
ports.push(httpPort);
|
||||
}
|
||||
} else if (servicePort !== undefined) {
|
||||
|
||||
@ -184,7 +184,8 @@ export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
|
||||
if (metric.data.result.length) {
|
||||
result[metricName] = +metric.data.result[0].values.slice(-1)[0][1];
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@ -34,6 +34,9 @@ import type { IKubeWatchEvent } from "./kube-watch-api";
|
||||
import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api";
|
||||
import { noop } from "../utils";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
|
||||
// BUG: https://github.com/mysticatea/abort-controller/pull/22
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import AbortController from "abort-controller";
|
||||
import { Agent, AgentOptions } from "https";
|
||||
import type { Patch } from "rfc6902";
|
||||
@ -698,21 +701,16 @@ export class KubeApi<T extends KubeObject> {
|
||||
}
|
||||
|
||||
protected modifyWatchEvent(event: IKubeWatchEvent<KubeJsonApiData>) {
|
||||
if (event.type === "ERROR") {
|
||||
return;
|
||||
|
||||
switch (event.type) {
|
||||
case "ADDED":
|
||||
case "DELETED":
|
||||
|
||||
case "MODIFIED": {
|
||||
ensureObjectSelfLink(this, event.object);
|
||||
|
||||
const { namespace, resourceVersion } = event.object.metadata;
|
||||
|
||||
this.setResourceVersion(namespace, resourceVersion);
|
||||
this.setResourceVersion("", resourceVersion);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ensureObjectSelfLink(this, event.object);
|
||||
|
||||
const { namespace, resourceVersion } = event.object.metadata;
|
||||
|
||||
this.setResourceVersion(namespace, resourceVersion);
|
||||
this.setResourceVersion("", resourceVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,9 @@ import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi } from "./kube-api";
|
||||
import { parseKubeApi } from "./kube-api-parse";
|
||||
import type { KubeJsonApiData } from "./kube-json-api";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
|
||||
// BUG: https://github.com/mysticatea/abort-controller/pull/22
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import AbortController from "abort-controller";
|
||||
import type { Patch } from "rfc6902";
|
||||
|
||||
@ -469,7 +472,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
|
||||
switch (type) {
|
||||
case "ADDED":
|
||||
case "MODIFIED":
|
||||
|
||||
// falls through
|
||||
case "MODIFIED": {
|
||||
const newItem = new this.api.objectConstructor(object);
|
||||
|
||||
if (!item) {
|
||||
@ -477,7 +482,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
} else {
|
||||
items[index] = newItem;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "DELETED":
|
||||
if (item) {
|
||||
items.splice(index, 1);
|
||||
|
||||
@ -88,7 +88,7 @@ export abstract class LensProtocolRouter {
|
||||
|
||||
public static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
|
||||
|
||||
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
||||
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
||||
|
||||
constructor(protected dependencies: Dependencies) {}
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ export class SearchStore {
|
||||
* @param value Unescaped string
|
||||
*/
|
||||
public static escapeRegex(value?: string): string {
|
||||
return value ? value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&") : "";
|
||||
return value ? value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") : "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -22,11 +22,11 @@
|
||||
import moment from "moment-timezone";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { ThemeStore } from "../../renderer/theme.store";
|
||||
import { getAppVersion, ObservableToggleSet } from "../utils";
|
||||
import type { editor } from "monaco-editor";
|
||||
import merge from "lodash/merge";
|
||||
import { SemVer } from "semver";
|
||||
import { defaultTheme } from "../vars";
|
||||
|
||||
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
||||
filePath: string;
|
||||
@ -72,10 +72,10 @@ const shell: PreferenceDescription<string | undefined> = {
|
||||
|
||||
const colorTheme: PreferenceDescription<string> = {
|
||||
fromStore(val) {
|
||||
return val || ThemeStore.defaultTheme;
|
||||
return val || defaultTheme;
|
||||
},
|
||||
toStore(val) {
|
||||
if (!val || val === ThemeStore.defaultTheme) {
|
||||
if (!val || val === defaultTheme) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
export function defineGlobal(propName: string, descriptor: PropertyDescriptor) {
|
||||
const scope = typeof global !== "undefined" ? global : window;
|
||||
|
||||
if (scope.hasOwnProperty(propName)) {
|
||||
if (Object.prototype.hasOwnProperty.call(scope, propName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ export type Falsey = false | 0 | "" | null | undefined;
|
||||
* Create a new type safe empty Iterable
|
||||
* @returns An `Iterable` that yields 0 items
|
||||
*/
|
||||
// eslint-disable-next-line require-yield
|
||||
export function* newEmpty<T>(): IterableIterator<T> {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -31,12 +31,13 @@ export interface ReadFileFromTarOpts {
|
||||
}
|
||||
|
||||
export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: ReadFileFromTarOpts): Promise<R> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileChunks: Buffer[] = [];
|
||||
|
||||
await tar.list({
|
||||
tar.list({
|
||||
file: tarPath,
|
||||
filter: entryPath => path.normalize(entryPath) === filePath,
|
||||
sync: true,
|
||||
onentry(entry: FileStat) {
|
||||
entry.on("data", chunk => {
|
||||
fileChunks.push(chunk);
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { array } from "../utils";
|
||||
import * as array from "../utils/array";
|
||||
|
||||
/**
|
||||
* A strict N-tuple of type T
|
||||
|
||||
@ -41,6 +41,7 @@ export const isIntegrationTesting = process.argv.includes(integrationTestingArg)
|
||||
export const productName = packageInfo.productName;
|
||||
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
||||
export const publicPath = "/build/" as string;
|
||||
export const defaultTheme = "lens-dark" as string;
|
||||
|
||||
// Webpack build paths
|
||||
export const contextDir = process.cwd();
|
||||
|
||||
@ -18,8 +18,7 @@
|
||||
* 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 } from "@ogre-tools/injectable";
|
||||
import { lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { ExtensionLoader } from "./extension-loader";
|
||||
|
||||
const extensionLoaderInjectable = getInjectable({
|
||||
|
||||
@ -263,10 +263,7 @@ export class ExtensionLoader {
|
||||
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
|
||||
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
|
||||
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.TopBarRegistry.getInstance().add(extension.topBarItems),
|
||||
];
|
||||
|
||||
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
||||
|
||||
@ -25,9 +25,10 @@ import { catalogEntityRegistry } from "../main/catalog";
|
||||
import type { CatalogEntity } from "../common/catalog";
|
||||
import type { IObservableArray } from "mobx";
|
||||
import type { MenuRegistration } from "../main/menu/menu-registration";
|
||||
|
||||
import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration";
|
||||
export class LensMainExtension extends LensExtension {
|
||||
appMenus: MenuRegistration[] = [];
|
||||
trayMenus: TrayMenuRegistration[] = [];
|
||||
|
||||
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
|
||||
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 { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-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 { 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 {
|
||||
globalPages: registries.PageRegistration[] = [];
|
||||
@ -40,10 +43,10 @@ export class LensRendererExtension extends LensExtension {
|
||||
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
|
||||
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
||||
commands: registries.CommandRegistration[] = [];
|
||||
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
||||
welcomeBanners: registries.WelcomeBannerRegistration[] = [];
|
||||
welcomeMenus: WelcomeMenuRegistration[] = [];
|
||||
welcomeBanners: WelcomeBannerRegistration[] = [];
|
||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||
topBarItems: registries.TopBarRegistration[] = [];
|
||||
topBarItems: TopBarRegistration[] = [];
|
||||
|
||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||
const { navigate } = await import("../renderer/navigation");
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
// Base class for extensions-api registries
|
||||
import { action, observable, makeObservable } from "mobx";
|
||||
import { Singleton } from "../../common/utils";
|
||||
import { LensExtension } from "../lens-extension";
|
||||
import type { LensExtension } from "../lens-extension";
|
||||
|
||||
export class BaseRegistry<T, I = T> extends Singleton {
|
||||
private items = observable.map<T, I>([], { deep: false });
|
||||
|
||||
@ -30,9 +30,6 @@ export * from "./kube-object-menu-registry";
|
||||
export * from "./kube-object-status-registry";
|
||||
export * from "./command-registry";
|
||||
export * from "./entity-setting-registry";
|
||||
export * from "./welcome-menu-registry";
|
||||
export * from "./welcome-banner-registry";
|
||||
export * from "./catalog-entity-detail-registry";
|
||||
export * from "./workloads-overview-detail-registry";
|
||||
export * from "./topbar-registry";
|
||||
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;
|
||||
@ -25,10 +25,9 @@ import { isLinux, isMac, isPublishConfigured, isTestEnv } from "../common/vars";
|
||||
import { delay } from "../common/utils";
|
||||
import { areArgsUpdateAvailableToBackchannel, AutoUpdateChecking, AutoUpdateLogPrefix, AutoUpdateNoUpdateAvailable, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
|
||||
import { once } from "lodash";
|
||||
import { ipcMain } from "electron";
|
||||
import { ipcMain, autoUpdater as electronAutoUpdater } from "electron";
|
||||
import { nextUpdateChannel } from "./utils/update-channel";
|
||||
import { UserStore } from "../common/user-store";
|
||||
import { autoUpdater as electronAutoUpdater } from "electron";
|
||||
|
||||
let installVersion: null | string = null;
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import * as tempy from "tempy";
|
||||
import tempy from "tempy";
|
||||
import fse from "fs-extra";
|
||||
import * as yaml from "js-yaml";
|
||||
import { promiseExecFile } from "../../common/utils/promise-exec";
|
||||
|
||||
@ -119,7 +119,9 @@ export class HelmRepoManager extends Singleton {
|
||||
if (typeof parsedConfig === "object" && parsedConfig) {
|
||||
return parsedConfig as HelmRepoConfig;
|
||||
}
|
||||
} catch { }
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
|
||||
return {
|
||||
repositories: [],
|
||||
|
||||
@ -60,7 +60,7 @@ import { SentryInit } from "../common/sentry";
|
||||
import { ensureDir } from "fs-extra";
|
||||
import { Router } from "./router";
|
||||
import { initMenu } from "./menu/menu";
|
||||
import { initTray } from "./tray";
|
||||
import { initTray } from "./tray/tray";
|
||||
import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions";
|
||||
import { AppPaths } from "../common/app-paths";
|
||||
import { ShellSession } from "./shell-session/shell-session";
|
||||
@ -68,6 +68,7 @@ import { getDi } from "./getDi";
|
||||
import electronMenuItemsInjectable from "./menu/electron-menu-items.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 trayMenuItemsInjectable from "./tray/tray-menu-items.injectable";
|
||||
|
||||
const di = getDi();
|
||||
|
||||
@ -104,6 +105,7 @@ mangleProxyEnv();
|
||||
logger.debug("[APP-MAIN] initializing ipc main handlers");
|
||||
|
||||
const menuItems = di.inject(electronMenuItemsInjectable);
|
||||
const trayMenuItems = di.inject(trayMenuItemsInjectable);
|
||||
|
||||
initializers.initIpcMainHandlers(menuItems);
|
||||
|
||||
@ -244,7 +246,7 @@ app.on("ready", async () => {
|
||||
|
||||
onQuitCleanup.push(
|
||||
initMenu(windowManager, menuItems),
|
||||
initTray(windowManager),
|
||||
initTray(windowManager, trayMenuItems),
|
||||
() => ShellSession.cleanup(),
|
||||
);
|
||||
|
||||
|
||||
@ -97,7 +97,9 @@ export function initIpcMainHandlers(electronMenuItems: IComputedValue<MenuRegist
|
||||
const localStorageFilePath = path.resolve(AppPaths.get("userData"), "lens-local-storage", `${cluster.id}.json`);
|
||||
|
||||
await remove(localStorageFilePath);
|
||||
} catch {}
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
});
|
||||
|
||||
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {
|
||||
|
||||
@ -357,10 +357,10 @@ export class Kubectl {
|
||||
bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`;
|
||||
bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
|
||||
|
||||
bashScript += `NO_PROXY=\",\${NO_PROXY:-localhost},\"\n`;
|
||||
bashScript += `NO_PROXY=\"\${NO_PROXY//,localhost,/,}\"\n`;
|
||||
bashScript += `NO_PROXY=\"\${NO_PROXY//,127.0.0.1,/,}\"\n`;
|
||||
bashScript += `NO_PROXY=\"localhost,127.0.0.1\${NO_PROXY%,}\"\n`;
|
||||
bashScript += `NO_PROXY=",\${NO_PROXY:-localhost},"\n`;
|
||||
bashScript += `NO_PROXY="\${NO_PROXY//,localhost,/,}"\n`;
|
||||
bashScript += `NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"\n`;
|
||||
bashScript += `NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"\n`;
|
||||
bashScript += "export NO_PROXY\n";
|
||||
bashScript += "unset tempkubeconfig\n";
|
||||
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });
|
||||
@ -378,18 +378,18 @@ export class Kubectl {
|
||||
zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n";
|
||||
|
||||
// voodoo to replace any previous occurrences of kubectl path in the PATH
|
||||
zshScript += `kubectlpath=\"${kubectlPath}"\n`;
|
||||
zshScript += `helmpath=\"${helmPath}"\n`;
|
||||
zshScript += `kubectlpath="${kubectlPath}"\n`;
|
||||
zshScript += `helmpath="${helmPath}"\n`;
|
||||
zshScript += "p=\":$kubectlpath:\"\n";
|
||||
zshScript += "d=\":$PATH:\"\n";
|
||||
zshScript += `d=\${d//$p/:}\n`;
|
||||
zshScript += `d=\${d/#:/}\n`;
|
||||
zshScript += `export PATH=\"$helmpath:$kubectlpath:\${d/%:/}\"\n`;
|
||||
zshScript += `export PATH="$helmpath:$kubectlpath:\${d/%:/}"\n`;
|
||||
zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
|
||||
zshScript += `NO_PROXY=\",\${NO_PROXY:-localhost},\"\n`;
|
||||
zshScript += `NO_PROXY=\"\${NO_PROXY//,localhost,/,}\"\n`;
|
||||
zshScript += `NO_PROXY=\"\${NO_PROXY//,127.0.0.1,/,}\"\n`;
|
||||
zshScript += `NO_PROXY=\"localhost,127.0.0.1\${NO_PROXY%,}\"\n`;
|
||||
zshScript += `NO_PROXY=",\${NO_PROXY:-localhost},"\n`;
|
||||
zshScript += `NO_PROXY="\${NO_PROXY//,localhost,/,}"\n`;
|
||||
zshScript += `NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"\n`;
|
||||
zshScript += `NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"\n`;
|
||||
zshScript += "export NO_PROXY\n";
|
||||
zshScript += "unset tempkubeconfig\n";
|
||||
zshScript += "unset OLD_ZDOTDIR\n";
|
||||
|
||||
@ -29,8 +29,7 @@ const electronMenuItemsInjectable = getInjectable({
|
||||
const extensions = di.inject(mainExtensionsInjectable);
|
||||
|
||||
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":
|
||||
switch (queryName) {
|
||||
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":
|
||||
return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`;
|
||||
case "memoryRequests":
|
||||
@ -50,7 +50,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
||||
case "memoryAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
|
||||
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":
|
||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
|
||||
case "cpuLimits":
|
||||
@ -66,31 +66,31 @@ export class PrometheusOperator extends PrometheusProvider {
|
||||
case "podAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
|
||||
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":
|
||||
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;
|
||||
case "nodes":
|
||||
switch (queryName) {
|
||||
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":
|
||||
return `sum(container_memory_working_set_bytes{container!=""}) by (node)`;
|
||||
return `sum(container_memory_working_set_bytes{container!="POD", container!=""}) by (node)`;
|
||||
case "memoryCapacity":
|
||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||
case "memoryAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
|
||||
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":
|
||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||
case "cpuAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||
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":
|
||||
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;
|
||||
case "pods":
|
||||
|
||||
@ -25,7 +25,7 @@ import { exec } from "child_process";
|
||||
import fs from "fs-extra";
|
||||
import * as yaml from "js-yaml";
|
||||
import path from "path";
|
||||
import * as tempy from "tempy";
|
||||
import tempy from "tempy";
|
||||
import logger from "./logger";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { cloneJsonObject } from "../common/utils";
|
||||
|
||||
@ -72,6 +72,7 @@ export class NodeShellSession extends ShellSession {
|
||||
switch (nodeOs) {
|
||||
default:
|
||||
logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`);
|
||||
// fallthrough
|
||||
case "linux":
|
||||
args.push("sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))");
|
||||
break;
|
||||
|
||||
@ -134,7 +134,9 @@ export abstract class ShellSession {
|
||||
for (const shellProcess of this.processes.values()) {
|
||||
try {
|
||||
process.kill(shellProcess.pid);
|
||||
} catch {}
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
|
||||
this.processes.clear();
|
||||
@ -214,7 +216,9 @@ export abstract class ShellSession {
|
||||
if (stats.isDirectory()) {
|
||||
return potentialCwd;
|
||||
}
|
||||
} catch {}
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
|
||||
return "."; // Always valid
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
30
src/main/tray/tray-menu-registration.d.ts
vendored
Normal file
30
src/main/tray/tray-menu-registration.d.ts
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface TrayMenuRegistration {
|
||||
label?: string;
|
||||
click?: (menuItem: TrayMenuRegistration) => void;
|
||||
id?: string;
|
||||
type?: "normal" | "separator" | "submenu"
|
||||
toolTip?: string;
|
||||
enabled?: boolean;
|
||||
submenu?: TrayMenuRegistration[]
|
||||
}
|
||||
@ -20,16 +20,18 @@
|
||||
*/
|
||||
|
||||
import path from "path";
|
||||
import packageInfo from "../../package.json";
|
||||
import packageInfo from "../../../package.json";
|
||||
import { Menu, Tray } from "electron";
|
||||
import { autorun } from "mobx";
|
||||
import { showAbout } from "./menu/menu";
|
||||
import { checkForUpdates, isAutoUpdateEnabled } from "./app-updater";
|
||||
import type { WindowManager } from "./window-manager";
|
||||
import logger from "./logger";
|
||||
import { isDevelopment, isWindows, productName } from "../common/vars";
|
||||
import { exitApp } from "./exit-app";
|
||||
import { preferencesURL } from "../common/routes";
|
||||
import { autorun, IComputedValue } from "mobx";
|
||||
import { showAbout } from "../menu/menu";
|
||||
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
|
||||
import type { WindowManager } from "../window-manager";
|
||||
import logger from "../logger";
|
||||
import { isDevelopment, isWindows, productName } from "../../common/vars";
|
||||
import { exitApp } from "../exit-app";
|
||||
import { preferencesURL } from "../../common/routes";
|
||||
import { toJS } from "../../common/utils";
|
||||
import type { TrayMenuRegistration } from "./tray-menu-registration";
|
||||
|
||||
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();
|
||||
|
||||
tray = new Tray(icon);
|
||||
@ -62,7 +67,7 @@ export function initTray(windowManager: WindowManager) {
|
||||
const disposers = [
|
||||
autorun(() => {
|
||||
try {
|
||||
const menu = createTrayMenu(windowManager);
|
||||
const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()));
|
||||
|
||||
tray.setContextMenu(menu);
|
||||
} catch (error) {
|
||||
@ -78,8 +83,21 @@ export function initTray(windowManager: WindowManager) {
|
||||
};
|
||||
}
|
||||
|
||||
function createTrayMenu(windowManager: WindowManager): Menu {
|
||||
const template: Electron.MenuItemConstructorOptions[] = [
|
||||
function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): 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}`,
|
||||
click() {
|
||||
@ -108,6 +126,8 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
||||
});
|
||||
}
|
||||
|
||||
template = template.concat(extensionTrayItems.map(getMenuItemConstructorOptions));
|
||||
|
||||
return Menu.buildFromTemplate(template.concat([
|
||||
{
|
||||
label: `About ${productName}`,
|
||||
@ -33,7 +33,7 @@ export default {
|
||||
const contextName = value[0];
|
||||
|
||||
// Looping all the keys gives out the store internal stuff too...
|
||||
if (contextName === "__internal__" || value[1].hasOwnProperty("kubeConfig")) continue;
|
||||
if (contextName === "__internal__" || Object.prototype.hasOwnProperty.call(value[1], "kubeConfig")) continue;
|
||||
store.set(contextName, { kubeConfig: value[1] });
|
||||
}
|
||||
},
|
||||
|
||||
@ -34,7 +34,7 @@ export default {
|
||||
if (!cluster.kubeConfig) continue;
|
||||
const config = yaml.load(cluster.kubeConfig);
|
||||
|
||||
if (!config || typeof config !== "object" || !config.hasOwnProperty("users")) {
|
||||
if (!config || typeof config !== "object" || !Object.prototype.hasOwnProperty.call(config, "users")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@
|
||||
*/
|
||||
|
||||
import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
||||
import "../../../common/catalog-entities";
|
||||
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
|
||||
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "../catalog-entity";
|
||||
import { KubernetesCluster, WebLink } from "../../../common/catalog-entities";
|
||||
|
||||
@ -28,14 +28,21 @@ import { ClusterStore } from "../../common/cluster-store";
|
||||
import { Disposer, iter } from "../utils";
|
||||
import { once } from "lodash";
|
||||
import logger from "../../common/logger";
|
||||
import { catalogEntityRunContext } from "./catalog-entity";
|
||||
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { CatalogIpcEvents } from "../../common/ipc/catalog";
|
||||
import { navigate } from "../navigation";
|
||||
|
||||
export type EntityFilter = (entity: CatalogEntity) => any;
|
||||
export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>;
|
||||
|
||||
export const catalogEntityRunContext = {
|
||||
navigate: (url: string) => navigate(url),
|
||||
setCommandPaletteContext: (entity?: CatalogEntity) => {
|
||||
catalogEntityRegistry.activeEntity = entity;
|
||||
},
|
||||
};
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
@observable protected activeEntityId: string | undefined = undefined;
|
||||
protected _entities = observable.map<string, CatalogEntity>([], { deep: true });
|
||||
|
||||
@ -19,10 +19,7 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { navigate } from "../navigation";
|
||||
import type { CatalogEntity } from "../../common/catalog";
|
||||
import { catalogEntityRegistry } from "./catalog-entity-registry";
|
||||
|
||||
export { catalogEntityRunContext } from "./catalog-entity-registry";
|
||||
export { CatalogCategory, CatalogEntity } from "../../common/catalog";
|
||||
export type {
|
||||
CatalogEntityData,
|
||||
@ -33,10 +30,3 @@ export type {
|
||||
CatalogEntityContextMenu,
|
||||
CatalogEntityContextMenuContext,
|
||||
} from "../../common/catalog";
|
||||
|
||||
export const catalogEntityRunContext = {
|
||||
navigate: (url: string) => navigate(url),
|
||||
setCommandPaletteContext: (entity?: CatalogEntity) => {
|
||||
catalogEntityRegistry.activeEntity = entity;
|
||||
},
|
||||
};
|
||||
|
||||
@ -114,9 +114,6 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
||||
logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`);
|
||||
initializers.initKubeObjectDetailRegistry();
|
||||
|
||||
logger.info(`${logPrefix} initializing WelcomeMenuRegistry`);
|
||||
initializers.initWelcomeMenuRegistry();
|
||||
|
||||
logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`);
|
||||
initializers.initWorkloadsOverviewDetailRegistry();
|
||||
|
||||
|
||||
@ -45,15 +45,17 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
|
||||
|
||||
const renderName = (metric: IHpaMetric) => {
|
||||
switch (metric.type) {
|
||||
case HpaMetricType.Resource:
|
||||
const addition = metric.resource.targetAverageUtilization ? <>(as a percentage of request)</> : "";
|
||||
case HpaMetricType.Resource: {
|
||||
const addition = metric.resource.targetAverageUtilization
|
||||
? "(as a percentage of request)"
|
||||
: "";
|
||||
|
||||
return <>Resource {metric.resource.name} on Pods {addition}</>;
|
||||
|
||||
}
|
||||
case HpaMetricType.Pods:
|
||||
return <>{metric.pods.metricName} on Pods</>;
|
||||
|
||||
case HpaMetricType.Object:
|
||||
case HpaMetricType.Object: {
|
||||
const { target } = metric.object;
|
||||
const { kind, name } = target;
|
||||
const objectUrl = getDetailsUrl(apiManager.lookupApiLink(target, hpa));
|
||||
@ -64,6 +66,7 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
|
||||
<Link to={objectUrl}>{kind}/{name}</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case HpaMetricType.External:
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -18,8 +18,7 @@
|
||||
* 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 } from "@ogre-tools/injectable";
|
||||
import { lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { unpackExtension } from "./unpack-extension";
|
||||
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
|
||||
|
||||
@ -47,8 +47,8 @@ export const getBaseRegistryUrl = ({ getRegistryUrlPreference }: Dependencies) =
|
||||
} catch (error) {
|
||||
Notifications.error(<p>Failed to get configured registry from <code>.npmrc</code>. Falling back to default registry</p>);
|
||||
console.warn("[EXTENSIONS]: failed to get configured registry from .npmrc", error);
|
||||
// fallthrough
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
default:
|
||||
case ExtensionRegistryLocation.DEFAULT:
|
||||
|
||||
@ -21,11 +21,10 @@
|
||||
|
||||
import React from "react";
|
||||
import { boundMethod, cssNames } from "../../utils";
|
||||
import { openPortForward, PortForwardItem, removePortForward } from "../../port-forward";
|
||||
import { openPortForward, PortForwardItem, removePortForward, PortForwardDialog } from "../../port-forward";
|
||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { PortForwardDialog } from "../../port-forward";
|
||||
import { Notifications } from "../notifications";
|
||||
|
||||
interface Props extends MenuActionsProps {
|
||||
|
||||
@ -27,7 +27,7 @@ import { ThemeStore } from "../../theme.store";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { Input } from "../input";
|
||||
import { isWindows } from "../../../common/vars";
|
||||
import { FormSwitch, Switcher } from "../switch";
|
||||
import { Switch } from "../switch";
|
||||
import moment from "moment-timezone";
|
||||
import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
|
||||
import { action } from "mobx";
|
||||
@ -86,16 +86,12 @@ export const Application = observer(() => {
|
||||
|
||||
<section id="terminalSelection">
|
||||
<SubTitle title="Terminal copy & paste" />
|
||||
<FormSwitch
|
||||
label="Copy on select and paste on right-click"
|
||||
control={
|
||||
<Switcher
|
||||
checked={userStore.terminalCopyOnSelect}
|
||||
onChange={v => userStore.terminalCopyOnSelect = v.target.checked}
|
||||
name="terminalCopyOnSelect"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Switch
|
||||
checked={userStore.terminalCopyOnSelect}
|
||||
onChange={() => userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect}
|
||||
>
|
||||
Copy on select and paste on right-click
|
||||
</Switch>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
@ -135,16 +131,9 @@ export const Application = observer(() => {
|
||||
|
||||
<section id="other">
|
||||
<SubTitle title="Start-up"/>
|
||||
<FormSwitch
|
||||
control={
|
||||
<Switcher
|
||||
checked={userStore.openAtLogin}
|
||||
onChange={v => userStore.openAtLogin = v.target.checked}
|
||||
name="startup"
|
||||
/>
|
||||
}
|
||||
label="Automatically start Lens on login"
|
||||
/>
|
||||
<Switch checked={userStore.openAtLogin} onChange={() => userStore.openAtLogin = !userStore.openAtLogin}>
|
||||
Automatically start Lens on login
|
||||
</Switch>
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { FormSwitch, Switcher } from "../switch";
|
||||
import { Switch } from "../switch";
|
||||
import { Select } from "../select";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { SubHeader } from "../layout/sub-header";
|
||||
@ -45,15 +45,12 @@ export const Editor = observer(() => {
|
||||
<section>
|
||||
<div className="flex gaps justify-space-between">
|
||||
<div className="flex gaps align-center">
|
||||
<FormSwitch
|
||||
label={<SubHeader compact>Show minimap</SubHeader>}
|
||||
control={
|
||||
<Switcher
|
||||
checked={editorConfiguration.minimap.enabled}
|
||||
onChange={(evt, checked) => editorConfiguration.minimap.enabled = checked}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Switch
|
||||
checked={editorConfiguration.minimap.enabled}
|
||||
onChange={() => editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled}
|
||||
>
|
||||
Show minimap
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="flex gaps align-center">
|
||||
<SubHeader compact>Position</SubHeader>
|
||||
|
||||
@ -26,7 +26,7 @@ import { getDefaultKubectlDownloadPath, UserStore } from "../../../common/user-s
|
||||
import { observer } from "mobx-react";
|
||||
import { bundledKubectlPath } from "../../../main/kubectl";
|
||||
import { SelectOption, Select } from "../select";
|
||||
import { FormSwitch, Switcher } from "../switch";
|
||||
import { Switch } from "../switch";
|
||||
import { packageMirrors } from "../../../common/user-store/preferences-helpers";
|
||||
|
||||
export const KubectlBinaries = observer(() => {
|
||||
@ -48,16 +48,12 @@ export const KubectlBinaries = observer(() => {
|
||||
<>
|
||||
<section>
|
||||
<SubTitle title="Kubectl binary download"/>
|
||||
<FormSwitch
|
||||
control={
|
||||
<Switcher
|
||||
checked={userStore.downloadKubectlBinaries}
|
||||
onChange={v => userStore.downloadKubectlBinaries = v.target.checked}
|
||||
name="kubectl-download"
|
||||
/>
|
||||
}
|
||||
label="Download kubectl binaries matching the Kubernetes cluster version"
|
||||
/>
|
||||
<Switch
|
||||
checked={userStore.downloadKubectlBinaries}
|
||||
onChange={() => userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries}
|
||||
>
|
||||
Download kubectl binaries matching the Kubernetes cluster version
|
||||
</Switch>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
@ -24,10 +24,11 @@ import React from "react";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { Input } from "../input";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { FormSwitch, Switcher } from "../switch";
|
||||
import { Switch } from "../switch";
|
||||
|
||||
export const LensProxy = observer(() => {
|
||||
const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || "");
|
||||
const store = UserStore.getInstance();
|
||||
|
||||
return (
|
||||
<section id="proxy">
|
||||
@ -50,16 +51,9 @@ export const LensProxy = observer(() => {
|
||||
|
||||
<section className="small">
|
||||
<SubTitle title="Certificate Trust"/>
|
||||
<FormSwitch
|
||||
control={
|
||||
<Switcher
|
||||
checked={UserStore.getInstance().allowUntrustedCAs}
|
||||
onChange={v => UserStore.getInstance().allowUntrustedCAs = v.target.checked}
|
||||
name="startup"
|
||||
/>
|
||||
}
|
||||
label="Allow untrusted Certificate Authorities"
|
||||
/>
|
||||
<Switch checked={store.allowUntrustedCAs} onChange={() => store.allowUntrustedCAs = !store.allowUntrustedCAs}>
|
||||
Allow untrusted Certificate Authorities
|
||||
</Switch>
|
||||
<small className="hint">
|
||||
This will make Lens to trust ANY certificate authority without any validations.{" "}
|
||||
Needed with some corporate proxies that do certificate re-writing.{" "}
|
||||
|
||||
@ -20,45 +20,55 @@
|
||||
*/
|
||||
|
||||
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 { Welcome } from "../welcome";
|
||||
import { TopBarRegistry, WelcomeMenuRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries";
|
||||
import { defaultWidth } from "../welcome";
|
||||
import { defaultWidth, Welcome } from "../welcome";
|
||||
import { computed } from "mobx";
|
||||
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(
|
||||
"electron",
|
||||
() => ({
|
||||
ipcRenderer: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
getPath: () => "tmp",
|
||||
},
|
||||
}),
|
||||
);
|
||||
jest.mock("electron", () => ({
|
||||
ipcRenderer: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
getPath: () => "tmp",
|
||||
},
|
||||
}));
|
||||
|
||||
describe("<Welcome/>", () => {
|
||||
beforeEach(() => {
|
||||
TopBarRegistry.createInstance();
|
||||
WelcomeMenuRegistry.createInstance();
|
||||
WelcomeBannerRegistry.createInstance();
|
||||
});
|
||||
let render: DiRender;
|
||||
let di: ConfigurableDependencyInjectionContainer;
|
||||
let welcomeBannersStub: WelcomeBannerRegistration[];
|
||||
|
||||
afterEach(() => {
|
||||
TopBarRegistry.resetInstance();
|
||||
WelcomeMenuRegistry.resetInstance();
|
||||
WelcomeBannerRegistry.resetInstance();
|
||||
beforeEach(() => {
|
||||
di = getDiForUnitTesting();
|
||||
|
||||
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 () => {
|
||||
const testId = "testId";
|
||||
|
||||
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
||||
{
|
||||
Banner: () => <div data-testid={testId} />,
|
||||
},
|
||||
]);
|
||||
welcomeBannersStub.push({
|
||||
Banner: () => <div data-testid={testId} />,
|
||||
});
|
||||
|
||||
const { container } = render(<Welcome />);
|
||||
|
||||
@ -67,16 +77,15 @@ describe("<Welcome/>", () => {
|
||||
});
|
||||
|
||||
it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => {
|
||||
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
||||
{
|
||||
width: 100,
|
||||
Banner: () => <div />,
|
||||
},
|
||||
{
|
||||
width: 800,
|
||||
Banner: () => <div />,
|
||||
},
|
||||
]);
|
||||
welcomeBannersStub.push({
|
||||
width: 100,
|
||||
Banner: () => <div />,
|
||||
});
|
||||
|
||||
welcomeBannersStub.push({
|
||||
width: 800,
|
||||
Banner: () => <div />,
|
||||
});
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
/**
|
||||
* WelcomeBannerRegistration is for an extension to register
|
||||
* Provide a Banner component to be renderered in the welcome screen.
|
||||
@ -35,5 +33,3 @@ export interface WelcomeBannerRegistration {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
export interface WelcomeMenuRegistration {
|
||||
title: string | (() => string);
|
||||
icon: string;
|
||||
click: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export class WelcomeMenuRegistry extends BaseRegistry<WelcomeMenuRegistration> {}
|
||||
@ -22,78 +22,129 @@
|
||||
import "./welcome.scss";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import Carousel from "react-material-ui-carousel";
|
||||
import { Icon } from "../icon";
|
||||
import { productName, slackUrl } from "../../../common/vars";
|
||||
import { WelcomeMenuRegistry } from "../../../extensions/registries";
|
||||
import { WelcomeBannerRegistry } from "../../../extensions/registries";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
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;
|
||||
|
||||
@observer
|
||||
export class Welcome extends React.Component {
|
||||
render() {
|
||||
const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems();
|
||||
interface Dependencies {
|
||||
welcomeMenuItems: IComputedValue<WelcomeMenuRegistration[]>
|
||||
welcomeBannerItems: IComputedValue<WelcomeBannerRegistration[]>
|
||||
}
|
||||
|
||||
// if there is banner with specified width, use it to calculate the width of the container
|
||||
const maxWidth = welcomeBanner.reduce((acc, curr) => {
|
||||
const currWidth = curr.width ?? 0;
|
||||
const NonInjectedWelcome: React.FC<Dependencies> = ({ welcomeMenuItems, welcomeBannerItems }) => {
|
||||
const welcomeBanners = welcomeBannerItems.get();
|
||||
|
||||
if (acc > currWidth) {
|
||||
return acc;
|
||||
}
|
||||
// if there is banner with specified width, use it to calculate the width of the container
|
||||
const maxWidth = welcomeBanners.reduce((acc, curr) => {
|
||||
const currWidth = curr.width ?? 0;
|
||||
|
||||
return currWidth;
|
||||
}, defaultWidth);
|
||||
if (acc > currWidth) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center Welcome align-center">
|
||||
<div style={{ width: `${maxWidth}px` }} data-testid="welcome-banner-container">
|
||||
{welcomeBanner.length > 0 ? (
|
||||
<Carousel
|
||||
stopAutoPlayOnHover={true}
|
||||
indicators={welcomeBanner.length > 1}
|
||||
autoPlay={true}
|
||||
navButtonsAlwaysInvisible={true}
|
||||
indicatorIconButtonProps={{
|
||||
style: {
|
||||
color: "var(--iconActiveBackground)",
|
||||
},
|
||||
}}
|
||||
activeIndicatorIconButtonProps={{
|
||||
style: {
|
||||
color: "var(--iconActiveColor)",
|
||||
},
|
||||
}}
|
||||
interval={8000}
|
||||
return currWidth;
|
||||
}, defaultWidth);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center Welcome align-center">
|
||||
<div
|
||||
style={{ width: `${maxWidth}px` }}
|
||||
data-testid="welcome-banner-container"
|
||||
>
|
||||
{welcomeBanners.length > 0 ? (
|
||||
<Carousel
|
||||
stopAutoPlayOnHover={true}
|
||||
indicators={welcomeBanners.length > 1}
|
||||
autoPlay={true}
|
||||
navButtonsAlwaysInvisible={true}
|
||||
indicatorIconButtonProps={{
|
||||
style: {
|
||||
color: "var(--iconActiveBackground)",
|
||||
},
|
||||
}}
|
||||
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) =>
|
||||
<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">
|
||||
{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>
|
||||
{welcomeMenuItems.get().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>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#terminal-init {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
@ -39,8 +39,8 @@ import { DeleteClusterDialog } from "../delete-cluster-dialog";
|
||||
import { reaction } from "mobx";
|
||||
import { navigation } from "../../navigation";
|
||||
import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync";
|
||||
import { TopBar } from "../layout/topbar";
|
||||
import { catalogURL, getPreviousTabUrl } from "../../../common/routes";
|
||||
import { TopBar } from "../layout/top-bar/top-bar";
|
||||
|
||||
@observer
|
||||
export class ClusterManager extends React.Component {
|
||||
|
||||
@ -23,7 +23,7 @@ import { KubeConfig } from "@kubernetes/client-node";
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import mockFs from "mock-fs";
|
||||
import React from "react";
|
||||
import selectEvent from "react-select-event";
|
||||
import * as selectEvent from "react-select-event";
|
||||
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { DeleteClusterDialog } from "../delete-cluster-dialog";
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { render } from "@testing-library/react";
|
||||
import selectEvent from "react-select-event";
|
||||
import * as selectEvent from "react-select-event";
|
||||
|
||||
import { Pod } from "../../../../common/k8s-api/endpoints";
|
||||
import { LogResourceSelector } from "../log-resource-selector";
|
||||
|
||||
@ -34,17 +34,9 @@ import { clipboard } from "electron";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
export class Terminal {
|
||||
public static readonly spawningPool = (() => {
|
||||
// terminal element must be in DOM before attaching via xterm.open(elem)
|
||||
// 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;
|
||||
})();
|
||||
public static get spawningPool() {
|
||||
return document.getElementById("terminal-init");
|
||||
}
|
||||
|
||||
static async preloadFonts() {
|
||||
const fontPath = require("../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
|
||||
@ -133,7 +133,8 @@ export class FilePicker extends React.Component<Props> {
|
||||
switch (onOverSizeLimit) {
|
||||
case OverSizeLimitStyle.FILTER:
|
||||
return files.filter(file => file.size <= maxSize );
|
||||
case OverSizeLimitStyle.REJECT:
|
||||
|
||||
case OverSizeLimitStyle.REJECT: {
|
||||
const firstFileToLarge = files.find(file => file.size > maxSize);
|
||||
|
||||
if (firstFileToLarge) {
|
||||
@ -141,6 +142,7 @@ export class FilePicker extends React.Component<Props> {
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,7 +158,9 @@ export class FilePicker extends React.Component<Props> {
|
||||
switch (onOverTotalSizeLimit) {
|
||||
case OverTotalSizeLimitStyle.FILTER_LARGEST:
|
||||
files = _.orderBy(files, ["size"]);
|
||||
case OverTotalSizeLimitStyle.FILTER_LAST:
|
||||
|
||||
// fallthrough
|
||||
case OverTotalSizeLimitStyle.FILTER_LAST: {
|
||||
let newTotalSize = totalSize;
|
||||
|
||||
for (;files.length > 0;) {
|
||||
@ -168,6 +172,7 @@ export class FilePicker extends React.Component<Props> {
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
case OverTotalSizeLimitStyle.REJECT:
|
||||
throw `Total file size to upload is too large. Expected at most ${maxTotalSize}. Found ${totalSize}.`;
|
||||
}
|
||||
|
||||
@ -73,6 +73,7 @@ export class Icon extends React.PureComponent<IconProps> {
|
||||
switch (evt.nativeEvent.code) {
|
||||
case "Space":
|
||||
|
||||
// fallthrough
|
||||
case "Enter": {
|
||||
// eslint-disable-next-line react/no-find-dom-node
|
||||
const icon = findDOMNode(this) as HTMLElement;
|
||||
|
||||
@ -39,7 +39,7 @@ export const isRequired: InputValidator = {
|
||||
export const isEmail: InputValidator = {
|
||||
condition: ({ type }) => type === "email",
|
||||
message: () => `Wrong email format`,
|
||||
validate: value => !!value.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/),
|
||||
validate: value => !!value.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/),
|
||||
};
|
||||
|
||||
export const isNumber: InputValidator = {
|
||||
|
||||
@ -31,7 +31,6 @@ import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { crdStore } from "../+custom-resources/crd.store";
|
||||
import { KubeObjectMenu } from "../kube-object-menu";
|
||||
import { KubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import logger from "../../../main/logger";
|
||||
import { CrdResourceDetails } from "../+custom-resources";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { hideDetails, kubeDetailsUrlParam } from "../kube-detail-params";
|
||||
@ -62,7 +61,7 @@ export class KubeObjectDetails extends React.Component {
|
||||
.getStore(this.path)
|
||||
?.getByPath(this.path);
|
||||
} catch (error) {
|
||||
logger.error(`[KUBE-OBJECT-DETAILS]: failed to get store or object: ${error}`, { path: this.path });
|
||||
console.error(`[KUBE-OBJECT-DETAILS]: failed to get store or object: ${error}`, { path: this.path });
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
import { hideDetails } from "../../kube-detail-params";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
|
||||
export const hideDetailsInjectable = getInjectable({
|
||||
const hideDetailsInjectable = getInjectable({
|
||||
instantiate: () => hideDetails,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -129,41 +129,7 @@
|
||||
}
|
||||
|
||||
> .toolsRegion {
|
||||
.fixedTools {
|
||||
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;
|
||||
}
|
||||
}
|
||||
width: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,8 +25,8 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { navigation } from "../../navigation";
|
||||
import { Icon } from "../icon";
|
||||
import { catalogURL } from "../../../common/routes";
|
||||
import { CloseButton } from "./close-button";
|
||||
|
||||
export interface SettingLayoutProps extends React.DOMAttributes<any> {
|
||||
className?: IClassName;
|
||||
@ -104,13 +104,8 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
|
||||
<div className="toolsRegion">
|
||||
{
|
||||
this.props.provideBackButtonNavigation && (
|
||||
<div className="fixedTools">
|
||||
<div className="closeBtn" role="button" aria-label="Close" onClick={back}>
|
||||
<Icon material="close" />
|
||||
</div>
|
||||
<div className="esc" aria-hidden="true">
|
||||
ESC
|
||||
</div>
|
||||
<div className="fixed top-[60px]">
|
||||
<CloseButton onClick={back}/>
|
||||
</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
|
||||
* 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 {
|
||||
Item: React.ComponentType;
|
||||
}
|
||||
@ -29,6 +25,3 @@ interface TopBarComponents {
|
||||
export interface TopBarRegistration {
|
||||
components: TopBarComponents;
|
||||
}
|
||||
|
||||
export class TopBarRegistry extends BaseRegistry<TopBarRegistration> {
|
||||
}
|
||||
@ -20,23 +20,29 @@
|
||||
*/
|
||||
|
||||
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 { TopBar } from "../topbar";
|
||||
import { TopBarRegistry } from "../../../../extensions/registries";
|
||||
import { TopBar } from "./top-bar";
|
||||
import { IpcMainWindowEvents } from "../../../../main/window-manager";
|
||||
import { broadcastMessage } from "../../../../common/ipc";
|
||||
import * as vars from "../../../../common/vars";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||
|
||||
const mockConfig = vars as { isWindows: boolean, isLinux: boolean };
|
||||
const mockConfig = vars as { isWindows: boolean; isLinux: boolean };
|
||||
|
||||
jest.mock("../../../../common/ipc");
|
||||
|
||||
jest.mock("../../../../common/vars", () => {
|
||||
const SemVer = require("semver").SemVer;
|
||||
|
||||
const versionStub = new SemVer("1.0.0");
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
isWindows: null,
|
||||
isLinux: null,
|
||||
appSemVer: versionStub,
|
||||
};
|
||||
});
|
||||
|
||||
@ -57,20 +63,20 @@ jest.mock("@electron/remote", () => {
|
||||
};
|
||||
});
|
||||
|
||||
describe("<Tobar/> in Windows and Linux", () => {
|
||||
beforeEach(() => {
|
||||
TopBarRegistry.createInstance();
|
||||
});
|
||||
describe("<TopBar/> in Windows and Linux", () => {
|
||||
let render: DiRender;
|
||||
|
||||
afterEach(() => {
|
||||
TopBarRegistry.resetInstance();
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting();
|
||||
|
||||
render = renderFor(di);
|
||||
});
|
||||
|
||||
it("shows window controls on Windows", () => {
|
||||
mockConfig.isWindows = true;
|
||||
mockConfig.isLinux = false;
|
||||
|
||||
const { getByTestId } = render(<TopBar/>);
|
||||
const { getByTestId } = render(<TopBar />);
|
||||
|
||||
expect(getByTestId("window-menu")).toBeInTheDocument();
|
||||
expect(getByTestId("window-minimize")).toBeInTheDocument();
|
||||
@ -82,7 +88,7 @@ describe("<Tobar/> in Windows and Linux", () => {
|
||||
mockConfig.isWindows = false;
|
||||
mockConfig.isLinux = true;
|
||||
|
||||
const { getByTestId } = render(<TopBar/>);
|
||||
const { getByTestId } = render(<TopBar />);
|
||||
|
||||
expect(getByTestId("window-menu")).toBeInTheDocument();
|
||||
expect(getByTestId("window-minimize")).toBeInTheDocument();
|
||||
@ -93,7 +99,7 @@ describe("<Tobar/> in Windows and Linux", () => {
|
||||
it("triggers ipc events on click", () => {
|
||||
mockConfig.isWindows = true;
|
||||
|
||||
const { getByTestId } = render(<TopBar/>);
|
||||
const { getByTestId } = render(<TopBar />);
|
||||
|
||||
const menu = getByTestId("window-menu");
|
||||
const minimize = getByTestId("window-minimize");
|
||||
@ -20,14 +20,23 @@
|
||||
*/
|
||||
|
||||
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 { TopBar } from "../topbar";
|
||||
import { TopBarRegistry } from "../../../../extensions/registries";
|
||||
import { TopBar } from "./top-bar";
|
||||
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";
|
||||
|
||||
jest.mock("../../../../common/vars", () => {
|
||||
const SemVer = require("semver").SemVer;
|
||||
|
||||
const versionStub = new SemVer("1.0.0");
|
||||
|
||||
return {
|
||||
isMac: true,
|
||||
appSemVer: versionStub,
|
||||
};
|
||||
});
|
||||
|
||||
@ -76,12 +85,13 @@ jest.mock("@electron/remote", () => {
|
||||
});
|
||||
|
||||
describe("<TopBar/>", () => {
|
||||
beforeEach(() => {
|
||||
TopBarRegistry.createInstance();
|
||||
});
|
||||
let di: ConfigurableDependencyInjectionContainer;
|
||||
let render: DiRender;
|
||||
|
||||
afterEach(() => {
|
||||
TopBarRegistry.resetInstance();
|
||||
beforeEach(() => {
|
||||
di = getDiForUnitTesting();
|
||||
|
||||
render = renderFor(di);
|
||||
});
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
@ -129,13 +139,13 @@ describe("<TopBar/>", () => {
|
||||
const testId = "testId";
|
||||
const text = "an item";
|
||||
|
||||
TopBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
||||
di.override(topBarItemsInjectable, () => computed(() => [
|
||||
{
|
||||
components: {
|
||||
Item: () => <span data-testid={testId}>{text}</span>,
|
||||
},
|
||||
},
|
||||
]);
|
||||
]));
|
||||
|
||||
const { getByTestId } = render(<TopBar/>);
|
||||
|
||||
@ -19,22 +19,28 @@
|
||||
* 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 { observer } from "mobx-react";
|
||||
import { TopBarRegistry } from "../../../extensions/registries";
|
||||
import { Icon } from "../icon";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { Icon } from "../../icon";
|
||||
import { webContents, getCurrentWindow } from "@electron/remote";
|
||||
import { observable } from "mobx";
|
||||
import { broadcastMessage, ipcRendererOn } from "../../../common/ipc";
|
||||
import { watchHistoryState } from "../../remote-helpers/history-updater";
|
||||
import { isActiveRoute, navigate } from "../../navigation";
|
||||
import { catalogRoute, catalogURL } from "../../../common/routes";
|
||||
import { IpcMainWindowEvents } from "../../../main/window-manager";
|
||||
import { isLinux, isWindows } from "../../../common/vars";
|
||||
import { cssNames } from "../../utils";
|
||||
import { broadcastMessage, ipcRendererOn } from "../../../../common/ipc";
|
||||
import { watchHistoryState } from "../../../remote-helpers/history-updater";
|
||||
import { isActiveRoute, navigate } from "../../../navigation";
|
||||
import { catalogRoute, catalogURL } from "../../../../common/routes";
|
||||
import { IpcMainWindowEvents } from "../../../../main/window-manager";
|
||||
import { isLinux, isWindows } from "../../../../common/vars";
|
||||
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);
|
||||
@ -48,34 +54,10 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => {
|
||||
nextEnabled.set(state);
|
||||
});
|
||||
|
||||
export const TopBar = observer(({ children, ...rest }: Props) => {
|
||||
const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) => {
|
||||
const elem = useRef<HTMLDivElement>();
|
||||
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 = () => {
|
||||
broadcastMessage(IpcMainWindowEvents.OPEN_CONTEXT_MENU);
|
||||
};
|
||||
@ -156,7 +138,7 @@ export const TopBar = observer(({ children, ...rest }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.controls}>
|
||||
{renderRegisteredItems()}
|
||||
{renderRegisteredItems(items.get())}
|
||||
{children}
|
||||
{(isWindows || isLinux) && (
|
||||
<div className={cssNames(styles.windowButtons, { [styles.linuxButtons]: isLinux })}>
|
||||
@ -174,3 +156,29 @@ export const TopBar = observer(({ children, ...rest }: Props) => {
|
||||
</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,
|
||||
}),
|
||||
});
|
||||
@ -27,7 +27,7 @@ import { observer } from "mobx-react";
|
||||
import { boundMethod, cssNames } from "../../utils";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { Icon, IconProps } from "../icon";
|
||||
import { Menu, MenuItem, MenuProps } from "../menu";
|
||||
import { Menu, MenuItem, MenuProps } from "./menu";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import isString from "lodash/isString";
|
||||
|
||||
|
||||
@ -243,7 +243,9 @@ export class Menu extends React.Component<MenuProps, State> {
|
||||
break;
|
||||
|
||||
case "Space":
|
||||
case "Enter":
|
||||
// fallthrough
|
||||
|
||||
case "Enter": {
|
||||
const focusedItem = this.focusedItem;
|
||||
|
||||
if (focusedItem) {
|
||||
@ -251,10 +253,12 @@ export class Menu extends React.Component<MenuProps, State> {
|
||||
evt.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "ArrowUp":
|
||||
this.focusNextItem(true);
|
||||
break;
|
||||
|
||||
case "ArrowDown":
|
||||
this.focusNextItem();
|
||||
break;
|
||||
|
||||
@ -24,7 +24,8 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { action, computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { editor, Uri } from "monaco-editor";
|
||||
import { MonacoTheme, MonacoValidator, monacoValidators } from "./index";
|
||||
import type { MonacoTheme } from "./monaco-themes";
|
||||
import { MonacoValidator, monacoValidators } from "./monaco-validators";
|
||||
import { debounce, merge } from "lodash";
|
||||
import { cssNames, disposer } from "../../utils";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
|
||||
@ -228,10 +228,15 @@ html {
|
||||
}
|
||||
|
||||
.Select {
|
||||
&__value-container {
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__control {
|
||||
box-shadow: 0 0 0 1px var(--inputControlBorder);
|
||||
background: var(--inputControlBackground);
|
||||
border-radius: 5px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
&__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) {
|
||||
const classes = useStyles();
|
||||
|
||||
|
||||
@ -21,3 +21,4 @@
|
||||
|
||||
export * from "./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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use <Switch/> instead from "../switch.tsx".
|
||||
*/
|
||||
export const Switcher = withStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
|
||||
@ -20,8 +20,7 @@
|
||||
*/
|
||||
|
||||
import styles from "./react-table.module.scss";
|
||||
import React from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { useFlexLayout, useSortBy, useTable, UseTableOptions } from "react-table";
|
||||
import { Icon } from "../icon";
|
||||
import { cssNames } from "../../utils";
|
||||
|
||||
@ -27,7 +27,6 @@ export * from "./ipc";
|
||||
export * from "./kube-object-detail-registry";
|
||||
export * from "./kube-object-menu-registry";
|
||||
export * from "./registries";
|
||||
export * from "./welcome-menu-registry";
|
||||
export * from "./workloads-overview-detail-registry";
|
||||
export * from "./catalog-category-registry";
|
||||
export * from "./status-bar-registry";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user