1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into font-settings-for-editor-and-ternimal

# Conflicts:
#	src/renderer/components/+preferences/application.tsx
#	src/renderer/components/+preferences/preferences.tsx
This commit is contained in:
DMYTRO ZHARKOV 2022-01-19 15:07:42 +01:00
commit 3fd2d6121d
45 changed files with 444 additions and 620 deletions

View File

@ -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: [
{

View File

@ -20,6 +20,7 @@ Each guide or code sample includes the following:
| [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) | |

View 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);
```

View File

@ -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

View File

@ -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",

View File

@ -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();

View File

@ -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;

View File

@ -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";

View File

@ -252,7 +252,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),
@ -270,11 +269,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 [];
}
@ -299,11 +299,15 @@ export class ExtensionLoader {
});
};
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) {
@ -312,7 +316,8 @@ export class ExtensionLoader {
if (!LensExtensionClass) {
this.nonInstancesByName.add(extension.manifest.name);
continue;
return null;
}
const instance = this.dependencies.createExtensionInstance(
@ -320,27 +325,49 @@ export class ExtensionLoader {
extension,
);
const loaded = instance.enable(register).catch((err) => {
logger.error(`${logModule}: failed to enable`, { ext: extension, err });
});
loadingExtensions.push({
return {
extId,
instance,
isBundled: extension.isBundled,
loaded,
});
this.instances.set(extId, instance);
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 });
});
this.instances.set(extension.extId, extension.instance);
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 {

View File

@ -86,7 +86,6 @@ export class LensExtension {
}
try {
await this.onActivate();
this._isEnabled = true;
this[Disposers].push(...await register(this));
@ -113,6 +112,11 @@ export class LensExtension {
}
}
@action
activate() {
return this.onActivate();
}
protected onActivate(): Promise<void> | void {
return;
}

View File

@ -15,13 +15,14 @@ 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";
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[] = [];

View File

@ -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";

View File

@ -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[];
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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"

View File

@ -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);

View File

@ -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,
};
}
}

View 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 { 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;

View File

@ -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)
));
};

View File

@ -13,10 +13,12 @@ import { Input } from "../input";
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,
@ -27,18 +29,23 @@ 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 [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
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"
@ -78,10 +85,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>
@ -94,7 +101,7 @@ export const Application = observer(() => {
))}
<section id="update-channel">
<SubTitle title="Update Channel"/>
<SubTitle title="Update Channel" />
<Select
options={updateChannelOptions}
value={userStore.updateChannel}
@ -116,4 +123,14 @@ export const Application = observer(() => {
</section>
</section>
);
});
};
export const Application = withInjectables<Dependencies>(
observer(NonInjectedApplication),
{
getProps: (di) => ({
appPreferenceItems: di.inject(appPreferencesInjectable),
}),
},
);

View File

@ -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"
}

View File

@ -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),
}),
},
);

View File

@ -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";
@ -25,7 +25,6 @@ import {
terminalRoute,
terminalURL,
} 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";
@ -37,18 +36,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 });
@ -71,24 +70,32 @@ 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={terminalURL()} component={Terminal}/>
<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={terminalURL()} component={Terminal}/>
<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),
}),
},
);

View File

@ -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),
}),
},
);

View File

@ -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,
}),
});

View File

@ -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(() => [

View File

@ -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>

View File

@ -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%;

View File

@ -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) => ({

View File

@ -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();
}
}
}

View File

@ -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 {

View File

@ -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 = (

View File

@ -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}>

View File

@ -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) => {

View File

@ -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)

View File

@ -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";

View File

@ -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();

View File

@ -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",
},
},
]);
}

View File

@ -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==