mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into extensions/validate-lens-engine-version
This commit is contained in:
commit
ff9dd1a8c6
37
.github/workflows/publish-master-npm.yml
vendored
Normal file
37
.github/workflows/publish-master-npm.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: Publish NPM Package `master`
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Publish NPM Package `master`
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'area/extension') }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [12.x]
|
||||||
|
steps:
|
||||||
|
- name: Checkout Release
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Using Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Generate NPM package
|
||||||
|
run: |
|
||||||
|
make build-npm
|
||||||
|
|
||||||
|
- name: publish new release
|
||||||
|
run: |
|
||||||
|
make publish-npm
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
NPM_RELEASE_TAG: master
|
||||||
@ -1,11 +1,11 @@
|
|||||||
name: Publish NPM
|
name: Publish NPM Package Release
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
name: Publish NPM
|
name: Publish NPM Package Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
3
Makefile
3
Makefile
@ -110,7 +110,8 @@ build-extension-types: node_modules src/extensions/npm/extensions/dist
|
|||||||
.PHONY: publish-npm
|
.PHONY: publish-npm
|
||||||
publish-npm: node_modules build-npm
|
publish-npm: node_modules build-npm
|
||||||
./node_modules/.bin/npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
|
./node_modules/.bin/npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
|
||||||
cd src/extensions/npm/extensions && npm publish --access=public
|
cd src/extensions/npm/extensions && npm publish --access=public --tag=${NPM_RELEASE_TAG:-latest}
|
||||||
|
git restore src/extensions/npm/extensions/package.json
|
||||||
|
|
||||||
.PHONY: docs
|
.PHONY: docs
|
||||||
docs:
|
docs:
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import appInfo from "../package.json";
|
import appInfo from "../package.json";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
|
import fastGlob from "fast-glob";
|
||||||
|
|
||||||
const packagePath = path.join(__dirname, "../package.json");
|
const packagePath = path.join(__dirname, "../package.json");
|
||||||
const versionInfo = semver.parse(appInfo.version);
|
const versionInfo = semver.parse(appInfo.version);
|
||||||
@ -41,3 +42,14 @@ if (versionInfo.prerelease) {
|
|||||||
|
|
||||||
|
|
||||||
fs.writeFileSync(packagePath, `${JSON.stringify(appInfo, null, 2)}\n`);
|
fs.writeFileSync(packagePath, `${JSON.stringify(appInfo, null, 2)}\n`);
|
||||||
|
|
||||||
|
const extensionManifests = fastGlob.sync(["extensions/*/package.json"]);
|
||||||
|
|
||||||
|
for (const manifestPath of extensionManifests) {
|
||||||
|
const packagePath = path.join(__dirname, "..", manifestPath);
|
||||||
|
|
||||||
|
import(packagePath).then((packageInfo) => {
|
||||||
|
packageInfo.default.version = `${versionInfo.raw}.${Date.now()}`;
|
||||||
|
fs.writeFileSync(packagePath, `${JSON.stringify(packageInfo.default, null, 2)}\n`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -22,8 +22,20 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import packageInfo from "../src/extensions/npm/extensions/package.json";
|
import packageInfo from "../src/extensions/npm/extensions/package.json";
|
||||||
import appInfo from "../package.json";
|
import appInfo from "../package.json";
|
||||||
|
import { SemVer } from "semver";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json");
|
const { NPM_RELEASE_TAG = "latest" } = process.env;
|
||||||
|
const version = new SemVer(appInfo.version);
|
||||||
|
|
||||||
packageInfo.version = appInfo.version;
|
if (NPM_RELEASE_TAG !== "latest") {
|
||||||
fs.writeFileSync(packagePath, `${JSON.stringify(packageInfo, null, 2)}\n`);
|
const gitRef = execSync("git rev-parse --short HEAD", {
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
|
||||||
|
version.inc("prerelease", `git.${gitRef.trim()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
packageInfo.version = version.format();
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(__dirname, "../src/extensions/npm/extensions/package.json"), `${JSON.stringify(packageInfo, null, 2)}\n`);
|
||||||
|
|||||||
@ -14,9 +14,9 @@ In order to see logs from this extension, you need to start Lens from the comman
|
|||||||
This extension can register a custom callback that is executed when an extension is activated (started).
|
This extension can register a custom callback that is executed when an extension is activated (started).
|
||||||
|
|
||||||
``` javascript
|
``` javascript
|
||||||
import { LensMainExtension } from "@k8slens/extensions"
|
import { Main } from "@k8slens/extensions"
|
||||||
|
|
||||||
export default class ExampleMainExtension extends LensMainExtension {
|
export default class ExampleMainExtension extends Main.LensExtension {
|
||||||
async onActivate() {
|
async onActivate() {
|
||||||
console.log("hello world")
|
console.log("hello world")
|
||||||
}
|
}
|
||||||
@ -28,9 +28,9 @@ export default class ExampleMainExtension extends LensMainExtension {
|
|||||||
This extension can register a custom callback that is executed when an extension is deactivated (stopped).
|
This extension can register a custom callback that is executed when an extension is deactivated (stopped).
|
||||||
|
|
||||||
``` javascript
|
``` javascript
|
||||||
import { LensMainExtension } from "@k8slens/extensions"
|
import { Main } from "@k8slens/extensions"
|
||||||
|
|
||||||
export default class ExampleMainExtension extends LensMainExtension {
|
export default class ExampleMainExtension extends Main.LensExtension {
|
||||||
async onDeactivate() {
|
async onDeactivate() {
|
||||||
console.log("bye bye")
|
console.log("bye bye")
|
||||||
}
|
}
|
||||||
@ -44,15 +44,15 @@ This extension can register custom app menus that will be displayed on OS native
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensMainExtension, windowManager } from "@k8slens/extensions"
|
import { Main } from "@k8slens/extensions"
|
||||||
|
|
||||||
export default class ExampleMainExtension extends LensMainExtension {
|
export default class ExampleMainExtension extends Main.LensExtension {
|
||||||
appMenus = [
|
appMenus = [
|
||||||
{
|
{
|
||||||
parentId: "help",
|
parentId: "help",
|
||||||
label: "Example item",
|
label: "Example item",
|
||||||
click() {
|
click() {
|
||||||
windowManager.navigate("https://k8slens.dev");
|
Main.Navigation.navigate("https://k8slens.dev");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -69,9 +69,9 @@ In order to see logs from this extension you need to check them via **View** > *
|
|||||||
This extension can register a custom callback that is executed when an extension is activated (started).
|
This extension can register a custom callback that is executed when an extension is activated (started).
|
||||||
|
|
||||||
``` javascript
|
``` javascript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
async onActivate() {
|
async onActivate() {
|
||||||
console.log("hello world")
|
console.log("hello world")
|
||||||
}
|
}
|
||||||
@ -83,9 +83,9 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
This extension can register a custom callback that is executed when an extension is deactivated (stopped).
|
This extension can register a custom callback that is executed when an extension is deactivated (stopped).
|
||||||
|
|
||||||
``` javascript
|
``` javascript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
|
|
||||||
export default class ExampleMainExtension extends LensRendererExtension {
|
export default class ExampleMainExtension extends Renderer.LensExtension {
|
||||||
async onDeactivate() {
|
async onDeactivate() {
|
||||||
console.log("bye bye")
|
console.log("bye bye")
|
||||||
}
|
}
|
||||||
@ -99,10 +99,16 @@ The global page is a full-screen page that hides all other content from a window
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Component, LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
import { ExamplePage } from "./src/example-page"
|
import { ExamplePage } from "./src/example-page"
|
||||||
|
|
||||||
export default class ExampleRendererExtension extends LensRendererExtension {
|
const {
|
||||||
|
Component: {
|
||||||
|
Icon,
|
||||||
|
}
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export default class ExampleRendererExtension extends Renderer.LensExtension {
|
||||||
globalPages = [
|
globalPages = [
|
||||||
{
|
{
|
||||||
id: "example",
|
id: "example",
|
||||||
@ -117,7 +123,7 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
|||||||
title: "Example page", // used in icon's tooltip
|
title: "Example page", // used in icon's tooltip
|
||||||
target: { pageId: "example" }
|
target: { pageId: "example" }
|
||||||
components: {
|
components: {
|
||||||
Icon: () => <Component.Icon material="arrow"/>,
|
Icon: () => <Icon material="arrow"/>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -131,12 +137,12 @@ It is responsible for storing a state for custom preferences.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
import { myCustomPreferencesStore } from "./src/my-custom-preferences-store"
|
import { myCustomPreferencesStore } from "./src/my-custom-preferences-store"
|
||||||
import { MyCustomPreferenceHint, MyCustomPreferenceInput } from "./src/my-custom-preference"
|
import { MyCustomPreferenceHint, MyCustomPreferenceInput } from "./src/my-custom-preference"
|
||||||
|
|
||||||
|
|
||||||
export default class ExampleRendererExtension extends LensRendererExtension {
|
export default class ExampleRendererExtension extends Renderer.LensExtension {
|
||||||
appPreferences = [
|
appPreferences = [
|
||||||
{
|
{
|
||||||
title: "My Custom Preference",
|
title: "My Custom Preference",
|
||||||
@ -156,10 +162,10 @@ These pages are visible in a cluster menu when a cluster is opened.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./src/page"
|
import { ExampleIcon, ExamplePage } from "./src/page"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterPages = [
|
clusterPages = [
|
||||||
{
|
{
|
||||||
id: "extension-example", // optional
|
id: "extension-example", // optional
|
||||||
@ -190,10 +196,10 @@ These features are visible in the "Cluster Settings" page.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
import { MyCustomFeature } from "./src/my-custom-feature"
|
import { MyCustomFeature } from "./src/my-custom-feature"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterFeatures = [
|
clusterFeatures = [
|
||||||
{
|
{
|
||||||
title: "My Custom Feature",
|
title: "My Custom Feature",
|
||||||
@ -219,15 +225,21 @@ This extension can register custom icons and text to a status bar area.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Component, LensRendererExtension, Navigation } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
const {
|
||||||
|
Component: {
|
||||||
|
Icon,
|
||||||
|
}
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
statusBarItems = [
|
statusBarItems = [
|
||||||
{
|
{
|
||||||
components: {
|
components: {
|
||||||
Item: (
|
Item: (
|
||||||
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
|
<div className="flex align-center gaps hover-highlight" onClick={() => this.navigate("/example-page")} >
|
||||||
<Component.Icon material="favorite" />
|
<Icon material="favorite" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -243,10 +255,10 @@ This extension can register custom menu items (actions) for specified Kubernetes
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { CustomMenuItem, CustomMenuItemProps } from "./src/custom-menu-item"
|
import { CustomMenuItem, CustomMenuItemProps } from "./src/custom-menu-item"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
kubeObjectMenuItems = [
|
kubeObjectMenuItems = [
|
||||||
{
|
{
|
||||||
kind: "Node",
|
kind: "Node",
|
||||||
@ -266,10 +278,10 @@ This extension can register custom details (content) for specified Kubernetes ki
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { CustomKindDetails, CustomKindDetailsProps } from "./src/custom-kind-details"
|
import { CustomKindDetails, CustomKindDetailsProps } from "./src/custom-kind-details"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
kubeObjectDetailItems = [
|
kubeObjectDetailItems = [
|
||||||
{
|
{
|
||||||
kind: "CustomKind",
|
kind: "CustomKind",
|
||||||
|
|||||||
@ -114,14 +114,14 @@ There is a way of detect active theme and its changes in JS. [MobX observer func
|
|||||||
```js
|
```js
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { observer } from "mobx-react"
|
import { observer } from "mobx-react"
|
||||||
import { App, Component, Theme } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class SupportPage extends React.Component {
|
export class SupportPage extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="SupportPage">
|
<div className="SupportPage">
|
||||||
<h1>Active theme is {Theme.getActiveTheme().name}</h1>
|
<h1>Active theme is {Renderer.Theme.getActiveTheme().name}</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,11 +96,11 @@ It also registers the `MenuItem` component that displays the `ExampleIcon` React
|
|||||||
These React components are defined in the additional `./src/page.tsx` file.
|
These React components are defined in the additional `./src/page.tsx` file.
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page"
|
import { ExampleIcon, ExamplePage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterPages = [
|
clusterPages = [
|
||||||
{
|
{
|
||||||
id: "extension-example",
|
id: "extension-example",
|
||||||
|
|||||||
@ -30,9 +30,8 @@ Each guide or code sample includes the following:
|
|||||||
|
|
||||||
| Sample | APIs |
|
| Sample | APIs |
|
||||||
| ----- | ----- |
|
| ----- | ----- |
|
||||||
[hello-world](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> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||||
[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> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||||
[styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||||
[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
[styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension <br> LensRendererExtension <br> Renderer.Component.Icon <br> Renderer.Component.IconProps |
|
||||||
[styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
[custom-resource-page](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page) | LensRendererExtension <br> Renderer.K8sApi.KubeApi <br> Renderer.K8sApi.KubeObjectStore <br> Renderer.Component.KubeObjectListLayout <br> Renderer.Component.KubeObjectDetailsProps <br> Renderer.Component.IconProps |
|
||||||
[custom-resource-page](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page) | LensRendererExtension <br> K8sApi.KubeApi <br> K8sApi.KubeObjectStore <br> Component.KubeObjectListLayout <br> Component.KubeObjectDetailsProps <br> Component.IconProps |
|
|
||||||
|
|||||||
@ -43,10 +43,10 @@ To register either a handler or a listener, you should do something like the fol
|
|||||||
|
|
||||||
`main.ts`:
|
`main.ts`:
|
||||||
```typescript
|
```typescript
|
||||||
import { LensMainExtension } from "@k8slens/extensions";
|
import { Main } from "@k8slens/extensions";
|
||||||
import { IpcMain } from "./helpers/main";
|
import { IpcMain } from "./helpers/main";
|
||||||
|
|
||||||
export class ExampleExtensionMain extends LensMainExtension {
|
export class ExampleExtensionMain extends Main.LensExtension {
|
||||||
onActivate() {
|
onActivate() {
|
||||||
IpcMain.createInstance(this);
|
IpcMain.createInstance(this);
|
||||||
}
|
}
|
||||||
@ -60,10 +60,10 @@ Lens will automatically clean up that store and all the handlers on deactivation
|
|||||||
|
|
||||||
`helpers/main.ts`:
|
`helpers/main.ts`:
|
||||||
```typescript
|
```typescript
|
||||||
import { Ipc, Types } from "@k8slens/extensions";
|
import { Main } from "@k8slens/extensions";
|
||||||
|
|
||||||
export class IpcMain extends Ipc.Main {
|
export class IpcMain extends Main.Ipc {
|
||||||
constructor(extension: LensMainExtension) {
|
constructor(extension: Main.LensExtension) {
|
||||||
super(extension);
|
super(extension);
|
||||||
|
|
||||||
this.listen("initialize", onInitialize);
|
this.listen("initialize", onInitialize);
|
||||||
@ -82,10 +82,10 @@ You should be able to just call `IpcMain.getInstance()` anywhere it is needed in
|
|||||||
|
|
||||||
`renderer.ts`:
|
`renderer.ts`:
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { IpcRenderer } from "./helpers/renderer";
|
import { IpcRenderer } from "./helpers/renderer";
|
||||||
|
|
||||||
export class ExampleExtensionRenderer extends LensRendererExtension {
|
export class ExampleExtensionRenderer extends Renderer.LensExtension {
|
||||||
onActivate() {
|
onActivate() {
|
||||||
const ipc = IpcRenderer.createInstance(this);
|
const ipc = IpcRenderer.createInstance(this);
|
||||||
|
|
||||||
@ -100,9 +100,9 @@ It is also needed to create an instance to broadcast messages too.
|
|||||||
|
|
||||||
`helpers/renderer.ts`:
|
`helpers/renderer.ts`:
|
||||||
```typescript
|
```typescript
|
||||||
import { Ipc } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
export class IpcRenderer extends Ipc.Renderer {}
|
export class IpcRenderer extends Renderer.Ipc {}
|
||||||
```
|
```
|
||||||
|
|
||||||
It is necessary to create child classes of these `abstract class`'s in your extension before you can use them.
|
It is necessary to create child classes of these `abstract class`'s in your extension before you can use them.
|
||||||
@ -116,7 +116,7 @@ There is no behind the scenes transfer of these functions.
|
|||||||
To register a "handler" call `IpcMain.getInstance().handle(...)`.
|
To register a "handler" call `IpcMain.getInstance().handle(...)`.
|
||||||
The cleanup of these handlers is handled by Lens itself.
|
The cleanup of these handlers is handled by Lens itself.
|
||||||
|
|
||||||
The `listen()` methods on `Ipc.Main` and `Ipc.Renderer` return a `Disposer`, or more specifically, a `() => void`.
|
The `listen()` methods on `Main.Ipc` and `Renderer.Ipc` return a `Disposer`, or more specifically, a `() => void`.
|
||||||
This can be optionally called to remove the listener early.
|
This can be optionally called to remove the listener early.
|
||||||
|
|
||||||
Calling either `IpcRenderer.getInstance().broadcast(...)` or `IpcMain.getInstance().broadcast(...)` sends an event to all `renderer` frames and to `main`.
|
Calling either `IpcRenderer.getInstance().broadcast(...)` or `IpcMain.getInstance().broadcast(...)` sends an event to all `renderer` frames and to `main`.
|
||||||
|
|||||||
@ -18,7 +18,7 @@ First thing we need to do with our extension is to register new menu item in the
|
|||||||
We will do this in our extension class `CrdSampleExtension` that is derived `LensRendererExtension` class:
|
We will do this in our extension class `CrdSampleExtension` that is derived `LensRendererExtension` class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export default class CrdSampleExtension extends LensRendererExtension {
|
export default class CrdSampleExtension extends Renderer.LensExtension {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -27,11 +27,21 @@ This object will register a menu item with "Certificates" text.
|
|||||||
It will also use `CertificateIcon` component to render an icon and navigate to cluster page that is having `certificates` page id.
|
It will also use `CertificateIcon` component to render an icon and navigate to cluster page that is having `certificates` page id.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export function CertificateIcon(props: Component.IconProps) {
|
import { Renderer } from "@k8slens/extensions";
|
||||||
return <Component.Icon {...props} material="security" tooltip="Certificates"/>
|
|
||||||
|
type IconProps = Renderer.Component.IconProps;
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export function CertificateIcon(props: IconProps) {
|
||||||
|
return <Icon {...props} material="security" tooltip="Certificates"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CrdSampleExtension extends LensRendererExtension {
|
export default class CrdSampleExtension extends Renderer.LensExtension {
|
||||||
|
|
||||||
clusterPageMenus = [
|
clusterPageMenus = [
|
||||||
{
|
{
|
||||||
@ -48,7 +58,7 @@ export default class CrdSampleExtension extends LensRendererExtension {
|
|||||||
Then we need to register `PageRegistration` object with `certificates` id and define `CertificatePage` component to render certificates.
|
Then we need to register `PageRegistration` object with `certificates` id and define `CertificatePage` component to render certificates.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export default class CrdSampleExtension extends LensRendererExtension {
|
export default class CrdSampleExtension extends Renderer.LensExtension {
|
||||||
...
|
...
|
||||||
|
|
||||||
clusterPages = [{
|
clusterPages = [{
|
||||||
@ -65,18 +75,29 @@ export default class CrdSampleExtension extends LensRendererExtension {
|
|||||||
|
|
||||||
In the previous step we defined `CertificatePage` component to render certificates.
|
In the previous step we defined `CertificatePage` component to render certificates.
|
||||||
In this step we will actually implement that.
|
In this step we will actually implement that.
|
||||||
`CertificatePage` is a React component that will render `Component.KubeObjectListLayout` component to list `Certificate` CRD objects.
|
`CertificatePage` is a React component that will render `Renderer.Component.KubeObjectListLayout` component to list `Certificate` CRD objects.
|
||||||
|
|
||||||
### Get CRD objects
|
### Get CRD objects
|
||||||
|
|
||||||
In order to list CRD objects, we need first fetch those from Kubernetes API.
|
In order to list CRD objects, we need first fetch those from Kubernetes API.
|
||||||
Lens Extensions API provides easy mechanism to do this.
|
Lens Extensions API provides easy mechanism to do this.
|
||||||
We just need to define `Certificate` class derived from `K8sApi.KubeObject`, `CertificatesApi`derived from `K8sApi.KubeApi` and `CertificatesStore` derived from `K8sApi.KubeObjectStore`.
|
We just need to define `Certificate` class derived from `Renderer.K8sApi.KubeObject`, `CertificatesApi`derived from `Renderer.K8sApi.KubeApi` and `CertificatesStore` derived from `Renderer.K8sApi.KubeObjectStore`.
|
||||||
|
|
||||||
`Certificate` class defines properties found in the CRD object:
|
`Certificate` class defines properties found in the CRD object:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export class Certificate extends K8sApi.KubeObject {
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
const {
|
||||||
|
K8sApi: {
|
||||||
|
KubeObject,
|
||||||
|
KubeObjectStore,
|
||||||
|
KubeApi,
|
||||||
|
apiManager,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export class Certificate extends KubeObject {
|
||||||
static kind = "Certificate"
|
static kind = "Certificate"
|
||||||
static namespaced = true
|
static namespaced = true
|
||||||
static apiBase = "/apis/cert-manager.io/v1alpha2/certificates"
|
static apiBase = "/apis/cert-manager.io/v1alpha2/certificates"
|
||||||
@ -121,8 +142,8 @@ export class Certificate extends K8sApi.KubeObject {
|
|||||||
With `CertificatesApi` class we are able to manage `Certificate` objects in Kubernetes API:
|
With `CertificatesApi` class we are able to manage `Certificate` objects in Kubernetes API:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export class CertificatesApi extends K8sApi.KubeApi<Certificate> {
|
export class CertificatesApi extends KubeApi<Certificate> {}
|
||||||
}
|
|
||||||
export const certificatesApi = new CertificatesApi({
|
export const certificatesApi = new CertificatesApi({
|
||||||
objectConstructor: Certificate
|
objectConstructor: Certificate
|
||||||
});
|
});
|
||||||
@ -131,7 +152,7 @@ export const certificatesApi = new CertificatesApi({
|
|||||||
`CertificateStore` defines storage for `Certificate` objects
|
`CertificateStore` defines storage for `Certificate` objects
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export class CertificatesStore extends K8sApi.KubeObjectStore<Certificate> {
|
export class CertificatesStore extends KubeObjectStore<Certificate> {
|
||||||
api = certificatesApi
|
api = certificatesApi
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +162,7 @@ export const certificatesStore = new CertificatesStore();
|
|||||||
And, finally, we register this store to Lens's API manager.
|
And, finally, we register this store to Lens's API manager.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
K8sApi.apiManager.registerStore(certificatesStore);
|
apiManager.registerStore(certificatesStore);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -153,23 +174,32 @@ Then we need to fetch those and render them in the UI.
|
|||||||
First we define `CertificatePage` class that extends `React.Component`.
|
First we define `CertificatePage` class that extends `React.Component`.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Component, LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { certificatesStore } from "../certificate-store";
|
import { certificatesStore } from "../certificate-store";
|
||||||
import { Certificate } from "../certificate"
|
import { Certificate } from "../certificate"
|
||||||
|
|
||||||
export class CertificatePage extends React.Component<{ extension: LensRendererExtension }> {
|
export class CertificatePage extends React.Component<{ extension: Renderer.LensExtension }> {
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Next we will implement `render` method that will display certificates in a list.
|
Next we will implement `render` method that will display certificates in a list.
|
||||||
To do that, we just need to add `Component.KubeObjectListLayout` component inside `Component.TabLayout` component in render method.
|
To do that, we just need to add `Renderer.Component.KubeObjectListLayout` component inside `Renderer.Component.TabLayout` component in render method.
|
||||||
To define which objects the list is showing, we need to pass `certificateStore` object to `Component.KubeObjectListLayout` in `store` property.
|
To define which objects the list is showing, we need to pass `certificateStore` object to `Renderer.Component.KubeObjectListLayout` in `store` property.
|
||||||
`Component.KubeObjectListLayout` will fetch automatically items from the given store when component is mounted.
|
`Renderer.Component.KubeObjectListLayout` will fetch automatically items from the given store when component is mounted.
|
||||||
Also, we can define needed sorting callbacks and search filters for the list:
|
Also, we can define needed sorting callbacks and search filters for the list:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
TabLayout,
|
||||||
|
KubeObjectListLayout,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
namespace = "namespace",
|
namespace = "namespace",
|
||||||
@ -181,8 +211,8 @@ export class CertificatePage extends React.Component<{ extension: LensRendererEx
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Component.TabLayout>
|
<TabLayout>
|
||||||
<Component.KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
className="Certicates" store={certificatesStore}
|
className="Certicates" store={certificatesStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (certificate: Certificate) => certificate.getName(),
|
[sortBy.name]: (certificate: Certificate) => certificate.getName(),
|
||||||
@ -204,7 +234,7 @@ export class CertificatePage extends React.Component<{ extension: LensRendererEx
|
|||||||
certificate.spec.issuerRef.name
|
certificate.spec.issuerRef.name
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Component.TabLayout>
|
</TabLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,7 +249,7 @@ First, we need to register our custom component to render details for the specif
|
|||||||
We will do this again in `CrdSampleExtension` class:
|
We will do this again in `CrdSampleExtension` class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export default class CrdSampleExtension extends LensRendererExtension {
|
export default class CrdSampleExtension extends Renderer.LensExtension {
|
||||||
//...
|
//...
|
||||||
|
|
||||||
kubeObjectDetailItems = [{
|
kubeObjectDetailItems = [{
|
||||||
@ -235,14 +265,22 @@ export default class CrdSampleExtension extends LensRendererExtension {
|
|||||||
Here we defined that `CertificateDetails` component will render the resource details.
|
Here we defined that `CertificateDetails` component will render the resource details.
|
||||||
So, next we need to implement that component.
|
So, next we need to implement that component.
|
||||||
Lens will inject `Certificate` object into our component so we just need to render some information out of it.
|
Lens will inject `Certificate` object into our component so we just need to render some information out of it.
|
||||||
We can use `Component.DrawerItem` component from Lens Extensions API to give the same look and feel as Lens is using elsewhere:
|
We can use `Renderer.Component.DrawerItem` component from Lens Extensions API to give the same look and feel as Lens is using elsewhere:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Component, K8sApi } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Certificate } from "../certificate";
|
import { Certificate } from "../certificate";
|
||||||
|
|
||||||
export interface CertificateDetailsProps extends Component.KubeObjectDetailsProps<Certificate>{
|
const {
|
||||||
|
Component: {
|
||||||
|
KubeObjectDetailsProps,
|
||||||
|
DrawerItem,
|
||||||
|
Badge,
|
||||||
|
}
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export interface CertificateDetailsProps extends KubeObjectDetailsProps<Certificate>{
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CertificateDetails extends React.Component<CertificateDetailsProps> {
|
export class CertificateDetails extends React.Component<CertificateDetailsProps> {
|
||||||
@ -252,29 +290,29 @@ export class CertificateDetails extends React.Component<CertificateDetailsProps>
|
|||||||
if (!certificate) return null;
|
if (!certificate) return null;
|
||||||
return (
|
return (
|
||||||
<div className="Certificate">
|
<div className="Certificate">
|
||||||
<Component.DrawerItem name="Created">
|
<DrawerItem name="Created">
|
||||||
{certificate.getAge(true, false)} ago ({certificate.metadata.creationTimestamp })
|
{certificate.getAge(true, false)} ago ({certificate.metadata.creationTimestamp })
|
||||||
</Component.DrawerItem>
|
</DrawerItem>
|
||||||
<Component.DrawerItem name="DNS Names">
|
<DrawerItem name="DNS Names">
|
||||||
{certificate.spec.dnsNames.join(",")}
|
{certificate.spec.dnsNames.join(",")}
|
||||||
</Component.DrawerItem>
|
</DrawerItem>
|
||||||
<Component.DrawerItem name="Secret">
|
<DrawerItem name="Secret">
|
||||||
{certificate.spec.secretName}
|
{certificate.spec.secretName}
|
||||||
</Component.DrawerItem>
|
</DrawerItem>
|
||||||
<Component.DrawerItem name="Status" className="status" labelsOnly>
|
<DrawerItem name="Status" className="status" labelsOnly>
|
||||||
{certificate.status.conditions.map((condition, index) => {
|
{certificate.status.conditions.map((condition, index) => {
|
||||||
const { type, reason, message, status } = condition;
|
const { type, reason, message, status } = condition;
|
||||||
const kind = type || reason;
|
const kind = type || reason;
|
||||||
if (!kind) return null;
|
if (!kind) return null;
|
||||||
return (
|
return (
|
||||||
<Component.Badge
|
<Badge
|
||||||
key={kind + index} label={kind}
|
key={kind + index} label={kind}
|
||||||
className={"success "+kind.toLowerCase()}
|
className={"success "+kind.toLowerCase()}
|
||||||
tooltip={message}
|
tooltip={message}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Component.DrawerItem>
|
</DrawerItem>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,9 @@ The Main Extension API allows you to access, configure, and customize Lens data,
|
|||||||
To create a main extension simply extend the `LensMainExtension` class:
|
To create a main extension simply extend the `LensMainExtension` class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensMainExtension } from "@k8slens/extensions";
|
import { Main } from "@k8slens/extensions";
|
||||||
|
|
||||||
export default class ExampleExtensionMain extends LensMainExtension {
|
export default class ExampleExtensionMain extends Main.LensExtension {
|
||||||
onActivate() {
|
onActivate() {
|
||||||
console.log('custom main process extension code started');
|
console.log('custom main process extension code started');
|
||||||
}
|
}
|
||||||
@ -39,34 +39,6 @@ The example above logs messages when the extension is enabled and disabled.
|
|||||||
To see standard output from the main process there must be a console connected to it.
|
To see standard output from the main process there must be a console connected to it.
|
||||||
Achieve this by starting Lens from the command prompt.
|
Achieve this by starting Lens from the command prompt.
|
||||||
|
|
||||||
The following example is a little more interesting.
|
|
||||||
It accesses some Lens state data, and it periodically logs the name of the cluster that is currently active in Lens.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { LensMainExtension, Store } from "@k8slens/extensions";
|
|
||||||
|
|
||||||
export default class ActiveClusterExtensionMain extends LensMainExtension {
|
|
||||||
|
|
||||||
timer: NodeJS.Timeout
|
|
||||||
|
|
||||||
onActivate() {
|
|
||||||
console.log("Cluster logger activated");
|
|
||||||
this.timer = setInterval(() => {
|
|
||||||
if (!Store.ClusterStore.getInstance().active) {
|
|
||||||
console.log("No active cluster");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("active cluster is", Store.ClusterStore.getInstance().active.contextName)
|
|
||||||
}, 5000)
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeactivate() {
|
|
||||||
clearInterval(this.timer)
|
|
||||||
console.log("Cluster logger deactivated");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For more details on accessing Lens state data, please see the [Stores](../stores) guide.
|
For more details on accessing Lens state data, please see the [Stores](../stores) guide.
|
||||||
|
|
||||||
### `appMenus`
|
### `appMenus`
|
||||||
@ -76,9 +48,9 @@ Note that this is the only UI feature that the Main Extension API allows you to
|
|||||||
The following example demonstrates adding an item to the **Help** menu.
|
The following example demonstrates adding an item to the **Help** menu.
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensMainExtension } from "@k8slens/extensions";
|
import { Main } from "@k8slens/extensions";
|
||||||
|
|
||||||
export default class SamplePageMainExtension extends LensMainExtension {
|
export default class SamplePageMainExtension extends Main.LensExtension {
|
||||||
appMenus = [
|
appMenus = [
|
||||||
{
|
{
|
||||||
parentId: "help",
|
parentId: "help",
|
||||||
@ -102,4 +74,4 @@ Valid values include: `"file"`, `"edit"`, `"view"`, and `"help"`.
|
|||||||
* `click()` is called when the menu item is selected.
|
* `click()` is called when the menu item is selected.
|
||||||
In this example, we simply log a message.
|
In this example, we simply log a message.
|
||||||
However, you would typically have this navigate to a specific page or perform another operation.
|
However, you would typically have this navigate to a specific page or perform another operation.
|
||||||
Note that pages are associated with the [`LensRendererExtension`](renderer-extension.md) class and can be defined in the process of extending it.
|
Note that pages are associated with the [`Renderer.LensExtension`](renderer-extension.md) class and can be defined in the process of extending it.
|
||||||
|
|||||||
@ -18,13 +18,13 @@ In other words, which handler is selected in either process is independent from
|
|||||||
Example of registering a handler:
|
Example of registering a handler:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensMainExtension, Interface } from "@k8slens/extensions";
|
import { Main, Common } from "@k8slens/extensions";
|
||||||
|
|
||||||
function rootHandler(params: Iterface.ProtocolRouteParams) {
|
function rootHandler(params: Common.Types.ProtocolRouteParams) {
|
||||||
console.log("routed to ExampleExtension", params);
|
console.log("routed to ExampleExtension", params);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ExampleExtensionMain extends LensMainExtension {
|
export default class ExampleExtensionMain extends Main.LensExtension {
|
||||||
protocolHandlers = [
|
protocolHandlers = [
|
||||||
pathSchema: "/",
|
pathSchema: "/",
|
||||||
handler: rootHandler,
|
handler: rootHandler,
|
||||||
|
|||||||
@ -24,9 +24,9 @@ All UI elements are based on React components.
|
|||||||
To create a renderer extension, extend the `LensRendererExtension` class:
|
To create a renderer extension, extend the `LensRendererExtension` class:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
export default class ExampleExtensionMain extends LensRendererExtension {
|
export default class ExampleExtensionMain extends Renderer.LensExtension {
|
||||||
onActivate() {
|
onActivate() {
|
||||||
console.log('custom renderer process extension code started');
|
console.log('custom renderer process extension code started');
|
||||||
}
|
}
|
||||||
@ -61,11 +61,11 @@ Use your extension to access Kubernetes resources in the active cluster with [`C
|
|||||||
Add a cluster page definition to a `LensRendererExtension` subclass with the following example:
|
Add a cluster page definition to a `LensRendererExtension` subclass with the following example:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page"
|
import { ExampleIcon, ExamplePage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterPages = [
|
clusterPages = [
|
||||||
{
|
{
|
||||||
id: "hello",
|
id: "hello",
|
||||||
@ -88,7 +88,7 @@ It offers flexibility in defining the appearance and behavior of your page.
|
|||||||
`ExamplePage` in the example above can be defined in `page.tsx`:
|
`ExamplePage` in the example above can be defined in `page.tsx`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
||||||
@ -116,11 +116,11 @@ Use `clusterPageMenus`, covered in the next section, to add cluster pages to the
|
|||||||
By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition:
|
By expanding on the above example, you can add a cluster page menu item to the `ExampleExtension` definition:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page"
|
import { ExampleIcon, ExamplePage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterPages = [
|
clusterPages = [
|
||||||
{
|
{
|
||||||
id: "hello",
|
id: "hello",
|
||||||
@ -157,14 +157,20 @@ When users click **Hello World**, the cluster dashboard will show the contents o
|
|||||||
This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`, as follows:
|
This example requires the definition of another React-based component, `ExampleIcon`, which has been added to `page.tsx`, as follows:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export function ExampleIcon(props: Component.IconProps) {
|
type IconProps = Renderer.Component.IconProps;
|
||||||
return <Component.Icon {...props} material="pages" tooltip={"Hi!"}/>
|
|
||||||
|
const {
|
||||||
|
Component: { Icon },
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export function ExampleIcon(props: IconProps) {
|
||||||
|
return <Icon {...props} material="pages" tooltip={"Hi!"}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
export class ExamplePage extends React.Component<{ extension: Renderer.LensExtension }> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -176,8 +182,8 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
|
|||||||
```
|
```
|
||||||
|
|
||||||
Lens includes various built-in components available for extension developers to use.
|
Lens includes various built-in components available for extension developers to use.
|
||||||
One of these is the `Component.Icon`, introduced in `ExampleIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io).
|
One of these is the `Renderer.Component.Icon`, introduced in `ExampleIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io).
|
||||||
The properties that `Component.Icon` uses are defined as follows:
|
The properties that `Renderer.Component.Icon` uses are defined as follows:
|
||||||
|
|
||||||
* `material` takes the name of the icon you want to use.
|
* `material` takes the name of the icon you want to use.
|
||||||
* `tooltip` sets the text you want to appear when a user hovers over the icon.
|
* `tooltip` sets the text you want to appear when a user hovers over the icon.
|
||||||
@ -187,11 +193,11 @@ The following example groups two sub menu items under one parent menu item:
|
|||||||
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page"
|
import { ExampleIcon, ExamplePage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
clusterPages = [
|
clusterPages = [
|
||||||
{
|
{
|
||||||
id: "hello",
|
id: "hello",
|
||||||
@ -261,11 +267,11 @@ Unlike cluster pages, users can trigger global pages even when there is no activ
|
|||||||
The following example defines a `LensRendererExtension` subclass with a single global page definition:
|
The following example defines a `LensRendererExtension` subclass with a single global page definition:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from '@k8slens/extensions';
|
import { Renderer } from '@k8slens/extensions';
|
||||||
import { HelpPage } from './page';
|
import { HelpPage } from './page';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default class HelpExtension extends LensRendererExtension {
|
export default class HelpExtension extends Renderer.LensExtension {
|
||||||
globalPages = [
|
globalPages = [
|
||||||
{
|
{
|
||||||
id: "help",
|
id: "help",
|
||||||
@ -288,7 +294,7 @@ It offers flexibility in defining the appearance and behavior of your page.
|
|||||||
`HelpPage` in the example above can be defined in `page.tsx`:
|
`HelpPage` in the example above can be defined in `page.tsx`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export class HelpPage extends React.Component<{ extension: LensRendererExtension }> {
|
export class HelpPage extends React.Component<{ extension: LensRendererExtension }> {
|
||||||
@ -320,11 +326,11 @@ Global pages can be made available in the following ways:
|
|||||||
By expanding on the above example, you can add a global page menu item to the `HelpExtension` definition:
|
By expanding on the above example, you can add a global page menu item to the `HelpExtension` definition:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { HelpIcon, HelpPage } from "./page"
|
import { HelpIcon, HelpPage } from "./page"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class HelpExtension extends LensRendererExtension {
|
export default class HelpExtension extends Renderer.LensExtension {
|
||||||
globalPages = [
|
globalPages = [
|
||||||
{
|
{
|
||||||
id: "help",
|
id: "help",
|
||||||
@ -362,14 +368,20 @@ This example requires the definition of another React-based component, `HelpIcon
|
|||||||
Update `page.tsx` from the example above with the `HelpIcon` definition, as follows:
|
Update `page.tsx` from the example above with the `HelpIcon` definition, as follows:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export function HelpIcon(props: Component.IconProps) {
|
type IconProps = Renderer.Component.IconProps;
|
||||||
return <Component.Icon {...props} material="help"/>
|
|
||||||
|
const {
|
||||||
|
Component: { Icon },
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
export function HelpIcon(props: IconProps) {
|
||||||
|
return <Icon {...props} material="help"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HelpPage extends React.Component<{ extension: LensRendererExtension }> {
|
export class HelpPage extends React.Component<{ extension: Renderer.LensExtension }> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -381,8 +393,8 @@ export class HelpPage extends React.Component<{ extension: LensRendererExtension
|
|||||||
```
|
```
|
||||||
|
|
||||||
Lens includes various built-in components available for extension developers to use.
|
Lens includes various built-in components available for extension developers to use.
|
||||||
One of these is the `Component.Icon`, introduced in `HelpIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io).
|
One of these is the `Renderer.Component.Icon`, introduced in `HelpIcon`, which you can use to access any of the [icons](https://material.io/resources/icons/) available at [Material Design](https://material.io).
|
||||||
The property that `Component.Icon` uses is defined as follows:
|
The property that `Renderer.Component.Icon` uses is defined as follows:
|
||||||
|
|
||||||
* `material` takes the name of the icon you want to use.
|
* `material` takes the name of the icon you want to use.
|
||||||
|
|
||||||
@ -401,11 +413,11 @@ They can be installed and uninstalled by the Lens user from the cluster **Settin
|
|||||||
The following example shows how to add a cluster feature as part of a `LensRendererExtension`:
|
The following example shows how to add a cluster feature as part of a `LensRendererExtension`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions"
|
import { Renderer } from "@k8slens/extensions"
|
||||||
import { ExampleFeature } from "./src/example-feature"
|
import { ExampleFeature } from "./src/example-feature"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class ExampleFeatureExtension extends LensRendererExtension {
|
export default class ExampleFeatureExtension extends Renderer.LensExtension {
|
||||||
clusterFeatures = [
|
clusterFeatures = [
|
||||||
{
|
{
|
||||||
title: "Example Feature",
|
title: "Example Feature",
|
||||||
@ -462,45 +474,69 @@ Consider using the following properties with `updateStatus()`:
|
|||||||
The following shows a very simple implementation of a `ClusterFeature`:
|
The following shows a very simple implementation of a `ClusterFeature`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { ClusterFeature, Store, K8sApi } from "@k8slens/extensions";
|
import { Renderer, Common } from "@k8slens/extensions";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
export class ExampleFeature extends ClusterFeature.Feature {
|
const {
|
||||||
|
K8sApi: {
|
||||||
|
ResourceStack,
|
||||||
|
forCluster,
|
||||||
|
StorageClass,
|
||||||
|
Namespace,
|
||||||
|
}
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
async install(cluster: Store.Cluster): Promise<void> {
|
type ResourceStack = Renderer.K8sApi.ResourceStack;
|
||||||
|
type Pod = Renderer.K8sApi.Pod;
|
||||||
|
type KubernetesCluster = Common.Catalog.KubernetesCluster;
|
||||||
|
|
||||||
super.applyResources(cluster, path.join(__dirname, "../resources/"));
|
export interface MetricsStatus {
|
||||||
|
installed: boolean;
|
||||||
|
canUpgrade: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExampleFeature {
|
||||||
|
protected stack: ResourceStack;
|
||||||
|
|
||||||
|
constructor(protected cluster: KubernetesCluster) {
|
||||||
|
this.stack = new ResourceStack(cluster, this.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async upgrade(cluster: Store.Cluster): Promise<void> {
|
install(): Promise<string> {
|
||||||
return this.install(cluster);
|
return this.stack.kubectlApplyFolder(path.join(__dirname, "../resources/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> {
|
upgrade(): Promise<string> {
|
||||||
|
return this.install(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatus(): Promise<MetricsStatus> {
|
||||||
|
const status: MetricsStatus = { installed: false, canUpgrade: false};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pod = K8sApi.forCluster(cluster, K8sApi.Pod);
|
const pod = forCluster(cluster, Pod);
|
||||||
const examplePod = await pod.get({name: "example-pod", namespace: "default"});
|
const examplePod = await pod.get({name: "example-pod", namespace: "default"});
|
||||||
|
|
||||||
if (examplePod?.kind) {
|
if (examplePod?.kind) {
|
||||||
this.status.installed = true;
|
status.installed = true;
|
||||||
this.status.currentVersion = examplePod.spec.containers[0].image.split(":")[1];
|
status.currentVersion = examplePod.spec.containers[0].image.split(":")[1];
|
||||||
this.status.canUpgrade = true; // a real implementation would perform a check here that is relevant to the specific feature
|
status.canUpgrade = true; // a real implementation would perform a check here that is relevant to the specific feature
|
||||||
} else {
|
} else {
|
||||||
this.status.installed = false;
|
status.installed = false;
|
||||||
this.status.canUpgrade = false;
|
status.canUpgrade = false;
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (e?.error?.code === 404) {
|
if (e?.error?.code === 404) {
|
||||||
this.status.installed = false;
|
status.installed = false;
|
||||||
this.status.canUpgrade = false;
|
status.canUpgrade = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninstall(cluster: Store.Cluster): Promise<void> {
|
async uninstall(): Promise<string> {
|
||||||
const podApi = K8sApi.forCluster(cluster, K8sApi.Pod);
|
return this.stack.kubectlDeleteFolder(this.resourceFolder);
|
||||||
await podApi.delete({name: "example-pod", namespace: "default"});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -539,12 +575,12 @@ You can use Lens extensions to add custom preferences to the Preferences page, p
|
|||||||
The following example demonstrates adding a custom preference:
|
The following example demonstrates adding a custom preference:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default class ExampleRendererExtension extends LensRendererExtension {
|
export default class ExampleRendererExtension extends Renderer.LensExtension {
|
||||||
|
|
||||||
@observable preference = { enabled: false };
|
@observable preference = { enabled: false };
|
||||||
|
|
||||||
@ -578,10 +614,16 @@ This is how `ExampleRendererExtension` handles the state of the preference input
|
|||||||
In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreferenceProps` are defined in `./src/example-preference.tsx` as follows:
|
In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreferenceProps` are defined in `./src/example-preference.tsx` as follows:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Component } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
Checkbox,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
export class ExamplePreferenceProps {
|
export class ExamplePreferenceProps {
|
||||||
preference: {
|
preference: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -594,7 +636,7 @@ export class ExamplePreferenceInput extends React.Component<ExamplePreferencePro
|
|||||||
render() {
|
render() {
|
||||||
const { preference } = this.props;
|
const { preference } = this.props;
|
||||||
return (
|
return (
|
||||||
<Component.Checkbox
|
<Checkbox
|
||||||
label="I understand appPreferences"
|
label="I understand appPreferences"
|
||||||
value={preference.enabled}
|
value={preference.enabled}
|
||||||
onChange={v => { preference.enabled = v; }}
|
onChange={v => { preference.enabled = v; }}
|
||||||
@ -612,7 +654,7 @@ export class ExamplePreferenceHint extends React.Component {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`ExamplePreferenceInput` implements a simple checkbox using Lens's `Component.Checkbox` using the following properties:
|
`ExamplePreferenceInput` implements a simple checkbox using Lens's `Renderer.Component.Checkbox` using the following properties:
|
||||||
|
|
||||||
* `label` sets the text that displays next to the checkbox.
|
* `label` sets the text that displays next to the checkbox.
|
||||||
* `value` is initially set to `preference.enabled`.
|
* `value` is initially set to `preference.enabled`.
|
||||||
@ -645,11 +687,11 @@ The following example adds a `statusBarItems` definition and a `globalPages` def
|
|||||||
It configures the status bar item to navigate to the global page upon activation (normally a mouse click):
|
It configures the status bar item to navigate to the global page upon activation (normally a mouse click):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LensRendererExtension } from '@k8slens/extensions';
|
import { Renderer } from '@k8slens/extensions';
|
||||||
import { HelpIcon, HelpPage } from "./page"
|
import { HelpIcon, HelpPage } from "./page"
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default class HelpExtension extends LensRendererExtension {
|
export default class HelpExtension extends Renderer.LensExtension {
|
||||||
globalPages = [
|
globalPages = [
|
||||||
{
|
{
|
||||||
id: "help",
|
id: "help",
|
||||||
@ -703,16 +745,19 @@ The following example shows how to add a `kubeObjectMenuItems` for namespace res
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { NamespaceMenuItem } from "./src/namespace-menu-item"
|
import { NamespaceMenuItem } from "./src/namespace-menu-item"
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
|
||||||
|
type Namespace = Renderer.K8sApi.Namespace;
|
||||||
|
|
||||||
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
kubeObjectMenuItems = [
|
kubeObjectMenuItems = [
|
||||||
{
|
{
|
||||||
kind: "Namespace",
|
kind: "Namespace",
|
||||||
apiVersions: ["v1"],
|
apiVersions: ["v1"],
|
||||||
components: {
|
components: {
|
||||||
MenuItem: (props: Component.KubeObjectMenuProps<K8sApi.Namespace>) => <NamespaceMenuItem {...props} />
|
MenuItem: (props: KubeObjectMenuProps<Namespace>) => <NamespaceMenuItem {...props} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -734,16 +779,28 @@ In this example a `NamespaceMenuItem` object is returned.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Component, K8sApi, Navigation} from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
export function NamespaceMenuItem(props: Component.KubeObjectMenuProps<K8sApi.Namespace>) {
|
const {
|
||||||
|
Component: {
|
||||||
|
terminalStore,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
Navigation,
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
|
||||||
|
type Namespace = Renderer.K8sApi.Namespace;
|
||||||
|
|
||||||
|
export function NamespaceMenuItem(props: KubeObjectMenuProps<Namespace>) {
|
||||||
const { object: namespace, toolbar } = props;
|
const { object: namespace, toolbar } = props;
|
||||||
if (!namespace) return null;
|
if (!namespace) return null;
|
||||||
|
|
||||||
const namespaceName = namespace.getName();
|
const namespaceName = namespace.getName();
|
||||||
|
|
||||||
const sendToTerminal = (command: string) => {
|
const sendToTerminal = (command: string) => {
|
||||||
Component.terminalStore.sendCommand(command, {
|
terminalStore.sendCommand(command, {
|
||||||
enter: true,
|
enter: true,
|
||||||
newTab: true,
|
newTab: true,
|
||||||
});
|
});
|
||||||
@ -755,21 +812,21 @@ export function NamespaceMenuItem(props: Component.KubeObjectMenuProps<K8sApi.Na
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem onClick={getPods}>
|
<MenuItem onClick={getPods}>
|
||||||
<Component.Icon material="speaker_group" interactive={toolbar} title="Get pods in terminal"/>
|
<Icon material="speaker_group" interactive={toolbar} title="Get pods in terminal"/>
|
||||||
<span className="title">Get Pods</span>
|
<span className="title">Get Pods</span>
|
||||||
</Component.MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`NamespaceMenuItem` returns a `Component.MenuItem` which defines the menu item's appearance and its behavior when activated via the `onClick` property.
|
`NamespaceMenuItem` returns a `Renderer.Component.MenuItem` which defines the menu item's appearance and its behavior when activated via the `onClick` property.
|
||||||
In the example, `getPods()` opens a terminal tab and runs `kubectl` to get a list of pods running in the current namespace.
|
In the example, `getPods()` opens a terminal tab and runs `kubectl` to get a list of pods running in the current namespace.
|
||||||
|
|
||||||
The name of the namespace is retrieved from `props` passed into `NamespaceMenuItem()`.
|
The name of the namespace is retrieved from `props` passed into `NamespaceMenuItem()`.
|
||||||
`namespace` is the `props.object`, which is of type `K8sApi.Namespace`.
|
`namespace` is the `props.object`, which is of type `Renderer.K8sApi.Namespace`.
|
||||||
`K8sApi.Namespace` is the API for accessing namespaces.
|
`Renderer.K8sApi.Namespace` is the API for accessing namespaces.
|
||||||
The current namespace in this example is simply given by `namespace.getName()`.
|
The current namespace in this example is simply given by `namespace.getName()`.
|
||||||
Thus, `kubeObjectMenuItems` afford convenient access to the specific resource selected by the user.
|
Thus, `kubeObjectMenuItems` afford convenient access to the specific resource selected by the user.
|
||||||
|
|
||||||
@ -783,18 +840,22 @@ These custom details appear on the details page for a specific resource, such as
|
|||||||
The following example shows how to use `kubeObjectDetailItems` to add a tabulated list of pods to the Namespace resource details page:
|
The following example shows how to use `kubeObjectDetailItems` to add a tabulated list of pods to the Namespace resource details page:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react";
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { NamespaceDetailsItem } from "./src/namespace-details-item"
|
import { NamespaceDetailsItem } from "./src/namespace-details-item";
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
|
||||||
|
type KubeObjectDetailsProps = Renderer.Component.KubeObjectDetailsProps;
|
||||||
|
type Namespace = Renderer.K8sApi.Namespace;
|
||||||
|
|
||||||
|
export default class ExampleExtension extends Renderer.LensExtension {
|
||||||
kubeObjectDetailItems = [
|
kubeObjectDetailItems = [
|
||||||
{
|
{
|
||||||
kind: "Namespace",
|
kind: "Namespace",
|
||||||
apiVersions: ["v1"],
|
apiVersions: ["v1"],
|
||||||
priority: 10,
|
priority: 10,
|
||||||
components: {
|
components: {
|
||||||
Details: (props: Component.KubeObjectDetailsProps<K8sApi.Namespace>) => <NamespaceDetailsItem {...props} />
|
Details: (props: KubeObjectDetailsProps<Namespace>) => <NamespaceDetailsItem {...props} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -814,25 +875,39 @@ In this example a `NamespaceDetailsItem` object is returned.
|
|||||||
`NamespaceDetailsItem` is defined in `./src/namespace-details-item.tsx`:
|
`NamespaceDetailsItem` is defined in `./src/namespace-details-item.tsx`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { Component, K8sApi } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { PodsDetailsList } from "./pods-details-list";
|
import { PodsDetailsList } from "./pods-details-list";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
@observer
|
const {
|
||||||
export class NamespaceDetailsItem extends React.Component<Component.KubeObjectDetailsProps<K8sApi.Namespace>> {
|
K8sApi: {
|
||||||
|
podsApi,
|
||||||
|
},
|
||||||
|
Component: {
|
||||||
|
DrawerTitle,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
@observable private pods: K8sApi.Pod[];
|
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
|
||||||
|
type Namespace = Renderer.K8sApi.Namespace;
|
||||||
|
type Pod = Renderer.K8sApi.Pod;
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class NamespaceDetailsItem extends React.Component<KubeObjectDetailsProps<Namespace>> {
|
||||||
|
@observable private pods: Pod[];
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
this.pods = await K8sApi.podsApi.list({namespace: this.props.object.getName()});
|
const namespace = this.props.object.getName();
|
||||||
|
|
||||||
|
this.pods = await podsApi.list({ namespace });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Component.DrawerTitle title="Pods" />
|
<DrawerTitle title="Pods" />
|
||||||
<PodsDetailsList pods={this.pods}/>
|
<PodsDetailsList pods={this.pods}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -840,14 +915,14 @@ export class NamespaceDetailsItem extends React.Component<Component.KubeObjectDe
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Since `NamespaceDetailsItem` extends `React.Component<Component.KubeObjectDetailsProps<K8sApi.Namespace>>`, it can access the current namespace object (type `K8sApi.Namespace`) through `this.props.object`.
|
Since `NamespaceDetailsItem` extends `React.Component<KubeObjectDetailsProps<Namespace>>`, it can access the current namespace object (type `Namespace`) through `this.props.object`.
|
||||||
You can query this object for many details about the current namespace.
|
You can query this object for many details about the current namespace.
|
||||||
In the example above, `componentDidMount()` gets the namespace's name using the `K8sApi.Namespace` `getName()` method.
|
In the example above, `componentDidMount()` gets the namespace's name using the `Namespace` `getName()` method.
|
||||||
Use the namespace's name to limit the list of pods only to those in the relevant namespace.
|
Use the namespace's name to limit the list of pods only to those in the relevant namespace.
|
||||||
To get this list of pods, this example uses the Kubernetes pods API `K8sApi.podsApi.list()` method.
|
To get this list of pods, this example uses the Kubernetes pods API `podsApi.list()` method.
|
||||||
The `K8sApi.podsApi` is automatically configured for the active cluster.
|
The `podsApi` is automatically configured for the active cluster.
|
||||||
|
|
||||||
Note that `K8sApi.podsApi.list()` is an asynchronous method.
|
Note that `podsApi.list()` is an asynchronous method.
|
||||||
Getting the pods list should occur prior to rendering the `NamespaceDetailsItem`.
|
Getting the pods list should occur prior to rendering the `NamespaceDetailsItem`.
|
||||||
It is a common technique in React development to await async calls in `componentDidMount()`.
|
It is a common technique in React development to await async calls in `componentDidMount()`.
|
||||||
However, `componentDidMount()` is called right after the first call to `render()`.
|
However, `componentDidMount()` is called right after the first call to `render()`.
|
||||||
@ -856,51 +931,59 @@ Like in the [`appPreferences` guide](#apppreferences), [`mobx`](https://mobx.js.
|
|||||||
This is done simply by marking the `pods` field as an `observable` and the `NamespaceDetailsItem` class itself as an `observer`.
|
This is done simply by marking the `pods` field as an `observable` and the `NamespaceDetailsItem` class itself as an `observer`.
|
||||||
|
|
||||||
Finally, the `NamespaceDetailsItem` renders using the `render()` method.
|
Finally, the `NamespaceDetailsItem` renders using the `render()` method.
|
||||||
Details are placed in drawers, and using `Component.DrawerTitle` provides a separator from details above this one.
|
Details are placed in drawers, and using `Renderer.Component.DrawerTitle` provides a separator from details above this one.
|
||||||
Multiple details in a drawer can be placed in `<Component.DrawerItem>` elements for further separation, if desired.
|
Multiple details in a drawer can be placed in `<Renderer.Component.DrawerItem>` elements for further separation, if desired.
|
||||||
The rest of this example's details are defined in `PodsDetailsList`, found in `./pods-details-list.tsx`:
|
The rest of this example's details are defined in `PodsDetailsList`, found in `./pods-details-list.tsx`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Component, K8sApi } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
Table,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
|
type Pod = Renderer.K8sApi.Pod;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pods: K8sApi.Pod[];
|
pods?: Pod[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PodsDetailsList extends React.Component<Props> {
|
export class PodsDetailsList extends React.Component<Props> {
|
||||||
|
getTableRow = (pod: Pod) => {
|
||||||
getTableRow(index: number) {
|
return (
|
||||||
const {pods} = this.props;
|
<TableRow key={index} nowrap>
|
||||||
return (
|
<TableCell className="podName">{pods[index].getName()}</TableCell>
|
||||||
<Component.TableRow key={index} nowrap>
|
<TableCell className="podAge">{pods[index].getAge()}</TableCell>
|
||||||
<Component.TableCell className="podName">{pods[index].getName()}</Component.TableCell>
|
<TableCell className="podStatus">{pods[index].getStatus()}</TableCell>
|
||||||
<Component.TableCell className="podAge">{pods[index].getAge()}</Component.TableCell>
|
</TableRow>
|
||||||
<Component.TableCell className="podStatus">{pods[index].getStatus()}</Component.TableCell>
|
)
|
||||||
</Component.TableRow>
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {pods} = this.props
|
const { pods } = this.props
|
||||||
if (!pods?.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
if (!pods?.length) {
|
||||||
<div >
|
return null;
|
||||||
<Component.Table>
|
}
|
||||||
<Component.TableHead>
|
|
||||||
<Component.TableCell className="podName">Name</Component.TableCell>
|
return (
|
||||||
<Component.TableCell className="podAge">Age</Component.TableCell>
|
<div>
|
||||||
<Component.TableCell className="podStatus">Status</Component.TableCell>
|
<Table>
|
||||||
</Component.TableHead>
|
<TableHead>
|
||||||
{
|
<TableCell className="podName">Name</TableCell>
|
||||||
pods.map((pod, index) => this.getTableRow(index))
|
<TableCell className="podAge">Age</TableCell>
|
||||||
}
|
<TableCell className="podStatus">Status</TableCell>
|
||||||
</Component.Table>
|
</TableHead>
|
||||||
</div>
|
{ pods.map(this.getTableRow) }
|
||||||
)
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -909,9 +992,9 @@ export class PodsDetailsList extends React.Component<Props> {
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Obtain the name, age, and status for each pod using the `K8sApi.Pod` methods.
|
Obtain the name, age, and status for each pod using the `Renderer.K8sApi.Pod` methods.
|
||||||
Construct the table using the `Component.Table` and related elements.
|
Construct the table using the `Renderer.Component.Table` and related elements.
|
||||||
|
|
||||||
For each pod the name, age, and status are obtained using the `K8sApi.Pod` methods.
|
For each pod the name, age, and status are obtained using the `Renderer.K8sApi.Pod` methods.
|
||||||
The table is constructed using the `Component.Table` and related elements.
|
The table is constructed using the `Renderer.Component.Table` and related elements.
|
||||||
See [Component documentation](https://docs.k8slens.dev/latest/extensions/api/modules/_renderer_api_components_/) for further details.
|
See [Component documentation](https://docs.k8slens.dev/latest/extensions/api/modules/_renderer_api_components_/) for further details.
|
||||||
|
|||||||
@ -24,14 +24,14 @@ This is so that your data is kept up to date and not persisted longer than expec
|
|||||||
The following example code creates a store for the `appPreferences` guide example:
|
The following example code creates a store for the `appPreferences` guide example:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { Store } from "@k8slens/extensions";
|
import { Common } from "@k8slens/extensions";
|
||||||
import { observable, makeObservable } from "mobx";
|
import { observable, makeObservable } from "mobx";
|
||||||
|
|
||||||
export type ExamplePreferencesModel = {
|
export type ExamplePreferencesModel = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ExamplePreferencesStore extends Store.ExtensionStore<ExamplePreferencesModel> {
|
export class ExamplePreferencesStore extends Common.Store.ExtensionStore<ExamplePreferencesModel> {
|
||||||
|
|
||||||
@observable enabled = false;
|
@observable enabled = false;
|
||||||
|
|
||||||
@ -87,10 +87,10 @@ The following example code, modified from the [`appPreferences`](../renderer-ext
|
|||||||
This can be done in `./main.ts`:
|
This can be done in `./main.ts`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensMainExtension } from "@k8slens/extensions";
|
import { Main } from "@k8slens/extensions";
|
||||||
import { ExamplePreferencesStore } from "./src/example-preference-store";
|
import { ExamplePreferencesStore } from "./src/example-preference-store";
|
||||||
|
|
||||||
export default class ExampleMainExtension extends LensMainExtension {
|
export default class ExampleMainExtension extends Main.LensExtension {
|
||||||
async onActivate() {
|
async onActivate() {
|
||||||
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
|
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
|
||||||
}
|
}
|
||||||
@ -102,12 +102,12 @@ Similarly, `ExamplePreferencesStore` must load in the renderer process where the
|
|||||||
This can be done in `./renderer.ts`:
|
This can be done in `./renderer.ts`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
||||||
import { ExamplePreferencesStore } from "./src/example-preference-store";
|
import { ExamplePreferencesStore } from "./src/example-preference-store";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default class ExampleRendererExtension extends LensRendererExtension {
|
export default class ExampleRendererExtension extends Renderer.LensExtension {
|
||||||
|
|
||||||
async onActivate() {
|
async onActivate() {
|
||||||
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
|
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
|
||||||
@ -130,17 +130,23 @@ Again, `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)` is ca
|
|||||||
`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`:
|
`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { Component } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ExamplePreferencesStore } from "./example-preference-store";
|
import { ExamplePreferencesStore } from "./example-preference-store";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
Checkbox,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ExamplePreferenceInput extends React.Component {
|
export class ExamplePreferenceInput extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Component.Checkbox
|
<Checkbox
|
||||||
label="I understand appPreferences"
|
label="I understand appPreferences"
|
||||||
value={ExamplePreferencesStore.getInstace().enabled}
|
value={ExamplePreferencesStore.getInstace().enabled}
|
||||||
onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }}
|
onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }}
|
||||||
|
|||||||
@ -14,7 +14,13 @@ My component `GlobalPageMenuIcon`
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Component: { Icon } } from "@k8slens/extensions";
|
import { Renderer } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component: {
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
} = Renderer;
|
||||||
|
|
||||||
const GlobalPageMenuIcon = ({ navigate }: { navigate?: () => void }): JSX.Element => (
|
const GlobalPageMenuIcon = ({ navigate }: { navigate?: () => void }): JSX.Element => (
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kube-object-event-status",
|
"name": "kube-object-event-status",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"description": "Adds kube object status from events",
|
"description": "Adds kube object status from events",
|
||||||
"renderer": "dist/renderer.js",
|
"renderer": "dist/renderer.js",
|
||||||
"lens": {
|
"lens": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lens-metrics-cluster-feature",
|
"name": "lens-metrics-cluster-feature",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"description": "Lens metrics cluster feature",
|
"description": "Lens metrics cluster feature",
|
||||||
"renderer": "dist/renderer.js",
|
"renderer": "dist/renderer.js",
|
||||||
"lens": {
|
"lens": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lens-node-menu",
|
"name": "lens-node-menu",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"description": "Lens node menu",
|
"description": "Lens node menu",
|
||||||
"renderer": "dist/renderer.js",
|
"renderer": "dist/renderer.js",
|
||||||
"lens": {
|
"lens": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lens-pod-menu",
|
"name": "lens-pod-menu",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"description": "Lens pod menu",
|
"description": "Lens pod menu",
|
||||||
"renderer": "dist/renderer.js",
|
"renderer": "dist/renderer.js",
|
||||||
"lens": {
|
"lens": {
|
||||||
|
|||||||
@ -64,6 +64,8 @@ export async function waitForMinikubeDashboard(app: Application) {
|
|||||||
await app.client.setValue(".Input.SearchInput input", "minikube");
|
await app.client.setValue(".Input.SearchInput input", "minikube");
|
||||||
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
await app.client.waitUntilTextExists("div.TableCell", "minikube");
|
||||||
await app.client.click("div.TableRow");
|
await app.client.click("div.TableRow");
|
||||||
|
await app.client.waitUntilTextExists("div.drawer-title-text", "KubernetesCluster: minikube");
|
||||||
|
await app.client.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||||
await app.client.frame("minikube");
|
await app.client.frame("minikube");
|
||||||
|
|||||||
13
package.json
13
package.json
@ -2,6 +2,7 @@
|
|||||||
"name": "open-lens",
|
"name": "open-lens",
|
||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.0.0-beta.6",
|
"version": "5.0.0-beta.6",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
@ -178,7 +179,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/call": "^8.0.0",
|
"@hapi/call": "^8.0.1",
|
||||||
"@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",
|
"abort-controller": "^3.0.0",
|
||||||
@ -267,7 +268,7 @@
|
|||||||
"@types/jsdom": "^16.2.4",
|
"@types/jsdom": "^16.2.4",
|
||||||
"@types/jsonpath": "^0.2.0",
|
"@types/jsonpath": "^0.2.0",
|
||||||
"@types/lodash": "^4.14.155",
|
"@types/lodash": "^4.14.155",
|
||||||
"@types/marked": "^0.7.4",
|
"@types/marked": "^2.0.3",
|
||||||
"@types/md5-file": "^4.0.2",
|
"@types/md5-file": "^4.0.2",
|
||||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||||
"@types/mock-fs": "^4.10.0",
|
"@types/mock-fs": "^4.10.0",
|
||||||
@ -281,7 +282,7 @@
|
|||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"@types/react-router-dom": "^5.1.6",
|
"@types/react-router-dom": "^5.1.6",
|
||||||
"@types/react-select": "^3.0.13",
|
"@types/react-select": "3.1.2",
|
||||||
"@types/react-table": "^7.7.0",
|
"@types/react-table": "^7.7.0",
|
||||||
"@types/react-window": "^1.8.2",
|
"@types/react-window": "^1.8.2",
|
||||||
"@types/readable-stream": "^2.3.9",
|
"@types/readable-stream": "^2.3.9",
|
||||||
@ -333,7 +334,7 @@
|
|||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"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": "^1.6.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",
|
||||||
@ -348,14 +349,14 @@
|
|||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-refresh": "^0.9.0",
|
"react-refresh": "^0.9.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-select": "^3.1.0",
|
"react-select": "3.1.1",
|
||||||
"react-select-event": "^5.1.0",
|
"react-select-event": "^5.1.0",
|
||||||
"react-table": "^7.7.0",
|
"react-table": "^7.7.0",
|
||||||
"react-window": "^1.8.5",
|
"react-window": "^1.8.5",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"sharp": "^0.26.1",
|
"sharp": "^0.26.1",
|
||||||
"spectron": "11.0.0",
|
"spectron": "11.0.0",
|
||||||
"style-loader": "^1.2.1",
|
"style-loader": "^2.0.0",
|
||||||
"tailwindcss": "^2.1.2",
|
"tailwindcss": "^2.1.2",
|
||||||
"ts-jest": "26.3.0",
|
"ts-jest": "26.3.0",
|
||||||
"ts-loader": "^7.0.5",
|
"ts-loader": "^7.0.5",
|
||||||
|
|||||||
@ -104,6 +104,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
|||||||
context.menuItems = [
|
context.menuItems = [
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
|
icon: "edit",
|
||||||
onlyVisibleForSource: "local",
|
onlyVisibleForSource: "local",
|
||||||
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
|
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
|
||||||
},
|
},
|
||||||
@ -112,6 +113,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
|||||||
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
|
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
|
||||||
context.menuItems.push({
|
context.menuItems.push({
|
||||||
title: "Delete",
|
title: "Delete",
|
||||||
|
icon: "delete",
|
||||||
onlyVisibleForSource: "local",
|
onlyVisibleForSource: "local",
|
||||||
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
|
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
|
||||||
confirm: {
|
confirm: {
|
||||||
@ -123,6 +125,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
|||||||
if (this.status.phase == "connected") {
|
if (this.status.phase == "connected") {
|
||||||
context.menuItems.push({
|
context.menuItems.push({
|
||||||
title: "Disconnect",
|
title: "Disconnect",
|
||||||
|
icon: "link_off",
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
requestMain(clusterDisconnectHandler, this.metadata.uid);
|
requestMain(clusterDisconnectHandler, this.metadata.uid);
|
||||||
}
|
}
|
||||||
@ -130,6 +133,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
|
|||||||
} else {
|
} else {
|
||||||
context.menuItems.push({
|
context.menuItems.push({
|
||||||
title: "Connect",
|
title: "Connect",
|
||||||
|
icon: "link",
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
context.navigate(`/cluster/${this.metadata.uid}`);
|
context.navigate(`/cluster/${this.metadata.uid}`);
|
||||||
}
|
}
|
||||||
@ -147,7 +151,7 @@ export class KubernetesClusterCategory extends CatalogCategory {
|
|||||||
public readonly kind = "CatalogCategory";
|
public readonly kind = "CatalogCategory";
|
||||||
public metadata = {
|
public metadata = {
|
||||||
name: "Kubernetes Clusters",
|
name: "Kubernetes Clusters",
|
||||||
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default // eslint-disable-line
|
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default, // eslint-disable-line
|
||||||
};
|
};
|
||||||
public spec: CatalogCategorySpec = {
|
public spec: CatalogCategorySpec = {
|
||||||
group: "entity.k8slens.dev",
|
group: "entity.k8slens.dev",
|
||||||
|
|||||||
@ -96,9 +96,25 @@ export interface CatalogEntityActionContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogEntityContextMenu {
|
export interface CatalogEntityContextMenu {
|
||||||
|
/**
|
||||||
|
* Menu title
|
||||||
|
*/
|
||||||
title: string;
|
title: string;
|
||||||
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
|
/**
|
||||||
|
* Menu icon
|
||||||
|
*/
|
||||||
|
icon?: string;
|
||||||
|
/**
|
||||||
|
* Show only if empty or if value matches with entity.metadata.source
|
||||||
|
*/
|
||||||
|
onlyVisibleForSource?: string;
|
||||||
|
/**
|
||||||
|
* OnClick handler
|
||||||
|
*/
|
||||||
onClick: () => void | Promise<void>;
|
onClick: () => void | Promise<void>;
|
||||||
|
/**
|
||||||
|
* Confirm click with a message
|
||||||
|
*/
|
||||||
confirm?: {
|
confirm?: {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
@ -175,7 +191,6 @@ export abstract class CatalogEntity<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>;
|
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>;
|
||||||
public abstract onDetailsOpen(context: CatalogEntityActionContext): void | Promise<void>;
|
|
||||||
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>;
|
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>;
|
||||||
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
|
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,9 +25,12 @@ import { appEventBus } from "./event-bus";
|
|||||||
import { ResourceApplier } from "../main/resource-applier";
|
import { ResourceApplier } from "../main/resource-applier";
|
||||||
import { ipcMain, IpcMainInvokeEvent } from "electron";
|
import { ipcMain, IpcMainInvokeEvent } from "electron";
|
||||||
import { clusterFrameMap } from "./cluster-frames";
|
import { clusterFrameMap } from "./cluster-frames";
|
||||||
|
import { catalogEntityRegistry } from "../main/catalog";
|
||||||
|
import type { KubernetesCluster } from "./catalog-entities";
|
||||||
|
|
||||||
export const clusterActivateHandler = "cluster:activate";
|
export const clusterActivateHandler = "cluster:activate";
|
||||||
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
|
||||||
|
export const clusterVisibilityHandler = "cluster:visibility";
|
||||||
export const clusterRefreshHandler = "cluster:refresh";
|
export const clusterRefreshHandler = "cluster:refresh";
|
||||||
export const clusterDisconnectHandler = "cluster:disconnect";
|
export const clusterDisconnectHandler = "cluster:disconnect";
|
||||||
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
||||||
@ -49,6 +52,18 @@ if (ipcMain) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
handleRequest(clusterVisibilityHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId, visible: boolean) => {
|
||||||
|
const entity = catalogEntityRegistry.getById<KubernetesCluster>(clusterId);
|
||||||
|
|
||||||
|
for (const kubeEntity of catalogEntityRegistry.getItemsForApiKind(entity.apiVersion, entity.kind)) {
|
||||||
|
kubeEntity.status.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity) {
|
||||||
|
entity.status.active = visible;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
||||||
return ClusterStore.getInstance()
|
return ClusterStore.getInstance()
|
||||||
.getById(clusterId)
|
.getById(clusterId)
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import { match, matchPath } from "react-router";
|
import { match, matchPath } from "react-router";
|
||||||
import { countBy } from "lodash";
|
import { countBy } from "lodash";
|
||||||
import { Singleton } from "../utils";
|
import { iter, Singleton } from "../utils";
|
||||||
import { pathToRegexp } from "path-to-regexp";
|
import { pathToRegexp } from "path-to-regexp";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import type Url from "url-parse";
|
import type Url from "url-parse";
|
||||||
@ -35,7 +35,8 @@ import type { RouteHandler, RouteParams } from "../../extensions/registries/prot
|
|||||||
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
||||||
|
|
||||||
export const ProtocolHandlerInternal = `${ProtocolHandlerIpcPrefix}:internal`;
|
export const ProtocolHandlerInternal = `${ProtocolHandlerIpcPrefix}:internal`;
|
||||||
export const ProtocolHandlerExtension= `${ProtocolHandlerIpcPrefix}:extension`;
|
export const ProtocolHandlerExtension = `${ProtocolHandlerIpcPrefix}:extension`;
|
||||||
|
export const ProtocolHandlerInvalid = `${ProtocolHandlerIpcPrefix}:invalid`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These two names are long and cumbersome by design so as to decrease the chances
|
* These two names are long and cumbersome by design so as to decrease the chances
|
||||||
@ -47,6 +48,34 @@ export const ProtocolHandlerExtension= `${ProtocolHandlerIpcPrefix}:extension`;
|
|||||||
export const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH";
|
export const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH";
|
||||||
export const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH";
|
export const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned from routing attempts
|
||||||
|
*/
|
||||||
|
export enum RouteAttempt {
|
||||||
|
/**
|
||||||
|
* A handler was found in the set of registered routes
|
||||||
|
*/
|
||||||
|
MATCHED = "matched",
|
||||||
|
/**
|
||||||
|
* A handler was not found within the set of registered routes
|
||||||
|
*/
|
||||||
|
MISSING = "missing",
|
||||||
|
/**
|
||||||
|
* The extension that was matched in the route was not activated
|
||||||
|
*/
|
||||||
|
MISSING_EXTENSION = "no-extension",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: RouteAttempt): RouteAttempt {
|
||||||
|
switch (mainAttempt) {
|
||||||
|
case RouteAttempt.MATCHED:
|
||||||
|
return RouteAttempt.MATCHED;
|
||||||
|
case RouteAttempt.MISSING:
|
||||||
|
case RouteAttempt.MISSING_EXTENSION:
|
||||||
|
return rendererAttempt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class LensProtocolRouter extends Singleton {
|
export abstract class LensProtocolRouter extends Singleton {
|
||||||
// Map between path schemas and the handlers
|
// Map between path schemas and the handlers
|
||||||
protected internalRoutes = new Map<string, RouteHandler>();
|
protected internalRoutes = new Map<string, RouteHandler>();
|
||||||
@ -56,11 +85,12 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Attempts to route the given URL to all internal routes that have been registered
|
||||||
* @param url the parsed URL that initiated the `lens://` protocol
|
* @param url the parsed URL that initiated the `lens://` protocol
|
||||||
|
* @returns true if a route has been found
|
||||||
*/
|
*/
|
||||||
protected _routeToInternal(url: Url): void {
|
protected _routeToInternal(url: Url): RouteAttempt {
|
||||||
this._route(Array.from(this.internalRoutes.entries()), url);
|
return this._route(this.internalRoutes.entries(), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,7 +99,7 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
* @param routes the array of path schemas, handler pairs to match against
|
* @param routes the array of path schemas, handler pairs to match against
|
||||||
* @param url the url (in its current state)
|
* @param url the url (in its current state)
|
||||||
*/
|
*/
|
||||||
protected _findMatchingRoute(routes: [string, RouteHandler][], url: Url): null | [match<Record<string, string>>, RouteHandler] {
|
protected _findMatchingRoute(routes: Iterable<[string, RouteHandler]>, url: Url): null | [match<Record<string, string>>, RouteHandler] {
|
||||||
const matches: [match<Record<string, string>>, RouteHandler][] = [];
|
const matches: [match<Record<string, string>>, RouteHandler][] = [];
|
||||||
|
|
||||||
for (const [schema, handler] of routes) {
|
for (const [schema, handler] of routes) {
|
||||||
@ -96,7 +126,7 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
* @param routes the array of (path schemas, handler) pairs to match against
|
* @param routes the array of (path schemas, handler) pairs to match against
|
||||||
* @param url the url (in its current state)
|
* @param url the url (in its current state)
|
||||||
*/
|
*/
|
||||||
protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void {
|
protected _route(routes: Iterable<[string, RouteHandler]>, url: Url, extensionName?: string): RouteAttempt {
|
||||||
const route = this._findMatchingRoute(routes, url);
|
const route = this._findMatchingRoute(routes, url);
|
||||||
|
|
||||||
if (!route) {
|
if (!route) {
|
||||||
@ -106,7 +136,9 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
data.extensionName = extensionName;
|
data.extensionName = extensionName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return void logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data);
|
logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data);
|
||||||
|
|
||||||
|
return RouteAttempt.MISSING;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [match, handler] = route;
|
const [match, handler] = route;
|
||||||
@ -121,6 +153,8 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handler(params);
|
handler(params);
|
||||||
|
|
||||||
|
return RouteAttempt.MATCHED;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -174,23 +208,22 @@ export abstract class LensProtocolRouter extends Singleton {
|
|||||||
* Note: this function modifies its argument, do not reuse
|
* Note: this function modifies its argument, do not reuse
|
||||||
* @param url the protocol request URI that was "open"-ed
|
* @param url the protocol request URI that was "open"-ed
|
||||||
*/
|
*/
|
||||||
protected async _routeToExtension(url: Url): Promise<void> {
|
protected async _routeToExtension(url: Url): Promise<RouteAttempt> {
|
||||||
const extension = await this._findMatchingExtensionByName(url);
|
const extension = await this._findMatchingExtensionByName(url);
|
||||||
|
|
||||||
if (typeof extension === "string") {
|
if (typeof extension === "string") {
|
||||||
// failed to find an extension, it returned its name
|
// failed to find an extension, it returned its name
|
||||||
return;
|
return RouteAttempt.MISSING_EXTENSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the extension name from the path name so we don't need to match on it anymore
|
// 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));
|
url.set("pathname", url.pathname.slice(extension.name.length + 1));
|
||||||
|
|
||||||
const handlers = extension
|
|
||||||
.protocolHandlers
|
|
||||||
.map<[string, RouteHandler]>(({ pathSchema, handler }) => [pathSchema, handler]);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._route(handlers, url, extension.name);
|
const handlers = iter.map(extension.protocolHandlers, ({ pathSchema, handler }) => [pathSchema, handler] as [string, RouteHandler]);
|
||||||
|
|
||||||
|
return this._route(handlers, url, extension.name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RoutingError) {
|
if (error instanceof RoutingError) {
|
||||||
error.extensionName = extension.name;
|
error.extensionName = extension.name;
|
||||||
|
|||||||
@ -41,3 +41,11 @@ export function buildURL<P extends object = {}, Q extends object = {}>(path: str
|
|||||||
return parts.filter(Boolean).join("");
|
return parts.filter(Boolean).join("");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildURLPositional<P extends object = {}, Q extends object = {}>(path: string | any) {
|
||||||
|
const builder = buildURL(path);
|
||||||
|
|
||||||
|
return function(params?: P, query?: Q, fragment?: string): string {
|
||||||
|
return builder({ params, query, fragment });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export const isPublishConfigured = Object.keys(packageInfo.build).includes("publ
|
|||||||
|
|
||||||
export const productName = packageInfo.productName;
|
export const productName = packageInfo.productName;
|
||||||
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
||||||
export const publicPath = "/build/";
|
export const publicPath = "/build/" as string;
|
||||||
|
|
||||||
// Webpack build paths
|
// Webpack build paths
|
||||||
export const contextDir = process.cwd();
|
export const contextDir = process.cwd();
|
||||||
@ -60,13 +60,13 @@ defineGlobal("__static", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Apis
|
// Apis
|
||||||
export const apiPrefix = "/api"; // local router apis
|
export const apiPrefix = "/api" as string; // local router apis
|
||||||
export const apiKubePrefix = "/api-kube"; // k8s cluster apis
|
export const apiKubePrefix = "/api-kube" as string; // k8s cluster apis
|
||||||
|
|
||||||
// Links
|
// Links
|
||||||
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues";
|
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string;
|
||||||
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI";
|
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" as string;
|
||||||
export const supportUrl = "https://docs.k8slens.dev/latest/support/";
|
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
||||||
|
|
||||||
// This explicitly ignores the prerelease info on the package version
|
// This explicitly ignores the prerelease info on the package version
|
||||||
export const appSemVer = new SemVer(packageInfo.version);
|
export const appSemVer = new SemVer(packageInfo.version);
|
||||||
|
|||||||
@ -250,6 +250,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
registries.statusBarRegistry.add(extension.statusBarItems),
|
registries.statusBarRegistry.add(extension.statusBarItems),
|
||||||
registries.commandRegistry.add(extension.commands),
|
registries.commandRegistry.add(extension.commands),
|
||||||
registries.welcomeMenuRegistry.add(extension.welcomeMenus),
|
registries.welcomeMenuRegistry.add(extension.welcomeMenus),
|
||||||
|
registries.catalogEntityDetailRegistry.add(extension.catalogEntityDetailItems),
|
||||||
];
|
];
|
||||||
|
|
||||||
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
||||||
@ -279,6 +280,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
registries.kubeObjectMenuRegistry.add(extension.kubeObjectMenuItems),
|
registries.kubeObjectMenuRegistry.add(extension.kubeObjectMenuItems),
|
||||||
registries.kubeObjectDetailRegistry.add(extension.kubeObjectDetailItems),
|
registries.kubeObjectDetailRegistry.add(extension.kubeObjectDetailItems),
|
||||||
registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts),
|
registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts),
|
||||||
|
registries.workloadsOverviewDetailRegistry.add(extension.kubeWorkloadsOverviewItems),
|
||||||
registries.commandRegistry.add(extension.commands),
|
registries.commandRegistry.add(extension.commands),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -20,8 +20,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppPreferenceRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration,
|
AppPreferenceRegistration, CatalogEntityDetailRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration,
|
||||||
KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration,
|
KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, WelcomeMenuRegistration, WorkloadsOverviewDetailRegistration,
|
||||||
} from "./registries";
|
} from "./registries";
|
||||||
import type { Cluster } from "../main/cluster";
|
import type { Cluster } from "../main/cluster";
|
||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
@ -40,8 +40,10 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
statusBarItems: StatusBarRegistration[] = [];
|
statusBarItems: StatusBarRegistration[] = [];
|
||||||
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
|
||||||
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
|
||||||
|
kubeWorkloadsOverviewItems: WorkloadsOverviewDetailRegistration[] = [];
|
||||||
commands: CommandRegistration[] = [];
|
commands: CommandRegistration[] = [];
|
||||||
welcomeMenus: WelcomeMenuRegistration[] = [];
|
welcomeMenus: WelcomeMenuRegistration[] = [];
|
||||||
|
catalogEntityDetailItems: CatalogEntityDetailRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||||
const { navigate } = await import("../renderer/navigation");
|
const { navigate } = await import("../renderer/navigation");
|
||||||
|
|||||||
@ -20,11 +20,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Catalog from "./catalog";
|
import * as Catalog from "./catalog";
|
||||||
|
import * as Navigation from "./navigation";
|
||||||
import { IpcMain as Ipc } from "../ipc/ipc-main";
|
import { IpcMain as Ipc } from "../ipc/ipc-main";
|
||||||
import { LensMainExtension as LensExtension } from "../lens-main-extension";
|
import { LensMainExtension as LensExtension } from "../lens-main-extension";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Catalog,
|
Catalog,
|
||||||
|
Navigation,
|
||||||
Ipc,
|
Ipc,
|
||||||
LensExtension,
|
LensExtension,
|
||||||
};
|
};
|
||||||
|
|||||||
26
src/extensions/main-api/navigation.ts
Normal file
26
src/extensions/main-api/navigation.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WindowManager } from "../../main/window-manager";
|
||||||
|
|
||||||
|
export function navigate(url: string) {
|
||||||
|
return WindowManager.getInstance().navigate(url);
|
||||||
|
}
|
||||||
46
src/extensions/registries/catalog-entity-detail-registry.ts
Normal file
46
src/extensions/registries/catalog-entity-detail-registry.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from "react";
|
||||||
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
|
export interface CatalogEntityDetailComponents {
|
||||||
|
Details: React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogEntityDetailRegistration {
|
||||||
|
kind: string;
|
||||||
|
apiVersions: string[];
|
||||||
|
components: CatalogEntityDetailComponents;
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration> {
|
||||||
|
getItemsForKind(kind: string, apiVersion: string) {
|
||||||
|
const items = this.getItems().filter((item) => {
|
||||||
|
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
||||||
|
});
|
||||||
|
|
||||||
|
return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const catalogEntityDetailRegistry = new CatalogEntityDetailRegistry();
|
||||||
@ -33,3 +33,5 @@ export * from "./command-registry";
|
|||||||
export * from "./entity-setting-registry";
|
export * from "./entity-setting-registry";
|
||||||
export * from "./welcome-menu-registry";
|
export * from "./welcome-menu-registry";
|
||||||
export * from "./protocol-handler-registry";
|
export * from "./protocol-handler-registry";
|
||||||
|
export * from "./catalog-entity-detail-registry";
|
||||||
|
export * from "./workloads-overview-detail-registry";
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from "react";
|
||||||
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
|
export interface WorkloadsOverviewDetailComponents {
|
||||||
|
Details: React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkloadsOverviewDetailRegistration {
|
||||||
|
components: WorkloadsOverviewDetailComponents;
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkloadsOverviewDetailRegistry extends BaseRegistry<WorkloadsOverviewDetailRegistration> {
|
||||||
|
getItems() {
|
||||||
|
const items = super.getItems();
|
||||||
|
|
||||||
|
return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const workloadsOverviewDetailRegistry = new WorkloadsOverviewDetailRegistry();
|
||||||
@ -53,6 +53,7 @@ export * from "../../renderer/components/stepper";
|
|||||||
export * from "../../renderer/components/wizard";
|
export * from "../../renderer/components/wizard";
|
||||||
export * from "../../renderer/components/+workloads-pods/pod-details-list";
|
export * from "../../renderer/components/+workloads-pods/pod-details-list";
|
||||||
export * from "../../renderer/components/+namespaces/namespace-select";
|
export * from "../../renderer/components/+namespaces/namespace-select";
|
||||||
|
export * from "../../renderer/components/+namespaces/namespace-select-filter";
|
||||||
export * from "../../renderer/components/layout/sub-title";
|
export * from "../../renderer/components/layout/sub-title";
|
||||||
export * from "../../renderer/components/input/search-input";
|
export * from "../../renderer/components/input/search-input";
|
||||||
export * from "../../renderer/components/chart/bar-chart";
|
export * from "../../renderer/components/chart/bar-chart";
|
||||||
|
|||||||
@ -48,6 +48,15 @@ export class CatalogEntityRegistry {
|
|||||||
return allItems.filter((entity) => this.categoryRegistry.getCategoryForEntity(entity) !== undefined);
|
return allItems.filter((entity) => this.categoryRegistry.getCategoryForEntity(entity) !== undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getById<T extends CatalogEntity>(id: string): T | undefined {
|
||||||
|
const item = this.items.find((entity) => entity.metadata.uid === id);
|
||||||
|
|
||||||
|
if (item) return item as T;
|
||||||
|
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||||
const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
|
const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
|
||||||
|
|
||||||
|
|||||||
@ -79,8 +79,7 @@ export class ClusterManager extends Singleton {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
|
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
|
||||||
|
|
||||||
entity.status.phase = cluster.disconnected ? "disconnected" : "connected";
|
this.updateEntityStatus(entity, cluster);
|
||||||
entity.status.active = !cluster.disconnected;
|
|
||||||
|
|
||||||
if (cluster.preferences?.clusterName) {
|
if (cluster.preferences?.clusterName) {
|
||||||
entity.metadata.name = cluster.preferences.clusterName;
|
entity.metadata.name = cluster.preferences.clusterName;
|
||||||
@ -101,6 +100,10 @@ export class ClusterManager extends Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected updateEntityStatus(entity: KubernetesCluster, cluster: Cluster) {
|
||||||
|
entity.status.phase = cluster.accessible ? "connected" : "disconnected";
|
||||||
|
}
|
||||||
|
|
||||||
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
const cluster = this.store.getById(entity.metadata.uid);
|
const cluster = this.store.getById(entity.metadata.uid);
|
||||||
@ -118,10 +121,7 @@ export class ClusterManager extends Singleton {
|
|||||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||||
cluster.contextName = entity.spec.kubeconfigContext;
|
cluster.contextName = entity.spec.kubeconfigContext;
|
||||||
|
|
||||||
entity.status = {
|
this.updateEntityStatus(entity, cluster);
|
||||||
phase: cluster.disconnected ? "disconnected" : "connected",
|
|
||||||
active: !cluster.disconnected
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
import * as tempy from "tempy";
|
import * as tempy from "tempy";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import { promiseExec} from "../promise-exec";
|
import { promiseExec } from "../promise-exec";
|
||||||
import { helmCli } from "./helm-cli";
|
import { helmCli } from "./helm-cli";
|
||||||
import type { Cluster } from "../cluster";
|
import type { Cluster } from "../cluster";
|
||||||
import { toCamelCase } from "../../common/utils/camelCase";
|
import { toCamelCase } from "../../common/utils/camelCase";
|
||||||
@ -39,7 +39,7 @@ export async function listReleases(pathToKubeconfig: string, namespace?: string)
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
output.forEach((release: any, index: number) => {
|
output.forEach((release: any, index: number) => {
|
||||||
output[index] = toCamelCase(release);
|
output[index] = toCamelCase(release);
|
||||||
});
|
});
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
@ -49,9 +49,9 @@ export async function listReleases(pathToKubeconfig: string, namespace?: string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function installChart(chart: string, values: any, name: string | undefined, namespace: string, version: string, pathToKubeconfig: string){
|
export async function installChart(chart: string, values: any, name: string | undefined, namespace: string, version: string, pathToKubeconfig: string) {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const fileName = tempy.file({name: "values.yaml"});
|
const fileName = tempy.file({ name: "values.yaml" });
|
||||||
|
|
||||||
await fse.writeFile(fileName, yaml.safeDump(values));
|
await fse.writeFile(fileName, yaml.safeDump(values));
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ export async function installChart(chart: string, values: any, name: string | un
|
|||||||
|
|
||||||
export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster) {
|
export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster) {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const fileName = tempy.file({name: "values.yaml"});
|
const fileName = tempy.file({ name: "values.yaml" });
|
||||||
|
|
||||||
await fse.writeFile(fileName, yaml.safeDump(values));
|
await fse.writeFile(fileName, yaml.safeDump(values));
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ export async function getRelease(name: string, namespace: string, cluster: Clust
|
|||||||
export async function deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
|
export async function deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
|
||||||
try {
|
try {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
} catch ({ stderr }) {
|
} catch ({ stderr }) {
|
||||||
@ -127,10 +127,16 @@ export async function deleteRelease(name: string, namespace: string, pathToKubec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getValues(name: string, namespace: string, all: boolean, pathToKubeconfig: string) {
|
interface GetValuesOptions {
|
||||||
|
namespace: string;
|
||||||
|
all?: boolean;
|
||||||
|
pathToKubeconfig: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getValues(name: string, { namespace, all = false, pathToKubeconfig }: GetValuesOptions) {
|
||||||
try {
|
try {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout, } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all": ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
const { stdout } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all" : ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
} catch ({ stderr }) {
|
} catch ({ stderr }) {
|
||||||
@ -167,8 +173,8 @@ async function getResources(name: string, namespace: string, cluster: Cluster) {
|
|||||||
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
|
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
|
||||||
|
|
||||||
return stdout;
|
return JSON.parse(stdout).items;
|
||||||
} catch {
|
} catch {
|
||||||
return { stdout: JSON.stringify({ items: [] }) };
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,12 @@ import { HelmChartManager } from "./helm-chart-manager";
|
|||||||
import type { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
|
import type { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
|
||||||
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
||||||
|
|
||||||
|
interface GetReleaseValuesArgs {
|
||||||
|
cluster: Cluster;
|
||||||
|
namespace: string;
|
||||||
|
all: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
class HelmService {
|
class HelmService {
|
||||||
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
|
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
|
||||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||||
@ -86,12 +92,12 @@ class HelmService {
|
|||||||
return getRelease(releaseName, namespace, cluster);
|
return getRelease(releaseName, namespace, cluster);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string, all: boolean) {
|
public async getReleaseValues(releaseName: string, { cluster, namespace, all }: GetReleaseValuesArgs) {
|
||||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||||
|
|
||||||
logger.debug("Fetch release values");
|
logger.debug("Fetch release values");
|
||||||
|
|
||||||
return getValues(releaseName, namespace, all, proxyKubeconfig);
|
return getValues(releaseName, { namespace, all, pathToKubeconfig });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
|
|||||||
@ -93,8 +93,11 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
|
|
||||||
for (const arg of process.argv) {
|
for (const arg of process.argv) {
|
||||||
if (arg.toLowerCase().startsWith("lens://")) {
|
if (arg.toLowerCase().startsWith("lens://")) {
|
||||||
lprm.route(arg)
|
try {
|
||||||
.catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg }));
|
lprm.route(arg);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,8 +107,11 @@ app.on("second-instance", (event, argv) => {
|
|||||||
|
|
||||||
for (const arg of argv) {
|
for (const arg of argv) {
|
||||||
if (arg.toLowerCase().startsWith("lens://")) {
|
if (arg.toLowerCase().startsWith("lens://")) {
|
||||||
lprm.route(arg)
|
try {
|
||||||
.catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg }));
|
lprm.route(arg);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl: arg });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,9 +258,10 @@ autoUpdater.on("before-quit-for-update", () => blockQuit = false);
|
|||||||
app.on("will-quit", (event) => {
|
app.on("will-quit", (event) => {
|
||||||
// Quit app on Cmd+Q (MacOS)
|
// 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" });
|
||||||
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
||||||
KubeconfigSyncManager.getInstance(false)?.stopSync();
|
KubeconfigSyncManager.getInstance(false)?.stopSync();
|
||||||
|
LensProtocolRouterMain.getInstance(false)?.cleanup();
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
if (blockQuit) {
|
if (blockQuit) {
|
||||||
@ -268,10 +275,11 @@ app.on("open-url", (event, rawUrl) => {
|
|||||||
// lens:// protocol handler
|
// lens:// protocol handler
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
LensProtocolRouterMain
|
try {
|
||||||
.getInstance()
|
LensProtocolRouterMain.getInstance().route(rawUrl);
|
||||||
.route(rawUrl)
|
} catch (error) {
|
||||||
.catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl }));
|
logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import * as uuid from "uuid";
|
|||||||
|
|
||||||
import { broadcastMessage } from "../../../common/ipc";
|
import { broadcastMessage } from "../../../common/ipc";
|
||||||
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
|
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
|
||||||
import { noop } from "../../../common/utils";
|
import { delay, noop } from "../../../common/utils";
|
||||||
import { LensExtension } from "../../../extensions/main-api";
|
import { LensExtension } from "../../../extensions/main-api";
|
||||||
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||||
import { ExtensionsStore } from "../../../extensions/extensions-store";
|
import { ExtensionsStore } from "../../../extensions/extensions-store";
|
||||||
@ -56,27 +56,27 @@ describe("protocol router tests", () => {
|
|||||||
LensProtocolRouterMain.resetInstance();
|
LensProtocolRouterMain.resetInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw on non-lens URLS", async () => {
|
it("should throw on non-lens URLS", () => {
|
||||||
try {
|
try {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
|
|
||||||
expect(await lpr.route("https://google.ca")).toBeUndefined();
|
expect(lpr.route("https://google.ca")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(Error);
|
expect(error).toBeInstanceOf(Error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw when host not internal or extension", async () => {
|
it("should throw when host not internal or extension", () => {
|
||||||
try {
|
try {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
|
|
||||||
expect(await lpr.route("lens://foobar")).toBeUndefined();
|
expect(lpr.route("lens://foobar")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(Error);
|
expect(error).toBeInstanceOf(Error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only("should not throw when has valid host", async () => {
|
it("should not throw when has valid host", async () => {
|
||||||
const extId = uuid.v4();
|
const extId = uuid.v4();
|
||||||
const ext = new LensExtension({
|
const ext = new LensExtension({
|
||||||
id: extId,
|
id: extId,
|
||||||
@ -103,38 +103,39 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/", noop);
|
lpr.addInternalHandler("/", noop);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://app")).toBeUndefined();
|
expect(lpr.route("lens://app")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
|
expect(lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(broadcastMessage).toHaveBeenNthCalledWith(1, ProtocolHandlerInternal, "lens://app/");
|
await delay(50);
|
||||||
expect(broadcastMessage).toHaveBeenNthCalledWith(2, ProtocolHandlerExtension, "lens://extension/@mirantis/minikube");
|
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app/", "matched");
|
||||||
|
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call handler if matches", async () => {
|
it("should call handler if matches", () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called = false;
|
let called = false;
|
||||||
|
|
||||||
lpr.addInternalHandler("/page", () => { called = true; });
|
lpr.addInternalHandler("/page", () => { called = true; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://app/page")).toBeUndefined();
|
expect(lpr.route("lens://app/page")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(called).toBe(true);
|
expect(called).toBe(true);
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler", async () => {
|
it("should call most exact handler", () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -142,13 +143,13 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
|
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://app/page/foo")).toBeUndefined();
|
expect(lpr.route("lens://app/page/foo")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(called).toBe("foo");
|
expect(called).toBe("foo");
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler for an extension", async () => {
|
it("should call most exact handler for an extension", async () => {
|
||||||
@ -182,13 +183,14 @@ describe("protocol router tests", () => {
|
|||||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
expect(lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await delay(50);
|
||||||
expect(called).toBe("foob");
|
expect(called).toBe("foob");
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/@foobar/icecream/page/foob");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/@foobar/icecream/page/foob", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work with non-org extensions", async () => {
|
it("should work with non-org extensions", async () => {
|
||||||
@ -249,13 +251,15 @@ describe("protocol router tests", () => {
|
|||||||
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
|
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
expect(lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await delay(50);
|
||||||
|
|
||||||
expect(called).toBe(1);
|
expect(called).toBe(1);
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/icecream/page");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/icecream/page", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw if urlSchema is invalid", () => {
|
it("should throw if urlSchema is invalid", () => {
|
||||||
@ -264,7 +268,7 @@ describe("protocol router tests", () => {
|
|||||||
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler with 3 found handlers", async () => {
|
it("should call most exact handler with 3 found handlers", () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -274,16 +278,16 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(called).toBe(3);
|
expect(called).toBe(3);
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call most exact handler with 2 found handlers", async () => {
|
it("should call most exact handler with 2 found handlers", () => {
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
let called: any = 0;
|
let called: any = 0;
|
||||||
|
|
||||||
@ -292,12 +296,12 @@ describe("protocol router tests", () => {
|
|||||||
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
expect(lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(throwIfDefined(error)).not.toThrow();
|
expect(throwIfDefined(error)).not.toThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(called).toBe(1);
|
expect(called).toBe(1);
|
||||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat");
|
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -25,6 +25,8 @@ import Url from "url-parse";
|
|||||||
import type { LensExtension } from "../../extensions/lens-extension";
|
import type { LensExtension } from "../../extensions/lens-extension";
|
||||||
import { broadcastMessage } from "../../common/ipc";
|
import { broadcastMessage } from "../../common/ipc";
|
||||||
import { observable, when, makeObservable } from "mobx";
|
import { observable, when, makeObservable } from "mobx";
|
||||||
|
import { ProtocolHandlerInvalid, RouteAttempt } from "../../common/protocol-handler";
|
||||||
|
import { disposer } from "../../common/utils";
|
||||||
|
|
||||||
export interface FallbackHandler {
|
export interface FallbackHandler {
|
||||||
(name: string): Promise<boolean>;
|
(name: string): Promise<boolean>;
|
||||||
@ -36,19 +38,25 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
@observable rendererLoaded = false;
|
@observable rendererLoaded = false;
|
||||||
@observable extensionsLoaded = false;
|
@observable extensionsLoaded = false;
|
||||||
|
|
||||||
|
protected disposers = disposer();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public cleanup() {
|
||||||
|
this.disposers();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the most specific registered handler, if it exists, and invoke it.
|
* 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
|
* This will send an IPC message to the renderer router to do the same
|
||||||
* in the renderer.
|
* in the renderer.
|
||||||
*/
|
*/
|
||||||
public async route(rawUrl: string): Promise<void> {
|
public route(rawUrl: string) {
|
||||||
try {
|
try {
|
||||||
const url = new Url(rawUrl, true);
|
const url = new Url(rawUrl, true);
|
||||||
|
|
||||||
@ -60,16 +68,18 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
|
|
||||||
switch (url.host) {
|
switch (url.host) {
|
||||||
case "app":
|
case "app":
|
||||||
return this._routeToInternal(url);
|
this._routeToInternal(url);
|
||||||
|
break;
|
||||||
case "extension":
|
case "extension":
|
||||||
await when(() => this.extensionsLoaded);
|
this.disposers.push(when(() => this.extensionsLoaded, () => this._routeToExtension(url)));
|
||||||
|
break;
|
||||||
return this._routeToExtension(url);
|
|
||||||
default:
|
default:
|
||||||
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_HOST, url);
|
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_HOST, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);
|
||||||
|
|
||||||
if (error instanceof proto.RoutingError) {
|
if (error instanceof proto.RoutingError) {
|
||||||
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url });
|
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url });
|
||||||
} else {
|
} else {
|
||||||
@ -102,17 +112,16 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _routeToInternal(url: Url): Promise<void> {
|
protected _routeToInternal(url: Url): RouteAttempt {
|
||||||
const rawUrl = url.toString(); // for sending to renderer
|
const rawUrl = url.toString(); // for sending to renderer
|
||||||
|
const attempt = super._routeToInternal(url);
|
||||||
|
|
||||||
super._routeToInternal(url);
|
this.disposers.push(when(() => this.rendererLoaded, () => broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt)));
|
||||||
|
|
||||||
await when(() => this.rendererLoaded);
|
return attempt;
|
||||||
|
|
||||||
return broadcastMessage(proto.ProtocolHandlerInternal, rawUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _routeToExtension(url: Url): Promise<void> {
|
protected async _routeToExtension(url: Url): Promise<RouteAttempt> {
|
||||||
const rawUrl = url.toString(); // for sending to renderer
|
const rawUrl = url.toString(); // for sending to renderer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,10 +131,11 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
|||||||
* Note: this needs to clone the url because _routeToExtension modifies its
|
* Note: this needs to clone the url because _routeToExtension modifies its
|
||||||
* argument.
|
* argument.
|
||||||
*/
|
*/
|
||||||
await super._routeToExtension(new Url(url.toString(), true));
|
const attempt = await super._routeToExtension(new Url(url.toString(), true));
|
||||||
await when(() => this.rendererLoaded);
|
|
||||||
|
|
||||||
return broadcastMessage(proto.ProtocolHandlerExtension, rawUrl);
|
this.disposers.push(when(() => this.rendererLoaded, () => broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt)));
|
||||||
|
|
||||||
|
return attempt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -21,8 +21,9 @@
|
|||||||
|
|
||||||
import type { LensApiRequest } from "../router";
|
import type { LensApiRequest } from "../router";
|
||||||
import { helmService } from "../helm/helm-service";
|
import { helmService } from "../helm/helm-service";
|
||||||
import { respondJson, respondText } from "../utils/http-responses";
|
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
|
import { respondJson, respondText } from "../utils/http-responses";
|
||||||
|
import { getBoolean } from "./utils/parse-query";
|
||||||
|
|
||||||
export class HelmApiRoute {
|
export class HelmApiRoute {
|
||||||
static async listCharts(request: LensApiRequest) {
|
static async listCharts(request: LensApiRequest) {
|
||||||
@ -122,10 +123,11 @@ export class HelmApiRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async getReleaseValues(request: LensApiRequest) {
|
static async getReleaseValues(request: LensApiRequest) {
|
||||||
const { cluster, params, response, query } = request;
|
const { cluster, params: { namespace, release }, response, query } = request;
|
||||||
|
const all = getBoolean(query, "all");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace, query.has("all"));
|
const result = await helmService.getReleaseValues(release, { cluster, namespace, all });
|
||||||
|
|
||||||
respondText(response, result);
|
respondText(response, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -18,7 +18,4 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
export default {
|
export * from "./parse-query";
|
||||||
Trans: ({ children }: { children: React.ReactNode }) => children,
|
|
||||||
t: (message: string) => message
|
|
||||||
};
|
|
||||||
34
src/main/routes/utils/parse-query.ts
Normal file
34
src/main/routes/utils/parse-query.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
export function getBoolean(query: URLSearchParams, key: string): boolean {
|
||||||
|
const value = query.get(key);
|
||||||
|
|
||||||
|
switch (value?.toLowerCase()) {
|
||||||
|
case "false":
|
||||||
|
case "f":
|
||||||
|
case "0":
|
||||||
|
case null:
|
||||||
|
case undefined:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,7 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import jsYaml from "js-yaml";
|
import jsYaml from "js-yaml";
|
||||||
import { compile } from "path-to-regexp";
|
|
||||||
import { autoBind, formatDuration } from "../../utils";
|
import { autoBind, formatDuration } from "../../utils";
|
||||||
import capitalize from "lodash/capitalize";
|
import capitalize from "lodash/capitalize";
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
@ -28,6 +27,8 @@ import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.st
|
|||||||
import type { ItemObject } from "../../item.store";
|
import type { ItemObject } from "../../item.store";
|
||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import type { JsonApiData } from "../json-api";
|
import type { JsonApiData } from "../json-api";
|
||||||
|
import { buildURLPositional } from "../../../common/utils/buildUrl";
|
||||||
|
import type { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
|
||||||
interface IReleasePayload {
|
interface IReleasePayload {
|
||||||
name: string;
|
name: string;
|
||||||
@ -46,7 +47,7 @@ interface IReleasePayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IReleaseRawDetails extends IReleasePayload {
|
interface IReleaseRawDetails extends IReleasePayload {
|
||||||
resources: string;
|
resources: KubeJsonApiData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleaseDetails extends IReleasePayload {
|
export interface IReleaseDetails extends IReleasePayload {
|
||||||
@ -83,12 +84,16 @@ export interface IReleaseRevision {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = compile(`/v2/releases/:namespace?/:name?`) as (
|
type EndpointParams = {}
|
||||||
params?: {
|
| { namespace: string }
|
||||||
namespace?: string;
|
| { namespace: string, name: string }
|
||||||
name?: string;
|
| { namespace: string, name: string, route: string };
|
||||||
}
|
|
||||||
) => string;
|
interface EndpointQuery {
|
||||||
|
all?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = buildURLPositional<EndpointParams, EndpointQuery>("/v2/releases/:namespace?/:name?/:route?");
|
||||||
|
|
||||||
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
||||||
const releases = await apiBase.get<HelmRelease[]>(endpoint({ namespace }));
|
const releases = await apiBase.get<HelmRelease[]>(endpoint({ namespace }));
|
||||||
@ -98,10 +103,8 @@ export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
|||||||
|
|
||||||
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
|
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
|
||||||
const path = endpoint({ name, namespace });
|
const path = endpoint({ name, namespace });
|
||||||
|
const { resources: rawResources, ...details } = await apiBase.get<IReleaseRawDetails>(path);
|
||||||
const details = await apiBase.get<IReleaseRawDetails>(path);
|
const resources = rawResources.map(KubeObject.create);
|
||||||
const items: KubeObject[] = JSON.parse(details.resources).items;
|
|
||||||
const resources = items.map(item => KubeObject.create(item));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...details,
|
...details,
|
||||||
@ -134,25 +137,25 @@ export async function deleteRelease(name: string, namespace: string): Promise<Js
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getReleaseValues(name: string, namespace: string, all?: boolean): Promise<string> {
|
export async function getReleaseValues(name: string, namespace: string, all?: boolean): Promise<string> {
|
||||||
const path = `${endpoint({ name, namespace })}/values${all? "?all": ""}`;
|
const route = "values";
|
||||||
|
const path = endpoint({ name, namespace, route }, { all });
|
||||||
|
|
||||||
return apiBase.get<string>(path);
|
return apiBase.get<string>(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
|
export async function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
|
||||||
const path = `${endpoint({ name, namespace })}/history`;
|
const route = "history";
|
||||||
|
const path = endpoint({ name, namespace, route });
|
||||||
|
|
||||||
return apiBase.get(path);
|
return apiBase.get(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rollbackRelease(name: string, namespace: string, revision: number): Promise<JsonApiData> {
|
export async function rollbackRelease(name: string, namespace: string, revision: number): Promise<JsonApiData> {
|
||||||
const path = `${endpoint({ name, namespace })}/rollback`;
|
const route = "rollback";
|
||||||
|
const path = endpoint({ name, namespace, route });
|
||||||
|
const data = { revision };
|
||||||
|
|
||||||
return apiBase.put(path, {
|
return apiBase.put(path, { data });
|
||||||
data: {
|
|
||||||
revision
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HelmRelease {
|
export interface HelmRelease {
|
||||||
@ -190,7 +193,7 @@ export class HelmRelease implements ItemObject {
|
|||||||
getChart(withVersion = false) {
|
getChart(withVersion = false) {
|
||||||
let chart = this.chart;
|
let chart = this.chart;
|
||||||
|
|
||||||
if(!withVersion && this.getVersion() != "" ) {
|
if (!withVersion && this.getVersion() != "") {
|
||||||
const search = new RegExp(`-${this.getVersion()}`);
|
const search = new RegExp(`-${this.getVersion()}`);
|
||||||
|
|
||||||
chart = chart.replace(search, "");
|
chart = chart.replace(search, "");
|
||||||
@ -210,12 +213,7 @@ export class HelmRelease implements ItemObject {
|
|||||||
getVersion() {
|
getVersion() {
|
||||||
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
||||||
|
|
||||||
if (versions) {
|
return versions?.[0] ?? "";
|
||||||
return versions[0];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUpdated(humanize = true, compact = true) {
|
getUpdated(humanize = true, compact = true) {
|
||||||
|
|||||||
@ -97,7 +97,7 @@ export class KubeObject<Metadata extends IKubeObjectMetadata = IKubeObjectMetada
|
|||||||
status?: Status;
|
status?: Status;
|
||||||
spec?: Spec;
|
spec?: Spec;
|
||||||
|
|
||||||
static create(data: any) {
|
static create(data: KubeJsonApiData) {
|
||||||
return new KubeObject(data);
|
return new KubeObject(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ export class KubeObject<Metadata extends IKubeObjectMetadata = IKubeObjectMetada
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static isJsonApiDataList<T>(object: unknown, verifyItem:(val: unknown) => val is T): object is KubeJsonApiDataList<T> {
|
static isJsonApiDataList<T>(object: unknown, verifyItem: (val: unknown) => val is T): object is KubeJsonApiDataList<T> {
|
||||||
return (
|
return (
|
||||||
isObject(object)
|
isObject(object)
|
||||||
&& hasTypedProperty(object, "kind", isString)
|
&& hasTypedProperty(object, "kind", isString)
|
||||||
|
|||||||
@ -81,7 +81,6 @@ export class HelmCharts extends Component<Props> {
|
|||||||
tableId="helm_charts"
|
tableId="helm_charts"
|
||||||
className="HelmCharts"
|
className="HelmCharts"
|
||||||
store={helmChartStore}
|
store={helmChartStore}
|
||||||
isClusterScoped={true}
|
|
||||||
isSelectable={false}
|
isSelectable={false}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[columnId.name]: (chart: HelmChart) => chart.getName(),
|
[columnId.name]: (chart: HelmChart) => chart.getName(),
|
||||||
|
|||||||
@ -58,32 +58,35 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
@observable details: IReleaseDetails;
|
@observable details: IReleaseDetails;
|
||||||
@observable values = "";
|
@observable values = "";
|
||||||
@observable valuesLoading = false;
|
@observable valuesLoading = false;
|
||||||
@observable userSuppliedOnly = false;
|
@observable showOnlyUserSuppliedValues = false;
|
||||||
@observable saving = false;
|
@observable saving = false;
|
||||||
@observable releaseSecret: Secret;
|
@observable releaseSecret: Secret;
|
||||||
|
|
||||||
@disposeOnUnmount
|
componentDidMount() {
|
||||||
releaseSelector = reaction(() => this.props.release, release => {
|
disposeOnUnmount(this, [
|
||||||
if (!release) return;
|
reaction(() => this.props.release, release => {
|
||||||
this.loadDetails();
|
if (!release) return;
|
||||||
this.loadValues();
|
this.loadDetails();
|
||||||
this.releaseSecret = null;
|
this.loadValues();
|
||||||
|
this.releaseSecret = null;
|
||||||
|
}),
|
||||||
|
reaction(() => secretsStore.getItems(), () => {
|
||||||
|
if (!this.props.release) return;
|
||||||
|
const { getReleaseSecret } = releaseStore;
|
||||||
|
const { release } = this.props;
|
||||||
|
const secret = getReleaseSecret(release);
|
||||||
|
|
||||||
|
if (this.releaseSecret) {
|
||||||
|
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return;
|
||||||
|
this.loadDetails();
|
||||||
|
}
|
||||||
|
this.releaseSecret = secret;
|
||||||
|
}),
|
||||||
|
reaction(() => this.showOnlyUserSuppliedValues, () => {
|
||||||
|
this.loadValues();
|
||||||
|
}),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
@disposeOnUnmount
|
|
||||||
secretWatcher = reaction(() => secretsStore.getItems(), () => {
|
|
||||||
if (!this.props.release) return;
|
|
||||||
const { getReleaseSecret } = releaseStore;
|
|
||||||
const { release } = this.props;
|
|
||||||
const secret = getReleaseSecret(release);
|
|
||||||
|
|
||||||
if (this.releaseSecret) {
|
|
||||||
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return;
|
|
||||||
this.loadDetails();
|
|
||||||
}
|
|
||||||
this.releaseSecret = secret;
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -100,10 +103,15 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
async loadValues() {
|
async loadValues() {
|
||||||
const { release } = this.props;
|
const { release } = this.props;
|
||||||
|
|
||||||
this.values = "";
|
try {
|
||||||
this.valuesLoading = true;
|
this.valuesLoading = true;
|
||||||
this.values = (await getReleaseValues(release.getName(), release.getNs(), !this.userSuppliedOnly)) ?? "";
|
this.values = (await getReleaseValues(release.getName(), release.getNs(), !this.showOnlyUserSuppliedValues)) ?? "";
|
||||||
this.valuesLoading = false;
|
} catch (error) {
|
||||||
|
Notifications.error(`Failed to load values for ${release.getName()}: ${error}`);
|
||||||
|
this.values = "";
|
||||||
|
} finally {
|
||||||
|
this.valuesLoading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateValues = async () => {
|
updateValues = async () => {
|
||||||
@ -146,21 +154,19 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
<div className="flex column gaps">
|
<div className="flex column gaps">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="User-supplied values only"
|
label="User-supplied values only"
|
||||||
value={this.userSuppliedOnly}
|
value={this.showOnlyUserSuppliedValues}
|
||||||
onChange={values => {
|
onChange={value => this.showOnlyUserSuppliedValues = value}
|
||||||
this.userSuppliedOnly = values;
|
|
||||||
this.loadValues();
|
|
||||||
}}
|
|
||||||
disabled={valuesLoading}
|
disabled={valuesLoading}
|
||||||
/>
|
/>
|
||||||
{valuesLoading
|
<AceEditor
|
||||||
? <Spinner />
|
mode="yaml"
|
||||||
: <AceEditor
|
value={values}
|
||||||
mode="yaml"
|
onChange={text => this.values = text}
|
||||||
value={values}
|
className={cssNames({ loading: valuesLoading })}
|
||||||
onChange={values => this.values = values}
|
readOnly={valuesLoading || this.showOnlyUserSuppliedValues}
|
||||||
/>
|
>
|
||||||
}
|
{valuesLoading && <Spinner center />}
|
||||||
|
</AceEditor>
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
label="Save"
|
label="Save"
|
||||||
|
|||||||
@ -66,10 +66,8 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
|||||||
|
|
||||||
onOpen = async () => {
|
onOpen = async () => {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const currentRevision = this.release.getRevision();
|
|
||||||
let releases = await getReleaseHistory(this.release.getName(), this.release.getNs());
|
let releases = await getReleaseHistory(this.release.getName(), this.release.getNs());
|
||||||
|
|
||||||
releases = releases.filter(item => item.revision !== currentRevision); // remove current
|
|
||||||
releases = orderBy(releases, "revision", "desc"); // sort
|
releases = orderBy(releases, "revision", "desc"); // sort
|
||||||
this.revisions.replace(releases);
|
this.revisions.replace(releases);
|
||||||
this.revision = this.revisions[0];
|
this.revision = this.revisions[0];
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { navigation } from "../../navigation";
|
|||||||
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
||||||
import { HelmReleaseMenu } from "./release-menu";
|
import { HelmReleaseMenu } from "./release-menu";
|
||||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||||
|
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||||
|
|
||||||
enum columnId {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
@ -117,6 +118,15 @@ export class HelmReleases extends Component<Props> {
|
|||||||
(release: HelmRelease) => release.getVersion(),
|
(release: HelmRelease) => release.getVersion(),
|
||||||
]}
|
]}
|
||||||
renderHeaderTitle="Releases"
|
renderHeaderTitle="Releases"
|
||||||
|
customizeHeader={({ filters, ...headerPlaceholders }) => ({
|
||||||
|
filters: (
|
||||||
|
<>
|
||||||
|
{filters}
|
||||||
|
<NamespaceSelectFilter />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
...headerPlaceholders,
|
||||||
|
})}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
|
|||||||
43
src/renderer/components/+catalog/catalog-entity-details.scss
Normal file
43
src/renderer/components/+catalog/catalog-entity-details.scss
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.CatalogEntityDetails {
|
||||||
|
.EntityMetadata {
|
||||||
|
margin-right: $margin;
|
||||||
|
}
|
||||||
|
.EntityIcon.box.top.left {
|
||||||
|
margin-right: $margin * 2;
|
||||||
|
|
||||||
|
.IconHint {
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-top: $margin;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
div * {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/renderer/components/+catalog/catalog-entity-details.tsx
Normal file
129
src/renderer/components/+catalog/catalog-entity-details.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./catalog-entity-details.scss";
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { Drawer, DrawerItem, DrawerItemLabels } from "../drawer";
|
||||||
|
import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity";
|
||||||
|
import type { CatalogCategory } from "../../../common/catalog";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { KubeObject } from "../../api/kube-object";
|
||||||
|
import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu";
|
||||||
|
import { catalogEntityDetailRegistry } from "../../../extensions/registries";
|
||||||
|
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entity: CatalogEntity;
|
||||||
|
hideDetails(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class CatalogEntityDetails extends Component<Props> {
|
||||||
|
private abortController?: AbortController;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.abortController?.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryIcon(category: CatalogCategory) {
|
||||||
|
if (category.metadata.icon.includes("<svg")) {
|
||||||
|
return <Icon svg={category.metadata.icon} smallest />;
|
||||||
|
} else {
|
||||||
|
return <Icon material={category.metadata.icon} smallest />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openEntity() {
|
||||||
|
this.props.entity.onRun(catalogEntityRunContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
const { entity } = this.props;
|
||||||
|
const labels = KubeObject.stringifyLabels(entity.metadata.labels);
|
||||||
|
const detailItems = catalogEntityDetailRegistry.getItemsForKind(entity.kind, entity.apiVersion);
|
||||||
|
const details = detailItems.map((item, index) => {
|
||||||
|
return <item.components.Details entity={entity} key={index}/>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showDetails = detailItems.find((item) => item.priority > 999) === undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showDetails && (
|
||||||
|
<div className="flex CatalogEntityDetails">
|
||||||
|
<div className="EntityIcon box top left">
|
||||||
|
<HotbarIcon
|
||||||
|
uid={entity.metadata.uid}
|
||||||
|
title={entity.metadata.name}
|
||||||
|
source={entity.metadata.source}
|
||||||
|
onClick={() => this.openEntity()}
|
||||||
|
size={128} />
|
||||||
|
<div className="IconHint">
|
||||||
|
Click to open
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="box grow EntityMetadata">
|
||||||
|
<DrawerItem name="Name">
|
||||||
|
{entity.metadata.name}
|
||||||
|
</DrawerItem>
|
||||||
|
<DrawerItem name="Kind">
|
||||||
|
{entity.kind}
|
||||||
|
</DrawerItem>
|
||||||
|
<DrawerItem name="Source">
|
||||||
|
{entity.metadata.source}
|
||||||
|
</DrawerItem>
|
||||||
|
<DrawerItemLabels
|
||||||
|
name="Labels"
|
||||||
|
labels={labels}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="box grow">
|
||||||
|
{details}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { entity, hideDetails } = this.props;
|
||||||
|
const title = `${entity.kind}: ${entity.metadata.name}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
className="CatalogEntityDetails"
|
||||||
|
usePortal={true}
|
||||||
|
open={true}
|
||||||
|
title={title}
|
||||||
|
toolbar={<CatalogEntityDrawerMenu entity={entity} key={entity.getId()} />}
|
||||||
|
onClose={hideDetails}
|
||||||
|
>
|
||||||
|
{this.renderContent()}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx
Normal file
127
src/renderer/components/+catalog/catalog-entity-drawer-menu.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { cssNames } from "../../utils";
|
||||||
|
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||||
|
import type { CatalogEntity, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { makeObservable, observable } from "mobx";
|
||||||
|
import { navigate } from "../../navigation";
|
||||||
|
import { MenuItem } from "../menu";
|
||||||
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
|
import { HotbarStore } from "../../../common/hotbar-store";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
|
||||||
|
export interface CatalogEntityDrawerMenuProps<T extends CatalogEntity> extends MenuActionsProps {
|
||||||
|
entity: T | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Component<CatalogEntityDrawerMenuProps<T>> {
|
||||||
|
@observable private contextMenu: CatalogEntityContextMenuContext;
|
||||||
|
|
||||||
|
constructor(props: CatalogEntityDrawerMenuProps<T>) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.contextMenu = {
|
||||||
|
menuItems: [],
|
||||||
|
navigate: (url: string) => navigate(url)
|
||||||
|
};
|
||||||
|
this.props.entity?.onContextMenuOpen(this.contextMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||||
|
if (menuItem.confirm) {
|
||||||
|
ConfirmDialog.open({
|
||||||
|
okButtonProps: {
|
||||||
|
primary: false,
|
||||||
|
accent: true,
|
||||||
|
},
|
||||||
|
ok: () => {
|
||||||
|
menuItem.onClick();
|
||||||
|
},
|
||||||
|
message: menuItem.confirm.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
menuItem.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addToHotbar(entity: CatalogEntity): void {
|
||||||
|
HotbarStore.getInstance().addToHotbar(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenuItems(entity: T): React.ReactChild[] {
|
||||||
|
if (!entity) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = this.contextMenu.menuItems.filter((menuItem) => {
|
||||||
|
return menuItem.icon && !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === entity.metadata.source;
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = menuItems.map((menuItem, index) => {
|
||||||
|
const props = menuItem.icon.includes("<svg") ? { svg: menuItem.icon } : { material: menuItem.icon };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
|
||||||
|
<Icon
|
||||||
|
title={menuItem.title}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
items.unshift(
|
||||||
|
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(entity) }>
|
||||||
|
<Icon material="playlist_add" small title="Add to Hotbar" />
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
items.reverse();
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.contextMenu) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { className, entity, ...menuProps } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuActions
|
||||||
|
className={cssNames("CatalogEntityDrawerMenu", className)}
|
||||||
|
toolbar
|
||||||
|
{...menuProps}
|
||||||
|
>
|
||||||
|
{this.getMenuItems(entity)}
|
||||||
|
</MenuActions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,6 +28,10 @@ import { autoBind } from "../../../common/utils";
|
|||||||
export class CatalogEntityItem implements ItemObject {
|
export class CatalogEntityItem implements ItemObject {
|
||||||
constructor(public entity: CatalogEntity) {}
|
constructor(public entity: CatalogEntity) {}
|
||||||
|
|
||||||
|
get kind() {
|
||||||
|
return this.entity.kind;
|
||||||
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return this.entity.metadata.name;
|
return this.entity.metadata.name;
|
||||||
}
|
}
|
||||||
|
|||||||
91
src/renderer/components/+catalog/catalog.module.css
Normal file
91
src/renderer/components/+catalog/catalog.module.css
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.iconCell {
|
||||||
|
max-width: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameCell {
|
||||||
|
}
|
||||||
|
|
||||||
|
.sourceCell {
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusCell {
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connected {
|
||||||
|
color: var(--colorSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disconnected {
|
||||||
|
color: var(--halfGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelsCell {
|
||||||
|
overflow-x: scroll;
|
||||||
|
text-overflow: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelsCell::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
overflow: unset;
|
||||||
|
text-overflow: unset;
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge:not(:first-child) {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalogIcon {
|
||||||
|
font-size: 10px;
|
||||||
|
-webkit-font-smoothing: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
@apply flex flex-grow flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
@apply px-8 py-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
background-color: var(--sidebarItemHoverBackground);
|
||||||
|
--color-active: var(--textColorTertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeTab, .activeTab:hover {
|
||||||
|
background-color: var(--blue);
|
||||||
|
--color-active: white;
|
||||||
|
}
|
||||||
@ -75,6 +75,10 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.TableCell.kind {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
.TableCell.source {
|
.TableCell.source {
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,8 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./catalog.scss";
|
import styles from "./catalog.module.css";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { ItemListLayout } from "../item-object-list";
|
import { ItemListLayout } from "../item-object-list";
|
||||||
@ -27,9 +28,8 @@ import { action, makeObservable, observable, reaction, when } from "mobx";
|
|||||||
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { kebabCase } from "lodash";
|
import { kebabCase } from "lodash";
|
||||||
import { PageLayout } from "../layout/page-layout";
|
|
||||||
import { MenuItem, MenuActions } from "../menu";
|
import { MenuItem, MenuActions } from "../menu";
|
||||||
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
|
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
import { HotbarStore } from "../../../common/hotbar-store";
|
||||||
import { ConfirmDialog } from "../confirm-dialog";
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
@ -40,9 +40,17 @@ import type { RouteComponentProps } from "react-router";
|
|||||||
import type { ICatalogViewRouteParam } from "./catalog.route";
|
import type { ICatalogViewRouteParam } from "./catalog.route";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Avatar } from "../avatar/avatar";
|
import { Avatar } from "../avatar/avatar";
|
||||||
|
import { MainLayout } from "../layout/main-layout";
|
||||||
|
import { cssNames } from "../../utils";
|
||||||
|
import { TopBar } from "../layout/topbar";
|
||||||
|
import { welcomeURL } from "../+welcome";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { MaterialTooltip } from "../material-tooltip/material-tooltip";
|
||||||
|
import { CatalogEntityDetails } from "./catalog-entity-details";
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
|
kind = "kind",
|
||||||
source = "source",
|
source = "source",
|
||||||
status = "status"
|
status = "status"
|
||||||
}
|
}
|
||||||
@ -54,6 +62,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
@observable private catalogEntityStore?: CatalogEntityStore;
|
@observable private catalogEntityStore?: CatalogEntityStore;
|
||||||
@observable private contextMenu: CatalogEntityContextMenuContext;
|
@observable private contextMenu: CatalogEntityContextMenuContext;
|
||||||
@observable activeTab?: string;
|
@observable activeTab?: string;
|
||||||
|
@observable selectedItem?: CatalogEntityItem;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -102,7 +111,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDetails(item: CatalogEntityItem) {
|
onDetails(item: CatalogEntityItem) {
|
||||||
item.onRun(catalogEntityRunContext);
|
this.selectedItem = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||||
@ -136,14 +145,14 @@ export class Catalog extends React.Component<Props> {
|
|||||||
|
|
||||||
renderNavigation() {
|
renderNavigation() {
|
||||||
return (
|
return (
|
||||||
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
|
<Tabs className={cssNames(styles.tabs, "flex column")} scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
|
||||||
<div className="sidebarHeader">Catalog</div>
|
<div>
|
||||||
<div className="sidebarTabs">
|
|
||||||
<Tab
|
<Tab
|
||||||
value={undefined}
|
value={undefined}
|
||||||
key="*"
|
key="*"
|
||||||
label="Browse"
|
label="Browse"
|
||||||
data-testid="*-tab"
|
data-testid="*-tab"
|
||||||
|
className={cssNames(styles.tab, { [styles.activeTab]: this.activeTab == null })}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
this.categories.map(category => (
|
this.categories.map(category => (
|
||||||
@ -152,6 +161,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
key={category.getId()}
|
key={category.getId()}
|
||||||
label={category.metadata.name}
|
label={category.metadata.name}
|
||||||
data-testid={`${category.getId()}-tab`}
|
data-testid={`${category.getId()}-tab`}
|
||||||
|
className={cssNames(styles.tab, { [styles.activeTab]: this.activeTab == category.getId() })}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -180,19 +190,91 @@ export class Catalog extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderIcon(item: CatalogEntityItem) {
|
renderIcon(item: CatalogEntityItem) {
|
||||||
const category = catalogCategoryRegistry.getCategoryForEntity(item.entity);
|
|
||||||
|
|
||||||
if (!category) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
title={item.name}
|
title={item.name}
|
||||||
colorHash={`${item.name}-${item.source}`}
|
colorHash={`${item.name}-${item.source}`}
|
||||||
width={24}
|
width={24}
|
||||||
height={24}
|
height={24}
|
||||||
className="catalogIcon"
|
className={styles.catalogIcon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSingleCategoryList() {
|
||||||
|
return (
|
||||||
|
<ItemListLayout
|
||||||
|
renderHeaderTitle={this.catalogEntityStore.activeCategory?.metadata.name ?? "Browse All"}
|
||||||
|
isSearchable={true}
|
||||||
|
isSelectable={false}
|
||||||
|
className="CatalogItemList"
|
||||||
|
store={this.catalogEntityStore}
|
||||||
|
tableId="catalog-items"
|
||||||
|
sortingCallbacks={{
|
||||||
|
[sortBy.name]: (item: CatalogEntityItem) => item.name,
|
||||||
|
[sortBy.source]: (item: CatalogEntityItem) => item.source,
|
||||||
|
[sortBy.status]: (item: CatalogEntityItem) => item.phase,
|
||||||
|
}}
|
||||||
|
searchFilters={[
|
||||||
|
(entity: CatalogEntityItem) => entity.searchFields,
|
||||||
|
]}
|
||||||
|
renderTableHeader={[
|
||||||
|
{ title: "", className: "icon" },
|
||||||
|
{ title: "Name", className: "name", sortBy: sortBy.name },
|
||||||
|
{ title: "Source", className: "source", sortBy: sortBy.source },
|
||||||
|
{ title: "Labels", className: "labels" },
|
||||||
|
{ title: "Status", className: "status", sortBy: sortBy.status },
|
||||||
|
]}
|
||||||
|
renderTableContents={(item: CatalogEntityItem) => [
|
||||||
|
this.renderIcon(item),
|
||||||
|
item.name,
|
||||||
|
item.source,
|
||||||
|
item.labels.map((label) => <Badge key={label} label={label} title={label} />),
|
||||||
|
{ title: item.phase, className: kebabCase(item.phase) }
|
||||||
|
]}
|
||||||
|
onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
|
||||||
|
renderItemMenu={this.renderItemMenu}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAllCategoriesList() {
|
||||||
|
return (
|
||||||
|
<ItemListLayout
|
||||||
|
renderHeaderTitle={this.catalogEntityStore.activeCategory?.metadata.name ?? "Browse All"}
|
||||||
|
isSearchable={true}
|
||||||
|
isSelectable={false}
|
||||||
|
className="CatalogItemList"
|
||||||
|
store={this.catalogEntityStore}
|
||||||
|
tableId="catalog-items"
|
||||||
|
sortingCallbacks={{
|
||||||
|
[sortBy.name]: (item: CatalogEntityItem) => item.name,
|
||||||
|
[sortBy.kind]: (item: CatalogEntityItem) => item.kind,
|
||||||
|
[sortBy.source]: (item: CatalogEntityItem) => item.source,
|
||||||
|
[sortBy.status]: (item: CatalogEntityItem) => item.phase,
|
||||||
|
}}
|
||||||
|
searchFilters={[
|
||||||
|
(entity: CatalogEntityItem) => entity.searchFields,
|
||||||
|
]}
|
||||||
|
renderTableHeader={[
|
||||||
|
{ title: "", className: "icon" },
|
||||||
|
{ title: "Name", className: "name", sortBy: sortBy.name },
|
||||||
|
{ title: "Kind", className: "kind", sortBy: sortBy.kind },
|
||||||
|
{ title: "Source", className: "source", sortBy: sortBy.source },
|
||||||
|
{ title: "Labels", className: "labels" },
|
||||||
|
{ title: "Status", className: "status", sortBy: sortBy.status },
|
||||||
|
]}
|
||||||
|
renderTableContents={(item: CatalogEntityItem) => [
|
||||||
|
this.renderIcon(item),
|
||||||
|
item.name,
|
||||||
|
item.kind,
|
||||||
|
item.source,
|
||||||
|
item.labels.map((label) => <Badge key={label} label={label} title={label} />),
|
||||||
|
{ title: item.phase, className: kebabCase(item.phase) }
|
||||||
|
]}
|
||||||
|
detailsItem={this.selectedItem}
|
||||||
|
onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
|
||||||
|
renderItemMenu={this.renderItemMenu}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -203,46 +285,29 @@ export class Catalog extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<>
|
||||||
className="CatalogPage"
|
<TopBar label="Catalog">
|
||||||
navigation={this.renderNavigation()}
|
<div>
|
||||||
provideBackButtonNavigation={false}
|
<MaterialTooltip title="Close Catalog" placement="left">
|
||||||
contentGaps={false}>
|
<Icon style={{ cursor: "default" }} material="close" onClick={() => navigate(welcomeURL())}/>
|
||||||
<ItemListLayout
|
</MaterialTooltip>
|
||||||
renderHeaderTitle={this.catalogEntityStore.activeCategory?.metadata.name ?? "Browse All"}
|
</div>
|
||||||
isClusterScoped
|
</TopBar>
|
||||||
isSearchable={true}
|
<MainLayout sidebar={this.renderNavigation()}>
|
||||||
isSelectable={false}
|
<div className="p-6 h-full">
|
||||||
className="CatalogItemList"
|
{ this.catalogEntityStore.activeCategory ? this.renderSingleCategoryList() : this.renderAllCategoriesList() }
|
||||||
store={this.catalogEntityStore}
|
</div>
|
||||||
tableId="catalog-items"
|
{ !this.selectedItem && (
|
||||||
sortingCallbacks={{
|
<CatalogAddButton category={this.catalogEntityStore.activeCategory} />
|
||||||
[sortBy.name]: (item: CatalogEntityItem) => item.name,
|
)}
|
||||||
[sortBy.source]: (item: CatalogEntityItem) => item.source,
|
{ this.selectedItem && (
|
||||||
[sortBy.status]: (item: CatalogEntityItem) => item.phase,
|
<CatalogEntityDetails
|
||||||
}}
|
entity={this.selectedItem.entity}
|
||||||
searchFilters={[
|
hideDetails={() => this.selectedItem = null}
|
||||||
(entity: CatalogEntityItem) => entity.searchFields,
|
/>
|
||||||
]}
|
)}
|
||||||
renderTableHeader={[
|
</MainLayout>
|
||||||
{ title: "", className: "icon" },
|
</>
|
||||||
{ title: "Name", className: "name", sortBy: sortBy.name },
|
|
||||||
{ title: "Source", className: "source", sortBy: sortBy.source },
|
|
||||||
{ title: "Labels", className: "labels" },
|
|
||||||
{ title: "Status", className: "status", sortBy: sortBy.status },
|
|
||||||
]}
|
|
||||||
renderTableContents={(item: CatalogEntityItem) => [
|
|
||||||
this.renderIcon(item),
|
|
||||||
item.name,
|
|
||||||
item.source,
|
|
||||||
item.labels.map((label) => <Badge key={label} label={label} title={label} />),
|
|
||||||
{ title: item.phase, className: kebabCase(item.phase) }
|
|
||||||
]}
|
|
||||||
onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
|
|
||||||
renderItemMenu={this.renderItemMenu}
|
|
||||||
/>
|
|
||||||
<CatalogAddButton category={this.catalogEntityStore.activeCategory} />
|
|
||||||
</PageLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,7 +64,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#c93dce",
|
"#c93dce",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "cpuUsage"
|
id: "cpuUsage",
|
||||||
|
label: "Usage"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: [
|
data: [
|
||||||
@ -75,7 +76,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#4caf50",
|
"#4caf50",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "cpuRequests"
|
id: "cpuRequests",
|
||||||
|
label: "Requests"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: [
|
data: [
|
||||||
@ -86,7 +88,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#3d90ce",
|
"#3d90ce",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "cpuLimits"
|
id: "cpuLimits",
|
||||||
|
label: "Limits"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
labels: [
|
labels: [
|
||||||
@ -107,7 +110,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#c93dce",
|
"#c93dce",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "memoryUsage"
|
id: "memoryUsage",
|
||||||
|
label: "Usage"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: [
|
data: [
|
||||||
@ -118,7 +122,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#4caf50",
|
"#4caf50",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "memoryRequests"
|
id: "memoryRequests",
|
||||||
|
label: "Requests"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: [
|
data: [
|
||||||
@ -129,7 +134,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#3d90ce",
|
"#3d90ce",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "memoryLimits"
|
id: "memoryLimits",
|
||||||
|
label: "Limits"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
labels: [
|
labels: [
|
||||||
@ -150,7 +156,8 @@ export const ClusterPieCharts = observer(() => {
|
|||||||
"#4caf50",
|
"#4caf50",
|
||||||
defaultColor,
|
defaultColor,
|
||||||
],
|
],
|
||||||
id: "podUsage"
|
id: "podUsage",
|
||||||
|
label: "Usage"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
labels: [
|
labels: [
|
||||||
|
|||||||
@ -145,7 +145,7 @@ export class CRDDetails extends React.Component<Props> {
|
|||||||
<>
|
<>
|
||||||
<DrawerTitle title="Validation"/>
|
<DrawerTitle title="Validation"/>
|
||||||
<AceEditor
|
<AceEditor
|
||||||
mode="json"
|
mode="yaml"
|
||||||
className="validation"
|
className="validation"
|
||||||
value={validation}
|
value={validation}
|
||||||
readOnly
|
readOnly
|
||||||
|
|||||||
@ -90,7 +90,6 @@ export class CrdList extends React.Component {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="crd"
|
tableId="crd"
|
||||||
className="CrdList"
|
className="CrdList"
|
||||||
isClusterScoped={true}
|
|
||||||
store={crdStore}
|
store={crdStore}
|
||||||
items={items}
|
items={items}
|
||||||
sortingCallbacks={sortingCallbacks}
|
sortingCallbacks={sortingCallbacks}
|
||||||
|
|||||||
@ -95,7 +95,6 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="crd_resources"
|
tableId="crd_resources"
|
||||||
className="CrdResources"
|
className="CrdResources"
|
||||||
isClusterScoped={!isNamespaced}
|
|
||||||
store={store}
|
store={store}
|
||||||
sortingCallbacks={sortingCallbacks}
|
sortingCallbacks={sortingCallbacks}
|
||||||
searchFilters={[
|
searchFilters={[
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import { namespaceStore } from "./namespace.store";
|
|||||||
|
|
||||||
import type { SelectOption, SelectProps } from "../select";
|
import type { SelectOption, SelectProps } from "../select";
|
||||||
|
|
||||||
const Placeholder = observer((props: PlaceholderProps<any>) => {
|
const Placeholder = observer((props: PlaceholderProps<any, boolean>) => {
|
||||||
const getPlaceholder = (): React.ReactNode => {
|
const getPlaceholder = (): React.ReactNode => {
|
||||||
const namespaces = namespaceStore.contextNamespaces;
|
const namespaces = namespaceStore.contextNamespaces;
|
||||||
|
|
||||||
|
|||||||
@ -47,7 +47,6 @@ export class Namespaces extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<TabLayout>
|
<TabLayout>
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
isClusterScoped
|
|
||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="namespaces"
|
tableId="namespaces"
|
||||||
className="Namespaces" store={namespaceStore}
|
className="Namespaces" store={namespaceStore}
|
||||||
|
|||||||
@ -166,7 +166,7 @@ export class Nodes extends React.Component<Props> {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="nodes"
|
tableId="nodes"
|
||||||
className="Nodes"
|
className="Nodes"
|
||||||
store={nodesStore} isClusterScoped
|
store={nodesStore}
|
||||||
isReady={nodesStore.isLoaded}
|
isReady={nodesStore.isLoaded}
|
||||||
dependentStores={[podsStore]}
|
dependentStores={[podsStore]}
|
||||||
isSelectable={false}
|
isSelectable={false}
|
||||||
|
|||||||
@ -43,7 +43,6 @@ export class PodSecurityPolicies extends React.Component {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="access_pod_security_policies"
|
tableId="access_pod_security_policies"
|
||||||
className="PodSecurityPolicies"
|
className="PodSecurityPolicies"
|
||||||
isClusterScoped={true}
|
|
||||||
store={podSecurityPoliciesStore}
|
store={podSecurityPoliciesStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[columnId.name]: (item: PodSecurityPolicy) => item.getName(),
|
[columnId.name]: (item: PodSecurityPolicy) => item.getName(),
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export class StorageClasses extends React.Component<Props> {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="storage_classes"
|
tableId="storage_classes"
|
||||||
className="StorageClasses"
|
className="StorageClasses"
|
||||||
store={storageClassStore} isClusterScoped
|
store={storageClassStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[columnId.name]: (item: StorageClass) => item.getName(),
|
[columnId.name]: (item: StorageClass) => item.getName(),
|
||||||
[columnId.age]: (item: StorageClass) => item.getTimeDiffFromNow(),
|
[columnId.age]: (item: StorageClass) => item.getTimeDiffFromNow(),
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export class PersistentVolumes extends React.Component<Props> {
|
|||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="storage_volumes"
|
tableId="storage_volumes"
|
||||||
className="PersistentVolumes"
|
className="PersistentVolumes"
|
||||||
store={volumesStore} isClusterScoped
|
store={volumesStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[columnId.name]: (item: PersistentVolume) => item.getName(),
|
[columnId.name]: (item: PersistentVolume) => item.getName(),
|
||||||
[columnId.storageClass]: (item: PersistentVolume) => item.getStorageClass(),
|
[columnId.storageClass]: (item: PersistentVolume) => item.getStorageClass(),
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
.Welcome {
|
.Welcome {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import { Events } from "../+events";
|
|||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { isAllowedResource } from "../../../common/rbac";
|
||||||
import { kubeWatchApi } from "../../api/kube-watch-api";
|
import { kubeWatchApi } from "../../api/kube-watch-api";
|
||||||
import { clusterContext } from "../context";
|
import { clusterContext } from "../context";
|
||||||
|
import { workloadsOverviewDetailRegistry } from "../../../extensions/registries";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IWorkloadsOverviewRouteParams> {
|
interface Props extends RouteComponentProps<IWorkloadsOverviewRouteParams> {
|
||||||
}
|
}
|
||||||
@ -57,11 +58,34 @@ export class WorkloadsOverview extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const items = workloadsOverviewDetailRegistry.getItems().map((item, index) => {
|
||||||
|
return (
|
||||||
|
<item.components.Details key={`workload-overview-${index}`}/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="WorkloadsOverview flex column gaps">
|
<div className="WorkloadsOverview flex column gaps">
|
||||||
<OverviewStatuses/>
|
{items}
|
||||||
{isAllowedResource("events") && <Events compact hideFilters className="box grow"/>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workloadsOverviewDetailRegistry.add([
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
Details: (props: any) => <OverviewStatuses {...props} />,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
priority: 5,
|
||||||
|
components: {
|
||||||
|
Details: () => {
|
||||||
|
return (
|
||||||
|
isAllowedResource("events") && <Events compact hideFilters className="box grow"/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|||||||
@ -30,6 +30,19 @@
|
|||||||
border: 1px solid gainsboro;
|
border: 1px solid gainsboro;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
pointer-events: none;
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: transparentize(white, .85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .editor {
|
> .editor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: inherit;
|
width: inherit;
|
||||||
|
|||||||
@ -169,11 +169,12 @@ export class AceEditor extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, hidden } = this.props;
|
const { className, hidden, children } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("AceEditor", className, { hidden })}>
|
<div className={cssNames("AceEditor", className, { hidden })}>
|
||||||
<div className="editor" ref={e => this.elem = e}/>
|
<div className="editor" ref={e => this.elem = e}/>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
--font-weight-normal: 400;
|
--font-weight-normal: 400;
|
||||||
--font-weight-bold: 500;
|
--font-weight-bold: 500;
|
||||||
--main-layout-header: 40px;
|
--main-layout-header: 40px;
|
||||||
--drag-region-height: 22px
|
--drag-region-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
*, *:before, *:after {
|
*, *:before, *:after {
|
||||||
|
|||||||
@ -71,6 +71,8 @@ import { CommandContainer } from "./command-palette/command-container";
|
|||||||
import { KubeObjectStore } from "../kube-object.store";
|
import { KubeObjectStore } from "../kube-object.store";
|
||||||
import { clusterContext } from "./context";
|
import { clusterContext } from "./context";
|
||||||
import { namespaceStore } from "./+namespaces/namespace.store";
|
import { namespaceStore } from "./+namespaces/namespace.store";
|
||||||
|
import { Sidebar } from "./layout/sidebar";
|
||||||
|
import { Dock } from "./dock";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class App extends React.Component {
|
export class App extends React.Component {
|
||||||
@ -176,7 +178,7 @@ export class App extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<MainLayout>
|
<MainLayout sidebar={<Sidebar/>} footer={<Dock/>}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route component={ClusterOverview} {...clusterRoute}/>
|
<Route component={ClusterOverview} {...clusterRoute}/>
|
||||||
<Route component={Nodes} {...nodesRoute}/>
|
<Route component={Nodes} {...nodesRoute}/>
|
||||||
|
|||||||
@ -46,10 +46,11 @@ export class PieChart extends React.Component<Props> {
|
|||||||
const dataset: any = data["datasets"][tooltipItem.datasetIndex];
|
const dataset: any = data["datasets"][tooltipItem.datasetIndex];
|
||||||
const metaData = Object.values<{ total: number }>(dataset["_meta"])[0];
|
const metaData = Object.values<{ total: number }>(dataset["_meta"])[0];
|
||||||
const percent = Math.round((dataset["data"][tooltipItem["index"]] / metaData.total) * 100);
|
const percent = Math.round((dataset["data"][tooltipItem["index"]] / metaData.total) * 100);
|
||||||
|
const label = dataset["label"];
|
||||||
|
|
||||||
if (isNaN(percent)) return "N/A";
|
if (isNaN(percent)) return `${label}: N/A`;
|
||||||
|
|
||||||
return `${percent}%`;
|
return `${label}: ${percent}%`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
filter: ({ datasetIndex, index }, { datasets }) => {
|
filter: ({ datasetIndex, index }, { datasets }) => {
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
grid-area: main;
|
grid-area: main;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HotbarMenu {
|
.HotbarMenu {
|
||||||
@ -45,7 +46,7 @@
|
|||||||
#lens-views {
|
#lens-views {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: var(--main-layout-header); // Move below the TopBar
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
44
src/renderer/components/cluster-manager/cluster-topbar.tsx
Normal file
44
src/renderer/components/cluster-manager/cluster-topbar.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { catalogURL } from "../+catalog";
|
||||||
|
import type { Cluster } from "../../../main/cluster";
|
||||||
|
import { navigate } from "../../navigation";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { TopBar } from "../layout/topbar";
|
||||||
|
import { MaterialTooltip } from "../material-tooltip/material-tooltip";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cluster: Cluster
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClusterTopbar({ cluster }: Props) {
|
||||||
|
return (
|
||||||
|
<TopBar label={cluster.name}>
|
||||||
|
<div>
|
||||||
|
<MaterialTooltip title="Back to Catalog" placement="left">
|
||||||
|
<Icon style={{ cursor: "default" }} material="close" onClick={() => navigate(catalogURL())}/>
|
||||||
|
</MaterialTooltip>
|
||||||
|
</div>
|
||||||
|
</TopBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -30,20 +30,26 @@ import { ClusterStore } from "../../../common/cluster-store";
|
|||||||
import { requestMain } from "../../../common/ipc";
|
import { requestMain } from "../../../common/ipc";
|
||||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import { getMatchedClusterId, navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { catalogURL } from "../+catalog/catalog.route";
|
import { catalogURL } from "../+catalog/catalog.route";
|
||||||
|
import { ClusterTopbar } from "./cluster-topbar";
|
||||||
|
import type { RouteComponentProps } from "react-router-dom";
|
||||||
|
import type { IClusterViewRouteParams } from "./cluster-view.route";
|
||||||
|
|
||||||
|
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterView extends React.Component {
|
export class ClusterView extends React.Component<Props> {
|
||||||
private store = ClusterStore.getInstance();
|
private store = ClusterStore.getInstance();
|
||||||
|
|
||||||
constructor(props: {}) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get clusterId() {
|
@computed get clusterId() {
|
||||||
return getMatchedClusterId();
|
return this.props.match.params.clusterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get cluster(): Cluster | undefined {
|
@computed get cluster(): Cluster | undefined {
|
||||||
@ -60,6 +66,10 @@ export class ClusterView extends React.Component {
|
|||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
refreshViews();
|
||||||
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.clusterId, async (clusterId) => {
|
reaction(() => this.clusterId, async (clusterId) => {
|
||||||
@ -71,14 +81,12 @@ export class ClusterView extends React.Component {
|
|||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
reaction(() => this.isReady, (ready) => {
|
reaction(() => [this.cluster?.ready, this.cluster?.disconnected], (values) => {
|
||||||
if (ready) {
|
const disconnected = values[1];
|
||||||
refreshViews(this.clusterId); // show cluster's view (iframe)
|
|
||||||
} else if (hasLoadedView(this.clusterId)) {
|
if (hasLoadedView(this.clusterId) && disconnected) {
|
||||||
navigate(catalogURL()); // redirect to catalog when active cluster get disconnected/not available
|
navigate(catalogURL()); // redirect to catalog when active cluster get disconnected/not available
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
fireImmediately: true,
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -87,7 +95,7 @@ export class ClusterView extends React.Component {
|
|||||||
const { clusterId, cluster, isReady } = this;
|
const { clusterId, cluster, isReady } = this;
|
||||||
|
|
||||||
if (cluster && !isReady) {
|
if (cluster && !isReady) {
|
||||||
return <ClusterStatus clusterId={clusterId} className="box center"/>;
|
return <ClusterStatus key={clusterId} clusterId={clusterId} className="box center"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -95,7 +103,8 @@ export class ClusterView extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="ClusterView flex align-center">
|
<div className="ClusterView flex column align-center">
|
||||||
|
{this.cluster && <ClusterTopbar cluster={this.cluster}/>}
|
||||||
{this.renderStatus()}
|
{this.renderStatus()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -22,6 +22,8 @@
|
|||||||
import { observable, when } from "mobx";
|
import { observable, when } from "mobx";
|
||||||
import { ClusterId, ClusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
|
import { ClusterId, ClusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
|
||||||
import logger from "../../../main/logger";
|
import logger from "../../../main/logger";
|
||||||
|
import { requestMain } from "../../../common/ipc";
|
||||||
|
import { clusterVisibilityHandler } from "../../../common/cluster-ipc";
|
||||||
|
|
||||||
export interface LensView {
|
export interface LensView {
|
||||||
isLoaded?: boolean
|
isLoaded?: boolean
|
||||||
@ -56,8 +58,13 @@ export async function initView(clusterId: ClusterId) {
|
|||||||
parentElem.appendChild(iframe);
|
parentElem.appendChild(iframe);
|
||||||
|
|
||||||
logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`);
|
logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`);
|
||||||
await cluster.whenReady;
|
|
||||||
await autoCleanOnRemove(clusterId, iframe);
|
try {
|
||||||
|
await when(() => cluster.ready, { timeout: 5_000 }); // we cannot wait forever because cleanup would be blocked for broken cluster connections
|
||||||
|
logger.info(`[LENS-VIEW]: cluster is ready, clusterId=${clusterId}`);
|
||||||
|
} finally {
|
||||||
|
await autoCleanOnRemove(clusterId, iframe);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) {
|
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) {
|
||||||
@ -88,5 +95,9 @@ export function refreshViews(visibleClusterId?: string) {
|
|||||||
const isVisible = isCurrent && isLoaded && isReady;
|
const isVisible = isCurrent && isLoaded && isReady;
|
||||||
|
|
||||||
view.style.display = isVisible ? "flex" : "none";
|
view.style.display = isVisible ? "flex" : "none";
|
||||||
|
|
||||||
|
requestMain(clusterVisibilityHandler, clusterId, isVisible).catch(() => {
|
||||||
|
logger.error(`[LENS-VIEW]: failed to set cluster visibility, clusterId=${clusterId}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export class LogList extends React.Component<Props> {
|
|||||||
*/
|
*/
|
||||||
@computed
|
@computed
|
||||||
get logs() {
|
get logs() {
|
||||||
const showTimestamps = logTabStore.getData(this.props.id).showTimestamps;
|
const showTimestamps = logTabStore.getData(this.props.id)?.showTimestamps;
|
||||||
|
|
||||||
if (!showTimestamps) {
|
if (!showTimestamps) {
|
||||||
return logStore.logsWithoutTimestamps;
|
return logStore.logsWithoutTimestamps;
|
||||||
|
|||||||
@ -39,6 +39,7 @@ interface Props extends DOMAttributes<HTMLElement> {
|
|||||||
errorClass?: IClassName;
|
errorClass?: IClassName;
|
||||||
add: (item: CatalogEntity, index: number) => void;
|
add: (item: CatalogEntity, index: number) => void;
|
||||||
remove: (uid: string) => void;
|
remove: (uid: string) => void;
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import { MaterialTooltip } from "../material-tooltip/material-tooltip";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Avatar } from "../avatar/avatar";
|
import { Avatar } from "../avatar/avatar";
|
||||||
|
|
||||||
interface Props extends DOMAttributes<HTMLElement> {
|
export interface HotbarIconProps extends DOMAttributes<HTMLElement> {
|
||||||
uid: string;
|
uid: string;
|
||||||
title: string;
|
title: string;
|
||||||
source: string;
|
source: string;
|
||||||
@ -40,6 +40,7 @@ interface Props extends DOMAttributes<HTMLElement> {
|
|||||||
active?: boolean;
|
active?: boolean;
|
||||||
menuItems?: CatalogEntityContextMenu[];
|
menuItems?: CatalogEntityContextMenu[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
||||||
@ -59,7 +60,7 @@ function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => {
|
export const HotbarIcon = observer(({menuItems = [], size = 40, ...props}: HotbarIconProps) => {
|
||||||
const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props;
|
const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props;
|
||||||
const id = `hotbarIcon-${uid}`;
|
const id = `hotbarIcon-${uid}`;
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
@ -77,8 +78,8 @@ export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => {
|
|||||||
title={title}
|
title={title}
|
||||||
colorHash={`${title}-${source}`}
|
colorHash={`${title}-${source}`}
|
||||||
className={active ? "active" : "default"}
|
className={active ? "active" : "default"}
|
||||||
width={40}
|
width={size}
|
||||||
height={40}
|
height={size}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -157,6 +157,7 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
className={cssNames({ isDragging: snapshot.isDragging })}
|
className={cssNames({ isDragging: snapshot.isDragging })}
|
||||||
remove={this.removeItem}
|
remove={this.removeItem}
|
||||||
add={this.addItem}
|
add={this.addItem}
|
||||||
|
size={40}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<HotbarIcon
|
<HotbarIcon
|
||||||
@ -165,6 +166,7 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
source={item.entity.source}
|
source={item.entity.source}
|
||||||
menuItems={disabledMenuItems}
|
menuItems={disabledMenuItems}
|
||||||
disabled
|
disabled
|
||||||
|
size={40}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -135,7 +135,7 @@
|
|||||||
&.interactive {
|
&.interactive {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: 250ms color, 250ms opacity, 150ms background-color, 150ms box-shadow;
|
transition: 250ms color, 250ms opacity, 150ms background-color, 150ms box-shadow;
|
||||||
border-radius: 50%;
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
&.focusable:focus:not(:hover) {
|
&.focusable:focus:not(:hover) {
|
||||||
box-shadow: 0 0 0 2px var(--focus-color);
|
box-shadow: 0 0 0 2px var(--focus-color);
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
padding: var(--flex-gap);
|
padding: var(--flex-gap);
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: $textColorPrimary;
|
color: var(--textColorTertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-panel {
|
.info-panel {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import groupBy from "lodash/groupBy";
|
|||||||
|
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { computed, makeObservable } from "mobx";
|
import { computed, makeObservable } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog";
|
import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog";
|
||||||
import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallback } from "../table";
|
import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallback } from "../table";
|
||||||
import { boundMethod, createStorage, cssNames, IClassName, isReactNode, noop, ObservableToggleSet, prevDefault, stopPropagation } from "../../utils";
|
import { boundMethod, createStorage, cssNames, IClassName, isReactNode, noop, ObservableToggleSet, prevDefault, stopPropagation } from "../../utils";
|
||||||
@ -36,13 +36,14 @@ import { SearchInputUrl } from "../input";
|
|||||||
import { Filter, FilterType, pageFilters } from "./page-filters.store";
|
import { Filter, FilterType, pageFilters } from "./page-filters.store";
|
||||||
import { PageFiltersList } from "./page-filters-list";
|
import { PageFiltersList } from "./page-filters-list";
|
||||||
import { PageFiltersSelect } from "./page-filters-select";
|
import { PageFiltersSelect } from "./page-filters-select";
|
||||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
|
||||||
import { ThemeStore } from "../../theme.store";
|
import { ThemeStore } from "../../theme.store";
|
||||||
import { MenuActions } from "../menu/menu-actions";
|
import { MenuActions } from "../menu/menu-actions";
|
||||||
import { MenuItem } from "../menu";
|
import { MenuItem } from "../menu";
|
||||||
import { Checkbox } from "../checkbox";
|
import { Checkbox } from "../checkbox";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
|
import { KubeObjectStore } from "../../kube-object.store";
|
||||||
|
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||||
|
|
||||||
// todo: refactor, split to small re-usable components
|
// todo: refactor, split to small re-usable components
|
||||||
|
|
||||||
@ -63,7 +64,6 @@ export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
|
|||||||
store: ItemStore<T>;
|
store: ItemStore<T>;
|
||||||
dependentStores?: ItemStore[];
|
dependentStores?: ItemStore[];
|
||||||
preloadStores?: boolean;
|
preloadStores?: boolean;
|
||||||
isClusterScoped?: boolean;
|
|
||||||
hideFilters?: boolean;
|
hideFilters?: boolean;
|
||||||
searchFilters?: SearchFilter<T>[];
|
searchFilters?: SearchFilter<T>[];
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
@ -137,7 +137,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { isClusterScoped, isConfigurable, tableId, preloadStores } = this.props;
|
const { isConfigurable, tableId, preloadStores } = this.props;
|
||||||
|
|
||||||
if (isConfigurable && !tableId) {
|
if (isConfigurable && !tableId) {
|
||||||
throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified");
|
throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified");
|
||||||
@ -149,12 +149,6 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
|
|
||||||
if (preloadStores) {
|
if (preloadStores) {
|
||||||
this.loadStores();
|
this.loadStores();
|
||||||
|
|
||||||
if (!isClusterScoped) {
|
|
||||||
disposeOnUnmount(this, [
|
|
||||||
namespaceStore.onContextChange(() => this.loadStores())
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +156,6 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
const { store, dependentStores } = this.props;
|
const { store, dependentStores } = this.props;
|
||||||
const stores = Array.from(new Set([store, ...dependentStores]));
|
const stores = Array.from(new Set([store, ...dependentStores]));
|
||||||
|
|
||||||
// load context namespaces by default (see also: `<NamespaceSelectFilter/>`)
|
|
||||||
stores.forEach(store => store.loadAll(namespaceStore.contextNamespaces));
|
stores.forEach(store => store.loadAll(namespaceStore.contextNamespaces));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +217,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get items() {
|
@computed get items() {
|
||||||
const {filters, filterCallbacks } = this;
|
const { filters, filterCallbacks } = this;
|
||||||
const filterGroups = groupBy<Filter>(filters, ({ type }) => type);
|
const filterGroups = groupBy<Filter>(filters, ({ type }) => type);
|
||||||
|
|
||||||
const filterItems: ItemsFilter[] = [];
|
const filterItems: ItemsFilter[] = [];
|
||||||
@ -330,7 +323,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PageFiltersList filters={filters}/>;
|
return <PageFiltersList filters={filters} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNoItems() {
|
renderNoItems() {
|
||||||
@ -355,7 +348,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <NoItems/>;
|
return <NoItems />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItems() {
|
renderItems() {
|
||||||
@ -397,20 +390,26 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderHeader() {
|
renderHeader() {
|
||||||
const { showHeader, customizeHeader, renderHeaderTitle, headerClassName, isClusterScoped } = this.props;
|
const { showHeader, customizeHeader, renderHeaderTitle, headerClassName } = this.props;
|
||||||
|
|
||||||
if (!showHeader) return null;
|
if (!showHeader) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showNamespaceSelectFilter = this.props.store instanceof KubeObjectStore && this.props.store.api.isNamespaced;
|
||||||
const title = typeof renderHeaderTitle === "function" ? renderHeaderTitle(this) : renderHeaderTitle;
|
const title = typeof renderHeaderTitle === "function" ? renderHeaderTitle(this) : renderHeaderTitle;
|
||||||
const placeholders: IHeaderPlaceholders = {
|
const placeholders: IHeaderPlaceholders = {
|
||||||
title: <h5 className="title">{title}</h5>,
|
title: <h5 className="title">{title}</h5>,
|
||||||
info: this.renderInfo(),
|
info: this.renderInfo(),
|
||||||
filters: <>
|
filters: (
|
||||||
{!isClusterScoped && <NamespaceSelectFilter/>}
|
<>
|
||||||
<PageFiltersSelect allowEmpty disableFilters={{
|
{showNamespaceSelectFilter && <NamespaceSelectFilter />}
|
||||||
[FilterType.NAMESPACE]: true, // namespace-select used instead
|
<PageFiltersSelect allowEmpty disableFilters={{
|
||||||
}}/>
|
[FilterType.NAMESPACE]: true, // namespace-select used instead
|
||||||
</>,
|
}} />
|
||||||
search: <SearchInputUrl/>,
|
</>
|
||||||
|
),
|
||||||
|
search: <SearchInputUrl />,
|
||||||
};
|
};
|
||||||
let header = this.renderHeaderContent(placeholders);
|
let header = this.renderHeaderContent(placeholders);
|
||||||
|
|
||||||
|
|||||||
@ -1,88 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2021 OpenLens Authors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
jest.mock("../../../../common/ipc");
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { render } from "@testing-library/react";
|
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
|
||||||
|
|
||||||
import { MainLayoutHeader } from "../main-layout-header";
|
|
||||||
import { Cluster } from "../../../../main/cluster";
|
|
||||||
import { ClusterStore } from "../../../../common/cluster-store";
|
|
||||||
import mockFs from "mock-fs";
|
|
||||||
|
|
||||||
describe("<MainLayoutHeader />", () => {
|
|
||||||
let cluster: Cluster;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const mockOpts = {
|
|
||||||
"minikube-config.yml": JSON.stringify({
|
|
||||||
apiVersion: "v1",
|
|
||||||
clusters: [{
|
|
||||||
name: "minikube",
|
|
||||||
cluster: {
|
|
||||||
server: "https://192.168.64.3:8443",
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
contexts: [{
|
|
||||||
context: {
|
|
||||||
cluster: "minikube",
|
|
||||||
user: "minikube",
|
|
||||||
},
|
|
||||||
name: "minikube",
|
|
||||||
}],
|
|
||||||
users: [{
|
|
||||||
name: "minikube",
|
|
||||||
}],
|
|
||||||
kind: "Config",
|
|
||||||
preferences: {},
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
mockFs(mockOpts);
|
|
||||||
|
|
||||||
ClusterStore.createInstance();
|
|
||||||
|
|
||||||
cluster = new Cluster({
|
|
||||||
id: "foo",
|
|
||||||
contextName: "minikube",
|
|
||||||
kubeConfigPath: "minikube-config.yml",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
ClusterStore.resetInstance();
|
|
||||||
mockFs.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders w/o errors", () => {
|
|
||||||
const { container } = render(<MainLayoutHeader cluster={cluster} />);
|
|
||||||
|
|
||||||
expect(container).toBeInstanceOf(HTMLElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders cluster name", () => {
|
|
||||||
const { getByText } = render(<MainLayoutHeader cluster={cluster} />);
|
|
||||||
|
|
||||||
expect(getByText("minikube")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
50
src/renderer/components/layout/main-layout.module.css
Normal file
50
src/renderer/components/layout/main-layout.module.css
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mainLayout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"sidebar contents"
|
||||||
|
"sidebar footer";
|
||||||
|
grid-template-rows: [contents] 1fr [footer] auto;
|
||||||
|
grid-template-columns: [sidebar] var(--sidebar-width) [contents] 1fr;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
height: calc(100% - var(--main-layout-header));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
grid-area: sidebar;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
background: var(--sidebarBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contents {
|
||||||
|
grid-area: contents;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: relative;
|
||||||
|
grid-area: footer;
|
||||||
|
min-width: 0; /* restrict size when overflow content (e.g. <Dock> tabs scrolling) */
|
||||||
|
}
|
||||||
@ -1,76 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2021 OpenLens Authors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.MainLayout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas:
|
|
||||||
"aside header"
|
|
||||||
"aside tabs"
|
|
||||||
"aside main"
|
|
||||||
"aside footer";
|
|
||||||
grid-template-rows: [header] var(--main-layout-header) [tabs] min-content [main] 1fr [footer] auto;
|
|
||||||
grid-template-columns: [sidebar] minmax(var(--main-layout-header), min-content) [main] 1fr;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
> header {
|
|
||||||
grid-area: header;
|
|
||||||
background: $layoutBackground;
|
|
||||||
padding: $padding $padding * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
> aside {
|
|
||||||
grid-area: aside;
|
|
||||||
position: relative;
|
|
||||||
background: $sidebarBackground;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
width: var(--sidebar-width);
|
|
||||||
|
|
||||||
&.compact {
|
|
||||||
position: absolute;
|
|
||||||
width: var(--main-layout-header);
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
width: var(--sidebar-width);
|
|
||||||
transition-delay: 750ms;
|
|
||||||
box-shadow: 3px 3px 16px rgba(0, 0, 0, 0.35);
|
|
||||||
z-index: $zIndex-sidebar-hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> main {
|
|
||||||
display: contents;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
grid-area: main;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
position: relative;
|
|
||||||
grid-area: footer;
|
|
||||||
min-width: 0; // restrict size when overflow content (e.g. <Dock> tabs scrolling)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -19,73 +19,53 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./main-layout.scss";
|
import styles from "./main-layout.module.css";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { getHostedCluster } from "../../../common/cluster-store";
|
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Dock } from "../dock";
|
|
||||||
import { ErrorBoundary } from "../error-boundary";
|
import { ErrorBoundary } from "../error-boundary";
|
||||||
import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor";
|
import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor";
|
||||||
import { MainLayoutHeader } from "./main-layout-header";
|
|
||||||
import { Sidebar } from "./sidebar";
|
|
||||||
import { sidebarStorage } from "./sidebar-storage";
|
import { sidebarStorage } from "./sidebar-storage";
|
||||||
|
|
||||||
export interface MainLayoutProps {
|
interface Props {
|
||||||
className?: any;
|
sidebar: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
footer?: React.ReactNode;
|
footer?: React.ReactNode;
|
||||||
headerClass?: string;
|
|
||||||
footerClass?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class MainLayout extends React.Component<MainLayoutProps> {
|
export class MainLayout extends React.Component<Props> {
|
||||||
onSidebarCompactModeChange = () => {
|
|
||||||
sidebarStorage.merge(draft => {
|
|
||||||
draft.compact = !draft.compact;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onSidebarResize = (width: number) => {
|
onSidebarResize = (width: number) => {
|
||||||
sidebarStorage.merge({ width });
|
sidebarStorage.merge({ width });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const cluster = getHostedCluster();
|
const { onSidebarResize } = this;
|
||||||
const { onSidebarCompactModeChange, onSidebarResize } = this;
|
const { className, footer, children, sidebar } = this.props;
|
||||||
const { className, headerClass, footer, footerClass, children } = this.props;
|
const { width: sidebarWidth } = sidebarStorage.get();
|
||||||
const { compact, width: sidebarWidth } = sidebarStorage.get();
|
|
||||||
const style = { "--sidebar-width": `${sidebarWidth}px` } as React.CSSProperties;
|
const style = { "--sidebar-width": `${sidebarWidth}px` } as React.CSSProperties;
|
||||||
|
|
||||||
if (!cluster) {
|
|
||||||
return null; // fix: skip render when removing active (visible) cluster
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("MainLayout", className)} style={style}>
|
<div className={cssNames(styles.mainLayout, className)} style={style}>
|
||||||
<MainLayoutHeader className={headerClass} cluster={cluster}/>
|
<div className={styles.sidebar}>
|
||||||
|
{sidebar}
|
||||||
<aside className={cssNames("flex column", { compact })}>
|
|
||||||
<Sidebar className="box grow" compact={compact} toggle={onSidebarCompactModeChange}/>
|
|
||||||
<ResizingAnchor
|
<ResizingAnchor
|
||||||
direction={ResizeDirection.HORIZONTAL}
|
direction={ResizeDirection.HORIZONTAL}
|
||||||
placement={ResizeSide.TRAILING}
|
placement={ResizeSide.TRAILING}
|
||||||
growthDirection={ResizeGrowthDirection.LEFT_TO_RIGHT}
|
growthDirection={ResizeGrowthDirection.LEFT_TO_RIGHT}
|
||||||
getCurrentExtent={() => sidebarWidth}
|
getCurrentExtent={() => sidebarWidth}
|
||||||
onDrag={onSidebarResize}
|
onDrag={onSidebarResize}
|
||||||
onDoubleClick={onSidebarCompactModeChange}
|
|
||||||
disabled={compact}
|
|
||||||
minExtent={120}
|
minExtent={120}
|
||||||
maxExtent={400}
|
maxExtent={400}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</div>
|
||||||
|
|
||||||
<main>
|
<div className={styles.contents}>
|
||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
<footer className={footerClass}>{footer ?? <Dock/>}</footer>
|
<div className={styles.footer}>{footer}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,10 +62,6 @@ export class SidebarItem extends React.Component<SidebarItemProps> {
|
|||||||
return this.props.id;
|
return this.props.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get compact(): boolean {
|
|
||||||
return Boolean(sidebarStorage.get().compact);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get expanded(): boolean {
|
@computed get expanded(): boolean {
|
||||||
return Boolean(sidebarStorage.get().expanded[this.id]);
|
return Boolean(sidebarStorage.get().expanded[this.id]);
|
||||||
}
|
}
|
||||||
@ -78,8 +74,6 @@ export class SidebarItem extends React.Component<SidebarItemProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get isExpandable(): boolean {
|
@computed get isExpandable(): boolean {
|
||||||
if (this.compact) return false; // not available in compact-mode currently
|
|
||||||
|
|
||||||
return Boolean(this.props.children);
|
return Boolean(this.props.children);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,10 +102,8 @@ export class SidebarItem extends React.Component<SidebarItemProps> {
|
|||||||
|
|
||||||
if (isHidden) return null;
|
if (isHidden) return null;
|
||||||
|
|
||||||
const { isActive, id, compact, expanded, isExpandable, toggleExpand } = this;
|
const { isActive, id, expanded, isExpandable, toggleExpand } = this;
|
||||||
const classNames = cssNames(SidebarItem.displayName, className, {
|
const classNames = cssNames(SidebarItem.displayName, className);
|
||||||
compact,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames} data-test-id={id}>
|
<div className={classNames} data-test-id={id}>
|
||||||
|
|||||||
@ -23,14 +23,12 @@ import { createStorage } from "../../utils";
|
|||||||
|
|
||||||
export interface SidebarStorageState {
|
export interface SidebarStorageState {
|
||||||
width: number;
|
width: number;
|
||||||
compact: boolean;
|
|
||||||
expanded: {
|
expanded: {
|
||||||
[itemId: string]: boolean;
|
[itemId: string]: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sidebarStorage = createStorage<SidebarStorageState>("sidebar", {
|
export const sidebarStorage = createStorage<SidebarStorageState>("sidebar", {
|
||||||
width: 200, // sidebar size in non-compact mode
|
width: 200,
|
||||||
compact: false, // compact-mode (icons only)
|
|
||||||
expanded: {},
|
expanded: {},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -23,49 +23,9 @@
|
|||||||
$iconSize: 24px;
|
$iconSize: 24px;
|
||||||
$itemSpacing: floor($unit / 2.6) floor($unit / 1.6);
|
$itemSpacing: floor($unit / 2.6) floor($unit / 1.6);
|
||||||
|
|
||||||
&.compact {
|
|
||||||
.sidebar-nav {
|
|
||||||
@include hidden-scrollbar; // fix: scrollbar overlaps icons
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background: $sidebarLogoBackground;
|
|
||||||
padding: $padding / 2;
|
|
||||||
height: var(--main-layout-header);
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-size: 18.5px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.logo-text {
|
|
||||||
position: absolute;
|
|
||||||
left: 42px;
|
|
||||||
top: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-icon {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
margin-left: 2px;
|
|
||||||
margin-top: 2px;
|
|
||||||
margin-right: 10px;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
--size: 28px;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-icon {
|
|
||||||
margin: auto;
|
|
||||||
margin-right: $padding / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-nav {
|
.sidebar-nav {
|
||||||
padding: $padding / 1.5 0;
|
width: var(--sidebar-width);
|
||||||
|
padding-bottom: calc(var(--padding) * 3);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
.Icon {
|
.Icon {
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import type { TabLayoutRoute } from "./tab-layout";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { NavLink } from "react-router-dom";
|
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { workloadsRoute, workloadsURL } from "../+workloads/workloads.route";
|
import { workloadsRoute, workloadsURL } from "../+workloads/workloads.route";
|
||||||
@ -52,8 +51,6 @@ import { SidebarItem } from "./sidebar-item";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
compact?: boolean; // compact-mode view: show only icons and expand on :hover
|
|
||||||
toggle(): void; // compact-mode updater
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -173,24 +170,11 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { toggle, compact, className } = this.props;
|
const { className } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames(Sidebar.displayName, "flex column", { compact }, className)}>
|
<div className={cssNames(Sidebar.displayName, "flex column", className)}>
|
||||||
<div className="header flex align-center">
|
<div className={cssNames("sidebar-nav flex column box grow-fixed")}>
|
||||||
<NavLink exact to="/" className="box grow">
|
|
||||||
<Icon svg="logo-lens" className="logo-icon"/>
|
|
||||||
<div className="logo-text">Lens</div>
|
|
||||||
</NavLink>
|
|
||||||
<Icon
|
|
||||||
focusable={false}
|
|
||||||
className="pin-icon"
|
|
||||||
tooltip="Compact view"
|
|
||||||
material={compact ? "keyboard_arrow_right" : "keyboard_arrow_left"}
|
|
||||||
onClick={toggle}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={cssNames("sidebar-nav flex column box grow-fixed", { compact })}>
|
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
id="cluster"
|
id="cluster"
|
||||||
text="Cluster"
|
text="Cluster"
|
||||||
|
|||||||
@ -21,18 +21,19 @@
|
|||||||
|
|
||||||
|
|
||||||
.TabLayout {
|
.TabLayout {
|
||||||
display: contents;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
> .Tabs {
|
> .Tabs {
|
||||||
grid-area: tabs;
|
|
||||||
background: $layoutTabsBackground;
|
background: $layoutTabsBackground;
|
||||||
|
min-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
main {
|
main {
|
||||||
$spacing: $margin * 2;
|
$spacing: $margin * 2;
|
||||||
|
|
||||||
grid-area: main;
|
flex-grow: 1;
|
||||||
overflow-y: scroll; // always reserve space for scrollbar (17px)
|
overflow-y: scroll; // always reserve space for scrollbar (17px)
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: $spacing;
|
margin: $spacing;
|
||||||
|
|||||||
45
src/renderer/components/layout/topbar.module.css
Normal file
45
src/renderer/components/layout/topbar.module.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.topBar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: [title] 1fr [controls] auto;
|
||||||
|
grid-template-rows: var(--main-layout-header);
|
||||||
|
grid-template-areas: "title controls";
|
||||||
|
background-color: var(--layoutBackground);
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@apply font-bold px-6;
|
||||||
|
color: var(--textColorAccent);
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
align-self: flex-end;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@ -19,20 +19,19 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import styles from "./topbar.module.css";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import type { Cluster } from "../../../main/cluster";
|
|
||||||
import { cssNames } from "../../utils";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props extends React.HTMLAttributes<any> {
|
||||||
cluster: Cluster
|
label: React.ReactNode;
|
||||||
className?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MainLayoutHeader = observer(({ cluster, className }: Props) => {
|
export const TopBar = observer(({ label, children, ...rest }: Props) => {
|
||||||
return (
|
return (
|
||||||
<header className={cssNames("flex gaps align-center justify-space-between", className)}>
|
<div className={styles.topBar} {...rest}>
|
||||||
<span className="cluster">{cluster.name}</span>
|
<div className={styles.title}>{label}</div>
|
||||||
</header>
|
<div className={styles.controls}>{children}</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -37,7 +37,7 @@ export class Notifications extends React.Component {
|
|||||||
static ok(message: NotificationMessage) {
|
static ok(message: NotificationMessage) {
|
||||||
notificationsStore.add({
|
notificationsStore.add({
|
||||||
message,
|
message,
|
||||||
timeout: 2500,
|
timeout: 2_500,
|
||||||
status: NotificationStatus.OK
|
status: NotificationStatus.OK
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -45,12 +45,19 @@ export class Notifications extends React.Component {
|
|||||||
static error(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
static error(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
||||||
notificationsStore.add({
|
notificationsStore.add({
|
||||||
message,
|
message,
|
||||||
timeout: 10000,
|
timeout: 10_000,
|
||||||
status: NotificationStatus.ERROR,
|
status: NotificationStatus.ERROR,
|
||||||
...customOpts
|
...customOpts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static shortInfo(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
||||||
|
this.info(message, {
|
||||||
|
timeout: 5_000,
|
||||||
|
...customOpts
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static info(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
static info(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
||||||
return notificationsStore.add({
|
return notificationsStore.add({
|
||||||
status: NotificationStatus.INFO,
|
status: NotificationStatus.INFO,
|
||||||
|
|||||||
@ -31,6 +31,23 @@ body.resizing {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
width: 3px;
|
||||||
|
height: 100%;
|
||||||
|
margin-left: 50%;
|
||||||
|
background: transparent;
|
||||||
|
transition: all 0.2s 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::after {
|
||||||
|
background: var(--blue);
|
||||||
|
transition: all 0.2s 0.5s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -56,6 +73,17 @@ body.resizing {
|
|||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
width: $dimension;
|
width: $dimension;
|
||||||
|
|
||||||
|
// Expand hoverable area while resizing to keep highlighting resizer.
|
||||||
|
// Otherwise, cursor can move far away dropping hover indicator.
|
||||||
|
.resizing & {
|
||||||
|
$expandedWidth: 200px;
|
||||||
|
width: $expandedWidth;
|
||||||
|
|
||||||
|
&.trailing {
|
||||||
|
right: -$expandedWidth / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.leading {
|
&.leading {
|
||||||
left: -$dimension / 2;
|
left: -$dimension / 2;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user