diff --git a/.github/workflows/electronegativity.yml b/.github/workflows/electronegativity.yml new file mode 100644 index 0000000000..059894530b --- /dev/null +++ b/.github/workflows/electronegativity.yml @@ -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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 84b8020605..300843633f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 }} diff --git a/RELEASE_GUIDE.md b/RELEASE_GUIDE.md index 50f8b759e2..32dbfe8a74 100644 --- a/RELEASE_GUIDE.md +++ b/RELEASE_GUIDE.md @@ -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 == "") | 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 == "") | 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 +``` \ No newline at end of file diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts index 375321aaaf..d9f91d3dc8 100644 --- a/integration/__tests__/cluster-pages.tests.ts +++ b/integration/__tests__/cluster-pages.tests.ts @@ -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", }, diff --git a/package.json b/package.json index fb25dcb132..76deb82028 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/cluster-store/allowed-resources.injectable.ts b/src/common/cluster-store/allowed-resources.injectable.ts new file mode 100644 index 0000000000..3e4d4e67f5 --- /dev/null +++ b/src/common/cluster-store/allowed-resources.injectable.ts @@ -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; diff --git a/src/common/cluster-store/hosted-cluster/hosted-cluster.injectable.ts b/src/common/cluster-store/hosted-cluster.injectable.ts similarity index 81% rename from src/common/cluster-store/hosted-cluster/hosted-cluster.injectable.ts rename to src/common/cluster-store/hosted-cluster.injectable.ts index 6a11c80a0e..2d093a1600 100644 --- a/src/common/cluster-store/hosted-cluster/hosted-cluster.injectable.ts +++ b/src/common/cluster-store/hosted-cluster.injectable.ts @@ -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) => { diff --git a/src/common/item.store.ts b/src/common/item.store.ts index fee402491b..68ef6813b9 100644 --- a/src/common/item.store.ts +++ b/src/common/item.store.ts @@ -21,7 +21,7 @@ export abstract class ItemStore { @observable isLoading = false; @observable isLoaded = false; @observable items = observable.array([], { deep: false }); - @observable selectedItemsIds = observable.map(); + @observable selectedItemsIds = observable.set(); constructor() { makeObservable(this); @@ -29,7 +29,11 @@ export abstract class ItemStore { } @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 { } 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 { async removeSelectedItems?(): Promise; + async removeItems?(items: Item[]): Promise; + * [Symbol.iterator]() { yield* this.items; } diff --git a/src/common/k8s-api/__tests__/nodes.test.ts b/src/common/k8s-api/__tests__/nodes.test.ts new file mode 100644 index 0000000000..4b109a8f96 --- /dev/null +++ b/src/common/k8s-api/__tests__/nodes.test.ts @@ -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"); + }); + }); +}); diff --git a/src/common/k8s-api/endpoints/helm-releases.api.ts b/src/common/k8s-api/endpoints/helm-releases.api.ts index b0da1ff3cb..3f46e343cd 100644 --- a/src/common/k8s-api/endpoints/helm-releases.api.ts +++ b/src/common/k8s-api/endpoints/helm-releases.api.ts @@ -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"; diff --git a/src/common/k8s-api/endpoints/nodes.api.ts b/src/common/k8s-api/endpoints/nodes.api.ts index 34e1dd0863..afe379f3b1 100644 --- a/src/common/k8s-api/endpoints/nodes.api.ts +++ b/src/common/k8s-api/endpoints/nodes.api.ts @@ -140,6 +140,12 @@ function* getTrueConditionTypes(conditions: IterableIterator | 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\/+(?.+)$/; + 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(", "); diff --git a/src/common/k8s-api/kube-api.ts b/src/common/k8s-api/kube-api.ts index db7f2516b6..64f4620c17 100644 --- a/src/common/k8s-api/kube-api.ts +++ b/src/common/k8s-api/kube-api.ts @@ -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 { 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 { protected modifyWatchEvent(event: IKubeWatchEvent) { if (event.type === "ERROR") { return; - } ensureObjectSelfLink(this, event.object); diff --git a/src/common/k8s-api/kube-object.store.ts b/src/common/k8s-api/kube-object.store.ts index 8f0d5351f0..d1582e8b51 100644 --- a/src/common/k8s-api/kube-object.store.ts +++ b/src/common/k8s-api/kube-object.store.ts @@ -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 extends ItemStore 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>([], { deep: false }); @@ -378,7 +382,7 @@ export abstract class KubeObjectStore extends ItemStore }); } - 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(() => { diff --git a/src/common/routes/helm-charts.ts b/src/common/routes/helm-charts.ts index 6d36e0d06c..3777ac0ebd 100644 --- a/src/common/routes/helm-charts.ts +++ b/src/common/routes/helm-charts.ts @@ -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 { diff --git a/src/common/routes/apps.ts b/src/common/routes/helm.ts similarity index 70% rename from src/common/routes/apps.ts rename to src/common/routes/helm.ts index 304fbaf255..bb5d5b6914 100644 --- a/src/common/routes/apps.ts +++ b/src/common/routes/helm.ts @@ -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); diff --git a/src/common/routes/index.ts b/src/common/routes/index.ts index cb8a1d1653..916806fa16 100644 --- a/src/common/routes/index.ts +++ b/src/common/routes/index.ts @@ -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"; diff --git a/src/common/routes/releases.ts b/src/common/routes/releases.ts index 092ec1ab63..191d186570 100644 --- a/src/common/routes/releases.ts +++ b/src/common/routes/releases.ts @@ -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 { diff --git a/src/common/utils/abort-controller.ts b/src/common/utils/abort-controller.ts new file mode 100644 index 0000000000..e115fd5868 --- /dev/null +++ b/src/common/utils/abort-controller.ts @@ -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(); + }); + } +} diff --git a/src/common/utils/allowed-resource.ts b/src/common/utils/allowed-resource.ts deleted file mode 100644 index d1be86b720..0000000000 --- a/src/common/utils/allowed-resource.ts +++ /dev/null @@ -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)); -} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index d8a07f31f6..d653214c68 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -10,6 +10,7 @@ export function noop(...args: T): void { return void args; } +export * from "./abort-controller"; export * from "./app-version"; export * from "./autobind"; export * from "./camelCase"; diff --git a/src/common/utils/is-allowed-resource.injectable.ts b/src/common/utils/is-allowed-resource.injectable.ts new file mode 100644 index 0000000000..0b9d07d5bb --- /dev/null +++ b/src/common/utils/is-allowed-resource.injectable.ts @@ -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; diff --git a/src/extensions/main-api/k8s-api.ts b/src/extensions/main-api/k8s-api.ts index cc54ab0134..e9ea0650ac 100644 --- a/src/extensions/main-api/k8s-api.ts +++ b/src/extensions/main-api/k8s-api.ts @@ -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"; diff --git a/src/extensions/renderer-api/k8s-api.ts b/src/extensions/renderer-api/k8s-api.ts index 47a17e4427..1f3829f0cc 100644 --- a/src/extensions/renderer-api/k8s-api.ts +++ b/src/extensions/renderer-api/k8s-api.ts @@ -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"; diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts index 879bfed8ec..9c1d78bcfc 100644 --- a/src/main/helm/helm-release-manager.ts +++ b/src/main/helm/helm-release-manager.ts @@ -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) { diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts index fbb22eb6fc..d3a17d3f63 100644 --- a/src/main/helm/helm-service.ts +++ b/src/main/helm/helm-service.ts @@ -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); } } diff --git a/src/main/routes/helm-route.ts b/src/main/routes/helm-route.ts index 0f0905a675..958f729228 100644 --- a/src/main/routes/helm-route.ts +++ b/src/main/routes/helm-route.ts @@ -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); diff --git a/src/main/shell-session/node-shell-session/node-shell-session.ts b/src/main/shell-session/node-shell-session/node-shell-session.ts index 1fa05a4ae2..8c5861bff5 100644 --- a/src/main/shell-session/node-shell-session/node-shell-session.ts +++ b/src/main/shell-session/node-shell-session/node-shell-session.ts @@ -94,6 +94,7 @@ export class NodeShellSession extends ShellSession { tolerations: [{ operator: "Exists", }], + priorityClassName: "system-node-critical", containers: [{ name: "shell", image: this.cluster.nodeShellImage, diff --git a/src/renderer/cluster-frame-context/cluster-frame-context.injectable.ts b/src/renderer/cluster-frame-context/cluster-frame-context.injectable.ts index 1d15174f04..da139cf8b0 100644 --- a/src/renderer/cluster-frame-context/cluster-frame-context.injectable.ts +++ b/src/renderer/cluster-frame-context/cluster-frame-context.injectable.ts @@ -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) => { diff --git a/src/renderer/components/+apps-releases/update-release/update-release.injectable.ts b/src/renderer/components/+apps-releases/update-release/update-release.injectable.ts deleted file mode 100644 index 2e5a681f51..0000000000 --- a/src/renderer/components/+apps-releases/update-release/update-release.injectable.ts +++ /dev/null @@ -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; diff --git a/src/renderer/components/+apps/apps.tsx b/src/renderer/components/+apps/apps.tsx deleted file mode 100644 index 99f5130113..0000000000 --- a/src/renderer/components/+apps/apps.tsx +++ /dev/null @@ -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 ( - - ); - } -} diff --git a/src/renderer/components/+apps/index.ts b/src/renderer/components/+apps/index.ts deleted file mode 100644 index 07b7a6bcdc..0000000000 --- a/src/renderer/components/+apps/index.ts +++ /dev/null @@ -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"; diff --git a/src/renderer/components/+catalog/catalog.module.scss b/src/renderer/components/+catalog/catalog.module.scss index 5948d64a62..e285fb3ced 100644 --- a/src/renderer/components/+catalog/catalog.module.scss +++ b/src/renderer/components/+catalog/catalog.module.scss @@ -43,6 +43,8 @@ } .pinIcon { + --color-active: var(--textColorAccent); + transition: none; opacity: 0; margin-left: var(--padding); diff --git a/src/renderer/components/+cluster/sidebar-item.tsx b/src/renderer/components/+cluster/sidebar-item.tsx new file mode 100644 index 0000000000..597118eb02 --- /dev/null +++ b/src/renderer/components/+cluster/sidebar-item.tsx @@ -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) => ( + } + /> +)); + +export const ClusterSidebarItem = withInjectables(NonInjectedClusterSidebarItem, { + getProps: (di, props) => ({ + isAllowedResource: di.inject(isAllowedResourceInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+config-resource-quotas/add-quota-dialog.scss b/src/renderer/components/+config-resource-quotas/add-quota-dialog.scss index 8d2e6a0ed7..941f1c9a49 100644 --- a/src/renderer/components/+config-resource-quotas/add-quota-dialog.scss +++ b/src/renderer/components/+config-resource-quotas/add-quota-dialog.scss @@ -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); diff --git a/src/renderer/components/+config/index.ts b/src/renderer/components/+config/index.ts deleted file mode 100644 index 5fa22d8bf5..0000000000 --- a/src/renderer/components/+config/index.ts +++ /dev/null @@ -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"; diff --git a/src/renderer/components/+config/config.tsx b/src/renderer/components/+config/route-tabs.injectable.ts similarity index 75% rename from src/renderer/components/+config/config.tsx rename to src/renderer/components/+config/route-tabs.injectable.ts index 0cd471ae2d..d715d4ac4b 100644 --- a/src/renderer/components/+config/config.tsx +++ b/src/renderer/components/+config/route-tabs.injectable.ts @@ -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 ( - - ); - } + }); } + +const configRoutesInjectable = getInjectable({ + instantiate: (di) => getRouteTabs({ + isAllowedResource: di.inject(isAllowedResourceInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default configRoutesInjectable; diff --git a/src/renderer/components/+config/route.tsx b/src/renderer/components/+config/route.tsx new file mode 100644 index 0000000000..44b0934bc6 --- /dev/null +++ b/src/renderer/components/+config/route.tsx @@ -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; +} + +const NonInjectedConfigRoute = observer(({ routes }: Dependencies & ConfigRouteProps) => ( + +)); + +export const ConfigRoute = withInjectables(NonInjectedConfigRoute, { + getProps: (di, props) => ({ + routes: di.inject(configRoutesInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+config/sidebar-item.tsx b/src/renderer/components/+config/sidebar-item.tsx new file mode 100644 index 0000000000..5298ae251a --- /dev/null +++ b/src/renderer/components/+config/sidebar-item.tsx @@ -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; +} + +const NonInjectedConfigSidebarItem = observer(({ routes }: Dependencies & ConfigSidebarItemProps) => { + const tabRoutes = routes.get(); + + return ( + } + > + + + ); +}); + +export const ConfigSidebarItem = withInjectables(NonInjectedConfigSidebarItem, { + getProps: (di, props) => ({ + routes: di.inject(configRoutesInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+custom-resources/custom-resources.tsx b/src/renderer/components/+custom-resources/custom-resources.tsx deleted file mode 100644 index 825e2b6908..0000000000 --- a/src/renderer/components/+custom-resources/custom-resources.tsx +++ /dev/null @@ -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 ( - - - - - - - - ); - } -} diff --git a/src/renderer/components/+custom-resources/grouped-custom-resources.injectable.ts b/src/renderer/components/+custom-resources/grouped-custom-resources.injectable.ts new file mode 100644 index 0000000000..190bd544f0 --- /dev/null +++ b/src/renderer/components/+custom-resources/grouped-custom-resources.injectable.ts @@ -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; +} + +function getGroupedCustomResourceDefinitions({ definitions }: Dependencies) { + return computed(() => { + const groups = new Map(); + + 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; diff --git a/src/renderer/components/+custom-resources/route-tabs.injectable.ts b/src/renderer/components/+custom-resources/route-tabs.injectable.ts new file mode 100644 index 0000000000..9030077388 --- /dev/null +++ b/src/renderer/components/+custom-resources/route-tabs.injectable.ts @@ -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>; +} + +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; diff --git a/src/renderer/components/+custom-resources/route.tsx b/src/renderer/components/+custom-resources/route.tsx new file mode 100644 index 0000000000..a8f3da4388 --- /dev/null +++ b/src/renderer/components/+custom-resources/route.tsx @@ -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; +} + +const NonInjectedCustomResourcesRoute = observer(({ routes }: Dependencies) => ( + + + { + routes.get().map(({ id, component, routePath, exact }) => ( + + )) + } + + + +)); + +export const CustomResourcesRoute = withInjectables(NonInjectedCustomResourcesRoute, { + getProps: (di, props) => ({ + routes: di.inject(customResourcesRouteTabsInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+custom-resources/sidebar-item.tsx b/src/renderer/components/+custom-resources/sidebar-item.tsx new file mode 100644 index 0000000000..40fb76b34a --- /dev/null +++ b/src/renderer/components/+custom-resources/sidebar-item.tsx @@ -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; + isAllowedResource: IsAllowedResource; + subscribeStores: SubscribeStores; +} + +const NonInjectedCustomResourcesSidebarItem = observer(({ routes, isAllowedResource, subscribeStores }: Dependencies & CustomResourcesSidebarItemProps) => { + useEffect(() => subscribeStores([ + crdStore, + ]), []); + + return ( + } + > + {routes.get().map((route) => ( + + {route.subRoutes?.map((subRoute) => ( + + ))} + + ))} + {crdStore.isLoading && ( +
+ +
+ )} +
+ ); +}); + +export const CustomResourcesSidebarItem = withInjectables(NonInjectedCustomResourcesSidebarItem, { + getProps: (di, props) => ({ + routes: di.inject(customResourcesRouteTabsInjectable), + isAllowedResource: di.inject(isAllowedResourceInjectable), + subscribeStores: di.inject(subscribeStoresInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+events/events.tsx b/src/renderer/components/+events/events.tsx index 8b8ae46ccb..033189ea7d 100644 --- a/src/renderer/components/+events/events.tsx +++ b/src/renderer/components/+events/events.tsx @@ -124,7 +124,7 @@ export class Events extends React.Component { }; 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 { renderHeaderTitle="Events" customizeHeader={this.customizeHeader} isSelectable={false} - items={visibleItems} + getItems={() => this.visibleItems} virtual={!compact} tableProps={{ sortSyncWithUrl: false, diff --git a/src/renderer/components/+events/index.ts b/src/renderer/components/+events/index.ts index 3eaea4bdae..03a60b7266 100644 --- a/src/renderer/components/+events/index.ts +++ b/src/renderer/components/+events/index.ts @@ -3,5 +3,4 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export * from "./events"; export * from "./event-details"; diff --git a/src/renderer/components/+events/sidebar-item.tsx b/src/renderer/components/+events/sidebar-item.tsx new file mode 100644 index 0000000000..50ffddcefc --- /dev/null +++ b/src/renderer/components/+events/sidebar-item.tsx @@ -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) => ( + } + /> +)); + +export const EventsSidebarItem = withInjectables(NonInjectedEventsSidebarItem, { + getProps: (di, props) => ({ + isAllowedResource: di.inject(isAllowedResourceInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+extensions/attempt-install/attempt-install.tsx b/src/renderer/components/+extensions/attempt-install/attempt-install.tsx index eccd75da3e..97b5f46ad2 100644 --- a/src/renderer/components/+extensions/attempt-install/attempt-install.tsx +++ b/src/renderer/components/+extensions/attempt-install/attempt-install.tsx @@ -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(
@@ -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); } }; diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.scss b/src/renderer/components/+helm-charts/helm-chart-details.scss similarity index 100% rename from src/renderer/components/+apps-helm-charts/helm-chart-details.scss rename to src/renderer/components/+helm-charts/helm-chart-details.scss diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx b/src/renderer/components/+helm-charts/helm-chart-details.tsx similarity index 100% rename from src/renderer/components/+apps-helm-charts/helm-chart-details.tsx rename to src/renderer/components/+helm-charts/helm-chart-details.tsx diff --git a/src/renderer/components/+apps-helm-charts/helm-chart.store.ts b/src/renderer/components/+helm-charts/helm-chart.store.ts similarity index 100% rename from src/renderer/components/+apps-helm-charts/helm-chart.store.ts rename to src/renderer/components/+helm-charts/helm-chart.store.ts diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.scss b/src/renderer/components/+helm-charts/helm-charts.scss similarity index 100% rename from src/renderer/components/+apps-helm-charts/helm-charts.scss rename to src/renderer/components/+helm-charts/helm-charts.scss diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.tsx b/src/renderer/components/+helm-charts/helm-charts.tsx similarity index 100% rename from src/renderer/components/+apps-helm-charts/helm-charts.tsx rename to src/renderer/components/+helm-charts/helm-charts.tsx diff --git a/src/renderer/components/+apps-helm-charts/helm-placeholder.svg b/src/renderer/components/+helm-charts/helm-placeholder.svg similarity index 100% rename from src/renderer/components/+apps-helm-charts/helm-placeholder.svg rename to src/renderer/components/+helm-charts/helm-placeholder.svg diff --git a/src/renderer/components/+apps-helm-charts/index.ts b/src/renderer/components/+helm-charts/index.ts similarity index 100% rename from src/renderer/components/+apps-helm-charts/index.ts rename to src/renderer/components/+helm-charts/index.ts diff --git a/src/renderer/components/+apps-releases/create-release/create-release.injectable.ts b/src/renderer/components/+helm-releases/create-release/create-release.injectable.ts similarity index 100% rename from src/renderer/components/+apps-releases/create-release/create-release.injectable.ts rename to src/renderer/components/+helm-releases/create-release/create-release.injectable.ts diff --git a/src/renderer/components/+apps-releases/delete-release/delete-release.injectable.ts b/src/renderer/components/+helm-releases/delete-release/delete-release.injectable.ts similarity index 100% rename from src/renderer/components/+apps-releases/delete-release/delete-release.injectable.ts rename to src/renderer/components/+helm-releases/delete-release/delete-release.injectable.ts diff --git a/src/renderer/components/+apps-releases/index.ts b/src/renderer/components/+helm-releases/index.ts similarity index 100% rename from src/renderer/components/+apps-releases/index.ts rename to src/renderer/components/+helm-releases/index.ts diff --git a/src/renderer/components/+apps-releases/release-details/release-details.injectable.ts b/src/renderer/components/+helm-releases/release-details/release-details.injectable.ts similarity index 91% rename from src/renderer/components/+apps-releases/release-details/release-details.injectable.ts rename to src/renderer/components/+helm-releases/release-details/release-details.injectable.ts index 1dac3b716f..c726e0429c 100644 --- a/src/renderer/components/+apps-releases/release-details/release-details.injectable.ts +++ b/src/renderer/components/+helm-releases/release-details/release-details.injectable.ts @@ -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); }), diff --git a/src/renderer/components/+apps-releases/release-details/release-details.scss b/src/renderer/components/+helm-releases/release-details/release-details.scss similarity index 100% rename from src/renderer/components/+apps-releases/release-details/release-details.scss rename to src/renderer/components/+helm-releases/release-details/release-details.scss diff --git a/src/renderer/components/+apps-releases/release-details/release-details.tsx b/src/renderer/components/+helm-releases/release-details/release-details.tsx similarity index 98% rename from src/renderer/components/+apps-releases/release-details/release-details.tsx rename to src/renderer/components/+helm-releases/release-details/release-details.tsx index 30d34e1857..a41d0cae16 100644 --- a/src/renderer/components/+apps-releases/release-details/release-details.tsx +++ b/src/renderer/components/+helm-releases/release-details/release-details.tsx @@ -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 + release: IComputedValue releaseDetails: IAsyncComputed releaseValues: IAsyncComputed updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise @@ -59,7 +59,7 @@ class NonInjectedReleaseDetails extends Component { } @computed get release() { - return this.props.release.value.get(); + return this.props.release.get(); } @computed get details() { diff --git a/src/renderer/components/+apps-releases/release-details/release-route-parameters.injectable.ts b/src/renderer/components/+helm-releases/release-details/release-route-parameters.injectable.ts similarity index 100% rename from src/renderer/components/+apps-releases/release-details/release-route-parameters.injectable.ts rename to src/renderer/components/+helm-releases/release-details/release-route-parameters.injectable.ts diff --git a/src/renderer/components/+apps-releases/release-details/release-values.injectable.ts b/src/renderer/components/+helm-releases/release-details/release-values.injectable.ts similarity index 87% rename from src/renderer/components/+apps-releases/release-details/release-values.injectable.ts rename to src/renderer/components/+helm-releases/release-details/release-values.injectable.ts index d7cc2c965c..965db1324f 100644 --- a/src/renderer/components/+apps-releases/release-details/release-values.injectable.ts +++ b/src/renderer/components/+helm-releases/release-details/release-values.injectable.ts @@ -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 { diff --git a/src/renderer/components/+apps-releases/release-details/release.injectable.ts b/src/renderer/components/+helm-releases/release-details/release.injectable.ts similarity index 89% rename from src/renderer/components/+apps-releases/release-details/release.injectable.ts rename to src/renderer/components/+helm-releases/release-details/release.injectable.ts index 6da2295765..75b821890a 100644 --- a/src/renderer/components/+apps-releases/release-details/release.injectable.ts +++ b/src/renderer/components/+helm-releases/release-details/release.injectable.ts @@ -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) { diff --git a/src/renderer/components/+apps-releases/release-details/user-supplied-values-are-shown.injectable.ts b/src/renderer/components/+helm-releases/release-details/user-supplied-values-are-shown.injectable.ts similarity index 100% rename from src/renderer/components/+apps-releases/release-details/user-supplied-values-are-shown.injectable.ts rename to src/renderer/components/+helm-releases/release-details/user-supplied-values-are-shown.injectable.ts diff --git a/src/renderer/components/+apps-releases/release-menu.tsx b/src/renderer/components/+helm-releases/release-menu.tsx similarity index 100% rename from src/renderer/components/+apps-releases/release-menu.tsx rename to src/renderer/components/+helm-releases/release-menu.tsx diff --git a/src/renderer/components/+apps-releases/release-rollback-dialog-model/release-rollback-dialog-model.injectable.ts b/src/renderer/components/+helm-releases/release-rollback-dialog-model/release-rollback-dialog-model.injectable.ts similarity index 100% rename from src/renderer/components/+apps-releases/release-rollback-dialog-model/release-rollback-dialog-model.injectable.ts rename to src/renderer/components/+helm-releases/release-rollback-dialog-model/release-rollback-dialog-model.injectable.ts diff --git a/src/renderer/components/+apps-releases/release-rollback-dialog-model/release-rollback-dialog-model.ts b/src/renderer/components/+helm-releases/release-rollback-dialog-model/release-rollback-dialog-model.ts similarity index 100% rename from src/renderer/components/+apps-releases/release-rollback-dialog-model/release-rollback-dialog-model.ts rename to src/renderer/components/+helm-releases/release-rollback-dialog-model/release-rollback-dialog-model.ts diff --git a/src/renderer/components/+apps-releases/release-rollback-dialog.scss b/src/renderer/components/+helm-releases/release-rollback-dialog.scss similarity index 100% rename from src/renderer/components/+apps-releases/release-rollback-dialog.scss rename to src/renderer/components/+helm-releases/release-rollback-dialog.scss diff --git a/src/renderer/components/+apps-releases/release-rollback-dialog.tsx b/src/renderer/components/+helm-releases/release-rollback-dialog.tsx similarity index 100% rename from src/renderer/components/+apps-releases/release-rollback-dialog.tsx rename to src/renderer/components/+helm-releases/release-rollback-dialog.tsx diff --git a/src/renderer/components/+apps-releases/release.mixins.scss b/src/renderer/components/+helm-releases/release.mixins.scss similarity index 100% rename from src/renderer/components/+apps-releases/release.mixins.scss rename to src/renderer/components/+helm-releases/release.mixins.scss diff --git a/src/renderer/components/+apps-releases/releases.injectable.ts b/src/renderer/components/+helm-releases/releases.injectable.ts similarity index 100% rename from src/renderer/components/+apps-releases/releases.injectable.ts rename to src/renderer/components/+helm-releases/releases.injectable.ts diff --git a/src/renderer/components/+apps-releases/releases.scss b/src/renderer/components/+helm-releases/releases.scss similarity index 100% rename from src/renderer/components/+apps-releases/releases.scss rename to src/renderer/components/+helm-releases/releases.scss diff --git a/src/renderer/components/+apps-releases/releases.tsx b/src/renderer/components/+helm-releases/releases.tsx similarity index 90% rename from src/renderer/components/+apps-releases/releases.tsx rename to src/renderer/components/+helm-releases/releases.tsx index 34f9798783..0017fa1ba8 100644 --- a/src/renderer/components/+apps-releases/releases.tsx +++ b/src/renderer/components/+helm-releases/releases.tsx @@ -109,11 +109,23 @@ class NonInjectedHelmReleases extends Component { 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 { 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; @@ -197,7 +211,7 @@ class NonInjectedHelmReleases extends Component { })} onDetails={this.onDetails} /> - + diff --git a/src/renderer/components/+apps-releases/removable-releases.injectable.ts b/src/renderer/components/+helm-releases/removable-releases.injectable.ts similarity index 100% rename from src/renderer/components/+apps-releases/removable-releases.injectable.ts rename to src/renderer/components/+helm-releases/removable-releases.injectable.ts diff --git a/src/renderer/components/+apps-releases/removable-releases.ts b/src/renderer/components/+helm-releases/removable-releases.ts similarity index 100% rename from src/renderer/components/+apps-releases/removable-releases.ts rename to src/renderer/components/+helm-releases/removable-releases.ts diff --git a/src/renderer/components/+apps-releases/rollback-release/rollback-release.injectable.ts b/src/renderer/components/+helm-releases/rollback-release/rollback-release.injectable.ts similarity index 100% rename from src/renderer/components/+apps-releases/rollback-release/rollback-release.injectable.ts rename to src/renderer/components/+helm-releases/rollback-release/rollback-release.injectable.ts diff --git a/src/renderer/components/+helm-releases/update-release/update-release.injectable.ts b/src/renderer/components/+helm-releases/update-release/update-release.injectable.ts new file mode 100644 index 0000000000..06b1949da2 --- /dev/null +++ b/src/renderer/components/+helm-releases/update-release/update-release.injectable.ts @@ -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; diff --git a/src/renderer/components/+helm/route-tabs.injectable.ts b/src/renderer/components/+helm/route-tabs.injectable.ts new file mode 100644 index 0000000000..34422f7c4b --- /dev/null +++ b/src/renderer/components/+helm/route-tabs.injectable.ts @@ -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; diff --git a/src/renderer/components/+helm/route.tsx b/src/renderer/components/+helm/route.tsx new file mode 100644 index 0000000000..da2983a8ba --- /dev/null +++ b/src/renderer/components/+helm/route.tsx @@ -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; +} + +const NonInjectedHelmRoute = observer(({ routes }: Dependencies & HelmRouteProps) => ( + +)); + +export const HelmRoute = withInjectables(NonInjectedHelmRoute, { + getProps: (di, props) => ({ + routes: di.inject(helmRoutesInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+helm/sidebar-item.tsx b/src/renderer/components/+helm/sidebar-item.tsx new file mode 100644 index 0000000000..2893d369a9 --- /dev/null +++ b/src/renderer/components/+helm/sidebar-item.tsx @@ -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; +} + +const NonInjectedHelmSidebarItem = observer(({ routes }: Dependencies & HelmSidebarItemProps) => { + const tabRoutes = routes.get(); + + return ( + } + > + + + ); +}); + +export const HelmSidebarItem = withInjectables(NonInjectedHelmSidebarItem, { + getProps: (di, props) => ({ + routes: di.inject(networkRouteTabsInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+namespaces/index.ts b/src/renderer/components/+namespaces/index.ts index ce705883ea..1ce2e35264 100644 --- a/src/renderer/components/+namespaces/index.ts +++ b/src/renderer/components/+namespaces/index.ts @@ -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"; diff --git a/src/renderer/components/+namespaces/namespaces.tsx b/src/renderer/components/+namespaces/namespaces.tsx deleted file mode 100644 index f997613c17..0000000000 --- a/src/renderer/components/+namespaces/namespaces.tsx +++ /dev/null @@ -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 { -} - -interface Dependencies { - namespaceStore: NamespaceStore - openAddNamespaceDialog: () => void -} - -class NonInjectedNamespaces extends React.Component { - render() { - return ( - - 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(), - , - item.getLabels().map(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, - })} - /> - - - ); - } -} - -export const Namespaces = withInjectables( - NonInjectedNamespaces, - - { - getProps: (di, props) => ({ - namespaceStore: di.inject(namespaceStoreInjectable), - openAddNamespaceDialog: di.inject(addNamespaceDialogModelInjectable).open, - ...props, - }), - }, -); diff --git a/src/renderer/components/+namespaces/route.tsx b/src/renderer/components/+namespaces/route.tsx new file mode 100644 index 0000000000..a05231dc05 --- /dev/null +++ b/src/renderer/components/+namespaces/route.tsx @@ -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 { +} + +interface Dependencies { + namespaceStore: NamespaceStore + openAddNamespaceDialog: () => void +} + +export const NonInjectedNamespacesRoute = ({ namespaceStore, openAddNamespaceDialog }: Dependencies & NamespacesRouteProps) => ( + + 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(), + , + item.getLabels().map(label => ), + item.getAge(), + { title: item.getStatus(), className: item.getStatus().toLowerCase() }, + ]} + addRemoveButtons={{ + addTooltip: "Add Namespace", + onAdd: openAddNamespaceDialog, + }} + customizeTableRowProps={item => ({ + disabled: item.getStatus() === NamespaceStatus.TERMINATING, + })} + /> + + +); + + +export const NamespacesRoute = withInjectables(NonInjectedNamespacesRoute, { + getProps: (di, props) => ({ + namespaceStore: di.inject(namespaceStoreInjectable), + openAddNamespaceDialog: di.inject(addNamespaceDialogModelInjectable).open, + ...props, + }), +}); diff --git a/src/renderer/components/+namespaces/sidebar-item.tsx b/src/renderer/components/+namespaces/sidebar-item.tsx new file mode 100644 index 0000000000..78bbc8a3ae --- /dev/null +++ b/src/renderer/components/+namespaces/sidebar-item.tsx @@ -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) => ( + } + /> +)); + +export const NamespacesSidebarItem = withInjectables(NonInjectedNamespacesSidebarItem, { + getProps: (di, props) => ({ + isAllowedResource: di.inject(isAllowedResourceInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+network/index.ts b/src/renderer/components/+network/index.ts deleted file mode 100644 index 37d73526e3..0000000000 --- a/src/renderer/components/+network/index.ts +++ /dev/null @@ -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"; diff --git a/src/renderer/components/+network/network.tsx b/src/renderer/components/+network/route-tabs.injectable.ts similarity index 68% rename from src/renderer/components/+network/network.tsx rename to src/renderer/components/+network/route-tabs.injectable.ts index 2bdad62d8d..95476fe17f 100644 --- a/src/renderer/components/+network/network.tsx +++ b/src/renderer/components/+network/route-tabs.injectable.ts @@ -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 ( - - ); - } + }); } + +const networkRouteTabsInjectable = getInjectable({ + instantiate: (di) => getRouteTabs({ + isAllowedResource: di.inject(isAllowedResourceInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default networkRouteTabsInjectable; diff --git a/src/renderer/components/+network/route.tsx b/src/renderer/components/+network/route.tsx new file mode 100644 index 0000000000..1abf1ca445 --- /dev/null +++ b/src/renderer/components/+network/route.tsx @@ -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; +} + +const NonInjectedNetworksRoute = observer(({ routes }: Dependencies & NetworksRouteProps) => ( + +)); + +export const NetworkRoute = withInjectables(NonInjectedNetworksRoute, { + getProps: (di, props) => ({ + routes: di.inject(networkRouteTabsInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+network/sidebar-item.tsx b/src/renderer/components/+network/sidebar-item.tsx new file mode 100644 index 0000000000..2dcf056a97 --- /dev/null +++ b/src/renderer/components/+network/sidebar-item.tsx @@ -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; +} + +const NonInjectedNetworkSidebarItem = observer(({ routes }: Dependencies & NetworkSidebarItemProps) => { + const tabRoutes = routes.get(); + + return ( + } + > + + + ); +}); + +export const NetworkSidebarItem = withInjectables(NonInjectedNetworkSidebarItem, { + getProps: (di, props) => ({ + routes: di.inject(networkRouteTabsInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+nodes/node-details-resources.scss b/src/renderer/components/+nodes/details-resources.scss similarity index 100% rename from src/renderer/components/+nodes/node-details-resources.scss rename to src/renderer/components/+nodes/details-resources.scss diff --git a/src/renderer/components/+nodes/node-details-resources.tsx b/src/renderer/components/+nodes/details-resources.tsx similarity index 98% rename from src/renderer/components/+nodes/node-details-resources.tsx rename to src/renderer/components/+nodes/details-resources.tsx index 98dab82d91..50d3a7cb7c 100644 --- a/src/renderer/components/+nodes/node-details-resources.tsx +++ b/src/renderer/components/+nodes/details-resources.tsx @@ -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"; diff --git a/src/renderer/components/+nodes/node-details.scss b/src/renderer/components/+nodes/details.scss similarity index 100% rename from src/renderer/components/+nodes/node-details.scss rename to src/renderer/components/+nodes/details.scss diff --git a/src/renderer/components/+nodes/node-details.tsx b/src/renderer/components/+nodes/details.tsx similarity index 98% rename from src/renderer/components/+nodes/node-details.tsx rename to src/renderer/components/+nodes/details.tsx index 82580224ec..9af106f43f 100644 --- a/src/renderer/components/+nodes/node-details.tsx +++ b/src/renderer/components/+nodes/details.tsx @@ -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"; diff --git a/src/renderer/components/+nodes/index.ts b/src/renderer/components/+nodes/index.ts deleted file mode 100644 index 78b0f3b279..0000000000 --- a/src/renderer/components/+nodes/index.ts +++ /dev/null @@ -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"; diff --git a/src/renderer/components/+nodes/nodes.tsx b/src/renderer/components/+nodes/route.tsx similarity index 99% rename from src/renderer/components/+nodes/nodes.tsx rename to src/renderer/components/+nodes/route.tsx index 43a3b3a84e..8801e6e832 100644 --- a/src/renderer/components/+nodes/nodes.tsx +++ b/src/renderer/components/+nodes/route.tsx @@ -50,7 +50,7 @@ interface UsageArgs { } @observer -export class Nodes extends React.Component { +export class NodesRoute extends React.Component { @observable.ref metrics: Partial = {}; private metricsWatcher = interval(30, async () => this.metrics = await getMetricsForAllNodes()); diff --git a/src/renderer/components/+nodes/sidebar-item.tsx b/src/renderer/components/+nodes/sidebar-item.tsx new file mode 100644 index 0000000000..1a384cc0fc --- /dev/null +++ b/src/renderer/components/+nodes/sidebar-item.tsx @@ -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) => ( + } + /> +)); + +export const NodesSidebarItem = withInjectables(NonInjectedNodeSidebarItem, { + getProps: (di, props) => ({ + isAllowedResource: di.inject(isAllowedResourceInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+preferences/kubectl-binaries.tsx b/src/renderer/components/+preferences/kubectl-binaries.tsx index fdd0ab4131..c39dd333c3 100644 --- a/src/renderer/components/+preferences/kubectl-binaries.tsx +++ b/src/renderer/components/+preferences/kubectl-binaries.tsx @@ -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 = (({ defaultPathForKubectlBinaries }) => { +const NonInjectedKubectlBinaries: React.FC = observer(({ defaultPathForKubectlBinaries }) => { const userStore = UserStore.getInstance(); const [downloadPath, setDownloadPath] = useState(userStore.downloadBinariesPath || ""); const [binariesPath, setBinariesPath] = useState(userStore.kubectlBinariesPath || ""); diff --git a/src/renderer/components/+preferences/terminal.tsx b/src/renderer/components/+preferences/terminal.tsx index b76f2ba567..f9f1226609 100644 --- a/src/renderer/components/+preferences/terminal.tsx +++ b/src/renderer/components/+preferences/terminal.tsx @@ -24,7 +24,9 @@ export const Terminal = observer(() => { : "System default shell" ); - return (
+ return (
+

Terminal

+
{ onChange={(value) => userStore.terminalConfig.fontFamily=value} />
-
); + ); }); diff --git a/src/renderer/components/+storage/index.ts b/src/renderer/components/+storage/index.ts deleted file mode 100644 index 90fa5c7eea..0000000000 --- a/src/renderer/components/+storage/index.ts +++ /dev/null @@ -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"; diff --git a/src/renderer/components/+storage/storage.tsx b/src/renderer/components/+storage/route-tabs.injectable.ts similarity index 61% rename from src/renderer/components/+storage/storage.tsx rename to src/renderer/components/+storage/route-tabs.injectable.ts index 882160cb4e..512e0ddc09 100644 --- a/src/renderer/components/+storage/storage.tsx +++ b/src/renderer/components/+storage/route-tabs.injectable.ts @@ -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 ( - - ); - } + }); } + +const storageRouteTabsInjectable = getInjectable({ + instantiate: (di) => getRouteTabs({ + isAllowedResource: di.inject(isAllowedResourceInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default storageRouteTabsInjectable; diff --git a/src/renderer/components/+storage/route.tsx b/src/renderer/components/+storage/route.tsx new file mode 100644 index 0000000000..b11d643186 --- /dev/null +++ b/src/renderer/components/+storage/route.tsx @@ -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; +} + +const NonInjectedStorageRoute = observer(({ routes }: Dependencies & StorageRouteProps) => ( + +)); + +export const StorageRoute = withInjectables(NonInjectedStorageRoute, { + getProps: (di, props) => ({ + routes: di.inject(storageRouteTabsInjectable), + ...props, + }), +}); + diff --git a/src/renderer/components/+storage/sidebar-item.tsx b/src/renderer/components/+storage/sidebar-item.tsx new file mode 100644 index 0000000000..11c598a622 --- /dev/null +++ b/src/renderer/components/+storage/sidebar-item.tsx @@ -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 { storageRoute, storageURL } 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 storageRouteTabsInjectable from "./route-tabs.injectable"; + +export interface StorageSidebarItemProps {} + +interface Dependencies { + routes: IComputedValue; +} + +const NonInjectedStorageSidebarItem = observer(({ routes }: Dependencies & StorageSidebarItemProps) => { + const tabRoutes = routes.get(); + + return ( + } + > + + + ); +}); + +export const StorageSidebarItem = withInjectables(NonInjectedStorageSidebarItem, { + getProps: (di, props) => ({ + routes: di.inject(storageRouteTabsInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+user-management/index.ts b/src/renderer/components/+user-management/index.ts deleted file mode 100644 index 8d292d0a2d..0000000000 --- a/src/renderer/components/+user-management/index.ts +++ /dev/null @@ -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 "./user-management"; diff --git a/src/renderer/components/+user-management/user-management.tsx b/src/renderer/components/+user-management/route-tabs.injectable.ts similarity index 67% rename from src/renderer/components/+user-management/user-management.tsx rename to src/renderer/components/+user-management/route-tabs.injectable.ts index 1d39eba153..a33249d5c6 100644 --- a/src/renderer/components/+user-management/user-management.tsx +++ b/src/renderer/components/+user-management/route-tabs.injectable.ts @@ -2,28 +2,29 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ - -import "./user-management.scss"; - -import React from "react"; -import { observer } from "mobx-react"; -import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; -import { PodSecurityPolicies } from "../+pod-security-policies"; -import { isAllowedResource } from "../../../common/utils/allowed-resource"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import type { TabLayoutRoute } from "../layout/tab-layout"; +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"; +import { PodSecurityPolicies } from "../+pod-security-policies"; import { ClusterRoleBindings } from "./+cluster-role-bindings"; -import { ServiceAccounts } from "./+service-accounts"; -import { Roles } from "./+roles"; -import { RoleBindings } from "./+role-bindings"; import { ClusterRoles } from "./+cluster-roles"; +import { RoleBindings } from "./+role-bindings"; +import { Roles } from "./+roles"; +import { ServiceAccounts } from "./+service-accounts"; -@observer -export class UserManagement extends React.Component { - static get tabRoutes() { - const tabRoutes: TabLayoutRoute[] = []; +interface Dependencies { + isAllowedResource: IsAllowedResource; +} + +function getRouteTabs({ isAllowedResource }: Dependencies) { + return computed(() => { + const tabs: TabLayoutRoute[] = []; if (isAllowedResource("serviceaccounts")) { - tabRoutes.push({ + tabs.push({ title: "Service Accounts", component: ServiceAccounts, url: routes.serviceAccountsURL(), @@ -32,7 +33,7 @@ export class UserManagement extends React.Component { } if (isAllowedResource("clusterroles")) { - tabRoutes.push({ + tabs.push({ title: "Cluster Roles", component: ClusterRoles, url: routes.clusterRolesURL(), @@ -41,7 +42,7 @@ export class UserManagement extends React.Component { } if (isAllowedResource("roles")) { - tabRoutes.push({ + tabs.push({ title: "Roles", component: Roles, url: routes.rolesURL(), @@ -50,7 +51,7 @@ export class UserManagement extends React.Component { } if (isAllowedResource("clusterrolebindings")) { - tabRoutes.push({ + tabs.push({ title: "Cluster Role Bindings", component: ClusterRoleBindings, url: routes.clusterRoleBindingsURL(), @@ -59,7 +60,7 @@ export class UserManagement extends React.Component { } if (isAllowedResource("rolebindings")) { - tabRoutes.push({ + tabs.push({ title: "Role Bindings", component: RoleBindings, url: routes.roleBindingsURL(), @@ -68,7 +69,7 @@ export class UserManagement extends React.Component { } if (isAllowedResource("podsecuritypolicies")) { - tabRoutes.push({ + tabs.push({ title: "Pod Security Policies", component: PodSecurityPolicies, url: routes.podSecurityPoliciesURL(), @@ -76,12 +77,15 @@ export class UserManagement extends React.Component { }); } - return tabRoutes; - } - - render() { - return ( - - ); - } + return tabs; + }); } + +const userManagementRouteTabsInjectable = getInjectable({ + instantiate: (di) => getRouteTabs({ + isAllowedResource: di.inject(isAllowedResourceInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default userManagementRouteTabsInjectable; diff --git a/src/renderer/components/+user-management/route.tsx b/src/renderer/components/+user-management/route.tsx new file mode 100644 index 0000000000..360b0f57d0 --- /dev/null +++ b/src/renderer/components/+user-management/route.tsx @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./user-management.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 userManagementRouteTabsInjectable from "./route-tabs.injectable"; + +interface Dependencies { + routes: IComputedValue; +} + +const NonInjectedUserManagementRoute = observer(({ routes }: Dependencies) => ( + +)); + +export const UserManagementRoute = withInjectables(NonInjectedUserManagementRoute, { + getProps: (di, props) => ({ + routes: di.inject(userManagementRouteTabsInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+user-management/sidebar-item.tsx b/src/renderer/components/+user-management/sidebar-item.tsx new file mode 100644 index 0000000000..47977cc3c1 --- /dev/null +++ b/src/renderer/components/+user-management/sidebar-item.tsx @@ -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 { usersManagementRoute, usersManagementURL } 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 userManagementRouteTabsInjectable from "./route-tabs.injectable"; + +export interface UserManagementSidebarItemProps {} + +interface Dependencies { + routes: IComputedValue; +} + +const NonInjectedUserManagementSidebarItem = observer(({ routes }: Dependencies & UserManagementSidebarItemProps) => { + const tabRoutes = routes.get(); + + return ( + } + > + + + ); +}); + +export const UserManagementSidebarItem = withInjectables(NonInjectedUserManagementSidebarItem, { + getProps: (di, props) => ({ + routes: di.inject(userManagementRouteTabsInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+workloads-overview/overview-statuses.tsx b/src/renderer/components/+workloads-overview/overview-statuses.tsx index ad2f02c5ca..6862695ccf 100644 --- a/src/renderer/components/+workloads-overview/overview-statuses.tsx +++ b/src/renderer/components/+workloads-overview/overview-statuses.tsx @@ -9,73 +9,49 @@ import React from "react"; import { observer } from "mobx-react"; import { OverviewWorkloadStatus } from "./overview-workload-status"; import { Link } from "react-router-dom"; -import { workloadStores } from "../+workloads"; -import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store"; import type { KubeResource } from "../../../common/rbac"; -import { ResourceNames } from "../../utils/rbac"; -import { boundMethod } from "../../utils"; -import { workloadURL } from "../../../common/routes"; -import { isAllowedResource } from "../../../common/utils/allowed-resource"; import { withInjectables } from "@ogre-tools/injectable-react"; -import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; +import type { IComputedValue } from "mobx"; +import workloadsInjectable from "./workloads.injectable"; -const resources: KubeResource[] = [ - "pods", - "deployments", - "statefulsets", - "daemonsets", - "replicasets", - "jobs", - "cronjobs", -]; +export interface OverviewStatusesProps {} + +interface Workload { + resource: KubeResource; + amountOfItems: number; + href: string; + status: Record; + title: string +} interface Dependencies { - namespaceStore: NamespaceStore + workloads: IComputedValue; } -@observer -class NonInjectedOverviewStatuses extends React.Component { - @boundMethod - renderWorkload(resource: KubeResource): React.ReactElement { - const store = workloadStores.get(resource); +const NonInjectedOverviewStatuses = observer( + ({ workloads }: Dependencies & OverviewStatusesProps) => ( +
+
+ {workloads.get() + .map(({ resource, title, href, status, amountOfItems }) => ( +
+
+ + {title} ({amountOfItems}) + +
- if (!store) { - return null; - } - - const items = store.getAllByNs(this.props.namespaceStore.contextNamespaces); - - return ( -
-
- {ResourceNames[resource]} ({items.length}) -
- + +
+ ))}
- ); - } - - render() { - const workloads = resources - .filter(isAllowedResource) - .map(this.renderWorkload); - - return ( -
-
- {workloads} -
-
- ); - } -} - -export const OverviewStatuses = withInjectables( - NonInjectedOverviewStatuses, - - { - getProps: (di) => ({ - namespaceStore: di.inject(namespaceStoreInjectable), - }), - }, +
+ ), ); + +export const OverviewStatuses = withInjectables(NonInjectedOverviewStatuses, { + getProps: (di, props) => ({ + workloads: di.inject(workloadsInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+workloads-overview/workloads.injectable.ts b/src/renderer/components/+workloads-overview/workloads.injectable.ts new file mode 100644 index 0000000000..013a195816 --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads.injectable.ts @@ -0,0 +1,40 @@ +/** + * 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 type { KubeResource } from "../../../common/rbac"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import { podsStore } from "../+workloads-pods/pods.store"; +import { deploymentStore } from "../+workloads-deployments/deployments.store"; +import { daemonSetStore } from "../+workloads-daemonsets/daemonsets.store"; +import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store"; +import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; +import { jobStore } from "../+workloads-jobs/job.store"; +import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; +import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable"; +import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; +import { workloads } from "./workloads"; + +const workloadsInjectable = getInjectable({ + instantiate: (di) => + workloads({ + isAllowedResource: di.inject(isAllowedResourceInjectable), + namespaceStore: di.inject(namespaceStoreInjectable), + + workloadStores: new Map>([ + ["pods", podsStore], + ["deployments", deploymentStore], + ["daemonsets", daemonSetStore], + ["statefulsets", statefulSetStore], + ["replicasets", replicaSetStore], + ["jobs", jobStore], + ["cronjobs", cronJobStore], + ]), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default workloadsInjectable; diff --git a/src/renderer/components/+workloads-overview/workloads.ts b/src/renderer/components/+workloads-overview/workloads.ts new file mode 100644 index 0000000000..f3db713e63 --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { computed } from "mobx"; +import type { KubeResource } from "../../../common/rbac"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import { workloadURL } from "../../../common/routes"; +import { ResourceNames } from "../../utils/rbac"; +import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store"; +import type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable"; + +interface Dependencies { + workloadStores: Map>; + isAllowedResource: IsAllowedResource; + namespaceStore: NamespaceStore; +} + +export const workloads = ({ + workloadStores, + isAllowedResource, + namespaceStore, +}: Dependencies) => + computed(() => + [...workloadStores.entries()] + .filter(([resource]) => isAllowedResource(resource)) + .map(([resource, store]) => { + const items = store.getAllByNs(namespaceStore.contextNamespaces); + + return { + resource, + href: workloadURL[resource](), + amountOfItems: items.length, + status: store.getStatuses(items), + title: ResourceNames[resource], + }; + }), + ); diff --git a/src/renderer/components/+workloads/index.ts b/src/renderer/components/+workloads/index.ts deleted file mode 100644 index 502ff98107..0000000000 --- a/src/renderer/components/+workloads/index.ts +++ /dev/null @@ -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 "./workloads"; -export * from "./workloads.stores"; diff --git a/src/renderer/components/+workloads/workloads.tsx b/src/renderer/components/+workloads/route-tabs.injectable.ts similarity index 76% rename from src/renderer/components/+workloads/workloads.tsx rename to src/renderer/components/+workloads/route-tabs.injectable.ts index aaa9ba6209..e31c171d38 100644 --- a/src/renderer/components/+workloads/workloads.tsx +++ b/src/renderer/components/+workloads/route-tabs.injectable.ts @@ -2,11 +2,9 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ - -import "./workloads.scss"; - -import React from "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 { WorkloadsOverview } from "../+workloads-overview/overview"; import { Pods } from "../+workloads-pods"; import { Deployments } from "../+workloads-deployments"; @@ -14,12 +12,17 @@ import { DaemonSets } from "../+workloads-daemonsets"; import { StatefulSets } from "../+workloads-statefulsets"; import { Jobs } from "../+workloads-jobs"; import { CronJobs } from "../+workloads-cronjobs"; -import { isAllowedResource } from "../../../common/utils/allowed-resource"; import { ReplicaSets } from "../+workloads-replicasets"; 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"; -export class Workloads extends React.Component { - static get tabRoutes(): TabLayoutRoute[] { +interface Dependencies { + isAllowedResource: IsAllowedResource; +} + +function getRouteTabs({ isAllowedResource }: Dependencies) { + return computed(() => { const tabs: TabLayoutRoute[] = [ { title: "Overview", @@ -93,11 +96,14 @@ export class Workloads extends React.Component { } return tabs; - } - - render() { - return ( - - ); - } + }); } + +const workloadsRouteTabsInjectable = getInjectable({ + instantiate: (di) => getRouteTabs({ + isAllowedResource: di.inject(isAllowedResourceInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default workloadsRouteTabsInjectable; diff --git a/src/renderer/components/+workloads/route.tsx b/src/renderer/components/+workloads/route.tsx new file mode 100644 index 0000000000..9778ef5ee9 --- /dev/null +++ b/src/renderer/components/+workloads/route.tsx @@ -0,0 +1,34 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./workloads.scss"; + +import React from "react"; +import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; +import type { IComputedValue } from "mobx"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import { observer } from "mobx-react"; +import workloadsRouteTabsInjectable from "./route-tabs.injectable"; + +export interface WorkloadsRouteProps {} + +interface Dependencies { + routes: IComputedValue; +} + +const NonInjectedWorkloadsRoute = observer(({ routes }: Dependencies & WorkloadsRouteProps) => ( + +)); + +export const WorkloadsRoute = withInjectables(NonInjectedWorkloadsRoute, { + getProps: (di, props) => ({ + routes: di.inject(workloadsRouteTabsInjectable), + ...props, + }), +}); + diff --git a/src/renderer/components/+workloads/sidebar-item.tsx b/src/renderer/components/+workloads/sidebar-item.tsx new file mode 100644 index 0000000000..36b9728bc7 --- /dev/null +++ b/src/renderer/components/+workloads/sidebar-item.tsx @@ -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 { workloadsRoute, workloadsURL } 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 workloadsRouteTabsInjectable from "./route-tabs.injectable"; + +export interface WorkloadSidebarItemProps {} + +interface Dependencies { + routes: IComputedValue; +} + +const NonInjectedWorkloadsSidebarItem = observer(({ routes }: Dependencies & WorkloadSidebarItemProps) => { + const tabRoutes = routes.get(); + + return ( + } + > + + + ); +}); + +export const WorkloadsSidebarItem = withInjectables(NonInjectedWorkloadsSidebarItem, { + getProps: (di, props) => ({ + routes: di.inject(workloadsRouteTabsInjectable), + ...props, + }), +}); diff --git a/src/renderer/components/+workloads/workloads.stores.ts b/src/renderer/components/+workloads/workloads.stores.ts deleted file mode 100644 index 8fb00c5a17..0000000000 --- a/src/renderer/components/+workloads/workloads.stores.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; -import { podsStore } from "../+workloads-pods/pods.store"; -import { deploymentStore } from "../+workloads-deployments/deployments.store"; -import { daemonSetStore } from "../+workloads-daemonsets/daemonsets.store"; -import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store"; -import { jobStore } from "../+workloads-jobs/job.store"; -import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; -import type { KubeResource } from "../../../common/rbac"; -import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; -import type { KubeObject } from "../../../common/k8s-api/kube-object"; - -export const workloadStores = new Map>([ - ["pods", podsStore], - ["deployments", deploymentStore], - ["daemonsets", daemonSetStore], - ["statefulsets", statefulSetStore], - ["replicasets", replicaSetStore], - ["jobs", jobStore], - ["cronjobs", cronJobStore], -]); diff --git a/src/renderer/components/dock/install-chart/view.tsx b/src/renderer/components/dock/install-chart/view.tsx index f162ae13a3..90a02a4a79 100644 --- a/src/renderer/components/dock/install-chart/view.tsx +++ b/src/renderer/components/dock/install-chart/view.tsx @@ -27,7 +27,7 @@ import type { IReleaseCreatePayload, IReleaseUpdateDetails } from "../../../../c import { withInjectables } from "@ogre-tools/injectable-react"; import installChartTabStoreInjectable from "./store.injectable"; import dockStoreInjectable from "../dock/store.injectable"; -import createReleaseInjectable from "../../+apps-releases/create-release/create-release.injectable"; +import createReleaseInjectable from "../../+helm-releases/create-release/create-release.injectable"; import { Notifications } from "../../notifications"; interface Props { diff --git a/src/renderer/components/dock/upgrade-chart/view.tsx b/src/renderer/components/dock/upgrade-chart/view.tsx index bff0491610..6309262195 100644 --- a/src/renderer/components/dock/upgrade-chart/view.tsx +++ b/src/renderer/components/dock/upgrade-chart/view.tsx @@ -15,13 +15,13 @@ import type { UpgradeChartTabStore } from "./store"; import { Spinner } from "../../spinner"; import { Badge } from "../../badge"; import { EditorPanel } from "../editor-panel"; -import { helmChartStore, IChartVersion } from "../../+apps-helm-charts/helm-chart.store"; +import { helmChartStore, IChartVersion } from "../../+helm-charts/helm-chart.store"; import type { HelmRelease, IReleaseUpdateDetails, IReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api"; import { Select, SelectOption } from "../../select"; import { IAsyncComputed, withInjectables } from "@ogre-tools/injectable-react"; import upgradeChartTabStoreInjectable from "./store.injectable"; -import updateReleaseInjectable from "../../+apps-releases/update-release/update-release.injectable"; -import releasesInjectable from "../../+apps-releases/releases.injectable"; +import updateReleaseInjectable from "../../+helm-releases/update-release/update-release.injectable"; +import releasesInjectable from "../../+helm-releases/releases.injectable"; interface Props { className?: string; @@ -136,8 +136,8 @@ export class NonInjectedUpgradeChart extends React.Component; + if (!release || !this.props.upgradeChartTabStore.isReady(tabId) || !version) { + return ; } const currentVersion = release.getVersion(); const controlsAndInfo = ( diff --git a/src/renderer/components/icon/icon.scss b/src/renderer/components/icon/icon.scss index b8d97cc3c8..de8d6fbdf2 100644 --- a/src/renderer/components/icon/icon.scss +++ b/src/renderer/components/icon/icon.scss @@ -111,7 +111,7 @@ } &.active { - color: var(--textColorAccent); + color: var(--color-active); box-shadow: 0 0 0 2px var(--iconActiveBackground); background-color: var(--iconActiveBackground); } diff --git a/src/renderer/components/item-object-list/content.tsx b/src/renderer/components/item-object-list/content.tsx index ccfdddd7d3..8877f04e02 100644 --- a/src/renderer/components/item-object-list/content.tsx +++ b/src/renderer/components/item-object-list/content.tsx @@ -7,7 +7,7 @@ import "./item-list-layout.scss"; import React, { ReactNode } from "react"; import { computed, makeObservable } from "mobx"; -import { observer } from "mobx-react"; +import { Observer, observer } from "mobx-react"; import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog"; import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallbacks } from "../table"; import { boundMethod, cssNames, IClassName, isReactNode, prevDefault, stopPropagation } from "../../utils"; @@ -70,75 +70,93 @@ export class ItemListLayoutContent extends React.Component @boundMethod getRow(uid: string) { - const { - isSelectable, renderTableHeader, renderTableContents, renderItemMenu, - store, hasDetailsView, onDetails, - copyClassNameFromHeadCells, customizeTableRowProps, detailsItem, - } = this.props; - const { isSelected } = store; - const item = this.props.getItems().find(item => item.getId() == uid); - - if (!item) return null; - const itemId = item.getId(); - return ( - onDetails(item)) : undefined} - {...customizeTableRowProps(item)} - > - {isSelectable && ( - store.toggleSelection(item))} - /> - )} - { - renderTableContents(item).map((content, index) => { - const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content; - const headCell = renderTableHeader?.[index]; +
+ + {() => { + const { + isSelectable, renderTableHeader, renderTableContents, renderItemMenu, + store, hasDetailsView, onDetails, + copyClassNameFromHeadCells, customizeTableRowProps, detailsItem, + } = this.props; + const { isSelected } = store; + const item = this.props.getItems().find(item => item.getId() == uid); - if (copyClassNameFromHeadCells && headCell) { - cellProps.className = cssNames(cellProps.className, headCell.className); - } + if (!item) return null; + const itemId = item.getId(); - if (!headCell || this.showColumn(headCell)) { - return ; - } + return ( + onDetails(item)) : undefined} + {...customizeTableRowProps(item)} + > + {isSelectable && ( + store.toggleSelection(item))} + /> + )} + {renderTableContents(item).map((content, index) => { + const cellProps: TableCellProps = isReactNode(content) + ? { children: content } + : content; + const headCell = renderTableHeader?.[index]; - return null; - }) - } - {renderItemMenu && ( - -
- {renderItemMenu(item, store)} -
-
- )} -
+ if (copyClassNameFromHeadCells && headCell) { + cellProps.className = cssNames( + cellProps.className, + headCell.className, + ); + } + + if (!headCell || this.showColumn(headCell)) { + return ; + } + + return null; + })} + {renderItemMenu && ( + +
+ {renderItemMenu(item, store)} +
+
+ )} + + ); + }} +
+
); } @boundMethod - removeItemsDialog() { + removeItemsDialog(selectedItems: I[]) { const { customizeRemoveDialog, store } = this.props; - const { selectedItems, removeSelectedItems } = store; const visibleMaxNamesCount = 5; const selectedNames = selectedItems.map(ns => ns.getName()).slice(0, visibleMaxNamesCount).join(", "); const dialogCustomProps = customizeRemoveDialog ? customizeRemoveDialog(selectedItems) : {}; const selectedCount = selectedItems.length; - const tailCount = selectedCount > visibleMaxNamesCount ? selectedCount - visibleMaxNamesCount : 0; - const tail = tailCount > 0 ? <>, and {tailCount} more : null; - const message = selectedCount <= 1 ?

Remove item {selectedNames}?

:

Remove {selectedCount} items {selectedNames}{tail}?

; + const tailCount = selectedCount > visibleMaxNamesCount + ? selectedCount - visibleMaxNamesCount + : 0; + const tail = tailCount > 0 + ? <>, and {tailCount} more + : null; + const message = selectedCount <= 1 + ?

Remove item {selectedNames}?

+ :

Remove {selectedCount} items {selectedNames}{tail}?

; + const onConfirm = store.removeItems + ? () => store.removeItems(selectedItems) + : store.removeSelectedItems; ConfirmDialog.open({ - ok: removeSelectedItems, + ok: onConfirm, labelOk: "Remove", message, ...dialogCustomProps, @@ -190,11 +208,15 @@ export class ItemListLayoutContent extends React.Component return ( {isSelectable && ( - store.toggleSelectionAll(enabledItems))} - /> + + {() => ( + store.toggleSelectionAll(enabledItems))} + /> + )} + )} {renderTableHeader.map((cellProps, index) => ( this.showColumn(cellProps) && ( @@ -211,11 +233,12 @@ export class ItemListLayoutContent extends React.Component render() { const { store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, - detailsItem, className, tableProps = {}, tableId, + detailsItem, className, tableProps = {}, tableId, getItems, } = this.props; - const { selectedItems } = store; const selectedItemId = detailsItem && detailsItem.getId(); const classNames = cssNames(className, "box", "grow", ThemeStore.getInstance().activeTheme.type); + const items = getItems(); + const selectedItems = store.pickOnlySelected(items); return (
@@ -225,7 +248,7 @@ export class ItemListLayoutContent extends React.Component selectable={hasDetailsView} sortable={sortingCallbacks} getTableRow={this.getRow} - items={this.props.getItems()} + items={items} selectedItemId={selectedItemId} noItems={this.renderNoItems()} className={classNames} @@ -234,11 +257,20 @@ export class ItemListLayoutContent extends React.Component {this.renderTableHeader()} {this.renderItems()} - + + + {() => ( + 0 + ? () => this.removeItemsDialog(selectedItems) + : null + } + removeTooltip={`Remove selected items (${selectedItems.length})`} + {...addRemoveButtons} + /> + )} +
); } diff --git a/src/renderer/components/item-object-list/list-layout.tsx b/src/renderer/components/item-object-list/list-layout.tsx index 959f814bce..ca8a4b1293 100644 --- a/src/renderer/components/item-object-list/list-layout.tsx +++ b/src/renderer/components/item-object-list/list-layout.tsx @@ -13,7 +13,7 @@ import { boundMethod, cssNames, IClassName, noop, StorageHelper } from "../../ut import type { AddRemoveButtonsProps } from "../add-remove-buttons"; import type { ItemObject, ItemStore } from "../../../common/item.store"; import type { SearchInputUrlProps } from "../input"; -import { Filter, FilterType, pageFilters } from "./page-filters.store"; +import { FilterType, pageFilters } from "./page-filters.store"; import { PageFiltersList } from "./page-filters-list"; import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store"; import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; @@ -202,19 +202,18 @@ class NonInjectedItemListLayout extends React.Component(this.filters, ({ type }) => type); - + const filterGroups = groupBy(this.filters, ({ type }) => type); const filterItems: ItemsFilter[] = []; - Object.entries(filterGroups).forEach(([type, filtersGroup]) => { + for (const [type, filtersGroup] of Object.entries(filterGroups)) { const filterCallback = this.filterCallbacks[type] ?? this.props.filterCallbacks?.[type]; if (filterCallback && filtersGroup.length > 0) { filterItems.push(filterCallback); } - }); + } - const items = this.props.getItems ? this.props.getItems() : (this.props.items ?? this.props.store.items); + const items = this.props.getItems?.() ?? this.props.items ?? this.props.store.items; return applyFilters(filterItems.concat(this.props.filterItems), items); } diff --git a/src/renderer/components/kube-detail-params/params.ts b/src/renderer/components/kube-detail-params/params.ts index 953b666406..9eeb141e3f 100644 --- a/src/renderer/components/kube-detail-params/params.ts +++ b/src/renderer/components/kube-detail-params/params.ts @@ -46,7 +46,6 @@ export function hideDetails() { } export function getDetailsUrl(selfLink: string, resetSelected = false, mergeGlobals = true) { - console.debug("getDetailsUrl", { selfLink, resetSelected, mergeGlobals }); const params = new URLSearchParams(mergeGlobals ? navigation.searchParams : ""); params.set(kubeDetailsUrlParam.name, selfLink); diff --git a/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx b/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx index de1253fd65..5ecf4f1425 100644 --- a/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx +++ b/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx @@ -138,23 +138,14 @@ class NonInjectedKubeObjectListLayout extends React.Compon } } -export function KubeObjectListLayout( - props: KubeObjectListLayoutProps, -) { - const InjectedKubeObjectListLayout = withInjectables< - Dependencies, - KubeObjectListLayoutProps - >( - NonInjectedKubeObjectListLayout, - - { - getProps: (di, props) => ({ - clusterFrameContext: di.inject(clusterFrameContextInjectable), - subscribeToStores: di.inject(kubeWatchApiInjectable).subscribeStores, - ...props, - }), - }, - ); +const InjectedKubeObjectListLayout = withInjectables>(NonInjectedKubeObjectListLayout, { + getProps: (di, props) => ({ + clusterFrameContext: di.inject(clusterFrameContextInjectable), + subscribeToStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), +}); +export function KubeObjectListLayout(props: KubeObjectListLayoutProps) { return ; } diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.tsx index 6d425ab9c3..83cba3ade7 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.tsx @@ -108,25 +108,25 @@ class NonInjectedKubeObjectMenu extends React.Co } } +const InjectedKubeObjectMenu = withInjectables>( + NonInjectedKubeObjectMenu, + { + getProps: (di, props) => ({ + clusterName: di.inject(clusterNameInjectable), + apiManager: di.inject(apiManagerInjectable), + createEditResourceTab: di.inject(createEditResourceTabInjectable), + hideDetails: di.inject(hideDetailsInjectable), + + kubeObjectMenuItems: di.inject(kubeObjectMenuItemsInjectable, { + kubeObject: props.object, + }), + ...props, + }), + }, +); + export function KubeObjectMenu( props: KubeObjectMenuProps, ) { - const InjectedKubeObjectMenu = withInjectables>( - NonInjectedKubeObjectMenu, - { - getProps: (di, props) => ({ - clusterName: di.inject(clusterNameInjectable), - apiManager: di.inject(apiManagerInjectable), - createEditResourceTab: di.inject(createEditResourceTabInjectable), - hideDetails: di.inject(hideDetailsInjectable), - - kubeObjectMenuItems: di.inject(kubeObjectMenuItemsInjectable, { - kubeObject: props.object, - }), - ...props, - }), - }, - ); - return ; } diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index 3600eb482f..69d3cbfda4 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -7,99 +7,34 @@ import styles from "./sidebar.module.scss"; import type { TabLayoutRoute } from "./tab-layout"; import React from "react"; -import { disposeOnUnmount, observer } from "mobx-react"; -import { cssNames, Disposer } from "../../utils"; -import { Icon } from "../icon"; -import { Workloads } from "../+workloads"; -import { UserManagement } from "../+user-management"; -import { Storage } from "../+storage"; -import { Network } from "../+network"; -import { crdStore } from "../+custom-resources/crd.store"; -import { CustomResources } from "../+custom-resources/custom-resources"; +import { observer } from "mobx-react"; +import { cssNames } from "../../utils"; import { isActiveRoute } from "../../navigation"; -import { isAllowedResource } from "../../../common/utils/allowed-resource"; -import { Spinner } from "../spinner"; import { ClusterPageMenuRegistration, ClusterPageMenuRegistry, ClusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries"; import { SidebarItem } from "./sidebar-item"; -import { Apps } from "../+apps"; -import * as routes from "../../../common/routes"; -import { Config } from "../+config"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { SidebarCluster } from "./sidebar-cluster"; -import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; -import type { KubeObject } from "../../../common/k8s-api/kube-object"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import kubeWatchApiInjectable - from "../../kube-watch-api/kube-watch-api.injectable"; +import { TabRoutesSidebarItems } from "./tab-routes-sidebar-items"; +import { ConfigSidebarItem } from "../+config/sidebar-item"; +import { ClusterSidebarItem } from "../+cluster/sidebar-item"; +import { NodesSidebarItem } from "../+nodes/sidebar-item"; +import { WorkloadsSidebarItem } from "../+workloads/sidebar-item"; +import { NetworkSidebarItem } from "../+network/sidebar-item"; +import { StorageSidebarItem } from "../+storage/sidebar-item"; +import { NamespacesSidebarItem } from "../+namespaces/sidebar-item"; +import { EventsSidebarItem } from "../+events/sidebar-item"; +import { HelmSidebarItem } from "../+helm/sidebar-item"; +import { UserManagementSidebarItem } from "../+user-management/sidebar-item"; +import { CustomResourcesSidebarItem } from "../+custom-resources/sidebar-item"; -interface Props { +interface SidebarProps { className?: string; } -interface Dependencies { - subscribeStores: (stores: KubeObjectStore[]) => Disposer -} - @observer -class NonInjectedSidebar extends React.Component { +export class Sidebar extends React.Component { static displayName = "Sidebar"; - componentDidMount() { - disposeOnUnmount(this, [ - this.props.subscribeStores([ - crdStore, - ]), - ]); - } - - renderCustomResources() { - if (crdStore.isLoading) { - return ( -
- -
- ); - } - - return Object.entries(crdStore.groups).map(([group, crds]) => { - const id = `crd-group:${group}`; - const crdGroupsPageUrl = routes.crdURL({ query: { groups: group }}); - - return ( - - {crds.map((crd) => ( - - ))} - - ); - }); - } - - renderTreeFromTabRoutes(tabRoutes: TabLayoutRoute[] = []): React.ReactNode { - if (!tabRoutes.length) { - return null; - } - - return tabRoutes.map(({ title, routePath, url = routePath, exact = true }) => { - const subMenuItemId = `tab-route-item-${url}`; - - return ( - - ); - }); - } - getTabLayoutRoutes(menu: ClusterPageMenuRegistration): TabLayoutRoute[] { if (!menu.id) { return []; @@ -169,7 +104,7 @@ class NonInjectedSidebar extends React.Component { text={menuItem.title} icon={} > - {this.renderTreeFromTabRoutes(tabRoutes)} + ); }); @@ -186,122 +121,20 @@ class NonInjectedSidebar extends React.Component {
- } - /> - } - /> - } - > - {this.renderTreeFromTabRoutes(Workloads.tabRoutes)} - - } - > - {this.renderTreeFromTabRoutes(Config.tabRoutes)} - - } - > - {this.renderTreeFromTabRoutes(Network.tabRoutes)} - - } - > - {this.renderTreeFromTabRoutes(Storage.tabRoutes)} - - } - /> - } - /> - } - > - {this.renderTreeFromTabRoutes(Apps.tabRoutes)} - - } - > - {this.renderTreeFromTabRoutes(UserManagement.tabRoutes)} - - } - > - {this.renderTreeFromTabRoutes(CustomResources.tabRoutes)} - {this.renderCustomResources()} - + + + + + + + + + + + {this.renderRegisteredMenus()}
); } } - -export const Sidebar = withInjectables( - NonInjectedSidebar, - - { - getProps: (di, props) => ({ - subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, - ...props, - }), - }, -); diff --git a/src/renderer/components/layout/tab-routes-sidebar-items.tsx b/src/renderer/components/layout/tab-routes-sidebar-items.tsx new file mode 100644 index 0000000000..8445cae1f1 --- /dev/null +++ b/src/renderer/components/layout/tab-routes-sidebar-items.tsx @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import React from "react"; +import { isActiveRoute } from "../../navigation"; +import { SidebarItem } from "./sidebar-item"; +import type { TabLayoutRoute } from "./tab-layout"; + +export interface SidebarTreeProps { + routes: TabLayoutRoute[]; +} + +function withId(src: TabLayoutRoute) { + return { + ...src, + id: `tab-route-item-${src.url ?? src.routePath}`, + }; +} + +export const TabRoutesSidebarItems = ({ routes }: SidebarTreeProps) => ( + <> + { + routes + .map(withId) + .map(({ title, routePath, url = routePath, exact = true, id }) => ( + + )) + } + +); diff --git a/src/renderer/components/table/table.tsx b/src/renderer/components/table/table.tsx index 4823626094..be9a6f6ac8 100644 --- a/src/renderer/components/table/table.tsx +++ b/src/renderer/components/table/table.tsx @@ -246,18 +246,18 @@ class NonInjectedTable extends React.Component & Dependen } } +const InjectedTable = withInjectables>( + NonInjectedTable, + + { + getProps: (di, props) => ({ + model: di.inject(tableModelInjectable), + ...props, + }), + }, +); + export function Table(props: TableProps) { - const InjectedTable = withInjectables>( - NonInjectedTable, - - { - getProps: (di, props) => ({ - model: di.inject(tableModelInjectable), - ...props, - }), - }, - ); - return ; } diff --git a/src/renderer/frames/cluster-frame/cluster-frame.tsx b/src/renderer/frames/cluster-frame/cluster-frame.tsx index 9ef25ab844..9392b9f4c8 100755 --- a/src/renderer/frames/cluster-frame/cluster-frame.tsx +++ b/src/renderer/frames/cluster-frame/cluster-frame.tsx @@ -3,17 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import React from "react"; -import { observable, makeObservable } from "mobx"; +import { makeObservable, computed } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { Redirect, Route, Router, Switch } from "react-router"; -import { UserManagement } from "../../components/+user-management/user-management"; +import { UserManagementRoute } from "../../components/+user-management/route"; import { ConfirmDialog } from "../../components/confirm-dialog"; import { ClusterOverview } from "../../components/+cluster/cluster-overview"; import { Events } from "../../components/+events/events"; import { DeploymentScaleDialog } from "../../components/+workloads-deployments/deployment-scale-dialog"; import { CronJobTriggerDialog } from "../../components/+workloads-cronjobs/cronjob-trigger-dialog"; -import { CustomResources } from "../../components/+custom-resources/custom-resources"; -import { isAllowedResource } from "../../../common/utils/allowed-resource"; +import { CustomResourcesRoute } from "../../components/+custom-resources/route"; import { ClusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries/page-registry"; import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../../../extensions/registries"; import { StatefulSetScaleDialog } from "../../components/+workloads-statefulsets/statefulset-scale-dialog"; @@ -28,13 +27,12 @@ import { KubeObjectDetails } from "../../components/kube-object-details"; import { KubeConfigDialog } from "../../components/kubeconfig-dialog"; import { Sidebar } from "../../components/layout/sidebar"; import { Dock } from "../../components/dock"; -import { Apps } from "../../components/+apps"; -import { Namespaces } from "../../components/+namespaces"; -import { Network } from "../../components/+network"; -import { Nodes } from "../../components/+nodes"; -import { Workloads } from "../../components/+workloads"; -import { Config } from "../../components/+config"; -import { Storage } from "../../components/+storage"; +import { NamespacesRoute } from "../../components/+namespaces/route"; +import { NetworkRoute } from "../../components/+network/route"; +import { NodesRoute } from "../../components/+nodes/route"; +import { WorkloadsRoute } from "../../components/+workloads/route"; +import { ConfigRoute } from "../../components/+config/route"; +import { StorageRoute } from "../../components/+storage/route"; import { watchHistoryState } from "../../remote-helpers/history-updater"; import { PortForwardDialog } from "../../port-forward"; import { DeleteClusterDialog } from "../../components/delete-cluster-dialog"; @@ -42,19 +40,24 @@ import type { NamespaceStore } from "../../components/+namespaces/namespace-stor import { withInjectables } from "@ogre-tools/injectable-react"; import namespaceStoreInjectable from "../../components/+namespaces/namespace-store/namespace-store.injectable"; import type { ClusterId } from "../../../common/cluster-types"; -import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster/hosted-cluster.injectable"; +import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster.injectable"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import type { Disposer } from "../../../common/utils"; import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; import historyInjectable from "../../navigation/history.injectable"; import type { History } from "history"; +import type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable"; +import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable"; +import { HelmRoute } from "../../components/+helm/route"; +import type { KubeResource } from "../../../common/rbac"; interface Dependencies { - history: History, - namespaceStore: NamespaceStore - hostedClusterId: ClusterId - subscribeStores: (stores: KubeObjectStore[]) => Disposer + history: History; + namespaceStore: NamespaceStore; + hostedClusterId: ClusterId; + subscribeStores: (stores: KubeObjectStore[]) => Disposer; + isAllowedResource: IsAllowedResource; } @observer @@ -75,7 +78,13 @@ class NonInjectedClusterFrame extends React.Component { ]); } - @observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? routes.clusterURL() : routes.workloadsURL(); + @computed get startUrl() { + const resources : KubeResource[] = ["events", "nodes", "pods"]; + + return resources.every(x => this.props.isAllowedResource(x)) + ? routes.clusterURL() + : routes.workloadsURL(); + } getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) { const routes: TabLayoutRoute[] = []; @@ -138,18 +147,17 @@ class NonInjectedClusterFrame extends React.Component { } footer={}> - - - - - - - + + + + + + - - - + + + {this.renderExtensionTabLayoutRoutes()} {this.renderExtensionRoutes()} @@ -185,5 +193,6 @@ export const ClusterFrame = withInjectables(NonInjectedClusterFram namespaceStore: di.inject(namespaceStoreInjectable), hostedClusterId: di.inject(hostedClusterInjectable).id, subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + isAllowedResource: di.inject(isAllowedResourceInjectable), }), }); diff --git a/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts b/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts index 7553877515..d8767fea27 100644 --- a/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts +++ b/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts @@ -7,7 +7,7 @@ import { initClusterFrame } from "./init-cluster-frame"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import catalogEntityRegistryInjectable from "../../../api/catalog-entity-registry/catalog-entity-registry.injectable"; import frameRoutingIdInjectable from "./frame-routing-id/frame-routing-id.injectable"; -import hostedClusterInjectable from "../../../../common/cluster-store/hosted-cluster/hosted-cluster.injectable"; +import hostedClusterInjectable from "../../../../common/cluster-store/hosted-cluster.injectable"; import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable"; import clusterFrameContextInjectable from "../../../cluster-frame-context/cluster-frame-context.injectable"; diff --git a/src/renderer/initializers/kube-object-detail-registry.tsx b/src/renderer/initializers/kube-object-detail-registry.tsx index eef56b16b5..74dff0a7b5 100644 --- a/src/renderer/initializers/kube-object-detail-registry.tsx +++ b/src/renderer/initializers/kube-object-detail-registry.tsx @@ -19,7 +19,7 @@ import { EndpointDetails } from "../components/+network-endpoints"; import { IngressDetails } from "../components/+network-ingresses"; import { NetworkPolicyDetails } from "../components/+network-policies"; import { ServiceDetails } from "../components/+network-services"; -import { NodeDetails } from "../components/+nodes"; +import { NodeDetails } from "../components/+nodes/details"; import { PodSecurityPolicyDetails } from "../components/+pod-security-policies"; import { StorageClassDetails } from "../components/+storage-classes"; import { PersistentVolumeClaimDetails } from "../components/+storage-volume-claims"; diff --git a/src/renderer/initializers/workload-events.tsx b/src/renderer/initializers/workload-events.tsx new file mode 100644 index 0000000000..be58a5e39f --- /dev/null +++ b/src/renderer/initializers/workload-events.tsx @@ -0,0 +1,38 @@ +/** + * 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 type { IsAllowedResource } from "../../common/utils/is-allowed-resource.injectable"; +import isAllowedResourceInjectable from "../../common/utils/is-allowed-resource.injectable"; +import { Events } from "../components/+events/events"; + +export interface WorkloadEventsProps {} + +interface Dependencies { + isAllowedResource: IsAllowedResource; +} + +const NonInjectedWorkloadEvents = observer(({ isAllowedResource }: Dependencies & WorkloadEventsProps) => { + if (!isAllowedResource("events")) { + return null; + } + + return ( + + ); +}); + +export const WorkloadEvents = withInjectables(NonInjectedWorkloadEvents, { + getProps: (di, props) => ({ + isAllowedResource: di.inject(isAllowedResourceInjectable), + ...props, + }), +}); diff --git a/src/renderer/initializers/workloads-overview-detail-registry.tsx b/src/renderer/initializers/workloads-overview-detail-registry.tsx index 9ab0e17ccb..5654281b6b 100644 --- a/src/renderer/initializers/workloads-overview-detail-registry.tsx +++ b/src/renderer/initializers/workloads-overview-detail-registry.tsx @@ -3,11 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import React from "react"; -import { isAllowedResource } from "../../common/utils/allowed-resource"; import { WorkloadsOverviewDetailRegistry } from "../../extensions/registries"; -import { Events } from "../components/+events"; import { OverviewStatuses } from "../components/+workloads-overview/overview-statuses"; +import { WorkloadEvents } from "./workload-events"; export function initWorkloadsOverviewDetailRegistry() { WorkloadsOverviewDetailRegistry.getInstance() @@ -20,9 +18,7 @@ export function initWorkloadsOverviewDetailRegistry() { { priority: 5, components: { - Details: () => ( - isAllowedResource("events") && - ), + Details: WorkloadEvents, }, }, ]); diff --git a/src/renderer/kube-watch-api/kube-watch-api.ts b/src/renderer/kube-watch-api/kube-watch-api.ts index f38ffcec1c..6652d4f15f 100644 --- a/src/renderer/kube-watch-api/kube-watch-api.ts +++ b/src/renderer/kube-watch-api/kube-watch-api.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { comparer, reaction } from "mobx"; -import { disposer, Disposer, noop } from "../../common/utils"; +import { disposer, Disposer, noop, WrappedAbortController } from "../../common/utils"; import type { KubeObject } from "../../common/k8s-api/kube-object"; import AbortController from "abort-controller"; import { once } from "lodash"; @@ -14,16 +14,6 @@ import logger from "../../common/logger"; // Kubernetes watch-api client // API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams -class WrappedAbortController extends AbortController { - constructor(protected parent: AbortController) { - super(); - - parent.signal.addEventListener("abort", () => { - this.abort(); - }); - } -} - interface SubscribeStoreParams { store: KubeObjectStore; parent: AbortController; @@ -58,7 +48,7 @@ class WatchCount { throw new Error(`Cannot dec count more times than it has been inc: ${store.api.objectConstructor.kind}`); } - logger.debug(`[KUBE-WATCH-API]: dec() count for ${store.api.objectConstructor.apiBase} is now ${newCount}`); + logger.info(`[KUBE-WATCH-API]: dec() count for ${store.api.objectConstructor.apiBase} is now ${newCount}`); this.#data.set(store, newCount); return newCount; @@ -83,6 +73,8 @@ interface Dependencies { clusterFrameContext: ClusterFrameContext } +export type SubscribeStores = (stores: KubeObjectStore[], opts?: KubeWatchSubscribeStoreOptions) => Disposer; + export class KubeWatchApi { #watch = new WatchCount(); @@ -153,7 +145,7 @@ export class KubeWatchApi { }; } - subscribeStores = (stores: KubeObjectStore[], { namespaces, onLoadFailure }: KubeWatchSubscribeStoreOptions = {}): Disposer => { + subscribeStores: SubscribeStores = (stores, { namespaces, onLoadFailure } = {}) => { const parent = new AbortController(); const unsubscribe = disposer( ...stores.map(store => this.subscribeStore({ diff --git a/src/renderer/port-forward/port-forward-store/port-forward-store.ts b/src/renderer/port-forward/port-forward-store/port-forward-store.ts index 070f6746b7..5ec58002e6 100644 --- a/src/renderer/port-forward/port-forward-store/port-forward-store.ts +++ b/src/renderer/port-forward/port-forward-store/port-forward-store.ts @@ -73,8 +73,8 @@ export class PortForwardStore extends ItemStore { }); } - async removeSelectedItems() { - return Promise.all(this.selectedItems.map(this.remove)); + async removeItems(items: PortForwardItem[]) { + await Promise.all(items.map(this.remove)); } getById(id: string) { diff --git a/yarn.lock b/yarn.lock index 96849d8771..c824c443e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -958,19 +958,19 @@ dependencies: lodash "^4.17.21" -"@ogre-tools/injectable-react@3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-3.2.0.tgz#468542e846952deb8e7a4f6757da4813ee8f11fa" - integrity sha512-VU5l0uKe86psVzEPbXl1TLlflnoL+uSeOaOCy/mAGzau4nqRb+eA4RzYgzUs/D9tDYzJ7Es+LZWD9uSyXmpyXg== +"@ogre-tools/injectable-react@3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-3.2.1.tgz#b0bff8c32a2eb77653cc4b0496cd123c80cfaae5" + integrity sha512-ufC9bR5nnO/cQJ6TiIxKdg0j2liGPTb6XvuK/0QGeGwd534giMHHL5L4CxF6qRLX6QDQ3FejhOTUn7+DV5uugg== dependencies: "@ogre-tools/fp" "^3.0.0" - "@ogre-tools/injectable" "^3.2.0" + "@ogre-tools/injectable" "^3.2.1" lodash "^4.17.21" -"@ogre-tools/injectable@3.2.0", "@ogre-tools/injectable@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-3.2.0.tgz#7d8f653cb3a2c0253a29422bcffd5123308600a9" - integrity sha512-aRlRdvLefJMBvFu1tRlTGNgpMbqE250lwMXT8Y6/ruC88rCL+TygCWcsJELad1OgqX1cfHkCgCYeQeohV+G3Zg== +"@ogre-tools/injectable@3.2.1", "@ogre-tools/injectable@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-3.2.1.tgz#30ca60f47b84c91219acb333d4342df4ccd2827c" + integrity sha512-IEG3IIYTrTkKemDkxcI+qL8EDDQB3RS8k1qxy4obQ9g3SAURWUfB1j02dBbcIYmtOHEXWtP3AY57HE5sPG0prQ== dependencies: "@ogre-tools/fp" "^3.0.0" lodash "^4.17.21" @@ -1775,10 +1775,10 @@ "@types/react-dom" "*" "@types/react-transition-group" "*" -"@types/react-table@^7.7.8": - version "7.7.8" - resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.8.tgz#b1aa5fb7a54432969262d2306b87fdbb9a5ee647" - integrity sha512-OMhbPlf+uUGte3M1WdArEKeBkyQ1XJxKvFYs+o1dGGGyaSVIqxPPQmBZ6Skkw0V9y0F/kOY7rnTD8r9GbfpBOg== +"@types/react-table@^7.7.9": + version "7.7.9" + resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.9.tgz#ea82875775fc6ee71a28408dcc039396ae067c92" + integrity sha512-ejP/J20Zlj9VmuLh73YgYkW2xOSFTW39G43rPH93M4mYWdMmqv66lCCr+axZpkdtlNLGjvMG2CwzT4S6abaeGQ== dependencies: "@types/react" "*"