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

Allow extensions to have external dependencies (#1018)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2020-10-05 16:39:58 +03:00 committed by GitHub
parent de3849d22c
commit 2b6f283e1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1861 additions and 305 deletions

View File

@ -1 +1,3 @@
*.js
node_modules/
dist/

View File

@ -1,3 +1,7 @@
# Lens Example Extension
*TODO*: add more info
## Build
`npm run build`

View File

@ -1,4 +1,5 @@
import { Button, DynamicPageType, Icon, IconProps, LensExtension, React } from "@lens/extensions";
import { CoffeeDoodle } from "react-open-doodles";
import path from "path";
export default class ExampleExtension extends LensExtension {
@ -21,7 +22,7 @@ export default class ExampleExtension extends LensExtension {
}
export function ExtensionIcon(props: IconProps) {
return <Icon {...props} material="camera" tooltip={path.basename(__filename)}/>
return <Icon {...props} material="pages" tooltip={path.basename(__filename)}/>
}
export class ExtensionPage extends React.Component<{ extension: ExampleExtension }> {
@ -33,10 +34,14 @@ export class ExtensionPage extends React.Component<{ extension: ExampleExtension
render() {
const { TabLayout } = this.props.extension.runtime.components;
const doodleStyle = {
width: "200px"
}
return (
<TabLayout className="ExampleExtension">
<div className="flex column gaps align-flex-start">
<p>Hello from extensions-api!</p>
<div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div>
<p>Hello from Example extension!</p>
<p>File: <i>{__filename}</i></p>
<Button accent label="Deactivate" onClick={this.deactivate}/>
</div>

View File

@ -0,0 +1,19 @@
{
"name": "extension-example",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"react-open-doodles": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/react-open-doodles/-/react-open-doodles-1.0.5.tgz",
"integrity": "sha512-TGYuRDL2XX2PG/9ZH2MXfYi/vbQLRnvqPxfzNRc4X3Nxmsiz93kIRmT5OWbItadvqEwZTD4IncwHMSLOTUvSZQ=="
},
"typescript": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
"integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==",
"dev": true
}
}
}

View File

@ -2,11 +2,19 @@
"name": "extension-example",
"version": "1.0.0",
"description": "Example extension",
"main": "example-extension.js",
"main": "dist/index.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {
"react-open-doodles": "^1.0.5"
},
"devDependencies": {
"typescript": "^4.0.3"
}
}

View File

@ -1,13 +1,25 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": ".",
"outDir": "dist",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"jsx": "react"
},
"include": [
"../../types",
"./example-extension.tsx"
"./index.tsx"
],
"exclude": [
"node_modules",
"*.js"
]
}

View File

