mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add lens:// protocol handling with a routing mechanism (#1949)
- Add lens:// protocol handling with a routing mechanism - document the methods in an extension guide - remove handlers when an extension is deactivated or removed - make sure that the found extension when routing a request is currently enabled (as a backup) - added documentation about the above behaviour to the guide - tweaked the naming convention so that it is clearer that the router uses extension names as not IDs (which currently are folder paths) - Convert the extension API to use an array for registering handlers - switch design to execute both main and renderer handlers simultaneously, without any overlap checking - change open to be a dev dep - improve docs, export types for extensions, skip integration tests - switch to event emitting renderer being ready - Add logging and fix renderer:loaded send to main Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
38c3734d5c
commit
1470103fd4
15
Makefile
15
Makefile
@ -9,7 +9,7 @@ else
|
|||||||
DETECTED_OS := $(shell uname)
|
DETECTED_OS := $(shell uname)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
binaries/client:
|
binaries/client: node_modules
|
||||||
yarn download-bins
|
yarn download-bins
|
||||||
|
|
||||||
node_modules: yarn.lock
|
node_modules: yarn.lock
|
||||||
@ -37,17 +37,24 @@ test: binaries/client
|
|||||||
yarn test
|
yarn test
|
||||||
|
|
||||||
.PHONY: integration-linux
|
.PHONY: integration-linux
|
||||||
integration-linux: build-extension-types build-extensions
|
integration-linux: binaries/client build-extension-types build-extensions
|
||||||
|
# ifdef XDF_CONFIG_HOME
|
||||||
|
# rm -rf ${XDG_CONFIG_HOME}/.config/Lens
|
||||||
|
# else
|
||||||
|
# rm -rf ${HOME}/.config/Lens
|
||||||
|
# endif
|
||||||
yarn build:linux
|
yarn build:linux
|
||||||
yarn integration
|
yarn integration
|
||||||
|
|
||||||
.PHONY: integration-mac
|
.PHONY: integration-mac
|
||||||
integration-mac: build-extension-types build-extensions
|
integration-mac: binaries/client build-extension-types build-extensions
|
||||||
|
# rm ${HOME}/Library/Application\ Support/Lens
|
||||||
yarn build:mac
|
yarn build:mac
|
||||||
yarn integration
|
yarn integration
|
||||||
|
|
||||||
.PHONY: integration-win
|
.PHONY: integration-win
|
||||||
integration-win: build-extension-types build-extensions
|
integration-win: binaries/client build-extension-types build-extensions
|
||||||
|
# rm %APPDATA%/Lens
|
||||||
yarn build:win
|
yarn build:win
|
||||||
yarn integration
|
yarn integration
|
||||||
|
|
||||||
|
|||||||
@ -21,12 +21,13 @@ Each guide or code sample includes the following:
|
|||||||
| [Components](components.md) | |
|
| [Components](components.md) | |
|
||||||
| [KubeObjectListLayout](kube-object-list-layout.md) | |
|
| [KubeObjectListLayout](kube-object-list-layout.md) | |
|
||||||
| [Working with mobx](working-with-mobx.md) | |
|
| [Working with mobx](working-with-mobx.md) | |
|
||||||
|
| [Protocol Handlers](protocol-handlers.md) | |
|
||||||
|
|
||||||
## Samples
|
## Samples
|
||||||
|
|
||||||
| Sample | APIs |
|
| Sample | APIs |
|
||||||
| ----- | ----- |
|
| ----- | ----- |
|
||||||
[helloworld](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||||
[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension <br> Store.clusterStore <br> Store.workspaceStore |
|
[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension <br> Store.clusterStore <br> Store.workspaceStore |
|
||||||
[styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
[styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||||
[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||||
|
|||||||
BIN
docs/extensions/guides/images/routing-diag.png
Normal file
BIN
docs/extensions/guides/images/routing-diag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
83
docs/extensions/guides/protocol-handlers.md
Normal file
83
docs/extensions/guides/protocol-handlers.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Lens Protocol Handlers
|
||||||
|
|
||||||
|
Lens has a file association with the `lens://` protocol.
|
||||||
|
This means that Lens can be opened by external programs by providing a link that has `lens` as its protocol.
|
||||||
|
Lens provides a routing mechanism that extensions can use to register custom handlers.
|
||||||
|
|
||||||
|
## Registering A Protocol Handler
|
||||||
|
|
||||||
|
The field `protocolHandlers` exists both on [`LensMainExtension`](extensions/api/classes/lensmainextension/#protocolhandlers) and on [`LensRendererExtension`](extensions/api/classes/lensrendererextension/#protocolhandlers).
|
||||||
|
This field will be iterated through every time a `lens://` request gets sent to the application.
|
||||||
|
The `pathSchema` argument must comply with the [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) package's `compileToRegex` function.
|
||||||
|
|
||||||
|
Once you have registered a handler it will be called when a user opens a link on their computer.
|
||||||
|
Handlers will be run in both `main` and `renderer` in parallel with no synchronization between the two processes.
|
||||||
|
Furthermore, both `main` and `renderer` are routed separately.
|
||||||
|
In other words, which handler is selected in either process is independent from the list of possible handlers in the other.
|
||||||
|
|
||||||
|
Example of registering a handler:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { LensMainExtension, Interface } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
function rootHandler(params: Iterface.ProtocolRouteParams) {
|
||||||
|
console.log("routed to ExampleExtension", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ExampleExtensionMain extends LensMainExtension {
|
||||||
|
protocolHandlers = [
|
||||||
|
pathSchema: "/",
|
||||||
|
handler: rootHandler,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For testing the routing of URIs the `open` (on macOS) or `xdg-open` (on most linux) CLI utilities can be used.
|
||||||
|
For the above handler, the following URI would be always routed to it:
|
||||||
|
|
||||||
|
```
|
||||||
|
open lens://extension/example-extension/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deregistering A Protocol Handler
|
||||||
|
|
||||||
|
All that is needed to deregister a handler is to remove it from the array of handlers.
|
||||||
|
|
||||||
|
## Routing Algorithm
|
||||||
|
|
||||||
|
The routing mechanism for extensions is quite straight forward.
|
||||||
|
For example consider an extension `example-extension` which is published by the `@mirantis` org.
|
||||||
|
If it were to register a handler with `"/display/:type"` as its corresponding link then we would match the following URI like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Once matched, the handler would be called with the following argument (note both `"search"` and `"pathname"` will always be defined):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"search": {
|
||||||
|
"text": "Hello"
|
||||||
|
},
|
||||||
|
"pathname": {
|
||||||
|
"type": "notification"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As the diagram above shows, the search (or query) params are not considered as part of the handler resolution.
|
||||||
|
If the URI had instead been `lens://extension/@mirantis/example-extension/display/notification/green` then a third (and optional) field will have the rest of the path.
|
||||||
|
The `tail` field would be filled with `"/green"`.
|
||||||
|
If multiple `pathSchema`'s match a given URI then the most specific handler will be called.
|
||||||
|
|
||||||
|
For example consider the following `pathSchema`'s:
|
||||||
|
|
||||||
|
1. `"/"`
|
||||||
|
1. `"/display"`
|
||||||
|
1. `"/display/:type"`
|
||||||
|
1. `"/show/:id"`
|
||||||
|
|
||||||
|
The URI sub-path `"/display"` would be routed to #2 since it is an exact match.
|
||||||
|
On the other hand, the subpath `"/display/notification"` would be routed to #3.
|
||||||
|
|
||||||
|
The URI is routed to the most specific matching `pathSchema`.
|
||||||
|
This way the `"/"` (root) `pathSchema` acts as a sort of catch all or default route if no other route matches.
|
||||||
@ -28,6 +28,28 @@ Review the [System Requirements](../supporting/requirements.md) to check if your
|
|||||||
|
|
||||||
See the [Download Lens](https://github.com/lensapp/lens/releases) page for a complete list of available installation options.
|
See the [Download Lens](https://github.com/lensapp/lens/releases) page for a complete list of available installation options.
|
||||||
|
|
||||||
|
After installing Lens manually (not using a package manager file such as `.deb` or `.rpm`) the following will need to be done to allow protocol handling.
|
||||||
|
This assumes that your linux distribution uses `xdg-open` and the `xdg-*` suite of programs for determining which application can handle custom URIs.
|
||||||
|
|
||||||
|
1. Create a file called `lens.desktop` in either `~/.local/share/applications/` or `/usr/share/applications` (if you have permissions and are installing Lens for all users).
|
||||||
|
1. That file should have the following contents, with `<path/to/executable>` being the absolute path to where you have installed the unpacked `Lens` executable:
|
||||||
|
```
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Lens
|
||||||
|
Exec=<path/to/executable> %U
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Icon=lens
|
||||||
|
StartupWMClass=Lens
|
||||||
|
Comment=Lens - The Kubernetes IDE
|
||||||
|
MimeType=x-scheme-handler/lens;
|
||||||
|
Categories=Network;
|
||||||
|
```
|
||||||
|
1. Then run the following command:
|
||||||
|
```
|
||||||
|
xdg-settings set default-url-scheme-handler lens lens.desktop
|
||||||
|
```
|
||||||
|
1. If that succeeds (exits with code `0`) then your Lens install should be set up to handle `lens://` URIs.
|
||||||
|
|
||||||
### Snap
|
### Snap
|
||||||
|
|
||||||
@ -52,4 +74,3 @@ To stay current with the Lens features, you can review the [release notes](https
|
|||||||
|
|
||||||
- [Add clusters](../clusters/adding-clusters.md)
|
- [Add clusters](../clusters/adding-clusters.md)
|
||||||
- [Watch introductory videos](./introductory-videos.md)
|
- [Watch introductory videos](./introductory-videos.md)
|
||||||
|
|
||||||
|
|||||||
17
package.json
17
package.json
@ -25,7 +25,7 @@
|
|||||||
"build:linux": "yarn run compile && electron-builder --linux --dir -c.productName=Lens",
|
"build:linux": "yarn run compile && electron-builder --linux --dir -c.productName=Lens",
|
||||||
"build:mac": "yarn run compile && electron-builder --mac --dir -c.productName=Lens",
|
"build:mac": "yarn run compile && electron-builder --mac --dir -c.productName=Lens",
|
||||||
"build:win": "yarn run compile && electron-builder --win --dir -c.productName=Lens",
|
"build:win": "yarn run compile && electron-builder --win --dir -c.productName=Lens",
|
||||||
"test": "jest --env=jsdom src $@",
|
"test": "scripts/test.sh",
|
||||||
"integration": "jest --runInBand integration",
|
"integration": "jest --runInBand integration",
|
||||||
"dist": "yarn run compile && electron-builder --publish onTag",
|
"dist": "yarn run compile && electron-builder --publish onTag",
|
||||||
"dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32",
|
"dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32",
|
||||||
@ -170,7 +170,14 @@
|
|||||||
"repo": "lens",
|
"repo": "lens",
|
||||||
"owner": "lensapp"
|
"owner": "lensapp"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"protocols": {
|
||||||
|
"name": "Lens Protocol Handler",
|
||||||
|
"schemes": [
|
||||||
|
"lens"
|
||||||
|
],
|
||||||
|
"role": "Viewer"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"lens": {
|
"lens": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
@ -187,6 +194,7 @@
|
|||||||
"@hapi/call": "^8.0.0",
|
"@hapi/call": "^8.0.0",
|
||||||
"@hapi/subtext": "^7.0.3",
|
"@hapi/subtext": "^7.0.3",
|
||||||
"@kubernetes/client-node": "^0.12.0",
|
"@kubernetes/client-node": "^0.12.0",
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
"array-move": "^3.0.0",
|
"array-move": "^3.0.0",
|
||||||
"await-lock": "^2.1.0",
|
"await-lock": "^2.1.0",
|
||||||
"byline": "^5.0.0",
|
"byline": "^5.0.0",
|
||||||
@ -213,6 +221,7 @@
|
|||||||
"mobx-observable-history": "^1.0.3",
|
"mobx-observable-history": "^1.0.3",
|
||||||
"mobx-react": "^6.2.2",
|
"mobx-react": "^6.2.2",
|
||||||
"mock-fs": "^4.12.0",
|
"mock-fs": "^4.12.0",
|
||||||
|
"moment": "^2.26.0",
|
||||||
"node-pty": "^0.9.0",
|
"node-pty": "^0.9.0",
|
||||||
"npm": "^6.14.8",
|
"npm": "^6.14.8",
|
||||||
"openid-client": "^3.15.2",
|
"openid-client": "^3.15.2",
|
||||||
@ -232,6 +241,7 @@
|
|||||||
"tar": "^6.0.5",
|
"tar": "^6.0.5",
|
||||||
"tcp-port-used": "^1.0.1",
|
"tcp-port-used": "^1.0.1",
|
||||||
"tempy": "^0.5.0",
|
"tempy": "^0.5.0",
|
||||||
|
"url-parse": "^1.4.7",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"win-ca": "^3.2.0",
|
"win-ca": "^3.2.0",
|
||||||
"winston": "^3.2.1",
|
"winston": "^3.2.1",
|
||||||
@ -289,6 +299,7 @@
|
|||||||
"@types/tempy": "^0.3.0",
|
"@types/tempy": "^0.3.0",
|
||||||
"@types/terser-webpack-plugin": "^3.0.0",
|
"@types/terser-webpack-plugin": "^3.0.0",
|
||||||
"@types/universal-analytics": "^0.4.4",
|
"@types/universal-analytics": "^0.4.4",
|
||||||
|
"@types/url-parse": "^1.4.3",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"@types/webdriverio": "^4.13.0",
|
"@types/webdriverio": "^4.13.0",
|
||||||
"@types/webpack": "^4.41.17",
|
"@types/webpack": "^4.41.17",
|
||||||
@ -325,10 +336,10 @@
|
|||||||
"jest-mock-extended": "^1.0.10",
|
"jest-mock-extended": "^1.0.10",
|
||||||
"make-plural": "^6.2.2",
|
"make-plural": "^6.2.2",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"moment": "^2.26.0",
|
|
||||||
"node-loader": "^0.6.0",
|
"node-loader": "^0.6.0",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"nodemon": "^2.0.4",
|
"nodemon": "^2.0.4",
|
||||||
|
"open": "^7.3.1",
|
||||||
"patch-package": "^6.2.2",
|
"patch-package": "^6.2.2",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"prettier": "^2.2.0",
|
"prettier": "^2.2.0",
|
||||||
|
|||||||
1
scripts/test.sh
Executable file
1
scripts/test.sh
Executable file
@ -0,0 +1 @@
|
|||||||
|
jest --env=jsdom ${1:-src}
|
||||||
@ -47,7 +47,7 @@ export async function broadcastMessage(channel: string, ...args: any[]) {
|
|||||||
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
|
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[IPC]: failed to send IPC message", { error });
|
logger.error("[IPC]: failed to send IPC message", { error: String(error) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/common/protocol-handler/error.ts
Normal file
36
src/common/protocol-handler/error.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import Url from "url-parse";
|
||||||
|
|
||||||
|
export enum RoutingErrorType {
|
||||||
|
INVALID_PROTOCOL = "invalid-protocol",
|
||||||
|
INVALID_HOST = "invalid-host",
|
||||||
|
INVALID_PATHNAME = "invalid-pathname",
|
||||||
|
NO_HANDLER = "no-handler",
|
||||||
|
NO_EXTENSION_ID = "no-ext-id",
|
||||||
|
MISSING_EXTENSION = "missing-ext",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoutingError extends Error {
|
||||||
|
/**
|
||||||
|
* Will be set if the routing error originated in an extension route table
|
||||||
|
*/
|
||||||
|
public extensionName?: string;
|
||||||
|
|
||||||
|
constructor(public type: RoutingErrorType, public url: Url) {
|
||||||
|
super("routing error");
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
switch (this.type) {
|
||||||
|
case RoutingErrorType.INVALID_HOST:
|
||||||
|
return "invalid host";
|
||||||
|
case RoutingErrorType.INVALID_PROTOCOL:
|
||||||
|
return "invalid protocol";
|
||||||
|
case RoutingErrorType.INVALID_PATHNAME:
|
||||||
|
return "invalid pathname";
|
||||||
|
case RoutingErrorType.NO_EXTENSION_ID:
|
||||||
|
return "no extension ID";
|
||||||
|
case RoutingErrorType.MISSING_EXTENSION:
|
||||||
|
return "extension not found";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/common/protocol-handler/index.ts
Normal file
2
src/common/protocol-handler/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./error";
|
||||||
|
export * from "./router";
|
||||||
218
src/common/protocol-handler/router.ts
Normal file
218
src/common/protocol-handler/router.ts
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import { match, matchPath } from "react-router";
|
||||||
|
import { countBy } from "lodash";
|
||||||
|
import { Singleton } from "../utils";
|
||||||
|
import { pathToRegexp } from "path-to-regexp";
|
||||||
|
import logger from "../../main/logger";
|
||||||
|
import Url from "url-parse";
|
||||||
|
import { RoutingError, RoutingErrorType } from "./error";
|
||||||
|
import { extensionsStore } from "../../extensions/extensions-store";
|
||||||
|
import { extensionLoader } from "../../extensions/extension-loader";
|
||||||
|
import { LensExtension } from "../../extensions/lens-extension";
|
||||||
|
import { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler-registry";
|
||||||
|
|
||||||
|
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
|
||||||
|
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
||||||
|
|
||||||
|
export const ProtocolHandlerInternal = `${ProtocolHandlerIpcPrefix}:internal`;
|
||||||
|
export const ProtocolHandlerExtension= `${ProtocolHandlerIpcPrefix}:extension`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These two names are long and cumbersome by design so as to decrease the chances
|
||||||
|
* of an extension using the same names.
|
||||||
|
*
|
||||||
|
* Though under the current (2021/01/18) implementation, these are never matched
|
||||||
|
* against in the final matching so their names are less of a concern.
|
||||||
|
*/
|
||||||
|
const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH";
|
||||||
|
const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH";
|
||||||
|
|
||||||
|
export abstract class LensProtocolRouter extends Singleton {
|
||||||
|
// Map between path schemas and the handlers
|
||||||
|
protected internalRoutes = new Map<string, RouteHandler>();
|
||||||
|
|
||||||
|
public static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
|
||||||
|
|
||||||
|
protected static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param url the parsed URL that initiated the `lens://` protocol
|
||||||
|
*/
|
||||||
|
protected _routeToInternal(url: Url): void {
|
||||||
|
this._route(Array.from(this.internalRoutes.entries()), url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* match against all matched URIs, returning either the first exact match or
|
||||||
|
* the most specific match if none are exact.
|
||||||
|
* @param routes the array of path schemas, handler pairs to match against
|
||||||
|
* @param url the url (in its current state)
|
||||||
|
*/
|
||||||
|
protected _findMatchingRoute(routes: [string, RouteHandler][], url: Url): null | [match<Record<string, string>>, RouteHandler] {
|
||||||
|
const matches: [match<Record<string, string>>, RouteHandler][] = [];
|
||||||
|
|
||||||
|
for (const [schema, handler] of routes) {
|
||||||
|
const match = matchPath(url.pathname, { path: schema });
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefer an exact match
|
||||||
|
if (match.isExact) {
|
||||||
|
return [match, handler];
|
||||||
|
}
|
||||||
|
|
||||||
|
matches.push([match, handler]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no exact match pick the one that is the most specific
|
||||||
|
return matches.sort(([a], [b]) => compareMatches(a, b))[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find the most specific matching handler and call it
|
||||||
|
* @param routes the array of (path schemas, handler) paris to match against
|
||||||
|
* @param url the url (in its current state)
|
||||||
|
*/
|
||||||
|
protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void {
|
||||||
|
const route = this._findMatchingRoute(routes, url);
|
||||||
|
|
||||||
|
if (!route) {
|
||||||
|
const data: Record<string, string> = { url: url.toString() };
|
||||||
|
|
||||||
|
if (extensionName) {
|
||||||
|
data.extensionName = extensionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return void logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [match, handler] = route;
|
||||||
|
|
||||||
|
const params: RouteParams = {
|
||||||
|
pathname: match.params,
|
||||||
|
search: url.query,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!match.isExact) {
|
||||||
|
params.tail = url.pathname.slice(match.url.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to find the matching LensExtension instance
|
||||||
|
*
|
||||||
|
* Note: this needs to be async so that `main`'s overloaded version can also be async
|
||||||
|
* @param url the protocol request URI that was "open"-ed
|
||||||
|
* @returns either the found name or the instance of `LensExtension`
|
||||||
|
*/
|
||||||
|
protected async _findMatchingExtensionByName(url: Url): Promise<LensExtension | string> {
|
||||||
|
interface ExtensionUrlMatch {
|
||||||
|
[EXTENSION_PUBLISHER_MATCH]: string;
|
||||||
|
[EXTENSION_NAME_MATCH]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = matchPath<ExtensionUrlMatch>(url.pathname, LensProtocolRouter.ExtensionUrlSchema);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new RoutingError(RoutingErrorType.NO_EXTENSION_ID, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
|
||||||
|
const name = [publisher, partialName].filter(Boolean).join("/");
|
||||||
|
|
||||||
|
const extension = extensionLoader.userExtensionsByName.get(name);
|
||||||
|
|
||||||
|
if (!extension) {
|
||||||
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`);
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extensionsStore.isEnabled(extension.id)) {
|
||||||
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched`);
|
||||||
|
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a matching extension by the first one or two path segments of `url` and then try to `_route`
|
||||||
|
* its correspondingly registered handlers.
|
||||||
|
*
|
||||||
|
* If no handlers are found or the extension is not enabled then `_missingHandlers` is called before
|
||||||
|
* checking if more handlers have been added.
|
||||||
|
*
|
||||||
|
* Note: this function modifies its argument, do not reuse
|
||||||
|
* @param url the protocol request URI that was "open"-ed
|
||||||
|
*/
|
||||||
|
protected async _routeToExtension(url: Url): Promise<void> {
|
||||||
|
const extension = await this._findMatchingExtensionByName(url);
|
||||||
|
|
||||||
|
if (typeof extension === "string") {
|
||||||
|
// failed to find an extension, it returned its name
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the extension name from the path name so we don't need to match on it anymore
|
||||||
|
url.set("pathname", url.pathname.slice(extension.name.length + 1));
|
||||||
|
|
||||||
|
const handlers = extension
|
||||||
|
.protocolHandlers
|
||||||
|
.map<[string, RouteHandler]>(({ pathSchema, handler }) => [pathSchema, handler]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._route(handlers, url, extension.name);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RoutingError) {
|
||||||
|
error.extensionName = extension.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a handler under the `lens://app` tree of routing.
|
||||||
|
* @param pathSchema the URI path schema to match against for this handler
|
||||||
|
* @param handler a function that will be called if a protocol path matches
|
||||||
|
*/
|
||||||
|
public addInternalHandler(urlSchema: string, handler: RouteHandler): void {
|
||||||
|
pathToRegexp(urlSchema); // verify now that the schema is valid
|
||||||
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: internal registering ${urlSchema}`);
|
||||||
|
this.internalRoutes.set(urlSchema, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an internal protocol handler.
|
||||||
|
* @param pathSchema the path schema that the handler was registered under
|
||||||
|
*/
|
||||||
|
public removeInternalHandler(urlSchema: string): void {
|
||||||
|
this.internalRoutes.delete(urlSchema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a comparison function for `array.sort(...)`. Sort order should be most path
|
||||||
|
* parts to least path parts.
|
||||||
|
* @param a the left side to compare
|
||||||
|
* @param b the right side to compare
|
||||||
|
*/
|
||||||
|
function compareMatches<T>(a: match<T>, b: match<T>): number {
|
||||||
|
if (a.path === "/") {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.path === "/") {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return countBy(b.path)["/"] - countBy(a.path)["/"];
|
||||||
|
}
|
||||||
@ -1,8 +1,19 @@
|
|||||||
|
import { AbortController } from "abort-controller";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a promise that will be resolved after at least `timeout` ms have
|
* Return a promise that will be resolved after at least `timeout` ms have
|
||||||
* passed
|
* passed. If `failFast` is provided then the promise is also resolved if it has
|
||||||
|
* been aborted.
|
||||||
* @param timeout The number of milliseconds before resolving
|
* @param timeout The number of milliseconds before resolving
|
||||||
|
* @param failFast An abort controller instance to cause the delay to short-circuit
|
||||||
*/
|
*/
|
||||||
export function delay(timeout = 1000): Promise<void> {
|
export function delay(timeout = 1000, failFast?: AbortController): Promise<void> {
|
||||||
return new Promise(resolve => setTimeout(resolve, timeout));
|
return new Promise(resolve => {
|
||||||
|
const timeoutId = setTimeout(resolve, timeout);
|
||||||
|
|
||||||
|
failFast?.signal.addEventListener("abort", () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,4 +18,4 @@ export * from "./openExternal";
|
|||||||
export * from "./downloadFile";
|
export * from "./downloadFile";
|
||||||
export * from "./escapeRegExp";
|
export * from "./escapeRegExp";
|
||||||
export * from "./tar";
|
export * from "./tar";
|
||||||
export * from "./delay";
|
export * from "./type-narrowing";
|
||||||
|
|||||||
13
src/common/utils/type-narrowing.ts
Normal file
13
src/common/utils/type-narrowing.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Narrows `val` to include the property `key` (if true is returned)
|
||||||
|
* @param val The object to be tested
|
||||||
|
* @param key The key to test if it is present on the object
|
||||||
|
*/
|
||||||
|
export function hasOwnProperty<V extends object, K extends PropertyKey>(val: V, key: K): val is (V & { [key in K]: unknown }) {
|
||||||
|
// this call syntax is for when `val` was created by `Object.create(null)`
|
||||||
|
return Object.prototype.hasOwnProperty.call(val, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasOwnProperties<V extends object, K extends PropertyKey>(val: V, ...keys: K[]): val is (V & { [key in K]: unknown}) {
|
||||||
|
return keys.every(key => hasOwnProperty(val, key));
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ import type { LensRendererExtension } from "./lens-renderer-extension";
|
|||||||
import * as registries from "./registries";
|
import * as registries from "./registries";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
// lazy load so that we get correct userData
|
|
||||||
export function extensionPackagesRoot() {
|
export function extensionPackagesRoot() {
|
||||||
return path.join((app || remote.app).getPath("userData"));
|
return path.join((app || remote.app).getPath("userData"));
|
||||||
}
|
}
|
||||||
@ -52,6 +52,30 @@ export class ExtensionLoader {
|
|||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get userExtensionsByName(): Map<string, LensExtension> {
|
||||||
|
const extensions = new Map();
|
||||||
|
|
||||||
|
for (const [, val] of this.instances.toJS()) {
|
||||||
|
if (val.isBundled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions.set(val.manifest.name, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
getExtensionByName(name: string): LensExtension | null {
|
||||||
|
for (const [, val] of this.instances) {
|
||||||
|
if (val.name === name) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Transform userExtensions to a state object for storing into ExtensionsStore
|
// Transform userExtensions to a state object for storing into ExtensionsStore
|
||||||
@computed get storeState() {
|
@computed get storeState() {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
@ -102,7 +126,6 @@ export class ExtensionLoader {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
|
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeExtension(lensExtensionId: LensExtensionId) {
|
removeExtension(lensExtensionId: LensExtensionId) {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
|||||||
|
|
||||||
protected state = observable.map<LensExtensionId, LensExtensionState>();
|
protected state = observable.map<LensExtensionId, LensExtensionState>();
|
||||||
|
|
||||||
isEnabled(extId: LensExtensionId) {
|
isEnabled(extId: LensExtensionId): boolean {
|
||||||
const state = this.state.get(extId);
|
const state = this.state.get(extId);
|
||||||
|
|
||||||
// By default false, so that copied extensions are disabled by default.
|
// By default false, so that copied extensions are disabled by default.
|
||||||
|
|||||||
@ -6,3 +6,4 @@ export type { KubeObjectStatusRegistration } from "../registries/kube-object-sta
|
|||||||
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry";
|
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry";
|
||||||
export type { PageMenuRegistration, ClusterPageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry";
|
export type { PageMenuRegistration, ClusterPageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry";
|
||||||
export type { StatusBarRegistration } from "../registries/status-bar-registry";
|
export type { StatusBarRegistration } from "../registries/status-bar-registry";
|
||||||
|
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler-registry";
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { InstalledExtension } from "./extension-discovery";
|
|||||||
import { action, observable, reaction } from "mobx";
|
import { action, observable, reaction } from "mobx";
|
||||||
import { filesystemProvisionerStore } from "../main/extension-filesystem";
|
import { filesystemProvisionerStore } from "../main/extension-filesystem";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
|
import { ProtocolHandlerRegistration } from "./registries/protocol-handler-registry";
|
||||||
|
|
||||||
export type LensExtensionId = string; // path to manifest (package.json)
|
export type LensExtensionId = string; // path to manifest (package.json)
|
||||||
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
|
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
|
||||||
@ -21,6 +22,8 @@ export class LensExtension {
|
|||||||
readonly manifestPath: string;
|
readonly manifestPath: string;
|
||||||
readonly isBundled: boolean;
|
readonly isBundled: boolean;
|
||||||
|
|
||||||
|
protocolHandlers: ProtocolHandlerRegistration[] = [];
|
||||||
|
|
||||||
@observable private isEnabled = false;
|
@observable private isEnabled = false;
|
||||||
|
|
||||||
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
|
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
|
||||||
|
|||||||
@ -31,8 +31,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
/**
|
/**
|
||||||
* Defines if extension is enabled for a given cluster. Defaults to `true`.
|
* Defines if extension is enabled for a given cluster. Defaults to `true`.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
|
||||||
async isEnabledForCluster(cluster: Cluster): Promise<Boolean> {
|
async isEnabledForCluster(cluster: Cluster): Promise<Boolean> {
|
||||||
return true;
|
return (void cluster) || true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/extensions/registries/protocol-handler-registry.ts
Normal file
44
src/extensions/registries/protocol-handler-registry.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* ProtocolHandlerRegistration is the data required for an extension to register
|
||||||
|
* a handler to a specific path or dynamic path.
|
||||||
|
*/
|
||||||
|
export interface ProtocolHandlerRegistration {
|
||||||
|
pathSchema: string;
|
||||||
|
handler: RouteHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collection of the dynamic parts of a URI which initiated a `lens://`
|
||||||
|
* protocol request
|
||||||
|
*/
|
||||||
|
export interface RouteParams {
|
||||||
|
/**
|
||||||
|
* the parts of the URI query string
|
||||||
|
*/
|
||||||
|
search: Record<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the matching parts of the path. The dynamic parts of the URI path.
|
||||||
|
*/
|
||||||
|
pathname: Record<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if the most specific path schema that is matched does not cover the whole
|
||||||
|
* of the URI's path. Then this field will be set to the remaining path
|
||||||
|
* segments.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* If the path schema `/landing/:type` is the matched schema for the URI
|
||||||
|
* `/landing/soft/easy` then this field will be set to `"/easy"`.
|
||||||
|
*/
|
||||||
|
tail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RouteHandler represents the function signature of the handler function for
|
||||||
|
* `lens://` protocol routing.
|
||||||
|
*/
|
||||||
|
export interface RouteHandler {
|
||||||
|
(params: RouteParams): void;
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import { isDevelopment, isTestEnv } from "../common/vars";
|
|||||||
import { delay } from "../common/utils";
|
import { delay } from "../common/utils";
|
||||||
import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
|
import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
|
import { once } from "lodash";
|
||||||
|
|
||||||
let installVersion: null | string = null;
|
let installVersion: null | string = null;
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: Upd
|
|||||||
* starts the automatic update checking
|
* starts the automatic update checking
|
||||||
* @param interval milliseconds between interval to check on, defaults to 24h
|
* @param interval milliseconds between interval to check on, defaults to 24h
|
||||||
*/
|
*/
|
||||||
export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
|
export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24): void {
|
||||||
if (isDevelopment || isTestEnv) {
|
if (isDevelopment || isTestEnv) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -83,7 +84,7 @@ export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
helper();
|
helper();
|
||||||
}
|
});
|
||||||
|
|
||||||
export async function checkForUpdates(): Promise<void> {
|
export async function checkForUpdates(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import "../common/system-ca";
|
|||||||
import "../common/prometheus-providers";
|
import "../common/prometheus-providers";
|
||||||
import * as Mobx from "mobx";
|
import * as Mobx from "mobx";
|
||||||
import * as LensExtensions from "../extensions/core-api";
|
import * as LensExtensions from "../extensions/core-api";
|
||||||
import { app, autoUpdater, dialog, powerMonitor } from "electron";
|
import { app, autoUpdater, ipcMain, dialog, powerMonitor } from "electron";
|
||||||
import { appName } from "../common/vars";
|
import { appName } from "../common/vars";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { LensProxy } from "./lens-proxy";
|
import { LensProxy } from "./lens-proxy";
|
||||||
@ -25,6 +25,7 @@ import { InstalledExtension, extensionDiscovery } from "../extensions/extension-
|
|||||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||||
import { installDeveloperTools } from "./developer-tools";
|
import { installDeveloperTools } from "./developer-tools";
|
||||||
import { filesystemProvisionerStore } from "./extension-filesystem";
|
import { filesystemProvisionerStore } from "./extension-filesystem";
|
||||||
|
import { LensProtocolRouterMain } from "./protocol-handler";
|
||||||
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
||||||
import { bindBroadcastHandlers } from "../common/ipc";
|
import { bindBroadcastHandlers } from "../common/ipc";
|
||||||
import { startUpdateChecking } from "./app-updater";
|
import { startUpdateChecking } from "./app-updater";
|
||||||
@ -37,30 +38,54 @@ let windowManager: WindowManager;
|
|||||||
|
|
||||||
app.setName(appName);
|
app.setName(appName);
|
||||||
|
|
||||||
|
logger.info("📟 Setting as Lens as protocol client for lens://");
|
||||||
|
|
||||||
|
if (app.setAsDefaultProtocolClient("lens")) {
|
||||||
|
logger.info("📟 succeeded ✅");
|
||||||
|
} else {
|
||||||
|
logger.info("📟 failed ❗");
|
||||||
|
}
|
||||||
|
|
||||||
if (!process.env.CICD) {
|
if (!process.env.CICD) {
|
||||||
app.setPath("userData", workingDir);
|
app.setPath("userData", workingDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.LENS_DISABLE_GPU) {
|
||||||
|
app.disableHardwareAcceleration();
|
||||||
|
}
|
||||||
|
|
||||||
mangleProxyEnv();
|
mangleProxyEnv();
|
||||||
|
|
||||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
|
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceLock = app.requestSingleInstanceLock();
|
if (!app.requestSingleInstanceLock()) {
|
||||||
|
|
||||||
if (!instanceLock) {
|
|
||||||
app.exit();
|
app.exit();
|
||||||
|
} else {
|
||||||
|
const lprm = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
|
||||||
|
|
||||||
|
for (const arg of process.argv) {
|
||||||
|
if (arg.toLowerCase().startsWith("lens://")) {
|
||||||
|
lprm.route(arg)
|
||||||
|
.catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on("second-instance", (event, argv) => {
|
||||||
|
const lprm = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
|
||||||
|
|
||||||
|
for (const arg of argv) {
|
||||||
|
if (arg.toLowerCase().startsWith("lens://")) {
|
||||||
|
lprm.route(arg)
|
||||||
|
.catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.on("second-instance", () => {
|
|
||||||
windowManager?.ensureMainWindow();
|
windowManager?.ensureMainWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.env.LENS_DISABLE_GPU) {
|
|
||||||
app.disableHardwareAcceleration();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on("ready", async () => {
|
app.on("ready", async () => {
|
||||||
logger.info(`🚀 Starting Lens from "${workingDir}"`);
|
logger.info(`🚀 Starting Lens from "${workingDir}"`);
|
||||||
logger.info("🐚 Syncing shell environment");
|
logger.info("🐚 Syncing shell environment");
|
||||||
@ -128,7 +153,19 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
logger.info("🖥️ Starting WindowManager");
|
logger.info("🖥️ Starting WindowManager");
|
||||||
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
|
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
|
||||||
windowManager.whenLoaded.then(() => startUpdateChecking());
|
|
||||||
|
ipcMain.on("renderer:loaded", () => {
|
||||||
|
startUpdateChecking();
|
||||||
|
LensProtocolRouterMain
|
||||||
|
.getInstance<LensProtocolRouterMain>()
|
||||||
|
.rendererLoaded = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
extensionLoader.whenLoaded.then(() => {
|
||||||
|
LensProtocolRouterMain
|
||||||
|
.getInstance<LensProtocolRouterMain>()
|
||||||
|
.extensionsLoaded = true;
|
||||||
|
});
|
||||||
|
|
||||||
logger.info("🧩 Initializing extensions");
|
logger.info("🧩 Initializing extensions");
|
||||||
|
|
||||||
@ -174,8 +211,8 @@ let blockQuit = true;
|
|||||||
|
|
||||||
autoUpdater.on("before-quit-for-update", () => blockQuit = false);
|
autoUpdater.on("before-quit-for-update", () => blockQuit = false);
|
||||||
|
|
||||||
// Quit app on Cmd+Q (MacOS)
|
|
||||||
app.on("will-quit", (event) => {
|
app.on("will-quit", (event) => {
|
||||||
|
// Quit app on Cmd+Q (MacOS)
|
||||||
logger.info("APP:QUIT");
|
logger.info("APP:QUIT");
|
||||||
appEventBus.emit({name: "app", action: "close"});
|
appEventBus.emit({name: "app", action: "close"});
|
||||||
|
|
||||||
@ -188,6 +225,16 @@ app.on("will-quit", (event) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on("open-url", (event, rawUrl) => {
|
||||||
|
// lens:// protocol handler
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
LensProtocolRouterMain
|
||||||
|
.getInstance<LensProtocolRouterMain>()
|
||||||
|
.route(rawUrl)
|
||||||
|
.catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl }));
|
||||||
|
});
|
||||||
|
|
||||||
// Extensions-api runtime exports
|
// Extensions-api runtime exports
|
||||||
export const LensExtensionsApi = {
|
export const LensExtensionsApi = {
|
||||||
...LensExtensions,
|
...LensExtensions,
|
||||||
|
|||||||
259
src/main/protocol-handler/__test__/router.test.ts
Normal file
259
src/main/protocol-handler/__test__/router.test.ts
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
import { LensProtocolRouterMain } from "../router";
|
||||||
|
import { noop } from "../../../common/utils";
|
||||||
|
import { extensionsStore } from "../../../extensions/extensions-store";
|
||||||
|
import { extensionLoader } from "../../../extensions/extension-loader";
|
||||||
|
import * as uuid from "uuid";
|
||||||
|
import { LensMainExtension } from "../../../extensions/core-api";
|
||||||
|
import { broadcastMessage } from "../../../common/ipc";
|
||||||
|
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
|
||||||
|
|
||||||
|
jest.mock("../../../common/ipc");
|
||||||
|
|
||||||
|
function throwIfDefined(val: any): void {
|
||||||
|
if (val != null) {
|
||||||
|
throw val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("protocol router tests", () => {
|
||||||
|
let lpr: LensProtocolRouterMain;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
(extensionsStore as any).state.clear();
|
||||||
|
(extensionLoader as any).instances.clear();
|
||||||
|
LensProtocolRouterMain.resetInstance();
|
||||||
|
lpr = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
|
||||||
|
lpr.extensionsLoaded = true;
|
||||||
|
lpr.rendererLoaded = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw on non-lens URLS", async () => {
|
||||||
|
try {
|
||||||
|
expect(await lpr.route("https://google.ca")).toBeUndefined();
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw when host not internal or extension", async () => {
|
||||||
|
try {
|
||||||
|
expect(await lpr.route("lens://foobar")).toBeUndefined();
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not throw when has valid host", async () => {
|
||||||
|
const extId = uuid.v4();
|
||||||
|
const ext = new LensMainExtension({
|
||||||
|
id: extId,
|
||||||
|
manifestPath: "/foo/bar",
|
||||||
|
manifest: {
|
||||||
|
name: "@mirantis/minikube",
|
||||||
|
version: "0.1.1",
|
||||||
|
},
|
||||||
|
isBundled: false,
|
||||||
|
isEnabled: true,
|
||||||
|
absolutePath: "/foo/bar",
|
||||||
|
});
|
||||||
|
|
||||||
|
ext.protocolHandlers.push({
|
||||||
|
pathSchema: "/",
|
||||||
|
handler: noop,
|
||||||
|
});
|
||||||
|
|
||||||
|
(extensionLoader as any).instances.set(extId, ext);
|
||||||
|
(extensionsStore as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" });
|
||||||
|
|
||||||
|
lpr.addInternalHandler("/", noop);
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(await lpr.route("lens://app")).toBeUndefined();
|
||||||
|
} catch (error) {
|
||||||
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(await lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
|
||||||
|
} catch (error) {
|
||||||
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(broadcastMessage).toHaveBeenNthCalledWith(1, ProtocolHandlerInternal, "lens://app");
|
||||||
|
expect(broadcastMessage).toHaveBeenNthCalledWith(2, ProtocolHandlerExtension, "lens://extension/@mirantis/minikube");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call handler if matches", async () => {
|
||||||
|
let called = false;
|
||||||
|
|
||||||
|
lpr.addInternalHandler("/page", () => { called = true; });
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(await lpr.route("lens://app/page")).toBeUndefined();
|
||||||
|
} catch (error) {
|
||||||
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(called).toBe(true);
|
||||||
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call most exact handler", async () => {
|
||||||
|
let called: any = 0;
|
||||||
|
|
||||||
|
lpr.addInternalHandler("/page", () => { called = 1; });
|
||||||
|
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(await lpr.route("lens://app/page/foo")).toBeUndefined();
|
||||||
|
} catch (error) {
|
||||||
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(called).toBe("foo");
|
||||||
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call most exact handler for an extension", async () => {
|
||||||
|
let called: any = 0;
|
||||||
|
|
||||||
|
const extId = uuid.v4();
|
||||||
|
const ext = new LensMainExtension({
|
||||||
|
id: extId,
|
||||||
|
manifestPath: "/foo/bar",
|
||||||
|
manifest: {
|
||||||
|
name: "@foobar/icecream",
|
||||||
|
version: "0.1.1",
|
||||||
|
},
|
||||||
|
isBundled: false,
|
||||||
|
isEnabled: true,
|
||||||
|
absolutePath: "/foo/bar",
|
||||||
|
});
|
||||||
|
|
||||||
|
ext.protocolHandlers
|
||||||
|
.push({
|
||||||
|
pathSchema: "/page",
|
||||||
|
handler: () => { called = 1; },
|
||||||
|
}, {
|
||||||
|
pathSchema: "/page/:id",
|
||||||
|
handler: params => { called = params.pathname.id; },
|
||||||
|
});
|
||||||
|
|
||||||
|
(extensionLoader as any).instances.set(extId, ext);
|
||||||
|
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
||||||
|
} catch (error) {
|
||||||
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(called).toBe("foob");
|
||||||
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/@foobar/icecream/page/foob");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with non-org extensions", async () => {
|
||||||
|
let called: any = 0;
|
||||||
|
|
||||||
|
{
|
||||||
|
const extId = uuid.v4();
|
||||||
|
const ext = new LensMainExtension({
|
||||||
|
id: extId,
|
||||||
|
manifestPath: "/foo/bar",
|
||||||
|
manifest: {
|
||||||
|
name: "@foobar/icecream",
|
||||||
|
version: "0.1.1",
|
||||||
|
},
|
||||||
|
isBundled: false,
|
||||||
|
isEnabled: true,
|
||||||
|
absolutePath: "/foo/bar",
|
||||||
|
});
|
||||||
|
|
||||||
|
ext.protocolHandlers
|
||||||
|
.push({
|
||||||
|
pathSchema: "/page/:id",
|
||||||
|
handler: params => { called = params.pathname.id; },
|
||||||
|
});
|
||||||
|
|
||||||
|
(extensionLoader as any).instances.set(extId, ext);
|
||||||
|
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const extId = uuid.v4();
|
||||||
|
const ext = new LensMainExtension({
|
||||||
|
id: extId,
|
||||||
|
manifestPath: "/foo/bar",
|
||||||
|
manifest: {
|
||||||
|
name: "icecream",
|
||||||
|
version: "0.1.1",
|
||||||
|
},
|
||||||
|
isBundled: false,
|
||||||
|
isEnabled: true,
|
||||||
|
absolutePath: "/foo/bar",
|
||||||
|
});
|
||||||
|
|
||||||
|
ext.protocolHandlers
|
||||||
|
.push({
|
||||||
|
pathSchema: "/page",
|
||||||
|
handler: () => { called = 1; },
|
||||||
|
});
|
||||||
|
|
||||||
|
(extensionLoader as any).instances.set(extId, ext);
|
||||||
|
(extensionsStore as any).state.set(extId, { enabled: true, name: "icecream" });
|
||||||
|
}
|
||||||
|
|
||||||
|
(extensionsStore as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" });
|
||||||
|
(extensionsStore as any).state.set("icecream", { enabled: true, name: "icecream" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
||||||
|
} catch (error) {
|
||||||
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(called).toBe(1);
|
||||||
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/icecream/page");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if urlSchema is invalid", () => {
|
||||||
|
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call most exact handler with 3 found handlers", async () => {
|
||||||
|
let called: any = 0;
|
||||||
|
|
||||||
|
lpr.addInternalHandler("/", () => { called = 2; });
|
||||||
|
lpr.addInternalHandler("/page", () => { called = 1; });
|
||||||
|
lpr.addInternalHandler("/page/foo", () => { called = 3; });
|
||||||
|
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
||||||
|
} catch (error) {
|
||||||
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(called).toBe(3);
|
||||||
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call most exact handler with 2 found handlers", async () => {
|
||||||
|
let called: any = 0;
|
||||||
|
|
||||||
|
lpr.addInternalHandler("/", () => { called = 2; });
|
||||||
|
lpr.addInternalHandler("/page", () => { called = 1; });
|
||||||
|
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
||||||
|
} catch (error) {
|
||||||
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(called).toBe(1);
|
||||||
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat");
|
||||||
|
});
|
||||||
|
});
|
||||||
1
src/main/protocol-handler/index.ts
Normal file
1
src/main/protocol-handler/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./router";
|
||||||
112
src/main/protocol-handler/router.ts
Normal file
112
src/main/protocol-handler/router.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import logger from "../logger";
|
||||||
|
import * as proto from "../../common/protocol-handler";
|
||||||
|
import Url from "url-parse";
|
||||||
|
import { LensExtension } from "../../extensions/lens-extension";
|
||||||
|
import { broadcastMessage } from "../../common/ipc";
|
||||||
|
import { observable, when } from "mobx";
|
||||||
|
|
||||||
|
export interface FallbackHandler {
|
||||||
|
(name: string): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||||
|
private missingExtensionHandlers: FallbackHandler[] = [];
|
||||||
|
|
||||||
|
@observable rendererLoaded = false;
|
||||||
|
@observable extensionsLoaded = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the most specific registered handler, if it exists, and invoke it.
|
||||||
|
*
|
||||||
|
* This will send an IPC message to the renderer router to do the same
|
||||||
|
* in the renderer.
|
||||||
|
*/
|
||||||
|
public async route(rawUrl: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const url = new Url(rawUrl, true);
|
||||||
|
|
||||||
|
if (url.protocol.toLowerCase() !== "lens:") {
|
||||||
|
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`);
|
||||||
|
|
||||||
|
switch (url.host) {
|
||||||
|
case "app":
|
||||||
|
return this._routeToInternal(url);
|
||||||
|
case "extension":
|
||||||
|
await when(() => this.extensionsLoaded);
|
||||||
|
|
||||||
|
return this._routeToExtension(url);
|
||||||
|
default:
|
||||||
|
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_HOST, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof proto.RoutingError) {
|
||||||
|
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url });
|
||||||
|
} else {
|
||||||
|
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { rawUrl });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _executeMissingExtensionHandlers(extensionName: string): Promise<boolean> {
|
||||||
|
for (const handler of this.missingExtensionHandlers) {
|
||||||
|
if (await handler(extensionName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _findMatchingExtensionByName(url: Url): Promise<LensExtension | string> {
|
||||||
|
const firstAttempt = await super._findMatchingExtensionByName(url);
|
||||||
|
|
||||||
|
if (typeof firstAttempt !== "string") {
|
||||||
|
return firstAttempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this._executeMissingExtensionHandlers(firstAttempt)) {
|
||||||
|
return super._findMatchingExtensionByName(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _routeToInternal(url: Url): Promise<void> {
|
||||||
|
const rawUrl = url.toString(); // for sending to renderer
|
||||||
|
|
||||||
|
super._routeToInternal(url);
|
||||||
|
|
||||||
|
await when(() => this.rendererLoaded);
|
||||||
|
|
||||||
|
return broadcastMessage(proto.ProtocolHandlerInternal, rawUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _routeToExtension(url: Url): Promise<void> {
|
||||||
|
const rawUrl = url.toString(); // for sending to renderer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This needs to be done first, so that the missing extension handlers can
|
||||||
|
* be called before notifying the renderer.
|
||||||
|
*
|
||||||
|
* Note: this needs to clone the url because _routeToExtension modifies its
|
||||||
|
* argument.
|
||||||
|
*/
|
||||||
|
await super._routeToExtension(new Url(url.toString(), true));
|
||||||
|
await when(() => this.rendererLoaded);
|
||||||
|
|
||||||
|
return broadcastMessage(proto.ProtocolHandlerExtension, rawUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a function to the list which will be sequentially called if an extension
|
||||||
|
* is not found while routing to the extensions
|
||||||
|
* @param handler A function that tries to find an extension
|
||||||
|
*/
|
||||||
|
public addMissingExtensionHandler(handler: FallbackHandler): void {
|
||||||
|
this.missingExtensionHandlers.push(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ClusterId } from "../common/cluster-store";
|
import type { ClusterId } from "../common/cluster-store";
|
||||||
import { observable, when } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { app, BrowserWindow, dialog, shell, webContents } from "electron";
|
import { app, BrowserWindow, dialog, shell, webContents } from "electron";
|
||||||
import windowStateKeeper from "electron-window-state";
|
import windowStateKeeper from "electron-window-state";
|
||||||
import { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
@ -16,9 +16,6 @@ export class WindowManager extends Singleton {
|
|||||||
protected windowState: windowStateKeeper.State;
|
protected windowState: windowStateKeeper.State;
|
||||||
protected disposers: Record<string, Function> = {};
|
protected disposers: Record<string, Function> = {};
|
||||||
|
|
||||||
@observable mainViewInitiallyLoaded = false;
|
|
||||||
whenLoaded = when(() => this.mainViewInitiallyLoaded);
|
|
||||||
|
|
||||||
@observable activeClusterId: ClusterId;
|
@observable activeClusterId: ClusterId;
|
||||||
|
|
||||||
constructor(protected proxyPort: number) {
|
constructor(protected proxyPort: number) {
|
||||||
@ -104,7 +101,6 @@ export class WindowManager extends Singleton {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
appEventBus.emit({ name: "app", action: "start" });
|
appEventBus.emit({ name: "app", action: "start" });
|
||||||
}, 1000);
|
}, 1000);
|
||||||
this.mainViewInitiallyLoaded = true;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
dialog.showErrorBox("ERROR!", err.toString());
|
dialog.showErrorBox("ERROR!", err.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,12 +12,15 @@ import { ConfirmDialog } from "./components/confirm-dialog";
|
|||||||
import { extensionLoader } from "../extensions/extension-loader";
|
import { extensionLoader } from "../extensions/extension-loader";
|
||||||
import { broadcastMessage } from "../common/ipc";
|
import { broadcastMessage } from "../common/ipc";
|
||||||
import { CommandContainer } from "./components/command-palette/command-container";
|
import { CommandContainer } from "./components/command-palette/command-container";
|
||||||
|
import { LensProtocolRouterRenderer } from "./protocol-handler/router";
|
||||||
import { registerIpcHandlers } from "./ipc";
|
import { registerIpcHandlers } from "./ipc";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class LensApp extends React.Component {
|
export class LensApp extends React.Component {
|
||||||
static async init() {
|
static async init() {
|
||||||
extensionLoader.loadOnClusterManagerRenderer();
|
extensionLoader.loadOnClusterManagerRenderer();
|
||||||
|
LensProtocolRouterRenderer.getInstance<LensProtocolRouterRenderer>().init();
|
||||||
window.addEventListener("offline", () => {
|
window.addEventListener("offline", () => {
|
||||||
broadcastMessage("network:offline");
|
broadcastMessage("network:offline");
|
||||||
});
|
});
|
||||||
@ -26,6 +29,7 @@ export class LensApp extends React.Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
registerIpcHandlers();
|
registerIpcHandlers();
|
||||||
|
ipcRenderer.send("renderer:loaded");
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
// Navigation (renderer)
|
// Navigation (renderer)
|
||||||
|
|
||||||
import { bindEvents } from "./events";
|
import { bindEvents } from "./events";
|
||||||
|
import { bindProtocolHandlers } from "./protocol-handlers";
|
||||||
|
|
||||||
export * from "./history";
|
export * from "./history";
|
||||||
export * from "./helpers";
|
export * from "./helpers";
|
||||||
|
|
||||||
bindEvents();
|
bindEvents();
|
||||||
|
bindProtocolHandlers();
|
||||||
|
|||||||
10
src/renderer/navigation/protocol-handlers.ts
Normal file
10
src/renderer/navigation/protocol-handlers.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { LensProtocolRouterRenderer } from "../protocol-handler/router";
|
||||||
|
import { navigate } from "./helpers";
|
||||||
|
|
||||||
|
export function bindProtocolHandlers() {
|
||||||
|
const lprr = LensProtocolRouterRenderer.getInstance<LensProtocolRouterRenderer>();
|
||||||
|
|
||||||
|
lprr.addInternalHandler("/preferences", () => {
|
||||||
|
navigate("/preferences");
|
||||||
|
});
|
||||||
|
}
|
||||||
1
src/renderer/protocol-handler/index.ts
Normal file
1
src/renderer/protocol-handler/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./router.ts";
|
||||||
40
src/renderer/protocol-handler/router.ts
Normal file
40
src/renderer/protocol-handler/router.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
import * as proto from "../../common/protocol-handler";
|
||||||
|
import logger from "../../main/logger";
|
||||||
|
import Url from "url-parse";
|
||||||
|
import { autobind } from "../utils";
|
||||||
|
|
||||||
|
export class LensProtocolRouterRenderer extends proto.LensProtocolRouter {
|
||||||
|
/**
|
||||||
|
* This function is needed to be called early on in the renderers lifetime.
|
||||||
|
*/
|
||||||
|
public init(): void {
|
||||||
|
ipcRenderer
|
||||||
|
.on(proto.ProtocolHandlerInternal, this.ipcInternalHandler)
|
||||||
|
.on(proto.ProtocolHandlerExtension, this.ipcExtensionHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
private ipcInternalHandler(event: Electron.IpcRendererEvent, ...args: any[]): void {
|
||||||
|
if (args.length !== 1) {
|
||||||
|
return void logger.warn(`${proto.LensProtocolRouter.LoggingPrefix}: unexpected number of args`, { args });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rawUrl] = args;
|
||||||
|
const url = new Url(rawUrl, true);
|
||||||
|
|
||||||
|
this._routeToInternal(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind()
|
||||||
|
private ipcExtensionHandler(event: Electron.IpcRendererEvent, ...args: any[]): void {
|
||||||
|
if (args.length !== 1) {
|
||||||
|
return void logger.warn(`${proto.LensProtocolRouter.LoggingPrefix}: unexpected number of args`, { args });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rawUrl] = args;
|
||||||
|
const url = new Url(rawUrl, true);
|
||||||
|
|
||||||
|
this._routeToExtension(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
// Common usage utils & helpers
|
// Common usage utils & helpers
|
||||||
|
|
||||||
export const isElectron = !!navigator.userAgent.match(/Electron/);
|
|
||||||
|
|
||||||
export * from "../../common/utils";
|
export * from "../../common/utils";
|
||||||
|
|
||||||
export * from "./cssVar";
|
export * from "./cssVar";
|
||||||
|
|||||||
27
yarn.lock
27
yarn.lock
@ -1771,6 +1771,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/universal-analytics/-/universal-analytics-0.4.4.tgz#496a52b92b599a0112bec7c12414062de6ea8449"
|
resolved "https://registry.yarnpkg.com/@types/universal-analytics/-/universal-analytics-0.4.4.tgz#496a52b92b599a0112bec7c12414062de6ea8449"
|
||||||
integrity sha512-9g3F0SGxVr4UDd6y07bWtFnkpSSX1Ake7U7AGHgSFrwM6pF53/fV85bfxT2JLWS/3sjLCcyzoYzQlCxpkVo7wA==
|
integrity sha512-9g3F0SGxVr4UDd6y07bWtFnkpSSX1Ake7U7AGHgSFrwM6pF53/fV85bfxT2JLWS/3sjLCcyzoYzQlCxpkVo7wA==
|
||||||
|
|
||||||
|
"@types/url-parse@^1.4.3":
|
||||||
|
version "1.4.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/url-parse/-/url-parse-1.4.3.tgz#fba49d90f834951cb000a674efee3d6f20968329"
|
||||||
|
integrity sha512-4kHAkbV/OfW2kb5BLVUuUMoumB3CP8rHqlw48aHvFy5tf9ER0AfOonBlX29l/DD68G70DmyhRlSYfQPSYpC5Vw==
|
||||||
|
|
||||||
"@types/uuid@^8.3.0":
|
"@types/uuid@^8.3.0":
|
||||||
version "8.3.0"
|
version "8.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
|
||||||
@ -2137,6 +2142,13 @@ abbrev@1, abbrev@~1.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||||
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
|
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
|
||||||
|
|
||||||
|
abort-controller@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||||
|
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
||||||
|
dependencies:
|
||||||
|
event-target-shim "^5.0.0"
|
||||||
|
|
||||||
accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
|
accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
|
||||||
version "1.3.7"
|
version "1.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
|
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
|
||||||
@ -5336,6 +5348,11 @@ etag@~1.8.1:
|
|||||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||||
|
|
||||||
|
event-target-shim@^5.0.0:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||||
|
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||||
|
|
||||||
eventemitter3@^4.0.0:
|
eventemitter3@^4.0.0:
|
||||||
version "4.0.4"
|
version "4.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384"
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384"
|
||||||
@ -10092,6 +10109,14 @@ onetime@^5.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-fn "^2.1.0"
|
mimic-fn "^2.1.0"
|
||||||
|
|
||||||
|
open@^7.3.1:
|
||||||
|
version "7.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/open/-/open-7.3.1.tgz#111119cb919ca1acd988f49685c4fdd0f4755356"
|
||||||
|
integrity sha512-f2wt9DCBKKjlFbjzGb8MOAW8LH8F0mrs1zc7KTjAJ9PZNQbfenzWbNP1VZJvw6ICMG9r14Ah6yfwPn7T7i646A==
|
||||||
|
dependencies:
|
||||||
|
is-docker "^2.0.0"
|
||||||
|
is-wsl "^2.1.1"
|
||||||
|
|
||||||
opener@^1.5.1:
|
opener@^1.5.1:
|
||||||
version "1.5.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
||||||
@ -13754,7 +13779,7 @@ url-parse-lax@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
prepend-http "^2.0.0"
|
prepend-http "^2.0.0"
|
||||||
|
|
||||||
url-parse@^1.4.3:
|
url-parse@^1.4.3, url-parse@^1.4.7:
|
||||||
version "1.4.7"
|
version "1.4.7"
|
||||||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
|
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
|
||||||
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
|
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user