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

Merge branch 'master' into fix-big-cluster-table-freeze

This commit is contained in:
Alex Andreev 2022-03-30 13:54:11 +03:00
commit 85703f11e5
112 changed files with 2894 additions and 1221 deletions

View File

@ -6,29 +6,29 @@ The Renderer Extension API allows you to access, configure, and customize Lens d
The custom Lens UI elements that you can add include:
* [Cluster pages](#clusterpages)
* [Cluster page menus](#clusterpagemenus)
* [Global pages](#globalpages)
* [Welcome menus](#welcomemenus)
* [App preferences](#apppreferences)
* [Top bar items](#topbaritems)
* [Status bar items](#statusbaritems)
* [KubeObject menu items](#kubeobjectmenuitems)
* [KubeObject detail items](#kubeobjectdetailitems)
* [KubeObject status texts](#kubeobjectstatustexts)
* [Kube workloads overview items](#kubeworkloadsoverviewitems)
- [Cluster pages](#clusterpages)
- [Cluster page menus](#clusterpagemenus)
- [Global pages](#globalpages)
- [Welcome menus](#welcomemenus)
- [App preferences](#apppreferences)
- [Top bar items](#topbaritems)
- [Status bar items](#statusbaritems)
- [KubeObject menu items](#kubeobjectmenuitems)
- [KubeObject detail items](#kubeobjectdetailitems)
- [KubeObject status texts](#kubeobjectstatustexts)
- [Kube workloads overview items](#kubeworkloadsoverviewitems)
as well as catalog-related UI elements:
* [Entity settings](#entitysettings)
* [Catalog entity detail items](#catalogentitydetailitems)
- [Entity settings](#entitysettings)
- [Catalog entity detail items](#catalogentitydetailitems)
All UI elements are based on React components.
Finally, you can also add commands and protocol handlers:
* [Command palette commands](#commandpalettecommands)
* [protocol handlers](protocol-handlers.md)
- [Command palette commands](#commandpalettecommands)
- [protocol handlers](protocol-handlers.md)
## `Renderer.LensExtension` Class
@ -41,11 +41,11 @@ import { Renderer } from "@k8slens/extensions";
export default class ExampleExtensionMain extends Renderer.LensExtension {
onActivate() {
console.log('custom renderer process extension code started');
console.log("custom renderer process extension code started");
}
onDeactivate() {
console.log('custom renderer process extension de-activated');
console.log("custom renderer process extension de-activated");
}
}
```
@ -56,7 +56,7 @@ You can initiate custom code by implementing `onActivate()`.
Implementing `onDeactivate()` gives you the opportunity to clean up after your extension.
!!! info
Disable extensions from the Lens Extensions page:
Disable extensions from the Lens Extensions page:
1. Navigate to **File** > **Extensions** in the top menu bar.
(On Mac, it is **Lens** > **Extensions**.)
@ -75,17 +75,17 @@ Add a cluster page definition to a `Renderer.LensExtension` subclass with the fo
```typescript
import { Renderer } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page"
import React from "react"
import { ExampleIcon, ExamplePage } from "./page";
import React from "react";
export default class ExampleExtension extends Renderer.LensExtension {
clusterPages = [
{
id: "hello",
components: {
Page: () => <ExamplePage extension={this}/>,
}
}
Page: () => <ExamplePage extension={this} />,
},
},
];
}
```
@ -93,24 +93,26 @@ export default class ExampleExtension extends Renderer.LensExtension {
`clusterPages` is an array of objects that satisfy the `PageRegistration` interface.
The properties of the `clusterPages` array objects are defined as follows:
* `id` is a string that identifies the page.
* `components` matches the `PageComponents` interface for which there is one field, `Page`.
* `Page` is of type ` React.ComponentType<any>`.
It offers flexibility in defining the appearance and behavior of your page.
- `id` is a string that identifies the page.
- `components` matches the `PageComponents` interface for which there is one field, `Page`.
- `Page` is of type ` React.ComponentType<any>`.
It offers flexibility in defining the appearance and behavior of your page.
`ExamplePage` in the example above can be defined in `page.tsx`:
```typescript
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;
}> {
render() {
return (
<div>
<p>Hello world!</p>
</div>
)
);
}
}
```
@ -130,17 +132,17 @@ By expanding on the above example, you can add a cluster page menu item to the `
```typescript
import { Renderer } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page"
import React from "react"
import { ExampleIcon, ExamplePage } from "./page";
import React from "react";
export default class ExampleExtension extends Renderer.LensExtension {
clusterPages = [
{
id: "hello",
components: {
Page: () => <ExamplePage extension={this}/>,
}
}
Page: () => <ExamplePage extension={this} />,
},
},
];
clusterPageMenus = [
@ -149,7 +151,7 @@ export default class ExampleExtension extends Renderer.LensExtension {
title: "Hello World",
components: {
Icon: ExampleIcon,
}
},
},
];
}
@ -159,10 +161,10 @@ export default class ExampleExtension extends Renderer.LensExtension {
This element defines how the cluster page menu item will appear and what it will do when you click it.
The properties of the `clusterPageMenus` array objects are defined as follows:
* `target` links to the relevant cluster page using `pageId`.
* `pageId` takes the value of the relevant cluster page's `id` property.
* `title` sets the name of the cluster page menu item that will appear in the left side menu.
* `components` is used to set an icon that appears to the left of the `title` text in the left side menu.
- `target` links to the relevant cluster page using `pageId`.
- `pageId` takes the value of the relevant cluster page's `id` property.
- `title` sets the name of the cluster page menu item that will appear in the left side menu.
- `components` is used to set an icon that appears to the left of the `title` text in the left side menu.
The above example creates a menu item that reads **Hello World**.
When users click **Hello World**, the cluster dashboard will show the contents of `Example Page`.
@ -171,7 +173,7 @@ This example requires the definition of another React-based component, `ExampleI
```typescript
import { Renderer } from "@k8slens/extensions";
import React from "react"
import React from "react";
type IconProps = Renderer.Component.IconProps;
@ -180,16 +182,18 @@ const {
} = Renderer;
export function ExampleIcon(props: IconProps) {
return <Icon {...props} material="pages" tooltip={"Hi!"}/>
return <Icon {...props} material="pages" tooltip={"Hi!"} />;
}
export class ExamplePage extends React.Component<{ extension: Renderer.LensExtension }> {
export class ExamplePage extends React.Component<{
extension: Renderer.LensExtension;
}> {
render() {
return (
<div>
<p>Hello world!</p>
</div>
)
);
}
}
```
@ -198,32 +202,31 @@ Lens includes various built-in components available for extension developers to
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 `Renderer.Component.Icon` uses are defined as follows:
* `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.
- `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.
`clusterPageMenus` can also be used to define sub menu items, so that you can create groups of cluster pages.
The following example groups two sub menu items under one parent menu item:
```typescript
import { Renderer } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page"
import React from "react"
import { ExampleIcon, ExamplePage } from "./page";
import React from "react";
export default class ExampleExtension extends Renderer.LensExtension {
clusterPages = [
{
id: "hello",
components: {
Page: () => <ExamplePage extension={this}/>,
}
Page: () => <ExamplePage extension={this} />,
},
},
{
id: "bonjour",
components: {
Page: () => <ExemplePage extension={this}/>,
}
}
Page: () => <ExemplePage extension={this} />,
},
},
];
clusterPageMenus = [
@ -232,7 +235,7 @@ export default class ExampleExtension extends Renderer.LensExtension {
title: "Greetings",
components: {
Icon: ExampleIcon,
}
},
},
{
parentId: "example",
@ -240,7 +243,7 @@ export default class ExampleExtension extends Renderer.LensExtension {
title: "Hello World",
components: {
Icon: ExampleIcon,
}
},
},
{
parentId: "example",
@ -248,8 +251,8 @@ export default class ExampleExtension extends Renderer.LensExtension {
title: "Bonjour le monde",
components: {
Icon: ExempleIcon,
}
}
},
},
];
}
```
@ -280,18 +283,18 @@ Unlike cluster pages, users can trigger global pages even when there is no activ
The following example defines a `Renderer.LensExtension` subclass with a single global page definition:
```typescript
import { Renderer } from '@k8slens/extensions';
import { HelpPage } from './page';
import React from 'react';
import { Renderer } from "@k8slens/extensions";
import { HelpPage } from "./page";
import React from "react";
export default class HelpExtension extends Renderer.LensExtension {
globalPages = [
{
id: "help",
components: {
Page: () => <HelpPage extension={this}/>,
}
}
Page: () => <HelpPage extension={this} />,
},
},
];
}
```
@ -299,24 +302,26 @@ export default class HelpExtension extends Renderer.LensExtension {
`globalPages` is an array of objects that satisfy the `PageRegistration` interface.
The properties of the `globalPages` array objects are defined as follows:
* `id` is a string that identifies the page.
* `components` matches the `PageComponents` interface for which there is one field, `Page`.
* `Page` is of type `React.ComponentType<any>`.
It offers flexibility in defining the appearance and behavior of your page.
- `id` is a string that identifies the page.
- `components` matches the `PageComponents` interface for which there is one field, `Page`.
- `Page` is of type `React.ComponentType<any>`.
It offers flexibility in defining the appearance and behavior of your page.
`HelpPage` in the example above can be defined in `page.tsx`:
```typescript
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;
}> {
render() {
return (
<div>
<p>Help yourself</p>
</div>
)
);
}
}
```
@ -328,11 +333,12 @@ This way, `HelpPage` can access all `HelpExtension` subclass data.
This example code shows how to create a global page, but not how to make that page available to the Lens user.
Global pages are typically made available in the following ways:
* To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide.
* To add global pages as an interactive element in the blue status bar along the bottom of the Lens UI, see [`statusBarItems`](#statusbaritems).
* To add global pages to the Welcome Page, see [`welcomeMenus`](#welcomemenus).
- To add global pages to the top menu bar, see [`appMenus`](../main-extension#appmenus) in the Main Extension guide.
- To add global pages as an interactive element in the blue status bar along the bottom of the Lens UI, see [`statusBarItems`](#statusbaritems).
- To add global pages to the Welcome Page, see [`welcomeMenus`](#welcomemenus).
### `welcomeMenus`
### `appPreferences`
The Lens **Preferences** page is a built-in global page.
@ -342,22 +348,24 @@ The following example demonstrates adding a custom preference:
```typescript
import { Renderer } from "@k8slens/extensions";
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
import {
ExamplePreferenceHint,
ExamplePreferenceInput,
} from "./src/example-preference";
import { observable } from "mobx";
import React from "react";
export default class ExampleRendererExtension extends Renderer.LensExtension {
@observable preference = { enabled: false };
appPreferences = [
{
title: "Example Preferences",
components: {
Input: () => <ExamplePreferenceInput preference={this.preference}/>,
Hint: () => <ExamplePreferenceHint/>
}
}
Input: () => <ExamplePreferenceInput preference={this.preference} />,
Hint: () => <ExamplePreferenceHint />,
},
},
];
}
```
@ -365,13 +373,13 @@ export default class ExampleRendererExtension extends Renderer.LensExtension {
`appPreferences` is an array of objects that satisfies the `AppPreferenceRegistration` interface.
The properties of the `appPreferences` array objects are defined as follows:
* `title` sets the heading text displayed on the Preferences page.
* `components` specifies two `React.Component` objects that define the interface for the preference.
* `Input` specifies an interactive input element for the preference.
* `Hint` provides descriptive information for the preference, shown below the `Input` element.
- `title` sets the heading text displayed on the Preferences page.
- `components` specifies two `React.Component` objects that define the interface for the preference.
- `Input` specifies an interactive input element for the preference.
- `Hint` provides descriptive information for the preference, shown below the `Input` element.
!!! note
Note that the input and the hint can be comprised of more sophisticated elements, according to the needs of the extension.
Note that the input and the hint can be comprised of more sophisticated elements, according to the needs of the extension.
`ExamplePreferenceInput` expects its React props to be set to an `ExamplePreferenceProps` instance.
This is how `ExampleRendererExtension` handles the state of the preference input.
@ -386,22 +394,19 @@ import { observer } from "mobx-react";
import React from "react";
const {
Component: {
Checkbox,
},
Component: { Checkbox },
} = Renderer;
export class ExamplePreferenceProps {
preference: {
enabled: boolean;
}
};
}
@observer
export class ExamplePreferenceInput extends React.Component<ExamplePreferenceProps> {
public constructor() {
super({preference: { enabled: false}});
super({ preference: { enabled: false } });
makeObservable(this);
}
@ -411,7 +416,9 @@ export class ExamplePreferenceInput extends React.Component<ExamplePreferencePro
<Checkbox
label="I understand appPreferences"
value={preference.enabled}
onChange={v => { preference.enabled = v; }}
onChange={(v) => {
preference.enabled = v;
}}
/>
);
}
@ -419,18 +426,16 @@ export class ExamplePreferenceInput extends React.Component<ExamplePreferencePro
export class ExamplePreferenceHint extends React.Component {
render() {
return (
<span>This is an example of an appPreference for extensions.</span>
);
return <span>This is an example of an appPreference for extensions.</span>;
}
}
```
`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.
* `value` is initially set to `preference.enabled`.
* `onChange` is a function that responds when the state of the checkbox changes.
- `label` sets the text that displays next to the checkbox.
- `value` is initially set to `preference.enabled`.
- `onChange` is a function that responds when the state of the checkbox changes.
`ExamplePreferenceInput` is defined with the `ExamplePreferenceProps` React props.
This is an object with the single `enabled` property.
@ -461,18 +466,18 @@ 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):
```typescript
import { Renderer } from '@k8slens/extensions';
import { HelpIcon, HelpPage } from "./page"
import React from 'react';
import { Renderer } from "@k8slens/extensions";
import { HelpIcon, HelpPage } from "./page";
import React from "react";
export default class HelpExtension extends Renderer.LensExtension {
globalPages = [
{
id: "help",
components: {
Page: () => <HelpPage extension={this}/>,
}
}
Page: () => <HelpPage extension={this} />,
},
},
];
statusBarItems = [
@ -486,7 +491,7 @@ export default class HelpExtension extends Renderer.LensExtension {
<HelpIcon />
My Status Bar Item
</div>
)
),
},
},
];
@ -495,14 +500,14 @@ export default class HelpExtension extends Renderer.LensExtension {
The properties of the `statusBarItems` array objects are defined as follows:
* `Item` specifies the `React.Component` that will be shown on the status bar.
By default, items are added starting from the right side of the status bar.
Due to limited space in the status bar, `Item` will typically specify only an icon or a short string of text.
The example above reuses the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus).
* `onClick` determines what the `statusBarItem` does when it is clicked.
In the example, `onClick` is set to a function that calls the `LensRendererExtension` `navigate()` method.
`navigate` takes the `id` of the associated global page as a parameter.
Thus, clicking the status bar item activates the associated global pages.
- `Item` specifies the `React.Component` that will be shown on the status bar.
By default, items are added starting from the right side of the status bar.
Due to limited space in the status bar, `Item` will typically specify only an icon or a short string of text.
The example above reuses the `HelpIcon` from the [`globalPageMenus` guide](#globalpagemenus).
- `onClick` determines what the `statusBarItem` does when it is clicked.
In the example, `onClick` is set to a function that calls the `LensRendererExtension` `navigate()` method.
`navigate` takes the `id` of the associated global page as a parameter.
Thus, clicking the status bar item activates the associated global pages.
### `kubeObjectMenuItems`
@ -518,9 +523,9 @@ They also appear on the title bar of the details page for specific resources:
The following example shows how to add a `kubeObjectMenuItems` for namespace resources with an associated action:
```typescript
import React from "react"
import React from "react";
import { Renderer } from "@k8slens/extensions";
import { NamespaceMenuItem } from "./src/namespace-menu-item"
import { NamespaceMenuItem } from "./src/namespace-menu-item";
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
type Namespace = Renderer.K8sApi.Namespace;
@ -531,23 +536,24 @@ export default class ExampleExtension extends Renderer.LensExtension {
kind: "Namespace",
apiVersions: ["v1"],
components: {
MenuItem: (props: KubeObjectMenuProps<Namespace>) => <NamespaceMenuItem {...props} />
}
}
MenuItem: (props: KubeObjectMenuProps<Namespace>) => (
<NamespaceMenuItem {...props} />
),
},
},
];
}
```
`kubeObjectMenuItems` is an array of objects matching the `KubeObjectMenuRegistration` interface.
The example above adds a menu item for namespaces in the cluster dashboard.
The properties of the `kubeObjectMenuItems` array objects are defined as follows:
* `kind` specifies the Kubernetes resource type the menu item will apply to.
* `apiVersion` specifies the Kubernetes API version number to use with the resource type.
* `components` defines the menu item's appearance and behavior.
* `MenuItem` provides a function that returns a `React.Component` given a set of menu item properties.
In this example a `NamespaceMenuItem` object is returned.
- `kind` specifies the Kubernetes resource type the menu item will apply to.
- `apiVersion` specifies the Kubernetes API version number to use with the resource type.
- `components` defines the menu item's appearance and behavior.
- `MenuItem` provides a function that returns a `React.Component` given a set of menu item properties.
In this example a `NamespaceMenuItem` object is returned.
`NamespaceMenuItem` is defined in `./src/namespace-menu-item.tsx`:
@ -556,11 +562,7 @@ import React from "react";
import { Renderer } from "@k8slens/extensions";
const {
Component: {
terminalStore,
MenuItem,
Icon,
},
Component: { terminalStore, MenuItem, Icon },
Navigation,
} = Renderer;
@ -587,12 +589,15 @@ export function NamespaceMenuItem(props: KubeObjectMenuProps<Namespace>) {
return (
<MenuItem onClick={getPods}>
<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>
</MenuItem>
);
}
```
`NamespaceMenuItem` returns a `Renderer.Component.MenuItem` which defines the menu item's appearance and its behavior when activated via the `onClick` property.
@ -629,9 +634,11 @@ export default class ExampleExtension extends Renderer.LensExtension {
apiVersions: ["v1"],
priority: 10,
components: {
Details: (props: KubeObjectDetailsProps<Namespace>) => <NamespaceDetailsItem {...props} />
}
}
Details: (props: KubeObjectDetailsProps<Namespace>) => (
<NamespaceDetailsItem {...props} />
),
},
},
];
}
```
@ -640,15 +647,15 @@ export default class ExampleExtension extends Renderer.LensExtension {
This example above adds a detail item for namespaces in the cluster dashboard.
The properties of the `kubeObjectDetailItems` array objects are defined as follows:
* `kind` specifies the Kubernetes resource type the detail item will apply to.
* `apiVersion` specifies the Kubernetes API version number to use with the resource type.
* `components` defines the detail item's appearance and behavior.
* `Details` provides a function that returns a `React.Component` given a set of detail item properties.
In this example a `NamespaceDetailsItem` object is returned.
- `kind` specifies the Kubernetes resource type the detail item will apply to.
- `apiVersion` specifies the Kubernetes API version number to use with the resource type.
- `components` defines the detail item's appearance and behavior.
- `Details` provides a function that returns a `React.Component` given a set of detail item properties.
In this example a `NamespaceDetailsItem` object is returned.
`NamespaceDetailsItem` is defined in `./src/namespace-details-item.tsx`:
``` typescript
```typescript
import { Renderer } from "@k8slens/extensions";
import { PodsDetailsList } from "./pods-details-list";
import React from "react";
@ -656,12 +663,8 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
const {
K8sApi: {
podsApi,
},
Component: {
DrawerTitle,
},
K8sApi: { podsApi },
Component: { DrawerTitle },
} = Renderer;
type KubeObjectMenuProps = Renderer.Component.KubeObjectMenuProps;
@ -669,7 +672,9 @@ type Namespace = Renderer.K8sApi.Namespace;
type Pod = Renderer.K8sApi.Pod;
@observer
export class NamespaceDetailsItem extends React.Component<KubeObjectDetailsProps<Namespace>> {
export class NamespaceDetailsItem extends React.Component<
KubeObjectDetailsProps<Namespace>
> {
@observable private pods: Pod[];
async componentDidMount() {
@ -681,10 +686,10 @@ export class NamespaceDetailsItem extends React.Component<KubeObjectDetailsProps
render() {
return (
<div>
<DrawerTitle title="Pods" />
<PodsDetailsList pods={this.pods}/>
<DrawerTitle>Pods</DrawerTitle>
<PodsDetailsList pods={this.pods} />
</div>
)
);
}
}
```
@ -709,17 +714,12 @@ Details are placed in drawers, and using `Renderer.Component.DrawerTitle` provid
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`:
``` typescript
```typescript
import React from "react";
import { Renderer } from "@k8slens/extensions";
const {
Component: {
TableHead,
TableRow,
TableCell,
Table,
},
Component: { TableHead, TableRow, TableCell, Table },
} = Renderer;
type Pod = Renderer.K8sApi.Pod;
@ -736,11 +736,11 @@ export class PodsDetailsList extends React.Component<PodsDetailsListProps> {
<TableCell className="podAge">{pods[index].getAge()}</TableCell>
<TableCell className="podStatus">{pods[index].getStatus()}</TableCell>
</TableRow>
)
);
};
render() {
const { pods } = this.props
const { pods } = this.props;
if (!pods?.length) {
return null;
@ -754,7 +754,7 @@ export class PodsDetailsList extends React.Component<PodsDetailsListProps> {
<TableCell className="podAge">Age</TableCell>
<TableCell className="podStatus">Status</TableCell>
</TableHead>
{ pods.map(this.getTableRow) }
{pods.map(this.getTableRow)}
</Table>
</div>
);

View File

@ -3,7 +3,7 @@
"productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens",
"version": "5.4.0",
"version": "5.5.0-alpha.0",
"main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors",
"license": "MIT",
@ -28,6 +28,8 @@
"build:mac": "yarn run compile && electron-builder --mac --dir",
"build:win": "yarn run compile && electron-builder --win --dir",
"integration": "jest --runInBand --detectOpenHandles --forceExit integration",
"test:unit": "jest --watch --testPathIgnorePatterns integration",
"test:integration": "func() { jest ${1:-xyz} --watch --runInBand --detectOpenHandles --forceExit --modulePaths=[\"<rootDir>/integration/\"]; }; func",
"dist": "yarn run compile && electron-builder --publish onTag",
"dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null",
"download:binaries": "yarn run ts-node build/download_binaries.ts",
@ -44,7 +46,7 @@
"postversion": "git push --set-upstream ${GIT_REMOTE:-origin} release/v$npm_package_version"
},
"config": {
"k8sProxyVersion": "0.1.5",
"k8sProxyVersion": "0.2.1",
"bundledKubectlVersion": "1.23.3",
"bundledHelmVersion": "3.7.2",
"sentryDsn": ""
@ -203,7 +205,7 @@
"@ogre-tools/injectable": "5.1.2",
"@ogre-tools/injectable-react": "5.1.2",
"@sentry/electron": "^2.5.4",
"@sentry/integrations": "^6.15.0",
"@sentry/integrations": "^6.19.2",
"@types/circular-dependency-plugin": "5.0.5",
"abort-controller": "^3.0.0",
"auto-bind": "^4.0.0",
@ -214,7 +216,7 @@
"conf": "^7.1.2",
"crypto-js": "^4.1.1",
"electron-devtools-installer": "^3.2.0",
"electron-updater": "^4.6.1",
"electron-updater": "^4.6.5",
"electron-window-state": "^5.0.3",
"filehound": "^1.17.5",
"fs-extra": "^9.0.1",
@ -223,7 +225,7 @@
"grapheme-splitter": "^1.0.4",
"handlebars": "^4.7.7",
"http-proxy": "^1.18.1",
"immer": "^9.0.6",
"immer": "^9.0.12",
"joi": "^17.6.0",
"js-yaml": "^4.1.0",
"jsdom": "^16.7.0",
@ -243,7 +245,7 @@
"monaco-editor-webpack-plugin": "^5.0.0",
"node-fetch": "lensapp/node-fetch#2.x",
"node-pty": "^0.10.1",
"npm": "^6.14.15",
"npm": "^6.14.16",
"p-limit": "^3.1.0",
"path-to-regexp": "^6.2.0",
"proper-lockfile": "^4.1.2",
@ -265,7 +267,7 @@
"tempy": "1.0.1",
"url-parse": "^1.5.10",
"uuid": "^8.3.2",
"win-ca": "^3.4.5",
"win-ca": "^3.5.0",
"winston": "^3.6.0",
"winston-console-format": "^1.0.8",
"winston-transport-browserconsole": "^1.0.5",
@ -277,8 +279,8 @@
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@sentry/types": "^6.18.2",
"@testing-library/jest-dom": "^5.16.2",
"@sentry/types": "^6.19.2",
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^13.5.0",
"@types/byline": "^4.2.33",
@ -293,18 +295,18 @@
"@types/gunzip-maybe": "^1.4.0",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/html-webpack-plugin": "^3.2.6",
"@types/http-proxy": "^1.17.7",
"@types/http-proxy": "^1.17.8",
"@types/jest": "^26.0.24",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^16.2.13",
"@types/jsdom": "^16.2.14",
"@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.180",
"@types/marked": "^4.0.3",
"@types/md5-file": "^4.0.2",
"@types/mini-css-extract-plugin": "^2.4.0",
"@types/mock-fs": "^4.13.1",
"@types/node": "14.17.33",
"@types/node-fetch": "^2.5.12",
"@types/node": "14.18.12",
"@types/node-fetch": "^2.6.1",
"@types/npm": "^2.0.32",
"@types/proper-lockfile": "^4.1.2",
"@types/randomcolor": "^0.5.6",
@ -320,7 +322,7 @@
"@types/request": "^2.48.7",
"@types/request-promise-native": "^1.0.18",
"@types/semver": "^7.3.9",
"@types/sharp": "^0.29.4",
"@types/sharp": "^0.30.0",
"@types/spdy": "^3.4.5",
"@types/tar": "^4.0.5",
"@types/tar-stream": "^2.2.2",
@ -348,7 +350,7 @@
"electron": "^14.2.7",
"electron-builder": "^22.14.13",
"electron-notarize": "^0.3.0",
"esbuild": "^0.13.15",
"esbuild": "^0.14.27",
"esbuild-loader": "^2.18.0",
"eslint": "^8.11.0",
"eslint-plugin-header": "^3.1.1",
@ -372,7 +374,7 @@
"node-gyp": "7.1.2",
"node-loader": "^2.0.0",
"nodemon": "^2.0.15",
"playwright": "^1.17.1",
"playwright": "^1.20.1",
"postcss": "^8.4.12",
"postcss-loader": "^6.2.1",
"randomcolor": "^0.6.2",
@ -384,27 +386,27 @@
"react-select-event": "^5.3.0",
"react-table": "^7.7.0",
"react-window": "^1.8.6",
"sass": "^1.45.1",
"sass": "^1.49.9",
"sass-loader": "^12.6.0",
"sharp": "^0.29.3",
"style-loader": "^3.3.1",
"tailwindcss": "^3.0.7",
"tailwindcss": "^3.0.23",
"tar-stream": "^2.2.0",
"ts-jest": "26.5.6",
"ts-loader": "^9.2.6",
"ts-loader": "^9.2.8",
"ts-node": "^10.7.0",
"type-fest": "^1.4.0",
"type-fest": "^2.12.0",
"typed-emitter": "^1.4.0",
"typedoc": "0.22.10",
"typedoc-plugin-markdown": "^3.11.12",
"typeface-roboto": "^1.1.13",
"typescript": "^4.5.2",
"typescript": "^4.5.5",
"typescript-plugin-css-modules": "^3.4.0",
"webpack": "^5.69.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4",
"webpack-node-externals": "^3.0.0",
"xterm": "^4.15.0",
"xterm": "^4.18.0",
"xterm-addon-fit": "^0.5.0"
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
class TestCatalogCategory extends CatalogCategory {
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
public readonly kind = "CatalogCategory";
public metadata = {
name: "Test Category",
icon: "",
};
public spec: CatalogCategorySpec = {
group: "entity.k8slens.dev",
versions: [],
names: {
kind: "Test",
},
};
}
describe("CatalogCategory", () => {
it("returns name", () => {
const category = new TestCatalogCategory();
expect(category.getName()).toEqual("Test Category");
});
});

View File

@ -24,8 +24,6 @@ import { createClusterInjectionToken } from "../cluster/create-cluster-injection
import directoryForUserDataInjectable
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import kubeAuthProxyCaInjectable from "../../main/kube-auth-proxy/kube-auth-proxy-ca.injectable";
import createKubeAuthProxyCertFilesInjectable from "../../main/kube-auth-proxy/create-kube-auth-proxy-cert-files.injectable";
console = new Console(stdout, stderr);
@ -89,8 +87,6 @@ describe("cluster-store", () => {
mainDi = dis.mainDi;
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
mainDi.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
mainDi.override(kubeAuthProxyCaInjectable, () => ({} as any));
await dis.runSetups();

View File

@ -173,6 +173,13 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
return `${this.spec.group}/${this.spec.names.kind}`;
}
/**
* Get the name of this category
*/
public getName(): string {
return this.metadata.name;
}
/**
* Add a filter for menu items of catalogAddMenu
* @param fn The function that should return a truthy value if that menu item should be displayed

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { JsonApi } from "./json-api";
import { apiPrefix, isDebugging, isDevelopment } from "../vars";
import { appEventBus } from "../app-event-bus/event-bus";
export let apiBase: JsonApi;
if (typeof window === "undefined") {
appEventBus.addListener((event) => {
if (event.name !== "lens-proxy" && event.action !== "listen") return;
const params = event.params as { port?: number };
if (!params.port) return;
apiBase = new JsonApi({
serverAddress: `http://127.0.0.1:${params.port}`,
apiBase: apiPrefix,
debug: isDevelopment || isDebugging,
}, {
headers: {
"Host": `localhost:${params.port}`,
},
});
});
} else {
apiBase = new JsonApi({
serverAddress: `http://127.0.0.1:${window.location.port}`,
apiBase: apiPrefix,
debug: isDevelopment || isDebugging,
}, {
headers: {
"Host": window.location.host,
},
});
}

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isClusterPageContext } from "../utils";
import { KubeJsonApi } from "./kube-json-api";
import { apiKubePrefix, isDevelopment } from "../vars";
export const apiKube = isClusterPageContext()
? new KubeJsonApi({
serverAddress: `http://127.0.0.1:${window.location.port}`,
apiBase: apiKubePrefix,
debug: isDevelopment,
}, {
headers: {
"Host": window.location.host,
},
})
: undefined;

View File

@ -141,8 +141,11 @@ export function isMetricsEmpty(metrics: Record<string, IMetrics>) {
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
}
export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> | void {
if (!metrics) return;
export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> | undefined {
if (!metrics) {
return undefined;
}
const itemMetrics = { ...metrics };
for (const metric in metrics) {

View File

@ -31,17 +31,25 @@ export interface IPvcMetrics<T = IMetrics> {
diskCapacity: T;
}
export interface PersistentVolumeClaim {
spec: {
accessModes: string[];
storageClassName: string;
selector: LabelSelector;
resources: {
requests: {
storage: string; // 8Gi
};
};
export interface PersistentVolumeClaimSpec {
accessModes: string[];
selector: LabelSelector;
resources: {
requests?: Record<string, string>;
limits?: Record<string, string>;
};
volumeName?: string;
storageClassName?: string;
volumeMode?: string;
dataSource?: {
apiGroup: string;
kind: string;
name: string;
};
}
export interface PersistentVolumeClaim {
spec: PersistentVolumeClaimSpec;
status: {
phase: string; // Pending
};

View File

@ -9,6 +9,10 @@ import { IMetrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { RequireExactlyOne } from "type-fest";
import type { KubeObjectMetadata, LocalObjectReference } from "../kube-object";
import type { SecretReference } from "./secret.api";
import type { PersistentVolumeClaimSpec } from "./persistent-volume-claims.api";
export class PodsApi extends KubeApi<Pod> {
getLogs = async (params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> => {
@ -112,12 +116,8 @@ export interface IPodContainer extends Partial<Record<PodContainerProbe, IContai
};
}[];
envFrom?: {
configMapRef?: {
name: string;
};
secretRef?: {
name: string;
};
configMapRef?: LocalObjectReference;
secretRef?: LocalObjectReference;
}[];
volumeMounts?: {
name: string;
@ -195,6 +195,444 @@ export interface IPodContainerStatus {
started?: boolean;
}
export interface AwsElasticBlockStoreSource {
volumeID: string;
fsType: string;
}
export interface AzureDiskSource {
/**
* The name of the VHD blob object OR the name of an Azure managed data disk if `kind` is `"Managed"`.
*/
diskName: string;
/**
* The URI of the vhd blob object OR the `resourceID` of an Azure managed data disk if `kind` is `"Managed"`.
*/
diskURI: string;
/**
* Kind of disk
* @default "Shared"
*/
kind?: "Shared" | "Dedicated" | "Managed";
/**
* Disk caching mode.
* @default "None"
*/
cachingMode?: "None" | "ReadOnly" | "ReadWrite";
/**
* The filesystem type to mount.
* @default "ext4"
*/
fsType?: string;
/**
* Whether the filesystem is used as readOnly.
* @default false
*/
readonly?: boolean;
}
export interface AzureFileSource {
/**
* The name of the secret that contains both Azure storage account name and key.
*/
secretName: string;
/**
* The share name to be used.
*/
shareName: string;
/**
* In case the secret is stored in a different namespace.
* @default "default"
*/
secretNamespace?: string;
/**
* Whether the filesystem is used as readOnly.
*/
readOnly: boolean;
}
export interface CephfsSource {
/**
* List of Ceph monitors
*/
monitors: string[];
/**
* Used as the mounted root, rather than the full Ceph tree.
* @default "/"
*/
path?: string;
/**
* The RADOS user name.
* @default "admin"
*/
user?: string;
/**
* The path to the keyring file.
* @default "/etc/ceph/user.secret"
*/
secretFile?: string;
/**
* Reference to Ceph authentication secrets. If provided, then the secret overrides `secretFile`
*/
secretRef?: SecretReference;
/**
* Whether the filesystem is used as readOnly.
*/
readOnly: boolean;
}
export interface CinderSource {
volumeID: string;
fsType: string;
/**
* @default false
*/
readOnly?: boolean;
secretRef?: SecretReference;
}
export interface ConfigMapSource {
name: string;
items: {
key: string;
path: string;
}[];
}
export interface DownwardApiSource {
items: {
path: string;
fieldRef: {
fieldPath: string;
};
}[];
}
export interface EphemeralSource {
volumeClaimTemplate: {
/**
* All the rest of the fields are ignored and rejected during validation
*/
metadata?: Pick<KubeObjectMetadata, "labels" | "annotations">;
spec: PersistentVolumeClaimSpec;
};
}
export interface EmptyDirSource {
medium?: string;
sizeLimit?: string;
}
export interface FiberChannelSource {
/**
* A list of World Wide Names
*/
targetWWNs: string[];
/**
* Logical Unit number
*/
lun: number;
/**
* The type of filesystem
* @default "ext4"
*/
fsType?: string;
readOnly: boolean;
}
export interface FlockerSource {
datasetName: string;
}
export interface FlexVolumeSource {
driver: string;
fsType?: string;
secretRef?: LocalObjectReference;
/**
* @default false
*/
readOnly?: boolean;
options?: Record<string, string>;
}
export interface GcePersistentDiskSource {
pdName: string;
fsType: string;
}
export interface GitRepoSource {
repository: string;
revision: string;
}
export interface GlusterFsSource {
/**
* The name of the Endpoints object that represents a Gluster cluster configuration.
*/
endpoints: string;
/**
* The Glusterfs volume name.
*/
path: string;
/**
* The boolean that sets the mountpoint readOnly or readWrite.
*/
readOnly: boolean;
}
export interface HostPathSource {
path: string;
/**
* Determines the sorts of checks that will be done
* @default ""
*/
type?: "" | "DirectoryOrCreate" | "Directory" | "FileOrCreate" | "File" | "Socket" | "CharDevice" | "BlockDevice";
}
export interface IScsiSource {
targetPortal: string;
iqn: string;
lun: number;
fsType: string;
readOnly: boolean;
chapAuthDiscovery?: boolean;
chapAuthSession?: boolean;
secretRef?: SecretReference;
}
export interface LocalSource {
path: string;
}
export interface NetworkFsSource {
server: string;
path: string;
readOnly?: boolean;
}
export interface PersistentVolumeClaimSource {
claimName: string;
}
export interface PhotonPersistentDiskSource {
pdID: string;
/**
* @default "ext4"
*/
fsType?: string;
}
export interface PortworxVolumeSource {
volumeID: string;
fsType?: string;
readOnly?: boolean;
}
export interface ProjectedSource {
sources: {
secret?: {
name: string;
items?: {
key: string;
path: string;
mode?: number;
}[];
};
downwardAPI?: {
items?: {
path: string;
fieldRef?: {
fieldPath: string;
apiVersion?: string;
};
resourceFieldRef?: {
resource: string;
containerName?: string;
};
mode?: number;
}[];
};
configMap?: {
name: string;
items?: {
key: string;
path: string;
mode?: number;
}[];
optional?: boolean;
};
serviceAccountToken?: {
audience?: string;
expirationSeconds?: number;
path: string;
};
}[];
defaultMode: number;
}
export interface QuobyteSource {
registry: string;
volume: string;
/**
* @default false
*/
readOnly?: boolean;
/**
* @default "serivceaccount"
*/
user?: string;
group?: string;
tenant?: string;
}
export interface RadosBlockDeviceSource {
monitors: string[];
image: string;
/**
* @default "ext4"
*/
fsType?: string;
/**
* @default "rbd"
*/
pool?: string;
/**
* @default "admin"
*/
user?: string;
/**
* @default "/etc/ceph/keyring"
*/
keyring?: string;
secretRef?: SecretReference;
/**
* @default false
*/
readOnly?: boolean;
}
export interface ScaleIoSource {
gateway: string;
system: string;
secretRef?: LocalObjectReference;
/**
* @default false
*/
sslEnabled?: boolean;
protectionDomain?: string;
storagePool?: string;
/**
* @default "ThinProvisioned"
*/
storageMode?: "ThickProvisioned" | "ThinProvisioned";
volumeName: string;
/**
* @default "xfs"
*/
fsType?: string;
/**
* @default false
*/
readOnly?: boolean;
}
export interface SecretSource {
secretName: string;
items?: {
key: string;
path: string;
mode?: number;
}[];
defaultMode?: number;
optional?: boolean;
}
export interface StorageOsSource {
volumeName: string;
/**
* @default Pod.metadata.namespace
*/
volumeNamespace?: string;
/**
* @default "ext4"
*/
fsType?: string;
/**
* @default false
*/
readOnly?: boolean;
secretRef?: LocalObjectReference;
}
export interface VsphereVolumeSource {
volumePath: string;
/**
* @default "ext4"
*/
fsType?: string;
storagePolicyName?: string;
storagePolicyID?: string;
}
export interface ContainerStorageInterfaceSource {
driver: string;
/**
* @default false
*/
readOnly?: boolean;
/**
* @default "ext4"
*/
fsType?: string;
volumeAttributes?: Record<string, string>;
controllerPublishSecretRef?: SecretReference;
nodeStageSecretRef?: SecretReference;
nodePublishSecretRef?: SecretReference;
controllerExpandSecretRef?: SecretReference;
}
export interface PodVolumeVariants {
awsElasticBlockStore: AwsElasticBlockStoreSource;
azureDisk: AzureDiskSource;
azureFile: AzureFileSource;
cephfs: CephfsSource;
cinder: CinderSource;
configMap: ConfigMapSource;
csi: ContainerStorageInterfaceSource;
downwardAPI: DownwardApiSource;
emptyDir: EmptyDirSource;
ephemeral: EphemeralSource;
fc: FiberChannelSource;
flexVolume: FlexVolumeSource;
flocker: FlockerSource;
gcePersistentDisk: GcePersistentDiskSource;
gitRepo: GitRepoSource;
glusterfs: GlusterFsSource;
hostPath: HostPathSource;
iscsi: IScsiSource;
local: LocalSource;
nfs: NetworkFsSource;
persistentVolumeClaim: PersistentVolumeClaimSource;
photonPersistentDisk: PhotonPersistentDiskSource;
portworxVolume: PortworxVolumeSource;
projected: ProjectedSource;
quobyte: QuobyteSource;
rbd: RadosBlockDeviceSource;
scaleIO: ScaleIoSource;
secret: SecretSource;
storageos: StorageOsSource;
vsphereVolume: VsphereVolumeSource;
}
/**
* The valid kinds of volume
*/
export type PodVolumeKind = keyof PodVolumeVariants;
export type PodVolume = RequireExactlyOne<PodVolumeVariants> & {
name: string;
};
export class Pod extends WorkloadKubeObject {
static kind = "Pod";
static namespaced = true;
@ -206,23 +644,7 @@ export class Pod extends WorkloadKubeObject {
}
declare spec?: {
volumes?: {
name: string;
persistentVolumeClaim: {
claimName: string;
};
emptyDir: {
medium?: string;
sizeLimit?: string;
};
configMap: {
name: string;
};
secret: {
secretName: string;
defaultMode: number;
};
}[];
volumes?: PodVolume[];
initContainers: IPodContainer[];
containers: IPodContainer[];
restartPolicy?: string;
@ -239,9 +661,7 @@ export class Pod extends WorkloadKubeObject {
[selector: string]: string;
};
securityContext?: {};
imagePullSecrets?: {
name: string;
}[];
imagePullSecrets?: LocalObjectReference[];
hostNetwork?: boolean;
hostPID?: boolean;
hostIPC?: boolean;

View File

@ -25,6 +25,11 @@ export interface ISecretRef {
name: string;
}
export interface SecretReference {
name: string;
namespace?: string;
}
export interface SecretData extends KubeJsonApiData {
type: SecretType;
data?: Record<string, string>;

View File

@ -3,58 +3,5 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { JsonApi } from "./json-api";
import { KubeJsonApi } from "./kube-json-api";
import { apiKubePrefix, apiPrefix, isDebugging, isDevelopment } from "../../common/vars";
import { isClusterPageContext } from "../utils/cluster-id-url-parsing";
import { appEventBus } from "../app-event-bus/event-bus";
let apiBase: JsonApi;
let apiKube: KubeJsonApi;
if (typeof window === "undefined") {
appEventBus.addListener((event) => {
if (event.name !== "lens-proxy" && event.action !== "listen") return;
const params = event.params as { port?: number };
if (!params.port) return;
apiBase = new JsonApi({
serverAddress: `http://127.0.0.1:${params.port}`,
apiBase: apiPrefix,
debug: isDevelopment || isDebugging,
}, {
headers: {
"Host": `localhost:${params.port}`,
},
});
});
} else {
apiBase = new JsonApi({
serverAddress: `http://127.0.0.1:${window.location.port}`,
apiBase: apiPrefix,
debug: isDevelopment || isDebugging,
}, {
headers: {
"Host": window.location.host,
},
});
}
if (isClusterPageContext()) {
apiKube = new KubeJsonApi({
serverAddress: `http://127.0.0.1:${window.location.port}`,
apiBase: apiKubePrefix,
debug: isDevelopment,
}, {
headers: {
"Host": window.location.host,
},
});
}
export {
apiBase,
apiKube,
};
export { apiBase } from "./api-base";
export { apiKube } from "./api-kube";

View File

@ -5,8 +5,8 @@
import { JsonApi, JsonApiData, JsonApiError } from "./json-api";
import type { Response } from "node-fetch";
import { LensProxy } from "../../main/lens-proxy";
import { apiKubePrefix, isDebugging } from "../vars";
import { apiBase } from "./api-base";
export interface KubeJsonApiListMetadata {
resourceVersion: string;
@ -57,15 +57,15 @@ export interface KubeJsonApiError extends JsonApiError {
export class KubeJsonApi extends JsonApi<KubeJsonApiData> {
static forCluster(clusterId: string): KubeJsonApi {
const port = LensProxy.getInstance().port;
const url = new URL(apiBase.config.serverAddress);
return new this({
serverAddress: `http://127.0.0.1:${port}`,
serverAddress: apiBase.config.serverAddress,
apiBase: apiKubePrefix,
debug: isDebugging,
}, {
headers: {
"Host": `${clusterId}.localhost:${port}`,
"Host": `${clusterId}.localhost:${url.port}`,
},
});
}

View File

@ -21,6 +21,13 @@ export type KubeObjectConstructor<K extends KubeObject> = (new (data: KubeJsonAp
apiBase?: string;
};
/**
* A reference to an object in the same namespace
*/
export interface LocalObjectReference {
name: string;
}
export interface KubeObjectMetadata {
uid: string;
name: string;

View File

@ -65,6 +65,9 @@ if (ipcMain) {
}
export default winston.createLogger({
format: format.simple(),
format: format.combine(
format.splat(),
format.simple(),
),
transports,
});

View File

@ -72,11 +72,34 @@ const resourcesDir = lazyInitialized(() => (
: path.join(process.cwd(), "binaries", "client", normalizedPlatform)
));
/**
* @deprecated for being explicit side effect.
*/
export const baseBinariesDir = lazyInitialized(() => path.join(resourcesDir.get(), normalizedArch));
/**
* @deprecated for being explicit side effect.
*/
export const kubeAuthProxyBinaryName = getBinaryName("lens-k8s-proxy");
/**
* @deprecated for being explicit side effect.
*/
export const helmBinaryName = getBinaryName("helm");
/**
* @deprecated for being explicit side effect.
*/
export const helmBinaryPath = lazyInitialized(() => path.join(baseBinariesDir.get(), helmBinaryName));
/**
* @deprecated for being explicit side effect.
*/
export const kubectlBinaryName = getBinaryName("kubectl");
/**
* @deprecated for being explicit side effect.
*/
export const kubectlBinaryPath = lazyInitialized(() => path.join(baseBinariesDir.get(), kubectlBinaryName));
// Webpack build paths

View File

@ -5,6 +5,9 @@
import { asLegacyGlobalForExtensionApi } from "./as-legacy-global-object-for-extension-api";
import type { Injectable } from "@ogre-tools/injectable";
/**
* @deprecated use asLegacyGlobalForExtensionApi instead, and use proper implementations instead of "modifications".
*/
export const asLegacyGlobalObjectForExtensionApiWithModifications = <
InjectableInstance extends InjectionTokenInstance,
InjectionTokenInstance,

View File

@ -20,6 +20,7 @@ jest.mock("winston", () => ({
label: jest.fn(),
timestamp: jest.fn(),
printf: jest.fn(),
splat: jest.fn(),
},
createLogger: jest.fn().mockReturnValue(logger),
transports: {

View File

@ -10,7 +10,6 @@ import mockFs from "mock-fs";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import kubeAuthProxyCaInjectable from "../kube-auth-proxy/kube-auth-proxy-ca.injectable";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
jest.mock("electron", () => ({
@ -83,7 +82,6 @@ describe("ContextHandler", () => {
});
di.override(createKubeAuthProxyInjectable, () => ({} as any));
di.override(kubeAuthProxyCaInjectable, () => ({} as any));
await di.runSetups();

View File

@ -15,6 +15,7 @@ jest.mock("winston", () => ({
printf: jest.fn(),
padLevels: jest.fn(),
ms: jest.fn(),
splat: jest.fn(),
},
createLogger: jest.fn().mockReturnValue({
silly: jest.fn(),
@ -51,8 +52,6 @@ import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-p
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import path from "path";
import spawnInjectable from "../child-process/spawn.injectable";
import kubeAuthProxyCaInjectable from "../kube-auth-proxy/kube-auth-proxy-ca.injectable";
import createKubeAuthProxyCertFilesInjectable from "../kube-auth-proxy/create-kube-auth-proxy-cert-files.injectable";
console = new Console(stdout, stderr);
@ -96,8 +95,6 @@ describe("kube auth proxy tests", () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(spawnInjectable, () => mockSpawn);
di.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
di.override(kubeAuthProxyCaInjectable, () => ({} as any));
mockFs(mockMinikubeConfig);

View File

@ -21,6 +21,7 @@ jest.mock("winston", () => ({
padLevels: jest.fn(),
ms: jest.fn(),
printf: jest.fn(),
splat: jest.fn(),
},
createLogger: jest.fn().mockReturnValue(logger),
transports: {

View File

@ -5,11 +5,11 @@
import { autoUpdater, UpdateInfo } from "electron-updater";
import logger from "./logger";
import { isLinux, isMac, isPublishConfigured, isTestEnv } from "../common/vars";
import { isPublishConfigured, isTestEnv } from "../common/vars";
import { delay } from "../common/utils";
import { areArgsUpdateAvailableToBackchannel, AutoUpdateChecking, AutoUpdateLogPrefix, AutoUpdateNoUpdateAvailable, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
import { once } from "lodash";
import { ipcMain, autoUpdater as electronAutoUpdater } from "electron";
import { ipcMain } from "electron";
import { nextUpdateChannel } from "./utils/update-channel";
import { UserStore } from "../common/user-store";
@ -23,25 +23,7 @@ function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: Upd
if (arg.doUpdate) {
if (arg.now) {
logger.info(`${AutoUpdateLogPrefix}: User chose to update now`);
setImmediate(() => {
if (isMac) {
/**
* This is a necessary workaround until electron-updater is fixed.
* The problem is that it downloads it but then never tries to
* download it from itself via electron.
*/
electronAutoUpdater.checkForUpdates();
} else if (isLinux) {
/**
* This is a necessary workaround until electron-updater is fixed.
* The problem is that because linux updating is not implemented at
* all via electron. Electron's autoUpdater.quitAndInstall() is never
* called.
*/
electronAutoUpdater.emit("before-quit-for-update");
}
autoUpdater.quitAndInstall(true, true);
});
autoUpdater.quitAndInstall(true, true);
} else {
logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`);
autoUpdater.autoInstallOnAppQuit = true;
@ -52,10 +34,10 @@ function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: Upd
}
autoUpdater.logger = {
info: message => logger.info(`[AUTO-UPDATE]: electron-updater:`, message),
warn: message => logger.warn(`[AUTO-UPDATE]: electron-updater:`, message),
error: message => logger.error(`[AUTO-UPDATE]: electron-updater:`, message),
debug: message => logger.debug(`[AUTO-UPDATE]: electron-updater:`, message),
info: message => logger.info(`[AUTO-UPDATE]: electron-updater: %s`, message),
warn: message => logger.warn(`[AUTO-UPDATE]: electron-updater: %s`, message),
error: message => logger.error(`[AUTO-UPDATE]: electron-updater: %s`, message),
debug: message => logger.debug(`[AUTO-UPDATE]: electron-updater: %s`, message),
};
/**

View File

@ -14,11 +14,8 @@ import { ClusterManager } from "../../cluster-manager";
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import directoryForKubeConfigsInjectable
from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import kubeAuthProxyCaInjectable from "../../kube-auth-proxy/kube-auth-proxy-ca.injectable";
import createKubeAuthProxyCertFilesInjectable from "../../kube-auth-proxy/create-kube-auth-proxy-cert-files.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
jest.mock("electron", () => ({
app: {
@ -42,9 +39,6 @@ describe("kubeconfig-sync.source tests", () => {
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(kubeAuthProxyCaInjectable, () => Promise.resolve(Buffer.from("ca")));
di.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
mockFs();
await di.runSetups();
@ -62,6 +56,7 @@ describe("kubeconfig-sync.source tests", () => {
afterEach(() => {
mockFs.restore();
ClusterManager.resetInstance();
ClusterStore.resetInstance();
});
describe("configsToModels", () => {

View File

@ -28,7 +28,7 @@ interface PrometheusServicePreferences {
interface Dependencies {
createKubeAuthProxy: CreateKubeAuthProxy;
authProxyCa: Promise<Buffer>;
authProxyCa: string;
}
export class ContextHandler {
@ -123,7 +123,7 @@ export class ContextHandler {
return `https://127.0.0.1:${this.kubeAuthProxy.port}${this.kubeAuthProxy.apiPrefix}${path}`;
}
async resolveAuthProxyCa() {
resolveAuthProxyCa() {
return this.dependencies.authProxyCa;
}
@ -140,7 +140,7 @@ export class ContextHandler {
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
await this.ensureServer();
const caFileContents = await this.resolveAuthProxyCa();
const ca = this.dependencies.authProxyCa;
const clusterPath = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
const apiPrefix = `${this.kubeAuthProxy.apiPrefix}${clusterPath}`;
@ -150,7 +150,7 @@ export class ContextHandler {
host: "127.0.0.1",
port: this.kubeAuthProxy.port,
path: apiPrefix,
ca: caFileContents.toString(),
ca,
},
changeOrigin: true,
timeout,

View File

@ -3,23 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import selfsigned from "selfsigned";
import type { Cluster } from "../../common/cluster/cluster";
import { ContextHandler } from "./context-handler";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import kubeAuthProxyCaInjectable from "../kube-auth-proxy/kube-auth-proxy-ca.injectable";
import { getKubeAuthProxyCertificate } from "../kube-auth-proxy/get-kube-auth-proxy-certificate";
import URLParse from "url-parse";
const createContextHandlerInjectable = getInjectable({
id: "create-context-handler",
instantiate: (di) => {
const authProxyCa = di.inject(kubeAuthProxyCaInjectable);
return (cluster: Cluster) => {
const clusterUrl = new URLParse(cluster.apiUrl);
const dependencies = {
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
authProxyCa,
const dependencies = {
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert,
};
return new ContextHandler(dependencies, cluster);
};
return (cluster: Cluster) => new ContextHandler(dependencies, cluster);
},
});

View File

@ -1,26 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import * as selfsigned from "selfsigned";
import { createKubeAuthProxyCertFiles } from "./create-kube-auth-proxy-cert-files";
import writeFileInjectable from "../../common/fs/write-file.injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import path from "path";
const createKubeAuthProxyCertFilesInjectable = getInjectable({
id: "create-kube-auth-proxy-cert-files",
instantiate: async (di) => {
const userData = di.inject(directoryForUserDataInjectable);
const certPath = path.join(userData, "kube-auth-proxy");
return createKubeAuthProxyCertFiles(certPath, {
generate: selfsigned.generate,
writeFile: di.inject(writeFileInjectable),
});
},
});
export default createKubeAuthProxyCertFilesInjectable;

View File

@ -1,43 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
import type * as selfsigned from "selfsigned";
type SelfSignedGenerate = typeof selfsigned.generate;
interface CreateKubeAuthProxyCertificateFilesDependencies {
generate: SelfSignedGenerate;
writeFile: (path: string, content: string | Buffer) => Promise<void>;
}
function getKubeAuthProxyCertificate(generate: SelfSignedGenerate): selfsigned.SelfSignedCert {
const opts = [
{ name: "commonName", value: "Lens Certificate Authority" },
{ name: "organizationName", value: "Lens" },
];
return generate(opts, {
keySize: 2048,
algorithm: "sha256",
days: 365,
extensions: [
{ name: "basicConstraints", cA: true },
{ name: "subjectAltName", altNames: [
{ type: 2, value: "localhost" },
{ type: 7, ip: "127.0.0.1" },
] },
],
});
}
export async function createKubeAuthProxyCertFiles(dir: string, dependencies: CreateKubeAuthProxyCertificateFilesDependencies): Promise<string> {
const cert = getKubeAuthProxyCertificate(dependencies.generate);
await dependencies.writeFile(path.join(dir, "proxy.key"), cert.private);
await dependencies.writeFile(path.join(dir, "proxy.crt"), cert.cert);
return dir;
}

View File

@ -6,10 +6,11 @@ import { getInjectable } from "@ogre-tools/injectable";
import { KubeAuthProxy, KubeAuthProxyDependencies } from "./kube-auth-proxy";
import type { Cluster } from "../../common/cluster/cluster";
import path from "path";
import selfsigned from "selfsigned";
import { getBinaryName } from "../../common/vars";
import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
import spawnInjectable from "../child-process/spawn.injectable";
import createKubeAuthProxyCertFilesInjectable from "./create-kube-auth-proxy-cert-files.injectable";
import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate";
export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
@ -18,15 +19,17 @@ const createKubeAuthProxyInjectable = getInjectable({
instantiate: (di): CreateKubeAuthProxy => {
const binaryName = getBinaryName("lens-k8s-proxy");
const dependencies: KubeAuthProxyDependencies = {
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
proxyCertPath: di.inject(createKubeAuthProxyCertFilesInjectable),
spawn: di.inject(spawnInjectable),
};
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => (
new KubeAuthProxy(dependencies, cluster, environmentVariables)
);
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => {
const clusterUrl = new URL(cluster.apiUrl);
const dependencies: KubeAuthProxyDependencies = {
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate),
spawn: di.inject(spawnInjectable),
};
return new KubeAuthProxy(dependencies, cluster, environmentVariables);
};
},
});

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type * as selfsigned from "selfsigned";
type SelfSignedGenerate = typeof selfsigned.generate;
const certCache: Map<string, selfsigned.SelfSignedCert> = new Map();
export function getKubeAuthProxyCertificate(hostname: string, generate: SelfSignedGenerate, useCache = true): selfsigned.SelfSignedCert {
if (useCache && certCache.has(hostname)) {
return certCache.get(hostname);
}
const opts = [
{ name: "commonName", value: "Lens Certificate Authority" },
{ name: "organizationName", value: "Lens" },
];
const cert = generate(opts, {
keySize: 2048,
algorithm: "sha256",
days: 365,
extensions: [
{ name: "basicConstraints", cA: true },
{ name: "subjectAltName", altNames: [
{ type: 2, value: hostname },
{ type: 2, value: "localhost" },
{ type: 7, ip: "127.0.0.1" },
] },
],
});
certCache.set(hostname, cert);
return cert;
}

View File

@ -1,22 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import path from "path";
import readFileInjectable from "../../common/fs/read-file.injectable";
import createKubeAuthProxyCertFilesInjectable from "./create-kube-auth-proxy-cert-files.injectable";
const kubeAuthProxyCaInjectable = getInjectable({
id: "kube-auth-proxy-ca",
instantiate: async (di) => {
const certPath = await di.inject(createKubeAuthProxyCertFilesInjectable);
const readFile = di.inject(readFileInjectable);
return readFile(path.join(certPath, "proxy.crt"));
},
});
export default kubeAuthProxyCaInjectable;

View File

@ -10,12 +10,13 @@ import type { Cluster } from "../../common/cluster/cluster";
import logger from "../logger";
import { getPortFrom } from "../utils/get-port";
import { makeObservable, observable, when } from "mobx";
import type { SelfSignedCert } from "selfsigned";
const startingServeRegex = /starting to serve on (?<address>.+)/i;
export interface KubeAuthProxyDependencies {
proxyBinPath: string;
proxyCertPath: Promise<string>;
proxyCert: SelfSignedCert;
spawn: typeof spawn;
}
@ -44,7 +45,7 @@ export class KubeAuthProxy {
}
const proxyBin = this.dependencies.proxyBinPath;
const certPath = await this.dependencies.proxyCertPath;
const cert = this.dependencies.proxyCert;
this.proxyProcess = this.dependencies.spawn(proxyBin, [], {
env: {
@ -52,7 +53,8 @@ export class KubeAuthProxy {
KUBECONFIG: this.cluster.kubeConfigPath,
KUBECONFIG_CONTEXT: this.cluster.contextName,
API_PREFIX: this.apiPrefix,
CERT_PATH: certPath,
PROXY_KEY: cert.private,
PROXY_CERT: cert.cert,
},
});
this.proxyProcess.on("error", (error) => {

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type net from "net";
import net from "net";
import type http from "http";
import spdy from "spdy";
import type httpProxy from "http-proxy";
@ -79,19 +79,18 @@ export class LensProxy extends Singleton {
this.proxyServer
.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
const isInternal = req.url.startsWith(`${apiPrefix}?`);
const cluster = getClusterForRequest(req);
if (!cluster) {
logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
socket.destroy();
} else {
const isInternal = req.url.startsWith(`${apiPrefix}?`);
const reqHandler = isInternal ? shellApiRequest : kubeApiUpgradeRequest;
return socket.destroy();
(async () => reqHandler({ req, socket, head, cluster }))()
.catch(error => logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error));
}
const reqHandler = isInternal ? shellApiRequest : kubeApiUpgradeRequest;
(async () => reqHandler({ req, socket, head, cluster }))()
.catch(error => logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error));
});
}
@ -189,7 +188,7 @@ export class LensProxy extends Singleton {
});
proxy.on("error", (error, req, res, target) => {
if (this.closed) {
if (this.closed || res instanceof net.Socket) {
return;
}

View File

@ -25,6 +25,8 @@ import { UserStore } from "../../../common/user-store";
import mockFs from "mock-fs";
import directoryForUserDataInjectable
from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { AppEvent } from "../../../common/app-event-bus/event-bus";
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
mockWindow();
jest.mock("electron", () => ({
@ -102,6 +104,7 @@ describe("<Catalog />", () => {
let di: DiContainer;
let catalogEntityStore: CatalogEntityStore;
let catalogEntityRegistry: CatalogEntityRegistry;
let emitEvent: (event: AppEvent) => void;
let render: DiRender;
beforeEach(async () => {
@ -125,6 +128,12 @@ describe("<Catalog />", () => {
di.override(catalogEntityRegistryInjectable, () => catalogEntityRegistry);
emitEvent = jest.fn();
di.override(appEventBusInjectable, () => ({
emit: emitEvent,
}));
catalogEntityStore = di.inject(catalogEntityStoreInjectable);
});
@ -320,4 +329,39 @@ describe("<Catalog />", () => {
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
});
it("emits catalog open AppEvent", () => {
render(
<Catalog
history={history}
location={mockLocation}
match={mockMatch}
/>,
);
expect(emitEvent).toHaveBeenCalledWith( {
action: "open",
name: "catalog",
});
});
it("emits catalog change AppEvent when changing the category", () => {
render(
<Catalog
history={history}
location={mockLocation}
match={mockMatch}
/>,
);
userEvent.click(screen.getByText("Web Links"));
expect(emitEvent).toHaveBeenLastCalledWith({
action: "change-category",
name: "catalog",
params: {
category: "Web Links",
},
});
});
});

View File

@ -33,6 +33,8 @@ import getCategoryColumnsInjectable from "./get-category-columns.injectable";
import type { RegisteredCustomCategoryViewDecl } from "./custom-views.injectable";
import customCategoryViewsInjectable from "./custom-views.injectable";
import type { CustomCategoryViewComponents } from "./custom-views";
import type { AppEvent } from "../../../common/app-event-bus/event-bus";
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
export interface CatalogProps extends RouteComponentProps<CatalogViewRouteParam> {}
@ -41,6 +43,7 @@ interface Dependencies {
catalogEntityStore: CatalogEntityStore;
getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
customCategoryViews: IComputedValue<Map<string, Map<string, RegisteredCustomCategoryViewDecl>>>;
emitEvent: (event: AppEvent) => void;
}
@observer
@ -101,6 +104,11 @@ class NonInjectedCatalog extends React.Component<CatalogProps & Dependencies> {
});
}
}));
this.props.emitEvent({
name: "catalog",
action: "open",
});
}
addToHotbar(entity: CatalogEntity): void {
@ -143,6 +151,14 @@ class NonInjectedCatalog extends React.Component<CatalogProps & Dependencies> {
onTabChange = action((tabId: string | null) => {
const activeCategory = this.categories.find(category => category.getId() === tabId);
this.props.emitEvent({
name: "catalog",
action: "change-category",
params: {
category: activeCategory ? activeCategory.getName() : "Browse",
},
});
if (activeCategory) {
navigate(catalogURL({ params: { group: activeCategory.spec.group, kind: activeCategory.spec.names.kind }}));
} else {
@ -280,6 +296,7 @@ export const Catalog = withInjectables<Dependencies, CatalogProps>( NonInjectedC
catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable),
getCategoryColumns: di.inject(getCategoryColumnsInjectable),
customCategoryViews: di.inject(customCategoryViewsInjectable),
emitEvent: di.inject(appEventBusInjectable).emit,
...props,
}),
});

View File

@ -134,7 +134,7 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
})}
</DrawerItem>
<DrawerTitle title="Metrics"/>
<DrawerTitle>Metrics</DrawerTitle>
<div className="metrics">
{this.renderMetrics()}
</div>

View File

@ -85,7 +85,7 @@ export class ConfigMapDetails extends React.Component<ConfigMapDetailsProps> {
{
data.length > 0 && (
<>
<DrawerTitle title="Data"/>
<DrawerTitle>Data</DrawerTitle>
{
data.map(([name, value]) => (
<div key={name} className="data">

View File

@ -82,7 +82,7 @@ export class ResourceQuotaDetails extends React.Component<ResourceQuotaDetailsPr
{quota.getScopeSelector().length > 0 && (
<>
<DrawerTitle title="Scope Selector"/>
<DrawerTitle>Scope Selector</DrawerTitle>
<Table className="paths">
<TableHead>
<TableCell>Operator</TableCell>

View File

@ -116,7 +116,7 @@ export class SecretDetails extends React.Component<SecretDetailsProps> {
return (
<>
<DrawerTitle title="Data" />
<DrawerTitle>Data</DrawerTitle>
{secrets.map(this.renderSecret)}
<Button
primary

View File

@ -92,7 +92,7 @@ export class CRDDetails extends React.Component<CRDDetailsProps> {
})
}
</DrawerItem>
<DrawerTitle title="Names"/>
<DrawerTitle>Names</DrawerTitle>
<Table selectable className="names box grow">
<TableHead>
<TableCell>plural</TableCell>
@ -109,7 +109,7 @@ export class CRDDetails extends React.Component<CRDDetailsProps> {
</Table>
{printerColumns.length > 0 &&
<>
<DrawerTitle title="Additional Printer Columns"/>
<DrawerTitle>Additional Printer Columns</DrawerTitle>
<Table selectable className="printer-columns box grow">
<TableHead>
<TableCell className="name">Name</TableCell>
@ -136,7 +136,7 @@ export class CRDDetails extends React.Component<CRDDetailsProps> {
}
{validation &&
<>
<DrawerTitle title="Validation"/>
<DrawerTitle>Validation</DrawerTitle>
<MonacoEditor
readOnly
value={validation}

View File

@ -71,7 +71,7 @@ export class EventDetails extends React.Component<EventDetailsProps> {
<span className={kebabCase(type)}>{type}</span>
</DrawerItem>
<DrawerTitle title="Involved object"/>
<DrawerTitle>Involved object</DrawerTitle>
<Table>
<TableHead>
<TableCell>Name</TableCell>

View File

@ -113,7 +113,7 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
return (
<div className="values">
<DrawerTitle title="Values" />
<DrawerTitle>Values</DrawerTitle>
<div className="flex column gaps">
<Checkbox
label="User-supplied values only"
@ -238,9 +238,9 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
/>
</DrawerItem>
{this.renderValues()}
<DrawerTitle title="Notes"/>
<DrawerTitle>Notes</DrawerTitle>
{this.renderNotes()}
<DrawerTitle title="Resources"/>
<DrawerTitle>Resources</DrawerTitle>
{resources && this.renderResources(resources)}
</div>
);

View File

@ -35,7 +35,7 @@ export class EndpointDetails extends React.Component<EndpointDetailsProps> {
return (
<div className="EndpointDetails">
<KubeObjectMeta object={endpoint}/>
<DrawerTitle title="Subsets"/>
<DrawerTitle>Subsets</DrawerTitle>
{
endpoint.getEndpointSubsets().map((subset) => (
<EndpointSubsetList key={subset.toString()} subset={subset} endpoint={endpoint} />

View File

@ -160,10 +160,10 @@ export class IngressDetails extends React.Component<IngressDetailsProps> {
{serviceName}:{servicePort}
</DrawerItem>
}
<DrawerTitle title="Rules"/>
<DrawerTitle>Rules</DrawerTitle>
{this.renderPaths(ingress)}
<DrawerTitle title="Load-Balancer Ingress Points"/>
<DrawerTitle>Load-Balancer Ingress Points</DrawerTitle>
{this.renderIngressPoints(ingressPoints)}
</div>
);

View File

@ -170,7 +170,7 @@ export class NetworkPolicyDetails extends React.Component<NetworkPolicyDetailsPr
{ingress && (
<>
<DrawerTitle title="Ingress"/>
<DrawerTitle>Ingress</DrawerTitle>
{ingress.map((ingress, i) => (
<div key={i} data-testid={`ingress-${i}`}>
{this.renderNetworkPolicyPorts(ingress.ports)}
@ -182,7 +182,7 @@ export class NetworkPolicyDetails extends React.Component<NetworkPolicyDetailsPr
{egress && (
<>
<DrawerTitle title="Egress"/>
<DrawerTitle>Egress</DrawerTitle>
{egress.map((egress, i) => (
<div key={i} data-testid={`egress-${i}`}>
{this.renderNetworkPolicyPorts(egress.ports)}

View File

@ -86,7 +86,7 @@ class NonInjectedServiceDetails extends React.Component<ServiceDetailsProps & De
{spec.sessionAffinity}
</DrawerItem>
<DrawerTitle title="Connection"/>
<DrawerTitle>Connection</DrawerTitle>
<DrawerItem name="Cluster IP">
{spec.clusterIP}
@ -129,7 +129,7 @@ class NonInjectedServiceDetails extends React.Component<ServiceDetailsProps & De
{spec.loadBalancerIP}
</DrawerItem>
)}
<DrawerTitle title="Endpoint"/>
<DrawerTitle>Endpoint</DrawerTitle>
<ServiceDetailsEndpoint endpoint={endpoint}/>
</div>

View File

@ -169,9 +169,9 @@ class NonInjectedNodeDetails extends React.Component<NodeDetailsProps & Dependen
}
</DrawerItem>
}
<DrawerTitle title="Capacity"/>
<DrawerTitle>Capacity</DrawerTitle>
<NodeDetailsResources node={node} type={"capacity"}/>
<DrawerTitle title="Allocatable"/>
<DrawerTitle>Allocatable</DrawerTitle>
<NodeDetailsResources node={node} type={"allocatable"}/>
<PodDetailsList
pods={childPods}

View File

@ -28,26 +28,23 @@ interface RuleGroup {
@observer
export class PodSecurityPolicyDetails extends React.Component<PodSecurityPolicyDetailsProps> {
renderRuleGroup( title: React.ReactNode, group: RuleGroup) {
if (!group) return null;
const { rule, ranges } = group;
return (
<>
<DrawerTitle title={title}/>
<DrawerItem name="Rule">
{rule}
renderRuleGroup = (title: React.ReactNode, { rule, ranges }: RuleGroup) => (
<>
<DrawerTitle>{title}</DrawerTitle>
<DrawerItem name="Rule">
{rule}
</DrawerItem>
{ranges && (
<DrawerItem name="Ranges (Min-Max)" labelsOnly>
{ranges.map(({ min, max }, index) => (
<Badge
key={index}
label={`${min} - ${max}`} />
))}
</DrawerItem>
{ranges && (
<DrawerItem name="Ranges (Min-Max)" labelsOnly>
{ranges.map(({ min, max }, index) => {
return <Badge key={index} label={`${min} - ${max}`}/>;
})}
</DrawerItem>
)}
</>
);
}
)}
</>
);
render() {
const { object: psp } = this.props;
@ -161,7 +158,7 @@ export class PodSecurityPolicyDetails extends React.Component<PodSecurityPolicyD
{allowedHostPaths && (
<>
<DrawerTitle title="Allowed Host Paths"/>
<DrawerTitle>Allowed Host Paths</DrawerTitle>
<Table>
<TableHead>
<TableCell>Path Prefix</TableCell>
@ -179,14 +176,14 @@ export class PodSecurityPolicyDetails extends React.Component<PodSecurityPolicyD
</>
)}
{this.renderRuleGroup("Fs Group", fsGroup)}
{this.renderRuleGroup("Run As Group", runAsGroup)}
{this.renderRuleGroup("Run As User", runAsUser)}
{this.renderRuleGroup("Supplemental Groups", supplementalGroups)}
{fsGroup && this.renderRuleGroup("Fs Group", fsGroup)}
{runAsGroup && this.renderRuleGroup("Run As Group", runAsGroup)}
{runAsUser && this.renderRuleGroup("Run As User", runAsUser)}
{supplementalGroups && this.renderRuleGroup("Supplemental Groups", supplementalGroups)}
{runtimeClass && (
<>
<DrawerTitle title="Runtime Class"/>
<DrawerTitle>Runtime Class</DrawerTitle>
<DrawerItem name="Allowed Runtime Class Names">
{runtimeClass.allowedRuntimeClassNames?.join(", ") || "-"}
</DrawerItem>
@ -198,7 +195,7 @@ export class PodSecurityPolicyDetails extends React.Component<PodSecurityPolicyD
{seLinux && (
<>
<DrawerTitle title="Se Linux"/>
<DrawerTitle>Se Linux</DrawerTitle>
<DrawerItem name="Rule">
{seLinux.rule}
</DrawerItem>

View File

@ -80,7 +80,7 @@ class NonInjectedStorageClassDetails extends React.Component<StorageClassDetails
)}
{parameters && (
<>
<DrawerTitle title="Parameters"/>
<DrawerTitle>Parameters</DrawerTitle>
{
Object.entries(parameters).map(([name, value]) => (
<DrawerItem key={name + value} name={startCase(name)}>

View File

@ -102,7 +102,7 @@ export class PersistentVolumeClaimDetails extends React.Component<PersistentVolu
{volumeClaim.getStatus()}
</DrawerItem>
<DrawerTitle title="Selector"/>
<DrawerTitle>Selector</DrawerTitle>
<DrawerItem name="Match Labels" labelsOnly>
{volumeClaim.getMatchLabels().map(label => <Badge key={label} label={label}/>)}

View File

@ -67,7 +67,7 @@ export class VolumeDetailsList extends React.Component<VolumeDetailsListProps> {
return (
<div className="VolumeDetailsList flex column">
<DrawerTitle title="Persistent Volumes"/>
<DrawerTitle>Persistent Volumes</DrawerTitle>
<Table
tableId="storage_volume_details_list"
items={persistentVolumes}

View File

@ -65,7 +65,7 @@ export class PersistentVolumeDetails extends React.Component<PersistentVolumeDet
{nfs && (
<>
<DrawerTitle title="Network File System"/>
<DrawerTitle>Network File System</DrawerTitle>
{
Object.entries(nfs).map(([name, value]) => (
<DrawerItem key={name} name={startCase(name)}>
@ -78,7 +78,7 @@ export class PersistentVolumeDetails extends React.Component<PersistentVolumeDet
{flexVolume && (
<>
<DrawerTitle title="FlexVolume"/>
<DrawerTitle>FlexVolume</DrawerTitle>
<DrawerItem name="Driver">
{flexVolume.driver}
</DrawerItem>
@ -94,7 +94,7 @@ export class PersistentVolumeDetails extends React.Component<PersistentVolumeDet
{claimRef && (
<>
<DrawerTitle title="Claim"/>
<DrawerTitle>Claim</DrawerTitle>
<DrawerItem name="Type">
{claimRef.kind}
</DrawerItem>

View File

@ -68,7 +68,7 @@ export class ClusterRoleBindingDetails extends React.Component<ClusterRoleBindin
<div className="ClusterRoleBindingDetails">
<KubeObjectMeta object={clusterRoleBinding} />
<DrawerTitle title="Reference" />
<DrawerTitle>Reference</DrawerTitle>
<Table>
<TableHead>
<TableCell>Kind</TableCell>
@ -82,7 +82,7 @@ export class ClusterRoleBindingDetails extends React.Component<ClusterRoleBindin
</TableRow>
</Table>
<DrawerTitle title="Bindings" />
<DrawerTitle>Bindings</DrawerTitle>
{subjects.length > 0 && (
<Table selectable className="bindings box grow">
<TableHead>

View File

@ -28,7 +28,7 @@ export class ClusterRoleDetails extends React.Component<ClusterRoleDetailsProps>
<div className="ClusterRoleDetails">
<KubeObjectMeta object={clusterRole}/>
<DrawerTitle title="Rules"/>
<DrawerTitle>Rules</DrawerTitle>
{rules.map(({ resourceNames, apiGroups, resources, verbs }, index) => {
return (
<div className="rule" key={index}>

View File

@ -64,7 +64,7 @@ export class RoleBindingDetails extends React.Component<RoleBindingDetailsProps>
<div className="RoleBindingDetails">
<KubeObjectMeta object={roleBinding} />
<DrawerTitle title="Reference" />
<DrawerTitle>Reference</DrawerTitle>
<Table>
<TableHead>
<TableCell>Kind</TableCell>
@ -78,7 +78,7 @@ export class RoleBindingDetails extends React.Component<RoleBindingDetailsProps>
</TableRow>
</Table>
<DrawerTitle title="Bindings" />
<DrawerTitle>Bindings</DrawerTitle>
{subjects.length > 0 && (
<Table selectable className="bindings box grow">
<TableHead>

View File

@ -27,7 +27,7 @@ export class RoleDetails extends React.Component<RoleDetailsProps> {
return (
<div className="RoleDetails">
<KubeObjectMeta object={role}/>
<DrawerTitle title="Rules"/>
<DrawerTitle>Rules</DrawerTitle>
{rules.map(({ resourceNames, apiGroups, resources, verbs }, index) => {
return (
<div className="rule" key={index}>

View File

@ -143,7 +143,7 @@ export class ServiceAccountsDetails extends React.Component<ServiceAccountsDetai
</DrawerItem>
}
<DrawerTitle title="Mountable secrets"/>
<DrawerTitle>Mountable secrets</DrawerTitle>
<div className="secrets">
{this.renderSecrets()}
</div>

View File

@ -78,7 +78,7 @@ class NonInjectedCronJobDetails extends React.Component<CronJobDetailsProps & De
</DrawerItem>
{childJobs.length > 0 &&
<>
<DrawerTitle title="Jobs"/>
<DrawerTitle>Jobs</DrawerTitle>
{childJobs.map((job: Job) => {
const selectors = job.getSelectors();
const condition = job.getCondition();

View File

@ -53,7 +53,7 @@ export class DeploymentReplicaSets extends React.Component<DeploymentReplicaSets
return (
<div className="ReplicaSets flex column">
<DrawerTitle title="Deploy Revisions"/>
<DrawerTitle>Deploy Revisions</DrawerTitle>
<Table
selectable
tableId="deployment_replica_sets_view"

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { Link } from "react-router-dom";
import type { PodVolumeVariants, Pod, SecretReference } from "../../../../../common/k8s-api/endpoints";
import type { KubeApi } from "../../../../../common/k8s-api/kube-api";
import type { LocalObjectReference, KubeObject } from "../../../../../common/k8s-api/kube-object";
import { DrawerItem } from "../../../drawer";
import { getDetailsUrl } from "../../../kube-detail-params";
export interface PodVolumeVariantSpecificProps<Kind extends keyof PodVolumeVariants> {
variant: PodVolumeVariants[Kind];
pod: Pod;
volumeName: string;
}
export type VolumeVariantComponent<Kind extends keyof PodVolumeVariants> = React.FunctionComponent<PodVolumeVariantSpecificProps<Kind>>;
export interface LocalRefProps {
pod: Pod;
title: string;
kubeRef: LocalObjectReference | SecretReference | undefined;
api: KubeApi<KubeObject>;
}
export const LocalRef = ({ pod, title, kubeRef: ref, api }: LocalRefProps) => {
if (!ref) {
return null;
}
return (
<DrawerItem name={title}>
<Link to={getDetailsUrl(api.getUrl({ namespace: pod.getNs(), ...ref }))}>
{ref.name}
</Link>
</DrawerItem>
);
};

View File

@ -0,0 +1,291 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { Pod, PodVolume, PodVolumeKind } from "../../../../../common/k8s-api/endpoints";
import { DrawerItem } from "../../../drawer";
import { Icon } from "../../../icon";
import { AwsElasticBlockStore } from "./variants/aws-elastic-block-store";
import { AzureDisk } from "./variants/azure-disk";
import { AzureFile } from "./variants/azure-file";
import { CephFs } from "./variants/ceph-fs";
import { Cinder } from "./variants/cinder";
import { ConfigMap } from "./variants/config-map";
import { ContainerStorageInterface } from "./variants/container-storage-interface";
import { DownwardAPI } from "./variants/downward-api";
import { EmptyDir } from "./variants/empty-dir";
import { Ephemeral } from "./variants/ephemeral";
import { FiberChannel } from "./variants/fiber-channel";
import { FlexVolume } from "./variants/flex-volume";
import { Flocker } from "./variants/flocker";
import { GcePersistentDisk } from "./variants/gce-persistent-disk";
import { GitRepo } from "./variants/git-repo";
import { GlusterFs } from "./variants/gluster-fs";
import { HostPath } from "./variants/host-path";
import { IScsi } from "./variants/i-scsi";
import { Local } from "./variants/local";
import { NetworkFs } from "./variants/network-fs";
import { PersistentVolumeClaim } from "./variants/persistent-volume-claim";
import { PhotonPersistentDisk } from "./variants/photon-persistent-disk";
import { PortworxVolume } from "./variants/portworx-volume";
import { Projected } from "./variants/projected";
import { Quobyte } from "./variants/quobyte";
import { RadosBlockDevice } from "./variants/rados-block-device";
import { ScaleIo } from "./variants/scale-io";
import { Secret } from "./variants/secret";
import { StorageOs } from "./variants/storage-os";
import { VsphereVolume } from "./variants/vsphere-volume";
const deprecatedVolumeTypes = new Set<PodVolumeKind>([
"flocker",
"gitRepo",
"quobyte",
"storageos",
]);
interface VolumeVariantProps {
pod: Pod;
volume: PodVolume;
}
interface VolumeVariantRender {
kind: PodVolumeKind;
element: JSX.Element;
}
function renderVolumeVariant({ pod, volume }: VolumeVariantProps): VolumeVariantRender | null {
if (volume.awsElasticBlockStore) {
return {
kind: "awsElasticBlockStore",
element: <AwsElasticBlockStore variant={volume.awsElasticBlockStore} pod={pod} volumeName={volume.name} />,
};
}
if (volume.azureDisk) {
return {
kind: "azureDisk",
element: <AzureDisk variant={volume.azureDisk} pod={pod} volumeName={volume.name} />,
};
}
if (volume.azureFile) {
return {
kind: "azureFile",
element: <AzureFile variant={volume.azureFile} pod={pod} volumeName={volume.name} />,
};
}
if (volume.cephfs) {
return {
kind: "cephfs",
element: <CephFs variant={volume.cephfs} pod={pod} volumeName={volume.name} />,
};
}
if (volume.cinder) {
return {
kind: "cinder",
element: <Cinder variant={volume.cinder} pod={pod} volumeName={volume.name} />,
};
}
if (volume.configMap) {
return {
kind: "configMap",
element: <ConfigMap variant={volume.configMap} pod={pod} volumeName={volume.name} />,
};
}
if (volume.csi) {
return {
kind: "csi",
element: <ContainerStorageInterface variant={volume.csi} pod={pod} volumeName={volume.name} />,
};
}
if (volume.downwardAPI) {
return {
kind: "downwardAPI",
element: <DownwardAPI variant={volume.downwardAPI} pod={pod} volumeName={volume.name} />,
};
}
if (volume.emptyDir) {
return {
kind: "emptyDir",
element: <EmptyDir variant={volume.emptyDir} pod={pod} volumeName={volume.name} />,
};
}
if (volume.ephemeral) {
return {
kind: "ephemeral",
element: <Ephemeral variant={volume.ephemeral} pod={pod} volumeName={volume.name} />,
};
}
if (volume.fc) {
return {
kind: "fc",
element: <FiberChannel variant={volume.fc} pod={pod} volumeName={volume.name} />,
};
}
if (volume.flexVolume) {
return {
kind: "flexVolume",
element: <FlexVolume variant={volume.flexVolume} pod={pod} volumeName={volume.name} />,
};
}
if (volume.flocker) {
return {
kind: "flocker",
element: <Flocker variant={volume.flocker} pod={pod} volumeName={volume.name} />,
};
}
if (volume.gcePersistentDisk) {
return {
kind: "gcePersistentDisk",
element: <GcePersistentDisk variant={volume.gcePersistentDisk} pod={pod} volumeName={volume.name} />,
};
}
if (volume.gitRepo) {
return {
kind: "gitRepo",
element: <GitRepo variant={volume.gitRepo} pod={pod} volumeName={volume.name} />,
};
}
if (volume.glusterfs) {
return {
kind: "glusterfs",
element: <GlusterFs variant={volume.glusterfs} pod={pod} volumeName={volume.name} />,
};
}
if (volume.hostPath) {
return {
kind: "hostPath",
element: <HostPath variant={volume.hostPath} pod={pod} volumeName={volume.name} />,
};
}
if (volume.iscsi) {
return {
kind: "iscsi",
element: <IScsi variant={volume.iscsi} pod={pod} volumeName={volume.name} />,
};
}
if (volume.local) {
return {
kind: "local",
element: <Local variant={volume.local} pod={pod} volumeName={volume.name} />,
};
}
if (volume.nfs) {
return {
kind: "nfs",
element: <NetworkFs variant={volume.nfs} pod={pod} volumeName={volume.name} />,
};
}
if (volume.persistentVolumeClaim) {
return {
kind: "persistentVolumeClaim",
element: <PersistentVolumeClaim variant={volume.persistentVolumeClaim} pod={pod} volumeName={volume.name} />,
};
}
if (volume.photonPersistentDisk) {
return {
kind: "photonPersistentDisk",
element: <PhotonPersistentDisk variant={volume.photonPersistentDisk} pod={pod} volumeName={volume.name} />,
};
}
if (volume.portworxVolume) {
return {
kind: "portworxVolume",
element: <PortworxVolume variant={volume.portworxVolume} pod={pod} volumeName={volume.name} />,
};
}
if (volume.projected) {
return {
kind: "projected",
element: <Projected variant={volume.projected} pod={pod} volumeName={volume.name} />,
};
}
if (volume.quobyte) {
return {
kind: "quobyte",
element: <Quobyte variant={volume.quobyte} pod={pod} volumeName={volume.name} />,
};
}
if (volume.rbd) {
return {
kind: "rbd",
element: <RadosBlockDevice variant={volume.rbd} pod={pod} volumeName={volume.name} />,
};
}
if (volume.scaleIO) {
return {
kind: "scaleIO",
element: <ScaleIo variant={volume.scaleIO} pod={pod} volumeName={volume.name} />,
};
}
if (volume.secret) {
return {
kind: "secret",
element: <Secret variant={volume.secret} pod={pod} volumeName={volume.name} />,
};
}
if (volume.storageos) {
return {
kind: "storageos",
element: <StorageOs variant={volume.storageos} pod={pod} volumeName={volume.name} />,
};
}
if (volume.vsphereVolume) {
return {
kind: "vsphereVolume",
element: <VsphereVolume variant={volume.vsphereVolume} pod={pod} volumeName={volume.name} />,
};
}
return null;
}
export function VolumeVariant(props: VolumeVariantProps) {
const result = renderVolumeVariant(props);
if (!result) {
return <p>Error! Unknown pod volume kind</p>;
}
const { kind, element } = result;
const isDeprecated = deprecatedVolumeTypes.has(kind);
return (
<>
<DrawerItem name="Kind">
{kind}
{isDeprecated && <Icon title="Deprecated" material="warning_amber" />}
</DrawerItem>
{element}
</>
);
}

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const AwsElasticBlockStore: VolumeVariantComponent<"awsElasticBlockStore"> = (
({ variant: { volumeID, fsType = "ext4" }}) => (
<>
<DrawerItem name="Volume ID">
{volumeID}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const AzureDisk: VolumeVariantComponent<"azureDisk"> = (
({ variant: { diskName, diskURI, kind = "Shared", cachingMode = "None", fsType = "ext4", readonly = false }}) => (
<>
<DrawerItem name={kind === "Managed" ? "Disk Name" : "VHD blob Name"}>
{diskName}
</DrawerItem>
<DrawerItem name={kind === "Managed" ? "Resource ID" : "Disk URI"}>
{diskURI}
</DrawerItem>
<DrawerItem name="Kind">
{kind}
</DrawerItem>
<DrawerItem name="Caching Mode">
{cachingMode}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readonly.toString()}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const AzureFile: VolumeVariantComponent<"azureFile"> = (
({ variant: { readOnly = false, secretName, shareName, secretNamespace = "default" }}) => (
<>
<DrawerItem name="Secret Name">
{secretName}
</DrawerItem>
<DrawerItem name="Share Name">
{shareName}
</DrawerItem>
<DrawerItem name="Namespace of Secret">
{secretNamespace}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { secretsApi } from "../../../../../../common/k8s-api/endpoints";
import { DrawerItem } from "../../../../drawer";
import { LocalRef, VolumeVariantComponent } from "../variant-helpers";
export const CephFs: VolumeVariantComponent<"cephfs"> = (
({ pod, variant: { monitors, path = "/", user = "admin", secretFile = "/etc/ceph/user.secret", secretRef, readOnly }}) => (
<>
<DrawerItem name="Monitors">
<ul>
{monitors.map(monitor => <li key={monitor}>{monitor}</li>)}
</ul>
</DrawerItem>
<DrawerItem name="Mount Path">
{path}
</DrawerItem>
<DrawerItem name="Username">
{user}
</DrawerItem>
{
secretRef
? (
<LocalRef
pod={pod}
title="Secret"
kubeRef={secretRef}
api={secretsApi}
/>
)
: (
<DrawerItem name="Secret Filepath">
{secretFile}
</DrawerItem>
)
}
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const Cinder: VolumeVariantComponent<"cinder"> = (
({ variant: { volumeID, fsType = "ext4" }}) => (
<>
<DrawerItem name="Volume ID">
{volumeID}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { configMapApi } from "../../../../../../common/k8s-api/endpoints";
import { LocalRef, VolumeVariantComponent } from "../variant-helpers";
export const ConfigMap: VolumeVariantComponent<"configMap"> = (
({ pod, variant: { name }}) => (
<LocalRef
pod={pod}
title="Name"
kubeRef={{ name }}
api={configMapApi}
/>
)
);

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { secretsApi } from "../../../../../../common/k8s-api/endpoints";
import { DrawerItem } from "../../../../drawer";
import { LocalRef, VolumeVariantComponent } from "../variant-helpers";
export const ContainerStorageInterface: VolumeVariantComponent<"csi"> = ({
pod,
variant: {
driver,
readOnly = false,
fsType = "ext4",
volumeAttributes = {},
nodePublishSecretRef,
controllerPublishSecretRef,
nodeStageSecretRef,
controllerExpandSecretRef,
},
}) => (
<>
<DrawerItem name="Driver">
{driver}
</DrawerItem>
<DrawerItem name="ReadOnly">
{readOnly.toString()}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<LocalRef
pod={pod}
title="Controller Publish Secret"
kubeRef={controllerPublishSecretRef}
api={secretsApi}
/>
<LocalRef
pod={pod}
title="Controller Expand Secret"
kubeRef={controllerExpandSecretRef}
api={secretsApi}
/>
<LocalRef
pod={pod}
title="Node Publish Secret"
kubeRef={nodePublishSecretRef}
api={secretsApi}
/>
<LocalRef
pod={pod}
title="Node Stage Secret"
kubeRef={nodeStageSecretRef}
api={secretsApi}
/>
{
Object.entries(volumeAttributes)
.map(([key, value]) => (
<DrawerItem key={key} name={key}>
{value}
</DrawerItem>
))
}
</>
);

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const DownwardAPI: VolumeVariantComponent<"downwardAPI"> = (
({ variant: { items }}) => (
<>
<DrawerItem name="Items">
<ul>
{items.map(item => <li key={item.path}>{item.path}</li>)}
</ul>
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const EmptyDir: VolumeVariantComponent<"emptyDir"> = (
({ variant: { medium, sizeLimit }}) => (
<>
<DrawerItem name="Medium">
{medium || "<node's default medium>"}
</DrawerItem>
<DrawerItem name="Size Limit" hidden={!sizeLimit}>
{sizeLimit}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { dump } from "js-yaml";
import React from "react";
import { DrawerItem, DrawerItemLabels } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const Ephemeral: VolumeVariantComponent<"ephemeral"> = (
({ pod, volumeName, variant: { volumeClaimTemplate: { metadata, spec }}}) => (
<>
<DrawerItem name="PVC Template Name">
{pod.getName()}-{volumeName}
</DrawerItem>
<DrawerItemLabels
name="Template Labels"
labels={metadata.labels}
/>
<DrawerItemLabels
name="Template Annotations"
labels={metadata.annotations}
/>
<DrawerItem name="Template PVC Spec">
{dump(spec)}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const FiberChannel: VolumeVariantComponent<"fc"> = (
({ variant: { targetWWNs, lun, fsType = "ext4", readOnly = false }}) => (
<>
<DrawerItem name="Target World Wide Names">
<ul>
{targetWWNs.map(targetWWN => <li key={targetWWN}>{targetWWN}</li>)}
</ul>
</DrawerItem>
<DrawerItem name="Logical Unit Number">
{lun.toString()}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { secretsApi } from "../../../../../../common/k8s-api/endpoints";
import { DrawerItem } from "../../../../drawer";
import { LocalRef, VolumeVariantComponent } from "../variant-helpers";
export const FlexVolume: VolumeVariantComponent<"flexVolume"> = (
({ pod, variant: { driver, fsType, secretRef, readOnly = false, options }}) => (
<>
<DrawerItem name="Driver">
{driver}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType || "-- system default --"}
</DrawerItem>
<LocalRef
pod={pod}
title="Secret"
kubeRef={secretRef}
api={secretsApi}
/>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
{
...Object.entries(options)
.map(([key, value]) => (
<DrawerItem key={key} name={`Option: ${key}`}>
{value}
</DrawerItem>
))
}
</>
)
);

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const Flocker: VolumeVariantComponent<"flocker"> = (
({ variant: { datasetName }}) => (
<>
<DrawerItem name="Dataset Name">
{datasetName}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const GcePersistentDisk: VolumeVariantComponent<"gcePersistentDisk"> = (
({ variant: { pdName, fsType = "ext4" }}) => (
<>
<DrawerItem name="Persistent Disk Name">
{pdName}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const GitRepo: VolumeVariantComponent<"gitRepo"> = (
({ variant: { repository, revision }}) => (
<>
<DrawerItem name="Repository URL">
{repository}
</DrawerItem>
<DrawerItem name="Commit Hash">
{revision}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const GlusterFs: VolumeVariantComponent<"glusterfs"> = (
({ variant: { endpoints, path, readOnly = false }}) => (
<>
<DrawerItem name="Endpoints object name">
{endpoints}
</DrawerItem>
<DrawerItem name="Glusterfs volume name">
{path}
</DrawerItem>
<DrawerItem name="Readonly Mountpoint">
{readOnly.toString()}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const HostPath: VolumeVariantComponent<"hostPath"> = (
({ variant: { path, type }}) => (
<>
<DrawerItem name="Node's Host Filesystem Path">
{path}
</DrawerItem>
<DrawerItem name="Check Behaviour">
{type || "-- none --"}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const IScsi: VolumeVariantComponent<"iscsi"> = (
({ variant: { targetPortal, iqn, lun, fsType = "ext4", readOnly = false, chapAuthDiscovery, chapAuthSession, secretRef }}) => (
<>
<DrawerItem name="Target Address">
{targetPortal}
</DrawerItem>
<DrawerItem name="iSCSI qualified name">
{iqn}
</DrawerItem>
<DrawerItem name="Logical Unit Number">
{lun.toString()}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
{chapAuthDiscovery && (
<DrawerItem name="CHAP Discovery Authentication">
{chapAuthDiscovery.toString()}
</DrawerItem>
)}
{chapAuthSession && (
<DrawerItem name="CHAP Session Authentication">
{chapAuthSession.toString()}
</DrawerItem>
)}
{ secretRef && (
<DrawerItem name="CHAP Secret">
{secretRef.name}
</DrawerItem>
)}
</>
)
);

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const Local: VolumeVariantComponent<"local"> = (
({ variant: { path }}) => (
<>
<DrawerItem name="Path">
{path}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const NetworkFs: VolumeVariantComponent<"nfs"> = (
({ variant: { server, path, readOnly = false }}) => (
<>
<DrawerItem name="Server">
{server}
</DrawerItem>
<DrawerItem name="Path">
{path}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { pvcApi } from "../../../../../../common/k8s-api/endpoints";
import { LocalRef, VolumeVariantComponent } from "../variant-helpers";
export const PersistentVolumeClaim: VolumeVariantComponent<"persistentVolumeClaim"> = (
({ pod, variant: { claimName }}) => (
<LocalRef
pod={pod}
title="Name"
kubeRef={{ name: claimName }}
api={pvcApi}
/>
)
);

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const PhotonPersistentDisk: VolumeVariantComponent<"photonPersistentDisk"> = (
({ variant: { pdID, fsType = "ext4" }}) => (
<>
<DrawerItem name="Persistent Disk ID">
{pdID}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const PortworxVolume: VolumeVariantComponent<"portworxVolume"> = (
({ variant: { volumeID, fsType = "ext4", readOnly = false }}) => (
<>
<DrawerItem name="Volume ID">
{volumeID}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem, DrawerTitle } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const Projected: VolumeVariantComponent<"projected"> = (
({ variant: { sources, defaultMode }}) => (
<>
<DrawerItem name="Default Mount Mode">
0o{defaultMode.toString(8)}
</DrawerItem>
<DrawerItem name="Sources">
{
sources.map(({ secret, downwardAPI, configMap, serviceAccountToken }, index) => (
<React.Fragment key={index}>
{secret && (
<>
<DrawerTitle size="sub-title">Secret</DrawerTitle>
<DrawerItem name="Name">
{secret.name}
</DrawerItem>
<DrawerItem name="Items">
<ul>
{secret.items?.map(({ key, path }) => (
<li key={key}>
{key} {path}
</li>
))}
</ul>
</DrawerItem>
</>
)}
{downwardAPI && (
<>
<DrawerTitle size="sub-title">Downward API</DrawerTitle>
<DrawerItem name="Items">
<ul>
{downwardAPI.items?.map(({ path }) => (
<li key={path}>
{path}
</li>
))}
</ul>
</DrawerItem>
</>
)}
{configMap && (
<>
<DrawerTitle size="sub-title">Config Map</DrawerTitle>
<DrawerItem name="Name">
{configMap.name}
</DrawerItem>
<DrawerItem name="Items">
<ul>
{configMap.items?.map(({ key, path }) => (
<li key={key}>
{key} {path}
</li>
))}
</ul>
</DrawerItem>
</>
)}
{serviceAccountToken && (
<>
<DrawerTitle size="sub-title">Service Account Token</DrawerTitle>
<DrawerItem name="Audience" hidden={!serviceAccountToken.audience}>
{serviceAccountToken.audience}
</DrawerItem>
<DrawerItem name="Expiration">
{String(serviceAccountToken.expirationSeconds || (60*60 /* an hour */))}s
</DrawerItem>
<DrawerItem name="Path">
{serviceAccountToken.path}
</DrawerItem>
</>
)}
</React.Fragment>
))
}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const Quobyte: VolumeVariantComponent<"quobyte"> = (
({ variant: { registry, volume, readOnly = false, user = "serviceaccount", group, tenant }}) => (
<>
<DrawerItem name="Registry">
{registry}
</DrawerItem>
<DrawerItem name="Volume">
{volume}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
<DrawerItem name="User">
{user}
</DrawerItem>
<DrawerItem name="Group">
{group ?? "-- no group --"}
</DrawerItem>
<DrawerItem name="Tenant" hidden={!tenant}>
{tenant}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { secretsApi } from "../../../../../../common/k8s-api/endpoints";
import { DrawerItem } from "../../../../drawer";
import { LocalRef, VolumeVariantComponent } from "../variant-helpers";
export const RadosBlockDevice: VolumeVariantComponent<"rbd"> = (
({ pod, variant: { monitors, image, fsType = "ext4", pool = "rbd", user = "admin", keyring = "/etc/ceph/keyright", secretRef, readOnly = false }}) => (
<>
<DrawerItem name="Ceph Monitors">
<ul>
{monitors.map(monitor => <li key={monitor}>{monitor}</li>)}
</ul>
</DrawerItem>
<DrawerItem name="Image">
{image}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Pool">
{pool}
</DrawerItem>
<DrawerItem name="User">
{user}
</DrawerItem>
{
secretRef
? (
<LocalRef
pod={pod}
title="Authentication Secret"
kubeRef={secretRef}
api={secretsApi}
/>
)
: (
<DrawerItem name="Keyright Path">
{keyring}
</DrawerItem>
)
}
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { secretsApi } from "../../../../../../common/k8s-api/endpoints";
import { DrawerItem } from "../../../../drawer";
import { LocalRef, VolumeVariantComponent } from "../variant-helpers";
export const ScaleIo: VolumeVariantComponent<"scaleIO"> = (
({ pod, variant: { gateway, system, secretRef, sslEnabled = false, protectionDomain, storagePool, storageMode = "ThinProvisioned", volumeName, fsType = "xfs", readOnly = false }}) => (
<>
<DrawerItem name="Gateway">
{gateway}
</DrawerItem>
<DrawerItem name="System">
{system}
</DrawerItem>
<LocalRef
pod={pod}
title="Name"
kubeRef={secretRef}
api={secretsApi}
/>
<DrawerItem name="SSL Enabled">
{sslEnabled.toString()}
</DrawerItem>
<DrawerItem name="Protection Domain Name" hidden={!protectionDomain}>
{protectionDomain}
</DrawerItem>
<DrawerItem name="Storage Pool" hidden={!storagePool}>
{storagePool}
</DrawerItem>
<DrawerItem name="Storage Mode" hidden={!storageMode}>
{storageMode}
</DrawerItem>
<DrawerItem name="Volume Name">
{volumeName}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { secretsApi } from "../../../../../../common/k8s-api/endpoints";
import { DrawerItem } from "../../../../drawer";
import { LocalRef, VolumeVariantComponent } from "../variant-helpers";
export const Secret: VolumeVariantComponent<"secret"> = (
({ pod, variant: { secretName, items = [], defaultMode = 0o644, optional = false }}) => (
<>
<LocalRef
pod={pod}
title="Name"
kubeRef={{ name: secretName }}
api={secretsApi}
/>
<DrawerItem name="Items" hidden={items.length === 0}>
<ul>
{items.map(({ key }) => <li key={key}>{key}</li>)}
</ul>
</DrawerItem>
<DrawerItem name="Default File Mode">
0o{defaultMode.toString(8)}
</DrawerItem>
<DrawerItem name="Optional">
{optional.toString()}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { secretsApi } from "../../../../../../common/k8s-api/endpoints";
import { DrawerItem } from "../../../../drawer";
import { LocalRef, VolumeVariantComponent } from "../variant-helpers";
export const StorageOs: VolumeVariantComponent<"storageos"> = (
({ pod, variant: { volumeName, volumeNamespace, fsType = "ext4", readOnly = false, secretRef }}) => (
<>
<DrawerItem name="Volume Name">
{volumeName}
</DrawerItem>
<DrawerItem name="Volume Namespace" hidden={volumeNamespace === "default"}>
{
volumeNamespace === volumeName
? "- no default behaviour -"
: volumeNamespace || pod.getNs()
}
</DrawerItem>
<DrawerItem name="Filesystem type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
<LocalRef
pod={pod}
title="Secret"
kubeRef={secretRef}
api={secretsApi}
/>
</>
)
);

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerItem } from "../../../../drawer";
import type { VolumeVariantComponent } from "../variant-helpers";
export const VsphereVolume: VolumeVariantComponent<"vsphereVolume"> = (
({ variant: { volumePath, fsType = "ext4", storagePolicyName, storagePolicyID }}) => (
<>
<DrawerItem name="Virtual Machine Disk Volume">
{volumePath}
</DrawerItem>
<DrawerItem name="Filesystem type">
{fsType}
</DrawerItem>
<DrawerItem name="Storage Policy Based Management Profile Name" hidden={!storagePolicyName}>
{storagePolicyName}
</DrawerItem>
<DrawerItem name="Storage Policy Based Management Profile ID" hidden={!storagePolicyID}>
{storagePolicyID}
</DrawerItem>
</>
)
);

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { observer } from "mobx-react";
import React from "react";
import type { Pod } from "../../../../../common/k8s-api/endpoints";
import { DrawerTitle } from "../../../drawer";
import { Icon } from "../../../icon";
import { VolumeVariant } from "./variant";
export interface PodVolumesProps {
pod: Pod;
}
export const PodVolumes = observer(({ pod }: PodVolumesProps) => {
const volumes = pod.getVolumes() ?? [];
if (volumes.length === 0) {
return null;
}
return (
<>
<DrawerTitle>Volumes</DrawerTitle>
{volumes.map(volume => (
<div key={volume.name} className="volume">
<div className="title flex gaps">
<Icon small material="storage" />
<span>{volume.name}</span>
</div>
<VolumeVariant pod={pod} volume={volume} />
</div>
))}
</>
);
});

View File

@ -131,7 +131,7 @@ export class PodDetailsList extends React.Component<PodDetailsListProps> {
return (
<div className="PodDetailsList flex column">
<DrawerTitle title="Pods" />
<DrawerTitle>Pods</DrawerTitle>
<Table
tableId="workloads_pod_details_list"
items={pods}

View File

@ -5,10 +5,10 @@
import "./pod-details-secrets.scss";
import React, { Component } from "react";
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { autorun, observable, makeObservable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { reaction } from "mobx";
import { observer } from "mobx-react";
import { Pod, Secret, secretsApi } from "../../../common/k8s-api/endpoints";
import { getDetailsUrl } from "../kube-detail-params";
@ -16,59 +16,50 @@ export interface PodDetailsSecretsProps {
pod: Pod;
}
@observer
export class PodDetailsSecrets extends Component<PodDetailsSecretsProps> {
@observable secrets: Map<string, Secret> = observable.map<string, Secret>();
export const PodDetailsSecrets = observer(({ pod }: PodDetailsSecretsProps) => {
const [secrets, setSecrets] = useState(new Map<string, Secret>());
componentDidMount(): void {
disposeOnUnmount(this, [
autorun(async () => {
const { pod } = this.props;
const secrets = await Promise.all(
pod.getSecrets().map(secretName => secretsApi.get({
useEffect(() => (
reaction(
() => pod.getSecrets(),
async (secretNames) => {
const results = await Promise.allSettled(
secretNames.map(secretName => secretsApi.get({
name: secretName,
namespace: pod.getNs(),
})),
);
secrets.forEach(secret => secret && this.secrets.set(secret.getName(), secret));
}),
]);
}
setSecrets(new Map(
results
.filter(result => result.status === "fulfilled" && result.value)
.map(result => (result as PromiseFulfilledResult<Secret>).value)
.map(secret => [secret.getName(), secret]),
));
},
{
fireImmediately: true,
})
), []);
constructor(props: PodDetailsSecretsProps) {
super(props);
makeObservable(this);
}
const renderSecret = (name: string) => {
const secret = secrets.get(name);
render() {
const { pod } = this.props;
if (!secret) {
return <span key={name}>{name}</span>;
}
return (
<div className="PodDetailsSecrets">
{
pod.getSecrets().map(secretName => {
const secret = this.secrets.get(secretName);
if (secret) {
return this.renderSecretLink(secret);
} else {
return (
<span key={secretName}>{secretName}</span>
);
}
})
}
</div>
);
}
protected renderSecretLink(secret: Secret) {
return (
<Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}>
{secret.getName()}
</Link>
);
}
}
};
return (
<div className="PodDetailsSecrets">
{pod.getSecrets().map(renderSecret)}
</div>
);
});

View File

@ -10,14 +10,13 @@ import kebabCase from "lodash/kebabCase";
import { disposeOnUnmount, observer } from "mobx-react";
import { Link } from "react-router-dom";
import { observable, reaction, makeObservable } from "mobx";
import { type IPodMetrics, nodesApi, Pod, pvcApi, configMapApi, getMetricsForPods } from "../../../common/k8s-api/endpoints";
import { type IPodMetrics, nodesApi, Pod, getMetricsForPods } from "../../../common/k8s-api/endpoints";
import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge";
import { boundMethod, cssNames, toJS } from "../../utils";
import { PodDetailsContainer } from "./pod-details-container";
import { PodDetailsAffinities } from "./pod-details-affinities";
import { PodDetailsTolerations } from "./pod-details-tolerations";
import { Icon } from "../icon";
import { PodDetailsSecrets } from "./pod-details-secrets";
import { ResourceMetrics } from "../resource-metrics";
import type { KubeObjectDetailsProps } from "../kube-object-details";
@ -28,6 +27,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { getDetailsUrl } from "../kube-detail-params";
import logger from "../../../common/logger";
import { PodVolumes } from "./details/volumes/view";
export interface PodDetailsProps extends KubeObjectDetailsProps<Pod> {
}
@ -73,12 +73,13 @@ export class PodDetails extends React.Component<PodDetailsProps> {
}
const { status, spec } = pod;
const { conditions, podIP } = status;
const { conditions = [], podIP } = status ?? {};
const podIPs = pod.getIPs();
const { nodeName } = spec;
const { nodeName } = spec ?? {};
const nodeSelector = pod.getNodeSelectors();
const volumes = pod.getVolumes();
const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Pod);
const initContainers = pod.getInitContainers();
const containers = pod.getContainers();
return (
<div className="PodDetails">
@ -90,26 +91,22 @@ export class PodDetails extends React.Component<PodDetailsProps> {
<PodCharts/>
</ResourceMetrics>
)}
<KubeObjectMeta object={pod}/>
<DrawerItem name="Status">
<span className={cssNames("status", kebabCase(pod.getStatusMessage()))}>{pod.getStatusMessage()}</span>
</DrawerItem>
<DrawerItem name="Node">
{nodeName && (
<Link to={getDetailsUrl(nodesApi.getUrl({ name: nodeName }))}>
{nodeName}
</Link>
)}
<DrawerItem name="Node" hidden={!nodeName}>
<Link to={getDetailsUrl(nodesApi.getUrl({ name: nodeName }))}>
{nodeName}
</Link>
</DrawerItem>
<DrawerItem name="Pod IP">
{podIP}
</DrawerItem>
<DrawerItem name="Pod IPs" hidden={!podIPs.length} labelsOnly>
{
podIPs.map(label => (
<Badge key={label} label={label}/>
))
}
<DrawerItem name="Pod IPs" hidden={podIPs.length === 0} labelsOnly>
{podIPs.map(label => <Badge key={label} label={label} />)}
</DrawerItem>
<DrawerItem name="Priority Class">
{pod.getPriorityClassName()}
@ -117,129 +114,55 @@ export class PodDetails extends React.Component<PodDetailsProps> {
<DrawerItem name="QoS Class">
{pod.getQosClass()}
</DrawerItem>
{conditions &&
<DrawerItem name="Conditions" className="conditions" labelsOnly>
{
conditions.map(condition => {
const { type, status, lastTransitionTime } = condition;
return (
<Badge
key={type}
label={type}
disabled={status === "False"}
tooltip={`Last transition time: ${lastTransitionTime}`}
/>
);
})
}
</DrawerItem>
}
{nodeSelector.length > 0 &&
<DrawerItem name="Node Selector">
<DrawerItem name="Conditions" className="conditions" hidden={conditions.length === 0} labelsOnly>
{
nodeSelector.map(label => (
<Badge key={label} label={label}/>
conditions.map(({ type, status, lastTransitionTime }) => (
<Badge
key={type}
label={type}
disabled={status === "False"}
tooltip={`Last transition time: ${lastTransitionTime}`}
/>
))
}
</DrawerItem>
}
<DrawerItem name="Node Selector" hidden={nodeSelector.length === 0}>
{nodeSelector.map(label => <Badge key={label} label={label} />)}
</DrawerItem>
<PodDetailsTolerations workload={pod}/>
<PodDetailsAffinities workload={pod}/>
{pod.getSecrets().length > 0 && (
<DrawerItem name="Secrets">
<PodDetailsSecrets pod={pod}/>
</DrawerItem>
)}
<DrawerItem name="Secrets" hidden={pod.getSecrets().length === 0}>
<PodDetailsSecrets pod={pod}/>
</DrawerItem>
{pod.getInitContainers() && pod.getInitContainers().length > 0 &&
<DrawerTitle title="Init Containers"/>
}
{
pod.getInitContainers() && pod.getInitContainers().map(container => {
return <PodDetailsContainer key={container.name} pod={pod} container={container}/>;
})
}
<DrawerTitle title="Containers"/>
{
pod.getContainers().map(container => {
const { name } = container;
const metrics = getItemMetrics(toJS(this.containerMetrics), name);
return (
{initContainers.length > 0 && (
<>
<DrawerTitle>Init Containers</DrawerTitle>
{initContainers.map(container => (
<PodDetailsContainer
key={name}
key={container.name}
pod={pod}
container={container}
metrics={metrics || null}
/>
);
})
}
{volumes.length > 0 && (
<>
<DrawerTitle title="Volumes"/>
{volumes.map(volume => {
const claimName = volume.persistentVolumeClaim ? volume.persistentVolumeClaim.claimName : null;
const configMap = volume.configMap ? volume.configMap.name : null;
const type = Object.keys(volume)[1];
return (
<div key={volume.name} className="volume">
<div className="title flex gaps">
<Icon small material="storage"/>
<span>{volume.name}</span>
</div>
<DrawerItem name="Type">
{type}
</DrawerItem>
{ type == "configMap" && (
<div>
{configMap && (
<DrawerItem name="Name">
<Link
to={getDetailsUrl(configMapApi.getUrl({
name: configMap,
namespace: pod.getNs(),
}))}>{configMap}
</Link>
</DrawerItem>
)}
</div>
)}
{ type === "emptyDir" && (
<div>
{ volume.emptyDir.medium && (
<DrawerItem name="Medium">
{volume.emptyDir.medium}
</DrawerItem>
)}
{ volume.emptyDir.sizeLimit && (
<DrawerItem name="Size Limit">
{volume.emptyDir.sizeLimit}
</DrawerItem>
)}
</div>
)}
{claimName && (
<DrawerItem name="Claim Name">
<Link
to={getDetailsUrl(pvcApi.getUrl({
name: claimName,
namespace: pod.getNs(),
}))}
>{claimName}
</Link>
</DrawerItem>
)}
</div>
);
})}
))}
</>
)}
<DrawerTitle>Containers</DrawerTitle>
{containers.map(container => (
<PodDetailsContainer
key={container.name}
pod={pod}
container={container}
metrics={getItemMetrics(toJS(this.containerMetrics), container.name)}
/>
))}
<PodVolumes pod={pod} />
</div>
);
}

View File

@ -7,9 +7,9 @@
@tailwind components;
@tailwind utilities;
@import "~flex.box"; // todo: replace with tailwind's flexbox classes
@import "./fonts";
@import "~flex.box";
@import "../themes/theme-vars";
@import "./fonts";
:root {
--unit: 8px;

View File

@ -59,6 +59,10 @@ export function Avatar(props: AvatarProps) {
const { title, variant = "rounded", size = 32, colorHash, children, background, imgProps, src, className, disabled, ...rest } = props;
const getBackgroundColor = () => {
if (src) {
return "transparent";
}
return background || randomColor({ seed: colorHash, luminosity: "dark" });
};

View File

@ -81,7 +81,7 @@
.close {
position: absolute;
right: 0px;
width: 5ch;
width: 4ch;
opacity: 0;
text-align: center;
background: linear-gradient(90deg, transparent 0%, var(--color-active) 25%);

View File

@ -46,7 +46,7 @@ class NonInjectedEditResource extends React.Component<EditResourceProps & Depend
const obj = this.resource;
if (!obj) {
if (store.isLoaded) {
if (store?.isLoaded) {
// auto-close tab when resource removed from store
this.props.closeTab(this.props.tab.id);
} else if (!store.isLoading) {

View File

@ -57,8 +57,9 @@ const NonInjectedLogsDockTab = observer(({ className, tab, model, subscribeStore
setTimeout(() => {
const overlay = document.querySelector(".PodLogs .list span.active");
if (!overlay) return;
overlay.scrollIntoViewIfNeeded();
if (typeof overlay?.scrollIntoViewIfNeeded === "function") {
overlay.scrollIntoViewIfNeeded();
}
}, 100);
};

View File

@ -4,7 +4,4 @@
*/
.TerminalTab {
@include font-preload {
font-family: "RobotoMono", monospace;
}
}

Some files were not shown because too many files have changed in this diff Show More