@ -34,8 +34,7 @@
"download-bins": "concurrently yarn:download:*",
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
"download:helm": "yarn run ts-node build/download_helm.ts",
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/",
"extensions:example": "tsc --project extensions/example-extension/tsconfig.json --watch"
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/"
},
"config": {
"bundledKubectlVersion": "1.17.11",
@ -201,6 +200,7 @@
"module-alias": "^2.2.2",
"node-machine-id": "^1.1.12",
"node-pty": "^0.9.0",
"npm": "^6.14.8",
"openid-client": "^3.15.2",
"path-to-regexp": "^6.1.0",
"proper-lockfile": "^4.1.1",
@ -235,33 +235,33 @@
"@lingui/react": "^3.0.0-13",
"@material-ui/core": "^4.10.1",
"@rollup/plugin-json": "^4.1.0",
"@types/chart.js": "^2.9.21",
"@types/circular-dependency-plugin": "^5.0.1",
"@types/color": "^3.0.1",
"@types/crypto-js": "^3.1.47",
"@types/dompurify": "^2.0.2",
"@types/electron-window-state": "^2.0.34",
"@types/fs-extra": "^9.0.1",
"@types/hapi": "^18.0.3",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/html-webpack-plugin": "^3.2.3",
"@types/http-proxy": "^1.17.4",
"@types/jest": "^25.2.3",
"@types/js-yaml": "^3.12.4",
"@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.155",
"@types/marked": "^0.7.4",
"@types/mock-fs": "^4.10.0",
"@types/module-alias": "^2.0.0",
"@types/node": "^12.12.45",
"@types/proper-lockfile": "^4.1.1",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/tar": "^4.0.3",
"@types/chart.js": "^2.9.21",
"@types/circular-dependency-plugin": "^5.0.1",
"@types/color": "^3.0.1",
"@types/dompurify": "^2.0.2",
"@types/hapi": "^18.0.3",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/html-webpack-plugin": "^3.2.3",
"@types/jest": "^25.2.3",
"@types/material-ui": "^0.21.7",
"@types/md5-file": "^4.0.2",
"@types/mini-css-extract-plugin": "^0.9.1",
"@types/mock-fs": "^4.10.0",
"@types/module-alias": "^2.0.0",
"@types/node": "^12.12.45",
"@types/npm": "^2.0.31",
"@types/progress-bar-webpack-plugin": "^2.1.0",
"@types/proper-lockfile": "^4.1.1",
"@types/react": "^16.9.35",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-router-dom": "^5.1.5",
"@types/react-select": "^3.0.13",
"@types/react-window": "^1.8.2",
@ -270,6 +270,7 @@
"@types/semver": "^7.2.0",
"@types/shelljs": "^0.8.8",
"@types/spdy": "^3.4.4",
"@types/tar": "^4.0.3",
"@types/tcp-port-used": "^1.0.0",
"@types/tempy": "^0.3.0",
"@types/terser-webpack-plugin": "^3.0.0",
@ -319,10 +320,10 @@
"progress-bar-webpack-plugin": "^2.1.0",
"raw-loader": "^4.0.1",
"react": "^16.13.1",
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-beautiful-dnd": "^13.0.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-select": "^3.1.0",
"react-window": "^1.8.5",
"rollup": "^2.28.2",

View File

@ -1,5 +1,6 @@
import { createIpcChannel } from "./ipc";
import { ClusterId, clusterStore } from "./cluster-store";
import { extensionLoader } from "../extensions/extension-loader"
import { tracker } from "./tracker";
export const clusterIpc = {
@ -8,7 +9,10 @@ export const clusterIpc = {
handle: (clusterId: ClusterId, frameId?: number) => {
const cluster = clusterStore.getById(clusterId);
if (cluster) {
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
if (frameId) {
cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
}
extensionLoader.broadcastExtensions(frameId)
return cluster.activate();
}
},

View File

@ -0,0 +1,110 @@
import type { ExtensionId, LensExtension, ExtensionManifest, ExtensionModel } from "./lens-extension"
import { broadcastIpc } from "../common/ipc"
import type { LensRuntimeRendererEnv } from "./lens-runtime"
import path from "path"
import { observable, reaction, toJS, } from "mobx"
import logger from "../main/logger"
import { app, remote, ipcRenderer } from "electron"
export interface InstalledExtension extends ExtensionModel {
manifestPath: string;
manifest: ExtensionManifest;
}
// lazy load so that we get correct userData
export function extensionPackagesRoot() {
return path.join((app || remote.app).getPath("userData"))
}
export function withExtensionPackagesRoot(callback: Function) {
const cwd = process.cwd()
try {
process.chdir(extensionPackagesRoot())
return callback()
} finally {
process.chdir(cwd)
}
}
export class ExtensionLoader {
@observable extensions = observable.map<ExtensionId, InstalledExtension>([], { deep: false });
@observable instances = observable.map<ExtensionId, LensExtension>([], { deep: false })
constructor() {
if (ipcRenderer) {
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
extensions.forEach((ext) => {
if (!this.getById(ext.manifestPath)) {
this.extensions.set(ext.manifestPath, ext)
}
})
})
}
}
autoEnableOnLoad(getLensRuntimeEnv: () => LensRuntimeRendererEnv, { delay = 0 } = {}) {
logger.info('[EXTENSIONS-LOADER]: auto-activation loaded extensions: ON');
return reaction(() => this.extensions.toJS(), installedExtensions => {
installedExtensions.forEach((ext) => {
let instance = this.instances.get(ext.manifestPath)
if (!instance) {
const extensionModule = this.requireExtension(ext)
if (!extensionModule) {
logger.error("[EXTENSION-LOADER] failed to load extension " + ext.manifestPath)
return
}
const LensExtensionClass = extensionModule.default;
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
instance.enable(getLensRuntimeEnv());
this.instances.set(ext.id, instance)
}
})
}, {
fireImmediately: true,
delay: delay,
})
}
protected requireExtension(extension: InstalledExtension) {
return withExtensionPackagesRoot(() => {
try {
const extMain = path.join(path.dirname(extension.manifestPath), extension.manifest.main)
return __non_webpack_require__(extMain)
} catch (err) {
console.error(`[EXTENSION-LOADER]: can't load extension main at ${extension.manifestPath}: ${err}`, { extension });
}
})
}
getById(id: ExtensionId): InstalledExtension {
return this.extensions.get(id);
}
async removeById(id: ExtensionId) {
const extension = this.getById(id);
if (extension) {
const instance = this.instances.get(extension.id)
if (instance) { await instance.uninstall() }
this.extensions.delete(id);
}
}
broadcastExtensions(frameId?: number) {
broadcastIpc({
channel: "extensions:loaded",
frameId: frameId,
frameOnly: !!frameId,
args: [this.toJSON().extensions],
})
}
toJSON() {
return toJS({
extensions: Array.from(this.extensions).map(([id, instance]) => instance),
}, {
recurseEverything: true,
})
}
}
export const extensionLoader = new ExtensionLoader()

View File

@ -0,0 +1,97 @@
import type { ExtensionManifest } from "./lens-extension"
import path from "path"
import fs from "fs-extra"
import logger from "../main/logger"
import { withExtensionPackagesRoot, extensionPackagesRoot, InstalledExtension } from "./extension-loader"
import npm from "npm"
export class ExtensionManager {
get extensionPackagesRoot() {
return extensionPackagesRoot()
}
get folderPath(): string {
return path.resolve(__static, "../extensions");
}
async load() {
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
await fs.ensureDir(path.join(this.extensionPackagesRoot, "node_modules"))
await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), `{"dependencies": []}`, {mode: 0o600})
return await this.loadExtensions();
}
async getExtensionByManifest(manifestPath: string): Promise<InstalledExtension> {
let manifestJson: ExtensionManifest;
try {
manifestJson = __non_webpack_require__(manifestPath)
withExtensionPackagesRoot(() => {
this.installPackageFromPath(path.dirname(manifestPath))
})
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
return {
id: manifestJson.name,
version: manifestJson.version,
name: manifestJson.name,
manifestPath: path.join(this.extensionPackagesRoot, "node_modules", manifestJson.name, "package.json"),
manifest: manifestJson
}
} catch (err) {
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
}
}
protected installPackageFromPath(path: string): Promise<void> {
const origLogger = console.log
return new Promise((resolve, reject) => {
npm.load({
production: true,
global: false,
prefix: this.extensionPackagesRoot,
dev: false,
spin: false,
"ignore-scripts": true,
loglevel: "silent"
}, (err) => {
console.log = function() {
// just to ignore ts empty function error
}
npm.commands.install([
path
], (err) => {
console.log = origLogger
if (err) {
reject(err)
} else {
resolve()
}
})
})
})
}
async loadExtensions() {
const extensions = await this.loadFromFolder(this.folderPath);
return new Map(extensions.map(ext => [ext.id, ext]));
}
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
const paths = await fs.readdir(folderPath);
const manifestsLoading = paths.map(fileName => {
const absPath = path.resolve(folderPath, fileName);
const manifestPath = path.resolve(absPath, "package.json");
return fs.access(manifestPath, fs.constants.F_OK)
.then(async () => await this.getExtensionByManifest(manifestPath))
.catch(() => null)
});
let extensions = await Promise.all(manifestsLoading);
extensions = extensions.filter(v => !!v); // filter out files and invalid folders (without manifest.json)
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions;
}
}
export const extensionManager = new ExtensionManager()

