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

Merge remote-tracking branch 'origin/master' into webpack5-upgrade-attempt

This commit is contained in:
Roman 2022-02-08 13:30:06 +02:00
commit 8a6ef6c9f0
132 changed files with 1951 additions and 929 deletions

28
.github/workflows/electronegativity.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Electronegativity
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build_job:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "14"
- uses: doyensec/electronegativity-action@v1.1
with:
input: src/
electron-version: "14.2.4"
severity: medium
- name: Upload sarif
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: ../results

View File

@ -6,6 +6,9 @@ on:
release:
types:
- published
concurrency:
group: publish-docs
cancel-in-progress: true
jobs:
verify-docs:
name: Verify docs
@ -17,7 +20,7 @@ jobs:
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: '3.x'
python-version: "3.x"
- name: Checkout Release from lens
uses: actions/checkout@v2
@ -31,8 +34,8 @@ jobs:
- name: Generate Extensions API Reference using typedocs
run: |
yarn install
yarn typedocs-extensions-api
yarn install
yarn typedocs-extensions-api
- name: Verify that the markdown is valid
run: |
@ -49,7 +52,7 @@ jobs:
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: '3.x'
python-version: "3.x"
- name: Install dependencies
run: |
@ -64,8 +67,8 @@ jobs:
- name: git config
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
- name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
@ -74,13 +77,13 @@ jobs:
- name: Generate Extensions API Reference using typedocs
run: |
yarn install
yarn typedocs-extensions-api
yarn install
yarn typedocs-extensions-api
- name: mkdocs deploy master
if: contains(github.ref, 'refs/heads/master')
run: |
mike deploy --push master
mike deploy --push master
- name: Get the release version
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease
@ -90,5 +93,5 @@ jobs:
- name: mkdocs deploy new release
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease
run: |
mike deploy --push --update-aliases ${{ steps.get_version.outputs.VERSION }} latest
mike set-default --push ${{ steps.get_version.outputs.VERSION }}
mike deploy --push --update-aliases ${{ steps.get_version.outputs.VERSION }} latest
mike set-default --push ${{ steps.get_version.outputs.VERSION }}

View File

@ -6,7 +6,7 @@ Lens releases are built by CICD automatically on git tags. The typical release p
1. If doing a patch release checkout the `release/vMAJOR.MINOR` branch for the appropriate `MAJOR`/`MINOR` version and manually `cherry-pick` the PRs required for the patch that were commited to master. If there are any conflicts they must be resolved manually. If necessary, get assistance from the PR authors.
This can be helped (if you have the `gh` CLI installed) by running:
This can be helped (if you have the `gh` CLI and `jq` installed) by running:
```
gh api -XGET "/repos/lensapp/lens/pulls?state=closed&per_page=100" | jq -r '[.[] | select(.milestone.title == "<VERSION>") | select((.merged_at | type) == "string")] | sort_by(.merged_at) | map(.merge_commit_sha) | join(" ")'
```
@ -36,11 +36,12 @@ But you will probably need to verify that all the PRs have correct milestones.
gh api -XGET "/repos/lensapp/lens/pulls?state=closed&per_page=100" | jq -r '[.[] | select(.milestone.title == "<VERSION>") | select((.merged_at | type) == "string")] | sort_by(.merged_at) | map([.title, " (**#", .number, "**) ", .user.html_url] | join ("")) | join("\n")' | pbcopy
```
And if you want to specify just bug fixes then you can add the following to the end of the `[ ... ]` section in the above command (before the `sort_by`) to just have bug fixes. Switch to `== "enhancement"` for enhancements and `all()` with `. != "bug && . != "enhancement"` for maintanence sections.
And if you want to specify just bug fixes then you can add the following to the end of the `[ ... ]` section in the above command (before the `sort_by`) to just have bug fixes. Switch to `== "enhancement"` for enhancements and `all()` instead of `any()` with `. != "bug and . != "enhancement"` for maintanence sections.
```
| select(any(.labels | map(.name)[]; . == "bug"))
```
See notes below for ideas for building changelogs.
1. After the PR is accepted and passes CI (and before merging), go to the same branch and run `make tag-release` (set GIT_REMOTE if necessary). This additionally triggers the azure jobs to build the binaries and put them on S3.
1. If the CI fails at this stage the problem needs to be fixed. Sometimes an azure job fails due to outside service issues (e.g. Apple signing occasionally fails), in which case the specific azure job can be rerun from https://dev.azure.com/lensapp/lensapp/_build. Otherwise changes to the codebase may need to be done and committed to the release branch and pushed to https://github.com/lensapp/lens. CI will run again. As well the release tag needs to be manually set to this new commit. You can do something like:
@ -64,3 +65,31 @@ Other tasks
- announce the release on lens and lens-hq slack channels (release is announced automatically on the community slack lens channel through the above publishing process)
- announce on lens-hq that master is open for PR merges for the next release (for major/minor releases)
- update issues on github (bump those that did not make it into the release to a subsequent release) (for major/minor/patch releases)
### Rough Notes
To get all of the PRs since a certain date you can use something like:
```
gh api -XGET "/repos/lensapp/lens/pulls?state=closed&per_page=100&q=committer-date:>YYYY-MM-DD" > prs.json
```
If there are more than 100 PRs you'll need to rerun for multiple pages:
```
gh api -XGET "/repos/lensapp/lens/pulls?state=closed&per_page=100&page=2&q=committer-date:>YYYY-MM-DD" >> prs.json
```
To get all the bug PRs:
```
jq -r '[.[] | select((.merged_at | type) == "string") | select(any(.labels | map(.name)[]; . == "bug"))] | sort_by(.merged_at) | reverse | map([.title, " (**#", .number, "**) ", .user.html_url] | join ("")) | join("\n")' prs.json
```
To get all the enhancement PRs:
```
jq -r '[.[] | select((.merged_at | type) == "string") | select(any(.labels | map(.name)[]; . == "enhancement"))] | sort_by(.merged_at) | reverse | map([.title, " (**#", .number, "**) ", .user.html_url] | join ("")) | join("\n")' prs.json
```
To get all of the maintenance PRs:
```
jq -r '[.[] | select((.merged_at | type) == "string") | select(all(.labels | map(.name)[]; . != "bug" and . != "enhancement" and . != "skip-changelog"))] | sort_by(.merged_at) | reverse | map([.title, " (**#", .number, "**) ", .user.html_url] | join ("")) | join("\n")' prs.json
```

View File

