mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into extension-auto-update
This commit is contained in:
commit
d26322df1a
@ -16,6 +16,12 @@ module.exports = {
|
||||
react: {
|
||||
version: packageJson.devDependencies.react || "detect",
|
||||
},
|
||||
// the package eslint-import-resolver-typescript is required for this line which fixes errors when using .d.ts files
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"alwaysTryTypes": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
6
Makefile
6
Makefile
@ -103,8 +103,12 @@ publish-npm: node_modules build-npm
|
||||
cd src/extensions/npm/extensions && npm publish --access=public --tag=$(NPM_RELEASE_TAG)
|
||||
git restore src/extensions/npm/extensions/package.json
|
||||
|
||||
.PHONY: build-docs
|
||||
build-docs:
|
||||
yarn typedocs-extensions-api
|
||||
|
||||
.PHONY: docs
|
||||
docs:
|
||||
docs: build-docs
|
||||
yarn mkdocs-serve-local
|
||||
|
||||
.PHONY: clean-extensions
|
||||
|
||||
@ -14,25 +14,27 @@ Each guide or code sample includes the following:
|
||||
|
||||
## Guides
|
||||
|
||||
| Guide | APIs |
|
||||
| ----- | ----- |
|
||||
| [Generate new extension project](generator.md) ||
|
||||
| [Main process extension](main-extension.md) | Main.LensExtension |
|
||||
| [Renderer process extension](renderer-extension.md) | Renderer.LensExtension |
|
||||
| [Resource stack (cluster feature)](resource-stack.md) | |
|
||||
| [Stores](stores.md) | |
|
||||
| [Components](components.md) | |
|
||||
| [KubeObjectListLayout](kube-object-list-layout.md) | |
|
||||
| [Working with mobx](working-with-mobx.md) | |
|
||||
| [Protocol Handlers](protocol-handlers.md) | |
|
||||
| [Sending Data between main and renderer](ipc.md) | |
|
||||
| Guide | APIs |
|
||||
| --------------------------------------------------------------- | ---------------------- |
|
||||
| [Generate new extension project](generator.md) | |
|
||||
| [Main process extension](main-extension.md) | Main.LensExtension |
|
||||
| [Renderer process extension](renderer-extension.md) | Renderer.LensExtension |
|
||||
| [Resource stack (cluster feature)](resource-stack.md) | |
|
||||
| [Extending KubernetesCluster)](extending-kubernetes-cluster.md) | |
|
||||
| [Stores](stores.md) | |
|
||||
| [Components](components.md) | |
|
||||
| [KubeObjectListLayout](kube-object-list-layout.md) | |
|
||||
| [Working with mobx](working-with-mobx.md) | |
|
||||
| [Protocol Handlers](protocol-handlers.md) | |
|
||||
| [Sending Data between main and renderer](ipc.md) | |
|
||||
| [Catalog Entities and Categories](catalog.md) | |
|
||||
|
||||
## Samples
|
||||
|
||||
| Sample | APIs |
|
||||
| ----- | ----- |
|
||||
[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||
[styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension <br> LensRendererExtension <br> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||
[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||
[styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension <br> LensRendererExtension <br> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||
[custom-resource-page](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page) | LensRendererExtension <br> Renderer.K8sApi.KubeApi <br> Renderer.K8sApi.KubeObjectStore <br> Renderer.Component.KubeObjectListLayout <br> Renderer.Component.KubeObjectDetailsProps <br> Renderer.Component.IconProps |
|
||||
| Sample | APIs |
|
||||
| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||
| [styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension <br> LensRendererExtension <br> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||
| [styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||
| [styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension <br> LensRendererExtension <br> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||
| [custom-resource-page](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page) | LensRendererExtension <br> Renderer.K8sApi.KubeApi <br> Renderer.K8sApi.KubeObjectStore <br> Renderer.Component.KubeObjectListLayout <br> Renderer.Component.KubeObjectDetailsProps <br> Renderer.Component.IconProps |
|
||||
|
||||
@ -1,5 +1,27 @@
|
||||
# Catalog (WIP)
|
||||
|
||||
## CatalogCategoryRegistry
|
||||
This guide is a brief overview about how the catalog works within Lens.
|
||||
The catalog should be thought of as the single source of truth about data within Lens.
|
||||
|
||||
## CatalogEntityRegistry
|
||||
The data flow is unidirectional, it only flows from the main side to the renderer side.
|
||||
All data is public within the catalog.
|
||||
|
||||
## Categories
|
||||
|
||||
A category is the declaration to the catalog of a specific kind of entity.
|
||||
It declares the currently supported versions of that kind of entity but providing the constructors for the entity classes.
|
||||
|
||||
To declare a new category class you must create a new class that extends [Common.Catalog.CatalogCategory](../api/classes/Common.Catalog.CatalogCategory.md) and implement all of the abstract fields.
|
||||
|
||||
The categories provided by Lens itself have the following names:
|
||||
|
||||
- `KubernetesClusters`
|
||||
- `WebLinks`
|
||||
- `General`
|
||||
|
||||
To register a category, call the `Main.Catalog.catalogCategories.add()` and `Renderer.Catalog.catalogCategories.add()` with instances of your class.
|
||||
|
||||
## Entities
|
||||
|
||||
An entity is the data within the catalog.
|
||||
All entities are typed and the class instances will be recreated on the renderer side by the catalog and the category registrations.
|
||||
|
||||
69
docs/extensions/guides/extending-kubernetes-cluster.md
Normal file
69
docs/extensions/guides/extending-kubernetes-cluster.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Extending KubernetesCluster
|
||||
|
||||
Extension can specify it's own subclass of Common.Catalog.KubernetesCluster. Extension can also specify a new Category for it in the Catalog.
|
||||
|
||||
## Extending Common.Catalog.KubernetesCluster
|
||||
|
||||
``` typescript
|
||||
import { Common } from "@k8slens/extensions";
|
||||
|
||||
// The kind must be different from KubernetesCluster's kind
|
||||
export const kind = "ManagedDevCluster";
|
||||
|
||||
export class ManagedDevCluster extends Common.Catalog.KubernetesCluster {
|
||||
public static readonly kind = kind;
|
||||
|
||||
public readonly kind = kind;
|
||||
}
|
||||
```
|
||||
|
||||
## Extending Common.Catalog.CatalogCategory
|
||||
|
||||
These custom Catalog entities can be added a new Category in the Catalog.
|
||||
|
||||
``` typescript
|
||||
import { Common } from "@k8slens/extensions";
|
||||
import { kind, ManagedDevCluster } from "../entities/ManagedDevCluster";
|
||||
|
||||
class ManagedDevClusterCategory extends Common.Catalog.CatalogCategory {
|
||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "CatalogCategory";
|
||||
public metadata = {
|
||||
name: "Managed Dev Clusters",
|
||||
icon: ""
|
||||
};
|
||||
public spec: Common.Catalog.CatalogCategorySpec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: ManagedDevCluster as any,
|
||||
},
|
||||
],
|
||||
names: {
|
||||
kind
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { ManagedDevClusterCategory };
|
||||
export type { ManagedDevClusterCategory as ManagedDevClusterCategoryType };
|
||||
```
|
||||
|
||||
The category needs to be registered in the `onActivate()` method both in main and renderer
|
||||
|
||||
``` typescript
|
||||
// in main's on onActivate
|
||||
Main.Catalog.catalogCategories.add(new ManagedDevClusterCategory());
|
||||
```
|
||||
|
||||
``` typescript
|
||||
// in renderer's on onActivate
|
||||
Renderer.Catalog.catalogCategories.add(new ManagedDevClusterCategory());
|
||||
```
|
||||
|
||||
You can then add the entities to the Catalog as a new source:
|
||||
|
||||
``` typescript
|
||||
this.addCatalogSource("managedDevClusters", this.managedDevClusters);
|
||||
```
|
||||
@ -16,11 +16,11 @@ import { Main } from "@k8slens/extensions";
|
||||
|
||||
export default class ExampleExtensionMain extends Main.LensExtension {
|
||||
onActivate() {
|
||||
console.log('custom main process extension code started');
|
||||
console.log("custom main process extension code started");
|
||||
}
|
||||
|
||||
onDeactivate() {
|
||||
console.log('custom main process extension de-activated');
|
||||
console.log("custom main process extension de-activated");
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -33,21 +33,21 @@ Implementing `onDeactivate()` gives you the opportunity to clean up after your e
|
||||
Disable extensions from the Lens Extensions page:
|
||||
|
||||
1. Navigate to **File** > **Extensions** in the top menu bar.
|
||||
(On Mac, it is **Lens** > **Extensions**.)
|
||||
(On Mac, it is **Lens** > **Extensions**.)
|
||||
2. Click **Disable** on the extension you want to disable.
|
||||
|
||||
The example above logs messages when the extension is enabled and disabled.
|
||||
To see standard output from the main process there must be a console connected to it.
|
||||
Achieve this by starting Lens from the command prompt.
|
||||
|
||||
For more details on accessing Lens state data, please see the [Stores](../stores) guide.
|
||||
For more details on accessing Lens state data, please see the [Stores](stores.md) guide.
|
||||
|
||||
### `appMenus`
|
||||
|
||||
The Main Extension API allows you to customize the UI application menu.
|
||||
The following example demonstrates adding an item to the **Help** menu.
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import { Main } from "@k8slens/extensions";
|
||||
|
||||
export default class SamplePageMainExtension extends Main.LensExtension {
|
||||
@ -57,9 +57,9 @@ export default class SamplePageMainExtension extends Main.LensExtension {
|
||||
label: "Sample",
|
||||
click() {
|
||||
console.log("Sample clicked");
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
@ -67,18 +67,18 @@ export default class SamplePageMainExtension extends Main.LensExtension {
|
||||
`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.
|
||||
Valid values include: `"file"`, `"edit"`, `"view"`, and `"help"`.
|
||||
`"lens"` is valid on Mac only.
|
||||
* `label` is the name of your menu item.
|
||||
* `click()` is called when the menu item is selected.
|
||||
In this example, we simply log a message.
|
||||
However, you would typically have this navigate to a specific page or perform another operation.
|
||||
Note that pages are associated with the [`Renderer.LensExtension`](renderer-extension.md) class and can be defined in the process of extending it.
|
||||
- `parentId` is the name of the menu where your new menu item will be listed.
|
||||
Valid values include: `"file"`, `"edit"`, `"view"`, and `"help"`.
|
||||
`"lens"` is valid on Mac only.
|
||||
- `label` is the name of your menu item.
|
||||
- `click()` is called when the menu item is selected.
|
||||
In this example, we simply log a message.
|
||||
However, you would typically have this navigate to a specific page or perform another operation.
|
||||
Note that pages are associated with the [`Renderer.LensExtension`](renderer-extension.md) class and can be defined in the process of extending it.
|
||||
|
||||
The following example demonstrates how an application menu can be used to navigate to such a page:
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import { Main } from "@k8slens/extensions";
|
||||
|
||||
export default class SamplePageMainExtension extends Main.LensExtension {
|
||||
@ -86,9 +86,9 @@ export default class SamplePageMainExtension extends Main.LensExtension {
|
||||
{
|
||||
parentId: "help",
|
||||
label: "Sample",
|
||||
click: () => this.navigate("myGlobalPage")
|
||||
}
|
||||
]
|
||||
click: () => this.navigate("myGlobalPage"),
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
@ -99,32 +99,36 @@ This page would be defined in your extension's `Renderer.LensExtension` implemen
|
||||
|
||||
`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
|
||||
```typescript
|
||||
interface TrayMenuRegistration {
|
||||
label?: string;
|
||||
click?: (menuItem: TrayMenuRegistration) => void;
|
||||
id?: string;
|
||||
type?: "normal" | "separator" | "submenu"
|
||||
type?: "normal" | "separator" | "submenu";
|
||||
toolTip?: string;
|
||||
enabled?: boolean;
|
||||
submenu?: TrayMenuRegistration[]
|
||||
submenu?: TrayMenuRegistration[];
|
||||
}
|
||||
```
|
||||
|
||||
The following example demonstrates how tray menus can be added from extension:
|
||||
|
||||
``` typescript
|
||||
```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!") }
|
||||
}]
|
||||
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).
|
||||
See the [`Catalog`](catalog.md) documentation for full details about the catalog.
|
||||
See the [`Catalog`](catalog.md) documentation for full details about the catalog.
|
||||
|
||||
@ -24,6 +24,7 @@ nav:
|
||||
- Renderer Extension: extensions/guides/renderer-extension.md
|
||||
- Catalog: extensions/guides/catalog.md
|
||||
- Resource Stack: extensions/guides/resource-stack.md
|
||||
- Extending KubernetesCluster: extensions/guides/extending-kubernetes-cluster.md
|
||||
- Stores: extensions/guides/stores.md
|
||||
- Working with MobX: extensions/guides/working-with-mobx.md
|
||||
- Protocol Handlers: extensions/guides/protocol-handlers.md
|
||||
|
||||
@ -340,6 +340,7 @@
|
||||
"esbuild": "^0.13.15",
|
||||
"esbuild-loader": "^2.16.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-react": "^7.27.1",
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
|
||||
import fs from "fs";
|
||||
import mockFs from "mock-fs";
|
||||
import yaml from "js-yaml";
|
||||
import path from "path";
|
||||
import fse from "fs-extra";
|
||||
import type { Cluster } from "../cluster/cluster";
|
||||
@ -334,159 +333,6 @@ users:
|
||||
});
|
||||
});
|
||||
|
||||
describe("pre 2.0 config with an existing cluster", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
|
||||
const mockOpts = {
|
||||
"some-directory-for-user-data": {
|
||||
"lens-cluster-store.json": JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
cluster1: minimalValidKubeConfig,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
|
||||
clusterStore = mainDi.inject(clusterStoreInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("migrates to modern format with kubeconfig in a file", async () => {
|
||||
const config = clusterStore.clustersList[0].kubeConfigPath;
|
||||
|
||||
expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
"some-directory-for-user-data": {
|
||||
"lens-cluster-store.json": JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "2.4.1",
|
||||
},
|
||||
},
|
||||
cluster1: {
|
||||
kubeConfig: JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
clusters: [
|
||||
{
|
||||
cluster: {
|
||||
server: "https://10.211.55.6:8443",
|
||||
},
|
||||
name: "minikube",
|
||||
},
|
||||
],
|
||||
contexts: [
|
||||
{
|
||||
context: {
|
||||
cluster: "minikube",
|
||||
user: "minikube",
|
||||
name: "minikube",
|
||||
},
|
||||
name: "minikube",
|
||||
},
|
||||
],
|
||||
"current-context": "minikube",
|
||||
kind: "Config",
|
||||
preferences: {},
|
||||
users: [
|
||||
{
|
||||
name: "minikube",
|
||||
user: {
|
||||
"client-certificate": "/Users/foo/.minikube/client.crt",
|
||||
"client-key": "/Users/foo/.minikube/client.key",
|
||||
"auth-provider": {
|
||||
config: {
|
||||
"access-token": ["should be string"],
|
||||
expiry: ["should be string"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
|
||||
clusterStore = mainDi.inject(clusterStoreInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("replaces array format access token and expiry into string", async () => {
|
||||
const file = clusterStore.clustersList[0].kubeConfigPath;
|
||||
const config = fs.readFileSync(file, "utf8");
|
||||
const kc = yaml.load(config) as Record<string, any>;
|
||||
|
||||
expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe(
|
||||
"should be string",
|
||||
);
|
||||
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe(
|
||||
"should be string",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pre 2.6.0 config with a cluster icon", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
"some-directory-for-user-data": {
|
||||
"lens-cluster-store.json": JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "2.4.1",
|
||||
},
|
||||
},
|
||||
cluster1: {
|
||||
kubeConfig: minimalValidKubeConfig,
|
||||
icon: "icon_path",
|
||||
preferences: {
|
||||
terminalCWD: "/some-directory-for-user-data",
|
||||
},
|
||||
},
|
||||
}),
|
||||
icon_path: testDataIcon,
|
||||
},
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
|
||||
clusterStore = mainDi.inject(clusterStoreInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("moves the icon into preferences", async () => {
|
||||
const storedClusterData = clusterStore.clustersList[0];
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
|
||||
@ -59,8 +59,8 @@ export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||
}
|
||||
|
||||
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||
public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||
public static readonly kind = "KubernetesCluster";
|
||||
public static readonly apiVersion: string = "entity.k8slens.dev/v1alpha1";
|
||||
public static readonly kind: string = "KubernetesCluster";
|
||||
|
||||
public readonly apiVersion = KubernetesCluster.apiVersion;
|
||||
public readonly kind = KubernetesCluster.kind;
|
||||
|
||||
@ -8,6 +8,7 @@ import type TypedEmitter from "typed-emitter";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { once } from "lodash";
|
||||
import { iter, Disposer } from "../utils";
|
||||
import type { CategoryColumnRegistration } from "../../renderer/components/+catalog/custom-category-columns";
|
||||
|
||||
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
|
||||
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never;
|
||||
@ -46,6 +47,7 @@ export interface CatalogCategorySpec {
|
||||
* The grouping for for the category. This MUST be a DNS label.
|
||||
*/
|
||||
group: string;
|
||||
|
||||
/**
|
||||
* The specific versions of the constructors.
|
||||
*
|
||||
@ -54,6 +56,10 @@ export interface CatalogCategorySpec {
|
||||
* `name = "v1alpha1"` then the resulting `.apiVersion` MUST be `entity.k8slens.dev/v1alpha1`
|
||||
*/
|
||||
versions: CatalogCategoryVersion<CatalogEntity>[];
|
||||
|
||||
/**
|
||||
* This is the concerning the category
|
||||
*/
|
||||
names: {
|
||||
/**
|
||||
* The kind of entity that this category is for. This value MUST be a DNS
|
||||
@ -62,38 +68,107 @@ export interface CatalogCategorySpec {
|
||||
*/
|
||||
kind: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* These are the columns used for displaying entities when in the catalog.
|
||||
*
|
||||
* If this is not provided then some default columns will be used, similar in
|
||||
* scope to the columns in the "Browse" view.
|
||||
*
|
||||
* Even if you provide columns, a "Name" column will be provided as well with
|
||||
* `priority: 0`.
|
||||
*
|
||||
* These columns will not be used in the "Browse" view.
|
||||
*/
|
||||
displayColumns?: CategoryColumnRegistration[];
|
||||
}
|
||||
|
||||
/**
|
||||
* If the filter returns true, the menu item is displayed
|
||||
* If the filter return a thruthy value, the menu item is displayed
|
||||
*/
|
||||
export type AddMenuFilter = (menu: CatalogEntityAddMenu) => any;
|
||||
|
||||
export interface CatalogCategoryEvents {
|
||||
/**
|
||||
* This event will be emitted when the category is loaded in the catalog
|
||||
* view.
|
||||
*/
|
||||
load: () => void;
|
||||
|
||||
/**
|
||||
* This event will be emitted when the catalog add menu is opened and is the
|
||||
* way to added entries to that menu.
|
||||
*/
|
||||
catalogAddMenu: (context: CatalogEntityAddMenuContext) => void;
|
||||
|
||||
/**
|
||||
* This event will be emitted when the context menu for an entity is declared
|
||||
* by this category is opened.
|
||||
*/
|
||||
contextMenuOpen: (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void;
|
||||
}
|
||||
|
||||
export abstract class CatalogCategory extends (EventEmitter as new () => TypedEmitter<CatalogCategoryEvents>) {
|
||||
/**
|
||||
* The version of category that you are wanting to declare.
|
||||
*
|
||||
* Currently supported values:
|
||||
*
|
||||
* - `"catalog.k8slens.dev/v1alpha1"`
|
||||
*/
|
||||
abstract readonly apiVersion: string;
|
||||
|
||||
/**
|
||||
* The kind of item you wish to declare.
|
||||
*
|
||||
* Currently supported values:
|
||||
*
|
||||
* - `"CatalogCategory"`
|
||||
*/
|
||||
abstract readonly kind: string;
|
||||
abstract metadata: {
|
||||
|
||||
/**
|
||||
* The data about the category itself
|
||||
*/
|
||||
abstract readonly metadata: {
|
||||
/**
|
||||
* The name of your category. The category can be searched for by this
|
||||
* value. This will also be used for the catalog menu.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Either an `<svg>` or the name of an icon from {@link IconProps}
|
||||
*/
|
||||
icon: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The most important part of a category, as it is where entity versions are declared.
|
||||
*/
|
||||
abstract spec: CatalogCategorySpec;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected filters = observable.set<AddMenuFilter>([], {
|
||||
deep: false,
|
||||
});
|
||||
|
||||
static parseId(id = ""): { group?: string, kind?: string } {
|
||||
/**
|
||||
* Parse a category ID into parts.
|
||||
* @param id The id of a category is parse
|
||||
* @returns The group and kind parts of the ID
|
||||
*/
|
||||
public static parseId(id: string): { group?: string, kind?: string } {
|
||||
const [group, kind] = id.split("/") ?? [];
|
||||
|
||||
return { group, kind };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of this category
|
||||
*/
|
||||
public getId(): string {
|
||||
return `${this.spec.group}/${this.spec.names.kind}`;
|
||||
}
|
||||
|
||||
@ -67,6 +67,15 @@ const colorTheme: PreferenceDescription<string> = {
|
||||
},
|
||||
};
|
||||
|
||||
const terminalTheme: PreferenceDescription<string | undefined> = {
|
||||
fromStore(val) {
|
||||
return val || "";
|
||||
},
|
||||
toStore(val) {
|
||||
return val || undefined;
|
||||
},
|
||||
};
|
||||
|
||||
const localeTimezone: PreferenceDescription<string> = {
|
||||
fromStore(val) {
|
||||
return val || moment.tz.guess(true) || "UTC";
|
||||
@ -335,6 +344,7 @@ export const DESCRIPTORS = {
|
||||
httpsProxy,
|
||||
shell,
|
||||
colorTheme,
|
||||
terminalTheme,
|
||||
localeTimezone,
|
||||
allowUntrustedCAs,
|
||||
allowTelemetry,
|
||||
|
||||
@ -49,6 +49,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
@observable allowErrorReporting: boolean;
|
||||
@observable allowUntrustedCAs: boolean;
|
||||
@observable colorTheme: string;
|
||||
@observable terminalTheme: string;
|
||||
@observable localeTimezone: string;
|
||||
@observable downloadMirror: string;
|
||||
@observable httpsProxy?: string;
|
||||
@ -170,6 +171,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
this.httpsProxy = DESCRIPTORS.httpsProxy.fromStore(preferences?.httpsProxy);
|
||||
this.shell = DESCRIPTORS.shell.fromStore(preferences?.shell);
|
||||
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(preferences?.colorTheme);
|
||||
this.terminalTheme = DESCRIPTORS.terminalTheme.fromStore(preferences?.terminalTheme);
|
||||
this.localeTimezone = DESCRIPTORS.localeTimezone.fromStore(preferences?.localeTimezone);
|
||||
this.allowUntrustedCAs = DESCRIPTORS.allowUntrustedCAs.fromStore(preferences?.allowUntrustedCAs);
|
||||
this.allowTelemetry = DESCRIPTORS.allowTelemetry.fromStore(preferences?.allowTelemetry);
|
||||
@ -194,6 +196,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
httpsProxy: DESCRIPTORS.httpsProxy.toStore(this.httpsProxy),
|
||||
shell: DESCRIPTORS.shell.toStore(this.shell),
|
||||
colorTheme: DESCRIPTORS.colorTheme.toStore(this.colorTheme),
|
||||
terminalTheme: DESCRIPTORS.terminalTheme.toStore(this.terminalTheme),
|
||||
localeTimezone: DESCRIPTORS.localeTimezone.toStore(this.localeTimezone),
|
||||
allowUntrustedCAs: DESCRIPTORS.allowUntrustedCAs.toStore(this.allowUntrustedCAs),
|
||||
allowTelemetry: DESCRIPTORS.allowTelemetry.toStore(this.allowTelemetry),
|
||||
|
||||
19
src/common/utils/__tests__/bind.test.ts
Normal file
19
src/common/utils/__tests__/bind.test.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { bind } from "../index";
|
||||
|
||||
describe("bind", () => {
|
||||
it("should work correctly", () => {
|
||||
function foobar(bound: number, nonBound: number): number {
|
||||
expect(typeof bound).toBe("number");
|
||||
expect(typeof nonBound).toBe("number");
|
||||
|
||||
return bound + nonBound;
|
||||
}
|
||||
const foobarBound = bind(foobar, null, 5);
|
||||
|
||||
expect(foobarBound(10)).toBe(15);
|
||||
});
|
||||
});
|
||||
19
src/common/utils/collection-functions.ts
Normal file
19
src/common/utils/collection-functions.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the value behind `key`. If it was not present, first insert `value`
|
||||
* @param map The map to interact with
|
||||
* @param key The key to insert into the map with
|
||||
* @param value The value to optional add to the map
|
||||
* @returns The value in the map
|
||||
*/
|
||||
export function getOrInsert<K, V>(map: Map<K, V>, key: K, value: V): V {
|
||||
if (!map.has(key)) {
|
||||
map.set(key, value);
|
||||
}
|
||||
|
||||
return map.get(key);
|
||||
}
|
||||
@ -10,11 +10,19 @@ export function noop<T extends any[]>(...args: T): void {
|
||||
return void args;
|
||||
}
|
||||
|
||||
/**
|
||||
* A typecorrect version of <function>.bind()
|
||||
*/
|
||||
export function bind<BoundArgs extends any[], NonBoundArgs extends any[], ReturnType>(fn: (...args: [...BoundArgs, ...NonBoundArgs]) => ReturnType, thisArg: any, ...boundArgs: BoundArgs): (...args: NonBoundArgs) => ReturnType {
|
||||
return fn.bind(thisArg, ...boundArgs);
|
||||
}
|
||||
|
||||
export * from "./app-version";
|
||||
export * from "./autobind";
|
||||
export * from "./camelCase";
|
||||
export * from "./cloneJson";
|
||||
export * from "./cluster-id-url-parsing";
|
||||
export * from "./collection-functions";
|
||||
export * from "./convertCpu";
|
||||
export * from "./convertMemory";
|
||||
export * from "./debouncePromise";
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export type { AppPreferenceRegistration, AppPreferenceComponents } from "../registries/app-preference-registry";
|
||||
export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../renderer/components/+preferences/app-preferences/app-preference-registration";
|
||||
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry";
|
||||
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
|
||||
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
|
||||
|
||||
@ -269,7 +269,6 @@ export class ExtensionLoader {
|
||||
return this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||
const removeItems = [
|
||||
registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension),
|
||||
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
|
||||
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
|
||||
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
|
||||
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
|
||||
@ -291,11 +290,12 @@ export class ExtensionLoader {
|
||||
});
|
||||
};
|
||||
|
||||
loadOnClusterRenderer = (entity: KubernetesCluster) => {
|
||||
loadOnClusterRenderer = (getCluster: () => KubernetesCluster) => {
|
||||
logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
|
||||
|
||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||
if ((await extension.isEnabledForCluster(entity)) === false) {
|
||||
// getCluster must be a callback, as the entity might be available only after an extension has been loaded
|
||||
if ((await extension.isEnabledForCluster(getCluster())) === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -324,11 +324,15 @@ export class ExtensionLoader {
|
||||
this.extensions.get(extension.id).availableUpdate = await extension.checkForUpdate();
|
||||
}
|
||||
|
||||
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
|
||||
const loadingExtensions: ExtensionLoading[] = [];
|
||||
protected async loadExtensions(installedExtensions: Map<string, InstalledExtension>, register: (ext: LensExtension) => Promise<Disposer[]>) {
|
||||
// Steps of the function:
|
||||
// 1. require and call .activate for each Extension
|
||||
// 2. Wait until every extension's onActivate has been resolved
|
||||
// 3. Call .enable for each extension
|
||||
// 4. Return ExtensionLoading[]
|
||||
|
||||
reaction(() => this.toJSON(), async installedExtensions => {
|
||||
for (const [extId, extension] of installedExtensions) {
|
||||
const extensions = [...installedExtensions.entries()]
|
||||
.map(([extId, extension]) => {
|
||||
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
|
||||
|
||||
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
|
||||
@ -337,7 +341,8 @@ export class ExtensionLoader {
|
||||
|
||||
if (!LensExtensionClass) {
|
||||
this.nonInstancesByName.add(extension.manifest.name);
|
||||
continue;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// const instance = new LensExtensionClass(extension, this.extensionUpdateChecker);
|
||||
@ -347,27 +352,49 @@ export class ExtensionLoader {
|
||||
this.extensionUpdateChecker,
|
||||
);
|
||||
|
||||
const loaded = instance.enable(register).catch((err) => {
|
||||
logger.error(`${logModule}: failed to enable`, { ext: extension, err });
|
||||
});
|
||||
|
||||
loadingExtensions.push({
|
||||
isBundled: extension.isBundled,
|
||||
loaded,
|
||||
});
|
||||
this.instances.set(extId, instance);
|
||||
|
||||
return {
|
||||
extId,
|
||||
instance,
|
||||
isBundled: extension.isBundled,
|
||||
activated: instance.activate(),
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error(`${logModule}: activation extension error`, { ext: extension, err });
|
||||
}
|
||||
} else if (!extension.isEnabled && alreadyInit) {
|
||||
this.removeInstance(extId);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
});
|
||||
|
||||
return loadingExtensions;
|
||||
return null;
|
||||
})
|
||||
// Remove null values
|
||||
.filter(extension => Boolean(extension));
|
||||
|
||||
// We first need to wait until each extension's `onActivate` is resolved,
|
||||
// as this might register new catalog categories. Afterwards we can safely .enable the extension.
|
||||
await Promise.all(extensions.map(extension => extension.activated));
|
||||
|
||||
// Return ExtensionLoading[]
|
||||
return extensions.map(extension => {
|
||||
const loaded = extension.instance.enable(register).catch((err) => {
|
||||
logger.error(`${logModule}: failed to enable`, { ext: extension, err });
|
||||
});
|
||||
|
||||
return {
|
||||
isBundled: extension.isBundled,
|
||||
loaded,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
|
||||
// Setup reaction to load extensions on JSON changes
|
||||
reaction(() => this.toJSON(), installedExtensions => this.loadExtensions(installedExtensions, register));
|
||||
|
||||
// Load initial extensions
|
||||
return this.loadExtensions(this.toJSON(), register);
|
||||
}
|
||||
|
||||
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null {
|
||||
|
||||
@ -91,7 +91,6 @@ export class LensExtension {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.onActivate();
|
||||
this._isEnabled = true;
|
||||
|
||||
this[Disposers].push(...await register(this));
|
||||
@ -118,6 +117,12 @@ export class LensExtension {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
activate() {
|
||||
return this.onActivate();
|
||||
}
|
||||
|
||||
|
||||
public async checkForUpdate() {
|
||||
return this.updateChecker?.run(this.manifest);
|
||||
}
|
||||
|
||||
@ -15,13 +15,15 @@ import type { KubernetesCluster } from "../common/catalog-entities";
|
||||
import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/welcome-menu-items/welcome-menu-registration";
|
||||
import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration";
|
||||
import type { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands";
|
||||
import type { AppPreferenceRegistration } from "../renderer/components/+preferences/app-preferences/app-preference-registration";
|
||||
import type { AdditionalCategoryColumnRegistration } from "../renderer/components/+catalog/custom-category-columns";
|
||||
|
||||
export class LensRendererExtension extends LensExtension {
|
||||
globalPages: registries.PageRegistration[] = [];
|
||||
clusterPages: registries.PageRegistration[] = [];
|
||||
clusterPageMenus: registries.ClusterPageMenuRegistration[] = [];
|
||||
kubeObjectStatusTexts: registries.KubeObjectStatusRegistration[] = [];
|
||||
appPreferences: registries.AppPreferenceRegistration[] = [];
|
||||
appPreferences: AppPreferenceRegistration[] = [];
|
||||
entitySettings: registries.EntitySettingRegistration[] = [];
|
||||
statusBarItems: registries.StatusBarRegistration[] = [];
|
||||
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
|
||||
@ -32,6 +34,7 @@ export class LensRendererExtension extends LensExtension {
|
||||
welcomeBanners: WelcomeBannerRegistration[] = [];
|
||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||
topBarItems: TopBarRegistration[] = [];
|
||||
additionalCategoryColumns: AdditionalCategoryColumnRegistration[] = [];
|
||||
|
||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||
const { navigate } = await import("../renderer/navigation");
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
|
||||
export * from "./page-registry";
|
||||
export * from "./page-menu-registry";
|
||||
export * from "./app-preference-registry";
|
||||
export * from "./status-bar-registry";
|
||||
export * from "./kube-object-detail-registry";
|
||||
export * from "./kube-object-menu-registry";
|
||||
|
||||
@ -31,6 +31,11 @@ export * from "../../renderer/components/input/input";
|
||||
// command-overlay
|
||||
export const CommandOverlay = asLegacyGlobalObjectForExtensionApi(commandOverlayInjectable);
|
||||
|
||||
export type {
|
||||
CategoryColumnRegistration,
|
||||
AdditionalCategoryColumnRegistration,
|
||||
} from "../../renderer/components/+catalog/custom-category-columns";
|
||||
|
||||
// other components
|
||||
export * from "../../renderer/components/icon";
|
||||
export * from "../../renderer/components/tooltip";
|
||||
|
||||
@ -8,10 +8,8 @@ import extensionsInjectable from "./extensions.injectable";
|
||||
import type { LensRendererExtension } from "./lens-renderer-extension";
|
||||
|
||||
const rendererExtensionsInjectable = getInjectable({
|
||||
instantiate: (di) => di.inject(extensionsInjectable) as IComputedValue<LensRendererExtension[]>,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
|
||||
instantiate: (di) =>
|
||||
di.inject(extensionsInjectable) as IComputedValue<LensRendererExtension[]>,
|
||||
});
|
||||
|
||||
export default rendererExtensionsInjectable;
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
|
||||
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityConstructor, CatalogEntityKindData } from "../../common/catalog";
|
||||
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityConstructor } from "../../common/catalog";
|
||||
import { iter } from "../../common/utils";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
@ -43,8 +43,8 @@ export class CatalogEntityRegistry {
|
||||
return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind) as T[];
|
||||
}
|
||||
|
||||
getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData & CatalogEntityConstructor<T>): T[] {
|
||||
return this.getItemsForApiKind(apiVersion, kind);
|
||||
getItemsByEntityClass<T extends CatalogEntity>(constructor: CatalogEntityConstructor<T>): T[] {
|
||||
return this.items.filter((item) => item instanceof constructor) as T[];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { MigrationDeclaration } from "../helpers";
|
||||
|
||||
/**
|
||||
* Early store format had the kubeconfig directly under context name, this moves
|
||||
* it under the kubeConfig key
|
||||
*/
|
||||
|
||||
export default {
|
||||
version: "2.0.0-beta.2",
|
||||
run(store) {
|
||||
for (const value of store) {
|
||||
const contextName = value[0];
|
||||
|
||||
// Looping all the keys gives out the store internal stuff too...
|
||||
if (contextName === "__internal__" || Object.prototype.hasOwnProperty.call(value[1], "kubeConfig")) continue;
|
||||
store.set(contextName, { kubeConfig: value[1] });
|
||||
}
|
||||
},
|
||||
} as MigrationDeclaration;
|
||||
@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { MigrationDeclaration } from "../helpers";
|
||||
|
||||
// Cleans up a store that had the state related data stored
|
||||
|
||||
export default {
|
||||
version: "2.4.1",
|
||||
run(store) {
|
||||
for (const value of store) {
|
||||
const contextName = value[0];
|
||||
|
||||
if (contextName === "__internal__") continue;
|
||||
const cluster = value[1];
|
||||
|
||||
store.set(contextName, { kubeConfig: cluster.kubeConfig, icon: cluster.icon || null, preferences: cluster.preferences || {}});
|
||||
}
|
||||
},
|
||||
} as MigrationDeclaration;
|
||||
@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Move cluster icon from root to preferences
|
||||
import type { MigrationDeclaration } from "../helpers";
|
||||
|
||||
export default {
|
||||
version: "2.6.0-beta.2",
|
||||
run(store) {
|
||||
for (const value of store) {
|
||||
const clusterKey = value[0];
|
||||
|
||||
if (clusterKey === "__internal__") continue;
|
||||
const cluster = value[1];
|
||||
|
||||
if (!cluster.preferences) cluster.preferences = {};
|
||||
|
||||
if (cluster.icon) {
|
||||
cluster.preferences.icon = cluster.icon;
|
||||
delete (cluster["icon"]);
|
||||
}
|
||||
store.set(clusterKey, { contextName: clusterKey, kubeConfig: value[1].kubeConfig, preferences: value[1].preferences });
|
||||
}
|
||||
},
|
||||
} as MigrationDeclaration;
|
||||
@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import { MigrationDeclaration, migrationLog } from "../helpers";
|
||||
|
||||
export default {
|
||||
version: "2.6.0-beta.3",
|
||||
run(store) {
|
||||
for (const value of store) {
|
||||
const clusterKey = value[0];
|
||||
|
||||
if (clusterKey === "__internal__") continue;
|
||||
const cluster = value[1];
|
||||
|
||||
if (!cluster.kubeConfig) continue;
|
||||
const config = yaml.load(cluster.kubeConfig);
|
||||
|
||||
if (!config || typeof config !== "object" || !Object.prototype.hasOwnProperty.call(config, "users")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const kubeConfig = config as Record<string, any>;
|
||||
const userObj = kubeConfig.users[0];
|
||||
|
||||
if (userObj) {
|
||||
const user = userObj.user;
|
||||
|
||||
if (user["auth-provider"] && user["auth-provider"].config) {
|
||||
const authConfig = user["auth-provider"].config;
|
||||
|
||||
if (authConfig["access-token"]) {
|
||||
authConfig["access-token"] = `${authConfig["access-token"]}`;
|
||||
}
|
||||
|
||||
if (authConfig.expiry) {
|
||||
authConfig.expiry = `${authConfig.expiry}`;
|
||||
}
|
||||
migrationLog(authConfig);
|
||||
user["auth-provider"].config = authConfig;
|
||||
kubeConfig.users = [{
|
||||
name: userObj.name,
|
||||
user,
|
||||
}];
|
||||
cluster.kubeConfig = yaml.dump(kubeConfig);
|
||||
store.set(clusterKey, cluster);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
} as MigrationDeclaration;
|
||||
@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Add existing clusters to "default" workspace
|
||||
import type { MigrationDeclaration } from "../helpers";
|
||||
|
||||
export default {
|
||||
version: "2.7.0-beta.0",
|
||||
run(store) {
|
||||
for (const value of store) {
|
||||
const clusterKey = value[0];
|
||||
|
||||
if (clusterKey === "__internal__") continue;
|
||||
const cluster = value[1];
|
||||
|
||||
cluster.workspace = "default";
|
||||
store.set(clusterKey, cluster);
|
||||
}
|
||||
},
|
||||
} as MigrationDeclaration;
|
||||
@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Add id for clusters and store them to array
|
||||
import { v4 as uuid } from "uuid";
|
||||
import type { MigrationDeclaration } from "../helpers";
|
||||
|
||||
export default {
|
||||
version: "2.7.0-beta.1",
|
||||
run(store) {
|
||||
const clusters: any[] = [];
|
||||
|
||||
for (const value of store) {
|
||||
const clusterKey = value[0];
|
||||
|
||||
if (clusterKey === "__internal__") continue;
|
||||
if (clusterKey === "clusters") continue;
|
||||
const cluster = value[1];
|
||||
|
||||
cluster.id = uuid();
|
||||
|
||||
if (!cluster.preferences.clusterName) {
|
||||
cluster.preferences.clusterName = clusterKey;
|
||||
}
|
||||
clusters.push(cluster);
|
||||
store.delete(clusterKey);
|
||||
}
|
||||
|
||||
if (clusters.length > 0) {
|
||||
store.set("clusters", clusters);
|
||||
}
|
||||
},
|
||||
} as MigrationDeclaration;
|
||||
@ -7,24 +7,12 @@
|
||||
|
||||
import { joinMigrations } from "../helpers";
|
||||
|
||||
import version200Beta2 from "./2.0.0-beta.2";
|
||||
import version241 from "./2.4.1";
|
||||
import version260Beta2 from "./2.6.0-beta.2";
|
||||
import version260Beta3 from "./2.6.0-beta.3";
|
||||
import version270Beta0 from "./2.7.0-beta.0";
|
||||
import version270Beta1 from "./2.7.0-beta.1";
|
||||
import version360Beta1 from "./3.6.0-beta.1";
|
||||
import version500Beta10 from "./5.0.0-beta.10";
|
||||
import version500Beta13 from "./5.0.0-beta.13";
|
||||
import snap from "./snap";
|
||||
|
||||
export default joinMigrations(
|
||||
version200Beta2,
|
||||
version241,
|
||||
version260Beta2,
|
||||
version260Beta3,
|
||||
version270Beta0,
|
||||
version270Beta1,
|
||||
version360Beta1,
|
||||
version500Beta10,
|
||||
version500Beta13,
|
||||
|
||||
@ -47,10 +47,25 @@ export class CatalogEntityRegistry {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
get activeEntity(): CatalogEntity | null {
|
||||
protected getActiveEntityById() {
|
||||
return this._entities.get(this.activeEntityId) || null;
|
||||
}
|
||||
|
||||
get activeEntity(): CatalogEntity | null {
|
||||
const entity = this.getActiveEntityById();
|
||||
|
||||
// If the entity was not found but there are rawEntities to be processed,
|
||||
// try to process them and return the entity.
|
||||
// This might happen if an extension registered a new Catalog category.
|
||||
if (this.activeEntityId && !entity && this.rawEntities.length > 0) {
|
||||
this.processRawEntities();
|
||||
|
||||
return this.getActiveEntityById();
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
set activeEntity(raw: CatalogEntity | string | null) {
|
||||
if (raw) {
|
||||
const id = typeof raw === "string"
|
||||
|
||||
@ -98,9 +98,6 @@ export async function bootstrap(di: DependencyInjectionContainer) {
|
||||
logger.info(`${logPrefix} initializing IpcRendererListeners`);
|
||||
initializers.initIpcRendererListeners(extensionLoader);
|
||||
|
||||
logger.info(`${logPrefix} initializing StatusBarRegistry`);
|
||||
initializers.initStatusBarRegistry();
|
||||
|
||||
extensionLoader.init();
|
||||
|
||||
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import type { CatalogCategorySpec } from "../../../../common/catalog";
|
||||
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||
import { CatalogCategory } from "../../../api/catalog-entity";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import type { AdditionalCategoryColumnRegistration, CategoryColumnRegistration } from "../custom-category-columns";
|
||||
import getCategoryColumnsInjectable, { CategoryColumns, GetCategoryColumnsParams } from "../get-category-columns.injectable";
|
||||
|
||||
class TestCategory extends CatalogCategory {
|
||||
apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
kind = "CatalogCategory";
|
||||
metadata: {
|
||||
name: "Test";
|
||||
icon: "question_mark";
|
||||
};
|
||||
spec: CatalogCategorySpec = {
|
||||
group: "foo.bar.bat",
|
||||
names: {
|
||||
kind: "Test",
|
||||
},
|
||||
versions: [],
|
||||
};
|
||||
|
||||
constructor(columns?: CategoryColumnRegistration[]) {
|
||||
super();
|
||||
this.spec.displayColumns = columns;
|
||||
}
|
||||
}
|
||||
|
||||
describe("Custom Category Columns", () => {
|
||||
let di: ConfigurableDependencyInjectionContainer;
|
||||
|
||||
beforeEach(() => {
|
||||
di = getDiForUnitTesting();
|
||||
});
|
||||
|
||||
describe("without extensions", () => {
|
||||
let getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
|
||||
|
||||
beforeEach(() => {
|
||||
di.override(rendererExtensionsInjectable, () => computed(() => [] as LensRendererExtension[]));
|
||||
getCategoryColumns = di.inject(getCategoryColumnsInjectable);
|
||||
});
|
||||
|
||||
it("should contain a kind column if activeCategory is falsy", () => {
|
||||
expect(getCategoryColumns({ activeCategory: null }).renderTableHeader.find(elem => elem.title === "Kind")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not contain a kind column if activeCategory is truthy", () => {
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem.title === "Kind")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should include the default columns if the provided category doesn't provide any", () => {
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem.title === "Source")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not include the default columns if the provided category provides any", () => {
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory([]) }).renderTableHeader.find(elem => elem.title === "Source")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should include the displayColumns from the provided category", () => {
|
||||
const columns: CategoryColumnRegistration[] = [
|
||||
{
|
||||
id: "foo",
|
||||
renderCell: () => null,
|
||||
titleProps: {
|
||||
title: "Foo",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory(columns) }).renderTableHeader.find(elem => elem.title === "Foo")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("with extensions", () => {
|
||||
let getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
|
||||
|
||||
beforeEach(() => {
|
||||
di.override(rendererExtensionsInjectable, () => computed(() => [
|
||||
{
|
||||
name: "test-extension",
|
||||
additionalCategoryColumns: [
|
||||
{
|
||||
group: "foo.bar.bat",
|
||||
id: "high",
|
||||
kind: "Test",
|
||||
renderCell: () => "",
|
||||
titleProps: {
|
||||
title: "High",
|
||||
},
|
||||
} as AdditionalCategoryColumnRegistration,
|
||||
{
|
||||
group: "foo.bar",
|
||||
id: "high",
|
||||
kind: "Test",
|
||||
renderCell: () => "",
|
||||
titleProps: {
|
||||
title: "High2",
|
||||
},
|
||||
} as AdditionalCategoryColumnRegistration,
|
||||
],
|
||||
} as LensRendererExtension,
|
||||
]));
|
||||
getCategoryColumns = di.inject(getCategoryColumnsInjectable);
|
||||
});
|
||||
|
||||
it("should include columns from extensions that match", () => {
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem.title === "High")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not include columns from extensions that don't match", () => {
|
||||
expect(getCategoryColumns({ activeCategory: new TestCategory() }).renderTableHeader.find(elem => elem.title === "High2")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -28,25 +28,18 @@ import { RenderDelay } from "../render-delay/render-delay";
|
||||
import { Icon } from "../icon";
|
||||
import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
|
||||
import { Avatar } from "../avatar";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { getLabelBadges } from "./helpers";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import catalogPreviousActiveTabStorageInjectable
|
||||
from "./catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
|
||||
import catalogPreviousActiveTabStorageInjectable from "./catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
|
||||
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
kind = "kind",
|
||||
source = "source",
|
||||
status = "status",
|
||||
}
|
||||
import type { GetCategoryColumnsParams, CategoryColumns } from "./get-category-columns.injectable";
|
||||
import getCategoryColumnsInjectable from "./get-category-columns.injectable";
|
||||
|
||||
interface Props extends RouteComponentProps<CatalogViewRouteParam> {}
|
||||
|
||||
interface Dependencies {
|
||||
catalogPreviousActiveTabStorage: { set: (value: string ) => void }
|
||||
catalogEntityStore: CatalogEntityStore
|
||||
catalogPreviousActiveTabStorage: { set: (value: string ) => void };
|
||||
catalogEntityStore: CatalogEntityStore;
|
||||
getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -228,46 +221,23 @@ class NonInjectedCatalog extends React.Component<Props & Dependencies> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { sortingCallbacks, searchFilters, renderTableContents, renderTableHeader } = this.props.getCategoryColumns({ activeCategory });
|
||||
|
||||
return (
|
||||
<ItemListLayout
|
||||
className={styles.Catalog}
|
||||
tableId={tableId}
|
||||
renderHeaderTitle={activeCategory?.metadata.name || "Browse All"}
|
||||
renderHeaderTitle={activeCategory?.metadata.name ?? "Browse All"}
|
||||
isSelectable={false}
|
||||
isConfigurable={true}
|
||||
store={this.props.catalogEntityStore}
|
||||
sortingCallbacks={{
|
||||
[sortBy.name]: entity => entity.getName(),
|
||||
[sortBy.source]: entity => entity.getSource(),
|
||||
[sortBy.status]: entity => entity.status.phase,
|
||||
[sortBy.kind]: entity => entity.kind,
|
||||
}}
|
||||
searchFilters={[
|
||||
entity => [
|
||||
entity.getName(),
|
||||
entity.getId(),
|
||||
entity.status.phase,
|
||||
`source=${entity.getSource()}`,
|
||||
...KubeObject.stringifyLabels(entity.metadata.labels),
|
||||
],
|
||||
]}
|
||||
renderTableHeader={[
|
||||
{ title: "Name", className: styles.entityName, sortBy: sortBy.name, id: "name" },
|
||||
!activeCategory && { title: "Kind", sortBy: sortBy.kind, id: "kind" },
|
||||
{ title: "Source", className: styles.sourceCell, sortBy: sortBy.source, id: "source" },
|
||||
{ title: "Labels", className: `${styles.labelsCell} scrollable`, id: "labels" },
|
||||
{ title: "Status", className: styles.statusCell, sortBy: sortBy.status, id: "status" },
|
||||
].filter(Boolean)}
|
||||
sortingCallbacks={sortingCallbacks}
|
||||
searchFilters={searchFilters}
|
||||
renderTableHeader={renderTableHeader}
|
||||
customizeTableRowProps={entity => ({
|
||||
disabled: !entity.isEnabled(),
|
||||
})}
|
||||
renderTableContents={entity => [
|
||||
this.renderName(entity),
|
||||
!activeCategory && entity.kind,
|
||||
entity.getSource(),
|
||||
getLabelBadges(entity),
|
||||
<span key="phase" className={entity.status.phase}>{entity.status.phase}</span>,
|
||||
].filter(Boolean)}
|
||||
renderTableContents={renderTableContents}
|
||||
onDetails={this.onDetails}
|
||||
renderItemMenu={this.renderItemMenu}
|
||||
/>
|
||||
@ -306,17 +276,11 @@ class NonInjectedCatalog extends React.Component<Props & Dependencies> {
|
||||
}
|
||||
}
|
||||
|
||||
export const Catalog = withInjectables<Dependencies, Props>(
|
||||
NonInjectedCatalog,
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
catalogEntityStore: di.inject(catalogEntityStoreInjectable),
|
||||
|
||||
catalogPreviousActiveTabStorage: di.inject(
|
||||
catalogPreviousActiveTabStorageInjectable,
|
||||
),
|
||||
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
export const Catalog = withInjectables<Dependencies, Props>( NonInjectedCatalog, {
|
||||
getProps: (di, props) => ({
|
||||
catalogEntityStore: di.inject(catalogEntityStoreInjectable),
|
||||
catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable),
|
||||
getCategoryColumns: di.inject(getCategoryColumnsInjectable),
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { computed, IComputedValue } from "mobx";
|
||||
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
|
||||
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
|
||||
import { getOrInsert } from "../../utils";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||
|
||||
interface Dependencies {
|
||||
extensions: IComputedValue<LensRendererExtension[]>;
|
||||
}
|
||||
|
||||
function getAdditionCategoryColumns({ extensions }: Dependencies): IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>> {
|
||||
return computed(() => {
|
||||
const res = new Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>();
|
||||
|
||||
for (const ext of extensions.get()) {
|
||||
for (const { renderCell, titleProps, priority = 50, searchFilter, sortCallback, ...registration } of ext.additionalCategoryColumns) {
|
||||
const byGroup = getOrInsert(res, registration.group, new Map<string, RegisteredAdditionalCategoryColumn[]>());
|
||||
const byKind = getOrInsert(byGroup, registration.kind, []);
|
||||
const id = `${ext.name}:${registration.id}`;
|
||||
|
||||
byKind.push({
|
||||
renderCell,
|
||||
priority,
|
||||
id,
|
||||
titleProps: {
|
||||
id,
|
||||
...titleProps,
|
||||
sortBy: sortCallback
|
||||
? id
|
||||
: undefined,
|
||||
},
|
||||
searchFilter,
|
||||
sortCallback,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
const categoryColumnsInjectable = getInjectable({
|
||||
instantiate: (di) => getAdditionCategoryColumns({
|
||||
extensions: di.inject(rendererExtensionsInjectable),
|
||||
}),
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default categoryColumnsInjectable;
|
||||
85
src/renderer/components/+catalog/custom-category-columns.ts
Normal file
85
src/renderer/components/+catalog/custom-category-columns.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
import type { TableCellProps } from "../table";
|
||||
|
||||
/**
|
||||
* These are the supported props for the title cell
|
||||
*/
|
||||
export interface TitleCellProps {
|
||||
className?: string;
|
||||
title: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface CategoryColumnRegistration {
|
||||
/**
|
||||
* The sorting order value.
|
||||
*
|
||||
* @default 50
|
||||
*/
|
||||
priority?: number;
|
||||
|
||||
/**
|
||||
* This value MUST to be unique to your extension
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* This function will be called to generate the cells (on demand) for the column
|
||||
*/
|
||||
renderCell: (entity: CatalogEntity) => React.ReactNode;
|
||||
|
||||
/**
|
||||
* This function will be used to generate the columns title cell.
|
||||
*/
|
||||
titleProps: TitleCellProps;
|
||||
|
||||
/**
|
||||
* If provided then the column will support sorting and this function will be called to
|
||||
* determine a row's ordering.
|
||||
*
|
||||
* strings are sorted ahead of numbers, and arrays determine ordering between equal
|
||||
* elements of the previous index.
|
||||
*/
|
||||
sortCallback?: (entity: CatalogEntity) => string | number | (string | number)[];
|
||||
|
||||
/**
|
||||
* If provided then searching is supported on this column and this function will be called
|
||||
* to determine if the current search string matches for this row.
|
||||
*/
|
||||
searchFilter?: (entity: CatalogEntity) => string | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the type used to declare new catalog category columns
|
||||
*/
|
||||
export interface AdditionalCategoryColumnRegistration extends CategoryColumnRegistration {
|
||||
/**
|
||||
* The catalog entity kind that is declared by the category for this registration
|
||||
*
|
||||
* e.g.
|
||||
* - `"KubernetesCluster"`
|
||||
*/
|
||||
kind: string;
|
||||
|
||||
/**
|
||||
* The catalog entity group that is declared by the category for this registration
|
||||
*
|
||||
* e.g.
|
||||
* - `"entity.k8slens.dev"`
|
||||
*/
|
||||
group: string;
|
||||
}
|
||||
|
||||
export interface RegisteredAdditionalCategoryColumn {
|
||||
id: string;
|
||||
priority: number;
|
||||
renderCell: (entity: CatalogEntity) => React.ReactNode;
|
||||
titleProps: TableCellProps;
|
||||
sortCallback?: (entity: CatalogEntity) => string | number | (string | number)[];
|
||||
searchFilter?: (entity: CatalogEntity) => string | string[];
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { orderBy } from "lodash";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
|
||||
import { bind } from "../../utils";
|
||||
import type { ItemListLayoutProps } from "../item-object-list";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||
import categoryColumnsInjectable from "./custom-category-columns.injectable";
|
||||
import { defaultCategoryColumns, browseAllColumns, nameCategoryColumn } from "./internal-category-columns";
|
||||
|
||||
interface Dependencies {
|
||||
extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>;
|
||||
}
|
||||
|
||||
export interface GetCategoryColumnsParams {
|
||||
activeCategory: CatalogCategory | null | undefined;
|
||||
}
|
||||
|
||||
export type CategoryColumns = Required<Pick<ItemListLayoutProps<CatalogEntity>, "sortingCallbacks" | "searchFilters" | "renderTableContents" | "renderTableHeader">>;
|
||||
|
||||
function getSpecificCategoryColumns(activeCategory: CatalogCategory, extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>): RegisteredAdditionalCategoryColumn[] {
|
||||
const fromExtensions = (
|
||||
extensionColumns
|
||||
.get()
|
||||
.get(activeCategory.spec.group)
|
||||
?.get(activeCategory.spec.names.kind)
|
||||
?? []
|
||||
);
|
||||
const fromCategory = activeCategory.spec.displayColumns?.map(({ priority = 50, ...column }) => ({
|
||||
priority,
|
||||
...column,
|
||||
})) ?? defaultCategoryColumns;
|
||||
|
||||
return [
|
||||
nameCategoryColumn,
|
||||
...fromExtensions,
|
||||
...fromCategory,
|
||||
];
|
||||
}
|
||||
|
||||
function getBrowseAllColumns(): RegisteredAdditionalCategoryColumn[] {
|
||||
return [
|
||||
...browseAllColumns,
|
||||
nameCategoryColumn,
|
||||
...defaultCategoryColumns,
|
||||
];
|
||||
}
|
||||
|
||||
function getCategoryColumns({ extensionColumns }: Dependencies, { activeCategory }: GetCategoryColumnsParams): CategoryColumns {
|
||||
const allRegistrations = orderBy(
|
||||
activeCategory
|
||||
? getSpecificCategoryColumns(activeCategory, extensionColumns)
|
||||
: getBrowseAllColumns(),
|
||||
"priority",
|
||||
"asc",
|
||||
);
|
||||
|
||||
const sortingCallbacks: CategoryColumns["sortingCallbacks"] = {};
|
||||
const searchFilters: CategoryColumns["searchFilters"] = [];
|
||||
const renderTableHeader: CategoryColumns["renderTableHeader"] = [];
|
||||
const tableRowRenderers: ((entity: CatalogEntity) => React.ReactNode)[] = [];
|
||||
|
||||
for (const registration of allRegistrations) {
|
||||
if (registration.sortCallback) {
|
||||
sortingCallbacks[registration.id] = registration.sortCallback;
|
||||
}
|
||||
|
||||
if (registration.searchFilter) {
|
||||
searchFilters.push(registration.searchFilter);
|
||||
}
|
||||
|
||||
tableRowRenderers.push(registration.renderCell);
|
||||
renderTableHeader.push(registration.titleProps);
|
||||
}
|
||||
|
||||
return {
|
||||
sortingCallbacks,
|
||||
renderTableHeader,
|
||||
renderTableContents: entity => tableRowRenderers.map(fn => fn(entity)),
|
||||
searchFilters,
|
||||
};
|
||||
}
|
||||
|
||||
const getCategoryColumnsInjectable = getInjectable({
|
||||
instantiate: (di) => bind(getCategoryColumns, null, {
|
||||
extensionColumns: di.inject(categoryColumnsInjectable),
|
||||
}),
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default getCategoryColumnsInjectable;
|
||||
121
src/renderer/components/+catalog/internal-category-columns.tsx
Normal file
121
src/renderer/components/+catalog/internal-category-columns.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import styles from "./catalog.module.scss";
|
||||
|
||||
import React from "react";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { Avatar } from "../avatar";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||
import { Icon } from "../icon";
|
||||
import { prevDefault } from "../../utils";
|
||||
import { getLabelBadges } from "./helpers";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
|
||||
function renderEntityName(entity: CatalogEntity) {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
const isItemInHotbar = hotbarStore.isAddedToActive(entity);
|
||||
const onClick = prevDefault(
|
||||
isItemInHotbar
|
||||
? () => hotbarStore.removeFromHotbar(entity.getId())
|
||||
: () => hotbarStore.addToHotbar(entity),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Avatar
|
||||
title={entity.getName()}
|
||||
colorHash={`${entity.getName()}-${entity.getSource()}`}
|
||||
src={entity.spec.icon?.src}
|
||||
background={entity.spec.icon?.background}
|
||||
className={styles.catalogAvatar}
|
||||
size={24}
|
||||
>
|
||||
{entity.spec.icon?.material && <Icon material={entity.spec.icon?.material} small/>}
|
||||
</Avatar>
|
||||
<span>{entity.getName()}</span>
|
||||
<Icon
|
||||
small
|
||||
className={styles.pinIcon}
|
||||
material={!isItemInHotbar && "push_pin"}
|
||||
svg={isItemInHotbar ? "push_off" : "push_pin"}
|
||||
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const browseAllColumns: RegisteredAdditionalCategoryColumn[] = [
|
||||
{
|
||||
id: "kind",
|
||||
priority: 5,
|
||||
renderCell: entity => entity.kind,
|
||||
titleProps: {
|
||||
id: "kind",
|
||||
sortBy: "kind",
|
||||
title: "Kind",
|
||||
},
|
||||
sortCallback: entity => entity.kind,
|
||||
},
|
||||
];
|
||||
|
||||
export const nameCategoryColumn: RegisteredAdditionalCategoryColumn = {
|
||||
id: "name",
|
||||
priority: 0,
|
||||
renderCell: renderEntityName,
|
||||
titleProps: {
|
||||
title: "Name",
|
||||
className: styles.entityName,
|
||||
id: "name",
|
||||
sortBy: "name",
|
||||
},
|
||||
searchFilter: entity => entity.getName(),
|
||||
sortCallback: entity => `name=${entity.getName()}`,
|
||||
};
|
||||
|
||||
export const defaultCategoryColumns: RegisteredAdditionalCategoryColumn[] = [
|
||||
{
|
||||
id: "source",
|
||||
priority: 10,
|
||||
renderCell: entity => entity.getSource(),
|
||||
titleProps: {
|
||||
title: "Source",
|
||||
className: styles.sourceCell,
|
||||
id: "source",
|
||||
sortBy: "source",
|
||||
},
|
||||
sortCallback: entity => entity.getSource(),
|
||||
searchFilter: entity => `source=${entity.getSource()}`,
|
||||
},
|
||||
{
|
||||
id: "labels",
|
||||
priority: 20,
|
||||
renderCell: getLabelBadges,
|
||||
titleProps: {
|
||||
id: "labels",
|
||||
title: "Labels",
|
||||
className: `${styles.labelsCell} scrollable`,
|
||||
},
|
||||
searchFilter: entity => KubeObject.stringifyLabels(entity.metadata.labels),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
priority: 30,
|
||||
renderCell: entity => (
|
||||
<span key="phase" className={entity.status.phase}>
|
||||
{entity.status.phase}
|
||||
</span>
|
||||
),
|
||||
titleProps: {
|
||||
title: "Status",
|
||||
className: styles.statusCell,
|
||||
id: "status",
|
||||
sortBy: "status",
|
||||
},
|
||||
searchFilter: entity => entity.status.phase,
|
||||
},
|
||||
];
|
||||
@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
export interface AppPreferenceComponents {
|
||||
Hint: React.ComponentType<any>;
|
||||
@ -22,11 +21,3 @@ export interface RegisteredAppPreference extends AppPreferenceRegistration {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class AppPreferenceRegistry extends BaseRegistry<AppPreferenceRegistration, RegisteredAppPreference> {
|
||||
getRegisteredItem(item: AppPreferenceRegistration): RegisteredAppPreference {
|
||||
return {
|
||||
id: item.id || item.title.toLowerCase().replace(/[^0-9a-zA-Z]+/g, "-"),
|
||||
...item,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||
import { getAppPreferences } from "./get-app-preferences";
|
||||
|
||||
const appPreferencesInjectable = getInjectable({
|
||||
instantiate: (di) =>
|
||||
getAppPreferences({
|
||||
extensions: di.inject(rendererExtensionsInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default appPreferencesInjectable;
|
||||
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { computed, IComputedValue } from "mobx";
|
||||
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||
import type { AppPreferenceRegistration, RegisteredAppPreference } from "./app-preference-registration";
|
||||
|
||||
interface Dependencies {
|
||||
extensions: IComputedValue<LensRendererExtension[]>;
|
||||
}
|
||||
|
||||
function getRegisteredItem(item: AppPreferenceRegistration): RegisteredAppPreference {
|
||||
return {
|
||||
id: item.id || item.title.toLowerCase().replace(/[^0-9a-zA-Z]+/g, "-"),
|
||||
...item,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const getAppPreferences = ({ extensions }: Dependencies) => {
|
||||
return computed(() => (
|
||||
extensions.get()
|
||||
.flatMap((extension) => extension.appPreferences)
|
||||
.map(getRegisteredItem)
|
||||
));
|
||||
};
|
||||
@ -14,10 +14,12 @@ import { isWindows } from "../../../common/vars";
|
||||
import { Switch } from "../switch";
|
||||
import moment from "moment-timezone";
|
||||
import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
|
||||
import { action } from "mobx";
|
||||
import { action, IComputedValue } from "mobx";
|
||||
import { isUrl } from "../input/input_validators";
|
||||
import { AppPreferenceRegistry } from "../../../extensions/registries";
|
||||
import { ExtensionSettings } from "./extension-settings";
|
||||
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import appPreferencesInjectable from "./app-preferences/app-preferences.injectable";
|
||||
|
||||
const timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({
|
||||
label: zone,
|
||||
@ -28,7 +30,11 @@ const updateChannelOptions: SelectOption<string>[] = Array.from(
|
||||
([value, { label }]) => ({ value, label }),
|
||||
);
|
||||
|
||||
export const Application = observer(() => {
|
||||
interface Dependencies {
|
||||
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>
|
||||
}
|
||||
|
||||
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems }) => {
|
||||
const userStore = UserStore.getInstance();
|
||||
const defaultShell = process.env.SHELL
|
||||
|| process.env.PTYSHELL
|
||||
@ -40,25 +46,39 @@ export const Application = observer(() => {
|
||||
|
||||
const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
|
||||
const [shell, setShell] = React.useState(userStore.shell || "");
|
||||
const extensionSettings = AppPreferenceRegistry.getInstance().getItems().filter((preference) => preference.showInPreferencesTab === "application");
|
||||
const extensionSettings = appPreferenceItems.get().filter((preference) => preference.showInPreferencesTab === "application");
|
||||
const themeStore = ThemeStore.getInstance();
|
||||
|
||||
return (
|
||||
<section id="application">
|
||||
<h2 data-testid="application-header">Application</h2>
|
||||
<section id="appearance">
|
||||
<SubTitle title="Theme"/>
|
||||
<SubTitle title="Theme" />
|
||||
<Select
|
||||
options={ThemeStore.getInstance().themeOptions}
|
||||
options={themeStore.themeOptions}
|
||||
value={userStore.colorTheme}
|
||||
onChange={({ value }) => userStore.colorTheme = value}
|
||||
themeName="lens"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
<hr />
|
||||
|
||||
<section id="terminalTheme">
|
||||
<SubTitle title="Terminal theme" />
|
||||
<Select
|
||||
themeName="lens"
|
||||
options={[
|
||||
{ label: "Match theme", value: "" },
|
||||
...themeStore.themeOptions,
|
||||
]}
|
||||
value={userStore.terminalTheme}
|
||||
onChange={({ value }) => userStore.terminalTheme = value}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section id="shell">
|
||||
<SubTitle title="Terminal Shell Path"/>
|
||||
<SubTitle title="Terminal Shell Path" />
|
||||
<Input
|
||||
theme="round-black"
|
||||
placeholder={defaultShell}
|
||||
@ -78,7 +98,7 @@ export const Application = observer(() => {
|
||||
</Switch>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
<hr />
|
||||
|
||||
<section id="extensionRegistryUrl">
|
||||
<SubTitle title="Extension Install Registry" />
|
||||
@ -111,10 +131,10 @@ export const Application = observer(() => {
|
||||
/>
|
||||
</section>
|
||||
|
||||
<hr/>
|
||||
<hr />
|
||||
|
||||
<section id="other">
|
||||
<SubTitle title="Start-up"/>
|
||||
<SubTitle title="Start-up" />
|
||||
<Switch checked={userStore.openAtLogin} onChange={() => userStore.openAtLogin = !userStore.openAtLogin}>
|
||||
Automatically start Lens on login
|
||||
</Switch>
|
||||
@ -127,7 +147,7 @@ export const Application = observer(() => {
|
||||
))}
|
||||
|
||||
<section id="update-channel">
|
||||
<SubTitle title="Update Channel"/>
|
||||
<SubTitle title="Update Channel" />
|
||||
<Select
|
||||
options={updateChannelOptions}
|
||||
value={userStore.updateChannel}
|
||||
@ -149,4 +169,14 @@ export const Application = observer(() => {
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const Application = withInjectables<Dependencies>(
|
||||
observer(NonInjectedApplication),
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
appPreferenceItems: di.inject(appPreferencesInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -3,12 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import type { RegisteredAppPreference } from "../../../extensions/registries/app-preference-registry";
|
||||
import type { AppPreferenceRegistration } from "./app-preferences/app-preference-registration";
|
||||
import React from "react";
|
||||
import { cssNames } from "../../../renderer/utils";
|
||||
|
||||
interface ExtensionSettingsProps {
|
||||
setting: RegisteredAppPreference;
|
||||
setting: AppPreferenceRegistration;
|
||||
size: "small" | "normal"
|
||||
}
|
||||
|
||||
|
||||
@ -3,13 +3,21 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { AppPreferenceRegistry } from "../../../extensions/registries";
|
||||
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
|
||||
import appPreferencesInjectable from "./app-preferences/app-preferences.injectable";
|
||||
import { ExtensionSettings } from "./extension-settings";
|
||||
|
||||
export const Extensions = observer(() => {
|
||||
const settings = AppPreferenceRegistry.getInstance().getItems();
|
||||
interface Dependencies {
|
||||
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>
|
||||
}
|
||||
|
||||
const NonInjectedExtensions: React.FC<Dependencies> = ({ appPreferenceItems }) => {
|
||||
|
||||
const settings = appPreferenceItems.get();
|
||||
|
||||
return (
|
||||
<section id="extensions">
|
||||
@ -19,4 +27,14 @@ export const Extensions = observer(() => {
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const Extensions = withInjectables<Dependencies>(
|
||||
observer(NonInjectedExtensions),
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
appPreferenceItems: di.inject(appPreferencesInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
import "./preferences.scss";
|
||||
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { matchPath, Redirect, Route, RouteProps, Switch } from "react-router";
|
||||
@ -23,7 +23,6 @@ import {
|
||||
telemetryRoute,
|
||||
telemetryURL,
|
||||
} from "../../../common/routes";
|
||||
import { AppPreferenceRegistry } from "../../../extensions/registries/app-preference-registry";
|
||||
import { navigateWithoutHistoryChange, navigation } from "../../navigation";
|
||||
import { SettingLayout } from "../layout/setting-layout";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
@ -34,18 +33,18 @@ import { LensProxy } from "./proxy";
|
||||
import { Telemetry } from "./telemetry";
|
||||
import { Extensions } from "./extensions";
|
||||
import { sentryDsn } from "../../../common/vars";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
|
||||
import appPreferencesInjectable from "./app-preferences/app-preferences.injectable";
|
||||
|
||||
@observer
|
||||
export class Preferences extends React.Component {
|
||||
@observable historyLength: number | undefined;
|
||||
interface Dependencies {
|
||||
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>
|
||||
}
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
const NonInjectedPreferences: React.FC<Dependencies> = ({ appPreferenceItems }) => {
|
||||
|
||||
renderNavigation() {
|
||||
const extensions = AppPreferenceRegistry.getInstance().getItems();
|
||||
function renderNavigation() {
|
||||
const extensions = appPreferenceItems.get();
|
||||
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == "telemetry");
|
||||
const currentLocation = navigation.location.pathname;
|
||||
const isActive = (route: RouteProps) => !!matchPath(currentLocation, { path: route.path, exact: route.exact });
|
||||
@ -67,23 +66,31 @@ export class Preferences extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SettingLayout
|
||||
navigation={this.renderNavigation()}
|
||||
className="Preferences"
|
||||
contentGaps={false}
|
||||
>
|
||||
<Switch>
|
||||
<Route path={appURL()} component={Application}/>
|
||||
<Route path={proxyURL()} component={LensProxy}/>
|
||||
<Route path={kubernetesURL()} component={Kubernetes}/>
|
||||
<Route path={editorURL()} component={Editor}/>
|
||||
<Route path={telemetryURL()} component={Telemetry}/>
|
||||
<Route path={extensionURL()} component={Extensions}/>
|
||||
<Redirect exact from={`${preferencesURL()}/`} to={appURL()}/>
|
||||
</Switch>
|
||||
</SettingLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<SettingLayout
|
||||
navigation={renderNavigation()}
|
||||
className="Preferences"
|
||||
contentGaps={false}
|
||||
>
|
||||
<Switch>
|
||||
<Route path={appURL()} component={Application}/>
|
||||
<Route path={proxyURL()} component={LensProxy}/>
|
||||
<Route path={kubernetesURL()} component={Kubernetes}/>
|
||||
<Route path={editorURL()} component={Editor}/>
|
||||
<Route path={telemetryURL()} component={Telemetry}/>
|
||||
<Route path={extensionURL()} component={Extensions}/>
|
||||
<Redirect exact from={`${preferencesURL()}/`} to={appURL()}/>
|
||||
</Switch>
|
||||
</SettingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const Preferences = withInjectables<Dependencies>(
|
||||
observer(NonInjectedPreferences),
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
appPreferenceItems: di.inject(appPreferencesInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -6,13 +6,20 @@ import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { sentryDsn } from "../../../common/vars";
|
||||
import { AppPreferenceRegistry } from "../../../extensions/registries";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { ExtensionSettings } from "./extension-settings";
|
||||
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
|
||||
import appPreferencesInjectable from "./app-preferences/app-preferences.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
|
||||
export const Telemetry = observer(() => {
|
||||
const extensions = AppPreferenceRegistry.getInstance().getItems();
|
||||
interface Dependencies {
|
||||
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>
|
||||
}
|
||||
|
||||
const NonInjectedTelemetry: React.FC<Dependencies> = ({ appPreferenceItems }) => {
|
||||
const extensions = appPreferenceItems.get();
|
||||
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == "telemetry");
|
||||
|
||||
return (
|
||||
@ -44,4 +51,14 @@ export const Telemetry = observer(() => {
|
||||
}
|
||||
</section>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const Telemetry = withInjectables<Dependencies>(
|
||||
observer(NonInjectedTelemetry),
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
appPreferenceItems: di.inject(appPreferencesInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Icon } from "../icon";
|
||||
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
|
||||
import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
openCommandOverlay: (component: React.ReactElement) => void;
|
||||
activeHotbarName: () => string | undefined;
|
||||
}
|
||||
|
||||
const NonInjectedActiveHotbarName = observer(({ openCommandOverlay, activeHotbarName }: Dependencies) => (
|
||||
<div
|
||||
className="flex items-center"
|
||||
data-testid="current-hotbar-name"
|
||||
onClick={() => openCommandOverlay(<HotbarSwitchCommand />)}
|
||||
>
|
||||
<Icon material="bookmarks" className="mr-2" size={14} />
|
||||
{activeHotbarName()}
|
||||
</div>
|
||||
));
|
||||
|
||||
export const ActiveHotbarName = withInjectables<Dependencies>(NonInjectedActiveHotbarName, {
|
||||
getProps: (di, props) => ({
|
||||
activeHotbarName: () => di.inject(hotbarManagerInjectable).getActive()?.name,
|
||||
openCommandOverlay: di.inject(commandOverlayInjectable).open,
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
@ -4,68 +4,24 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import mockFs from "mock-fs";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import { render } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { BottomBar } from "./bottom-bar";
|
||||
import { StatusBarRegistry } from "../../../extensions/registries";
|
||||
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
|
||||
import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command";
|
||||
import { ActiveHotbarName } from "./active-hotbar-name";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import { DiRender, renderFor } from "../test-utils/renderFor";
|
||||
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||
import { getEmptyHotbar } from "../../../common/hotbar-types";
|
||||
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
getName: () => "lens",
|
||||
setName: jest.fn(),
|
||||
setPath: jest.fn(),
|
||||
getPath: () => "tmp",
|
||||
},
|
||||
ipcMain: {
|
||||
handle: jest.fn(),
|
||||
on: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
off: jest.fn(),
|
||||
send: jest.fn(),
|
||||
getPath: () => "/foo",
|
||||
},
|
||||
}));
|
||||
|
||||
const foobarHotbar = getEmptyHotbar("foobar");
|
||||
|
||||
describe("<BottomBar />", () => {
|
||||
let di: DependencyInjectionContainer;
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockOpts = {
|
||||
"tmp": {
|
||||
"test-store.json": JSON.stringify({}),
|
||||
},
|
||||
};
|
||||
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs(mockOpts);
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
di.override(hotbarManagerInjectable, () => ({
|
||||
getActive: () => foobarHotbar,
|
||||
} as any));
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
beforeEach(() => {
|
||||
StatusBarRegistry.createInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
StatusBarRegistry.resetInstance();
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
@ -111,33 +67,6 @@ describe("<BottomBar />", () => {
|
||||
expect(getByTestId(testId)).toHaveTextContent(text);
|
||||
});
|
||||
|
||||
it("shows active hotbar name", () => {
|
||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
||||
{ item: () => <ActiveHotbarName/> },
|
||||
]);
|
||||
const { getByTestId } = render(<BottomBar />);
|
||||
|
||||
expect(getByTestId("current-hotbar-name")).toHaveTextContent("foobar");
|
||||
});
|
||||
|
||||
it("opens command palette on click", () => {
|
||||
const mockOpen = jest.fn();
|
||||
|
||||
di.override(commandOverlayInjectable, () => ({
|
||||
open: mockOpen,
|
||||
}) as any);
|
||||
|
||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
||||
{ item: () => <ActiveHotbarName/> },
|
||||
]);
|
||||
const { getByTestId } = render(<BottomBar />);
|
||||
const activeHotbar = getByTestId("current-hotbar-name");
|
||||
|
||||
fireEvent.click(activeHotbar);
|
||||
|
||||
|
||||
expect(mockOpen).toHaveBeenCalledWith(<HotbarSwitchCommand />);
|
||||
});
|
||||
|
||||
it("sort positioned items properly", () => {
|
||||
StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
||||
|
||||
@ -60,11 +60,14 @@
|
||||
|
||||
.tab-content {
|
||||
position: relative;
|
||||
background: var(--terminalBackground);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
transition: flex-basis 25ms ease-in;
|
||||
|
||||
&.terminal {
|
||||
background: var(--terminalBackground);
|
||||
}
|
||||
|
||||
> *:not(.Spinner) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@ -97,7 +97,7 @@ class NonInjectedDock extends React.Component<Props & Dependencies> {
|
||||
if (!isOpen || !selectedTab) return null;
|
||||
|
||||
return (
|
||||
<div className="tab-content" style={{ flexBasis: height }}>
|
||||
<div className={`tab-content ${selectedTab.kind}`} style={{ flexBasis: height }}>
|
||||
{this.renderTab(selectedTab)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -10,9 +10,9 @@ import { FitAddon } from "xterm-addon-fit";
|
||||
import type { DockStore, TabId } from "../dock-store/dock.store";
|
||||
import { TerminalApi, TerminalChannels } from "../../../api/terminal-api";
|
||||
import { ThemeStore } from "../../../theme.store";
|
||||
import { boundMethod, disposer } from "../../../utils";
|
||||
import { disposer } from "../../../utils";
|
||||
import { isMac } from "../../../../common/vars";
|
||||
import { camelCase, once } from "lodash";
|
||||
import { once } from "lodash";
|
||||
import { UserStore } from "../../../../common/user-store";
|
||||
import { clipboard } from "electron";
|
||||
import logger from "../../../../common/logger";
|
||||
@ -44,23 +44,6 @@ export class Terminal {
|
||||
private scrollPos = 0;
|
||||
private disposer = disposer();
|
||||
|
||||
@boundMethod
|
||||
protected setTheme(colors: Record<string, string>) {
|
||||
if (!this.xterm) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replacing keys stored in styles to format accepted by terminal
|
||||
// E.g. terminalBrightBlack -> brightBlack
|
||||
const colorPrefix = "terminal";
|
||||
const terminalColorEntries = Object.entries(colors)
|
||||
.filter(([name]) => name.startsWith(colorPrefix))
|
||||
.map(([name, color]) => [camelCase(name.slice(colorPrefix.length)), color]);
|
||||
const terminalColors = Object.fromEntries(terminalColorEntries);
|
||||
|
||||
this.xterm.setOption("theme", terminalColors);
|
||||
}
|
||||
|
||||
get elem() {
|
||||
return this.xterm?.element;
|
||||
}
|
||||
@ -109,7 +92,9 @@ export class Terminal {
|
||||
window.addEventListener("resize", this.onResize);
|
||||
|
||||
this.disposer.push(
|
||||
reaction(() => ThemeStore.getInstance().activeTheme.colors, this.setTheme, {
|
||||
reaction(() => ThemeStore.getInstance().xtermColors, colors => {
|
||||
this.xterm?.setOption("theme", colors);
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
dependencies.dockStore.onResize(this.onResize),
|
||||
|
||||
@ -54,6 +54,7 @@ export class EditableList<T> extends React.Component<Props<T>> {
|
||||
onSubmit={this.onSubmit}
|
||||
validators={validators}
|
||||
placeholder={placeholder}
|
||||
blurOnEnter={false}
|
||||
iconRight={({ isDirty }) => isDirty ? <Icon material="keyboard_return" size={16} /> : null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
*/
|
||||
|
||||
.HotbarSelector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
background-color: var(--layoutBackground);
|
||||
position: relative;
|
||||
@ -17,7 +19,13 @@
|
||||
top: -20px;
|
||||
}
|
||||
|
||||
.SelectorIndex {
|
||||
.HotbarIndex {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Badge {
|
||||
cursor: pointer;
|
||||
background: var(--secondaryBackground);
|
||||
width: 100%;
|
||||
@ -3,21 +3,18 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./hotbar-selector.scss";
|
||||
import React from "react";
|
||||
import styles from "./hotbar-selector.module.scss";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Icon } from "../icon";
|
||||
import { Badge } from "../badge";
|
||||
import hotbarManagerInjectable from "../../../common/hotbar-store.injectable";
|
||||
import { HotbarSwitchCommand } from "./hotbar-switch-command";
|
||||
import { TooltipPosition } from "../tooltip";
|
||||
import { Tooltip, TooltipPosition } from "../tooltip";
|
||||
import { observer } from "mobx-react";
|
||||
import type { Hotbar } from "../../../common/hotbar-types";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||
|
||||
export interface HotbarSelectorProps {
|
||||
hotbar: Hotbar;
|
||||
}
|
||||
import { cssNames } from "../../utils";
|
||||
|
||||
interface Dependencies {
|
||||
hotbarManager: {
|
||||
@ -29,25 +26,63 @@ interface Dependencies {
|
||||
openCommandOverlay: (component: React.ReactElement) => void;
|
||||
}
|
||||
|
||||
const NonInjectedHotbarSelector = observer(({ hotbar, hotbarManager, openCommandOverlay }: HotbarSelectorProps & Dependencies) => (
|
||||
<div className="HotbarSelector flex align-center">
|
||||
<Icon material="play_arrow" className="previous box" onClick={() => hotbarManager.switchToPrevious()} />
|
||||
<div className="box grow flex align-center">
|
||||
<Badge
|
||||
id="hotbarIndex"
|
||||
small
|
||||
label={hotbarManager.getDisplayIndex(hotbarManager.getActive())}
|
||||
onClick={() => openCommandOverlay(<HotbarSwitchCommand />)}
|
||||
tooltip={{
|
||||
preferredPositions: [TooltipPosition.TOP, TooltipPosition.TOP_LEFT],
|
||||
children: hotbar.name,
|
||||
}}
|
||||
className="SelectorIndex"
|
||||
export interface HotbarSelectorProps extends Partial<Dependencies> {
|
||||
hotbar: Hotbar;
|
||||
}
|
||||
|
||||
const NonInjectedHotbarSelector = observer(({ hotbar, hotbarManager, openCommandOverlay }: HotbarSelectorProps & Dependencies) => {
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
const tooltipTimeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
function clearTimer() {
|
||||
clearTimeout(tooltipTimeout.current);
|
||||
}
|
||||
|
||||
function onTooltipShow() {
|
||||
setTooltipVisible(true);
|
||||
clearTimer();
|
||||
tooltipTimeout.current = setTimeout(() => setTooltipVisible(false), 1500);
|
||||
}
|
||||
|
||||
function onArrowClick(switchTo: () => void) {
|
||||
onTooltipShow();
|
||||
switchTo();
|
||||
}
|
||||
|
||||
function onMouseEvent(event: React.MouseEvent) {
|
||||
clearTimer();
|
||||
setTooltipVisible(event.type == "mouseenter");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.HotbarSelector}>
|
||||
<Icon
|
||||
material="play_arrow"
|
||||
className={cssNames(styles.Icon, styles.previous)}
|
||||
onClick={() => onArrowClick(hotbarManager.switchToPrevious)}
|
||||
/>
|
||||
<div className={styles.HotbarIndex}>
|
||||
<Badge
|
||||
id="hotbarIndex"
|
||||
small
|
||||
label={hotbarManager.getDisplayIndex(hotbarManager.getActive())}
|
||||
onClick={() => openCommandOverlay(<HotbarSwitchCommand />)}
|
||||
className={styles.Badge}
|
||||
onMouseEnter={onMouseEvent}
|
||||
onMouseLeave={onMouseEvent}
|
||||
/>
|
||||
<Tooltip
|
||||
visible={tooltipVisible}
|
||||
targetId="hotbarIndex"
|
||||
preferredPositions={[TooltipPosition.TOP, TooltipPosition.TOP_LEFT]}
|
||||
>
|
||||
{hotbar.name}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Icon material="play_arrow" className={styles.Icon} onClick={() => onArrowClick(hotbarManager.switchToNext)} />
|
||||
</div>
|
||||
<Icon material="play_arrow" className="next box" onClick={() => hotbarManager.switchToNext()} />
|
||||
</div>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export const HotbarSelector = withInjectables<Dependencies, HotbarSelectorProps>(NonInjectedHotbarSelector, {
|
||||
getProps: (di, props) => ({
|
||||
|
||||
@ -52,6 +52,7 @@ export type InputProps = Omit<InputElementProps, "onChange" | "onSubmit"> & {
|
||||
iconRight?: IconData;
|
||||
contentRight?: string | React.ReactNode; // Any component of string goes after iconRight
|
||||
validators?: InputValidator | InputValidator[];
|
||||
blurOnEnter?: boolean;
|
||||
onChange?(value: string, evt: React.ChangeEvent<InputElement>): void;
|
||||
onSubmit?(value: string, evt: React.KeyboardEvent<InputElement>): void;
|
||||
};
|
||||
@ -70,6 +71,7 @@ const defaultProps: Partial<InputProps> = {
|
||||
maxRows: 10000,
|
||||
showValidationLine: true,
|
||||
validators: [],
|
||||
blurOnEnter: true,
|
||||
};
|
||||
|
||||
export class Input extends React.Component<InputProps, State> {
|
||||
@ -267,6 +269,11 @@ export class Input extends React.Component<InputProps, State> {
|
||||
} else {
|
||||
this.setDirty();
|
||||
}
|
||||
|
||||
if(this.props.blurOnEnter){
|
||||
//pressing enter indicates that the edit is complete, we can unfocus now
|
||||
this.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,18 +14,66 @@ import { Checkbox } from "../checkbox";
|
||||
export type TableCellElem = React.ReactElement<TableCellProps>;
|
||||
|
||||
export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
|
||||
id?: string; // used for configuration visibility of columns
|
||||
/**
|
||||
* used for configuration visibility of columns
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Any css class names for this table cell. Only used if `title` is a "simple" react node
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The actual value of the cell
|
||||
*/
|
||||
title?: ReactNode;
|
||||
scrollable?: boolean; // content inside could be scrolled
|
||||
checkbox?: boolean; // render cell with a checkbox
|
||||
isChecked?: boolean; // mark checkbox as checked or not
|
||||
renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean"
|
||||
sortBy?: TableSortBy; // column name, must be same as key in sortable object <Table sortable={}/>
|
||||
showWithColumn?: string // id of the column which follow same visibility rules
|
||||
_sorting?: Partial<TableSortParams>; // <Table> sorting state, don't use this prop outside (!)
|
||||
_sort?(sortBy: TableSortBy): void; // <Table> sort function, don't use this prop outside (!)
|
||||
_nowrap?: boolean; // indicator, might come from parent <TableHead>, don't use this prop outside (!)
|
||||
|
||||
/**
|
||||
* content inside could be scrolled
|
||||
*/
|
||||
scrollable?: boolean;
|
||||
|
||||
/**
|
||||
* render cell with a checkbox
|
||||
*/
|
||||
checkbox?: boolean;
|
||||
|
||||
/**
|
||||
* mark checkbox as checked or not
|
||||
*/
|
||||
isChecked?: boolean;
|
||||
|
||||
/**
|
||||
* show "true" or "false" for all of the children elements are "typeof boolean"
|
||||
*/
|
||||
renderBoolean?: boolean;
|
||||
|
||||
/**
|
||||
* column name, must be same as key in sortable object <Table sortable={}/>
|
||||
*/
|
||||
sortBy?: TableSortBy;
|
||||
|
||||
/**
|
||||
* id of the column which follow same visibility rules
|
||||
*/
|
||||
showWithColumn?: string
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_sorting?: Partial<TableSortParams>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_sort?(sortBy: TableSortBy): void;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* indicator, might come from parent <TableHead>, don't use this prop outside (!)
|
||||
*/
|
||||
_nowrap?: boolean;
|
||||
}
|
||||
|
||||
export class TableCell extends React.Component<TableCellProps> {
|
||||
|
||||
@ -25,14 +25,15 @@
|
||||
pointer-events: none;
|
||||
transition: opacity 150ms 150ms ease-in-out;
|
||||
z-index: 100000;
|
||||
opacity: 1;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.24);
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
&.invisible {
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
|
||||
@ -54,7 +54,7 @@ export class Tooltip extends React.Component<TooltipProps> {
|
||||
|
||||
@observable.ref elem: HTMLElement;
|
||||
@observable activePosition: TooltipPosition;
|
||||
@observable isVisible = !!this.props.visible;
|
||||
@observable isVisible = false;
|
||||
|
||||
constructor(props: TooltipProps) {
|
||||
super(props);
|
||||
@ -78,6 +78,10 @@ export class Tooltip extends React.Component<TooltipProps> {
|
||||
this.hoverTarget.addEventListener("mouseleave", this.onLeaveTarget);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.refreshPosition();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.hoverTarget.removeEventListener("mouseenter", this.onEnterTarget);
|
||||
this.hoverTarget.removeEventListener("mouseleave", this.onLeaveTarget);
|
||||
@ -210,9 +214,9 @@ export class Tooltip extends React.Component<TooltipProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { style, formatters, usePortal, children } = this.props;
|
||||
const { style, formatters, usePortal, children, visible } = this.props;
|
||||
const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, {
|
||||
invisible: !this.isVisible,
|
||||
visible: visible ?? this.isVisible,
|
||||
formatter: !!formatters,
|
||||
});
|
||||
const tooltip = (
|
||||
|
||||
@ -10,6 +10,7 @@ import { Button } from "../button";
|
||||
import { Stepper } from "../stepper";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { Spinner } from "../spinner";
|
||||
import { debounce } from "lodash";
|
||||
|
||||
interface WizardCommonProps<D = any> {
|
||||
data?: Partial<D>;
|
||||
@ -179,14 +180,16 @@ export class WizardStep extends React.Component<WizardStepProps, WizardStepState
|
||||
}
|
||||
};
|
||||
|
||||
submit = () => {
|
||||
//because submit MIGHT be called through pressing enter, it might be fired twice.
|
||||
//we'll debounce it to ensure it isn't
|
||||
submit = debounce(() => {
|
||||
if (!this.form.noValidate) {
|
||||
const valid = this.form.checkValidity();
|
||||
|
||||
if (!valid) return;
|
||||
}
|
||||
this.next();
|
||||
};
|
||||
}, 100);
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
@ -196,6 +199,17 @@ export class WizardStep extends React.Component<WizardStepProps, WizardStepState
|
||||
);
|
||||
}
|
||||
|
||||
//make sure we call submit if the "enter" keypress doesn't trigger the events
|
||||
keyDown(evt: React.KeyboardEvent<HTMLElement>) {
|
||||
if (evt.shiftKey || evt.metaKey || evt.altKey || evt.ctrlKey || evt.repeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(evt.key === "Enter"){
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
step, isFirst, isLast, children,
|
||||
@ -216,6 +230,7 @@ export class WizardStep extends React.Component<WizardStepProps, WizardStepState
|
||||
return (
|
||||
<form className={className}
|
||||
onSubmit={prevDefault(this.submit)} noValidate={noValidate}
|
||||
onKeyDown={(evt) => this.keyDown(evt)}
|
||||
ref={e => this.form = e}>
|
||||
{beforeContent}
|
||||
<div className={contentClass}>
|
||||
|
||||
@ -19,7 +19,7 @@ import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
|
||||
|
||||
interface Dependencies {
|
||||
hostedCluster: Cluster;
|
||||
loadExtensions: (entity: CatalogEntity) => void;
|
||||
loadExtensions: (getCluster: () => CatalogEntity) => void;
|
||||
catalogEntityRegistry: CatalogEntityRegistry;
|
||||
frameRoutingId: number;
|
||||
emitEvent: (event: AppEvent) => void;
|
||||
@ -47,11 +47,12 @@ export const initClusterFrame =
|
||||
|
||||
catalogEntityRegistry.activeEntity = hostedCluster.id;
|
||||
|
||||
// Only load the extensions once the catalog has been populated
|
||||
// Only load the extensions once the catalog has been populated.
|
||||
// Note that the Catalog might still have unprocessed entities until the extensions are fully loaded.
|
||||
when(
|
||||
() => Boolean(catalogEntityRegistry.activeEntity),
|
||||
() => catalogEntityRegistry.items.length > 0,
|
||||
() =>
|
||||
loadExtensions(catalogEntityRegistry.activeEntity as KubernetesCluster),
|
||||
loadExtensions(() => catalogEntityRegistry.activeEntity as KubernetesCluster),
|
||||
{
|
||||
timeout: 15_000,
|
||||
onError: (error) => {
|
||||
|
||||
@ -11,15 +11,15 @@ import type { ExtensionLoading } from "../../../../extensions/extension-loader";
|
||||
import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry";
|
||||
|
||||
interface Dependencies {
|
||||
loadExtensions: () => ExtensionLoading[]
|
||||
loadExtensions: () => Promise<ExtensionLoading[]>;
|
||||
|
||||
// TODO: Move usages of third party library behind abstraction
|
||||
ipcRenderer: { send: (name: string) => void }
|
||||
ipcRenderer: { send: (name: string) => void };
|
||||
|
||||
// TODO: Remove dependencies being here only for correct timing of initialization
|
||||
bindProtocolAddRouteHandlers: () => void;
|
||||
lensProtocolRouterRenderer: { init: () => void };
|
||||
catalogEntityRegistry: CatalogEntityRegistry
|
||||
catalogEntityRegistry: CatalogEntityRegistry;
|
||||
}
|
||||
|
||||
const logPrefix = "[ROOT-FRAME]:";
|
||||
@ -40,7 +40,7 @@ export const initRootFrame =
|
||||
// maximum time to let bundled extensions finish loading
|
||||
const timeout = delay(10000);
|
||||
|
||||
const loadingExtensions = loadExtensions();
|
||||
const loadingExtensions = await loadExtensions();
|
||||
|
||||
const loadingBundledExtensions = loadingExtensions
|
||||
.filter((e) => e.isBundled)
|
||||
|
||||
@ -12,4 +12,3 @@ export * from "./kube-object-menu-registry";
|
||||
export * from "./registries";
|
||||
export * from "./workloads-overview-detail-registry";
|
||||
export * from "./catalog-category-registry";
|
||||
export * from "./status-bar-registry";
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
import * as registries from "../../extensions/registries";
|
||||
|
||||
export function initRegistries() {
|
||||
registries.AppPreferenceRegistry.createInstance();
|
||||
registries.CatalogEntityDetailRegistry.createInstance();
|
||||
registries.ClusterPageMenuRegistry.createInstance();
|
||||
registries.ClusterPageRegistry.createInstance();
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { StatusBarRegistry } from "../../extensions/registries";
|
||||
import { ActiveHotbarName } from "../components/cluster-manager/active-hotbar-name";
|
||||
|
||||
export function initStatusBarRegistry() {
|
||||
StatusBarRegistry.getInstance().add([
|
||||
{
|
||||
components: {
|
||||
Item: () => <ActiveHotbarName/>,
|
||||
position: "left",
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { comparer, computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { autoBind, Singleton } from "./utils";
|
||||
import { UserStore } from "../common/user-store";
|
||||
import logger from "../main/logger";
|
||||
@ -12,6 +12,7 @@ import lensLightThemeJson from "./themes/lens-light.json";
|
||||
import type { SelectOption } from "./components/select";
|
||||
import type { MonacoEditorProps } from "./components/monaco-editor";
|
||||
import { defaultTheme } from "../common/vars";
|
||||
import { camelCase } from "lodash";
|
||||
|
||||
export type ThemeId = string;
|
||||
|
||||
@ -25,7 +26,7 @@ export interface Theme {
|
||||
}
|
||||
|
||||
export class ThemeStore extends Singleton {
|
||||
protected styles: HTMLStyleElement;
|
||||
private terminalColorPrefix = "terminal";
|
||||
|
||||
// bundled themes from `themes/${themeId}.json`
|
||||
private themes = observable.map<ThemeId, Theme>({
|
||||
@ -33,14 +34,37 @@ export class ThemeStore extends Singleton {
|
||||
"lens-light": lensLightThemeJson as Theme,
|
||||
});
|
||||
|
||||
@computed get activeThemeId(): string {
|
||||
@computed get activeThemeId(): ThemeId {
|
||||
return UserStore.getInstance().colorTheme;
|
||||
}
|
||||
|
||||
@computed get terminalThemeId(): ThemeId {
|
||||
return UserStore.getInstance().terminalTheme;
|
||||
}
|
||||
|
||||
@computed get activeTheme(): Theme {
|
||||
return this.themes.get(this.activeThemeId) ?? this.themes.get(defaultTheme);
|
||||
}
|
||||
|
||||
@computed get terminalColors(): [string, string][] {
|
||||
const theme = this.themes.get(this.terminalThemeId) ?? this.activeTheme;
|
||||
|
||||
return Object
|
||||
.entries(theme.colors)
|
||||
.filter(([name]) => name.startsWith(this.terminalColorPrefix));
|
||||
}
|
||||
|
||||
// Replacing keys stored in styles to format accepted by terminal
|
||||
// E.g. terminalBrightBlack -> brightBlack
|
||||
@computed get xtermColors(): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
this.terminalColors.map(([name, color]) => [
|
||||
camelCase(name.replace(this.terminalColorPrefix, "")),
|
||||
color,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@computed get themeOptions(): SelectOption<string>[] {
|
||||
return Array.from(this.themes).map(([themeId, theme]) => ({
|
||||
label: theme.name,
|
||||
@ -55,15 +79,19 @@ export class ThemeStore extends Singleton {
|
||||
autoBind(this);
|
||||
|
||||
// auto-apply active theme
|
||||
reaction(() => this.activeThemeId, themeId => {
|
||||
reaction(() => ({
|
||||
themeId: this.activeThemeId,
|
||||
terminalThemeId: this.terminalThemeId,
|
||||
}), ({ themeId }) => {
|
||||
try {
|
||||
this.applyTheme(this.getThemeById(themeId));
|
||||
this.applyTheme(themeId);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
UserStore.getInstance().resetTheme();
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
equals: comparer.shallow,
|
||||
});
|
||||
}
|
||||
|
||||
@ -71,20 +99,18 @@ export class ThemeStore extends Singleton {
|
||||
return this.themes.get(themeId);
|
||||
}
|
||||
|
||||
protected applyTheme(theme: Theme) {
|
||||
if (!this.styles) {
|
||||
this.styles = document.createElement("style");
|
||||
this.styles.id = "lens-theme";
|
||||
document.head.append(this.styles);
|
||||
}
|
||||
const cssVars = Object.entries(theme.colors).map(([cssName, color]) => {
|
||||
return `--${cssName}: ${color};`;
|
||||
protected applyTheme(themeId: ThemeId) {
|
||||
const theme = this.getThemeById(themeId);
|
||||
const colors = Object.entries({
|
||||
...theme.colors,
|
||||
...Object.fromEntries(this.terminalColors),
|
||||
});
|
||||
|
||||
this.styles.textContent = `:root {\n${cssVars.join("\n")}}`;
|
||||
// Adding universal theme flag which can be used in component styles
|
||||
const body = document.querySelector("body");
|
||||
colors.forEach(([name, value]) => {
|
||||
document.documentElement.style.setProperty(`--${name}`, value);
|
||||
});
|
||||
|
||||
body.classList.toggle("theme-light", theme.type === "light");
|
||||
// Adding universal theme flag which can be used in component styles
|
||||
document.body.classList.toggle("theme-light", theme.type === "light");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Dark (Lens)",
|
||||
"name": "Dark",
|
||||
"type": "dark",
|
||||
"description": "Original Lens dark theme",
|
||||
"author": "Mirantis",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Light (Lens)",
|
||||
"name": "Light",
|
||||
"type": "light",
|
||||
"description": "Original Lens light theme",
|
||||
"author": "Mirantis",
|
||||
@ -76,26 +76,26 @@
|
||||
"logsBackground": "#24292e",
|
||||
"logsForeground": "#ffffff",
|
||||
"logRowHoverBackground": "#35373a",
|
||||
"terminalBackground": "#24292e",
|
||||
"terminalForeground": "#ffffff",
|
||||
"terminalCursor": "#ffffff",
|
||||
"terminalCursorAccent": "#000000",
|
||||
"terminalSelection": "#ffffff77",
|
||||
"terminalBlack": "#2e3436",
|
||||
"terminalRed": "#cc0000",
|
||||
"terminalGreen": "#4e9a06",
|
||||
"terminalYellow": "#c4a000",
|
||||
"terminalBlue": "#3465a4",
|
||||
"terminalMagenta": "#75507b",
|
||||
"terminalCyan": "#06989a",
|
||||
"terminalBackground": "#ffffff",
|
||||
"terminalForeground": "#2d2d2d",
|
||||
"terminalCursor": "#2d2d2d",
|
||||
"terminalCursorAccent": "#ffffff",
|
||||
"terminalSelection": "#bfbfbf",
|
||||
"terminalBlack": "#2d2d2d",
|
||||
"terminalRed": "#cd3734 ",
|
||||
"terminalGreen": "#18cf12",
|
||||
"terminalYellow": "#acb300",
|
||||
"terminalBlue": "#3d90ce",
|
||||
"terminalMagenta": "#c100cd",
|
||||
"terminalCyan": "#07c4b9",
|
||||
"terminalWhite": "#d3d7cf",
|
||||
"terminalBrightBlack": "#555753",
|
||||
"terminalBrightRed": "#ef2929",
|
||||
"terminalBrightGreen": "#8ae234",
|
||||
"terminalBrightYellow": "#fce94f",
|
||||
"terminalBrightBlue": "#729fcf",
|
||||
"terminalBrightMagenta": "#ad7fa8",
|
||||
"terminalBrightCyan": "#34e2e2",
|
||||
"terminalBrightBlack": "#a8a8a8",
|
||||
"terminalBrightRed": "#ff6259",
|
||||
"terminalBrightGreen": "#5cdb59",
|
||||
"terminalBrightYellow": "#f8c000",
|
||||
"terminalBrightBlue": "#008db6",
|
||||
"terminalBrightMagenta": "#ee55f8",
|
||||
"terminalBrightCyan": "#50e8df",
|
||||
"terminalBrightWhite": "#eeeeec",
|
||||
"dialogTextColor": "#87909c",
|
||||
"dialogBackground": "#ffffff",
|
||||
|
||||
13
yarn.lock
13
yarn.lock
@ -5460,6 +5460,17 @@ eslint-import-resolver-node@^0.3.6:
|
||||
debug "^3.2.7"
|
||||
resolve "^1.20.0"
|
||||
|
||||
eslint-import-resolver-typescript@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.5.0.tgz#07661966b272d14ba97f597b51e1a588f9722f0a"
|
||||
integrity sha512-qZ6e5CFr+I7K4VVhQu3M/9xGv9/YmwsEXrsm3nimw8vWaVHRDrQRp26BgCypTxBp3vUp4o5aVEJRiy0F2DFddQ==
|
||||
dependencies:
|
||||
debug "^4.3.1"
|
||||
glob "^7.1.7"
|
||||
is-glob "^4.0.1"
|
||||
resolve "^1.20.0"
|
||||
tsconfig-paths "^3.9.0"
|
||||
|
||||
eslint-module-utils@^2.7.1:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz#b435001c9f8dd4ab7f6d0efcae4b9696d4c24b7c"
|
||||
@ -6516,7 +6527,7 @@ glob-to-regexp@^0.4.1:
|
||||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
|
||||
|
||||
glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0:
|
||||
glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
|
||||
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user