View File

@ -1,168 +0,0 @@
import type { ExtensionId, ExtensionManifest, ExtensionVersion, LensExtension } from "./lens-extension";
import type { LensRuntimeRendererEnv } from "./lens-runtime";
import path from "path";
import fs from "fs-extra";
import { action, observable, reaction, toJS, } from "mobx";
import { BaseStore } from "../common/base-store";
import logger from "../main/logger";
export interface ExtensionStoreModel {
version: ExtensionVersion;
extensions: [ExtensionId, ExtensionModel][]
}
export interface ExtensionModel {
id: ExtensionId;
version: ExtensionVersion;
name: string;
manifestPath: string;
description?: string;
enabled?: boolean;
updateUrl?: string;
}
export interface InstalledExtension<T extends ExtensionModel = any> {
manifestPath: string;
manifest: ExtensionManifest;
extensionModule: {
[name: string]: any;
default: new (model: ExtensionModel, manifest?: ExtensionManifest) => LensExtension
}
}
export class ExtensionStore extends BaseStore<ExtensionStoreModel> {
private constructor() {
super({
configName: "lens-extension-store",
});
}
@observable version: ExtensionVersion = "0.0.0";
@observable extensions = observable.map<ExtensionId, LensExtension>();
@observable removed = observable.map<ExtensionId, LensExtension>();
@observable installed = observable.map<ExtensionId, InstalledExtension>([], { deep: false });
get folderPath(): string {
return path.resolve(__static, "../extensions");
}
async load() {
await this.loadExtensions();
return super.load();
}
autoEnableOnLoad(getLensRuntimeEnv: () => LensRuntimeRendererEnv, { delay = 0 } = {}) {
logger.info('[EXTENSIONS-STORE]: auto-activation loaded extensions: ON');
return reaction(() => this.installed.toJS(), installedExtensions => {
installedExtensions.forEach(({ extensionModule, manifest, manifestPath }) => {
let instance = this.getById(manifestPath);
if (!instance) {
const LensExtensionClass = extensionModule.default;
instance = new LensExtensionClass({ ...manifest, manifestPath, id: manifestPath }, manifest);
instance.enable(getLensRuntimeEnv());
this.extensions.set(manifestPath, instance); // save
}
})
}, {
fireImmediately: true,
delay: delay,
})
}
getExtensionByManifest(manifestPath: string): InstalledExtension {
let manifestJson: ExtensionManifest;
let mainJs: string;
try {
manifestJson = __non_webpack_require__(manifestPath); // "__non_webpack_require__" converts to native node's require()-call
mainJs = path.resolve(path.dirname(manifestPath), manifestJson.main);
const extensionModule = __non_webpack_require__(mainJs);
return {
manifestPath: manifestPath,
manifest: manifestJson,
extensionModule: extensionModule,
}
} catch (err) {
console.error(`[EXTENSION-STORE]: can't load extension at ${manifestPath}: ${err}`, { manifestJson, mainJs });
}
}
@action
async loadExtensions() {
const extensions = await this.loadFromFolder(this.folderPath);
const extManifestMap = new Map(extensions.map(ext => [ext.manifestPath, ext]));
this.installed.replace(extManifestMap);
}
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
const paths = await fs.readdir(folderPath);
const manifestsLoading = paths.map(fileName => {
const absPath = path.resolve(folderPath, fileName);
const manifestPath = path.resolve(absPath, "package.json");
return fs.access(manifestPath, fs.constants.F_OK)
.then(() => this.getExtensionByManifest(manifestPath))
.catch(() => null)
});
let extensions = await Promise.all(manifestsLoading);
extensions = extensions.filter(v => !!v); // filter out files and invalid folders (without manifest.json)
console.info(`[EXTENSION-STORE]: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions;
}
getById(id: ExtensionId): LensExtension {
return this.extensions.get(id);
}
async removeById(id: ExtensionId) {
const extension = this.getById(id);
if (extension) {
await extension.uninstall();
this.extensions.delete(id);
}
}
@action
protected fromStore({ extensions, version }: ExtensionStoreModel) {
if (version) {
this.version = version;
}
if (extensions) {
const currentExtensions = new Map(extensions);
this.extensions.forEach(extension => {
if (!currentExtensions.has(extension.id)) {
this.removed.set(extension.id, extension);
}
})
currentExtensions.forEach(model => {
const extensionId = model.id || model.manifestPath;
const manifest = this.installed.get(extensionId);
if (!manifest) {
console.error(`[EXTENSION-STORE]: can't load extension manifest at ${model.manifestPath}`, { model })
return;
}
const extensionInstance = this.getById(extensionId)
if (!extensionInstance) {
try {
const { manifest: manifestJson, extensionModule } = manifest;
const LensExtensionClass = extensionModule.default;
this.extensions.set(model.id, new LensExtensionClass(model, manifestJson));
} catch (err) {
console.error(`[EXTENSION-STORE]: init extension failed: ${err}`, { model, manifest })
}
} else {
extensionInstance.importModel(model);
}
})
}
}
toJSON(): ExtensionStoreModel {
return toJS({
version: this.version,
extensions: Array.from(this.extensions).map(([id, instance]) => [id, instance.toJSON()]),
}, {
recurseEverything: true,
})
}
}
export const extensionStore = ExtensionStore.getInstance<ExtensionStore>()