@ -233,16 +233,16 @@ const commonPageTests: CommonPageTest[] = [{
},
},
{
drawerId: "apps",
drawerId: "helm",
pages: [
{
name: "Charts",
href: "/apps/charts",
href: "/helm/charts",
expectedSelector: "div.HelmCharts input",
},
{
name: "Releases",
href: "/apps/releases",
href: "/helm/releases",
expectedSelector: "h5.title",
expectedText: "Releases",
},

View File

@ -3,7 +3,7 @@
"productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens",
"version": "5.3.0",
"version": "5.4.0-beta.1",
"main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors",
"license": "MIT",
@ -194,8 +194,8 @@
"@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.1",
"@ogre-tools/injectable": "3.2.0",
"@ogre-tools/injectable-react": "3.2.0",
"@ogre-tools/injectable": "3.2.1",
"@ogre-tools/injectable-react": "3.2.1",
"@sentry/electron": "^2.5.4",
"@sentry/integrations": "^6.15.0",
"@types/circular-dependency-plugin": "5.0.4",
@ -302,7 +302,7 @@
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.2",
"@types/react-select": "3.1.2",
"@types/react-table": "^7.7.8",
"@types/react-table": "^7.7.9",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@types/readable-stream": "^2.3.12",

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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { comparer, computed } from "mobx";
import hostedClusterInjectable from "./hosted-cluster.injectable";
const allowedResourcesInjectable = getInjectable({
instantiate: (di) => {
const cluster = di.inject(hostedClusterInjectable);
return computed(() => new Set(cluster.allowedResources), {
// This needs to be here so that during refresh changes are only propogated when necessary
equals: (cur, prev) => comparer.structural(cur, prev),
});
},
lifecycle: lifecycleEnum.singleton,
});
export default allowedResourcesInjectable;

View File

@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { getHostedClusterId } from "../../utils";
import clusterStoreInjectable from "../cluster-store.injectable";
import { getHostedClusterId } from "../utils";
import clusterStoreInjectable from "./cluster-store.injectable";
const hostedClusterInjectable = getInjectable({
instantiate: (di) => {

View File

@ -21,7 +21,7 @@ export abstract class ItemStore<Item extends ItemObject> {
@observable isLoading = false;
@observable isLoaded = false;
@observable items = observable.array<Item>([], { deep: false });
@observable selectedItemsIds = observable.map<string, boolean>();
@observable selectedItemsIds = observable.set<string>();
constructor() {
makeObservable(this);
@ -29,7 +29,11 @@ export abstract class ItemStore<Item extends ItemObject> {
}
@computed get selectedItems(): Item[] {
return this.items.filter(item => this.selectedItemsIds.get(item.getId()));
return this.pickOnlySelected(this.items);
}
public pickOnlySelected(items: Item[]): Item[] {
return items.filter(item => this.selectedItemsIds.has(item.getId()));
}
public getItems(): Item[] {
@ -152,12 +156,12 @@ export abstract class ItemStore<Item extends ItemObject> {
}
isSelected(item: Item) {
return !!this.selectedItemsIds.get(item.getId());
return this.selectedItemsIds.has(item.getId());
}
@action
select(item: Item) {
this.selectedItemsIds.set(item.getId(), true);
this.selectedItemsIds.add(item.getId());
}
@action
@ -207,6 +211,8 @@ export abstract class ItemStore<Item extends ItemObject> {
async removeSelectedItems?(): Promise<any>;
async removeItems?(items: Item[]): Promise<void>;
* [Symbol.iterator]() {
yield* this.items;
}

View File

@ -0,0 +1,131 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Node } from "../endpoints";
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
describe("Nodes tests", () => {
describe("getRoleLabels()", () => {
it("should return empty string if labels is not present", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
},
});
expect(node.getRoleLabels()).toBe("");
});
it("should return empty string if labels is empty object", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {},
},
});
expect(node.getRoleLabels()).toBe("");
});
it("should return rest of keys with substring node-role.kubernetes.io/", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {
"node-role.kubernetes.io/foobar": "bat",
"hellonode-role.kubernetes.io/foobar1": "bat",
},
},
});
expect(node.getRoleLabels()).toBe("foobar, foobar1");
});
it("should return rest of keys with substring node-role.kubernetes.io/ after last /", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {
"node-role.kubernetes.io/foobar": "bat",
"hellonode-role.kubernetes.io//////foobar1": "bat",
},
},
});
expect(node.getRoleLabels()).toBe("foobar, foobar1");
});
it("should return value of label kubernetes.io/role if present", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {
"kubernetes.io/role": "master",
},
},
});
expect(node.getRoleLabels()).toBe("master");
});
it("should return value of label node.kubernetes.io/role if present", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {
"node.kubernetes.io/role": "master",
},
},
});
expect(node.getRoleLabels()).toBe("master");
});
it("all sources should be joined together", () => {
const node = new Node({
apiVersion: "foo",
kind: "Node",
metadata: {
name: "bar",
resourceVersion: "1",
uid: "bat",
labels: {
"aksjhdkjahsdnode-role.kubernetes.io/foobar": "bat",
"kubernetes.io/role": "master",
"node.kubernetes.io/role": "master-v2-max",
},
},
});
expect(node.getRoleLabels()).toBe("foobar, master, master-v2-max");
});
});
});

View File

@ -7,7 +7,7 @@ import yaml from "js-yaml";
import { formatDuration } from "../../utils";
import capitalize from "lodash/capitalize";
import { apiBase } from "../index";
import { helmChartStore } from "../../../renderer/components/+apps-helm-charts/helm-chart.store";
import { helmChartStore } from "../../../renderer/components/+helm-charts/helm-chart.store";
import type { ItemObject } from "../../item.store";
import { KubeObject } from "../kube-object";
import type { JsonApiData } from "../json-api";

View File