View File

@ -1,4 +1,3 @@
import type { ExtensionModel } from "./extension-store";
import type { LensRuntimeRendererEnv } from "./lens-runtime";
import type { PageRegistration } from "./register-page";
import { readJsonSync } from "fs-extra";
@ -9,6 +8,16 @@ export type ExtensionId = string | ExtensionPackageJsonPath;
export type ExtensionPackageJsonPath = string;
export type ExtensionVersion = string | number;
export interface ExtensionModel {
id: ExtensionId;
version: ExtensionVersion;
name: string;
manifestPath: string;
description?: string;
enabled?: boolean;
updateUrl?: string;
}
export interface ExtensionManifest extends ExtensionModel {
main: string;
description?: string; // todo: add more fields similar to package.json + some extra

View File

@ -17,6 +17,8 @@ import { clusterStore } from "../common/cluster-store"
import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store";
import { tracker } from "../common/tracker";
import { extensionManager } from "../extensions/extension-manager";
import { extensionLoader } from "../extensions/extension-loader";
import logger from "./logger"
const workingDir = path.join(app.getPath("appData"), appName);
@ -75,6 +77,8 @@ async function main() {
// create window manager and open app
windowManager = new WindowManager(proxyPort);
extensionLoader.extensions.replace(await extensionManager.load())
}
app.on("ready", main);

View File

@ -6,7 +6,6 @@ import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.r
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
import { extensionsURL } from "../renderer/components/+extensions/extensions.route";
import logger from "./logger";
export function initMenu(windowManager: WindowManager) {
@ -73,13 +72,6 @@ export function buildMenu(windowManager: WindowManager) {
navigate(preferencesURL())
}
},
{
label: 'Extensions',
accelerator: 'CmdOrCtrl+E',
click() {
navigate(extensionsURL())
}
},
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },

View File

@ -4,7 +4,7 @@ import { render, unmountComponentAtNode } from "react-dom";
import { isMac } from "../common/vars";
import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store";
import { extensionStore } from "../extensions/extension-store";
import { extensionLoader } from "../extensions/extension-loader";
import { clusterStore } from "../common/cluster-store";
import { i18nStore } from "./i18n";
import { themeStore } from "./theme.store";
@ -25,18 +25,17 @@ export async function bootstrap(App: AppComponent) {
userStore.load(),
workspaceStore.load(),
clusterStore.load(),
extensionStore.load(),
i18nStore.init(),
themeStore.init(),
]);
// Register additional store listeners
clusterStore.registerIpcListener();
extensionLoader.autoEnableOnLoad(getLensRuntime);
// init app's dependencies if any
if (App.init) {
await App.init();
extensionStore.autoEnableOnLoad(getLensRuntime);
}
window.addEventListener("message", (ev: MessageEvent) => {
if (ev.data === "teardown") {

View File

@ -1,11 +0,0 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
export const extensionsRoute: RouteProps = {
path: "/extensions"
}
export interface IExtensionsRouteParams {
}
export const extensionsURL = buildURL<IExtensionsRouteParams>(extensionsRoute.path);

View File

@ -1,4 +0,0 @@
.Extensions {
$spacing: $padding * 2;
padding: $spacing;
}

View File

@ -1,31 +0,0 @@
import "./extensions.scss"
import React from "react";
import { observer } from "mobx-react";
import { extensionStore } from "../../../extensions/extension-store";
import { WizardLayout } from "../layout/wizard-layout";
import { Icon } from "../icon";
@observer
export class Extensions extends React.Component {
// todo: add input-select to customize extensions loading folder(s)
renderInfoPanel() {
return (
<div className="info-panel flex gaps align-center">
<Icon material="info"/>
<p>Extensions available to install</p>
</div>
);
}
render() {
const { installed: installedExtensions } = extensionStore;
return (
<WizardLayout className="Extensions" infoPanel={this.renderInfoPanel()}>
<h2>Extensions</h2>
<pre>
{JSON.stringify(installedExtensions.toJSON(), null, 2)}
</pre>
</WizardLayout>
);
}
}

View File

@ -1,2 +0,0 @@
export * from "./extensions.route"
export * from "./extensions"

View File

@ -11,7 +11,6 @@ import { Workspaces, workspacesRoute } from "../+workspaces";
import { AddCluster, addClusterRoute } from "../+add-cluster";
import { ClusterView } from "./cluster-view";
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
import { Extensions, extensionsRoute } from "../+extensions";
import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route";
import { clusterStore } from "../../../common/cluster-store";
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
@ -64,7 +63,6 @@ export class ClusterManager extends React.Component {
<Route component={AddCluster} {...addClusterRoute} />
<Route component={ClusterView} {...clusterViewRoute} />
<Route component={ClusterSettings} {...clusterSettingsRoute} />
<Route component={Extensions} {...extensionsRoute}/>
{dynamicPages.globalPages.map(({ path, components: { Page } }) => {
return <Route key={path} path={path} component={Page}/>
})}

1604
yarn.lock

File diff suppressed because it is too large Load Diff