@ -140,6 +140,12 @@ function* getTrueConditionTypes(conditions: IterableIterator<NodeCondition> | It
}
}
/**
* This regex is used in the `getRoleLabels()` method bellow, but placed here
* as factoring out regexes is best practice.
*/
const nodeRoleLabelKeyMatcher = /^.*node-role.kubernetes.io\/+(?<role>.+)$/;
export class Node extends KubeObject {
static kind = "Node";
static namespaced = false;
@ -165,17 +171,29 @@ export class Node extends KubeObject {
return this.spec.taints || [];
}
getRoleLabels() {
if (!this.metadata?.labels || typeof this.metadata.labels !== "object") {
getRoleLabels(): string {
const { labels } = this.metadata;
if (!labels || typeof labels !== "object") {
return "";
}
const roleLabels = Object.keys(this.metadata.labels)
.filter(key => key.includes("node-role.kubernetes.io"))
.map(key => key.match(/([^/]+$)/)[0]); // all after last slash
const roleLabels: string[] = [];
if (this.metadata.labels["kubernetes.io/role"] != undefined) {
roleLabels.push(this.metadata.labels["kubernetes.io/role"]);
for (const labelKey of Object.keys(labels)) {
const match = nodeRoleLabelKeyMatcher.exec(labelKey);
if (match) {
roleLabels.push(match.groups.role);
}
}
if (typeof labels["kubernetes.io/role"] === "string") {
roleLabels.push(labels["kubernetes.io/role"]);
}
if (typeof labels["node.kubernetes.io/role"] === "string") {
roleLabels.push(labels["node.kubernetes.io/role"]);
}
return roleLabels.join(", ");

View File

@ -16,12 +16,9 @@ import { KubeObjectConstructor, KubeObject, KubeStatus } from "./kube-object";
import byline from "byline";
import type { IKubeWatchEvent } from "./kube-watch-event";
import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api";
import { noop } from "../utils";
import { noop, WrappedAbortController } from "../utils";
import type { RequestInit } from "node-fetch";
// BUG: https://github.com/mysticatea/abort-controller/pull/22
// eslint-disable-next-line import/no-named-as-default
import AbortController from "abort-controller";
import type AbortController from "abort-controller";
import { Agent, AgentOptions } from "https";
import type { Patch } from "rfc6902";
@ -582,14 +579,7 @@ export class KubeApi<T extends KubeObject> {
const { watchId = `${this.kind.toLowerCase()}-${this.watchId++}` } = opts;
// Create AbortController for this request
const abortController = new AbortController();
// If caller aborts, abort using request's abortController
if (opts.abortController) {
opts.abortController.signal.addEventListener("abort", () => {
abortController.abort();
});
}
const abortController = new WrappedAbortController(opts.abortController);
abortController.signal.addEventListener("abort", () => {
logger.info(`[KUBE-API] watch (${watchId}) aborted ${watchUrl}`);
@ -687,7 +677,6 @@ export class KubeApi<T extends KubeObject> {
protected modifyWatchEvent(event: IKubeWatchEvent<KubeJsonApiData>) {
if (event.type === "ERROR") {
return;
}
ensureObjectSelfLink(this, event.object);

View File

@ -6,7 +6,7 @@
import type { ClusterContext } from "./cluster-context";
import { action, computed, makeObservable, observable, reaction, when } from "mobx";
import { autoBind, noop, rejectPromiseBy } from "../utils";
import { autoBind, Disposer, noop, rejectPromiseBy } from "../utils";
import { KubeObject, KubeStatus } from "./kube-object";
import type { IKubeWatchEvent } from "./kube-watch-event";
import { ItemStore } from "../item.store";
@ -369,6 +369,10 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
return Promise.all(this.selectedItems.map(this.remove));
}
async removeItems(items: T[]) {
await Promise.all(items.map(this.remove));
}
// collect items from watch-api events to avoid UI blowing up with huge streams of data
protected eventsBuffer = observable.array<IKubeWatchEvent<KubeJsonApiData>>([], { deep: false });
@ -378,7 +382,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
});
}
subscribe({ onLoadFailure, abortController = new AbortController() }: KubeObjectStoreSubscribeParams = {}) {
subscribe({ onLoadFailure, abortController = new AbortController() }: KubeObjectStoreSubscribeParams = {}): Disposer {
if (this.api.isNamespaced) {
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
.then(() => {

View File

@ -5,10 +5,10 @@
import type { RouteProps } from "react-router";
import { buildURL } from "../utils/buildUrl";
import { appsRoute } from "./apps";
import { helmRoute } from "./helm";
export const helmChartsRoute: RouteProps = {
path: `${appsRoute.path}/charts/:repo?/:chartName?`,
path: `${helmRoute.path}/charts/:repo?/:chartName?`,
};
export interface HelmChartsRouteParams {

View File

@ -6,8 +6,8 @@
import type { RouteProps } from "react-router";
import { buildURL } from "../utils/buildUrl";
export const appsRoute: RouteProps = {
path: "/apps",
export const helmRoute: RouteProps = {
path: "/helm",
};
export const appsURL = buildURL(appsRoute.path);
export const helmURL = buildURL(helmRoute.path);

View File

@ -4,7 +4,6 @@
*/
export * from "./add-cluster";
export * from "./apps";
export * from "./catalog";
export * from "./cluster-view";
export * from "./cluster";
@ -16,6 +15,7 @@ export * from "./entity-settings";
export * from "./events";
export * from "./extensions";
export * from "./helm-charts";
export * from "./helm";
export * from "./hpa";
export * from "./ingresses";
export * from "./limit-ranges";

View File

@ -5,10 +5,10 @@
import type { RouteProps } from "react-router";
import { buildURL } from "../utils/buildUrl";
import { appsRoute } from "./apps";
import { helmRoute } from "./helm";
export const releaseRoute: RouteProps = {
path: `${appsRoute.path}/releases/:namespace?/:name?`,
path: `${helmRoute.path}/releases/:namespace?/:name?`,
};
export interface ReleaseRouteParams {

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import AbortController from "abort-controller";
export class WrappedAbortController extends AbortController {
constructor(parent?: AbortController) {
super();
parent?.signal.addEventListener("abort", () => {
this.abort();
});
}
}

View File

@ -1,25 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { ClusterStore } from "../cluster-store/cluster-store";
import type { KubeResource } from "../rbac";
import { getHostedClusterId } from "./cluster-id-url-parsing";
export function isAllowedResource(resource: KubeResource | KubeResource[]) {
const resources = [resource].flat();
const cluster = ClusterStore.getInstance().getById(getHostedClusterId());
if (!cluster?.allowedResources) {
return false;
}
if (resources.length === 0) {
return true;
}
const allowedResources = new Set(cluster.allowedResources);
return resources.every(resource => allowedResources.has(resource));
}

View File

@ -10,6 +10,7 @@ export function noop<T extends any[]>(...args: T): void {
return void args;
}
export * from "./abort-controller";
export * from "./app-version";
export * from "./autobind";
export * from "./camelCase";

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import allowedResourcesInjectable from "../cluster-store/allowed-resources.injectable";
import type { KubeResource } from "../rbac";
export type IsAllowedResource = (resource: KubeResource) => boolean;
// TODO: This injectable obscures MobX de-referencing. Make it more apparent in usage.
const isAllowedResourceInjectable = getInjectable({
instantiate: (di) => {
const allowedResources = di.inject(allowedResourcesInjectable);
return (resource: KubeResource) => allowedResources.get().has(resource);
},
lifecycle: lifecycleEnum.singleton,
});
export default isAllowedResourceInjectable;

View File

@ -3,7 +3,14 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export { isAllowedResource } from "../../common/utils/allowed-resource";
/**
* @deprecated This function never works
* @returns false
*/
export function isAllowedResource(...args: any[]) {
return Boolean(void args);
}
export { ResourceStack } from "../../common/k8s/resource-stack";
export { apiManager } from "../../common/k8s-api/api-manager";
export { KubeApi, forCluster, forRemoteCluster } from "../../common/k8s-api/kube-api";

View File

@ -2,8 +2,19 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeResource } from "../../common/rbac";
import isAllowedResourceInjectable from "../../common/utils/is-allowed-resource.injectable";
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import { castArray } from "lodash/fp";
export function isAllowedResource(resource: KubeResource | KubeResource[]) {
const _isAllowedResource = asLegacyGlobalFunctionForExtensionApi(isAllowedResourceInjectable);
const resources = castArray(resource);
return resources.every(x => _isAllowedResource(x));
}
export { isAllowedResource } from "../../common/utils/allowed-resource";
export { ResourceStack } from "../../common/k8s/resource-stack";
export { apiManager } from "../../common/k8s-api/api-manager";
export { KubeObjectStore } from "../../common/k8s-api/kube-object.store";

View File

@ -179,12 +179,13 @@ export async function getHistory(name: string, namespace: string, kubeconfigPath
}
export async function rollback(name: string, namespace: string, revision: number, kubeconfigPath: string) {
return JSON.parse(await execHelm([
await execHelm([
"rollback",
name,
`${revision}`,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
]));
]);
}
async function getResources(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {

View File

@ -102,9 +102,7 @@ class HelmService {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Rollback release");
const output = rollback(releaseName, namespace, revision, proxyKubeconfig);
return { message: output };
await rollback(releaseName, namespace, revision, proxyKubeconfig);
}
}

View File

@ -71,9 +71,9 @@ export class HelmApiRoute {
const { cluster, params, payload, response } = request;
try {
const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
respondJson(response, result);
response.end();
} catch (error) {
logger.debug(error);
respondText(response, error?.toString() || "Error rolling back chart", 422);

View File

@ -94,6 +94,7 @@ export class NodeShellSession extends ShellSession {
tolerations: [{
operator: "Exists",
}],
priorityClassName: "system-node-critical",
containers: [{
name: "shell",
image: this.cluster.nodeShellImage,

View File

@ -5,7 +5,7 @@
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ClusterFrameContext } from "./cluster-frame-context";
import namespaceStoreInjectable from "../components/+namespaces/namespace-store/namespace-store.injectable";
import hostedClusterInjectable from "../../common/cluster-store/hosted-cluster/hosted-cluster.injectable";
import hostedClusterInjectable from "../../common/cluster-store/hosted-cluster.injectable";
const clusterFrameContextInjectable = getInjectable({
instantiate: (di) => {

View File

@ -1,13 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { updateRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
const updateReleaseInjectable = getInjectable({
instantiate: () => updateRelease,
lifecycle: lifecycleEnum.singleton,
});
export default updateReleaseInjectable;

View File

@ -1,38 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { observer } from "mobx-react";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { HelmCharts } from "../+apps-helm-charts";
import { HelmReleases } from "../+apps-releases";
import { helmChartsURL, helmChartsRoute, releaseURL, releaseRoute } from "../../../common/routes";
@observer
export class Apps extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
return [
{
title: "Charts",
component: HelmCharts,
url: helmChartsURL(),
routePath: helmChartsRoute.path.toString(),
},
{
title: "Releases",
component: HelmReleases,
url: releaseURL(),
routePath: releaseRoute.path.toString(),
},
];
}
render() {
return (
<TabLayout className="Apps" tabs={Apps.tabRoutes}/>
);
}
}

View File

@ -1,6 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./apps";

View File

@ -43,6 +43,8 @@
}
.pinIcon {
--color-active: var(--textColorAccent);
transition: none;
opacity: 0;
margin-left: var(--padding);

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import { observer } from "mobx-react";
import React from "react";
import { clusterRoute, clusterURL } from "../../../common/routes";
import type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable";
import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable";
import { isActiveRoute } from "../../navigation";
import { Icon } from "../icon";
import { SidebarItem } from "../layout/sidebar-item";
export interface ClusterSidebarItemProps {}
interface Dependencies {
isAllowedResource: IsAllowedResource;
}
const NonInjectedClusterSidebarItem = observer(({ isAllowedResource }: Dependencies & ClusterSidebarItemProps) => (
<SidebarItem
id="cluster"
text="Cluster"
isActive={isActiveRoute(clusterRoute)}
isHidden={!isAllowedResource("nodes")}
url={clusterURL()}
icon={<Icon svg="kube"/>}
/>
));
export const ClusterSidebarItem = withInjectables<Dependencies, ClusterSidebarItemProps>(NonInjectedClusterSidebarItem, {
getProps: (di, props) => ({
isAllowedResource: di.inject(isAllowedResourceInjectable),
...props,
}),
});

View File

@ -9,19 +9,23 @@
}
.quota-entries {
margin: -$margin * 0.5;
display: flex;
gap: 8px;
margin-left: -1px;
flex-wrap: wrap;
&:empty {
display: none;
}
.quota {
--flex-gap: #{$padding};
border: 1px solid var(--halfGray);
border-radius: $radius;
margin: $margin * 0.5;
padding: $padding * 0.5 $padding;
transition: all 150ms ease;
display: flex;
align-items: center;
gap: 8px;
&:hover {
box-shadow: inset 0 0 0 1px var(--borderColor);

View File

@ -1,6 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./config";

View File

@ -2,22 +2,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 { observer } from "mobx-react";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { ConfigMaps } from "../+config-maps";
import { Secrets } from "../+config-secrets";
import { ResourceQuotas } from "../+config-resource-quotas";
import { PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import { HorizontalPodAutoscalers } from "../+config-autoscalers";
import { isAllowedResource } from "../../../common/utils/allowed-resource";
import { LimitRanges } from "../+config-limit-ranges";
import { ConfigMaps } from "../+config-maps";
import { PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
import { ResourceQuotas } from "../+config-resource-quotas";
import { Secrets } from "../+config-secrets";
import isAllowedResourceInjectable, { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable";
import type { TabLayoutRoute } from "../layout/tab-layout";
import * as routes from "../../../common/routes";
@observer
export class Config extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
interface Dependencies {
isAllowedResource: IsAllowedResource;
}
function getRouteTabs({ isAllowedResource }: Dependencies) {
return computed(() => {
const tabs: TabLayoutRoute[] = [];
if (isAllowedResource("configmaps")) {
@ -75,11 +77,14 @@ export class Config extends React.Component {
}
return tabs;
}
render() {
return (
<TabLayout className="Config" tabs={Config.tabRoutes}/>
);
}
});
}
const configRoutesInjectable = getInjectable({
instantiate: (di) => getRouteTabs({
isAllowedResource: di.inject(isAllowedResourceInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default configRoutesInjectable;

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { observer } from "mobx-react";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import type { IComputedValue } from "mobx";
import { withInjectables } from "@ogre-tools/injectable-react";
import configRoutesInjectable from "./route-tabs.injectable";
export interface ConfigRouteProps {}
interface Dependencies {
routes: IComputedValue<TabLayoutRoute[]>;
}
const NonInjectedConfigRoute = observer(({ routes }: Dependencies & ConfigRouteProps) => (
<TabLayout
className="Config"
tabs={routes.get()}
/>
));
export const ConfigRoute = withInjectables<Dependencies, ConfigRouteProps>(NonInjectedConfigRoute, {
getProps: (di, props) => ({
routes: di.inject(configRoutesInjectable),
...props,
}),
});

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 { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import { configRoute, configURL } from "../../../common/routes";
import { isActiveRoute } from "../../navigation";
import { Icon } from "../icon";
import { SidebarItem } from "../layout/sidebar-item";
import type { TabLayoutRoute } from "../layout/tab-layout";
import { TabRoutesSidebarItems } from "../layout/tab-routes-sidebar-items";
import configRoutesInjectable from "./route-tabs.injectable";
export interface ConfigSidebarItemProps {}
interface Dependencies {
routes: IComputedValue<TabLayoutRoute[]>;
}
const NonInjectedConfigSidebarItem = observer(({ routes }: Dependencies & ConfigSidebarItemProps) => {
const tabRoutes = routes.get();
return (
<SidebarItem
id="config"
text="Configuration"
isActive={isActiveRoute(configRoute)}
isHidden={tabRoutes.length == 0}
url={configURL()}
icon={<Icon material="list"/>}
>
<TabRoutesSidebarItems routes={tabRoutes} />
</SidebarItem>
);
});
export const ConfigSidebarItem = withInjectables<Dependencies, ConfigSidebarItemProps>(NonInjectedConfigSidebarItem, {
getProps: (di, props) => ({
routes: di.inject(configRoutesInjectable),
...props,
}),
});

View File

@ -1,38 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { CrdList } from "./crd-list";
import { CrdResources } from "./crd-resources";
import { crdURL, crdDefinitionsRoute, crdResourcesRoute } from "../../../common/routes";
@observer
export class CustomResources extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
return [
{
title: "Definitions",
component: CustomResources,
url: crdURL(),
routePath: String(crdDefinitionsRoute.path),
},
];
}
render() {
return (
<TabLayout>
<Switch>
<Route component={CrdList} {...crdDefinitionsRoute} exact/>
<Route component={CrdResources} {...crdResourcesRoute}/>
<Redirect to={crdURL()}/>
</Switch>
</TabLayout>
);
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed, IComputedValue } from "mobx";
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints";
import { getOrInsert } from "../../utils";
import customResourceDefinitionsInjectable from "./custom-resources.injectable";
interface Dependencies {
definitions: IComputedValue<CustomResourceDefinition[]>;
}
function getGroupedCustomResourceDefinitions({ definitions }: Dependencies) {
return computed(() => {
const groups = new Map<string, CustomResourceDefinition[]>();
for (const crd of definitions.get()) {
getOrInsert(groups, crd.getGroup(), []).push(crd);
}
return groups;
});
}
const groupedCustomResourceDefinitionsInjectable = getInjectable({
instantiate: (di) => getGroupedCustomResourceDefinitions({
definitions: di.inject(customResourceDefinitionsInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default groupedCustomResourceDefinitionsInjectable;

View File

@ -0,0 +1,65 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed, IComputedValue } from "mobx";
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints";
import { crdURL, crdDefinitionsRoute } from "../../../common/routes";
import type { TabLayoutRoute } from "../layout/tab-layout";
import { CrdList } from "./crd-list";
import { CrdResources } from "./crd-resources";
import groupedCustomResourceDefinitionsInjectable from "./grouped-custom-resources.injectable";
export interface CustomResourceTabLayoutRoute extends TabLayoutRoute {
id: string;
}
export interface CustomResourceGroupTabLayoutRoute extends CustomResourceTabLayoutRoute {
subRoutes?: CustomResourceTabLayoutRoute[];
}
interface Dependencies {
customResourcesDefinitions: IComputedValue<Map<string, CustomResourceDefinition[]>>;
}
function getRouteTabs({ customResourcesDefinitions }: Dependencies) {
return computed(() => {
const tabs: CustomResourceGroupTabLayoutRoute[] = [
{
id: "definitions",
title: "Definitions",
component: CrdList,
url: crdURL(),
routePath: String(crdDefinitionsRoute.path),
exact: true,
},
];
for (const [group, definitions] of customResourcesDefinitions.get()) {
tabs.push({
id: `crd-group:${group}`,
title: group,
routePath: crdURL({ query: { groups: group }}),
component: CrdResources,
subRoutes: definitions.map(crd => ({
id: `crd-resource:${crd.getResourceApiBase()}`,
title: crd.getResourceKind(),
routePath: crd.getResourceUrl(),
component: CrdResources,
})),
});
}
return tabs;
});
}
const customResourcesRouteTabsInjectable = getInjectable({
instantiate: (di) => getRouteTabs({
customResourcesDefinitions: di.inject(groupedCustomResourceDefinitionsInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default customResourcesRouteTabsInjectable;

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { observer } from "mobx-react";
import { Redirect, Route, Switch } from "react-router";
import { TabLayout } from "../layout/tab-layout";
import { crdURL } from "../../../common/routes";
import type { IComputedValue } from "mobx";
import type { CustomResourceGroupTabLayoutRoute } from "./route-tabs.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import customResourcesRouteTabsInjectable from "./route-tabs.injectable";
interface Dependencies {
routes: IComputedValue<CustomResourceGroupTabLayoutRoute[]>;
}
const NonInjectedCustomResourcesRoute = observer(({ routes }: Dependencies) => (
<TabLayout>
<Switch>
{
routes.get().map(({ id, component, routePath, exact }) => (
<Route
key={id}
component={component}
path={routePath}
exact={exact}
/>
))
}
<Redirect to={crdURL()}/>
</Switch>
</TabLayout>
));
export const CustomResourcesRoute = withInjectables<Dependencies>(NonInjectedCustomResourcesRoute, {
getProps: (di, props) => ({
routes: di.inject(customResourcesRouteTabsInjectable),
...props,
}),
});

View File

@ -0,0 +1,76 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React, { useEffect } from "react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import customResourcesRouteTabsInjectable, { type CustomResourceGroupTabLayoutRoute } from "./route-tabs.injectable";
import type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable";
import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable";
import { crdURL, crdRoute } from "../../../common/routes";
import { isActiveRoute } from "../../navigation";
import { Icon } from "../icon";
import { SidebarItem } from "../layout/sidebar-item";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import type { SubscribeStores } from "../../kube-watch-api/kube-watch-api";
import { crdStore } from "./crd.store";
import { Spinner } from "../spinner";
export interface CustomResourcesSidebarItemProps {}
interface Dependencies {
routes: IComputedValue<CustomResourceGroupTabLayoutRoute[]>;
isAllowedResource: IsAllowedResource;
subscribeStores: SubscribeStores;
}
const NonInjectedCustomResourcesSidebarItem = observer(({ routes, isAllowedResource, subscribeStores }: Dependencies & CustomResourcesSidebarItemProps) => {
useEffect(() => subscribeStores([
crdStore,
]), []);
return (
<SidebarItem
id="custom-resources"
text="Custom Resources"
url={crdURL()}
isActive={isActiveRoute(crdRoute)}
isHidden={!isAllowedResource("customresourcedefinitions")}
icon={<Icon material="extension"/>}
>
{routes.get().map((route) => (
<SidebarItem
key={route.id}
id={route.id}
text={route.title}
url={route.routePath}
>
{route.subRoutes?.map((subRoute) => (
<SidebarItem
key={subRoute.id}
id={subRoute.id}
url={subRoute.routePath}
text={subRoute.title}
/>
))}
</SidebarItem>
))}
{crdStore.isLoading && (
<div className="flex justify-center">
<Spinner />
</div>
)}
</SidebarItem>
);
});
export const CustomResourcesSidebarItem = withInjectables<Dependencies, CustomResourcesSidebarItemProps>(NonInjectedCustomResourcesSidebarItem, {
getProps: (di, props) => ({
routes: di.inject(customResourcesRouteTabsInjectable),
isAllowedResource: di.inject(isAllowedResourceInjectable),
subscribeStores: di.inject(subscribeStoresInjectable),
...props,
}),
});

View File

@ -124,7 +124,7 @@ export class Events extends React.Component<Props> {
};
render() {
const { store, visibleItems } = this;
const { store } = this;
const { compact, compactLimit, className, ...layoutProps } = this.props;
const events = (
@ -137,7 +137,7 @@ export class Events extends React.Component<Props> {
renderHeaderTitle="Events"
customizeHeader={this.customizeHeader}
isSelectable={false}
items={visibleItems}
getItems={() => this.visibleItems}
virtual={!compact}
tableProps={{
sortSyncWithUrl: false,

View File

@ -3,5 +3,4 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./events";
export * from "./event-details";

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import { observer } from "mobx-react";
import React from "react";
import { eventRoute, eventsURL } from "../../../common/routes";
import type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable";
import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable";
import { isActiveRoute } from "../../navigation";
import { Icon } from "../icon";
import { SidebarItem } from "../layout/sidebar-item";
export interface EventsSidebarItemProps {}
interface Dependencies {
isAllowedResource: IsAllowedResource;
}
const NonInjectedEventsSidebarItem = observer(({ isAllowedResource }: Dependencies & EventsSidebarItemProps) => (
<SidebarItem
id="events"
text="Events"
isActive={isActiveRoute(eventRoute)}
isHidden={!isAllowedResource("events")}
url={eventsURL()}
icon={<Icon material="access_time"/>}
/>
));
export const EventsSidebarItem = withInjectables<Dependencies, EventsSidebarItemProps>(NonInjectedEventsSidebarItem, {
getProps: (di, props) => ({
isAllowedResource: di.inject(isAllowedResourceInjectable),
...props,
}),
});

View File

@ -12,7 +12,7 @@ import { Button } from "../../button";
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import React from "react";
import fse from "fs-extra";
import { remove as removeDir } from "fs-extra";
import { shell } from "electron";
import type { InstallRequestValidated } from "./create-temp-files-and-validate/create-temp-files-and-validate";
import type { InstallRequest } from "./install-request";
@ -80,17 +80,12 @@ export const attemptInstall =
}
const extensionFolder = getExtensionDestFolder(name);
const folderExists = await fse.pathExists(extensionFolder);
const installedExtension = extensionLoader.getExtension(validatedRequest.id);
if (!folderExists) {
// install extension if not yet exists
await unpackExtension(validatedRequest, dispose);
} else {
const {
manifest: { version: oldVersion },
} = extensionLoader.getExtension(validatedRequest.id);
if (installedExtension) {
const { version: oldVersion } = installedExtension.manifest;
// otherwise confirmation required (re-install / update)
// confirm to uninstall old version before installing new version
const removeNotification = Notifications.info(
<div className="InstallingExtensionNotification flex gaps align-center">
<div className="flex column gaps">
@ -130,5 +125,11 @@ export const attemptInstall =
onClose: dispose,
},
);
} else {
// clean up old data if still around
await removeDir(extensionFolder);
// install extension if not yet exists
await unpackExtension(validatedRequest, dispose);
}
};

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -10,7 +10,7 @@ import releaseInjectable from "./release.injectable";
const releaseDetailsInjectable = getInjectable({
instantiate: (di) =>
asyncComputed(async () => {
const release = di.inject(releaseInjectable).value.get();
const release = di.inject(releaseInjectable).get();
return await getRelease(release.name, release.namespace);
}),

View File

@ -7,7 +7,7 @@ import "./release-details.scss";
import React, { Component } from "react";
import groupBy from "lodash/groupBy";
import { computed, makeObservable, observable } from "mobx";
import { computed, IComputedValue, makeObservable, observable } from "mobx";
import { Link } from "react-router-dom";
import kebabCase from "lodash/kebabCase";
import type { HelmRelease, IReleaseDetails, IReleaseUpdateDetails, IReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
@ -39,7 +39,7 @@ interface Props {
}
interface Dependencies {
release: IAsyncComputed<HelmRelease>
release: IComputedValue<HelmRelease>
releaseDetails: IAsyncComputed<IReleaseDetails>
releaseValues: IAsyncComputed<string>
updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise<IReleaseUpdateDetails>
@ -59,7 +59,7 @@ class NonInjectedReleaseDetails extends Component<Props & Dependencies> {
}
@computed get release() {
return this.props.release.value.get();
return this.props.release.get();
}
@computed get details() {

View File

@ -12,7 +12,13 @@ import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-sho
const releaseValuesInjectable = getInjectable({
instantiate: (di) =>
asyncComputed(async () => {
const release = di.inject(releaseInjectable).value.get();
const release = di.inject(releaseInjectable).get();
// TODO: Figure out way to get rid of defensive code
if (!release) {
return "";
}
const userSuppliedValuesAreShown = di.inject(userSuppliedValuesAreShownInjectable).value;
try {

View File

@ -6,14 +6,14 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { matches } from "lodash/fp";
import releasesInjectable from "../releases.injectable";
import releaseRouteParametersInjectable from "./release-route-parameters.injectable";
import { asyncComputed } from "@ogre-tools/injectable-react";
import { computed } from "mobx";
const releaseInjectable = getInjectable({
instantiate: (di) => {
const releases = di.inject(releasesInjectable);
const releaseRouteParameters = di.inject(releaseRouteParametersInjectable);
return asyncComputed(async () => {
return computed(() => {
const { name, namespace } = releaseRouteParameters.get();
if (!name || !namespace) {

View File

@ -109,11 +109,23 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
item.toggle();
},
isSelectedAll: () =>
releases.get().every((release) => release.isSelected),
isSelectedAll: (visibleItems: RemovableHelmRelease[]) => (
visibleItems.length > 0
&& visibleItems.every((release) => release.isSelected)
),
toggleSelectionAll: () => {
releases.get().forEach((release) => release.toggle());
toggleSelectionAll: (visibleItems: RemovableHelmRelease[]) => {
let selected = false;
if (!legacyReleaseStore.isSelectedAll(visibleItems)) {
selected = true;
}
visibleItems.forEach((release) => {
if (release.isSelected !== selected) {
release.toggle();
}
});
},
isSelected: (item) => item.isSelected,
@ -122,10 +134,12 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
return releases.get().filter((release) => release.isSelected);
},
removeSelectedItems() {
return Promise.all(
releases.get().filter((release) => release.isSelected).map((release) => release.delete()),
);
pickOnlySelected: (releases) => {
return releases.filter(release => release.isSelected);
},
removeItems: async (releases) => {
await Promise.all(releases.map(release => release.delete()));
},
} as ItemStore<RemovableHelmRelease>;
@ -197,7 +211,7 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
})}
onDetails={this.onDetails}
/>
<ReleaseDetails
hideDetails={this.hideDetails}
/>

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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import {
IReleaseUpdatePayload,
updateRelease,
} from "../../../../common/k8s-api/endpoints/helm-releases.api";
import releasesInjectable from "../releases.injectable";
const updateReleaseInjectable = getInjectable({
instantiate: (di) => {
const releases = di.inject(releasesInjectable);
return async (
name: string,
namespace: string,
payload: IReleaseUpdatePayload,
) => {
const result = await updateRelease(name, namespace, payload);
releases.invalidate();
return result;
};
},
lifecycle: lifecycleEnum.singleton,
});
export default updateReleaseInjectable;

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import type { TabLayoutRoute } from "../layout/tab-layout";
import { HelmCharts } from "../+helm-charts";
import { HelmReleases } from "../+helm-releases";
import { helmChartsURL, helmChartsRoute, releaseURL, releaseRoute } from "../../../common/routes";
function getRouteTabs() {
return computed((): TabLayoutRoute[] => [
{
title: "Charts",
component: HelmCharts,
url: helmChartsURL(),
routePath: helmChartsRoute.path.toString(),
},
{
title: "Releases",
component: HelmReleases,
url: releaseURL(),
routePath: releaseRoute.path.toString(),
},
]);
}
const helmRoutesInjectable = getInjectable({
instantiate: () => getRouteTabs(),
lifecycle: lifecycleEnum.singleton,
});
export default helmRoutesInjectable;

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { observer } from "mobx-react";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import helmRoutesInjectable from "./route-tabs.injectable";
export interface HelmRouteProps {}
interface Dependencies {
routes: IComputedValue<TabLayoutRoute[]>;
}
const NonInjectedHelmRoute = observer(({ routes }: Dependencies & HelmRouteProps) => (
<TabLayout
className="Apps"
tabs={routes.get()}
/>
));
export const HelmRoute = withInjectables<Dependencies, HelmRouteProps>(NonInjectedHelmRoute, {
getProps: (di, props) => ({
routes: di.inject(helmRoutesInjectable),
...props,
}),
});

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import { isActiveRoute } from "../../navigation";
import { Icon } from "../icon";
import { SidebarItem } from "../layout/sidebar-item";
import type { TabLayoutRoute } from "../layout/tab-layout";
import { TabRoutesSidebarItems } from "../layout/tab-routes-sidebar-items";
import { helmRoute, helmURL } from "../../../common/routes";
import networkRouteTabsInjectable from "./route-tabs.injectable";
export interface HelmSidebarItemProps {}
interface Dependencies {
routes: IComputedValue<TabLayoutRoute[]>;
}
const NonInjectedHelmSidebarItem = observer(({ routes }: Dependencies & HelmSidebarItemProps) => {
const tabRoutes = routes.get();
return (
<SidebarItem
id="helm"
text="Helm"
isActive={isActiveRoute(helmRoute)}
url={helmURL()}
icon={<Icon material="apps"/>}
>
<TabRoutesSidebarItems routes={tabRoutes} />
</SidebarItem>
);
});
export const HelmSidebarItem = withInjectables<Dependencies, HelmSidebarItemProps>(NonInjectedHelmSidebarItem, {
getProps: (di, props) => ({
routes: di.inject(networkRouteTabsInjectable),
...props,
}),
});

View File

@ -3,6 +3,5 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./namespaces";
export * from "./namespace-details";
export * from "./add-namespace-dialog";

View File

@ -1,96 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./namespaces.scss";
import React from "react";
import { NamespaceStatus } from "../../../common/k8s-api/endpoints";
import { AddNamespaceDialog } from "./add-namespace-dialog";
import { TabLayout } from "../layout/tab-layout";
import { Badge } from "../badge";
import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object-list-layout";
import type { NamespaceStore } from "./namespace-store/namespace.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { NamespacesRouteParams } from "../../../common/routes";
import { withInjectables } from "@ogre-tools/injectable-react";
import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable";
import addNamespaceDialogModelInjectable
from "./add-namespace-dialog-model/add-namespace-dialog-model.injectable";
enum columnId {
name = "name",
labels = "labels",
age = "age",
status = "status",
}
interface Props extends RouteComponentProps<NamespacesRouteParams> {
}
interface Dependencies {
namespaceStore: NamespaceStore
openAddNamespaceDialog: () => void
}
class NonInjectedNamespaces extends React.Component<Props & Dependencies> {
render() {
return (
<TabLayout>
<KubeObjectListLayout
isConfigurable
tableId="namespaces"
className="Namespaces"
store={this.props.namespaceStore}
sortingCallbacks={{
[columnId.name]: ns => ns.getName(),
[columnId.labels]: ns => ns.getLabels(),
[columnId.age]: ns => ns.getTimeDiffFromNow(),
[columnId.status]: ns => ns.getStatus(),
}}
searchFilters={[
item => item.getSearchFields(),
item => item.getStatus(),
]}
renderHeaderTitle="Namespaces"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ className: "warning", showWithColumn: columnId.name },
{ title: "Labels", className: "labels scrollable", sortBy: columnId.labels, id: columnId.labels },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
]}
renderTableContents={item => [
item.getName(),
<KubeObjectStatusIcon key="icon" object={item} />,
item.getLabels().map(label => <Badge scrollable key={label} label={label}/>),
item.getAge(),
{ title: item.getStatus(), className: item.getStatus().toLowerCase() },
]}
addRemoveButtons={{
addTooltip: "Add Namespace",
onAdd: () => this.props.openAddNamespaceDialog(),
}}
customizeTableRowProps={item => ({
disabled: item.getStatus() === NamespaceStatus.TERMINATING,
})}
/>
<AddNamespaceDialog/>
</TabLayout>
);
}
}
export const Namespaces = withInjectables<Dependencies, Props>(
NonInjectedNamespaces,
{
getProps: (di, props) => ({
namespaceStore: di.inject(namespaceStoreInjectable),
openAddNamespaceDialog: di.inject(addNamespaceDialogModelInjectable).open,
...props,
}),
},
);

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./namespaces.scss";
import React from "react";
import { NamespaceStatus } from "../../../common/k8s-api/endpoints";
import { AddNamespaceDialog } from "./add-namespace-dialog";
import { TabLayout } from "../layout/tab-layout";
import { Badge } from "../badge";
import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object-list-layout";
import type { NamespaceStore } from "./namespace-store/namespace.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { NamespacesRouteParams } from "../../../common/routes";
import { withInjectables } from "@ogre-tools/injectable-react";
import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable";
import addNamespaceDialogModelInjectable
from "./add-namespace-dialog-model/add-namespace-dialog-model.injectable";
enum columnId {
name = "name",
labels = "labels",
age = "age",
status = "status",
}
export interface NamespacesRouteProps extends RouteComponentProps<NamespacesRouteParams> {
}
interface Dependencies {
namespaceStore: NamespaceStore
openAddNamespaceDialog: () => void
}
export const NonInjectedNamespacesRoute = ({ namespaceStore, openAddNamespaceDialog }: Dependencies & NamespacesRouteProps) => (
<TabLayout>
<KubeObjectListLayout
isConfigurable
tableId="namespaces"
className="Namespaces"
store={namespaceStore}
sortingCallbacks={{
[columnId.name]: ns => ns.getName(),
[columnId.labels]: ns => ns.getLabels(),
[columnId.age]: ns => ns.getTimeDiffFromNow(),
[columnId.status]: ns => ns.getStatus(),
}}
searchFilters={[
item => item.getSearchFields(),
item => item.getStatus(),
]}
renderHeaderTitle="Namespaces"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ className: "warning", showWithColumn: columnId.name },
{ title: "Labels", className: "labels scrollable", sortBy: columnId.labels, id: columnId.labels },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
]}
renderTableContents={item => [
item.getName(),
<KubeObjectStatusIcon key="icon" object={item} />,
item.getLabels().map(label => <Badge scrollable key={label} label={label}/>),
item.getAge(),
{ title: item.getStatus(), className: item.getStatus().toLowerCase() },
]}
addRemoveButtons={{
addTooltip: "Add Namespace",
onAdd: openAddNamespaceDialog,
}}
customizeTableRowProps={item => ({
disabled: item.getStatus() === NamespaceStatus.TERMINATING,
})}
/>
<AddNamespaceDialog/>
</TabLayout>
);
export const NamespacesRoute = withInjectables<Dependencies, NamespacesRouteProps>(NonInjectedNamespacesRoute, {
getProps: (di, props) => ({
namespaceStore: di.inject(namespaceStoreInjectable),
openAddNamespaceDialog: di.inject(addNamespaceDialogModelInjectable).open,
...props,
}),
});

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import { observer } from "mobx-react";
import React from "react";
import { namespacesRoute, namespacesURL } from "../../../common/routes";
import type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable";
import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable";
import { isActiveRoute } from "../../navigation";
import { Icon } from "../icon";
import { SidebarItem } from "../layout/sidebar-item";
export interface NamespacesSidebarItemProps {}
interface Dependencies {
isAllowedResource: IsAllowedResource;
}
const NonInjectedNamespacesSidebarItem = observer(({ isAllowedResource }: Dependencies & NamespacesSidebarItemProps) => (
<SidebarItem
id="namespaces"
text="Namespaces"
isActive={isActiveRoute(namespacesRoute)}
isHidden={!isAllowedResource("namespaces")}
url={namespacesURL()}
icon={<Icon material="layers"/>}
/>
));
export const NamespacesSidebarItem = withInjectables<Dependencies, NamespacesSidebarItemProps>(NonInjectedNamespacesSidebarItem, {
getProps: (di, props) => ({
isAllowedResource: di.inject(isAllowedResourceInjectable),
...props,
}),
});

View File

@ -1,6 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./network";

View File

@ -2,23 +2,24 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./network.scss";
import React from "react";
import { observer } from "mobx-react";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import type { TabLayoutRoute } from "../layout/tab-layout";
import { Services } from "../+network-services";
import { Endpoints } from "../+network-endpoints";
import { Ingresses } from "../+network-ingresses";
import { NetworkPolicies } from "../+network-policies";
import { PortForwards } from "../+network-port-forwards";
import { isAllowedResource } from "../../../common/utils/allowed-resource";
import * as routes from "../../../common/routes";
import type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable";
import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable";
@observer
export class Network extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
interface Dependencies {
isAllowedResource: IsAllowedResource;
}
function getRouteTabs({ isAllowedResource }: Dependencies) {
return computed(() => {
const tabs: TabLayoutRoute[] = [];
if (isAllowedResource("services")) {
@ -65,11 +66,14 @@ export class Network extends React.Component {
});
return tabs;
}
render() {
return (
<TabLayout className="Network" tabs={Network.tabRoutes}/>
);
}
});
}
const networkRouteTabsInjectable = getInjectable({
instantiate: (di) => getRouteTabs({
isAllowedResource: di.inject(isAllowedResourceInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default networkRouteTabsInjectable;

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 "./network.scss";
import React from "react";
import { observer } from "mobx-react";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import type { IComputedValue } from "mobx";
import { withInjectables } from "@ogre-tools/injectable-react";
import networkRouteTabsInjectable from "./route-tabs.injectable";
export interface NetworksRouteProps {}
interface Dependencies {
routes: IComputedValue<TabLayoutRoute[]>;
}
const NonInjectedNetworksRoute = observer(({ routes }: Dependencies & NetworksRouteProps) => (
<TabLayout
className="Network"
tabs={routes.get()}
/>
));
export const NetworkRoute = withInjectables<Dependencies, NetworksRouteProps>(NonInjectedNetworksRoute, {
getProps: (di, props) => ({
routes: di.inject(networkRouteTabsInjectable),
...props,
}),
});

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 { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import { isActiveRoute } from "../../navigation";
import { Icon } from "../icon";
import { SidebarItem } from "../layout/sidebar-item";
import type { TabLayoutRoute } from "../layout/tab-layout";
import { TabRoutesSidebarItems } from "../layout/tab-routes-sidebar-items";
import { networkRoute, networkURL } from "../../../common/routes";
import networkRouteTabsInjectable from "./route-tabs.injectable";
export interface NetworkSidebarItemProps {}
interface Dependencies {
routes: IComputedValue<TabLayoutRoute[]>;
}
const NonInjectedNetworkSidebarItem = observer(({ routes }: Dependencies & NetworkSidebarItemProps) => {
const tabRoutes = routes.get();
return (
<SidebarItem
id="networks"
text="Network"
isActive={isActiveRoute(networkRoute)}
isHidden={tabRoutes.length == 0}
url={networkURL()}
icon={<Icon material="device_hub"/>}
>
<TabRoutesSidebarItems routes={tabRoutes} />
</SidebarItem>
);
});
export const NetworkSidebarItem = withInjectables<Dependencies, NetworkSidebarItemProps>(NonInjectedNetworkSidebarItem, {
getProps: (di, props) => ({
routes: di.inject(networkRouteTabsInjectable),
...props,
}),
});

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./node-details-resources.scss";
import "./details-resources.scss";
import { Table } from "../table/table";
import { TableHead } from "../table/table-head";

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./node-details.scss";
import "./details.scss";
import React from "react";
import upperFirst from "lodash/upperFirst";
@ -21,7 +21,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object-meta";
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { NodeDetailsResources } from "./node-details-resources";
import { NodeDetailsResources } from "./details-resources";
import { DrawerTitle } from "../drawer/drawer-title";
import { boundMethod, Disposer } from "../../utils";
import logger from "../../../common/logger";

View File

@ -1,7 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./nodes";
export * from "./node-details";

View File

@ -50,7 +50,7 @@ interface UsageArgs {
}
@observer
export class Nodes extends React.Component<Props> {
export class NodesRoute extends React.Component<Props> {
@observable.ref metrics: Partial<INodeMetrics> = {};
private metricsWatcher = interval(30, async () => this.metrics = await getMetricsForAllNodes());

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import { observer } from "mobx-react";
import React from "react";
import { nodesRoute, nodesURL } from "../../../common/routes";
import type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable";
import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable";
import { isActiveRoute } from "../../navigation";
import { Icon } from "../icon";
import { SidebarItem } from "../layout/sidebar-item";
export interface NodeSidebarItemProps {}
interface Dependencies {
isAllowedResource: IsAllowedResource;
}
const NonInjectedNodeSidebarItem = observer(({ isAllowedResource }: Dependencies & NodeSidebarItemProps) => (
<SidebarItem
id="nodes"
text="Nodes"
isActive={isActiveRoute(nodesRoute)}
isHidden={!isAllowedResource("nodes")}
url={nodesURL()}
icon={<Icon svg="nodes"/>}
/>
));
export const NodesSidebarItem = withInjectables<Dependencies, NodeSidebarItemProps>(NonInjectedNodeSidebarItem, {
getProps: (di, props) => ({
isAllowedResource: di.inject(isAllowedResourceInjectable),
...props,
}),
});

View File

@ -4,6 +4,7 @@
*/
import React, { useState } from "react";
import { observer } from "mobx-react";
import { Input, InputValidators } from "../input";
import { SubTitle } from "../layout/sub-title";
import { UserStore } from "../../../common/user-store";
@ -19,7 +20,7 @@ interface Dependencies {
defaultPathForKubectlBinaries: string
}
const NonInjectedKubectlBinaries: React.FC<Dependencies> = (({ defaultPathForKubectlBinaries }) => {
const NonInjectedKubectlBinaries: React.FC<Dependencies> = observer(({ defaultPathForKubectlBinaries }) => {
const userStore = UserStore.getInstance();
const [downloadPath, setDownloadPath] = useState(userStore.downloadBinariesPath || "");
const [binariesPath, setBinariesPath] = useState(userStore.kubectlBinariesPath || "");

View File

@ -24,7 +24,9 @@ export const Terminal = observer(() => {
: "System default shell"
);
return (<div>
return (<section>
<h2>Terminal</h2>
<section id="shell">
<SubTitle title="Terminal Shell Path"/>
<Input
@ -78,5 +80,5 @@ export const Terminal = observer(() => {
onChange={(value) => userStore.terminalConfig.fontFamily=value}
/>
</section>
</div>);
</section>);
});

View File

@ -1,6 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./storage";

View File

@ -2,21 +2,22 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./storage.scss";
import React from "react";
import { observer } from "mobx-react";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import type { TabLayoutRoute } from "../layout/tab-layout";
import { PersistentVolumes } from "../+storage-volumes";
import { StorageClasses } from "../+storage-classes";
import { PersistentVolumeClaims } from "../+storage-volume-claims";
import { isAllowedResource } from "../../../common/utils/allowed-resource";
import type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable";
import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable";
import * as routes from "../../../common/routes";
@observer
export class Storage extends React.Component {
static get tabRoutes() {
interface Dependencies {
isAllowedResource: IsAllowedResource;
}
function getRouteTabs({ isAllowedResource }: Dependencies) {
return computed(() => {
const tabs: TabLayoutRoute[] = [];
if (isAllowedResource("persistentvolumeclaims")) {
@ -47,11 +48,14 @@ export class Storage extends React.Component {
}
return tabs;
}
render() {
return (
<TabLayout className="Storage" tabs={Storage.tabRoutes}/>
);
}
});
}
const storageRouteTabsInjectable = getInjectable({
instantiate: (di) => getRouteTabs({
isAllowedResource: di.inject(isAllowedResourceInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default storageRouteTabsInjectable;

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./storage.scss";
import React from "react";
import { observer } from "mobx-react";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import type { IComputedValue } from "mobx";
import { withInjectables } from "@ogre-tools/injectable-react";
import storageRouteTabsInjectable from "./route-tabs.injectable";
export interface StorageRouteProps {}
interface Dependencies {
routes: IComputedValue<TabLayoutRoute[]>;
}
const NonInjectedStorageRoute = observer(({ routes }: Dependencies & StorageRouteProps) => (
<TabLayout
className="Storage"
tabs={routes.get()}
/>
));
export const StorageRoute = withInjectables<Dependencies, StorageRouteProps>(NonInjectedStorageRoute, {
getProps: (di, props) => ({
routes: di.inject(storageRouteTabsInjectable),
...props,
}),
});

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