From a277cfcf02730aa18932058eeb059e5dc8c34365 Mon Sep 17 00:00:00 2001 From: Iku-turso Date: Thu, 31 Mar 2022 16:57:05 +0300 Subject: [PATCH] Technical requirements for behavioural unit tests (#5084) * Implement a lot of technical requirements for behavioural unit tests Note: the crux of this was to make routing env-agnostic, and not based on URLs as magic strings, but instead something type-enforced. Note: extension-based routes comply to same exact interface by "late-registering" their routes when installed. Routes are just injectables. Note: another chunk of global shared state is no more. Note: a lot of explicit side effects have been cornered to injectables. Note: a lot of stuff has become reactive as part if this. Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make a directory commonly available Signed-off-by: Iku-turso * Require id for to satisfy previous commit Signed-off-by: Iku-turso * Prevent explicit side effect in component by using existing dependency instead Signed-off-by: Iku-turso * Extract instantiation of "conf" as injectables for causing side effects Signed-off-by: Iku-turso * Introduce a legacy-helper to make gradual refactoring of inheritors of Singleton easier Signed-off-by: Iku-turso * Make legacy unit tests for hotbar green and more simple by using the new legacy helper Signed-off-by: Iku-turso * Temporarily kludge all unit tests green with a disclaimer about allowing side-effects Signed-off-by: Iku-turso * Remove kludge in previous commit by explicitly permitting specific side effects where old unit tests require it Signed-off-by: Iku-turso * Prevent old unit test with side effects from accessing file system Signed-off-by: Iku-turso * Migrate to actual typing for di.permitSideEffects Signed-off-by: Iku-turso * Prevent unit tests from failing because of non-standard method of HTML-element not present in js-dom Signed-off-by: Iku-turso * Adapt integration tests to recent changes Signed-off-by: Iku-turso * Fix code style Signed-off-by: Iku-turso * Fix artifact from bad rebase Signed-off-by: Iku-turso * Add a deprecation from a review comment Signed-off-by: Iku-turso * Remove change that is not required Signed-off-by: Janne Savolainen * Remove redundant comment Signed-off-by: Janne Savolainen * Fix code style Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove redundant file Signed-off-by: Janne Savolainen * Fix bad merge Signed-off-by: Iku-turso * Improve variable name Signed-off-by: Iku-turso * Tweak logger interface to be more descriptive Signed-off-by: Janne Savolainen * Make injecting legacy singleton always provide new instance Signed-off-by: Janne Savolainen * Remove conditional typing when not needed Signed-off-by: Janne Savolainen * Improve naming of variable Signed-off-by: Janne Savolainen * Remove unnecessary code style changes Signed-off-by: Janne Savolainen * Remove flag for causing side effects from too broad scope Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Override side-effects in unit test using injectable instead of monkey patching Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Flag some side-effects and add general overrides Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Fix unit tests in CI by removing explicit side-effect Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Remove explicit side-effect from getting default shell Signed-off-by: Janne Savolainen * Introduce abstraction for getting absolute paths Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Switch to using abstraction for getting absolute path to control explicit side effect Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce abstraction for joining paths Signed-off-by: Janne Savolainen * Switch to using abstraction for joining paths to control explicit side effect Signed-off-by: Janne Savolainen * Fix fake implementation for join paths Signed-off-by: Janne Savolainen * Fix test after removing explicit side effect Signed-off-by: Janne Savolainen * Remove explicit side effects from kubeconfig-syncs Signed-off-by: Janne Savolainen * Fix arguments after removing explicit side effect Signed-off-by: Janne Savolainen * Make registrators not async for not being needed anymore Signed-off-by: Janne Savolainen * Make generalCatalogEntities non-observable, as there is no requirement Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Remove redundant code Co-authored-by: Mikko Aspiala Signed-off-by: Iku-turso * Simplify logic for registering general catalog entity sources Signed-off-by: Janne Savolainen * Add TODO Signed-off-by: Janne Savolainen * Replace function for getting application menu items with reactive solution Signed-off-by: Janne Savolainen * Fix typo in interface name Signed-off-by: Janne Savolainen * Remove global shared state usages of hot bar store Signed-off-by: Janne Savolainen * Remove redundant enum Signed-off-by: Janne Savolainen Co-authored-by: Janne Savolainen --- .../__tests__/app-preferences.tests.ts | 2 +- integration/__tests__/cluster-pages.tests.ts | 810 +++++++++--------- package.json | 5 +- src/common/__tests__/base-store.test.ts | 16 +- src/common/__tests__/cluster-store.test.ts | 18 +- src/common/__tests__/hotbar-store.test.ts | 460 +++++----- src/common/__tests__/user-store.test.ts | 35 +- src/common/app-paths/app-paths.test.ts | 9 +- .../directory-for-binaries.injectable.ts | 10 +- .../directory-for-kube-configs.injectable.ts | 13 +- ...rectory-for-kubectl-binaries.injectable.ts | 21 + ...custom-kube-config-directory.injectable.ts | 11 +- src/common/base-store.ts | 12 +- .../general-catalog-entity-injection-token.ts | 10 + .../catalog-catalog-entity.injectable.ts | 41 + .../preferences-catalog-entity.injectable.ts | 41 + .../welcome-catalog-entity.injectable.ts | 41 + .../cluster-store/cluster-store.injectable.ts | 2 + ...ctory-for-lens-local-storage.injectable.ts | 14 +- .../navigate-to-front-page.injectable.ts | 13 + .../navigate-to-route-injection-token.ts | 52 ++ .../navigate-to-url-injection-token.ts | 16 + .../navigation-ipc-channel.ts | 9 + .../route-injection-token.ts | 20 + .../add-cluster-route.injectable.ts | 21 + .../navigate-to-add-cluster.injectable.ts | 20 + .../catalog/catalog-route.injectable.ts | 26 + .../catalog/navigate-to-catalog.injectable.ts | 25 + .../cluster-view-route.injectable.ts | 21 + .../navigate-to-cluster-view.injectable.ts | 23 + .../config-maps-route.injectable.ts | 25 + .../navigate-to-config-maps.injectable.ts | 20 + ...zontal-pod-autoscalers-route.injectable.ts | 25 + ...o-horizontal-pod-autoscalers.injectable.ts | 20 + .../limit-ranges-route.injectable.ts | 28 + .../navigate-to-limit-ranges.injectable.ts | 20 + ...te-to-pod-disruption-budgets.injectable.ts | 20 + ...pod-disruption-budgets-route.injectable.ts | 25 + .../navigate-to-resource-quotas.injectable.ts | 20 + .../resource-quotas-route.injectable.ts | 25 + .../secrets/navigate-to-secrets.injectable.ts | 20 + .../secrets/secrets-route.injectable.ts | 25 + .../crd-list/crd-list-route.injectable.ts | 21 + .../navigate-to-crd-list.injectable.ts | 20 + .../custom-resources-route.injectable.ts | 26 + ...navigate-to-custom-resources.injectable.ts | 21 + .../cluster/events/events-route.injectable.ts | 25 + .../events/navigate-to-events.injectable.ts | 20 + .../charts/helm-charts-route.injectable.ts | 26 + .../navigate-to-helm-charts.injectable.ts | 25 + .../helm-releases-route.injectable.ts | 26 + .../navigate-to-helm-releases.injectable.ts | 23 + .../namespaces/namespaces-route.injectable.ts | 25 + .../navigate-to-namespaces.injectable.ts | 20 + .../endpoints/endpoints-route.injectable.ts | 25 + .../navigate-to-endpoints.injectable.ts | 20 + .../ingresses/ingresses-route.injectable.ts | 25 + .../navigate-to-ingresses.injectable.ts | 20 + ...navigate-to-network-policies.injectable.ts | 20 + .../network-policies-route.injectable.ts | 25 + .../navigate-to-port-forwards.injectable.ts | 23 + .../port-forwards-route.injectable.ts | 25 + .../navigate-to-services.injectable.ts | 20 + .../services/services-route.injectable.ts | 25 + .../nodes/navigate-to-nodes.injectable.ts | 20 + .../cluster/nodes/nodes-route.injectable.ts | 25 + .../cluster-overview-route.injectable.ts | 25 + ...navigate-to-cluster-overview.injectable.ts | 20 + ...-to-persistent-volume-claims.injectable.ts | 20 + ...rsistent-volume-claims-route.injectable.ts | 25 + ...vigate-to-persistent-volumes.injectable.ts | 20 + .../persistent-volumes-route.injectable.ts | 25 + .../navigate-to-storage-classes.injectable.ts | 20 + .../storage-classes-route.injectable.ts | 25 + .../cluster-role-bindings-route.injectable.ts | 25 + ...ate-to-cluster-role-bindings.injectable.ts | 20 + .../cluster-roles-route.injectable.ts | 25 + .../navigate-to-cluster-roles.injectable.ts | 20 + ...ate-to-pod-security-policies.injectable.ts | 20 + .../pod-security-policies-route.injectable.ts | 25 + .../navigate-to-role-bindings.injectable.ts | 20 + .../role-bindings-route.injectable.ts | 25 + .../roles/navigate-to-roles.injectable.ts | 20 + .../roles/roles-route.injectable.ts | 25 + ...navigate-to-service-accounts.injectable.ts | 20 + .../service-accounts-route.injectable.ts | 25 + .../cron-jobs/cron-jobs-route.injectable.ts | 25 + .../navigate-to-cron-jobs.injectable.ts | 20 + .../daemonsets/daemonsets-route.injectable.ts | 25 + .../navigate-to-daemonsets.injectable.ts | 20 + .../deployments-route.injectable.ts | 25 + .../navigate-to-deployments.injectable.ts | 20 + .../workloads/jobs/jobs-route.injectable.ts | 25 + .../jobs/navigate-to-jobs.injectable.ts | 20 + ...vigate-to-workloads-overview.injectable.ts | 20 + .../workloads-overview-route.injectable.ts | 21 + .../pods/navigate-to-pods.injectable.ts | 20 + .../workloads/pods/pods-route.injectable.ts | 25 + .../navigate-to-replicasets.injectable.ts | 20 + .../replicasets-route.injectable.ts | 25 + .../navigate-to-statefulsets.injectable.ts | 20 + .../statefulsets-route.injectable.ts | 25 + .../entity-settings-route.injectable.ts | 25 + .../navigate-to-entity-settings.injectable.ts | 23 + .../extensions/extensions-route.injectable.ts | 21 + .../navigate-to-extensions.injectable.ts | 20 + .../app/app-preferences-route.injectable.ts | 21 + .../navigate-to-app-preferences.injectable.ts | 20 + .../editor-preferences-route.injectable.ts | 21 + ...vigate-to-editor-preferences.injectable.ts | 20 + .../extension-preferences-route.injectable.ts | 21 + ...ate-to-extension-preferences.injectable.ts | 20 + ...kubernetes-preferences-route.injectable.ts | 21 + ...te-to-kubernetes-preferences.injectable.ts | 20 + .../navigate-to-preferences.injectable.ts | 14 + ...avigate-to-proxy-preferences.injectable.ts | 20 + .../proxy-preferences-route.injectable.ts | 21 + ...ate-to-telemetry-preferences.injectable.ts | 20 + .../telemetry-preferences-route.injectable.ts | 21 + ...gate-to-terminal-preferences.injectable.ts | 20 + .../terminal-preferences-route.injectable.ts | 21 + .../welcome/navigate-to-welcome.injectable.ts | 20 + .../welcome/welcome-route.injectable.ts | 21 + ...at-all-routes-have-route-component.test.ts | 46 + src/common/fs/path-exists.injectable.ts | 15 + src/common/fs/read-file-buffer.injectable.ts | 15 + src/common/fs/read-file.injectable.ts | 4 +- src/common/fs/read-json-file.injectable.ts | 5 +- src/common/fs/write-file.injectable.ts | 2 +- src/common/fs/write-json-file.injectable.ts | 4 +- .../app-version/app-version.injectable.ts | 14 + ...get-configuration-file-model.injectable.ts | 26 + src/common/hotbar-store.injectable.ts | 18 +- src/common/hotbar-store.ts | 82 +- .../create-channel/create-channel.ts | 2 +- src/common/k8s-api/endpoints/crd.api.ts | 10 +- src/common/logger.injectable.ts | 13 + src/common/logger.ts | 7 + .../path/get-absolute-path.injectable.ts | 20 + src/common/path/join-paths.injectable.ts | 18 + src/common/routes/add-cluster.ts | 13 - src/common/routes/catalog.ts | 30 - src/common/routes/cluster-view.ts | 18 - src/common/routes/cluster.ts | 13 - src/common/routes/config-maps.ts | 16 - src/common/routes/config.ts | 26 - src/common/routes/crd.ts | 31 - src/common/routes/endpoints.ts | 16 - src/common/routes/entity-settings.ts | 17 - src/common/routes/events.ts | 13 - src/common/routes/extensions.ts | 13 - src/common/routes/helm-charts.ts | 19 - src/common/routes/helm.ts | 13 - src/common/routes/hpa.ts | 16 - src/common/routes/index.ts | 39 - src/common/routes/ingresses.ts | 16 - src/common/routes/limit-ranges.ts | 16 - src/common/routes/namespaces.ts | 16 - src/common/routes/network-policies.ts | 16 - src/common/routes/network.ts | 24 - src/common/routes/nodes.ts | 16 - src/common/routes/pod-disruption-budgets.ts | 16 - src/common/routes/port-forwards.ts | 17 - src/common/routes/preferences.ts | 47 - src/common/routes/releases.ts | 19 - src/common/routes/resource-quotas.ts | 16 - src/common/routes/secrets.ts | 16 - src/common/routes/services.ts | 16 - src/common/routes/storage-classes.ts | 16 - src/common/routes/storage.ts | 20 - src/common/routes/user-management.ts | 63 -- src/common/routes/volume-claims.ts | 16 - src/common/routes/volumes.ts | 16 - src/common/routes/welcome.ts | 13 - src/common/routes/workloads.ts | 93 -- .../test-utils/get-absolute-path-fake.ts | 17 + .../test-utils/join-paths-fake.ts} | 4 +- .../user-store/user-store.injectable.ts | 9 +- src/common/utils/buildUrl.ts | 24 +- .../utils/is-allowed-resource.injectable.ts | 12 +- src/common/vars/is-linux.injectable.ts | 1 + src/common/vars/is-mac.injectable.ts | 14 + src/common/vars/is-windows.injectable.ts | 1 + ...r-extension-api-with-modifications.test.ts | 4 +- ...obal-singleton-object-for-extension-api.ts | 36 + .../legacy-global-di-for-extension-api.ts | 27 +- ...directory-for-extension-data.injectable.ts | 10 +- ...ile-system-provisioner-store.injectable.ts | 2 + ...tension-installation-counter.injectable.ts | 12 + .../extension-loader.injectable.ts | 7 +- .../extension-loader/extension-loader.ts | 17 +- .../extension-registrator-injection-token.ts | 12 + .../update-extensions-state.injectable.ts | 2 +- .../extensions-store.injectable.ts | 9 +- src/extensions/lens-extension.ts | 11 + src/extensions/lens-renderer-extension.ts | 56 +- .../__tests__/page-registry.test.ts | 177 ---- .../registries/page-menu-registry.ts | 37 +- src/extensions/registries/page-registry.ts | 99 +-- src/extensions/renderer-api/k8s-api.ts | 13 +- src/main/__test__/kube-auth-proxy.test.ts | 7 +- src/main/app-paths/app-paths.injectable.ts | 5 +- .../get-electron-app-path.test.ts | 3 + .../__test__/kubeconfig-sync.test.ts | 11 + src/main/catalog-sources/general.ts | 76 -- src/main/catalog-sources/index.ts | 1 - ...ync-general-catalog-entities.injectable.ts | 29 + .../catalog-entity-registry.injectable.ts | 14 + src/main/getDi.ts | 4 +- src/main/getDiForUnitTesting.ts | 54 +- src/main/index.ts | 29 +- .../init-ipc-main-handlers.injectable.ts | 9 +- .../init-ipc-main-handlers.ts | 19 +- src/main/is-auto-update-enabled.injectable.ts | 14 + src/main/kubectl/create-kubectl.injectable.ts | 2 +- ...rectory-for-kubectl-binaries.injectable.ts | 16 - .../menu/application-menu-items.injectable.ts | 337 ++++++++ src/main/menu/menu.ts | 304 +------ .../navigate-to-route.injectable.ts | 31 + .../navigate-to-url.injectable.ts | 23 + .../protocol-handler/__test__/router.test.ts | 21 +- src/main/router/router.ts | 2 - .../routes/static-file-route.injectable.ts | 24 +- src/main/tray/tray.ts | 10 +- src/main/window-manager.injectable.ts | 20 + src/main/window-manager.ts | 6 +- src/migrations/cluster-store/5.0.0-beta.10.ts | 3 +- src/migrations/hotbar-store/5.0.0-alpha.0.ts | 8 +- src/migrations/hotbar-store/5.0.0-beta.10.ts | 14 +- ...egister-ipc-channel-listener.injectable.ts | 25 + src/renderer/bootstrap.tsx | 34 +- .../add-cluster-route-component.injectable.ts | 21 + .../components/+add-cluster/add-cluster.tsx | 14 +- .../+catalog/__tests__/custom-columns.test.ts | 3 + .../catalog-browse-tab.ts} | 3 +- ...-previous-active-tab-storage.injectable.ts | 2 +- .../catalog-route-component.injectable.ts | 21 + .../catalog-route-parameters.injectable.ts | 24 + .../components/+catalog/catalog.test.tsx | 89 +- src/renderer/components/+catalog/catalog.tsx | 53 +- .../get-category-columns.injectable.ts | 15 +- .../+catalog/hotbar-toggle-menu-item.tsx | 42 +- .../+catalog/internal-category-columns.tsx | 53 -- .../name-category-column.injectable.tsx | 67 ++ ...ter-overview-route-component.injectable.ts | 21 + ...ster-overview-sidebar-items.injectable.tsx | 42 + .../components/+cluster/cluster-overview.tsx | 2 +- .../components/+cluster/sidebar-item.tsx | 37 - ...-auto-scalers-sidebar-items.injectable.tsx | 37 + ...-autoscalers-route-component.injectable.ts | 21 + .../components/+config-autoscalers/hpa.tsx | 112 ++- ...limit-ranges-route-component.injectable.ts | 21 + .../limit-ranges-sidebar-items.injectable.tsx | 38 + .../+config-limit-ranges/limit-ranges.tsx | 66 +- .../config-maps-route-component.injectable.ts | 21 + .../config-maps-sidebar-items.injectable.tsx | 38 + .../components/+config-maps/config-maps.tsx | 72 +- ...tion-budgets-route-component.injectable.ts | 21 + ...ption-budgets-sidebar-items.injectable.tsx | 38 + .../pod-disruption-budgets.tsx | 81 +- .../add-quota-dialog.tsx | 2 + ...ource-quotas-route-component.injectable.ts | 23 + ...source-quotas-sidebar-items.injectable.tsx | 38 + .../resource-quotas.tsx | 12 +- .../+config-secrets/add-secret-dialog.tsx | 2 + .../secrets-route-component.injectable.ts | 21 + .../secrets-sidebar-items.injectable.tsx | 38 + .../components/+config-secrets/secrets.tsx | 12 +- .../config-sidebar-items.injectable.tsx | 35 + .../+config/route-tabs.injectable.ts | 91 -- src/renderer/components/+config/route.tsx | 31 - .../components/+config/sidebar-item.tsx | 45 - .../crd-list-route-component.injectable.ts | 21 + .../components/+custom-resources/crd-list.tsx | 148 ++-- .../+custom-resources/crd-resources.tsx | 163 ++-- ...stom-resource-sidebar-items.injectable.tsx | 68 ++ ...om-resources-route-component.injectable.ts | 21 + ...m-resources-route-parameters.injectable.ts | 24 + .../custom-resources.injectable.ts | 14 +- .../grouped-custom-resources.injectable.ts | 35 - .../route-tabs.injectable.ts | 61 -- .../components/+custom-resources/route.tsx | 22 - .../+custom-resources/sidebar-item.tsx | 76 -- ...-items-for-definition-groups.injectable.ts | 81 ++ ...ity-settings-route-component.injectable.ts | 21 + ...ty-settings-route-parameters.injectable.ts | 23 + .../+entity-settings/entity-settings.tsx | 31 +- .../events-route-component.injectable.ts | 21 + .../events-sidebar-items.injectable.tsx | 43 + src/renderer/components/+events/events.tsx | 26 +- .../components/+events/sidebar-item.tsx | 37 - .../+extensions/__tests__/extensions.test.tsx | 7 + .../extensions-route-component.injectable.ts | 21 + .../components/+extensions/extensions.tsx | 2 +- .../+helm-charts/helm-chart-details.tsx | 1 + .../helm-charts-route-component.injectable.ts | 21 + ...helm-charts-route-parameters.injectable.ts | 24 + .../helm-charts-sidebar-items.injectable.tsx | 37 + .../components/+helm-charts/helm-charts.tsx | 52 +- ...elm-releases-route-component.injectable.ts | 21 + ...lm-releases-route-parameters.injectable.ts | 24 + ...helm-releases-sidebar-items.injectable.tsx | 38 + .../release-route-parameters.injectable.ts | 31 - .../release-details/release.injectable.ts | 7 +- .../release-rollback-dialog.tsx | 1 + .../components/+helm-releases/releases.tsx | 55 +- .../+helm/helm-sidebar-items.injectable.tsx | 35 + .../components/+helm/route-tabs.injectable.ts | 34 - src/renderer/components/+helm/route.tsx | 31 - .../components/+helm/sidebar-item.tsx | 44 - .../+namespaces/namespace-select-filter.tsx | 1 + .../+namespaces/namespace-select.tsx | 1 + .../namespaces-route-component.injectable.ts | 21 + .../namespaces-sidebar-items.injectable.tsx | 42 + src/renderer/components/+namespaces/route.tsx | 17 +- .../components/+namespaces/sidebar-item.tsx | 37 - .../endpoints-route-component.injectable.ts | 21 + .../endpoints-sidebar-items.injectable.tsx | 37 + .../+network-endpoints/endpoints.tsx | 78 +- .../ingresses-route-component.injectable.ts | 21 + .../ingresses-sidebar-items.injectable.tsx | 38 + .../+network-ingresses/ingresses.tsx | 109 +-- ...ork-policies-route-component.injectable.ts | 21 + ...work-policies-sidebar-items.injectable.tsx | 38 + .../+network-policies/network-policies.tsx | 68 +- ...ort-forwards-route-component.injectable.ts | 21 + ...rt-forwards-route-parameters.injectable.ts | 23 + ...port-forwards-sidebar-items.injectable.tsx | 39 + .../+network-port-forwards/port-forwards.tsx | 54 +- .../service-port-component.tsx | 17 +- .../services-route-component.injectable.ts | 21 + .../services-sidebar-items.injectable.tsx | 38 + .../components/+network-services/services.tsx | 112 ++- .../network-sidebar-items.injectable.tsx | 35 + src/renderer/components/+network/network.scss | 7 - .../+network/route-tabs.injectable.ts | 80 -- src/renderer/components/+network/route.tsx | 33 - .../components/+network/sidebar-item.tsx | 45 - .../nodes-route-component.injectable.ts | 21 + .../+nodes/nodes-sidebar-items.injectable.tsx | 43 + src/renderer/components/+nodes/route.tsx | 11 +- .../components/+nodes/sidebar-item.tsx | 37 - ...ity-policies-route-component.injectable.ts | 21 + ...rity-policies-sidebar-items.injectable.tsx | 38 + .../pod-security-policies.tsx | 67 +- ...-preferences-route-component.injectable.ts | 21 + .../components/+preferences/application.tsx | 189 ++-- .../close-preferences.injectable.ts | 28 + .../+preferences/default-shell.injectable.ts | 24 + ...-preferences-route-component.injectable.ts | 21 + .../components/+preferences/editor.tsx | 157 ++-- ...-preference-item-registrator.injectable.ts | 54 ++ .../extension-preference-items.injectable.ts | 50 ++ ...-preferences-route-component.injectable.ts | 21 + .../+preferences/extension-settings.tsx | 8 +- ...-preference-item-registrator.injectable.ts | 54 ++ .../components/+preferences/extensions.tsx | 29 +- .../components/+preferences/helm-charts.tsx | 3 +- .../+preferences/kubeconfig-syncs.tsx | 48 +- .../+preferences/kubectl-binaries.tsx | 162 ++-- ...-preferences-route-component.injectable.ts | 21 + .../components/+preferences/kubernetes.tsx | 15 +- ...-preferences-navigation-item.injectable.ts | 37 + ...-preferences-navigation-item.injectable.ts | 37 + ...-preferences-navigation-item.injectable.ts | 51 ++ ...-preferences-navigation-item.injectable.ts | 40 + .../navigate-to-preference-tab.injectable.ts | 21 + .../preference-navigation-items.injectable.ts | 37 + .../preferences-navigation.tsx | 61 ++ ...-preferences-navigation-item.injectable.ts | 37 + ...-preferences-navigation-item.injectable.ts | 49 ++ ...-preferences-navigation-item.injectable.ts | 37 + .../components/+preferences/preferences.tsx | 117 +-- ...-preferences-route-component.injectable.ts | 21 + .../components/+preferences/proxy.tsx | 82 +- .../+preferences/sentry-dns-url.injectable.ts | 13 + .../telemetry-preference-items.injectable.ts | 50 ++ ...-preferences-route-component.injectable.ts | 21 + .../components/+preferences/telemetry.tsx | 91 +- ...-preferences-route-component.injectable.ts | 21 + .../components/+preferences/terminal.tsx | 158 ++-- ...rage-classes-route-component.injectable.ts | 21 + ...orage-classes-sidebar-items.injectable.tsx | 38 + .../+storage-classes/storage-classes.tsx | 76 +- ...olume-claims-route-component.injectable.ts | 21 + ...volume-claims-sidebar-items.injectable.tsx | 38 + .../+storage-volume-claims/volume-claims.tsx | 119 ++- ...tent-volumes-route-component.injectable.ts | 22 + ...stent-volumes-sidebar-items.injectable.tsx | 38 + .../components/+storage-volumes/volumes.tsx | 107 ++- .../+storage/route-tabs.injectable.ts | 62 -- src/renderer/components/+storage/route.tsx | 34 - .../components/+storage/sidebar-item.tsx | 45 - .../storage-sidebar-items.injectable.tsx | 35 + src/renderer/components/+storage/storage.scss | 7 - ...ole-bindings-route-component.injectable.ts | 21 + ...role-bindings-sidebar-items.injectable.tsx | 38 + .../+cluster-role-bindings/dialog.tsx | 2 + .../+cluster-role-bindings/view.tsx | 12 +- ...luster-roles-route-component.injectable.ts | 21 + ...cluster-roles-sidebar-items.injectable.tsx | 38 + .../+user-management/+cluster-roles/view.tsx | 12 +- .../+role-bindings/dialog.tsx | 3 + ...ole-bindings-route-component.injectable.ts | 21 + ...role-bindings-sidebar-items.injectable.tsx | 38 + .../+user-management/+role-bindings/view.tsx | 12 +- .../+user-management/+roles/add-dialog.tsx | 1 + .../roles-route-component.injectable.ts | 21 + .../+roles/roles-sidebar-items.injectable.tsx | 38 + .../+user-management/+roles/view.tsx | 12 +- .../+service-accounts/create-dialog.tsx | 1 + ...ice-accounts-route-component.injectable.ts | 21 + ...vice-accounts-sidebar-items.injectable.tsx | 38 + .../+service-accounts/view.tsx | 12 +- .../+user-management/route-tabs.injectable.ts | 92 -- .../components/+user-management/route.tsx | 31 - .../+user-management/sidebar-item.tsx | 45 - ...er-management-sidebar-items.injectable.tsx | 35 + .../get-welcome-menu-items.ts | 19 +- .../welcome-menu-items.injectable.ts | 2 + .../welcome-route-component.injectable.ts | 21 + src/renderer/components/+welcome/welcome.tsx | 2 +- .../cron-jobs-route-component.injectable.ts | 21 + .../cron-jobs-sidebar-items.injectable.tsx | 38 + .../cron-jobs-store.injectable.ts | 14 + .../+workloads-cronjobs/cronjobs.tsx | 98 ++- .../daemonsets-route-component.injectable.ts | 21 + .../daemonsets-sidebar-items.injectable.tsx | 38 + .../daemonsets-store.injectable.ts | 15 + .../+workloads-daemonsets/daemonsets.tsx | 82 +- .../deployments-route-component.injectable.ts | 21 + .../deployments-sidebar-items.injectable.tsx | 38 + .../deployments-store.injectable.ts | 14 + .../+workloads-deployments/deployments.tsx | 82 +- .../jobs-route-component.injectable.ts | 21 + .../jobs-sidebar-items.injectable.tsx | 38 + .../+workloads-jobs/jobs-store.injectable.ts | 14 + .../components/+workloads-jobs/jobs.tsx | 86 +- .../+workloads-overview/overview-statuses.tsx | 34 +- .../+workloads-overview/overview.tsx | 37 +- ...ads-overview-route-component.injectable.ts | 21 + ...oads-overview-sidebar-items.injectable.tsx | 38 + .../workloads.injectable.ts | 40 - .../+workloads-overview/workloads.ts | 39 - .../cron-jobs-workload.injectable.ts | 41 + .../daemonsets-workload.injectable.ts | 41 + .../deployments-workload.injectable.ts | 41 + .../jobs-workload.injectable.ts | 41 + .../pods-workload.injectable.ts | 41 + .../replicasets-workload.injectable.ts | 41 + .../statefulsets-workload.injectable.ts | 41 + .../workloads/workload-injection-token.ts | 19 + .../workloads/workloads.injectable.ts | 42 + .../+workloads-pods/pod-container-port.tsx | 19 +- .../pods-route-component.injectable.ts | 21 + .../pods-sidebar-items.injectable.tsx | 38 + .../+workloads-pods/pods-store.injectable.ts | 14 + .../components/+workloads-pods/pods.tsx | 136 ++- .../replicasets-route-component.injectable.ts | 21 + .../replicasets-sidebar-items.injectable.tsx | 38 + .../replicasets-store.injectable.ts | 14 + .../+workloads-replicasets/replicasets.tsx | 82 +- ...statefulsets-route-component.injectable.ts | 21 + .../statefulsets-sidebar-items.injectable.tsx | 38 + .../statefulsets-store.injectable.ts | 14 + .../+workloads-statefulsets/statefulsets.tsx | 74 +- .../+workloads/route-tabs.injectable.ts | 110 --- src/renderer/components/+workloads/route.tsx | 34 - .../components/+workloads/sidebar-item.tsx | 45 - .../workloads-sidebar-items.injectable.tsx | 35 + .../activate-entity-command.tsx | 1 + src/renderer/components/button/button.tsx | 2 +- .../cluster-manager/cluster-manager.tsx | 74 +- .../cluster-manager/cluster-status.tsx | 31 +- ...cluster-view-route-component.injectable.ts | 21 + ...luster-view-route-parameters.injectable.ts | 23 + .../cluster-manager/cluster-view.tsx | 36 +- .../components/cluster-metrics-setting.tsx | 1 + .../components/cluster-prometheus-setting.tsx | 1 + .../command-palette/command-container.tsx | 37 +- .../command-palette/command-dialog.tsx | 1 + .../internal-commands.injectable.tsx | 133 ++- .../__tests__/delete-cluster-dialog.test.tsx | 28 +- .../delete-cluster-dialog-model.injectable.ts | 13 + .../delete-cluster-dialog-model.ts | 33 + .../delete-cluster-dialog.tsx | 73 +- .../dock/__test__/dock-tabs.test.tsx | 8 +- .../lens-templates.injectable.ts | 11 +- .../components/dock/create-resource/view.tsx | 1 + .../components/dock/install-chart/view.tsx | 19 +- .../__test__/log-resource-selector.test.tsx | 7 + .../dock/logs/resource-selector.tsx | 2 + src/renderer/components/dock/logs/view.tsx | 6 +- .../components/dock/upgrade-chart/view.tsx | 1 + .../__tests__/hotbar-remove-command.test.tsx | 14 +- .../components/hotbar/hotbar-add-command.tsx | 4 +- .../components/hotbar/hotbar-menu.tsx | 36 +- .../hotbar/hotbar-remove-command.tsx | 17 +- .../hotbar/hotbar-rename-command.tsx | 17 +- .../components/hotbar/hotbar-selector.tsx | 14 +- .../hotbar/hotbar-switch-command.tsx | 29 +- src/renderer/components/input/input.tsx | 2 + .../unique-hotbar-name.injectable.ts | 4 +- .../kube-object-list-layout.tsx | 2 +- .../layout/__tests__/sidebar-cluster.test.tsx | 27 +- ...on-sidebar-item-registrator.injectable.tsx | 83 ++ .../components/layout/setting-layout.tsx | 22 +- .../layout/siblings-in-tab-layout.tsx | 47 + .../components/layout/sidebar-cluster.tsx | 28 +- .../components/layout/sidebar-item.scss | 54 +- .../components/layout/sidebar-item.tsx | 115 +-- .../layout/sidebar-items.injectable.ts | 131 +++ src/renderer/components/layout/sidebar.tsx | 144 +--- .../components/layout/tab-layout-2.tsx | 60 ++ .../layout/tab-routes-sidebar-items.tsx | 37 - .../layout/top-bar/top-bar-win-linux.test.tsx | 36 +- .../components/layout/top-bar/top-bar.tsx | 43 +- .../components/select/select.test.tsx | 25 +- src/renderer/components/select/select.tsx | 6 +- src/renderer/components/tabs/tabs.tsx | 7 +- .../test-utils/get-application-builder.tsx | 314 +++++++ .../test-utils/get-renderer-extension-fake.ts | 23 + .../components/tooltip/withTooltip.tsx | 1 + .../frames/cluster-frame/cluster-frame.tsx | 159 +--- .../cluster-frame/start-url.injectable.ts | 34 + .../init-root-frame.injectable.ts | 3 + .../init-root-frame/init-root-frame.ts | 5 +- src/renderer/frames/root-frame/root-frame.tsx | 24 +- src/renderer/getDi.tsx | 4 +- src/renderer/getDiForUnitTesting.tsx | 85 +- .../add-sync-entries.injectable.tsx | 38 + .../catalog-category-registry.tsx | 23 +- src/renderer/initializers/catalog.tsx | 11 +- src/renderer/initializers/registries.ts | 5 +- src/renderer/initializers/workload-events.tsx | 10 +- .../focus-window.injectable.ts | 13 + .../ipc-channel-listener-injection-token.ts | 17 + .../navigation-listener.injectable.ts | 38 + ...gister-ipc-channel-listeners.injectable.ts | 27 + ...amespaces-forbidden-handler.injectable.tsx | 81 ++ .../ipc/register-ipc-listeners.injectable.ts | 17 + src/renderer/ipc/register-listeners.tsx | 69 +- .../subscribe-stores.injectable.ts | 1 + src/renderer/navigation/events.ts | 29 +- src/renderer/navigation/helpers.ts | 14 - src/renderer/navigation/index.ts | 4 - .../matched-cluster-id.injectable.ts | 18 + .../observable-history.injectable.ts | 1 + src/renderer/navigation/page-param.ts | 9 +- .../about-port-forwarding.injectable.ts | 18 + ...notify-error-port-forwarding.injectable.ts | 18 + .../port-forward/port-forward-dialog.tsx | 13 +- .../port-forward/port-forward-notify.tsx | 21 +- .../port-forward-store.injectable.ts | 6 +- .../port-forward-store/port-forward-store.ts | 4 +- ...-protocol-add-route-handlers.injectable.ts | 14 + .../bind-protocol-add-route-handlers.tsx | 44 +- ...avigate-to-preference-tab-id.injectable.ts | 30 + src/renderer/routes/all-routes.injectable.ts | 39 + .../current-path-parameters.injectable.ts | 23 + .../routes/current-path.injectable.ts | 19 + .../current-route-component.injectable.ts | 33 + .../routes/current-route.injectable.ts | 27 + .../currently-in-cluster-frame.injectable.ts | 12 + .../extension-page-parameters.injectable.ts | 68 ++ ...extension-route-registrator.injectable.tsx | 116 +++ src/renderer/routes/get-extension-route-id.ts | 9 + .../routes/get-extension-route-path.ts | 19 + .../routes/matching-route.injectable.ts | 38 + .../routes/navigate-to-route.injectable.ts | 39 + .../routes/navigate-to-url.injectable.ts | 40 + .../routes/query-parameters.injectable.ts | 24 + .../routes/route-is-active.injectable.ts | 24 + .../route-path-parameters.injectable.ts | 33 + ...oute-specific-component-injection-token.ts | 13 + src/renderer/routes/routes.injectable.ts | 30 + .../routes/sibling-tabs.injectable.ts | 28 + src/renderer/theme-store.injectable.ts | 20 + .../create-storage.injectable.ts | 12 + .../utils/create-storage/create-storage.ts | 25 +- src/test-utils/override-fs-with-fakes.ts | 65 ++ src/test-utils/override-ipc-bridge.ts | 33 + yarn.lock | 30 +- 583 files changed, 13073 insertions(+), 6556 deletions(-) create mode 100644 src/common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts create mode 100644 src/common/catalog-entities/general-catalog-entities/general-catalog-entity-injection-token.ts create mode 100644 src/common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable.ts create mode 100644 src/common/catalog-entities/general-catalog-entities/implementations/preferences-catalog-entity.injectable.ts create mode 100644 src/common/catalog-entities/general-catalog-entities/implementations/welcome-catalog-entity.injectable.ts create mode 100644 src/common/front-end-routing/navigate-to-front-page.injectable.ts create mode 100644 src/common/front-end-routing/navigate-to-route-injection-token.ts create mode 100644 src/common/front-end-routing/navigate-to-url-injection-token.ts create mode 100644 src/common/front-end-routing/navigation-ipc-channel.ts create mode 100644 src/common/front-end-routing/route-injection-token.ts create mode 100644 src/common/front-end-routing/routes/add-cluster/add-cluster-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable.ts create mode 100644 src/common/front-end-routing/routes/catalog/catalog-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/catalog/navigate-to-catalog.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster-view/cluster-view-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster-view/navigate-to-cluster-view.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/config-maps/config-maps-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/config-maps/navigate-to-config-maps.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/horizontal-pod-autoscalers-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/navigate-to-horizontal-pod-autoscalers.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/limit-ranges/limit-ranges-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/limit-ranges/navigate-to-limit-ranges.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/pod-disruption-budgets/navigate-to-pod-disruption-budgets.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/pod-disruption-budgets/pod-disruption-budgets-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/resource-quotas/navigate-to-resource-quotas.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/resource-quotas/resource-quotas-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/secrets/navigate-to-secrets.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/config/secrets/secrets-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/custom-resources/crd-list/crd-list-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/custom-resources/crd-list/navigate-to-crd-list.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/custom-resources/custom-resources/navigate-to-custom-resources.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/events/events-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/events/navigate-to-events.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/helm/charts/helm-charts-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/helm/releases/helm-releases-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/namespaces/namespaces-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/namespaces/navigate-to-namespaces.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/network/endpoints/endpoints-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/network/endpoints/navigate-to-endpoints.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/network/ingresses/ingresses-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/network/ingresses/navigate-to-ingresses.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/network/network-policies/navigate-to-network-policies.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/network/network-policies/network-policies-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/network/port-forwards/port-forwards-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/network/services/navigate-to-services.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/network/services/services-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/nodes/navigate-to-nodes.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/nodes/nodes-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/overview/cluster-overview-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/overview/navigate-to-cluster-overview.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/storage/persistent-volume-claims/navigate-to-persistent-volume-claims.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/storage/persistent-volume-claims/persistent-volume-claims-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/storage/persistent-volumes/navigate-to-persistent-volumes.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/storage/persistent-volumes/persistent-volumes-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/storage/storage-classes/navigate-to-storage-classes.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/storage/storage-classes/storage-classes-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/cluster-role-bindings/cluster-role-bindings-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/cluster-role-bindings/navigate-to-cluster-role-bindings.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/cluster-roles/cluster-roles-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/cluster-roles/navigate-to-cluster-roles.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/pod-security-policies/navigate-to-pod-security-policies.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/pod-security-policies/pod-security-policies-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/role-bindings/navigate-to-role-bindings.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/role-bindings/role-bindings-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/roles/navigate-to-roles.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/roles/roles-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/service-accounts/navigate-to-service-accounts.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/user-management/service-accounts/service-accounts-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/cron-jobs/cron-jobs-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/cron-jobs/navigate-to-cron-jobs.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/daemonsets/daemonsets-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/daemonsets/navigate-to-daemonsets.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/deployments/deployments-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/jobs/jobs-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/jobs/navigate-to-jobs.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/overview/navigate-to-workloads-overview.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/overview/workloads-overview-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/pods/navigate-to-pods.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/pods/pods-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/replicasets/navigate-to-replicasets.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/replicasets/replicasets-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/statefulsets/navigate-to-statefulsets.injectable.ts create mode 100644 src/common/front-end-routing/routes/cluster/workloads/statefulsets/statefulsets-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/entity-settings/entity-settings-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable.ts create mode 100644 src/common/front-end-routing/routes/extensions/extensions-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/extensions/navigate-to-extensions.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/app/app-preferences-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/app/navigate-to-app-preferences.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/editor/editor-preferences-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/editor/navigate-to-editor-preferences.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/kubernetes/kubernetes-preferences-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/kubernetes/navigate-to-kubernetes-preferences.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/navigate-to-preferences.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/proxy/navigate-to-proxy-preferences.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/proxy/proxy-preferences-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/telemetry/navigate-to-telemetry-preferences.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/telemetry/telemetry-preferences-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/terminal/navigate-to-terminal-preferences.injectable.ts create mode 100644 src/common/front-end-routing/routes/preferences/terminal/terminal-preferences-route.injectable.ts create mode 100644 src/common/front-end-routing/routes/welcome/navigate-to-welcome.injectable.ts create mode 100644 src/common/front-end-routing/routes/welcome/welcome-route.injectable.ts create mode 100644 src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts create mode 100644 src/common/fs/path-exists.injectable.ts create mode 100644 src/common/fs/read-file-buffer.injectable.ts create mode 100644 src/common/get-configuration-file-model/app-version/app-version.injectable.ts create mode 100644 src/common/get-configuration-file-model/get-configuration-file-model.injectable.ts create mode 100644 src/common/logger.injectable.ts create mode 100644 src/common/path/get-absolute-path.injectable.ts create mode 100644 src/common/path/join-paths.injectable.ts delete mode 100644 src/common/routes/add-cluster.ts delete mode 100644 src/common/routes/catalog.ts delete mode 100644 src/common/routes/cluster-view.ts delete mode 100644 src/common/routes/cluster.ts delete mode 100644 src/common/routes/config-maps.ts delete mode 100644 src/common/routes/config.ts delete mode 100644 src/common/routes/crd.ts delete mode 100644 src/common/routes/endpoints.ts delete mode 100644 src/common/routes/entity-settings.ts delete mode 100644 src/common/routes/events.ts delete mode 100644 src/common/routes/extensions.ts delete mode 100644 src/common/routes/helm-charts.ts delete mode 100644 src/common/routes/helm.ts delete mode 100644 src/common/routes/hpa.ts delete mode 100644 src/common/routes/index.ts delete mode 100644 src/common/routes/ingresses.ts delete mode 100644 src/common/routes/limit-ranges.ts delete mode 100644 src/common/routes/namespaces.ts delete mode 100644 src/common/routes/network-policies.ts delete mode 100644 src/common/routes/network.ts delete mode 100644 src/common/routes/nodes.ts delete mode 100644 src/common/routes/pod-disruption-budgets.ts delete mode 100644 src/common/routes/port-forwards.ts delete mode 100644 src/common/routes/preferences.ts delete mode 100644 src/common/routes/releases.ts delete mode 100644 src/common/routes/resource-quotas.ts delete mode 100644 src/common/routes/secrets.ts delete mode 100644 src/common/routes/services.ts delete mode 100644 src/common/routes/storage-classes.ts delete mode 100644 src/common/routes/storage.ts delete mode 100644 src/common/routes/user-management.ts delete mode 100644 src/common/routes/volume-claims.ts delete mode 100644 src/common/routes/volumes.ts delete mode 100644 src/common/routes/welcome.ts delete mode 100644 src/common/routes/workloads.ts create mode 100644 src/common/test-utils/get-absolute-path-fake.ts rename src/{renderer/components/+user-management/user-management.scss => common/test-utils/join-paths-fake.ts} (52%) create mode 100644 src/common/vars/is-mac.injectable.ts create mode 100644 src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api.ts create mode 100644 src/extensions/extension-loader/extension-installation-counter.injectable.ts create mode 100644 src/extensions/extension-loader/extension-registrator-injection-token.ts delete mode 100644 src/extensions/registries/__tests__/page-registry.test.ts delete mode 100644 src/main/catalog-sources/general.ts create mode 100644 src/main/catalog-sources/sync-general-catalog-entities.injectable.ts create mode 100644 src/main/catalog/catalog-entity-registry.injectable.ts create mode 100644 src/main/is-auto-update-enabled.injectable.ts delete mode 100644 src/main/kubectl/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts create mode 100644 src/main/menu/application-menu-items.injectable.ts create mode 100644 src/main/navigate-to-route/navigate-to-route.injectable.ts create mode 100644 src/main/navigate-to-url/navigate-to-url.injectable.ts create mode 100644 src/main/window-manager.injectable.ts create mode 100644 src/renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable.ts create mode 100644 src/renderer/components/+add-cluster/add-cluster-route-component.injectable.ts rename src/renderer/components/{+workloads/workloads.scss => +catalog/catalog-browse-tab.ts} (77%) create mode 100644 src/renderer/components/+catalog/catalog-route-component.injectable.ts create mode 100644 src/renderer/components/+catalog/catalog-route-parameters.injectable.ts create mode 100644 src/renderer/components/+catalog/name-category-column.injectable.tsx create mode 100644 src/renderer/components/+cluster/cluster-overview-route-component.injectable.ts create mode 100644 src/renderer/components/+cluster/cluster-overview-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+cluster/sidebar-item.tsx create mode 100644 src/renderer/components/+config-autoscalers/horizontal-pod-auto-scalers-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+config-autoscalers/horizontal-pod-autoscalers-route-component.injectable.ts create mode 100644 src/renderer/components/+config-limit-ranges/limit-ranges-route-component.injectable.ts create mode 100644 src/renderer/components/+config-limit-ranges/limit-ranges-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+config-maps/config-maps-route-component.injectable.ts create mode 100644 src/renderer/components/+config-maps/config-maps-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-route-component.injectable.ts create mode 100644 src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+config-resource-quotas/resource-quotas-route-component.injectable.ts create mode 100644 src/renderer/components/+config-resource-quotas/resource-quotas-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+config-secrets/secrets-route-component.injectable.ts create mode 100644 src/renderer/components/+config-secrets/secrets-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+config/config-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+config/route-tabs.injectable.ts delete mode 100644 src/renderer/components/+config/route.tsx delete mode 100644 src/renderer/components/+config/sidebar-item.tsx create mode 100644 src/renderer/components/+custom-resources/crd-list-route-component.injectable.ts create mode 100644 src/renderer/components/+custom-resources/custom-resource-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+custom-resources/custom-resources-route-component.injectable.ts create mode 100644 src/renderer/components/+custom-resources/custom-resources-route-parameters.injectable.ts delete mode 100644 src/renderer/components/+custom-resources/grouped-custom-resources.injectable.ts delete mode 100644 src/renderer/components/+custom-resources/route-tabs.injectable.ts delete mode 100644 src/renderer/components/+custom-resources/route.tsx delete mode 100644 src/renderer/components/+custom-resources/sidebar-item.tsx create mode 100644 src/renderer/components/+custom-resources/sidebar-items-for-definition-groups.injectable.ts create mode 100644 src/renderer/components/+entity-settings/entity-settings-route-component.injectable.ts create mode 100644 src/renderer/components/+entity-settings/entity-settings-route-parameters.injectable.ts create mode 100644 src/renderer/components/+events/events-route-component.injectable.ts create mode 100644 src/renderer/components/+events/events-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+events/sidebar-item.tsx create mode 100644 src/renderer/components/+extensions/extensions-route-component.injectable.ts create mode 100644 src/renderer/components/+helm-charts/helm-charts-route-component.injectable.ts create mode 100644 src/renderer/components/+helm-charts/helm-charts-route-parameters.injectable.ts create mode 100644 src/renderer/components/+helm-charts/helm-charts-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+helm-releases/helm-releases-route-component.injectable.ts create mode 100644 src/renderer/components/+helm-releases/helm-releases-route-parameters.injectable.ts create mode 100644 src/renderer/components/+helm-releases/helm-releases-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+helm-releases/release-details/release-route-parameters.injectable.ts create mode 100644 src/renderer/components/+helm/helm-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+helm/route-tabs.injectable.ts delete mode 100644 src/renderer/components/+helm/route.tsx delete mode 100644 src/renderer/components/+helm/sidebar-item.tsx create mode 100644 src/renderer/components/+namespaces/namespaces-route-component.injectable.ts create mode 100644 src/renderer/components/+namespaces/namespaces-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+namespaces/sidebar-item.tsx create mode 100644 src/renderer/components/+network-endpoints/endpoints-route-component.injectable.ts create mode 100644 src/renderer/components/+network-endpoints/endpoints-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+network-ingresses/ingresses-route-component.injectable.ts create mode 100644 src/renderer/components/+network-ingresses/ingresses-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+network-policies/network-policies-route-component.injectable.ts create mode 100644 src/renderer/components/+network-policies/network-policies-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+network-port-forwards/port-forwards-route-component.injectable.ts create mode 100644 src/renderer/components/+network-port-forwards/port-forwards-route-parameters.injectable.ts create mode 100644 src/renderer/components/+network-port-forwards/port-forwards-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+network-services/services-route-component.injectable.ts create mode 100644 src/renderer/components/+network-services/services-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+network/network-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+network/network.scss delete mode 100644 src/renderer/components/+network/route-tabs.injectable.ts delete mode 100644 src/renderer/components/+network/route.tsx delete mode 100644 src/renderer/components/+network/sidebar-item.tsx create mode 100644 src/renderer/components/+nodes/nodes-route-component.injectable.ts create mode 100644 src/renderer/components/+nodes/nodes-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+nodes/sidebar-item.tsx create mode 100644 src/renderer/components/+pod-security-policies/pod-security-policies-route-component.injectable.ts create mode 100644 src/renderer/components/+pod-security-policies/pod-security-policies-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+preferences/app-preferences-route-component.injectable.ts create mode 100644 src/renderer/components/+preferences/close-preferences.injectable.ts create mode 100644 src/renderer/components/+preferences/default-shell.injectable.ts create mode 100644 src/renderer/components/+preferences/editor-preferences-route-component.injectable.ts create mode 100644 src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts create mode 100644 src/renderer/components/+preferences/extension-preference-items.injectable.ts create mode 100644 src/renderer/components/+preferences/extension-preferences-route-component.injectable.ts create mode 100644 src/renderer/components/+preferences/extension-telemetry-preference-item-registrator.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes-preferences-route-component.injectable.ts create mode 100644 src/renderer/components/+preferences/preferences-navigation/application-preferences-navigation-item.injectable.ts create mode 100644 src/renderer/components/+preferences/preferences-navigation/editor-preferences-navigation-item.injectable.ts create mode 100644 src/renderer/components/+preferences/preferences-navigation/extensions-preferences-navigation-item.injectable.ts create mode 100644 src/renderer/components/+preferences/preferences-navigation/kubernetes-preferences-navigation-item.injectable.ts create mode 100644 src/renderer/components/+preferences/preferences-navigation/navigate-to-preference-tab.injectable.ts create mode 100644 src/renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable.ts create mode 100644 src/renderer/components/+preferences/preferences-navigation/preferences-navigation.tsx create mode 100644 src/renderer/components/+preferences/preferences-navigation/proxy-preferences-navigation-item.injectable.ts create mode 100644 src/renderer/components/+preferences/preferences-navigation/telemetry-preferences-navigation-item.injectable.ts create mode 100644 src/renderer/components/+preferences/preferences-navigation/terminal-preferences-navigation-item.injectable.ts create mode 100644 src/renderer/components/+preferences/proxy-preferences-route-component.injectable.ts create mode 100644 src/renderer/components/+preferences/sentry-dns-url.injectable.ts create mode 100644 src/renderer/components/+preferences/telemetry-preference-items.injectable.ts create mode 100644 src/renderer/components/+preferences/telemetry-preferences-route-component.injectable.ts create mode 100644 src/renderer/components/+preferences/terminal-preferences-route-component.injectable.ts create mode 100644 src/renderer/components/+storage-classes/storage-classes-route-component.injectable.ts create mode 100644 src/renderer/components/+storage-classes/storage-classes-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+storage-volume-claims/persistent-volume-claims-route-component.injectable.ts create mode 100644 src/renderer/components/+storage-volume-claims/persistent-volume-claims-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+storage-volumes/persistent-volumes-route-component.injectable.ts create mode 100644 src/renderer/components/+storage-volumes/persistent-volumes-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+storage/route-tabs.injectable.ts delete mode 100644 src/renderer/components/+storage/route.tsx delete mode 100644 src/renderer/components/+storage/sidebar-item.tsx create mode 100644 src/renderer/components/+storage/storage-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+storage/storage.scss create mode 100644 src/renderer/components/+user-management/+cluster-role-bindings/cluster-role-bindings-route-component.injectable.ts create mode 100644 src/renderer/components/+user-management/+cluster-role-bindings/cluster-role-bindings-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+user-management/+cluster-roles/cluster-roles-route-component.injectable.ts create mode 100644 src/renderer/components/+user-management/+cluster-roles/cluster-roles-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+user-management/+role-bindings/role-bindings-route-component.injectable.ts create mode 100644 src/renderer/components/+user-management/+role-bindings/role-bindings-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+user-management/+roles/roles-route-component.injectable.ts create mode 100644 src/renderer/components/+user-management/+roles/roles-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+user-management/+service-accounts/service-accounts-route-component.injectable.ts create mode 100644 src/renderer/components/+user-management/+service-accounts/service-accounts-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+user-management/route-tabs.injectable.ts delete mode 100644 src/renderer/components/+user-management/route.tsx delete mode 100644 src/renderer/components/+user-management/sidebar-item.tsx create mode 100644 src/renderer/components/+user-management/user-management-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+welcome/welcome-route-component.injectable.ts create mode 100644 src/renderer/components/+workloads-cronjobs/cron-jobs-route-component.injectable.ts create mode 100644 src/renderer/components/+workloads-cronjobs/cron-jobs-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+workloads-cronjobs/cron-jobs-store.injectable.ts create mode 100644 src/renderer/components/+workloads-daemonsets/daemonsets-route-component.injectable.ts create mode 100644 src/renderer/components/+workloads-daemonsets/daemonsets-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+workloads-daemonsets/daemonsets-store.injectable.ts create mode 100644 src/renderer/components/+workloads-deployments/deployments-route-component.injectable.ts create mode 100644 src/renderer/components/+workloads-deployments/deployments-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+workloads-deployments/deployments-store.injectable.ts create mode 100644 src/renderer/components/+workloads-jobs/jobs-route-component.injectable.ts create mode 100644 src/renderer/components/+workloads-jobs/jobs-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+workloads-jobs/jobs-store.injectable.ts create mode 100644 src/renderer/components/+workloads-overview/workloads-overview-route-component.injectable.ts create mode 100644 src/renderer/components/+workloads-overview/workloads-overview-sidebar-items.injectable.tsx delete mode 100644 src/renderer/components/+workloads-overview/workloads.injectable.ts delete mode 100644 src/renderer/components/+workloads-overview/workloads.ts create mode 100644 src/renderer/components/+workloads-overview/workloads/implementations/cron-jobs-workload.injectable.ts create mode 100644 src/renderer/components/+workloads-overview/workloads/implementations/daemonsets-workload.injectable.ts create mode 100644 src/renderer/components/+workloads-overview/workloads/implementations/deployments-workload.injectable.ts create mode 100644 src/renderer/components/+workloads-overview/workloads/implementations/jobs-workload.injectable.ts create mode 100644 src/renderer/components/+workloads-overview/workloads/implementations/pods-workload.injectable.ts create mode 100644 src/renderer/components/+workloads-overview/workloads/implementations/replicasets-workload.injectable.ts create mode 100644 src/renderer/components/+workloads-overview/workloads/implementations/statefulsets-workload.injectable.ts create mode 100644 src/renderer/components/+workloads-overview/workloads/workload-injection-token.ts create mode 100644 src/renderer/components/+workloads-overview/workloads/workloads.injectable.ts create mode 100644 src/renderer/components/+workloads-pods/pods-route-component.injectable.ts create mode 100644 src/renderer/components/+workloads-pods/pods-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+workloads-pods/pods-store.injectable.ts create mode 100644 src/renderer/components/+workloads-replicasets/replicasets-route-component.injectable.ts create mode 100644 src/renderer/components/+workloads-replicasets/replicasets-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+workloads-replicasets/replicasets-store.injectable.ts create mode 100644 src/renderer/components/+workloads-statefulsets/statefulsets-route-component.injectable.ts create mode 100644 src/renderer/components/+workloads-statefulsets/statefulsets-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/+workloads-statefulsets/statefulsets-store.injectable.ts delete mode 100644 src/renderer/components/+workloads/route-tabs.injectable.ts delete mode 100644 src/renderer/components/+workloads/route.tsx delete mode 100644 src/renderer/components/+workloads/sidebar-item.tsx create mode 100644 src/renderer/components/+workloads/workloads-sidebar-items.injectable.tsx create mode 100644 src/renderer/components/cluster-manager/cluster-view-route-component.injectable.ts create mode 100644 src/renderer/components/cluster-manager/cluster-view-route-parameters.injectable.ts create mode 100644 src/renderer/components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.injectable.ts create mode 100644 src/renderer/components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.ts create mode 100644 src/renderer/components/layout/extension-sidebar-item-registrator.injectable.tsx create mode 100644 src/renderer/components/layout/siblings-in-tab-layout.tsx create mode 100644 src/renderer/components/layout/sidebar-items.injectable.ts create mode 100644 src/renderer/components/layout/tab-layout-2.tsx delete mode 100644 src/renderer/components/layout/tab-routes-sidebar-items.tsx create mode 100644 src/renderer/components/test-utils/get-application-builder.tsx create mode 100644 src/renderer/components/test-utils/get-renderer-extension-fake.ts create mode 100644 src/renderer/frames/cluster-frame/start-url.injectable.ts create mode 100644 src/renderer/initializers/add-sync-entries.injectable.tsx create mode 100644 src/renderer/ipc-channel-listeners/focus-window.injectable.ts create mode 100644 src/renderer/ipc-channel-listeners/ipc-channel-listener-injection-token.ts create mode 100644 src/renderer/ipc-channel-listeners/navigation-listener.injectable.ts create mode 100644 src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts create mode 100644 src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx create mode 100644 src/renderer/ipc/register-ipc-listeners.injectable.ts create mode 100644 src/renderer/navigation/matched-cluster-id.injectable.ts create mode 100644 src/renderer/port-forward/about-port-forwarding.injectable.ts create mode 100644 src/renderer/port-forward/notify-error-port-forwarding.injectable.ts create mode 100644 src/renderer/protocol-handler/bind-protocol-add-route-handlers/navigate-to-preference-tab-id.injectable.ts create mode 100644 src/renderer/routes/all-routes.injectable.ts create mode 100644 src/renderer/routes/current-path-parameters.injectable.ts create mode 100644 src/renderer/routes/current-path.injectable.ts create mode 100644 src/renderer/routes/current-route-component.injectable.ts create mode 100644 src/renderer/routes/current-route.injectable.ts create mode 100644 src/renderer/routes/currently-in-cluster-frame.injectable.ts create mode 100644 src/renderer/routes/extension-page-parameters.injectable.ts create mode 100644 src/renderer/routes/extension-route-registrator.injectable.tsx create mode 100644 src/renderer/routes/get-extension-route-id.ts create mode 100644 src/renderer/routes/get-extension-route-path.ts create mode 100644 src/renderer/routes/matching-route.injectable.ts create mode 100644 src/renderer/routes/navigate-to-route.injectable.ts create mode 100644 src/renderer/routes/navigate-to-url.injectable.ts create mode 100644 src/renderer/routes/query-parameters.injectable.ts create mode 100644 src/renderer/routes/route-is-active.injectable.ts create mode 100644 src/renderer/routes/route-path-parameters.injectable.ts create mode 100644 src/renderer/routes/route-specific-component-injection-token.ts create mode 100644 src/renderer/routes/routes.injectable.ts create mode 100644 src/renderer/routes/sibling-tabs.injectable.ts create mode 100644 src/renderer/theme-store.injectable.ts create mode 100644 src/test-utils/override-fs-with-fakes.ts diff --git a/integration/__tests__/app-preferences.tests.ts b/integration/__tests__/app-preferences.tests.ts index 32b89bbdf3..ed3cc71099 100644 --- a/integration/__tests__/app-preferences.tests.ts +++ b/integration/__tests__/app-preferences.tests.ts @@ -52,7 +52,7 @@ describe("preferences page tests", () => { ]; for (const { id, header } of pages) { - await window.click(`[data-testid=${id}-tab]`); + await window.click(`[data-testid=tab-link-for-${id}]`); await window.waitForSelector(`[data-testid=${id}-header] >> text=${header}`); } }, 10*60*1000); diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts index 5a4ae5085a..48767c8de2 100644 --- a/integration/__tests__/cluster-pages.tests.ts +++ b/integration/__tests__/cluster-pages.tests.ts @@ -12,295 +12,11 @@ import * as utils from "../helpers/utils"; import { minikubeReady } from "../helpers/minikube"; import type { Frame, Page } from "playwright"; +import { groupBy, toPairs } from "lodash/fp"; +import { pipeline } from "@ogre-tools/fp"; const TEST_NAMESPACE = "integration-tests"; -function getSidebarSelectors(itemId: string) { - const root = `.SidebarItem[data-test-id="${itemId}"]`; - - return { - expandSubMenu: `${root} .nav-item`, - subMenuLink: (href: string) => `[data-testid=cluster-sidebar] .sub-menu a[href^="${href}"]`, - }; -} - -function getLoadedSelector(page: CommonPage): string { - if (page.expectedText) { - return `${page.expectedSelector} >> text='${page.expectedText}'`; - } - - return page.expectedSelector; -} - -interface CommonPage { - name: string; - href: string; - expectedSelector: string; - expectedText?: string; -} - -interface TopPageTest { - page: CommonPage; -} - -interface SubPageTest { - drawerId: string; - pages: CommonPage[]; -} - -type CommonPageTest = TopPageTest | SubPageTest; - -function isTopPageTest(test: CommonPageTest): test is TopPageTest { - return typeof (test as any).page === "object"; -} - -const commonPageTests: CommonPageTest[] = [{ - page: { - name: "Cluster", - href: "/overview", - expectedSelector: "div[data-testid='cluster-overview-page'] div.label", - expectedText: "CPU", - }, -}, -{ - page: { - name: "Nodes", - href: "/nodes", - expectedSelector: "h5.title", - expectedText: "Nodes", - }, -}, -{ - drawerId: "workloads", - pages: [ - { - name: "Overview", - href: "/workloads", - expectedSelector: "h5.box", - expectedText: "Overview", - }, - { - name: "Pods", - href: "/pods", - expectedSelector: "h5.title", - expectedText: "Pods", - }, - { - name: "Deployments", - href: "/deployments", - expectedSelector: "h5.title", - expectedText: "Deployments", - }, - { - name: "DaemonSets", - href: "/daemonsets", - expectedSelector: "h5.title", - expectedText: "Daemon Sets", - }, - { - name: "StatefulSets", - href: "/statefulsets", - expectedSelector: "h5.title", - expectedText: "Stateful Sets", - }, - { - name: "ReplicaSets", - href: "/replicasets", - expectedSelector: "h5.title", - expectedText: "Replica Sets", - }, - { - name: "Jobs", - href: "/jobs", - expectedSelector: "h5.title", - expectedText: "Jobs", - }, - { - name: "CronJobs", - href: "/cronjobs", - expectedSelector: "h5.title", - expectedText: "Cron Jobs", - }, - ], -}, -{ - drawerId: "config", - pages: [ - { - name: "ConfigMaps", - href: "/configmaps", - expectedSelector: "h5.title", - expectedText: "Config Maps", - }, - { - name: "Secrets", - href: "/secrets", - expectedSelector: "h5.title", - expectedText: "Secrets", - }, - { - name: "Resource Quotas", - href: "/resourcequotas", - expectedSelector: "h5.title", - expectedText: "Resource Quotas", - }, - { - name: "Limit Ranges", - href: "/limitranges", - expectedSelector: "h5.title", - expectedText: "Limit Ranges", - }, - { - name: "HPA", - href: "/hpa", - expectedSelector: "h5.title", - expectedText: "Horizontal Pod Autoscalers", - }, - { - name: "Pod Disruption Budgets", - href: "/poddisruptionbudgets", - expectedSelector: "h5.title", - expectedText: "Pod Disruption Budgets", - }, - ], -}, -{ - drawerId: "networks", - pages: [ - { - name: "Services", - href: "/services", - expectedSelector: "h5.title", - expectedText: "Services", - }, - { - name: "Endpoints", - href: "/endpoints", - expectedSelector: "h5.title", - expectedText: "Endpoints", - }, - { - name: "Ingresses", - href: "/ingresses", - expectedSelector: "h5.title", - expectedText: "Ingresses", - }, - { - name: "Network Policies", - href: "/network-policies", - expectedSelector: "h5.title", - expectedText: "Network Policies", - }, - ], -}, -{ - drawerId: "storage", - pages: [ - { - name: "Persistent Volume Claims", - href: "/persistent-volume-claims", - expectedSelector: "h5.title", - expectedText: "Persistent Volume Claims", - }, - { - name: "Persistent Volumes", - href: "/persistent-volumes", - expectedSelector: "h5.title", - expectedText: "Persistent Volumes", - }, - { - name: "Storage Classes", - href: "/storage-classes", - expectedSelector: "h5.title", - expectedText: "Storage Classes", - }, - ], -}, -{ - page: { - name: "Namespaces", - href: "/namespaces", - expectedSelector: "h5.title", - expectedText: "Namespaces", - }, -}, -{ - page: { - name: "Events", - href: "/events", - expectedSelector: "h5.title", - expectedText: "Events", - }, -}, -{ - drawerId: "helm", - pages: [ - { - name: "Charts", - href: "/helm/charts", - expectedSelector: "div.HelmCharts input", - }, - { - name: "Releases", - href: "/helm/releases", - expectedSelector: "h5.title", - expectedText: "Releases", - }, - ], -}, -{ - drawerId: "users", - pages: [ - { - name: "Service Accounts", - href: "/service-accounts", - expectedSelector: "h5.title", - expectedText: "Service Accounts", - }, - { - name: "Roles", - href: "/roles", - expectedSelector: "h5.title", - expectedText: "Roles", - }, - { - name: "Cluster Roles", - href: "/cluster-roles", - expectedSelector: "h5.title", - expectedText: "Cluster Roles", - }, - { - name: "Role Bindings", - href: "/role-bindings", - expectedSelector: "h5.title", - expectedText: "Role Bindings", - }, - { - name: "Cluster Role Bindings", - href: "/cluster-role-bindings", - expectedSelector: "h5.title", - expectedText: "Cluster Role Bindings", - }, - { - name: "Pod Security Policies", - href: "/pod-security-policies", - expectedSelector: "h5.title", - expectedText: "Pod Security Policies", - }, - ], -}, -{ - drawerId: "custom-resources", - pages: [ - { - name: "Definitions", - href: "/crd/definitions", - expectedSelector: "h5.title", - expectedText: "Custom Resources", - }, - ], -}]; - utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { let window: Page, cleanup: () => Promise, frame: Frame; @@ -309,11 +25,11 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { await utils.clickWelcomeButton(window); frame = await utils.lauchMinikubeClusterFromCatalog(window); - }, 10*60*1000); + }, 10 * 60 * 1000); afterEach(async () => { await cleanup(); - }, 10*60*1000); + }, 10 * 60 * 1000); it("shows cluster context menu in sidebar", async () => { await frame.click(`[data-testid="sidebar-cluster-dropdown"]`); @@ -323,148 +39,390 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { await frame.waitForSelector(`.Menu >> text="Remove"`); }); - it("should navigate around common cluster pages", async () => { - for (const test of commonPageTests) { - if (isTopPageTest(test)) { - const { href, expectedText, expectedSelector } = test.page; - const menuButton = await frame.waitForSelector(`a[href^="${href}"]`); + it( + "should navigate around common cluster pages", - await menuButton.click(); - await frame.waitForSelector(`${expectedSelector} >> text='${expectedText}'`); + async () => { + const scenariosByParent = pipeline( + scenarios, + groupBy("parentSidebarItemTestId"), + toPairs, + ); - continue; + for (const [parentSidebarItemTestId, scenarios] of scenariosByParent) { + if (parentSidebarItemTestId !== "null") { + await frame.click(`[data-testid="${parentSidebarItemTestId}"]`); + } + + for (const scenario of scenarios) { + await frame.click(`[data-testid="${scenario.sidebarItemTestId}"]`); + + await frame.waitForSelector( + scenario.expectedSelector, + selectorTimeout, + ); + } + } + }, + + 10 * 60 * 1000, + ); + + it( + "show logs and highlight the log search entries", + async () => { + await navigateToPods(frame); + + const namespacesSelector = await frame.waitForSelector( + ".NamespaceSelect", + ); + + await namespacesSelector.click(); + await namespacesSelector.type("kube-system"); + await namespacesSelector.press("Enter"); + await namespacesSelector.click(); + + const kubeApiServerRow = await frame.waitForSelector( + "div.TableCell >> text=kube-apiserver", + ); + + await kubeApiServerRow.click(); + await frame.waitForSelector(".Drawer", { state: "visible" }); + + const showPodLogsIcon = await frame.waitForSelector( + ".Drawer .drawer-title .Icon >> text=subject", + ); + + showPodLogsIcon.click(); + + // Check if controls are available + await frame.waitForSelector(".Dock.isOpen"); + await frame.waitForSelector(".LogList .VirtualList"); + await frame.waitForSelector(".LogResourceSelector"); + + const logSearchInput = await frame.waitForSelector( + ".LogSearch .SearchInput input", + ); + + await logSearchInput.type(":"); + await frame.waitForSelector(".LogList .list span.active"); + + const showTimestampsButton = await frame.waitForSelector( + ".LogControls .show-timestamps", + ); + + await showTimestampsButton.click(); + + const showPreviousButton = await frame.waitForSelector( + ".LogControls .show-previous", + ); + + await showPreviousButton.click(); + }, + 10 * 60 * 1000, + ); + + it( + "should show the default namespaces", + async () => { + await navigateToNamespaces(frame); + await frame.waitForSelector("div.TableCell >> text='default'"); + await frame.waitForSelector("div.TableCell >> text='kube-system'"); + }, + 10 * 60 * 1000, + ); + + it( + `should create the ${TEST_NAMESPACE} and a pod in the namespace`, + async () => { + await navigateToNamespaces(frame); + await frame.click("button.add-button"); + await frame.waitForSelector( + "div.AddNamespaceDialog >> text='Create Namespace'", + ); + + const namespaceNameInput = await frame.waitForSelector( + ".AddNamespaceDialog input", + ); + + await namespaceNameInput.type(TEST_NAMESPACE); + await namespaceNameInput.press("Enter"); + + await frame.waitForSelector(`div.TableCell >> text=${TEST_NAMESPACE}`); + + await navigateToPods(frame); + + const namespacesSelector = await frame.waitForSelector( + ".NamespaceSelect", + ); + + await namespacesSelector.click(); + await namespacesSelector.type(TEST_NAMESPACE); + await namespacesSelector.press("Enter"); + await namespacesSelector.click(); + + await frame.click(".Icon.new-dock-tab"); + + try { + await frame.click("li.MenuItem.create-resource-tab", { + // NOTE: the following shouldn't be required, but is because without it a TypeError is thrown + // see: https://github.com/microsoft/playwright/issues/8229 + position: { + y: 0, + x: 0, + }, + }); + } catch (error) { + console.log(error); + await frame.waitForTimeout(100_000); } - const { drawerId, pages } = test; - const selectors = getSidebarSelectors(drawerId); - const mainPageSelector = `${selectors.subMenuLink(pages[0].href)} >> text='${pages[0].name}'`; + const testPodName = "nginx-create-pod-test"; + const monacoEditor = await frame.waitForSelector( + `.Dock.isOpen [data-test-component="monaco-editor"]`, + ); - await frame.click(selectors.expandSubMenu); - await frame.waitForSelector(mainPageSelector); + await monacoEditor.click(); + await monacoEditor.type("apiVersion: v1", { delay: 10 }); + await monacoEditor.press("Enter", { delay: 10 }); + await monacoEditor.type("kind: Pod", { delay: 10 }); + await monacoEditor.press("Enter", { delay: 10 }); + await monacoEditor.type("metadata:", { delay: 10 }); + await monacoEditor.press("Enter", { delay: 10 }); + await monacoEditor.type(` name: ${testPodName}`, { delay: 10 }); + await monacoEditor.press("Enter", { delay: 10 }); + await monacoEditor.type(`namespace: ${TEST_NAMESPACE}`, { delay: 10 }); + await monacoEditor.press("Enter", { delay: 10 }); + await monacoEditor.press("Backspace", { delay: 10 }); + await monacoEditor.type("spec:", { delay: 10 }); + await monacoEditor.press("Enter", { delay: 10 }); + await monacoEditor.type(" containers:", { delay: 10 }); + await monacoEditor.press("Enter", { delay: 10 }); + await monacoEditor.type(`- name: ${testPodName}`, { delay: 10 }); + await monacoEditor.press("Enter", { delay: 10 }); + await monacoEditor.type(" image: nginx:alpine", { delay: 10 }); + await monacoEditor.press("Enter", { delay: 10 }); - for (const page of pages) { - const subPageButton = await frame.waitForSelector(selectors.subMenuLink(page.href)); - - await subPageButton.click(); - await frame.waitForSelector(getLoadedSelector(page)); - } - - await frame.click(selectors.expandSubMenu); - await frame.waitForSelector(mainPageSelector, { state: "hidden" }); - } - }, 10*60*1000); - - it("show logs and highlight the log search entries", async () => { - await frame.click(`a[href="/workloads"]`); - await frame.click(`a[href="/pods"]`); - - const namespacesSelector = await frame.waitForSelector(".NamespaceSelect"); - - await namespacesSelector.click(); - await namespacesSelector.type("kube-system"); - await namespacesSelector.press("Enter"); - await namespacesSelector.click(); - - const kubeApiServerRow = await frame.waitForSelector("div.TableCell >> text=kube-apiserver"); - - await kubeApiServerRow.click(); - await frame.waitForSelector(".Drawer", { state: "visible" }); - - const showPodLogsIcon = await frame.waitForSelector(".Drawer .drawer-title .Icon >> text=subject"); - - showPodLogsIcon.click(); - - // Check if controls are available - await frame.waitForSelector(".Dock.isOpen"); - await frame.waitForSelector(".LogList .VirtualList"); - await frame.waitForSelector(".LogResourceSelector"); - - const logSearchInput = await frame.waitForSelector(".LogSearch .SearchInput input"); - - await logSearchInput.type(":"); - await frame.waitForSelector(".LogList .list span.active"); - - const showTimestampsButton = await frame.waitForSelector(".LogControls .show-timestamps"); - - await showTimestampsButton.click(); - - const showPreviousButton = await frame.waitForSelector(".LogControls .show-previous"); - - await showPreviousButton.click(); - }, 10*60*1000); - - it("should show the default namespaces", async () => { - await frame.click('a[href="/namespaces"]'); - await frame.waitForSelector("div.TableCell >> text='default'"); - await frame.waitForSelector("div.TableCell >> text='kube-system'"); - }, 10*60*1000); - - it(`should create the ${TEST_NAMESPACE} and a pod in the namespace`, async () => { - await frame.click('a[href="/namespaces"]'); - await frame.click("button.add-button"); - await frame.waitForSelector("div.AddNamespaceDialog >> text='Create Namespace'"); - - const namespaceNameInput = await frame.waitForSelector(".AddNamespaceDialog input"); - - await namespaceNameInput.type(TEST_NAMESPACE); - await namespaceNameInput.press("Enter"); - - await frame.waitForSelector(`div.TableCell >> text=${TEST_NAMESPACE}`); - - if ((await frame.innerText(`a[href^="/workloads"] .expand-icon`)) === "keyboard_arrow_down") { - await frame.click(`a[href^="/workloads"]`); - } - - await frame.click(`a[href^="/pods"]`); - - const namespacesSelector = await frame.waitForSelector(".NamespaceSelect"); - - await namespacesSelector.click(); - await namespacesSelector.type(TEST_NAMESPACE); - await namespacesSelector.press("Enter"); - await namespacesSelector.click(); - - await frame.click(".Icon.new-dock-tab"); - - try { - await frame.click("li.MenuItem.create-resource-tab", { - // NOTE: the following shouldn't be required, but is because without it a TypeError is thrown - // see: https://github.com/microsoft/playwright/issues/8229 - position: { - y: 0, - x: 0, - }, - }); - } catch (error) { - console.log(error); - await frame.waitForTimeout(100_000); - } - - const testPodName = "nginx-create-pod-test"; - const monacoEditor = await frame.waitForSelector(`.Dock.isOpen [data-test-component="monaco-editor"]`); - - await monacoEditor.click(); - await monacoEditor.type("apiVersion: v1", { delay: 10 }); - await monacoEditor.press("Enter", { delay: 10 }); - await monacoEditor.type("kind: Pod", { delay: 10 }); - await monacoEditor.press("Enter", { delay: 10 }); - await monacoEditor.type("metadata:", { delay: 10 }); - await monacoEditor.press("Enter", { delay: 10 }); - await monacoEditor.type(` name: ${testPodName}`, { delay: 10 }); - await monacoEditor.press("Enter", { delay: 10 }); - await monacoEditor.type(`namespace: ${TEST_NAMESPACE}`, { delay: 10 }); - await monacoEditor.press("Enter", { delay: 10 }); - await monacoEditor.press("Backspace", { delay: 10 }); - await monacoEditor.type("spec:", { delay: 10 }); - await monacoEditor.press("Enter", { delay: 10 }); - await monacoEditor.type(" containers:", { delay: 10 }); - await monacoEditor.press("Enter", { delay: 10 }); - await monacoEditor.type(`- name: ${testPodName}`, { delay: 10 }); - await monacoEditor.press("Enter", { delay: 10 }); - await monacoEditor.type(" image: nginx:alpine", { delay: 10 }); - await monacoEditor.press("Enter", { delay: 10 }); - - await frame.click(".Dock .Button >> text='Create'"); - await frame.waitForSelector(`.TableCell >> text=${testPodName}`); - }, 10*60*1000); + await frame.click(".Dock .Button >> text='Create'"); + await frame.waitForSelector(`.TableCell >> text=${testPodName}`); + }, + 10 * 60 * 1000, + ); }); + +const selectorTimeout = { timeout: 30000 }; + +const scenarios = [ + { + expectedSelector: "div[data-testid='cluster-overview-page'] div.label", + parentSidebarItemTestId: null, + sidebarItemTestId: "sidebar-item-link-for-cluster-overview", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: null, + sidebarItemTestId: "sidebar-item-link-for-nodes", + }, + + { + expectedSelector: 'h5 >> text="Overview"', + parentSidebarItemTestId: "sidebar-item-link-for-workloads", + sidebarItemTestId: "sidebar-item-link-for-overview", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-workloads", + sidebarItemTestId: "sidebar-item-link-for-pods", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-workloads", + sidebarItemTestId: "sidebar-item-link-for-deployments", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-workloads", + sidebarItemTestId: "sidebar-item-link-for-daemon-sets", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-workloads", + sidebarItemTestId: "sidebar-item-link-for-stateful-sets", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-workloads", + sidebarItemTestId: "sidebar-item-link-for-replica-sets", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-workloads", + sidebarItemTestId: "sidebar-item-link-for-jobs", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-workloads", + sidebarItemTestId: "sidebar-item-link-for-cron-jobs", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-config", + sidebarItemTestId: "sidebar-item-link-for-config-maps", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-config", + sidebarItemTestId: "sidebar-item-link-for-secrets", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-config", + sidebarItemTestId: "sidebar-item-link-for-resource-quotas", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-config", + sidebarItemTestId: "sidebar-item-link-for-limit-ranges", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-config", + sidebarItemTestId: "sidebar-item-link-for-horizontal-pod-auto-scalers", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-config", + sidebarItemTestId: "sidebar-item-link-for-pod-disruption-budgets", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-network", + sidebarItemTestId: "sidebar-item-link-for-services", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-network", + sidebarItemTestId: "sidebar-item-link-for-endpoints", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-network", + sidebarItemTestId: "sidebar-item-link-for-ingresses", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-network", + sidebarItemTestId: "sidebar-item-link-for-network-policies", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-storage", + sidebarItemTestId: "sidebar-item-link-for-persistent-volume-claims", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-storage", + sidebarItemTestId: "sidebar-item-link-for-persistent-volumes", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-storage", + sidebarItemTestId: "sidebar-item-link-for-storage-classes", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: null, + sidebarItemTestId: "sidebar-item-link-for-namespaces", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: null, + sidebarItemTestId: "sidebar-item-link-for-events", + }, + + { + expectedSelector: "div.HelmCharts input", + parentSidebarItemTestId: "sidebar-item-link-for-helm", + sidebarItemTestId: "sidebar-item-link-for-charts", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-helm", + sidebarItemTestId: "sidebar-item-link-for-releases", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-user-management", + sidebarItemTestId: "sidebar-item-link-for-service-accounts", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-user-management", + sidebarItemTestId: "sidebar-item-link-for-roles", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-user-management", + sidebarItemTestId: "sidebar-item-link-for-cluster-roles", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-user-management", + sidebarItemTestId: "sidebar-item-link-for-role-bindings", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-user-management", + sidebarItemTestId: "sidebar-item-link-for-cluster-role-bindings", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: "sidebar-item-link-for-user-management", + sidebarItemTestId: "sidebar-item-link-for-pod-security-policies", + }, + + { + expectedSelector: "h5.title", + parentSidebarItemTestId: null, + sidebarItemTestId: "sidebar-item-link-for-custom-resources", + }, +]; + +const navigateToPods = async (frame: Frame) => { + await frame.click(`[data-testid="sidebar-item-link-for-workloads"]`); + await frame.click(`[data-testid="sidebar-item-link-for-pods"]`); +}; + +const navigateToNamespaces = async (frame: Frame) => { + await frame.click(`[data-testid="sidebar-item-link-for-namespaces"]`); +}; diff --git a/package.json b/package.json index 2eeed20f71..b861bcd1c5 100644 --- a/package.json +++ b/package.json @@ -202,8 +202,9 @@ "@hapi/call": "^8.0.1", "@hapi/subtext": "^7.0.3", "@kubernetes/client-node": "^0.16.3", - "@ogre-tools/injectable": "5.1.2", - "@ogre-tools/injectable-react": "5.1.2", + "@ogre-tools/injectable": "5.2.0", + "@ogre-tools/injectable-react": "5.2.0", + "@ogre-tools/fp": "5.2.0", "@sentry/electron": "^2.5.4", "@sentry/integrations": "^6.19.2", "@types/circular-dependency-plugin": "5.0.5", diff --git a/src/common/__tests__/base-store.test.ts b/src/common/__tests__/base-store.test.ts index f35ed7721f..07cea7039e 100644 --- a/src/common/__tests__/base-store.test.ts +++ b/src/common/__tests__/base-store.test.ts @@ -7,10 +7,14 @@ import mockFs from "mock-fs"; import { BaseStore } from "../base-store"; import { action, comparer, makeObservable, observable, toJS } from "mobx"; import { readFileSync } from "fs"; -import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; +import getConfigurationFileModelInjectable + from "../get-configuration-file-model/get-configuration-file-model.injectable"; +import appVersionInjectable + from "../get-configuration-file-model/app-version/app-version.injectable"; jest.mock("electron", () => ({ ipcMain: { @@ -78,11 +82,13 @@ describe("BaseStore", () => { let store: TestStore; beforeEach(async () => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + const mainDi = getDiForUnitTesting({ doGeneralOverrides: true }); - dis.mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory"); + mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory"); + mainDi.permitSideEffects(getConfigurationFileModelInjectable); + mainDi.permitSideEffects(appVersionInjectable); - await dis.runSetups(); + await mainDi.runSetups(); store = undefined; TestStore.resetInstance(); @@ -99,9 +105,9 @@ describe("BaseStore", () => { }); afterEach(() => { + mockFs.restore(); store.disableSync(); TestStore.resetInstance(); - mockFs.restore(); }); describe("persistence", () => { diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 9bfc5346ed..ccc2d897b9 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -18,12 +18,12 @@ import type { DiContainer, } from "@ogre-tools/injectable"; - -import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; -import directoryForUserDataInjectable - from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; +import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; +import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable"; console = new Console(stdout, stderr); @@ -80,15 +80,17 @@ describe("cluster-store", () => { let createCluster: (model: ClusterModel) => Cluster; beforeEach(async () => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + mainDi = getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(); - mainDi = dis.mainDi; - + mainDi.override(clusterStoreInjectable, (di) => ClusterStore.createInstance({ createCluster: di.inject(createClusterInjectionToken) })); mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); - await dis.runSetups(); + mainDi.permitSideEffects(getConfigurationFileModelInjectable); + mainDi.permitSideEffects(appVersionInjectable); + + await mainDi.runSetups(); createCluster = mainDi.inject(createClusterInjectionToken); }); diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index e93f53abe5..9bc12d103d 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -7,10 +7,13 @@ import { anyObject } from "jest-mock-extended"; import mockFs from "mock-fs"; import logger from "../../main/logger"; import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog"; -import { HotbarStore } from "../hotbar-store"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; -import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import writeFileInjectable from "../fs/write-file.injectable"; +import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; +import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable"; +import type { DiContainer } from "@ogre-tools/injectable"; +import hotbarStoreInjectable from "../hotbar-store.injectable"; +import { HotbarStore } from "../hotbar-store"; +import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; jest.mock("../../main/catalog/catalog-entity-registry", () => ({ catalogEntityRegistry: { @@ -18,9 +21,11 @@ jest.mock("../../main/catalog/catalog-entity-registry", () => ({ getMockCatalogEntity({ apiVersion: "v1", kind: "Cluster", + status: { phase: "Running", }, + metadata: { uid: "1dfa26e2ebab15780a3547e9c7fa785c", name: "mycluster", @@ -28,12 +33,15 @@ jest.mock("../../main/catalog/catalog-entity-registry", () => ({ labels: {}, }, }), + getMockCatalogEntity({ apiVersion: "v1", kind: "Cluster", + status: { phase: "Running", }, + metadata: { uid: "55b42c3c7ba3b04193416cda405269a5", name: "my_shiny_cluster", @@ -41,12 +49,15 @@ jest.mock("../../main/catalog/catalog-entity-registry", () => ({ labels: {}, }, }), + getMockCatalogEntity({ apiVersion: "v1", kind: "Cluster", + status: { phase: "Running", }, + metadata: { uid: "catalog-entity", name: "Catalog", @@ -113,270 +124,247 @@ const awsCluster = getMockCatalogEntity({ }); describe("HotbarStore", () => { + let di: DiContainer; + let hotbarStore: HotbarStore; + beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = getDiForUnitTesting({ doGeneralOverrides: true }); - di.override(writeFileInjectable, () => () => undefined); - di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.permitSideEffects(getConfigurationFileModelInjectable); + di.permitSideEffects(appVersionInjectable); - await di.runSetups(); + di.override(hotbarStoreInjectable, () => { + HotbarStore.resetInstance(); - mockFs({ - "some-directory-for-user-data": { - "lens-hotbar-store.json": JSON.stringify({}), - }, + return HotbarStore.createInstance({ + catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable), + }); }); - - HotbarStore.createInstance(); }); afterEach(() => { - HotbarStore.resetInstance(); mockFs.restore(); }); - describe("load", () => { - it("loads one hotbar by default", () => { - expect(HotbarStore.getInstance().hotbars.length).toEqual(1); + describe("given no migrations", () => { + beforeEach(async () => { + mockFs(); + + await di.runSetups(); + + hotbarStore = di.inject(hotbarStoreInjectable); + }); + + describe("load", () => { + it("loads one hotbar by default", () => { + expect(hotbarStore.hotbars.length).toEqual(1); + }); + }); + + describe("add", () => { + it("adds a hotbar", () => { + hotbarStore.add({ name: "hottest" }); + expect(hotbarStore.hotbars.length).toEqual(2); + }); + }); + + describe("hotbar items", () => { + it("initially creates 12 empty cells", () => { + expect(hotbarStore.getActive().items.length).toEqual(12); + }); + + it("initially adds catalog entity as first item", () => { + expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog"); + }); + + it("adds items", () => { + hotbarStore.addToHotbar(testCluster); + const items = hotbarStore.getActive().items.filter(Boolean); + + expect(items.length).toEqual(2); + }); + + it("removes items", () => { + hotbarStore.addToHotbar(testCluster); + hotbarStore.removeFromHotbar("test"); + hotbarStore.removeFromHotbar("catalog-entity"); + const items = hotbarStore.getActive().items.filter(Boolean); + + expect(items).toStrictEqual([]); + }); + + it("does nothing if removing with invalid uid", () => { + hotbarStore.addToHotbar(testCluster); + hotbarStore.removeFromHotbar("invalid uid"); + const items = hotbarStore.getActive().items.filter(Boolean); + + expect(items.length).toEqual(2); + }); + + it("moves item to empty cell", () => { + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(minikubeCluster); + hotbarStore.addToHotbar(awsCluster); + + expect(hotbarStore.getActive().items[6]).toBeNull(); + + hotbarStore.restackItems(1, 5); + + expect(hotbarStore.getActive().items[5]).toBeTruthy(); + expect(hotbarStore.getActive().items[5].entity.uid).toEqual("test"); + }); + + it("moves items down", () => { + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(minikubeCluster); + hotbarStore.addToHotbar(awsCluster); + + // aws -> catalog + hotbarStore.restackItems(3, 0); + + const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null); + + expect(items.slice(0, 4)).toEqual(["aws", "catalog-entity", "test", "minikube"]); + }); + + it("moves items up", () => { + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(minikubeCluster); + hotbarStore.addToHotbar(awsCluster); + + // test -> aws + hotbarStore.restackItems(1, 3); + + const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null); + + expect(items.slice(0, 4)).toEqual(["catalog-entity", "minikube", "aws", "test"]); + }); + + it("logs an error if cellIndex is out of bounds", () => { + hotbarStore.add({ name: "hottest", id: "hottest" }); + hotbarStore.setActiveHotbar("hottest"); + + const { error } = logger; + const mocked = jest.fn(); + + logger.error = mocked; + + hotbarStore.addToHotbar(testCluster, -1); + expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); + + hotbarStore.addToHotbar(testCluster, 12); + expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); + + hotbarStore.addToHotbar(testCluster, 13); + expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); + + logger.error = error; + }); + + it("throws an error if getId is invalid or returns not a string", () => { + expect(() => hotbarStore.addToHotbar({} as any)).toThrowError(TypeError); + expect(() => hotbarStore.addToHotbar({ getId: () => true } as any)).toThrowError(TypeError); + }); + + it("throws an error if getName is invalid or returns not a string", () => { + expect(() => hotbarStore.addToHotbar({ getId: () => "" } as any)).toThrowError(TypeError); + expect(() => hotbarStore.addToHotbar({ getId: () => "", getName: () => 4 } as any)).toThrowError(TypeError); + }); + + it("does nothing when item moved to same cell", () => { + hotbarStore.addToHotbar(testCluster); + hotbarStore.restackItems(1, 1); + + expect(hotbarStore.getActive().items[1].entity.uid).toEqual("test"); + }); + + it("new items takes first empty cell", () => { + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(awsCluster); + hotbarStore.restackItems(0, 3); + hotbarStore.addToHotbar(minikubeCluster); + + expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube"); + }); + + it("throws if invalid arguments provided", () => { + // Prevent writing to stderr during this render. + const { error, warn } = console; + + console.error = jest.fn(); + console.warn = jest.fn(); + + hotbarStore.addToHotbar(testCluster); + + expect(() => hotbarStore.restackItems(-5, 0)).toThrow(); + expect(() => hotbarStore.restackItems(2, -1)).toThrow(); + expect(() => hotbarStore.restackItems(14, 1)).toThrow(); + expect(() => hotbarStore.restackItems(11, 112)).toThrow(); + + // Restore writing to stderr. + console.error = error; + console.warn = warn; + }); + + it("checks if entity already pinned to hotbar", () => { + hotbarStore.addToHotbar(testCluster); + + expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy(); + expect(hotbarStore.isAddedToActive(awsCluster)).toBeFalsy(); + }); }); }); - describe("add", () => { - it("adds a hotbar", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.add({ name: "hottest" }); - expect(hotbarStore.hotbars.length).toEqual(2); - }); - }); - - describe("hotbar items", () => { - it("initially creates 12 empty cells", () => { - const hotbarStore = HotbarStore.getInstance(); - - expect(hotbarStore.getActive().items.length).toEqual(12); - }); - - it("initially adds catalog entity as first item", () => { - const hotbarStore = HotbarStore.getInstance(); - - expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog"); - }); - - it("adds items", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - const items = hotbarStore.getActive().items.filter(Boolean); - - expect(items.length).toEqual(2); - }); - - it("removes items", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.removeFromHotbar("test"); - hotbarStore.removeFromHotbar("catalog-entity"); - const items = hotbarStore.getActive().items.filter(Boolean); - - expect(items).toStrictEqual([]); - }); - - it("does nothing if removing with invalid uid", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.removeFromHotbar("invalid uid"); - const items = hotbarStore.getActive().items.filter(Boolean); - - expect(items.length).toEqual(2); - }); - - it("moves item to empty cell", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.addToHotbar(minikubeCluster); - hotbarStore.addToHotbar(awsCluster); - - expect(hotbarStore.getActive().items[6]).toBeNull(); - - hotbarStore.restackItems(1, 5); - - expect(hotbarStore.getActive().items[5]).toBeTruthy(); - expect(hotbarStore.getActive().items[5].entity.uid).toEqual("test"); - }); - - it("moves items down", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.addToHotbar(minikubeCluster); - hotbarStore.addToHotbar(awsCluster); - - // aws -> catalog - hotbarStore.restackItems(3, 0); - - const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null); - - expect(items.slice(0, 4)).toEqual(["aws", "catalog-entity", "test", "minikube"]); - }); - - it("moves items up", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.addToHotbar(minikubeCluster); - hotbarStore.addToHotbar(awsCluster); - - // test -> aws - hotbarStore.restackItems(1, 3); - - const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null); - - expect(items.slice(0, 4)).toEqual(["catalog-entity", "minikube", "aws", "test"]); - }); - - it("logs an error if cellIndex is out of bounds", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.add({ name: "hottest", id: "hottest" }); - hotbarStore.setActiveHotbar("hottest"); - - const { error } = logger; - const mocked = jest.fn(); - - logger.error = mocked; - - hotbarStore.addToHotbar(testCluster, -1); - expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); - - hotbarStore.addToHotbar(testCluster, 12); - expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); - - hotbarStore.addToHotbar(testCluster, 13); - expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); - - logger.error = error; - }); - - it("throws an error if getId is invalid or returns not a string", () => { - const hotbarStore = HotbarStore.getInstance(); - - expect(() => hotbarStore.addToHotbar({} as any)).toThrowError(TypeError); - expect(() => hotbarStore.addToHotbar({ getId: () => true } as any)).toThrowError(TypeError); - }); - - it("throws an error if getName is invalid or returns not a string", () => { - const hotbarStore = HotbarStore.getInstance(); - - expect(() => hotbarStore.addToHotbar({ getId: () => "" } as any)).toThrowError(TypeError); - expect(() => hotbarStore.addToHotbar({ getId: () => "", getName: () => 4 } as any)).toThrowError(TypeError); - }); - - it("does nothing when item moved to same cell", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.restackItems(1, 1); - - expect(hotbarStore.getActive().items[1].entity.uid).toEqual("test"); - }); - - it("new items takes first empty cell", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.addToHotbar(awsCluster); - hotbarStore.restackItems(0, 3); - hotbarStore.addToHotbar(minikubeCluster); - - expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube"); - }); - - it("throws if invalid arguments provided", () => { - // Prevent writing to stderr during this render. - const { error, warn } = console; - - console.error = jest.fn(); - console.warn = jest.fn(); - - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - - expect(() => hotbarStore.restackItems(-5, 0)).toThrow(); - expect(() => hotbarStore.restackItems(2, -1)).toThrow(); - expect(() => hotbarStore.restackItems(14, 1)).toThrow(); - expect(() => hotbarStore.restackItems(11, 112)).toThrow(); - - // Restore writing to stderr. - console.error = error; - console.warn = warn; - }); - - it("checks if entity already pinned to hotbar", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - - expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy(); - expect(hotbarStore.isAddedToActive(awsCluster)).toBeFalsy(); - }); - }); - - describe("pre beta-5 migrations", () => { - beforeEach(() => { - HotbarStore.resetInstance(); - const mockOpts = { - "some-directory-for-user-data": { + describe("given pre beta-5 configurations", () => { + beforeEach(async () => { + const configurationToBeMigrated = { + "some-electron-app-path-for-user-data": { "lens-hotbar-store.json": JSON.stringify({ __internal__: { migrations: { version: "5.0.0-beta.3", }, }, - "hotbars": [ + hotbars: [ { - "id": "3caac17f-aec2-4723-9694-ad204465d935", - "name": "myhotbar", - "items": [ + id: "3caac17f-aec2-4723-9694-ad204465d935", + name: "myhotbar", + items: [ { - "entity": { - "uid": "1dfa26e2ebab15780a3547e9c7fa785c", + entity: { + uid: "1dfa26e2ebab15780a3547e9c7fa785c", }, }, { - "entity": { - "uid": "55b42c3c7ba3b04193416cda405269a5", + entity: { + uid: "55b42c3c7ba3b04193416cda405269a5", }, }, { - "entity": { - "uid": "176fd331968660832f62283219d7eb6e", + entity: { + uid: "176fd331968660832f62283219d7eb6e", }, }, { - "entity": { - "uid": "61c4fb45528840ebad1badc25da41d14", - "name": "user1-context", - "source": "local", + entity: { + uid: "61c4fb45528840ebad1badc25da41d14", + name: "user1-context", + source: "local", }, }, { - "entity": { - "uid": "27d6f99fe9e7548a6e306760bfe19969", - "name": "foo2", - "source": "local", + entity: { + uid: "27d6f99fe9e7548a6e306760bfe19969", + name: "foo2", + source: "local", }, }, null, { - "entity": { - "uid": "c0b20040646849bb4dcf773e43a0bf27", - "name": "multinode-demo", - "source": "local", + entity: { + uid: "c0b20040646849bb4dcf773e43a0bf27", + name: "multinode-demo", + source: "local", }, }, null, @@ -391,29 +379,27 @@ describe("HotbarStore", () => { }, }; - mockFs(mockOpts); + mockFs(configurationToBeMigrated); - HotbarStore.createInstance(); - }); + await di.runSetups(); - afterEach(() => { - mockFs.restore(); + hotbarStore = di.inject(hotbarStoreInjectable); }); it("allows to retrieve a hotbar", () => { - const hotbar = HotbarStore.getInstance().getById("3caac17f-aec2-4723-9694-ad204465d935"); + const hotbar = hotbarStore.getById("3caac17f-aec2-4723-9694-ad204465d935"); expect(hotbar.id).toBe("3caac17f-aec2-4723-9694-ad204465d935"); }); it("clears cells without entity", () => { - const items = HotbarStore.getInstance().hotbars[0].items; + const items = hotbarStore.hotbars[0].items; expect(items[2]).toBeNull(); }); it("adds extra data to cells with according entity", () => { - const items = HotbarStore.getInstance().hotbars[0].items; + const items = hotbarStore.hotbars[0].items; expect(items[0]).toEqual({ entity: { diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index 465acc2a45..9fb3713be1 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -26,34 +26,41 @@ import { Console } from "console"; import { SemVer } from "semver"; import electron from "electron"; import { stdout, stderr } from "process"; -import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; import userStoreInjectable from "../user-store/user-store.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import type { ClusterStoreModel } from "../cluster-store/cluster-store"; import { defaultTheme } from "../vars"; import writeFileInjectable from "../fs/write-file.injectable"; +import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; +import getConfigurationFileModelInjectable + from "../get-configuration-file-model/get-configuration-file-model.injectable"; +import appVersionInjectable + from "../get-configuration-file-model/app-version/app-version.injectable"; console = new Console(stdout, stderr); describe("user store tests", () => { let userStore: UserStore; - let mainDi: DiContainer; + let di: DiContainer; beforeEach(async () => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + di = getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(); - mainDi = dis.mainDi; + di.override(writeFileInjectable, () => () => undefined); + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.override(userStoreInjectable, () => UserStore.createInstance()); - mainDi.override(writeFileInjectable, () => () => undefined); - mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.permitSideEffects(getConfigurationFileModelInjectable); + di.permitSideEffects(appVersionInjectable); - await dis.runSetups(); + await di.runSetups(); }); afterEach(() => { + UserStore.resetInstance(); mockFs.restore(); }); @@ -61,12 +68,7 @@ describe("user store tests", () => { beforeEach(() => { mockFs({ "some-directory-for-user-data": { "config.json": "{}", "kube_config": "{}" }}); - userStore = mainDi.inject(userStoreInjectable); - }); - - afterEach(() => { - mockFs.restore(); - UserStore.resetInstance(); + userStore = di.inject(userStoreInjectable); }); it("allows setting and retrieving lastSeenAppVersion", () => { @@ -128,12 +130,7 @@ describe("user store tests", () => { }, }); - userStore = mainDi.inject(userStoreInjectable); - }); - - afterEach(() => { - UserStore.resetInstance(); - mockFs.restore(); + userStore = di.inject(userStoreInjectable); }); it("sets last seen app version to 0.0.0", () => { diff --git a/src/common/app-paths/app-paths.test.ts b/src/common/app-paths/app-paths.test.ts index 5a811d3018..ff53454da1 100644 --- a/src/common/app-paths/app-paths.test.ts +++ b/src/common/app-paths/app-paths.test.ts @@ -10,7 +10,6 @@ import type { PathName } from "./app-path-names"; import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable"; import appNameInjectable from "../../main/app-paths/app-name/app-name.injectable"; import directoryForIntegrationTestingInjectable from "../../main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable"; -import path from "path"; describe("app-paths", () => { let mainDi: DiContainer; @@ -85,7 +84,7 @@ describe("app-paths", () => { recent: "some-recent", temp: "some-temp", videos: "some-videos", - userData: `some-app-data${path.sep}some-app-name`, + userData: "some-app-data/some-app-name", }); }); @@ -108,7 +107,7 @@ describe("app-paths", () => { recent: "some-recent", temp: "some-temp", videos: "some-videos", - userData: `some-app-data${path.sep}some-app-name`, + userData: "some-app-data/some-app-name", }); }); }); @@ -128,7 +127,7 @@ describe("app-paths", () => { expect({ appData, userData }).toEqual({ appData: "some-integration-testing-app-data", - userData: `some-integration-testing-app-data${path.sep}some-app-name`, + userData: `some-integration-testing-app-data/some-app-name`, }); }); @@ -137,7 +136,7 @@ describe("app-paths", () => { expect({ appData, userData }).toEqual({ appData: "some-integration-testing-app-data", - userData: `some-integration-testing-app-data${path.sep}some-app-name`, + userData: "some-integration-testing-app-data/some-app-name", }); }); }); diff --git a/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts b/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts index 7a7aa8658d..5f6b14f5ba 100644 --- a/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts +++ b/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts @@ -3,14 +3,18 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import path from "path"; import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable"; +import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; const directoryForBinariesInjectable = getInjectable({ id: "directory-for-binaries", - instantiate: (di) => - path.join(di.inject(directoryForUserDataInjectable), "binaries"), + instantiate: (di) => { + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const directoryForUserData = di.inject(directoryForUserDataInjectable); + + return getAbsolutePath(directoryForUserData, "binaries"); + }, }); export default directoryForBinariesInjectable; diff --git a/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts b/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts index c0869c4b11..f371860f18 100644 --- a/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts +++ b/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts @@ -4,13 +4,20 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable"; -import path from "path"; +import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; const directoryForKubeConfigsInjectable = getInjectable({ id: "directory-for-kube-configs", - instantiate: (di) => - path.resolve(di.inject(directoryForUserDataInjectable), "kubeconfigs"), + instantiate: (di) => { + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const directoryForUserData = di.inject(directoryForUserDataInjectable); + + return getAbsolutePath( + directoryForUserData, + "kubeconfigs", + ); + }, }); export default directoryForKubeConfigsInjectable; diff --git a/src/common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts b/src/common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts new file mode 100644 index 0000000000..4b6ce56601 --- /dev/null +++ b/src/common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.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 } from "@ogre-tools/injectable"; +import directoryForBinariesInjectable from "../directory-for-binaries/directory-for-binaries.injectable"; +import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; + +const directoryForKubectlBinariesInjectable = getInjectable({ + id: "directory-for-kubectl-binaries", + + instantiate: (di) => { + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const directoryForBinaries = di.inject(directoryForBinariesInjectable); + + + return getAbsolutePath(directoryForBinaries, "kubectl"); + }, +}); + +export default directoryForKubectlBinariesInjectable; diff --git a/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts b/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts index 1157ec96da..6f22aa32f2 100644 --- a/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts +++ b/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts @@ -3,19 +3,18 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import path from "path"; import directoryForKubeConfigsInjectable from "../directory-for-kube-configs/directory-for-kube-configs.injectable"; +import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; const getCustomKubeConfigDirectoryInjectable = getInjectable({ id: "get-custom-kube-config-directory", - instantiate: (di) => (directoryName: string) => { + instantiate: (di) => { const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable); + const getAbsolutePath = di.inject(getAbsolutePathInjectable); - return path.resolve( - directoryForKubeConfigs, - directoryName, - ); + return (directoryName: string) => + getAbsolutePath(directoryForKubeConfigs, directoryName); }, }); diff --git a/src/common/base-store.ts b/src/common/base-store.ts index 856860614a..b3d2e6e601 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -4,7 +4,7 @@ */ import path from "path"; -import Config from "conf"; +import type Config from "conf"; import type { Options as ConfOptions } from "conf/dist/source/types"; import { ipcMain, ipcRenderer } from "electron"; import { IEqualsComparer, makeObservable, reaction, runInAction } from "mobx"; @@ -15,8 +15,8 @@ import isEqual from "lodash/isEqual"; import { isTestEnv } from "./vars"; import { kebabCase } from "lodash"; import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; -import directoryForUserDataInjectable - from "./app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForUserDataInjectable from "./app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import getConfigurationFileModelInjectable from "./get-configuration-file-model/get-configuration-file-model.injectable"; export interface BaseStoreParams extends ConfOptions { syncOptions?: { @@ -51,7 +51,11 @@ export abstract class BaseStore extends Singleton { logger.info(`[${kebabCase(this.displayName).toUpperCase()}]: LOADING from ${this.path} ...`); } - this.storeConfig = new Config({ + const di = getLegacyGlobalDiForExtensionApi(); + + const getConfigurationFileModel = di.inject(getConfigurationFileModelInjectable); + + this.storeConfig = getConfigurationFileModel({ ...this.params, projectName: "lens", projectVersion: getAppVersion(), diff --git a/src/common/catalog-entities/general-catalog-entities/general-catalog-entity-injection-token.ts b/src/common/catalog-entities/general-catalog-entities/general-catalog-entity-injection-token.ts new file mode 100644 index 0000000000..ad743802bf --- /dev/null +++ b/src/common/catalog-entities/general-catalog-entities/general-catalog-entity-injection-token.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { GeneralEntity } from "../index"; + +export const generalCatalogEntityInjectionToken = getInjectionToken({ + id: "general-catalog-entity-injection-token", +}); diff --git a/src/common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable.ts b/src/common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable.ts new file mode 100644 index 0000000000..15195a4b74 --- /dev/null +++ b/src/common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { generalCatalogEntityInjectionToken } from "../general-catalog-entity-injection-token"; +import { GeneralEntity } from "../../index"; +import { buildURL } from "../../../utils/buildUrl"; +import catalogRouteInjectable from "../../../front-end-routing/routes/catalog/catalog-route.injectable"; + +const catalogCatalogEntityInjectable = getInjectable({ + id: "general-catalog-entity-for-catalog", + + instantiate: (di) => { + const route = di.inject(catalogRouteInjectable); + const url = buildURL(route.path); + + return new GeneralEntity({ + metadata: { + uid: "catalog-entity", + name: "Catalog", + source: "app", + labels: {}, + }, + spec: { + path: url, + icon: { + material: "view_list", + background: "#3d90ce", + }, + }, + status: { + phase: "active", + }, + }); + }, + + injectionToken: generalCatalogEntityInjectionToken, +}); + +export default catalogCatalogEntityInjectable; diff --git a/src/common/catalog-entities/general-catalog-entities/implementations/preferences-catalog-entity.injectable.ts b/src/common/catalog-entities/general-catalog-entities/implementations/preferences-catalog-entity.injectable.ts new file mode 100644 index 0000000000..80d4458b6d --- /dev/null +++ b/src/common/catalog-entities/general-catalog-entities/implementations/preferences-catalog-entity.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { generalCatalogEntityInjectionToken } from "../general-catalog-entity-injection-token"; +import { GeneralEntity } from "../../index"; +import { buildURL } from "../../../utils/buildUrl"; +import appPreferencesRouteInjectable from "../../../front-end-routing/routes/preferences/app/app-preferences-route.injectable"; + +const preferencesCatalogEntityInjectable = getInjectable({ + id: "general-catalog-entity-for-preferences", + + instantiate: (di) => { + const route = di.inject(appPreferencesRouteInjectable); + const url = buildURL(route.path); + + return new GeneralEntity({ + metadata: { + uid: "preferences-entity", + name: "Preferences", + source: "app", + labels: {}, + }, + spec: { + path: url, + icon: { + material: "settings", + background: "#3d90ce", + }, + }, + status: { + phase: "active", + }, + }); + }, + + injectionToken: generalCatalogEntityInjectionToken, +}); + +export default preferencesCatalogEntityInjectable; diff --git a/src/common/catalog-entities/general-catalog-entities/implementations/welcome-catalog-entity.injectable.ts b/src/common/catalog-entities/general-catalog-entities/implementations/welcome-catalog-entity.injectable.ts new file mode 100644 index 0000000000..363dd73c5f --- /dev/null +++ b/src/common/catalog-entities/general-catalog-entities/implementations/welcome-catalog-entity.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { generalCatalogEntityInjectionToken } from "../general-catalog-entity-injection-token"; +import { GeneralEntity } from "../../index"; +import { buildURL } from "../../../utils/buildUrl"; +import welcomeRouteInjectable from "../../../front-end-routing/routes/welcome/welcome-route.injectable"; + +const welcomeCatalogEntityInjectable = getInjectable({ + id: "general-catalog-entity-for-welcome", + + instantiate: (di) => { + const route = di.inject(welcomeRouteInjectable); + const url = buildURL(route.path); + + return new GeneralEntity({ + metadata: { + uid: "welcome-page-entity", + name: "Welcome Page", + source: "app", + labels: {}, + }, + spec: { + path: url, + icon: { + material: "meeting_room", + background: "#3d90ce", + }, + }, + status: { + phase: "active", + }, + }); + }, + + injectionToken: generalCatalogEntityInjectionToken, +}); + +export default welcomeCatalogEntityInjectable; diff --git a/src/common/cluster-store/cluster-store.injectable.ts b/src/common/cluster-store/cluster-store.injectable.ts index f6295e4bd7..a47978376a 100644 --- a/src/common/cluster-store/cluster-store.injectable.ts +++ b/src/common/cluster-store/cluster-store.injectable.ts @@ -13,6 +13,8 @@ const clusterStoreInjectable = getInjectable({ ClusterStore.createInstance({ createCluster: di.inject(createClusterInjectionToken), }), + + causesSideEffects: true, }); export default clusterStoreInjectable; diff --git a/src/common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable.ts b/src/common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable.ts index fc1e3ef0f0..4bb5cb2495 100644 --- a/src/common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable.ts +++ b/src/common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable.ts @@ -3,17 +3,21 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import path from "path"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import getAbsolutePathInjectable from "../path/get-absolute-path.injectable"; const directoryForLensLocalStorageInjectable = getInjectable({ id: "directory-for-lens-local-storage", - instantiate: (di) => - path.resolve( - di.inject(directoryForUserDataInjectable), + instantiate: (di) => { + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const directoryForUserData = di.inject(directoryForUserDataInjectable); + + return getAbsolutePath( + directoryForUserData, "lens-local-storage", - ), + ); + }, }); export default directoryForLensLocalStorageInjectable; diff --git a/src/common/front-end-routing/navigate-to-front-page.injectable.ts b/src/common/front-end-routing/navigate-to-front-page.injectable.ts new file mode 100644 index 0000000000..a48eee6c04 --- /dev/null +++ b/src/common/front-end-routing/navigate-to-front-page.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import navigateToCatalogInjectable from "./routes/catalog/navigate-to-catalog.injectable"; + +const navigateToFrontPageInjectable = getInjectable({ + id: "navigate-to-front-page", + instantiate: (di) => di.inject(navigateToCatalogInjectable), +}); + +export default navigateToFrontPageInjectable; diff --git a/src/common/front-end-routing/navigate-to-route-injection-token.ts b/src/common/front-end-routing/navigate-to-route-injection-token.ts new file mode 100644 index 0000000000..d6321f017e --- /dev/null +++ b/src/common/front-end-routing/navigate-to-route-injection-token.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { Route } from "./route-injection-token"; + +type InferParametersFrom = TRoute extends Route + ? TParameters + : never; + +type RequiredKeys = Exclude< + { + [K in keyof T]: T extends Record ? K : never; + }[keyof T], + undefined +>; + +type ObjectContainingNoRequired = T extends void + ? never + : RequiredKeys extends [] + ? any + : never; + +type ObjectContainsNoRequired = T extends ObjectContainingNoRequired + ? true + : false; + +// TODO: Missing types for: +// - Navigating to route without parameters, with parameters +// - Navigating to route with required parameters, without parameters +type Parameters = TParameters extends void + ? {} + : ObjectContainsNoRequired extends true + ? { parameters?: TParameters } + : { parameters: TParameters }; + +export type NavigateToRouteOptions = Parameters< + InferParametersFrom +> & { + query?: Record; + fragment?: string; + withoutAffectingBackButton?: boolean; +}; + +export type NavigateToRoute = >( + route: TRoute, + options?: NavigateToRouteOptions) => void; + +export const navigateToRouteInjectionToken = getInjectionToken( + { id: "navigate-to-route-injection-token" }, +); diff --git a/src/common/front-end-routing/navigate-to-url-injection-token.ts b/src/common/front-end-routing/navigate-to-url-injection-token.ts new file mode 100644 index 0000000000..560d62ce4a --- /dev/null +++ b/src/common/front-end-routing/navigate-to-url-injection-token.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; + +export interface NavigateToUrlOptions { + withoutAffectingBackButton?: boolean; + forceRootFrame?: boolean; +} + +export type NavigateToUrl = (url: string, options?: NavigateToUrlOptions) => void; + +export const navigateToUrlInjectionToken = getInjectionToken( + { id: "navigate-to-url-injection-token" }, +); diff --git a/src/common/front-end-routing/navigation-ipc-channel.ts b/src/common/front-end-routing/navigation-ipc-channel.ts new file mode 100644 index 0000000000..6094664f81 --- /dev/null +++ b/src/common/front-end-routing/navigation-ipc-channel.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { createChannel } from "../ipc-channel/create-channel/create-channel"; +import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; + +export const appNavigationIpcChannel = createChannel(IpcRendererNavigationEvents.NAVIGATE_IN_APP); +export const clusterFrameNavigationIpcChannel = createChannel(IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER); diff --git a/src/common/front-end-routing/route-injection-token.ts b/src/common/front-end-routing/route-injection-token.ts new file mode 100644 index 0000000000..7ecdbba635 --- /dev/null +++ b/src/common/front-end-routing/route-injection-token.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { IComputedValue } from "mobx"; +import type { LensRendererExtension } from "../../extensions/lens-renderer-extension"; + +export const routeInjectionToken = getInjectionToken>({ + id: "route-injection-token", +}); + +export interface Route { + path: string; + clusterFrame: boolean; + isEnabled: IComputedValue; + extension?: LensRendererExtension; + + readonly parameterSignature?: TParameter; +} diff --git a/src/common/front-end-routing/routes/add-cluster/add-cluster-route.injectable.ts b/src/common/front-end-routing/routes/add-cluster/add-cluster-route.injectable.ts new file mode 100644 index 0000000000..ce5bdcebd3 --- /dev/null +++ b/src/common/front-end-routing/routes/add-cluster/add-cluster-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../route-injection-token"; + +const addClusterRouteInjectable = getInjectable({ + id: "add-cluster-route", + + instantiate: () => ({ + path: "/add-cluster", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default addClusterRouteInjectable; diff --git a/src/common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable.ts b/src/common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable.ts new file mode 100644 index 0000000000..15595d51c2 --- /dev/null +++ b/src/common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import addClusterRouteInjectable from "./add-cluster-route.injectable"; +import { navigateToRouteInjectionToken } from "../../navigate-to-route-injection-token"; + +const navigateToAddClusterInjectable = getInjectable({ + id: "navigate-to-add-cluster", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(addClusterRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToAddClusterInjectable; diff --git a/src/common/front-end-routing/routes/catalog/catalog-route.injectable.ts b/src/common/front-end-routing/routes/catalog/catalog-route.injectable.ts new file mode 100644 index 0000000000..6dd4343049 --- /dev/null +++ b/src/common/front-end-routing/routes/catalog/catalog-route.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { Route, routeInjectionToken } from "../../route-injection-token"; + +export interface CatalogPathParameters { + group?: string; + kind?: string; +} + +const catalogRouteInjectable = getInjectable({ + id: "catalog-route", + + instantiate: (): Route => ({ + path: "/catalog/:group?/:kind?", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default catalogRouteInjectable; diff --git a/src/common/front-end-routing/routes/catalog/navigate-to-catalog.injectable.ts b/src/common/front-end-routing/routes/catalog/navigate-to-catalog.injectable.ts new file mode 100644 index 0000000000..0d52d41764 --- /dev/null +++ b/src/common/front-end-routing/routes/catalog/navigate-to-catalog.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import catalogRouteInjectable, { CatalogPathParameters } from "./catalog-route.injectable"; +import { navigateToRouteInjectionToken } from "../../navigate-to-route-injection-token"; + +export type NavigateToCatalog = (parameters?: CatalogPathParameters) => void; + +const navigateToCatalogInjectable = getInjectable({ + id: "navigate-to-catalog", + + instantiate: (di): NavigateToCatalog => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const catalogRoute = di.inject(catalogRouteInjectable); + + return (parameters) => + navigateToRoute(catalogRoute, { + parameters, + }); + }, +}); + +export default navigateToCatalogInjectable; diff --git a/src/common/front-end-routing/routes/cluster-view/cluster-view-route.injectable.ts b/src/common/front-end-routing/routes/cluster-view/cluster-view-route.injectable.ts new file mode 100644 index 0000000000..23fea9874b --- /dev/null +++ b/src/common/front-end-routing/routes/cluster-view/cluster-view-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../route-injection-token"; + +const clusterViewRouteInjectable = getInjectable({ + id: "cluster-view-route", + + instantiate: () => ({ + path: "/cluster/:clusterId", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default clusterViewRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster-view/navigate-to-cluster-view.injectable.ts b/src/common/front-end-routing/routes/cluster-view/navigate-to-cluster-view.injectable.ts new file mode 100644 index 0000000000..4edc3d3bb5 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster-view/navigate-to-cluster-view.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { navigateToRouteInjectionToken } from "../../navigate-to-route-injection-token"; +import clusterViewRouteInjectable from "./cluster-view-route.injectable"; + +export type NavigateToClusterView = (clusterId: string) => void; + +const navigateToClusterViewInjectable = getInjectable({ + id: "navigate-to-cluster-view", + + instantiate: (di): NavigateToClusterView => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(clusterViewRouteInjectable); + + return (clusterId) => + navigateToRoute(route, { parameters: { clusterId }}); + }, +}); + +export default navigateToClusterViewInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/config-maps/config-maps-route.injectable.ts b/src/common/front-end-routing/routes/cluster/config/config-maps/config-maps-route.injectable.ts new file mode 100644 index 0000000000..b8c605fc64 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/config-maps/config-maps-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const configMapsRouteInjectable = getInjectable({ + id: "config-maps-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "configmaps"); + + return { + path: "/configmaps", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default configMapsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/config-maps/navigate-to-config-maps.injectable.ts b/src/common/front-end-routing/routes/cluster/config/config-maps/navigate-to-config-maps.injectable.ts new file mode 100644 index 0000000000..d4fa31027f --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/config-maps/navigate-to-config-maps.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import configMapsRouteInjectable from "./config-maps-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToConfigMapsInjectable = getInjectable({ + id: "navigate-to-config-maps", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(configMapsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToConfigMapsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/horizontal-pod-autoscalers-route.injectable.ts b/src/common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/horizontal-pod-autoscalers-route.injectable.ts new file mode 100644 index 0000000000..d9eff7a026 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/horizontal-pod-autoscalers-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const horizontalPodAutoscalersRouteInjectable = getInjectable({ + id: "horizontal-pod-autoscalers-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "horizontalpodautoscalers"); + + return { + path: "/hpa", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default horizontalPodAutoscalersRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/navigate-to-horizontal-pod-autoscalers.injectable.ts b/src/common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/navigate-to-horizontal-pod-autoscalers.injectable.ts new file mode 100644 index 0000000000..ab171f4a08 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/navigate-to-horizontal-pod-autoscalers.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import horizontalPodAutoscalersRouteInjectable from "./horizontal-pod-autoscalers-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToHorizontalPodAutoscalersInjectable = getInjectable({ + id: "navigate-to-horizontal-pod-autoscalers", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(horizontalPodAutoscalersRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToHorizontalPodAutoscalersInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/limit-ranges/limit-ranges-route.injectable.ts b/src/common/front-end-routing/routes/cluster/config/limit-ranges/limit-ranges-route.injectable.ts new file mode 100644 index 0000000000..70f40cb99d --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/limit-ranges/limit-ranges-route.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const limitRangesRouteInjectable = getInjectable({ + id: "limit-ranges-route", + + instantiate: (di) => { + const limitRangesIsAllowed = di.inject( + isAllowedResourceInjectable, + "limitranges", + ); + + return { + path: "/limitranges", + clusterFrame: true, + isEnabled: limitRangesIsAllowed, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default limitRangesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/limit-ranges/navigate-to-limit-ranges.injectable.ts b/src/common/front-end-routing/routes/cluster/config/limit-ranges/navigate-to-limit-ranges.injectable.ts new file mode 100644 index 0000000000..00e8c89d7c --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/limit-ranges/navigate-to-limit-ranges.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import limitRangesRouteInjectable from "./limit-ranges-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToLimitRangesInjectable = getInjectable({ + id: "navigate-to-limit-ranges", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(limitRangesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToLimitRangesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/pod-disruption-budgets/navigate-to-pod-disruption-budgets.injectable.ts b/src/common/front-end-routing/routes/cluster/config/pod-disruption-budgets/navigate-to-pod-disruption-budgets.injectable.ts new file mode 100644 index 0000000000..5a3f17027d --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/pod-disruption-budgets/navigate-to-pod-disruption-budgets.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import podDisruptionBudgetsRouteInjectable from "./pod-disruption-budgets-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToPodDisruptionBudgetsInjectable = getInjectable({ + id: "navigate-to-pod-disruption-budgets", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(podDisruptionBudgetsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToPodDisruptionBudgetsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/pod-disruption-budgets/pod-disruption-budgets-route.injectable.ts b/src/common/front-end-routing/routes/cluster/config/pod-disruption-budgets/pod-disruption-budgets-route.injectable.ts new file mode 100644 index 0000000000..8d113e6727 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/pod-disruption-budgets/pod-disruption-budgets-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const podDisruptionBudgetsRouteInjectable = getInjectable({ + id: "pod-disruption-budgets-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "poddisruptionbudgets"); + + return { + path: "/poddisruptionbudgets", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default podDisruptionBudgetsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/resource-quotas/navigate-to-resource-quotas.injectable.ts b/src/common/front-end-routing/routes/cluster/config/resource-quotas/navigate-to-resource-quotas.injectable.ts new file mode 100644 index 0000000000..3637782115 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/resource-quotas/navigate-to-resource-quotas.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import resourceQuotasRouteInjectable from "./resource-quotas-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToResourceQuotasInjectable = getInjectable({ + id: "navigate-to-resource-quotas", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(resourceQuotasRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToResourceQuotasInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/resource-quotas/resource-quotas-route.injectable.ts b/src/common/front-end-routing/routes/cluster/config/resource-quotas/resource-quotas-route.injectable.ts new file mode 100644 index 0000000000..ee28deba5e --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/resource-quotas/resource-quotas-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const resourceQuotasRouteInjectable = getInjectable({ + id: "resource-quotas-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "resourcequotas"); + + return { + path: "/resourcequotas", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default resourceQuotasRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/secrets/navigate-to-secrets.injectable.ts b/src/common/front-end-routing/routes/cluster/config/secrets/navigate-to-secrets.injectable.ts new file mode 100644 index 0000000000..b8afdf74c3 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/secrets/navigate-to-secrets.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import secretsRouteInjectable from "./secrets-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToSecretsInjectable = getInjectable({ + id: "navigate-to-secrets", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(secretsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToSecretsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/config/secrets/secrets-route.injectable.ts b/src/common/front-end-routing/routes/cluster/config/secrets/secrets-route.injectable.ts new file mode 100644 index 0000000000..7806238e0c --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/config/secrets/secrets-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const secretsRouteInjectable = getInjectable({ + id: "secrets-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "secrets"); + + return { + path: "/secrets", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default secretsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/custom-resources/crd-list/crd-list-route.injectable.ts b/src/common/front-end-routing/routes/cluster/custom-resources/crd-list/crd-list-route.injectable.ts new file mode 100644 index 0000000000..3a4865cc53 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/custom-resources/crd-list/crd-list-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const crdListRouteInjectable = getInjectable({ + id: "crd-list-route", + + instantiate: () => ({ + path: "/crd/definitions", + clusterFrame: true, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default crdListRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/custom-resources/crd-list/navigate-to-crd-list.injectable.ts b/src/common/front-end-routing/routes/cluster/custom-resources/crd-list/navigate-to-crd-list.injectable.ts new file mode 100644 index 0000000000..bd78dd1174 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/custom-resources/crd-list/navigate-to-crd-list.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import crdListRouteInjectable from "./crd-list-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToCrdListInjectable = getInjectable({ + id: "navigate-to-crd-list", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(crdListRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToCrdListInjectable; diff --git a/src/common/front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable.ts b/src/common/front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable.ts new file mode 100644 index 0000000000..731b4e1e96 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { Route, routeInjectionToken } from "../../../../route-injection-token"; + +export interface CustomResourcesPathParameters { + group?: string; + name?: string; +} + +const customResourcesRouteInjectable = getInjectable({ + id: "custom-resources-route", + + instantiate: (): Route => ({ + path: "/crd/:group?/:name?", + clusterFrame: true, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default customResourcesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/custom-resources/custom-resources/navigate-to-custom-resources.injectable.ts b/src/common/front-end-routing/routes/cluster/custom-resources/custom-resources/navigate-to-custom-resources.injectable.ts new file mode 100644 index 0000000000..7a60068299 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/custom-resources/custom-resources/navigate-to-custom-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 } from "@ogre-tools/injectable"; +import customResourcesRouteInjectable, { CustomResourcesPathParameters } from "./custom-resources-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToCustomResourcesInjectable = getInjectable({ + id: "navigate-to-custom-resources", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(customResourcesRouteInjectable); + + return (parameters?: CustomResourcesPathParameters) => + navigateToRoute(route, { parameters }); + }, +}); + +export default navigateToCustomResourcesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/events/events-route.injectable.ts b/src/common/front-end-routing/routes/cluster/events/events-route.injectable.ts new file mode 100644 index 0000000000..6fb9ff51f9 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/events/events-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../route-injection-token"; + +const eventsRouteInjectable = getInjectable({ + id: "events-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "events"); + + return { + path: "/events", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default eventsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/events/navigate-to-events.injectable.ts b/src/common/front-end-routing/routes/cluster/events/navigate-to-events.injectable.ts new file mode 100644 index 0000000000..767f69b497 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/events/navigate-to-events.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import eventsRouteInjectable from "./events-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; + +const navigateToEventsInjectable = getInjectable({ + id: "navigate-to-events", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(eventsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToEventsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/helm/charts/helm-charts-route.injectable.ts b/src/common/front-end-routing/routes/cluster/helm/charts/helm-charts-route.injectable.ts new file mode 100644 index 0000000000..22aa837be4 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/helm/charts/helm-charts-route.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { Route, routeInjectionToken } from "../../../../route-injection-token"; + +export interface HelmChartsPathParameters { + repo?: string; + chartName?: string; +} + +const helmChartsRouteInjectable = getInjectable({ + id: "helm-charts-route", + + instantiate: (): Route => ({ + path: "/helm/charts/:repo?/:chartName?", + clusterFrame: true, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default helmChartsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable.ts b/src/common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable.ts new file mode 100644 index 0000000000..1e5e895aae --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import helmChartsRouteInjectable, { HelmChartsPathParameters } from "./helm-charts-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +export type NavigateToHelmCharts = (parameters?: HelmChartsPathParameters) => void; + +const navigateToHelmChartsInjectable = getInjectable({ + id: "navigate-to-helm-charts", + + instantiate: (di): NavigateToHelmCharts => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(helmChartsRouteInjectable); + + return (parameters) => + navigateToRoute(route, { + parameters, + }); + }, +}); + +export default navigateToHelmChartsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/helm/releases/helm-releases-route.injectable.ts b/src/common/front-end-routing/routes/cluster/helm/releases/helm-releases-route.injectable.ts new file mode 100644 index 0000000000..5a90e1e1ff --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/helm/releases/helm-releases-route.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { Route, routeInjectionToken } from "../../../../route-injection-token"; + +export interface HelmReleasesPathParameters { + namespace?: string; + name?: string; +} + +const helmReleasesRouteInjectable = getInjectable({ + id: "helm-releases-route", + + instantiate: (): Route => ({ + path: "/helm/releases/:namespace?/:name?", + clusterFrame: true, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default helmReleasesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable.ts b/src/common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable.ts new file mode 100644 index 0000000000..9302cdc02d --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import helmReleasesRouteInjectable, { HelmReleasesPathParameters } from "./helm-releases-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +export type NavigateToHelmReleases = (parameters?: HelmReleasesPathParameters) => void; + +const navigateToHelmReleasesInjectable = getInjectable({ + id: "navigate-to-helm-releases", + + instantiate: (di): NavigateToHelmReleases => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(helmReleasesRouteInjectable); + + return (parameters) => + navigateToRoute(route, { parameters }); + }, +}); + +export default navigateToHelmReleasesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/namespaces/namespaces-route.injectable.ts b/src/common/front-end-routing/routes/cluster/namespaces/namespaces-route.injectable.ts new file mode 100644 index 0000000000..361771d575 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/namespaces/namespaces-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../route-injection-token"; + +const namespacesRouteInjectable = getInjectable({ + id: "namespaces-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "namespaces"); + + return { + path: "/namespaces", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default namespacesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/namespaces/navigate-to-namespaces.injectable.ts b/src/common/front-end-routing/routes/cluster/namespaces/navigate-to-namespaces.injectable.ts new file mode 100644 index 0000000000..85fa63c695 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/namespaces/navigate-to-namespaces.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import namespacesRouteInjectable from "./namespaces-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; + +const navigateToNamespacesInjectable = getInjectable({ + id: "navigate-to-namespaces", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(namespacesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToNamespacesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/network/endpoints/endpoints-route.injectable.ts b/src/common/front-end-routing/routes/cluster/network/endpoints/endpoints-route.injectable.ts new file mode 100644 index 0000000000..cc093f6cf3 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/network/endpoints/endpoints-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const endpointsRouteInjectable = getInjectable({ + id: "endpoints-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "endpoints"); + + return { + path: "/endpoints", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default endpointsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/network/endpoints/navigate-to-endpoints.injectable.ts b/src/common/front-end-routing/routes/cluster/network/endpoints/navigate-to-endpoints.injectable.ts new file mode 100644 index 0000000000..04439b3d3b --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/network/endpoints/navigate-to-endpoints.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import endpointsRouteInjectable from "./endpoints-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToEndpointsInjectable = getInjectable({ + id: "navigate-to-endpoints", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(endpointsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToEndpointsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/network/ingresses/ingresses-route.injectable.ts b/src/common/front-end-routing/routes/cluster/network/ingresses/ingresses-route.injectable.ts new file mode 100644 index 0000000000..8c30cefe74 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/network/ingresses/ingresses-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const ingressesRouteInjectable = getInjectable({ + id: "ingresses-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "ingresses"); + + return { + path: "/ingresses", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default ingressesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/network/ingresses/navigate-to-ingresses.injectable.ts b/src/common/front-end-routing/routes/cluster/network/ingresses/navigate-to-ingresses.injectable.ts new file mode 100644 index 0000000000..b47f9d4be3 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/network/ingresses/navigate-to-ingresses.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import ingressesRouteInjectable from "./ingresses-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToIngressesInjectable = getInjectable({ + id: "navigate-to-ingresses", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(ingressesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToIngressesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/network/network-policies/navigate-to-network-policies.injectable.ts b/src/common/front-end-routing/routes/cluster/network/network-policies/navigate-to-network-policies.injectable.ts new file mode 100644 index 0000000000..82bc76ee9f --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/network/network-policies/navigate-to-network-policies.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import networkPoliciesRouteInjectable from "./network-policies-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToNetworkPoliciesInjectable = getInjectable({ + id: "navigate-to-network-policies", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(networkPoliciesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToNetworkPoliciesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/network/network-policies/network-policies-route.injectable.ts b/src/common/front-end-routing/routes/cluster/network/network-policies/network-policies-route.injectable.ts new file mode 100644 index 0000000000..99f94d68fe --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/network/network-policies/network-policies-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const networkPoliciesRouteInjectable = getInjectable({ + id: "network-policies-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "networkpolicies"); + + return { + path: "/network-policies", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default networkPoliciesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable.ts b/src/common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable.ts new file mode 100644 index 0000000000..ba16fd2866 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import portForwardsRouteInjectable, { PortForwardsPathParameters } from "./port-forwards-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +export type NavigateToPortForwards = (parameters?: PortForwardsPathParameters) => void; + +const navigateToPortForwardsInjectable = getInjectable({ + id: "navigate-to-port-forwards", + + instantiate: (di): NavigateToPortForwards => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(portForwardsRouteInjectable); + + return (parameters) => + navigateToRoute(route, { parameters }); + }, +}); + +export default navigateToPortForwardsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/network/port-forwards/port-forwards-route.injectable.ts b/src/common/front-end-routing/routes/cluster/network/port-forwards/port-forwards-route.injectable.ts new file mode 100644 index 0000000000..a089033b76 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/network/port-forwards/port-forwards-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { Route, routeInjectionToken } from "../../../../route-injection-token"; + +export interface PortForwardsPathParameters { + forwardport?: string; +} + +const portForwardsRouteInjectable = getInjectable({ + id: "port-forwards-route", + + instantiate: (): Route => ({ + path: "/port-forwards/:forwardport?", + clusterFrame: true, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default portForwardsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/network/services/navigate-to-services.injectable.ts b/src/common/front-end-routing/routes/cluster/network/services/navigate-to-services.injectable.ts new file mode 100644 index 0000000000..02aa060b89 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/network/services/navigate-to-services.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import servicesRouteInjectable from "./services-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToServicesInjectable = getInjectable({ + id: "navigate-to-services", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(servicesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToServicesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/network/services/services-route.injectable.ts b/src/common/front-end-routing/routes/cluster/network/services/services-route.injectable.ts new file mode 100644 index 0000000000..efd83a3ffa --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/network/services/services-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const servicesRouteInjectable = getInjectable({ + id: "services-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "services"); + + return { + path: "/services", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default servicesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/nodes/navigate-to-nodes.injectable.ts b/src/common/front-end-routing/routes/cluster/nodes/navigate-to-nodes.injectable.ts new file mode 100644 index 0000000000..eccdd2377d --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/nodes/navigate-to-nodes.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import nodesRouteInjectable from "./nodes-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; + +const navigateToNodesInjectable = getInjectable({ + id: "navigate-to-nodes", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(nodesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToNodesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/nodes/nodes-route.injectable.ts b/src/common/front-end-routing/routes/cluster/nodes/nodes-route.injectable.ts new file mode 100644 index 0000000000..2713f1cc18 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/nodes/nodes-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../route-injection-token"; + +const nodesRouteInjectable = getInjectable({ + id: "nodes-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "nodes"); + + return { + path: "/nodes", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default nodesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/overview/cluster-overview-route.injectable.ts b/src/common/front-end-routing/routes/cluster/overview/cluster-overview-route.injectable.ts new file mode 100644 index 0000000000..9598666209 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/overview/cluster-overview-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../route-injection-token"; + +const clusterOverviewRouteInjectable = getInjectable({ + id: "cluster-overview-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "nodes"); + + return { + path: "/overview", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default clusterOverviewRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/overview/navigate-to-cluster-overview.injectable.ts b/src/common/front-end-routing/routes/cluster/overview/navigate-to-cluster-overview.injectable.ts new file mode 100644 index 0000000000..f15dca518c --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/overview/navigate-to-cluster-overview.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import clusterOverviewRouteInjectable from "./cluster-overview-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; + +const navigateToClusterOverviewInjectable = getInjectable({ + id: "navigate-to-cluster-overview", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(clusterOverviewRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToClusterOverviewInjectable; diff --git a/src/common/front-end-routing/routes/cluster/storage/persistent-volume-claims/navigate-to-persistent-volume-claims.injectable.ts b/src/common/front-end-routing/routes/cluster/storage/persistent-volume-claims/navigate-to-persistent-volume-claims.injectable.ts new file mode 100644 index 0000000000..015ad6e988 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/storage/persistent-volume-claims/navigate-to-persistent-volume-claims.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import persistentVolumeClaimsRouteInjectable from "./persistent-volume-claims-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToPersistentVolumeClaimsInjectable = getInjectable({ + id: "navigate-to-persistent-volume-claims", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(persistentVolumeClaimsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToPersistentVolumeClaimsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/storage/persistent-volume-claims/persistent-volume-claims-route.injectable.ts b/src/common/front-end-routing/routes/cluster/storage/persistent-volume-claims/persistent-volume-claims-route.injectable.ts new file mode 100644 index 0000000000..a451c282e5 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/storage/persistent-volume-claims/persistent-volume-claims-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const persistentVolumeClaimsRouteInjectable = getInjectable({ + id: "persistent-volume-claims-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "persistentvolumeclaims"); + + return { + path: "/persistent-volume-claims", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default persistentVolumeClaimsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/storage/persistent-volumes/navigate-to-persistent-volumes.injectable.ts b/src/common/front-end-routing/routes/cluster/storage/persistent-volumes/navigate-to-persistent-volumes.injectable.ts new file mode 100644 index 0000000000..6702dd6712 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/storage/persistent-volumes/navigate-to-persistent-volumes.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import persistentVolumesRouteInjectable from "./persistent-volumes-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToPersistentVolumesInjectable = getInjectable({ + id: "navigate-to-persistent-volumes", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(persistentVolumesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToPersistentVolumesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/storage/persistent-volumes/persistent-volumes-route.injectable.ts b/src/common/front-end-routing/routes/cluster/storage/persistent-volumes/persistent-volumes-route.injectable.ts new file mode 100644 index 0000000000..68c8fa08f5 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/storage/persistent-volumes/persistent-volumes-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const persistentVolumesRouteInjectable = getInjectable({ + id: "persistent-volumes-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "persistentvolumes"); + + return { + path: "/persistent-volumes", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default persistentVolumesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/storage/storage-classes/navigate-to-storage-classes.injectable.ts b/src/common/front-end-routing/routes/cluster/storage/storage-classes/navigate-to-storage-classes.injectable.ts new file mode 100644 index 0000000000..adaba3225b --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/storage/storage-classes/navigate-to-storage-classes.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import storageClassesRouteInjectable from "./storage-classes-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToStorageClassesInjectable = getInjectable({ + id: "navigate-to-storage-classes", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(storageClassesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToStorageClassesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/storage/storage-classes/storage-classes-route.injectable.ts b/src/common/front-end-routing/routes/cluster/storage/storage-classes/storage-classes-route.injectable.ts new file mode 100644 index 0000000000..5499b5ba37 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/storage/storage-classes/storage-classes-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const storageClassesRouteInjectable = getInjectable({ + id: "storage-classes-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "storageclasses"); + + return { + path: "/storage-classes", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default storageClassesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/cluster-role-bindings/cluster-role-bindings-route.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/cluster-role-bindings/cluster-role-bindings-route.injectable.ts new file mode 100644 index 0000000000..483397e6e1 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/cluster-role-bindings/cluster-role-bindings-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const clusterRoleBindingsRouteInjectable = getInjectable({ + id: "cluster-role-bindings-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "clusterrolebindings"); + + return { + path: "/cluster-role-bindings", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default clusterRoleBindingsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/cluster-role-bindings/navigate-to-cluster-role-bindings.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/cluster-role-bindings/navigate-to-cluster-role-bindings.injectable.ts new file mode 100644 index 0000000000..f6bd0bfd97 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/cluster-role-bindings/navigate-to-cluster-role-bindings.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import clusterRoleBindingsRouteInjectable from "./cluster-role-bindings-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToClusterRoleBindingsInjectable = getInjectable({ + id: "navigate-to-cluster-role-bindings", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(clusterRoleBindingsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToClusterRoleBindingsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/cluster-roles/cluster-roles-route.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/cluster-roles/cluster-roles-route.injectable.ts new file mode 100644 index 0000000000..fced712dbf --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/cluster-roles/cluster-roles-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const clusterRolesRouteInjectable = getInjectable({ + id: "cluster-roles-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "clusterroles"); + + return { + path: "/cluster-roles", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default clusterRolesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/cluster-roles/navigate-to-cluster-roles.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/cluster-roles/navigate-to-cluster-roles.injectable.ts new file mode 100644 index 0000000000..ed56679155 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/cluster-roles/navigate-to-cluster-roles.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import clusterRolesRouteInjectable from "./cluster-roles-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToClusterRolesInjectable = getInjectable({ + id: "navigate-to-cluster-roles", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(clusterRolesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToClusterRolesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/pod-security-policies/navigate-to-pod-security-policies.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/pod-security-policies/navigate-to-pod-security-policies.injectable.ts new file mode 100644 index 0000000000..205354ba3b --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/pod-security-policies/navigate-to-pod-security-policies.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import podSecurityPoliciesRouteInjectable from "./pod-security-policies-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToPodSecurityPoliciesInjectable = getInjectable({ + id: "navigate-to-pod-security-policies", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(podSecurityPoliciesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToPodSecurityPoliciesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/pod-security-policies/pod-security-policies-route.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/pod-security-policies/pod-security-policies-route.injectable.ts new file mode 100644 index 0000000000..89220511c1 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/pod-security-policies/pod-security-policies-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const podSecurityPoliciesRouteInjectable = getInjectable({ + id: "pod-security-policies-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "podsecuritypolicies"); + + return { + path: "/pod-security-policies", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default podSecurityPoliciesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/role-bindings/navigate-to-role-bindings.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/role-bindings/navigate-to-role-bindings.injectable.ts new file mode 100644 index 0000000000..ce5bb3713b --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/role-bindings/navigate-to-role-bindings.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import roleBindingsRouteInjectable from "./role-bindings-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToRoleBindingsInjectable = getInjectable({ + id: "navigate-to-role-bindings", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(roleBindingsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToRoleBindingsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/role-bindings/role-bindings-route.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/role-bindings/role-bindings-route.injectable.ts new file mode 100644 index 0000000000..24e03d5cd9 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/role-bindings/role-bindings-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const roleBindingsRouteInjectable = getInjectable({ + id: "role-bindings-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "rolebindings"); + + return { + path: "/role-bindings", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default roleBindingsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/roles/navigate-to-roles.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/roles/navigate-to-roles.injectable.ts new file mode 100644 index 0000000000..adebb64ae1 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/roles/navigate-to-roles.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import rolesRouteInjectable from "./roles-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToRolesInjectable = getInjectable({ + id: "navigate-to-roles", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(rolesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToRolesInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/roles/roles-route.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/roles/roles-route.injectable.ts new file mode 100644 index 0000000000..097cbb1cf5 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/roles/roles-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const rolesRouteInjectable = getInjectable({ + id: "roles-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "roles"); + + return { + path: "/roles", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default rolesRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/service-accounts/navigate-to-service-accounts.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/service-accounts/navigate-to-service-accounts.injectable.ts new file mode 100644 index 0000000000..5eb1861a92 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/service-accounts/navigate-to-service-accounts.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import serviceAccountsRouteInjectable from "./service-accounts-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToServiceAccountsInjectable = getInjectable({ + id: "navigate-to-service-accounts", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(serviceAccountsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToServiceAccountsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/user-management/service-accounts/service-accounts-route.injectable.ts b/src/common/front-end-routing/routes/cluster/user-management/service-accounts/service-accounts-route.injectable.ts new file mode 100644 index 0000000000..f3424f1d8c --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/user-management/service-accounts/service-accounts-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const serviceAccountsRouteInjectable = getInjectable({ + id: "service-accounts-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "serviceaccounts"); + + return { + path: "/service-accounts", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default serviceAccountsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/cron-jobs/cron-jobs-route.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/cron-jobs/cron-jobs-route.injectable.ts new file mode 100644 index 0000000000..a99e13ea1f --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/cron-jobs/cron-jobs-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const cronJobsRouteInjectable = getInjectable({ + id: "cron-jobs-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "cronjobs"); + + return { + path: "/cronjobs", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default cronJobsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/cron-jobs/navigate-to-cron-jobs.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/cron-jobs/navigate-to-cron-jobs.injectable.ts new file mode 100644 index 0000000000..42b104055e --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/cron-jobs/navigate-to-cron-jobs.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import cronJobsRouteInjectable from "./cron-jobs-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToCronJobsInjectable = getInjectable({ + id: "navigate-to-cron-jobs", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(cronJobsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToCronJobsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/daemonsets/daemonsets-route.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/daemonsets/daemonsets-route.injectable.ts new file mode 100644 index 0000000000..be54e18c91 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/daemonsets/daemonsets-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const daemonsetsRouteInjectable = getInjectable({ + id: "daemonsets-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "daemonsets"); + + return { + path: "/daemonsets", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default daemonsetsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/daemonsets/navigate-to-daemonsets.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/daemonsets/navigate-to-daemonsets.injectable.ts new file mode 100644 index 0000000000..1cc5ec8d8d --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/daemonsets/navigate-to-daemonsets.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import daemonsetsRouteInjectable from "./daemonsets-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToDaemonsetsInjectable = getInjectable({ + id: "navigate-to-daemonsets", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(daemonsetsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToDaemonsetsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/deployments/deployments-route.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/deployments/deployments-route.injectable.ts new file mode 100644 index 0000000000..65eed20f7a --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/deployments/deployments-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const deploymentsRouteInjectable = getInjectable({ + id: "deployments-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "deployments"); + + return { + path: "/deployments", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default deploymentsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable.ts new file mode 100644 index 0000000000..b6d2f07391 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import deploymentsRouteInjectable from "./deployments-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToDeploymentsInjectable = getInjectable({ + id: "navigate-to-deployments", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(deploymentsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToDeploymentsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/jobs/jobs-route.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/jobs/jobs-route.injectable.ts new file mode 100644 index 0000000000..5b2f087dc9 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/jobs/jobs-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const jobsRouteInjectable = getInjectable({ + id: "jobs-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "jobs"); + + return { + path: "/jobs", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default jobsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/jobs/navigate-to-jobs.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/jobs/navigate-to-jobs.injectable.ts new file mode 100644 index 0000000000..920c5c4de7 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/jobs/navigate-to-jobs.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import jobsRouteInjectable from "./jobs-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToJobsInjectable = getInjectable({ + id: "navigate-to-jobs", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(jobsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToJobsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/overview/navigate-to-workloads-overview.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/overview/navigate-to-workloads-overview.injectable.ts new file mode 100644 index 0000000000..224950206b --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/overview/navigate-to-workloads-overview.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import workloadsOverviewRouteInjectable from "./workloads-overview-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToWorkloadsOverviewInjectable = getInjectable({ + id: "navigate-to-workloads-overview", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(workloadsOverviewRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToWorkloadsOverviewInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/overview/workloads-overview-route.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/overview/workloads-overview-route.injectable.ts new file mode 100644 index 0000000000..00441c2ce9 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/overview/workloads-overview-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const workloadsOverviewRouteInjectable = getInjectable({ + id: "workloads-overview-route", + + instantiate: () => ({ + path: "/workloads", + clusterFrame: true, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default workloadsOverviewRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/pods/navigate-to-pods.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/pods/navigate-to-pods.injectable.ts new file mode 100644 index 0000000000..4eab211598 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/pods/navigate-to-pods.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import podsRouteInjectable from "./pods-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToPodsInjectable = getInjectable({ + id: "navigate-to-pods", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(podsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToPodsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/pods/pods-route.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/pods/pods-route.injectable.ts new file mode 100644 index 0000000000..b7c036cb65 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/pods/pods-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const podsRouteInjectable = getInjectable({ + id: "pods-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "pods"); + + return { + path: "/pods", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default podsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/replicasets/navigate-to-replicasets.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/replicasets/navigate-to-replicasets.injectable.ts new file mode 100644 index 0000000000..9549abab09 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/replicasets/navigate-to-replicasets.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import replicasetsRouteInjectable from "./replicasets-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToReplicasetsInjectable = getInjectable({ + id: "navigate-to-replicasets", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(replicasetsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToReplicasetsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/replicasets/replicasets-route.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/replicasets/replicasets-route.injectable.ts new file mode 100644 index 0000000000..c1822339f7 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/replicasets/replicasets-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const replicasetsRouteInjectable = getInjectable({ + id: "replicasets-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "replicasets"); + + return { + path: "/replicasets", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default replicasetsRouteInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/statefulsets/navigate-to-statefulsets.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/statefulsets/navigate-to-statefulsets.injectable.ts new file mode 100644 index 0000000000..403c7fa042 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/statefulsets/navigate-to-statefulsets.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import statefulsetsRouteInjectable from "./statefulsets-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token"; + +const navigateToStatefulsetsInjectable = getInjectable({ + id: "navigate-to-statefulsets", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(statefulsetsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToStatefulsetsInjectable; diff --git a/src/common/front-end-routing/routes/cluster/workloads/statefulsets/statefulsets-route.injectable.ts b/src/common/front-end-routing/routes/cluster/workloads/statefulsets/statefulsets-route.injectable.ts new file mode 100644 index 0000000000..ca271a22c5 --- /dev/null +++ b/src/common/front-end-routing/routes/cluster/workloads/statefulsets/statefulsets-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable"; +import { routeInjectionToken } from "../../../../route-injection-token"; + +const statefulsetsRouteInjectable = getInjectable({ + id: "statefulsets-route", + + instantiate: (di) => { + const isAllowedResource = di.inject(isAllowedResourceInjectable, "statefulsets"); + + return { + path: "/statefulsets", + clusterFrame: true, + isEnabled: isAllowedResource, + }; + }, + + injectionToken: routeInjectionToken, +}); + +export default statefulsetsRouteInjectable; diff --git a/src/common/front-end-routing/routes/entity-settings/entity-settings-route.injectable.ts b/src/common/front-end-routing/routes/entity-settings/entity-settings-route.injectable.ts new file mode 100644 index 0000000000..be893c201f --- /dev/null +++ b/src/common/front-end-routing/routes/entity-settings/entity-settings-route.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { Route, routeInjectionToken } from "../../route-injection-token"; + +export interface EntitySettingsPathParameters { + entityId: string; +} + +const entitySettingsRouteInjectable = getInjectable({ + id: "entity-settings-route", + + instantiate: (): Route => ({ + path: "/entity/:entityId/settings", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default entitySettingsRouteInjectable; diff --git a/src/common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable.ts b/src/common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable.ts new file mode 100644 index 0000000000..bc49bc4041 --- /dev/null +++ b/src/common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import entitySettingsRouteInjectable from "./entity-settings-route.injectable"; +import { navigateToRouteInjectionToken } from "../../navigate-to-route-injection-token"; + +export type NavigateToEntitySettings = (entityId: string, targetTabId?: string) => void; + +const navigateToEntitySettingsInjectable = getInjectable({ + id: "navigate-to-entity-settings", + + instantiate: (di): NavigateToEntitySettings => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(entitySettingsRouteInjectable); + + return (entityId, targetTabId) => + navigateToRoute(route, { parameters: { entityId }, fragment: targetTabId }); + }, +}); + +export default navigateToEntitySettingsInjectable; diff --git a/src/common/front-end-routing/routes/extensions/extensions-route.injectable.ts b/src/common/front-end-routing/routes/extensions/extensions-route.injectable.ts new file mode 100644 index 0000000000..9459260412 --- /dev/null +++ b/src/common/front-end-routing/routes/extensions/extensions-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../route-injection-token"; + +const extensionsRouteInjectable = getInjectable({ + id: "extensions-route", + + instantiate: () => ({ + path: "/extensions", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default extensionsRouteInjectable; diff --git a/src/common/front-end-routing/routes/extensions/navigate-to-extensions.injectable.ts b/src/common/front-end-routing/routes/extensions/navigate-to-extensions.injectable.ts new file mode 100644 index 0000000000..2e4e6d7d4c --- /dev/null +++ b/src/common/front-end-routing/routes/extensions/navigate-to-extensions.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import extensionsRouteInjectable from "./extensions-route.injectable"; +import { navigateToRouteInjectionToken } from "../../navigate-to-route-injection-token"; + +const navigateToExtensionsInjectable = getInjectable({ + id: "navigate-to-extensions", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(extensionsRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToExtensionsInjectable; diff --git a/src/common/front-end-routing/routes/preferences/app/app-preferences-route.injectable.ts b/src/common/front-end-routing/routes/preferences/app/app-preferences-route.injectable.ts new file mode 100644 index 0000000000..f19321f800 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/app/app-preferences-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../../route-injection-token"; + +const appPreferencesRouteInjectable = getInjectable({ + id: "app-preferences-route", + + instantiate: () => ({ + path: "/preferences/app", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default appPreferencesRouteInjectable; diff --git a/src/common/front-end-routing/routes/preferences/app/navigate-to-app-preferences.injectable.ts b/src/common/front-end-routing/routes/preferences/app/navigate-to-app-preferences.injectable.ts new file mode 100644 index 0000000000..4f83d47dec --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/app/navigate-to-app-preferences.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import appPreferencesRouteInjectable from "./app-preferences-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; + +const navigateToAppPreferencesInjectable = getInjectable({ + id: "navigate-to-app-preferences", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(appPreferencesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToAppPreferencesInjectable; diff --git a/src/common/front-end-routing/routes/preferences/editor/editor-preferences-route.injectable.ts b/src/common/front-end-routing/routes/preferences/editor/editor-preferences-route.injectable.ts new file mode 100644 index 0000000000..485a5a1e18 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/editor/editor-preferences-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../../route-injection-token"; + +const editorPreferencesRouteInjectable = getInjectable({ + id: "editor-preferences-route", + + instantiate: () => ({ + path: "/preferences/editor", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default editorPreferencesRouteInjectable; diff --git a/src/common/front-end-routing/routes/preferences/editor/navigate-to-editor-preferences.injectable.ts b/src/common/front-end-routing/routes/preferences/editor/navigate-to-editor-preferences.injectable.ts new file mode 100644 index 0000000000..ab4be540cf --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/editor/navigate-to-editor-preferences.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import editorPreferencesRouteInjectable from "./editor-preferences-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; + +const navigateToEditorPreferencesInjectable = getInjectable({ + id: "navigate-to-editor-preferences", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(editorPreferencesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToEditorPreferencesInjectable; diff --git a/src/common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable.ts b/src/common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable.ts new file mode 100644 index 0000000000..92a97675b0 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/extension/extension-preferences-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../../route-injection-token"; + +const extensionPreferencesRouteInjectable = getInjectable({ + id: "extension-preferences-route", + + instantiate: () => ({ + path: "/preferences/extensions", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default extensionPreferencesRouteInjectable; diff --git a/src/common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable.ts b/src/common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable.ts new file mode 100644 index 0000000000..6e2f80d864 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import extensionPreferencesRouteInjectable from "./extension-preferences-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; + +const navigateToExtensionPreferencesInjectable = getInjectable({ + id: "navigate-to-extension-preferences", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(extensionPreferencesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToExtensionPreferencesInjectable; diff --git a/src/common/front-end-routing/routes/preferences/kubernetes/kubernetes-preferences-route.injectable.ts b/src/common/front-end-routing/routes/preferences/kubernetes/kubernetes-preferences-route.injectable.ts new file mode 100644 index 0000000000..7a84a965b6 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/kubernetes/kubernetes-preferences-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../../route-injection-token"; + +const kubernetesPreferencesRouteInjectable = getInjectable({ + id: "kubernetes-preferences-route", + + instantiate: () => ({ + path: "/preferences/kubernetes", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default kubernetesPreferencesRouteInjectable; diff --git a/src/common/front-end-routing/routes/preferences/kubernetes/navigate-to-kubernetes-preferences.injectable.ts b/src/common/front-end-routing/routes/preferences/kubernetes/navigate-to-kubernetes-preferences.injectable.ts new file mode 100644 index 0000000000..35f4de6621 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/kubernetes/navigate-to-kubernetes-preferences.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import kubernetesPreferencesRouteInjectable from "./kubernetes-preferences-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; + +const navigateToKubernetesPreferencesInjectable = getInjectable({ + id: "navigate-to-kubernetes-preferences", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(kubernetesPreferencesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToKubernetesPreferencesInjectable; diff --git a/src/common/front-end-routing/routes/preferences/navigate-to-preferences.injectable.ts b/src/common/front-end-routing/routes/preferences/navigate-to-preferences.injectable.ts new file mode 100644 index 0000000000..545751d7c3 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/navigate-to-preferences.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import navigateToAppPreferencesInjectable from "./app/navigate-to-app-preferences.injectable"; + +const navigateToPreferencesInjectable = getInjectable({ + id: "navigate-to-preferences", + + instantiate: (di) => di.inject(navigateToAppPreferencesInjectable), +}); + +export default navigateToPreferencesInjectable; diff --git a/src/common/front-end-routing/routes/preferences/proxy/navigate-to-proxy-preferences.injectable.ts b/src/common/front-end-routing/routes/preferences/proxy/navigate-to-proxy-preferences.injectable.ts new file mode 100644 index 0000000000..258be70c60 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/proxy/navigate-to-proxy-preferences.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import proxyPreferencesRouteInjectable from "./proxy-preferences-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; + +const navigateToProxyPreferencesInjectable = getInjectable({ + id: "navigate-to-proxy-preferences", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(proxyPreferencesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToProxyPreferencesInjectable; diff --git a/src/common/front-end-routing/routes/preferences/proxy/proxy-preferences-route.injectable.ts b/src/common/front-end-routing/routes/preferences/proxy/proxy-preferences-route.injectable.ts new file mode 100644 index 0000000000..b07925bab5 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/proxy/proxy-preferences-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../../route-injection-token"; + +const proxyPreferencesRouteInjectable = getInjectable({ + id: "proxy-preferences-route", + + instantiate: () => ({ + path: "/preferences/proxy", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default proxyPreferencesRouteInjectable; diff --git a/src/common/front-end-routing/routes/preferences/telemetry/navigate-to-telemetry-preferences.injectable.ts b/src/common/front-end-routing/routes/preferences/telemetry/navigate-to-telemetry-preferences.injectable.ts new file mode 100644 index 0000000000..45f7559ba9 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/telemetry/navigate-to-telemetry-preferences.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import telemetryPreferencesRouteInjectable from "./telemetry-preferences-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; + +const navigateToTelemetryPreferencesInjectable = getInjectable({ + id: "navigate-to-telemetry-preferences", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(telemetryPreferencesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToTelemetryPreferencesInjectable; diff --git a/src/common/front-end-routing/routes/preferences/telemetry/telemetry-preferences-route.injectable.ts b/src/common/front-end-routing/routes/preferences/telemetry/telemetry-preferences-route.injectable.ts new file mode 100644 index 0000000000..f42cb83717 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/telemetry/telemetry-preferences-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../../route-injection-token"; + +const telemetryPreferencesRouteInjectable = getInjectable({ + id: "telemetry-preferences-route", + + instantiate: () => ({ + path: "/preferences/telemetry", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default telemetryPreferencesRouteInjectable; diff --git a/src/common/front-end-routing/routes/preferences/terminal/navigate-to-terminal-preferences.injectable.ts b/src/common/front-end-routing/routes/preferences/terminal/navigate-to-terminal-preferences.injectable.ts new file mode 100644 index 0000000000..8f73e556d3 --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/terminal/navigate-to-terminal-preferences.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import terminalPreferencesRouteInjectable from "./terminal-preferences-route.injectable"; +import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; + +const navigateToTerminalPreferencesInjectable = getInjectable({ + id: "navigate-to-terminal-preferences", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(terminalPreferencesRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToTerminalPreferencesInjectable; diff --git a/src/common/front-end-routing/routes/preferences/terminal/terminal-preferences-route.injectable.ts b/src/common/front-end-routing/routes/preferences/terminal/terminal-preferences-route.injectable.ts new file mode 100644 index 0000000000..8f530b2b9d --- /dev/null +++ b/src/common/front-end-routing/routes/preferences/terminal/terminal-preferences-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../../route-injection-token"; + +const terminalPreferencesRouteInjectable = getInjectable({ + id: "terminal-preferences-route", + + instantiate: () => ({ + path: "/preferences/terminal", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default terminalPreferencesRouteInjectable; diff --git a/src/common/front-end-routing/routes/welcome/navigate-to-welcome.injectable.ts b/src/common/front-end-routing/routes/welcome/navigate-to-welcome.injectable.ts new file mode 100644 index 0000000000..46f5b42056 --- /dev/null +++ b/src/common/front-end-routing/routes/welcome/navigate-to-welcome.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import welcomeRouteInjectable from "./welcome-route.injectable"; +import { navigateToRouteInjectionToken } from "../../navigate-to-route-injection-token"; + +const navigateToWelcomeInjectable = getInjectable({ + id: "navigate-to-welcome", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const route = di.inject(welcomeRouteInjectable); + + return () => navigateToRoute(route); + }, +}); + +export default navigateToWelcomeInjectable; diff --git a/src/common/front-end-routing/routes/welcome/welcome-route.injectable.ts b/src/common/front-end-routing/routes/welcome/welcome-route.injectable.ts new file mode 100644 index 0000000000..516ee6cca4 --- /dev/null +++ b/src/common/front-end-routing/routes/welcome/welcome-route.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeInjectionToken } from "../../route-injection-token"; + +const welcomeRouteInjectable = getInjectable({ + id: "welcome-route", + + instantiate: () => ({ + path: "/welcome", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +export default welcomeRouteInjectable; diff --git a/src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts b/src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts new file mode 100644 index 0000000000..7cf871f9b9 --- /dev/null +++ b/src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting"; +import { routeSpecificComponentInjectionToken } from "../../renderer/routes/route-specific-component-injection-token"; +import { routeInjectionToken } from "./route-injection-token"; +import { filter, map, matches } from "lodash/fp"; +import clusterStoreInjectable from "../cluster-store/cluster-store.injectable"; +import type { ClusterStore } from "../cluster-store/cluster-store"; +import { pipeline } from "@ogre-tools/fp"; + +describe("verify-that-all-routes-have-component", () => { + it("verify that routes have route component", async () => { + const rendererDi = getDiForUnitTesting({ doGeneralOverrides: true }); + + rendererDi.override( + clusterStoreInjectable, + () => ({ getById: (): null => null } as unknown as ClusterStore), + ); + + await rendererDi.runSetups(); + + const routes = rendererDi.injectMany(routeInjectionToken); + const routeComponents = rendererDi.injectMany( + routeSpecificComponentInjectionToken, + ); + + const routesMissingComponent = pipeline( + routes, + + map( + (route) => ({ + path: route.path, + routeComponent: routeComponents.find(matches({ route })), + }), + ), + + filter({ routeComponent: undefined }), + + map("path"), + ); + + expect(routesMissingComponent).toEqual([]); + }); +}); diff --git a/src/common/fs/path-exists.injectable.ts b/src/common/fs/path-exists.injectable.ts new file mode 100644 index 0000000000..aee6cac52b --- /dev/null +++ b/src/common/fs/path-exists.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import fsInjectable from "./fs.injectable"; + +export type PathExists = (path: string) => Promise; + +const pathExistsInjectable = getInjectable({ + id: "path-exists", + instantiate: (di): PathExists => di.inject(fsInjectable).pathExists, +}); + +export default pathExistsInjectable; diff --git a/src/common/fs/read-file-buffer.injectable.ts b/src/common/fs/read-file-buffer.injectable.ts new file mode 100644 index 0000000000..f7707dc594 --- /dev/null +++ b/src/common/fs/read-file-buffer.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import fsInjectable from "./fs.injectable"; + +const readFileBufferInjectable = getInjectable({ + id: "read-file-buffer", + + instantiate: (di) => (filePath: string) => + di.inject(fsInjectable).readFile(filePath), +}); + +export default readFileBufferInjectable; diff --git a/src/common/fs/read-file.injectable.ts b/src/common/fs/read-file.injectable.ts index 5c0bad27ca..b0a2b7233e 100644 --- a/src/common/fs/read-file.injectable.ts +++ b/src/common/fs/read-file.injectable.ts @@ -7,7 +7,9 @@ import fsInjectable from "./fs.injectable"; const readFileInjectable = getInjectable({ id: "read-file", - instantiate: (di) => di.inject(fsInjectable).readFile, + + instantiate: (di) => (filePath: string) => + di.inject(fsInjectable).readFile(filePath, "utf-8"), }); export default readFileInjectable; diff --git a/src/common/fs/read-json-file.injectable.ts b/src/common/fs/read-json-file.injectable.ts index 308d3b8a91..d270368cf9 100644 --- a/src/common/fs/read-json-file.injectable.ts +++ b/src/common/fs/read-json-file.injectable.ts @@ -3,11 +3,14 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import type { JsonValue } from "type-fest"; import fsInjectable from "./fs.injectable"; +export type ReadJson = (filePath: string) => Promise; + const readJsonFileInjectable = getInjectable({ id: "read-json-file", - instantiate: (di) => di.inject(fsInjectable).readJson, + instantiate: (di): ReadJson => di.inject(fsInjectable).readJson, }); export default readJsonFileInjectable; diff --git a/src/common/fs/write-file.injectable.ts b/src/common/fs/write-file.injectable.ts index d7e536fb52..70dcb76373 100644 --- a/src/common/fs/write-file.injectable.ts +++ b/src/common/fs/write-file.injectable.ts @@ -14,7 +14,7 @@ const writeFileInjectable = getInjectable({ return async (filePath: string, content: string | Buffer) => { await ensureDir(path.dirname(filePath), { mode: 0o755 }); - + await writeFile(filePath, content, { encoding: "utf-8", }); diff --git a/src/common/fs/write-json-file.injectable.ts b/src/common/fs/write-json-file.injectable.ts index 89aae40736..6d05e01a7c 100644 --- a/src/common/fs/write-json-file.injectable.ts +++ b/src/common/fs/write-json-file.injectable.ts @@ -8,12 +8,14 @@ import path from "path"; import type { JsonValue } from "type-fest"; import fsInjectable from "./fs.injectable"; +export type WriteJson = (filePath: string, contents: JsonValue) => Promise; + interface Dependencies { writeJson: (file: string, object: any, options?: WriteOptions | BufferEncoding | string) => Promise; ensureDir: (dir: string, options?: EnsureOptions | number) => Promise; } -const writeJsonFile = ({ writeJson, ensureDir }: Dependencies) => async (filePath: string, content: JsonValue) => { +const writeJsonFile = ({ writeJson, ensureDir }: Dependencies): WriteJson => async (filePath, content) => { await ensureDir(path.dirname(filePath), { mode: 0o755 }); await writeJson(filePath, content, { diff --git a/src/common/get-configuration-file-model/app-version/app-version.injectable.ts b/src/common/get-configuration-file-model/app-version/app-version.injectable.ts new file mode 100644 index 0000000000..0fe3142332 --- /dev/null +++ b/src/common/get-configuration-file-model/app-version/app-version.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import packageInfo from "../../../../package.json"; + +const appVersionInjectable = getInjectable({ + id: "app-version", + instantiate: () => packageInfo.version, + causesSideEffects: true, +}); + +export default appVersionInjectable; diff --git a/src/common/get-configuration-file-model/get-configuration-file-model.injectable.ts b/src/common/get-configuration-file-model/get-configuration-file-model.injectable.ts new file mode 100644 index 0000000000..41eeca1ce7 --- /dev/null +++ b/src/common/get-configuration-file-model/get-configuration-file-model.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import Config from "conf"; +import type { BaseStoreParams } from "../base-store"; +import appVersionInjectable from "./app-version/app-version.injectable"; +import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; + +const getConfigurationFileModelInjectable = getInjectable({ + id: "get-configuration-file-model", + instantiate: + (di) => + (content: BaseStoreParams) => + new Config({ + ...content, + projectName: "lens", + projectVersion: di.inject(appVersionInjectable), + cwd: di.inject(directoryForUserDataInjectable), + }), + + causesSideEffects: true, +}); + +export default getConfigurationFileModelInjectable; diff --git a/src/common/hotbar-store.injectable.ts b/src/common/hotbar-store.injectable.ts index 47510d9555..e8a883cf0a 100644 --- a/src/common/hotbar-store.injectable.ts +++ b/src/common/hotbar-store.injectable.ts @@ -3,11 +3,21 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import catalogCatalogEntityInjectable from "./catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; import { HotbarStore } from "./hotbar-store"; -const hotbarManagerInjectable = getInjectable({ - id: "hotbar-manager", - instantiate: () => HotbarStore.getInstance(), +const hotbarStoreInjectable = getInjectable({ + id: "hotbar-store", + + instantiate: (di) => { + HotbarStore.resetInstance(); + + return HotbarStore.createInstance({ + catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable), + }); + }, + + causesSideEffects: true, }); -export default hotbarManagerInjectable; +export default hotbarStoreInjectable; diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index 82c697d0d7..7de8ff7617 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -8,23 +8,33 @@ import { BaseStore } from "./base-store"; import migrations from "../migrations/hotbar-store"; import { toJS } from "./utils"; import { CatalogEntity } from "./catalog"; -import { catalogEntity } from "../main/catalog-sources/general"; import logger from "../main/logger"; import { broadcastMessage } from "./ipc"; -import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types"; +import { + defaultHotbarCells, + getEmptyHotbar, + Hotbar, + CreateHotbarData, + CreateHotbarOptions, +} from "./hotbar-types"; import { hotbarTooManyItemsChannel } from "./ipc/hotbar"; +import type { GeneralEntity } from "./catalog-entities"; export interface HotbarStoreModel { hotbars: Hotbar[]; activeHotbarId: string; } +interface Dependencies { + catalogCatalogEntity: GeneralEntity; +} + export class HotbarStore extends BaseStore { readonly displayName = "HotbarStore"; @observable hotbars: Hotbar[] = []; @observable private _activeHotbarId: string; - constructor() { + constructor(private dependencies: Dependencies) { super({ configName: "lens-hotbar-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names @@ -77,7 +87,9 @@ export class HotbarStore extends BaseStore { protected fromStore(data: Partial = {}) { if (!data.hotbars || !data.hotbars.length) { const hotbar = getEmptyHotbar("Default"); - const { metadata: { uid, name, source }} = catalogEntity; + const { + metadata: { uid, name, source }, + } = this.dependencies.catalogCatalogEntity; const initialItem = { entity: { uid, name, source }}; hotbar.items[0] = initialItem; @@ -119,21 +131,29 @@ export class HotbarStore extends BaseStore { return this.hotbars.find((hotbar) => hotbar.id === id); } - add = action((data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) => { - const hotbar = getEmptyHotbar(data.name, data.id); + add = action( + ( + data: CreateHotbarData, + { setActive = false }: CreateHotbarOptions = {}, + ) => { + const hotbar = getEmptyHotbar(data.name, data.id); - this.hotbars.push(hotbar); + this.hotbars.push(hotbar); - if (setActive) { - this._activeHotbarId = hotbar.id; - } - }); + if (setActive) { + this._activeHotbarId = hotbar.id; + } + }, + ); setHotbarName = action((id: string, name: string) => { const index = this.hotbars.findIndex((hotbar) => hotbar.id === id); if (index < 0) { - return void console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id }); + return void console.warn( + `[HOTBAR-STORE]: cannot setHotbarName: unknown id`, + { id }, + ); } this.hotbars[index].name = name; @@ -188,14 +208,17 @@ export class HotbarStore extends BaseStore { } else if (0 <= cellIndex && cellIndex < hotbar.items.length) { hotbar.items[cellIndex] = newItem; } else { - logger.error(`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`, { entityId: uid, hotbarId: hotbar.id, cellIndex }); + logger.error( + `[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`, + { entityId: uid, hotbarId: hotbar.id, cellIndex }, + ); } } @action removeFromHotbar(uid: string): void { const hotbar = this.getActive(); - const index = hotbar.items.findIndex(item => item?.entity.uid === uid); + const index = hotbar.items.findIndex((item) => item?.entity.uid === uid); if (index < 0) { return; @@ -223,7 +246,7 @@ export class HotbarStore extends BaseStore { findClosestEmptyIndex(from: number, direction = 1) { let index = from; - while(this.getActive().items[index] != null) { + while (this.getActive().items[index] != null) { index += direction; } @@ -236,7 +259,14 @@ export class HotbarStore extends BaseStore { const source = items[from]; const moveDown = from < to; - if (from < 0 || to < 0 || from >= items.length || to >= items.length || isNaN(from) || isNaN(to)) { + if ( + from < 0 || + to < 0 || + from >= items.length || + to >= items.length || + isNaN(from) || + isNaN(to) + ) { throw new Error("Invalid 'from' or 'to' arguments"); } @@ -256,25 +286,23 @@ export class HotbarStore extends BaseStore { } switchToPrevious() { - const hotbarStore = HotbarStore.getInstance(); - let index = hotbarStore.activeHotbarIndex - 1; + let index = this.activeHotbarIndex - 1; if (index < 0) { - index = hotbarStore.hotbars.length - 1; + index = this.hotbars.length - 1; } - hotbarStore.setActiveHotbar(index); + this.setActiveHotbar(index); } switchToNext() { - const hotbarStore = HotbarStore.getInstance(); - let index = hotbarStore.activeHotbarIndex + 1; + let index = this.activeHotbarIndex + 1; - if (index >= hotbarStore.hotbars.length) { + if (index >= this.hotbars.length) { index = 0; } - hotbarStore.setActiveHotbar(index); + this.setActiveHotbar(index); } /** @@ -285,7 +313,11 @@ export class HotbarStore extends BaseStore { return false; } - return this.getActive().items.findIndex(item => item?.entity.uid === entity.getId()) >= 0; + return ( + this.getActive().items.findIndex( + (item) => item?.entity.uid === entity.getId(), + ) >= 0 + ); } getDisplayLabel(hotbar: Hotbar): string { diff --git a/src/common/ipc-channel/create-channel/create-channel.ts b/src/common/ipc-channel/create-channel/create-channel.ts index c98709a062..2a18e3c145 100644 --- a/src/common/ipc-channel/create-channel/create-channel.ts +++ b/src/common/ipc-channel/create-channel/create-channel.ts @@ -4,7 +4,7 @@ */ import type { Channel } from "../channel"; -export const createChannel = (name: string): Channel => ({ +export const createChannel = (name: string): Channel => ({ name, _template: null, }); diff --git a/src/common/k8s-api/endpoints/crd.api.ts b/src/common/k8s-api/endpoints/crd.api.ts index 98f78a0716..f668c819f3 100644 --- a/src/common/k8s-api/endpoints/crd.api.ts +++ b/src/common/k8s-api/endpoints/crd.api.ts @@ -5,9 +5,11 @@ import { KubeCreationError, KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; -import { crdResourcesURL } from "../../routes"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import type { KubeJsonApiData } from "../kube-json-api"; +import { getLegacyGlobalDiForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import customResourcesRouteInjectable from "../../front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable"; +import { buildURL } from "../../utils/buildUrl"; interface AdditionalPrinterColumnsCommon { name: string; @@ -99,7 +101,11 @@ export class CustomResourceDefinition extends KubeObject { } getResourceUrl() { - return crdResourcesURL({ + const di = getLegacyGlobalDiForExtensionApi(); + + const customResourcesRoute = di.inject(customResourcesRouteInjectable); + + return buildURL(customResourcesRoute.path, { params: { group: this.getGroup(), name: this.getPluralName(), diff --git a/src/common/logger.injectable.ts b/src/common/logger.injectable.ts new file mode 100644 index 0000000000..561494a20c --- /dev/null +++ b/src/common/logger.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import logger, { Logger } from "./logger"; + +const loggerInjectable = getInjectable({ + id: "logger", + instantiate: (): Logger => logger, +}); + +export default loggerInjectable; diff --git a/src/common/logger.ts b/src/common/logger.ts index f29008818b..172f3e2fe8 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -10,6 +10,13 @@ import { consoleFormat } from "winston-console-format"; import { isDebugging, isTestEnv } from "./vars"; import BrowserConsole from "winston-transport-browserconsole"; +export interface Logger { + info: (message: string, ...args: any) => void; + error: (message: string, ...args: any) => void; + debug: (message: string, ...args: any) => void; + warn: (message: string, ...args: any) => void; +} + const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging diff --git a/src/common/path/get-absolute-path.injectable.ts b/src/common/path/get-absolute-path.injectable.ts new file mode 100644 index 0000000000..8919605942 --- /dev/null +++ b/src/common/path/get-absolute-path.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import path from "path"; + +export type GetAbsolutePath = (...args: string[]) => string; + +const getAbsolutePathInjectable = getInjectable({ + id: "get-absolute-path", + + instantiate: (): GetAbsolutePath => path.resolve, + + // This causes side effect e.g. Windows creates different kinds of + // absolute paths than linux + causesSideEffects: true, +}); + +export default getAbsolutePathInjectable; diff --git a/src/common/path/join-paths.injectable.ts b/src/common/path/join-paths.injectable.ts new file mode 100644 index 0000000000..dc63b48307 --- /dev/null +++ b/src/common/path/join-paths.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import path from "path"; + +export type JoinPaths = (...args: string[]) => string; + +const joinPathsInjectable = getInjectable({ + id: "join-paths", + instantiate: (): JoinPaths => path.join, + + // This causes side effect e.g. Windows uses different separator than e.g. linux + causesSideEffects: true, +}); + +export default joinPathsInjectable; diff --git a/src/common/routes/add-cluster.ts b/src/common/routes/add-cluster.ts deleted file mode 100644 index 6642bc3313..0000000000 --- a/src/common/routes/add-cluster.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 type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const addClusterRoute: RouteProps = { - path: "/add-cluster", -}; - -export const addClusterURL = buildURL(addClusterRoute.path); diff --git a/src/common/routes/catalog.ts b/src/common/routes/catalog.ts deleted file mode 100644 index 7a74b5bce2..0000000000 --- a/src/common/routes/catalog.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export interface CatalogViewRouteParam { - group?: string; - kind?: string; -} -export const catalogRoute: RouteProps = { - path: "/catalog/:group?/:kind?", -}; - -export const getPreviousTabUrl = (path: string) => { - const [group, kind] = path.split("/"); - - return catalogURL({ - params: { - group: group || browseCatalogTab, - kind, - }, - }); -}; - -export const catalogURL = buildURL(catalogRoute.path); - -export const browseCatalogTab = "browse"; diff --git a/src/common/routes/cluster-view.ts b/src/common/routes/cluster-view.ts deleted file mode 100644 index 8ae6e9445c..0000000000 --- a/src/common/routes/cluster-view.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export interface ClusterViewRouteParams { - clusterId: string; -} - -export const clusterViewRoute: RouteProps = { - exact: true, - path: "/cluster/:clusterId", -}; - -export const clusterViewURL = buildURL(clusterViewRoute.path); diff --git a/src/common/routes/cluster.ts b/src/common/routes/cluster.ts deleted file mode 100644 index 201517fced..0000000000 --- a/src/common/routes/cluster.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 type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const clusterRoute: RouteProps = { - path: "/overview", -}; - -export const clusterURL = buildURL(clusterRoute.path); diff --git a/src/common/routes/config-maps.ts b/src/common/routes/config-maps.ts deleted file mode 100644 index ba27fd96d8..0000000000 --- a/src/common/routes/config-maps.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const configMapsRoute: RouteProps = { - path: "/configmaps", -}; - -export interface ConfigMapsRouteParams { -} - -export const configMapsURL = buildURL(configMapsRoute.path); diff --git a/src/common/routes/config.ts b/src/common/routes/config.ts deleted file mode 100644 index fd8e055927..0000000000 --- a/src/common/routes/config.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import type { URLParams } from "../utils/buildUrl"; -import { configMapsRoute, configMapsURL } from "./config-maps"; -import { hpaRoute } from "./hpa"; -import { limitRangesRoute } from "./limit-ranges"; -import { pdbRoute } from "./pod-disruption-budgets"; -import { resourceQuotaRoute } from "./resource-quotas"; -import { secretsRoute } from "./secrets"; - -export const configRoute: RouteProps = { - path: [ - configMapsRoute, - secretsRoute, - resourceQuotaRoute, - limitRangesRoute, - hpaRoute, - pdbRoute, - ].map(route => route.path.toString()), -}; - -export const configURL = (params?: URLParams) => configMapsURL(params); diff --git a/src/common/routes/crd.ts b/src/common/routes/crd.ts deleted file mode 100644 index 18a3b7c81d..0000000000 --- a/src/common/routes/crd.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const crdRoute: RouteProps = { - path: "/crd", -}; - -export const crdDefinitionsRoute: RouteProps = { - path: `${crdRoute.path}/definitions`, -}; - -export const crdResourcesRoute: RouteProps = { - path: `${crdRoute.path}/:group/:name`, -}; - -export interface CRDListQuery { - groups?: string; -} - -export interface CRDRouteParams { - group: string; - name: string; -} - -export const crdURL = buildURL<{}, CRDListQuery>(crdDefinitionsRoute.path); -export const crdResourcesURL = buildURL(crdResourcesRoute.path); diff --git a/src/common/routes/endpoints.ts b/src/common/routes/endpoints.ts deleted file mode 100644 index 657f6d8b45..0000000000 --- a/src/common/routes/endpoints.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const endpointRoute: RouteProps = { - path: "/endpoints", -}; - -export interface EndpointRouteParams { -} - -export const endpointURL = buildURL(endpointRoute.path); diff --git a/src/common/routes/entity-settings.ts b/src/common/routes/entity-settings.ts deleted file mode 100644 index 202ae8e9a6..0000000000 --- a/src/common/routes/entity-settings.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export interface EntitySettingsRouteParams { - entityId: string; -} - -export const entitySettingsRoute: RouteProps = { - path: `/entity/:entityId/settings`, -}; - -export const entitySettingsURL = buildURL(entitySettingsRoute.path); diff --git a/src/common/routes/events.ts b/src/common/routes/events.ts deleted file mode 100644 index 70b7a3c387..0000000000 --- a/src/common/routes/events.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 type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const eventRoute: RouteProps = { - path: "/events", -}; - -export const eventsURL = buildURL(eventRoute.path); diff --git a/src/common/routes/extensions.ts b/src/common/routes/extensions.ts deleted file mode 100644 index e9dba5421c..0000000000 --- a/src/common/routes/extensions.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 type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const extensionsRoute: RouteProps = { - path: "/extensions", -}; - -export const extensionsURL = buildURL(extensionsRoute.path); diff --git a/src/common/routes/helm-charts.ts b/src/common/routes/helm-charts.ts deleted file mode 100644 index 3777ac0ebd..0000000000 --- a/src/common/routes/helm-charts.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; -import { helmRoute } from "./helm"; - -export const helmChartsRoute: RouteProps = { - path: `${helmRoute.path}/charts/:repo?/:chartName?`, -}; - -export interface HelmChartsRouteParams { - chartName?: string; - repo?: string; -} - -export const helmChartsURL = buildURL(helmChartsRoute.path); diff --git a/src/common/routes/helm.ts b/src/common/routes/helm.ts deleted file mode 100644 index bb5d5b6914..0000000000 --- a/src/common/routes/helm.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 type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const helmRoute: RouteProps = { - path: "/helm", -}; - -export const helmURL = buildURL(helmRoute.path); diff --git a/src/common/routes/hpa.ts b/src/common/routes/hpa.ts deleted file mode 100644 index 2904dc933d..0000000000 --- a/src/common/routes/hpa.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const hpaRoute: RouteProps = { - path: "/hpa", -}; - -export interface HpaRouteParams { -} - -export const hpaURL = buildURL(hpaRoute.path); diff --git a/src/common/routes/index.ts b/src/common/routes/index.ts deleted file mode 100644 index 916806fa16..0000000000 --- a/src/common/routes/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -export * from "./add-cluster"; -export * from "./catalog"; -export * from "./cluster-view"; -export * from "./cluster"; -export * from "./config-maps"; -export * from "./config"; -export * from "./crd"; -export * from "./endpoints"; -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"; -export * from "./namespaces"; -export * from "./network-policies"; -export * from "./network"; -export * from "./nodes"; -export * from "./pod-disruption-budgets"; -export * from "./port-forwards"; -export * from "./preferences"; -export * from "./releases"; -export * from "./resource-quotas"; -export * from "./secrets"; -export * from "./services"; -export * from "./storage-classes"; -export * from "./storage"; -export * from "./user-management"; -export * from "./volume-claims"; -export * from "./volumes"; -export * from "./welcome"; -export * from "./workloads"; diff --git a/src/common/routes/ingresses.ts b/src/common/routes/ingresses.ts deleted file mode 100644 index fb6096aeba..0000000000 --- a/src/common/routes/ingresses.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const ingressRoute: RouteProps = { - path: "/ingresses", -}; - -export interface IngressRouteParams { -} - -export const ingressURL = buildURL(ingressRoute.path); diff --git a/src/common/routes/limit-ranges.ts b/src/common/routes/limit-ranges.ts deleted file mode 100644 index fbee123f09..0000000000 --- a/src/common/routes/limit-ranges.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const limitRangesRoute: RouteProps = { - path: "/limitranges", -}; - -export interface LimitRangeRouteParams { -} - -export const limitRangeURL = buildURL(limitRangesRoute.path); diff --git a/src/common/routes/namespaces.ts b/src/common/routes/namespaces.ts deleted file mode 100644 index 408abefeb4..0000000000 --- a/src/common/routes/namespaces.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const namespacesRoute: RouteProps = { - path: "/namespaces", -}; - -export interface NamespacesRouteParams { -} - -export const namespacesURL = buildURL(namespacesRoute.path); diff --git a/src/common/routes/network-policies.ts b/src/common/routes/network-policies.ts deleted file mode 100644 index 83a1fe0ed8..0000000000 --- a/src/common/routes/network-policies.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const networkPoliciesRoute: RouteProps = { - path: "/network-policies", -}; - -export interface NetworkPoliciesRouteParams { -} - -export const networkPoliciesURL = buildURL(networkPoliciesRoute.path); diff --git a/src/common/routes/network.ts b/src/common/routes/network.ts deleted file mode 100644 index 7a76e4f922..0000000000 --- a/src/common/routes/network.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import type { URLParams } from "../utils/buildUrl"; -import { endpointRoute } from "./endpoints"; -import { ingressRoute } from "./ingresses"; -import { networkPoliciesRoute } from "./network-policies"; -import { servicesRoute, servicesURL } from "./services"; -import { portForwardsRoute } from "./port-forwards"; - -export const networkRoute: RouteProps = { - path: [ - servicesRoute, - endpointRoute, - ingressRoute, - networkPoliciesRoute, - portForwardsRoute, - ].map(route => route.path.toString()), -}; - -export const networkURL = (params?: URLParams) => servicesURL(params); diff --git a/src/common/routes/nodes.ts b/src/common/routes/nodes.ts deleted file mode 100644 index 3a5fcf0f2f..0000000000 --- a/src/common/routes/nodes.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const nodesRoute: RouteProps = { - path: "/nodes", -}; - -export interface NodesRouteParams { -} - -export const nodesURL = buildURL(nodesRoute.path); diff --git a/src/common/routes/pod-disruption-budgets.ts b/src/common/routes/pod-disruption-budgets.ts deleted file mode 100644 index d97aa02236..0000000000 --- a/src/common/routes/pod-disruption-budgets.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const pdbRoute: RouteProps = { - path: "/poddisruptionbudgets", -}; - -export interface PodDisruptionBudgetsRouteParams { -} - -export const pdbURL = buildURL(pdbRoute.path); diff --git a/src/common/routes/port-forwards.ts b/src/common/routes/port-forwards.ts deleted file mode 100644 index 9fe74e03d5..0000000000 --- a/src/common/routes/port-forwards.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const portForwardsRoute: RouteProps = { - path: "/port-forwards/:forwardport?", -}; - -export interface PortForwardsRouteParams { - forwardport?: string; -} - -export const portForwardsURL = buildURL(portForwardsRoute.path); diff --git a/src/common/routes/preferences.ts b/src/common/routes/preferences.ts deleted file mode 100644 index 3afd81e614..0000000000 --- a/src/common/routes/preferences.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const preferencesRoute: RouteProps = { - path: "/preferences", -}; - -export const appRoute: RouteProps = { - path: `${preferencesRoute.path}/app`, -}; - -export const proxyRoute: RouteProps = { - path: `${preferencesRoute.path}/proxy`, -}; - -export const kubernetesRoute: RouteProps = { - path: `${preferencesRoute.path}/kubernetes`, -}; - -export const editorRoute: RouteProps = { - path: `${preferencesRoute.path}/editor`, -}; - -export const telemetryRoute: RouteProps = { - path: `${preferencesRoute.path}/telemetry`, -}; - -export const extensionRoute: RouteProps = { - path: `${preferencesRoute.path}/extensions`, -}; - -export const terminalRoute: RouteProps = { - path: `${preferencesRoute.path}/terminal`, -}; -export const preferencesURL = buildURL(preferencesRoute.path); -export const appURL = buildURL(appRoute.path); -export const proxyURL = buildURL(proxyRoute.path); -export const kubernetesURL = buildURL(kubernetesRoute.path); -export const editorURL = buildURL(editorRoute.path); -export const telemetryURL = buildURL(telemetryRoute.path); -export const extensionURL = buildURL(extensionRoute.path); -export const terminalURL = buildURL(terminalRoute.path); diff --git a/src/common/routes/releases.ts b/src/common/routes/releases.ts deleted file mode 100644 index 191d186570..0000000000 --- a/src/common/routes/releases.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; -import { helmRoute } from "./helm"; - -export const releaseRoute: RouteProps = { - path: `${helmRoute.path}/releases/:namespace?/:name?`, -}; - -export interface ReleaseRouteParams { - name?: string; - namespace?: string; -} - -export const releaseURL = buildURL(releaseRoute.path); diff --git a/src/common/routes/resource-quotas.ts b/src/common/routes/resource-quotas.ts deleted file mode 100644 index afe6f072e1..0000000000 --- a/src/common/routes/resource-quotas.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const resourceQuotaRoute: RouteProps = { - path: "/resourcequotas", -}; - -export interface ResourceQuotaRouteParams { -} - -export const resourceQuotaURL = buildURL(resourceQuotaRoute.path); diff --git a/src/common/routes/secrets.ts b/src/common/routes/secrets.ts deleted file mode 100644 index 4a467630bd..0000000000 --- a/src/common/routes/secrets.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const secretsRoute: RouteProps = { - path: "/secrets", -}; - -export interface SecretsRouteParams { -} - -export const secretsURL = buildURL(secretsRoute.path); diff --git a/src/common/routes/services.ts b/src/common/routes/services.ts deleted file mode 100644 index 595ea1b4c7..0000000000 --- a/src/common/routes/services.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const servicesRoute: RouteProps = { - path: "/services", -}; - -export interface ServicesRouteParams { -} - -export const servicesURL = buildURL(servicesRoute.path); diff --git a/src/common/routes/storage-classes.ts b/src/common/routes/storage-classes.ts deleted file mode 100644 index 235a1c6df4..0000000000 --- a/src/common/routes/storage-classes.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const storageClassesRoute: RouteProps = { - path: "/storage-classes", -}; - -export interface StorageClassesRouteParams { -} - -export const storageClassesURL = buildURL(storageClassesRoute.path); diff --git a/src/common/routes/storage.ts b/src/common/routes/storage.ts deleted file mode 100644 index baa5750113..0000000000 --- a/src/common/routes/storage.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import type { URLParams } from "../utils/buildUrl"; -import { storageClassesRoute } from "./storage-classes"; -import { volumeClaimsRoute, volumeClaimsURL } from "./volume-claims"; -import { volumesRoute } from "./volumes"; - -export const storageRoute: RouteProps = { - path: [ - volumeClaimsRoute, - volumesRoute, - storageClassesRoute, - ].map(route => route.path.toString()), -}; - -export const storageURL = (params?: URLParams) => volumeClaimsURL(params); diff --git a/src/common/routes/user-management.ts b/src/common/routes/user-management.ts deleted file mode 100644 index 47d781568d..0000000000 --- a/src/common/routes/user-management.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL, URLParams } from "../utils/buildUrl"; - -// Routes -export const serviceAccountsRoute: RouteProps = { - path: "/service-accounts", -}; -export const podSecurityPoliciesRoute: RouteProps = { - path: "/pod-security-policies", -}; -export const rolesRoute: RouteProps = { - path: "/roles", -}; -export const clusterRolesRoute: RouteProps = { - path: "/cluster-roles", -}; -export const roleBindingsRoute: RouteProps = { - path: "/role-bindings", -}; -export const clusterRoleBindingsRoute: RouteProps = { - path: "/cluster-role-bindings", -}; - -export const usersManagementRoute: RouteProps = { - path: [ - serviceAccountsRoute, - podSecurityPoliciesRoute, - roleBindingsRoute, - clusterRoleBindingsRoute, - rolesRoute, - clusterRolesRoute, - ].map(route => route.path.toString()), -}; - -// Route params -export interface ServiceAccountsRouteParams { -} - -export interface RoleBindingsRouteParams { -} - -export interface ClusterRoleBindingsRouteParams { -} - -export interface RolesRouteParams { -} - -export interface ClusterRolesRouteParams { -} - -// URL-builders -export const usersManagementURL = (params?: URLParams) => serviceAccountsURL(params); -export const serviceAccountsURL = buildURL(serviceAccountsRoute.path); -export const podSecurityPoliciesURL = buildURL(podSecurityPoliciesRoute.path); -export const rolesURL = buildURL(rolesRoute.path); -export const roleBindingsURL = buildURL(roleBindingsRoute.path); -export const clusterRolesURL = buildURL(clusterRolesRoute.path); -export const clusterRoleBindingsURL = buildURL(clusterRoleBindingsRoute.path); diff --git a/src/common/routes/volume-claims.ts b/src/common/routes/volume-claims.ts deleted file mode 100644 index d775395ef9..0000000000 --- a/src/common/routes/volume-claims.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const volumeClaimsRoute: RouteProps = { - path: "/persistent-volume-claims", -}; - -export interface VolumeClaimsRouteParams { -} - -export const volumeClaimsURL = buildURL(volumeClaimsRoute.path); diff --git a/src/common/routes/volumes.ts b/src/common/routes/volumes.ts deleted file mode 100644 index 89508904b0..0000000000 --- a/src/common/routes/volumes.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const volumesRoute: RouteProps = { - path: "/persistent-volumes", -}; - -export interface VolumesRouteParams { -} - -export const volumesURL = buildURL(volumesRoute.path); diff --git a/src/common/routes/welcome.ts b/src/common/routes/welcome.ts deleted file mode 100644 index 95465738a2..0000000000 --- a/src/common/routes/welcome.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 type { RouteProps } from "react-router"; -import { buildURL } from "../utils/buildUrl"; - -export const welcomeRoute: RouteProps = { - path: "/welcome", -}; - -export const welcomeURL = buildURL(welcomeRoute.path); diff --git a/src/common/routes/workloads.ts b/src/common/routes/workloads.ts deleted file mode 100644 index 6b053fc1bf..0000000000 --- a/src/common/routes/workloads.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RouteProps } from "react-router"; -import { buildURL, URLParams } from "../utils/buildUrl"; -import type { KubeResource } from "../rbac"; - -// Routes -export const overviewRoute: RouteProps = { - path: "/workloads", -}; -export const podsRoute: RouteProps = { - path: "/pods", -}; -export const deploymentsRoute: RouteProps = { - path: "/deployments", -}; -export const daemonSetsRoute: RouteProps = { - path: "/daemonsets", -}; -export const statefulSetsRoute: RouteProps = { - path: "/statefulsets", -}; -export const replicaSetsRoute: RouteProps = { - path: "/replicasets", -}; -export const jobsRoute: RouteProps = { - path: "/jobs", -}; -export const cronJobsRoute: RouteProps = { - path: "/cronjobs", -}; - -export const workloadsRoute: RouteProps = { - path: [ - overviewRoute, - podsRoute, - deploymentsRoute, - daemonSetsRoute, - statefulSetsRoute, - replicaSetsRoute, - jobsRoute, - cronJobsRoute, - ].map(route => route.path.toString()), -}; - -// Route params -export interface WorkloadsOverviewRouteParams { -} - -export interface PodsRouteParams { -} - -export interface DeploymentsRouteParams { -} - -export interface DaemonSetsRouteParams { -} - -export interface StatefulSetsRouteParams { -} - -export interface ReplicaSetsRouteParams { -} - -export interface JobsRouteParams { -} - -export interface CronJobsRouteParams { -} - -// URL-builders -export const workloadsURL = (params?: URLParams) => overviewURL(params); -export const overviewURL = buildURL(overviewRoute.path); -export const podsURL = buildURL(podsRoute.path); -export const deploymentsURL = buildURL(deploymentsRoute.path); -export const daemonSetsURL = buildURL(daemonSetsRoute.path); -export const statefulSetsURL = buildURL(statefulSetsRoute.path); -export const replicaSetsURL = buildURL(replicaSetsRoute.path); -export const jobsURL = buildURL(jobsRoute.path); -export const cronJobsURL = buildURL(cronJobsRoute.path); - -export const workloadURL: Partial>> = { - "pods": podsURL, - "deployments": deploymentsURL, - "daemonsets": daemonSetsURL, - "statefulsets": statefulSetsURL, - "replicasets": replicaSetsURL, - "jobs": jobsURL, - "cronjobs": cronJobsURL, -}; diff --git a/src/common/test-utils/get-absolute-path-fake.ts b/src/common/test-utils/get-absolute-path-fake.ts new file mode 100644 index 0000000000..89b5faa446 --- /dev/null +++ b/src/common/test-utils/get-absolute-path-fake.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { GetAbsolutePath } from "../path/get-absolute-path.injectable"; + +export const getAbsolutePathFake: GetAbsolutePath = (...args) => { + const maybeAbsolutePath = args.join("/"); + + if (isAbsolutePath(maybeAbsolutePath)) { + return maybeAbsolutePath; + } + + return `/some-absolute-root-directory/${maybeAbsolutePath}`; +}; + +const isAbsolutePath = (path: string) => path.startsWith("/"); diff --git a/src/renderer/components/+user-management/user-management.scss b/src/common/test-utils/join-paths-fake.ts similarity index 52% rename from src/renderer/components/+user-management/user-management.scss rename to src/common/test-utils/join-paths-fake.ts index d6788626da..2796423d3c 100644 --- a/src/renderer/components/+user-management/user-management.scss +++ b/src/common/test-utils/join-paths-fake.ts @@ -2,6 +2,6 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ +import type { JoinPaths } from "../path/join-paths.injectable"; -.UserManagement { -} +export const joinPathsFake: JoinPaths = (...args) => args.join("/"); diff --git a/src/common/user-store/user-store.injectable.ts b/src/common/user-store/user-store.injectable.ts index 36db7c0820..9a34144828 100644 --- a/src/common/user-store/user-store.injectable.ts +++ b/src/common/user-store/user-store.injectable.ts @@ -7,7 +7,14 @@ import { UserStore } from "./user-store"; const userStoreInjectable = getInjectable({ id: "user-store", - instantiate: () => UserStore.createInstance(), + + instantiate: () => { + UserStore.resetInstance(); + + return UserStore.createInstance(); + }, + + causesSideEffects: true, }); export default userStoreInjectable; diff --git a/src/common/utils/buildUrl.ts b/src/common/utils/buildUrl.ts index 8f7e994427..787086f027 100644 --- a/src/common/utils/buildUrl.ts +++ b/src/common/utils/buildUrl.ts @@ -11,25 +11,21 @@ export interface URLParams

{ fragment?: string; } -export function buildURL

(path: string | any) { +export function buildURL

(path: string, { params, query, fragment }: URLParams = {}) { const pathBuilder = compile(String(path)); - return function ({ params, query, fragment }: URLParams = {}): string { - const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : ""; - const parts = [ - pathBuilder(params), - queryParams && `?${queryParams}`, - fragment && `#${fragment}`, - ]; + const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : ""; + const parts = [ + pathBuilder(params), + queryParams && `?${queryParams}`, + fragment && `#${fragment}`, + ]; - return parts.filter(Boolean).join(""); - }; + return parts.filter(Boolean).join(""); } -export function buildURLPositional

(path: string | any) { - const builder = buildURL(path); - +export function buildURLPositional

(path: string) { return function (params?: P, query?: Q, fragment?: string): string { - return builder({ params, query, fragment }); + return buildURL(path, { params, query, fragment }); }; } diff --git a/src/common/utils/is-allowed-resource.injectable.ts b/src/common/utils/is-allowed-resource.injectable.ts index 9977f6e5f8..f66029c018 100644 --- a/src/common/utils/is-allowed-resource.injectable.ts +++ b/src/common/utils/is-allowed-resource.injectable.ts @@ -2,21 +2,25 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getInjectable } from "@ogre-tools/injectable"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; 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({ id: "is-allowed-resource", - instantiate: (di) => { + instantiate: (di, resourceName: KubeResource) => { const allowedResources = di.inject(allowedResourcesInjectable); - return (resource: KubeResource) => allowedResources.get().has(resource); + return computed(() => allowedResources.get().has(resourceName)); }, + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, resource: KubeResource) => resource, + }), }); export default isAllowedResourceInjectable; diff --git a/src/common/vars/is-linux.injectable.ts b/src/common/vars/is-linux.injectable.ts index b7a24e9a2b..dbd3436129 100644 --- a/src/common/vars/is-linux.injectable.ts +++ b/src/common/vars/is-linux.injectable.ts @@ -8,6 +8,7 @@ import { isLinux } from "../vars"; const isLinuxInjectable = getInjectable({ id: "is-linux", instantiate: () => isLinux, + causesSideEffects: true, }); export default isLinuxInjectable; diff --git a/src/common/vars/is-mac.injectable.ts b/src/common/vars/is-mac.injectable.ts new file mode 100644 index 0000000000..6a956b2426 --- /dev/null +++ b/src/common/vars/is-mac.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { isMac } from "../vars"; + +const isMacInjectable = getInjectable({ + id: "is-mac", + instantiate: () => isMac, + causesSideEffects: true, +}); + +export default isMacInjectable; diff --git a/src/common/vars/is-windows.injectable.ts b/src/common/vars/is-windows.injectable.ts index bdce3e827f..4b92b78a3b 100644 --- a/src/common/vars/is-windows.injectable.ts +++ b/src/common/vars/is-windows.injectable.ts @@ -8,6 +8,7 @@ import { isWindows } from "../vars"; const isWindowsInjectable = getInjectable({ id: "is-windows", instantiate: () => isWindows, + causesSideEffects: true, }); export default isWindowsInjectable; diff --git a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.test.ts b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.test.ts index 3e3cbb6ff1..c7ebd07872 100644 --- a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.test.ts +++ b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.test.ts @@ -8,7 +8,7 @@ import { getInjectable, Injectable, } from "@ogre-tools/injectable"; -import { setLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api"; +import { Environments, setLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api"; import { asLegacyGlobalObjectForExtensionApiWithModifications } from "./as-legacy-global-object-for-extension-api-with-modifications"; describe("asLegacyGlobalObjectForExtensionApiWithModifications", () => { @@ -24,7 +24,7 @@ describe("asLegacyGlobalObjectForExtensionApiWithModifications", () => { jest.spyOn(di, "inject"); - setLegacyGlobalDiForExtensionApi(di); + setLegacyGlobalDiForExtensionApi(di, Environments.renderer); someInjectable = getInjectable({ id: "some-injectable", diff --git a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api.ts b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api.ts new file mode 100644 index 0000000000..b9ed0f826e --- /dev/null +++ b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { Injectable } from "@ogre-tools/injectable"; +import { asLegacyGlobalForExtensionApi } from "./as-legacy-global-object-for-extension-api"; +import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api"; +import loggerInjectable from "../../common/logger.injectable"; + +export const asLegacyGlobalSingletonForExtensionApi = < + Instance, + InstantiationParameter = void, +>( + injectable: Injectable, + instantiationParameter?: InstantiationParameter, + ) => { + const instance = asLegacyGlobalForExtensionApi( + injectable, + instantiationParameter, + ); + + return { + createInstance: () => instance, + + getInstance: () => instance, + + resetInstance: () => { + const di = getLegacyGlobalDiForExtensionApi(); + const logger = di.inject(loggerInjectable); + + logger.warn( + `resetInstance() for a legacy global singleton of "${injectable.id}" does nothing.`, + ); + }, + }; +}; diff --git a/src/extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api.ts b/src/extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api.ts index ccb6403ce6..4e4dfba0a8 100644 --- a/src/extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api.ts +++ b/src/extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api.ts @@ -4,10 +4,29 @@ */ import type { DiContainer } from "@ogre-tools/injectable"; -let legacyGlobalDi: DiContainer; +const legacyGlobalDis = new Map(); -export const setLegacyGlobalDiForExtensionApi = (di: DiContainer) => { - legacyGlobalDi = di; +export enum Environments { + renderer, + main, +} + +export const setLegacyGlobalDiForExtensionApi = ( + di: DiContainer, + environment: Environments, +) => { + legacyGlobalDis.set(environment, di); }; -export const getLegacyGlobalDiForExtensionApi = () => legacyGlobalDi; +export const getLegacyGlobalDiForExtensionApi = () => { + const globalDis = [...legacyGlobalDis.values()]; + + if (globalDis.length > 1) { + throw new Error("Tried to get DI container using legacy globals where there is multiple containers available."); + } + + return globalDis[0]; +}; + +export const getEnvironmentSpecificLegacyGlobalDiForExtensionApi = (environment: Environments) => + legacyGlobalDis.get(environment); diff --git a/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/directory-for-extension-data/directory-for-extension-data.injectable.ts b/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/directory-for-extension-data/directory-for-extension-data.injectable.ts index 7856fd84de..854953e336 100644 --- a/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/directory-for-extension-data/directory-for-extension-data.injectable.ts +++ b/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/directory-for-extension-data/directory-for-extension-data.injectable.ts @@ -3,14 +3,18 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import path from "path"; import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import getAbsolutePathInjectable from "../../../../../common/path/get-absolute-path.injectable"; const directoryForExtensionDataInjectable = getInjectable({ id: "directory-for-extension-data", - instantiate: (di) => - path.join(di.inject(directoryForUserDataInjectable), "extension_data"), + instantiate: (di) => { + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const directoryForUserData = di.inject(directoryForUserDataInjectable); + + return getAbsolutePath(directoryForUserData, "extension_data"); + }, }); export default directoryForExtensionDataInjectable; diff --git a/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable.ts b/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable.ts index 4851771285..5c80330c0c 100644 --- a/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable.ts +++ b/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable.ts @@ -15,6 +15,8 @@ const fileSystemProvisionerStoreInjectable = getInjectable({ directoryForExtensionDataInjectable, ), }), + + causesSideEffects: true, }); export default fileSystemProvisionerStoreInjectable; diff --git a/src/extensions/extension-loader/extension-installation-counter.injectable.ts b/src/extensions/extension-loader/extension-installation-counter.injectable.ts new file mode 100644 index 0000000000..2510a679dc --- /dev/null +++ b/src/extensions/extension-loader/extension-installation-counter.injectable.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; + +const extensionInstallationCounterInjectable = getInjectable({ + id: "extension-installation-counter", + instantiate: () => new Map(), +}); + +export default extensionInstallationCounterInjectable; diff --git a/src/extensions/extension-loader/extension-loader.injectable.ts b/src/extensions/extension-loader/extension-loader.injectable.ts index e6dd489ecc..87f53cd686 100644 --- a/src/extensions/extension-loader/extension-loader.injectable.ts +++ b/src/extensions/extension-loader/extension-loader.injectable.ts @@ -5,8 +5,9 @@ import { getInjectable } from "@ogre-tools/injectable"; import { ExtensionLoader } from "./extension-loader"; import updateExtensionsStateInjectable from "./update-extensions-state/update-extensions-state.injectable"; -import createExtensionInstanceInjectable - from "./create-extension-instance/create-extension-instance.injectable"; +import createExtensionInstanceInjectable from "./create-extension-instance/create-extension-instance.injectable"; +import { extensionRegistratorInjectionToken } from "./extension-registrator-injection-token"; +import extensionInstallationCounterInjectable from "./extension-installation-counter.injectable"; const extensionLoaderInjectable = getInjectable({ id: "extension-loader", @@ -15,6 +16,8 @@ const extensionLoaderInjectable = getInjectable({ new ExtensionLoader({ updateExtensionsState: di.inject(updateExtensionsStateInjectable), createExtensionInstance: di.inject(createExtensionInstanceInjectable), + extensionRegistrators: di.injectMany(extensionRegistratorInjectionToken), + extensionInstallationCounter: di.inject(extensionInstallationCounterInjectable), }), }); diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index 5db134dd4c..11bfa1520e 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -25,6 +25,8 @@ const logModule = "[EXTENSIONS-LOADER]"; interface Dependencies { updateExtensionsState: (extensionsState: Record) => void; createExtensionInstance: (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => LensExtension; + extensionRegistrators: ((extension: LensExtension, extensionInstallationCount: number) => void)[]; + extensionInstallationCounter: Map; } export interface ExtensionLoading { @@ -247,7 +249,6 @@ export class ExtensionLoader { return this.autoInitExtensions(async (extension: LensRendererExtension) => { const removeItems = [ - registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension), registries.EntitySettingRegistry.getInstance().add(extension.entitySettings), registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), ]; @@ -274,8 +275,6 @@ export class ExtensionLoader { } const removeItems = [ - registries.ClusterPageRegistry.getInstance().add(extension.clusterPages, extension), - registries.ClusterPageMenuRegistry.getInstance().add(extension.clusterPageMenus, extension), registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems), ]; @@ -317,6 +316,14 @@ export class ExtensionLoader { extension, ); + const installationCount = (this.dependencies.extensionInstallationCounter.get(instance.sanitizedExtensionId) | 0) + 1; + + this.dependencies.extensionInstallationCounter.set(instance.sanitizedExtensionId, installationCount); + + this.dependencies.extensionRegistrators.forEach((register) => + register(instance, installationCount), + ); + this.instances.set(extId, instance); return { @@ -332,9 +339,7 @@ export class ExtensionLoader { } return null; - }) - // Remove null values - .filter(extension => Boolean(extension)); + }).filter(extension => Boolean(extension)); // We first need to wait until each extension's `onActivate` is resolved or rejected, // as this might register new catalog categories. Afterwards we can safely .enable the extension. diff --git a/src/extensions/extension-loader/extension-registrator-injection-token.ts b/src/extensions/extension-loader/extension-registrator-injection-token.ts new file mode 100644 index 0000000000..9fe35f5f39 --- /dev/null +++ b/src/extensions/extension-loader/extension-registrator-injection-token.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { LensExtension } from "../lens-extension"; + +export const extensionRegistratorInjectionToken = getInjectionToken< + (extension: LensExtension, extensionInstallationCount: number) => void + >({ + id: "extension-registrator-token", + }); diff --git a/src/extensions/extension-loader/update-extensions-state/update-extensions-state.injectable.ts b/src/extensions/extension-loader/update-extensions-state/update-extensions-state.injectable.ts index e808246033..754375dd5a 100644 --- a/src/extensions/extension-loader/update-extensions-state/update-extensions-state.injectable.ts +++ b/src/extensions/extension-loader/update-extensions-state/update-extensions-state.injectable.ts @@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import extensionsStoreInjectable from "../../extensions-store/extensions-store.injectable"; const updateExtensionsStateInjectable = getInjectable({ - id: "upadte-extensions-state", + id: "update-extensions-state", instantiate: (di) => di.inject(extensionsStoreInjectable).mergeState, }); diff --git a/src/extensions/extensions-store/extensions-store.injectable.ts b/src/extensions/extensions-store/extensions-store.injectable.ts index 352922143a..edf84e4b66 100644 --- a/src/extensions/extensions-store/extensions-store.injectable.ts +++ b/src/extensions/extensions-store/extensions-store.injectable.ts @@ -7,7 +7,14 @@ import { ExtensionsStore } from "./extensions-store"; const extensionsStoreInjectable = getInjectable({ id: "extensions-store", - instantiate: () => ExtensionsStore.createInstance(), + + instantiate: () => { + ExtensionsStore.resetInstance(); + + return ExtensionsStore.createInstance(); + }, + + causesSideEffects: true, }); export default extensionsStoreInjectable; diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index bdfb4578e5..6df8e3c08a 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -32,6 +32,10 @@ export class LensExtension { readonly manifestPath: string; readonly isBundled: boolean; + get sanitizedExtensionId() { + return sanitizeExtensionName(this.name); + } + protocolHandlers: ProtocolHandlerRegistration[] = []; @observable private _isEnabled = false; @@ -90,6 +94,7 @@ export class LensExtension { this[Disposers].push(...await register(this)); logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); + } catch (error) { logger.error(`[EXTENSION]: failed to activate ${this.name}@${this.version}: ${error}`); } @@ -129,6 +134,12 @@ export function sanitizeExtensionName(name: string) { return name.replace("@", "").replace("/", "--"); } +export const getSanitizedPath = (...parts: string[]) => parts + .filter(Boolean) + .join("/") + .replace(/\/+/g, "/") + .replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id) + export function extensionDisplayName(name: string, version: string) { return `${name}@${version}`; } diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 0c16b142a7..3baa9fd8bc 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -5,7 +5,6 @@ import type * as registries from "./registries"; import { Disposers, LensExtension } from "./lens-extension"; -import { getExtensionPageUrl } from "./registries/page-registry"; import type { CatalogEntity } from "../common/catalog"; import type { Disposer } from "../common/utils"; import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry"; @@ -22,6 +21,13 @@ import type { StatusBarRegistration } from "../renderer/components/status-bar/st import type { KubeObjectMenuRegistration } from "../renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-registration"; import type { WorkloadsOverviewDetailRegistration } from "../renderer/components/+workloads-overview/workloads-overview-detail-registration"; import type { KubeObjectStatusRegistration } from "../renderer/components/kube-object-status-icon/kube-object-status-registration"; +import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "./as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import routesInjectable from "../renderer/routes/routes.injectable"; +import { fromPairs, map, matches, toPairs } from "lodash/fp"; +import extensionPageParametersInjectable from "../renderer/routes/extension-page-parameters.injectable"; +import { pipeline } from "@ogre-tools/fp"; +import { getExtensionRoutePath } from "../renderer/routes/get-extension-route-path"; +import { navigateToRouteInjectionToken } from "../common/front-end-routing/navigate-to-route-injection-token"; export class LensRendererExtension extends LensExtension { globalPages: registries.PageRegistration[] = []; @@ -43,14 +49,48 @@ export class LensRendererExtension extends LensExtension { customCategoryViews: CustomCategoryViewRegistration[] = []; async navigate

(pageId?: string, params?: P) { - const { navigate } = await import("../renderer/navigation"); - const pageUrl = getExtensionPageUrl({ - extensionId: this.name, - pageId, - params: params ?? {}, // compile to url with params - }); + const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi( + Environments.renderer, + ); - navigate(pageUrl); + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + const routes = di.inject(routesInjectable).get(); + + const targetRegistration = [...this.globalPages, ...this.clusterPages].find( + registration => registration.id === (pageId || undefined), + ); + + const targetRoutePath = getExtensionRoutePath(this, targetRegistration.id); + + const targetRoute = routes.find(matches({ path: targetRoutePath })); + + if (targetRoute) { + const normalizedParams = di.inject(extensionPageParametersInjectable, { + extension: this, + registration: targetRegistration, + }); + + const query = pipeline( + params, + + toPairs, + + map(([key, value]) => { + const normalizedParam = normalizedParams[key]; + + return [ + key, + normalizedParam.stringify(value), + ]; + }), + + fromPairs, + ); + + navigateToRoute(targetRoute, { + query, + }); + } } /** diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts deleted file mode 100644 index 34c921cef5..0000000000 --- a/src/extensions/registries/__tests__/page-registry.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { jest } from "@jest/globals"; -import { ClusterPageRegistry, getExtensionPageUrl, GlobalPageRegistry, PageParams } from "../page-registry"; -import { LensExtension } from "../../lens-extension"; -import React from "react"; -import fse from "fs-extra"; -import { Console } from "console"; -import { stderr, stdout } from "process"; -import { ThemeStore } from "../../../renderer/theme.store"; -import { UserStore } from "../../../common/user-store"; -import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing"; -import mockFs from "mock-fs"; - -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, - ipcRenderer: { - on: jest.fn(), - invoke: jest.fn(), - }, -})); - -console = new Console(stdout, stderr); - -let ext: LensExtension = null; - -describe("page registry tests", () => { - beforeEach(async () => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); - - mockFs(); - - await dis.runSetups(); - - ext = new LensExtension({ - manifest: { - name: "foo-bar", - version: "0.1.1", - }, - id: "/this/is/fake/package.json", - absolutePath: "/absolute/fake/", - manifestPath: "/this/is/fake/package.json", - isBundled: false, - isEnabled: true, - isCompatible: true, - }); - UserStore.createInstance(); - ThemeStore.createInstance(); - ClusterPageRegistry.createInstance(); - GlobalPageRegistry.createInstance().add({ - id: "page-with-params", - components: { - Page: () => React.createElement("Page with params"), - }, - params: { - test1: "test1-default", - test2: "", // no default value, just declaration - }, - }, ext); - GlobalPageRegistry.createInstance().add([ - { - id: "test-page", - components: { - Page: () => React.createElement("Text"), - }, - }, - { - id: "another-page", - components: { - Page: () => React.createElement("Text"), - }, - }, - { - components: { - Page: () => React.createElement("Default"), - }, - }, - ], ext); - }); - - afterEach(() => { - GlobalPageRegistry.resetInstance(); - ClusterPageRegistry.resetInstance(); - ThemeStore.resetInstance(); - UserStore.resetInstance(); - fse.remove("tmp"); - mockFs.restore(); - }); - - describe("getPageUrl", () => { - it("returns a page url for extension", () => { - expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar"); - }); - - it("allows to pass base url as parameter", () => { - expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test"); - }); - - it("removes @ and replace `/` to `--`", () => { - expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo--bar"); - }); - - it("adds / prefix", () => { - expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test"); - }); - - it("normalize possible multi-slashes in page.id", () => { - expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "//test/" })).toBe("/extension/foo-bar/test"); - }); - - it("gets page url with custom params", () => { - const params: PageParams = { test1: "one", test2: "2" }; - const searchParams = new URLSearchParams(params); - const pageUrl = getExtensionPageUrl({ - extensionId: ext.name, - pageId: "page-with-params", - params, - }); - - expect(pageUrl).toBe(`/extension/foo-bar/page-with-params?${searchParams}`); - }); - - it("gets page url with default custom params", () => { - const defaultPageUrl = getExtensionPageUrl({ - extensionId: ext.name, - pageId: "page-with-params", - }); - - expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`); - }); - }); - - describe("globalPageRegistry", () => { - describe("getByPageTarget", () => { - it("matching to first registered page without id", () => { - const page = GlobalPageRegistry.getInstance().getByPageTarget({ extensionId: ext.name }); - - expect(page.id).toEqual(undefined); - expect(page.extensionId).toEqual(ext.name); - expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name })); - }); - - it("returns matching page", () => { - const page = GlobalPageRegistry.getInstance().getByPageTarget({ - pageId: "test-page", - extensionId: ext.name, - }); - - expect(page.id).toEqual("test-page"); - }); - - it("returns null if target not found", () => { - const page = GlobalPageRegistry.getInstance().getByPageTarget({ - pageId: "wrong-page", - extensionId: ext.name, - }); - - expect(page).toBeNull(); - }); - }); - }); -}); diff --git a/src/extensions/registries/page-menu-registry.ts b/src/extensions/registries/page-menu-registry.ts index 0c18bd07c0..d69bcfadfa 100644 --- a/src/extensions/registries/page-menu-registry.ts +++ b/src/extensions/registries/page-menu-registry.ts @@ -6,9 +6,7 @@ // Extensions-api -> Register page menu items import type { IconProps } from "../../renderer/components/icon"; import type React from "react"; -import type { PageTarget, RegisteredPage } from "./page-registry"; -import type { LensExtension } from "../lens-extension"; -import { BaseRegistry } from "./base-registry"; +import type { PageTarget } from "./page-registry"; export interface ClusterPageMenuRegistration { id?: string; @@ -21,36 +19,3 @@ export interface ClusterPageMenuRegistration { export interface ClusterPageMenuComponents { Icon: React.ComponentType; } - -export class ClusterPageMenuRegistry extends BaseRegistry { - add(items: ClusterPageMenuRegistration[], ext: LensExtension) { - const normalizedItems = items.map(menuItem => { - menuItem.target = { - extensionId: ext.name, - ...(menuItem.target || {}), - }; - - return menuItem; - }); - - return super.add(normalizedItems); - } - - getRootItems() { - return this.getItems().filter((item) => !item.parentId); - } - - getSubItems(parent: ClusterPageMenuRegistration) { - return this.getItems().filter((item) => ( - item.parentId === parent.id && - item.target.extensionId === parent.target.extensionId - )); - } - - getByPage({ id: pageId, extensionId }: RegisteredPage) { - return this.getItems().find((item) => ( - item.target.pageId == pageId && - item.target.extensionId === extensionId - )); - } -} diff --git a/src/extensions/registries/page-registry.ts b/src/extensions/registries/page-registry.ts index 79c1c81893..c4520b11e5 100644 --- a/src/extensions/registries/page-registry.ts +++ b/src/extensions/registries/page-registry.ts @@ -2,15 +2,10 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ +import type { PageParamInit, PageParam } from "../../renderer/navigation"; // Extensions-api -> Custom page registration -import React from "react"; -import { observer } from "mobx-react"; -import { BaseRegistry } from "./base-registry"; -import { LensExtension, LensExtensionId, sanitizeExtensionName } from "../lens-extension"; -import { createPageParam, PageParam, PageParamInit, searchParamsOptions } from "../../renderer/navigation"; - export interface PageRegistration { /** * Page ID, part of extension's page url, must be unique within same extension @@ -48,95 +43,3 @@ export interface RegisteredPage { params: PageParams; // normalized params components: PageComponents; // normalized components } - -export function getExtensionPageUrl(target: PageTarget): string { - const { extensionId, pageId = "", params: targetParams = {}} = target; - - const pagePath = ["/extension", sanitizeExtensionName(extensionId), pageId] - .filter(Boolean) - .join("/").replace(/\/+/g, "/").replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id) - - const pageUrl = new URL(pagePath, `http://localhost`); - - // stringify params to matched target page - const registeredPage = GlobalPageRegistry.getInstance().getByPageTarget(target) || ClusterPageRegistry.getInstance().getByPageTarget(target); - - if (registeredPage?.params) { - Object.entries(registeredPage.params).forEach(([name, param]) => { - pageUrl.searchParams.delete(name); // first off, clear existing value(s) - - param.stringify(targetParams[name]).forEach(value => { - if (searchParamsOptions.skipEmpty && !value) return; - pageUrl.searchParams.append(name, value); - }); - }); - } - - return pageUrl.href.replace(pageUrl.origin, ""); -} - -class PageRegistry extends BaseRegistry { - protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage { - const { id: pageId } = page; - const extensionId = ext.name; - const params = this.normalizeParams(extensionId, page.params); - const components = this.normalizeComponents(page.components, params); - const url = getExtensionPageUrl({ extensionId, pageId }); - - return { - id: pageId, extensionId, params, components, url, - }; - } - - protected normalizeComponents(components: PageComponents, params?: PageParams): PageComponents { - if (params) { - const { Page } = components; - - // inject extension's page component props.params - components.Page = observer((props: object) => React.createElement(Page, { params, ...props })); - } - - return components; - } - - protected normalizeParams(extensionId: LensExtensionId, params?: PageParams>): PageParams { - if (!params) return undefined; - const normalizedParams: PageParams = {}; - - Object.entries(params).forEach(([paramName, paramValue]) => { - const paramInit: PageParamInit = { - name: paramName, - prefix: `${extensionId}:`, - defaultValue: paramValue, - }; - - // handle non-string params - if (typeof paramValue !== "string") { - const { defaultValue: value, parse, stringify } = paramValue; - - const notAStringValue = typeof value !== "string" || ( - Array.isArray(value) && !value.every(value => typeof value === "string") - ); - - if (notAStringValue && !(parse || stringify)) { - throw new Error(`PageRegistry: param's "${paramName}" initialization has failed: paramInit.parse() and paramInit.stringify() are required for non string | string[] "defaultValue"`); - } - - paramInit.defaultValue = value; - paramInit.parse = parse; - paramInit.stringify = stringify; - } - - normalizedParams[paramName] = createPageParam(paramInit); - }); - - return normalizedParams; - } - - getByPageTarget(target: PageTarget): RegisteredPage | null { - return this.getItems().find(page => page.extensionId === target.extensionId && page.id === target.pageId) || null; - } -} - -export class ClusterPageRegistry extends PageRegistry {} -export class GlobalPageRegistry extends PageRegistry {} diff --git a/src/extensions/renderer-api/k8s-api.ts b/src/extensions/renderer-api/k8s-api.ts index 1f3829f0cc..f4952b2cd6 100644 --- a/src/extensions/renderer-api/k8s-api.ts +++ b/src/extensions/renderer-api/k8s-api.ts @@ -4,15 +4,20 @@ */ 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"; +import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; export function isAllowedResource(resource: KubeResource | KubeResource[]) { - const _isAllowedResource = asLegacyGlobalFunctionForExtensionApi(isAllowedResourceInjectable); - const resources = castArray(resource); - return resources.every(x => _isAllowedResource(x)); + const di = getLegacyGlobalDiForExtensionApi(); + + return resources.every((resourceName: any) => { + const _isAllowedResource = di.inject(isAllowedResourceInjectable, resourceName); + + // Note: Legacy isAllowedResource does not advertise reactivity + return _isAllowedResource.get(); + }); } export { ResourceStack } from "../../common/k8s/resource-stack"; diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 348112e791..92681218dd 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -52,6 +52,8 @@ import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-p import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import path from "path"; import spawnInjectable from "../child-process/spawn.injectable"; +import getConfigurationFileModelInjectable from "../../common/get-configuration-file-model/get-configuration-file-model.injectable"; +import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.injectable"; console = new Console(stdout, stderr); @@ -96,6 +98,9 @@ describe("kube auth proxy tests", () => { di.override(spawnInjectable, () => mockSpawn); + di.permitSideEffects(getConfigurationFileModelInjectable); + di.permitSideEffects(appVersionInjectable); + mockFs(mockMinikubeConfig); await di.runSetups(); @@ -134,7 +139,7 @@ describe("kube auth proxy tests", () => { beforeEach(async () => { mockedCP = mock(); listeners = new EventEmitter(); - + jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true)); jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false)); mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => { diff --git a/src/main/app-paths/app-paths.injectable.ts b/src/main/app-paths/app-paths.injectable.ts index 654f34d29b..b9df2c7f57 100644 --- a/src/main/app-paths/app-paths.injectable.ts +++ b/src/main/app-paths/app-paths.injectable.ts @@ -16,9 +16,9 @@ import registerChannelInjectable from "./register-channel/register-channel.injec import { getAppPaths } from "./get-app-paths"; import getElectronAppPathInjectable from "./get-electron-app-path/get-electron-app-path.injectable"; import setElectronAppPathInjectable from "./set-electron-app-path/set-electron-app-path.injectable"; -import path from "path"; import appNameInjectable from "./app-name/app-name.injectable"; import directoryForIntegrationTestingInjectable from "./directory-for-integration-testing/directory-for-integration-testing.injectable"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; const appPathsInjectable = getInjectable({ id: "app-paths", @@ -55,10 +55,11 @@ const setupPathForUserData = async (di: DiContainerForSetup) => { const setElectronAppPath = await di.inject(setElectronAppPathInjectable); const appName = await di.inject(appNameInjectable); const getAppPath = await di.inject(getElectronAppPathInjectable); + const joinPaths = await di.inject(joinPathsInjectable); const appDataPath = getAppPath("appData"); - setElectronAppPath("userData", path.join(appDataPath, appName)); + setElectronAppPath("userData", joinPaths(appDataPath, appName)); }; // Todo: this kludge is here only until we have a proper place to setup integration testing. diff --git a/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts index 8eeb24ce1f..2c0d19e571 100644 --- a/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts +++ b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts @@ -7,6 +7,8 @@ import getElectronAppPathInjectable from "./get-electron-app-path.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import type { App } from "electron"; import registerChannelInjectable from "../register-channel/register-channel.injectable"; +import joinPathsInjectable from "../../../common/path/join-paths.injectable"; +import { joinPathsFake } from "../../../common/test-utils/join-paths-fake"; describe("get-electron-app-path", () => { let getElectronAppPath: (name: string) => string | null; @@ -31,6 +33,7 @@ describe("get-electron-app-path", () => { di.override(electronAppInjectable, () => appStub); di.override(registerChannelInjectable, () => () => undefined); + di.override(joinPathsInjectable, () => joinPathsFake); await di.runSetups(); diff --git a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts index 4d3820fd37..782c6e5950 100644 --- a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts +++ b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts @@ -16,6 +16,10 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import { ClusterStore } from "../../../common/cluster-store/cluster-store"; +import getConfigurationFileModelInjectable + from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; +import appVersionInjectable + from "../../../common/get-configuration-file-model/app-version/app-version.injectable"; jest.mock("electron", () => ({ app: { @@ -48,6 +52,13 @@ describe("kubeconfig-sync.source tests", () => { createCluster: di.inject(createClusterInjectionToken), }); + di.override(clusterStoreInjectable, () => + ClusterStore.createInstance({ createCluster: () => null }), + ); + + di.permitSideEffects(getConfigurationFileModelInjectable); + di.permitSideEffects(appVersionInjectable); + di.inject(clusterStoreInjectable); ClusterManager.createInstance(); diff --git a/src/main/catalog-sources/general.ts b/src/main/catalog-sources/general.ts deleted file mode 100644 index 4738f6ac78..0000000000 --- a/src/main/catalog-sources/general.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { observable } from "mobx"; -import { GeneralEntity } from "../../common/catalog-entities/general"; -import { catalogURL, preferencesURL, welcomeURL } from "../../common/routes"; -import { catalogEntityRegistry } from "../catalog"; - -export const catalogEntity = new GeneralEntity({ - metadata: { - uid: "catalog-entity", - name: "Catalog", - source: "app", - labels: {}, - }, - spec: { - path: catalogURL(), - icon: { - material: "view_list", - background: "#3d90ce", - }, - }, - status: { - phase: "active", - }, -}); - -const preferencesEntity = new GeneralEntity({ - metadata: { - uid: "preferences-entity", - name: "Preferences", - source: "app", - labels: {}, - }, - spec: { - path: preferencesURL(), - icon: { - material: "settings", - background: "#3d90ce", - }, - }, - status: { - phase: "active", - }, -}); - -const welcomePageEntity = new GeneralEntity({ - metadata: { - uid: "welcome-page-entity", - name: "Welcome Page", - source: "app", - labels: {}, - }, - spec: { - path: welcomeURL(), - icon: { - material: "meeting_room", - background: "#3d90ce", - }, - }, - status: { - phase: "active", - }, -}); - -const generalEntities = observable([ - catalogEntity, - preferencesEntity, - welcomePageEntity, -]); - -export function syncGeneralEntities() { - catalogEntityRegistry.addObservableSource("lens:general", generalEntities); -} diff --git a/src/main/catalog-sources/index.ts b/src/main/catalog-sources/index.ts index c66bbd5a8f..98c3f08536 100644 --- a/src/main/catalog-sources/index.ts +++ b/src/main/catalog-sources/index.ts @@ -4,4 +4,3 @@ */ export { syncWeblinks } from "./weblinks"; -export { syncGeneralEntities } from "./general"; diff --git a/src/main/catalog-sources/sync-general-catalog-entities.injectable.ts b/src/main/catalog-sources/sync-general-catalog-entities.injectable.ts new file mode 100644 index 0000000000..167e74c719 --- /dev/null +++ b/src/main/catalog-sources/sync-general-catalog-entities.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import catalogEntityRegistryInjectable from "../catalog/catalog-entity-registry.injectable"; +import { generalCatalogEntityInjectionToken } from "../../common/catalog-entities/general-catalog-entities/general-catalog-entity-injection-token"; +import { computed } from "mobx"; + +const syncGeneralCatalogEntitiesInjectable = getInjectable({ + id: "sync-general-catalog-entities", + + instantiate: (di) => { + const generalCatalogEntities = di.injectMany(generalCatalogEntityInjectionToken); + const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable); + + // TODO: This shouldn't be reactive at all but catalogEntityRegistry accepts only reactive sources + const reactiveGeneralCatalogEntities = computed(() => generalCatalogEntities); + + return () => { + catalogEntityRegistry.addComputedSource( + "lens:general", + reactiveGeneralCatalogEntities, + ); + }; + }, +}); + +export default syncGeneralCatalogEntitiesInjectable; diff --git a/src/main/catalog/catalog-entity-registry.injectable.ts b/src/main/catalog/catalog-entity-registry.injectable.ts new file mode 100644 index 0000000000..37584bab78 --- /dev/null +++ b/src/main/catalog/catalog-entity-registry.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { catalogEntityRegistry } from "./catalog-entity-registry"; + +const catalogEntityRegistryInjectable = getInjectable({ + id: "catalog-entity-registry", + instantiate: () => catalogEntityRegistry, + causesSideEffects: true, +}); + +export default catalogEntityRegistryInjectable; diff --git a/src/main/getDi.ts b/src/main/getDi.ts index aa96e2ad17..d6f76122b7 100644 --- a/src/main/getDi.ts +++ b/src/main/getDi.ts @@ -4,7 +4,7 @@ */ import { createContainer } from "@ogre-tools/injectable"; -import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; export const getDi = () => { const di = createContainer( @@ -13,7 +13,7 @@ export const getDi = () => { getRequireContextForCommonCode, ); - setLegacyGlobalDiForExtensionApi(di); + setLegacyGlobalDiForExtensionApi(di, Environments.main); return di; }; diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 777fc47892..b1306457c5 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -4,10 +4,10 @@ */ import glob from "glob"; -import { memoize, kebabCase } from "lodash/fp"; +import { kebabCase, memoize, noop } from "lodash/fp"; import { createContainer } from "@ogre-tools/injectable"; -import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-electron-app-path.injectable"; import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable"; import appNameInjectable from "./app-paths/app-name/app-name.injectable"; @@ -16,14 +16,32 @@ import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; import readFileInjectable from "../common/fs/read-file.injectable"; import directoryForBundledBinariesInjectable from "../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable"; +import loggerInjectable from "../common/logger.injectable"; import spawnInjectable from "./child-process/spawn.injectable"; +import extensionsStoreInjectable from "../extensions/extensions-store/extensions-store.injectable"; +import type { ExtensionsStore } from "../extensions/extensions-store/extensions-store"; +import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable"; +import type { FileSystemProvisionerStore } from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store"; +import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; +import type { ClusterStore } from "../common/cluster-store/cluster-store"; +import type { Cluster } from "../common/cluster/cluster"; +import userStoreInjectable from "../common/user-store/user-store.injectable"; +import type { UserStore } from "../common/user-store"; +import isMacInjectable from "../common/vars/is-mac.injectable"; +import isWindowsInjectable from "../common/vars/is-windows.injectable"; +import isLinuxInjectable from "../common/vars/is-linux.injectable"; +import getAbsolutePathInjectable from "../common/path/get-absolute-path.injectable"; +import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake"; +import joinPathsInjectable from "../common/path/join-paths.injectable"; +import { joinPathsFake } from "../common/test-utils/join-paths-fake"; +import hotbarStoreInjectable from "../common/hotbar-store.injectable"; export const getDiForUnitTesting = ( { doGeneralOverrides } = { doGeneralOverrides: false }, ) => { const di = createContainer(); - setLegacyGlobalDiForExtensionApi(di); + setLegacyGlobalDiForExtensionApi(di, Environments.main); for (const filePath of getInjectableFilePaths()) { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -38,6 +56,24 @@ export const getDiForUnitTesting = ( di.preventSideEffects(); if (doGeneralOverrides) { + di.override(isMacInjectable, () => true); + di.override(isWindowsInjectable, () => false); + di.override(isLinuxInjectable, () => false); + + di.override(getAbsolutePathInjectable, () => getAbsolutePathFake); + di.override(joinPathsInjectable, () => joinPathsFake); + + // eslint-disable-next-line unused-imports/no-unused-vars-ts + di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore); + + di.override(hotbarStoreInjectable, () => ({})); + + di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore); + + // eslint-disable-next-line unused-imports/no-unused-vars-ts + di.override(clusterStoreInjectable, () => ({ getById: (id): Cluster => ({}) as Cluster }) as ClusterStore); + di.override(userStoreInjectable, () => ({}) as UserStore); + di.override( getElectronAppPathInjectable, () => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`, @@ -47,8 +83,9 @@ export const getDiForUnitTesting = ( di.override(appNameInjectable, () => "some-electron-app-name"); di.override(registerChannelInjectable, () => () => undefined); di.override(directoryForBundledBinariesInjectable, () => "some-bin-directory"); - di.override(spawnInjectable, () => () => { - return { + + di.override(spawnInjectable, () => () => { + return { stderr: { on: jest.fn(), removeAllListeners: jest.fn() }, stdout: { on: jest.fn(), removeAllListeners: jest.fn() }, on: jest.fn(), @@ -66,6 +103,13 @@ export const getDiForUnitTesting = ( di.override(readFileInjectable, () => () => { throw new Error("Tried to read file from file system without specifying explicit override."); }); + + di.override(loggerInjectable, () => ({ + warn: noop, + debug: noop, + error: (message: string, ...args: any) => console.error(message, ...args), + info: noop, + })); } return di; diff --git a/src/main/index.ts b/src/main/index.ts index d60d0ea4e6..5927bb3769 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -30,11 +30,10 @@ import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { startCatalogSyncToRenderer } from "./catalog-pusher"; import { catalogEntityRegistry } from "./catalog"; import { HelmRepoManager } from "./helm/helm-repo-manager"; -import { syncGeneralEntities, syncWeblinks } from "./catalog-sources"; +import { syncWeblinks } from "./catalog-sources"; import configurePackages from "../common/configure-packages"; import { PrometheusProviderRegistry } from "./prometheus"; import * as initializers from "./initializers"; -import { HotbarStore } from "../common/hotbar-store"; import { WeblinkStore } from "../common/weblink-store"; import { SentryInit } from "../common/sentry"; import { ensureDir } from "fs-extra"; @@ -48,7 +47,6 @@ import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-r import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable"; import directoryForExesInjectable from "../common/app-paths/directory-for-exes/directory-for-exes.injectable"; import initIpcMainHandlersInjectable from "./initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable"; -import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable"; import directoryForKubeConfigsInjectable from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import kubeconfigSyncManagerInjectable from "./catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable"; import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; @@ -57,6 +55,11 @@ import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell import userStoreInjectable from "../common/user-store/user-store.injectable"; import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable"; import { broadcastNativeThemeOnUpdate } from "./native-theme"; +import windowManagerInjectable from "./window-manager.injectable"; +import navigateToPreferencesInjectable from "../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; +import syncGeneralCatalogEntitiesInjectable from "./catalog-sources/sync-general-catalog-entities.injectable"; +import hotbarStoreInjectable from "../common/hotbar-store.injectable"; +import applicationMenuItemsInjectable from "./menu/application-menu-items.injectable"; const di = getDi(); @@ -66,7 +69,7 @@ app.on("ready", async () => { await di.runSetups(); injectSystemCAs(); - + const onCloseCleanup = disposer(); const onQuitCleanup = disposer(); @@ -184,7 +187,7 @@ app.on("ready", async () => { lensProtocolRouterMain.route(rawUrl); }); - logger.debug("[APP-MAIN] waiting for 'ready' and other messages"); + logger.debug("[APP-MAIN] waiting for 'ready' and other messages"); const directoryForExes = di.inject(directoryForExesInjectable); @@ -204,7 +207,9 @@ app.on("ready", async () => { * store has migrations that will remove items that previous migrations add * if this is not present */ - syncGeneralEntities(); + const syncGeneralCatalogEntities = di.inject(syncGeneralCatalogEntitiesInjectable); + + syncGeneralCatalogEntities(); logger.info("💾 Loading stores"); @@ -218,7 +223,7 @@ app.on("ready", async () => { clusterStore.provideInitialFromMain(); // HotbarStore depends on: ClusterStore - HotbarStore.createInstance(); + di.inject(hotbarStoreInjectable); WeblinkStore.createInstance(); @@ -291,13 +296,15 @@ app.on("ready", async () => { const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden); logger.info("🖥️ Starting WindowManager"); - const windowManager = WindowManager.createInstance(); - const menuItems = di.inject(electronMenuItemsInjectable); + const windowManager = di.inject(windowManagerInjectable); + + const applicationMenuItems = di.inject(applicationMenuItemsInjectable); const trayMenuItems = di.inject(trayMenuItemsInjectable); + const navigateToPreferences = di.inject(navigateToPreferencesInjectable); onQuitCleanup.push( - initMenu(windowManager, menuItems), - initTray(windowManager, trayMenuItems), + initMenu(applicationMenuItems), + initTray(windowManager, trayMenuItems, navigateToPreferences), () => ShellSession.cleanup(), ); diff --git a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts index 794cfc522a..d92897bb50 100644 --- a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts +++ b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts @@ -3,17 +3,18 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import electronMenuItemsInjectable from "../../menu/electron-menu-items.injectable"; -import directoryForLensLocalStorageInjectable - from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; +import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; import { initIpcMainHandlers } from "./init-ipc-main-handlers"; +import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable"; +import applicationMenuItemsInjectable from "../../menu/application-menu-items.injectable"; const initIpcMainHandlersInjectable = getInjectable({ id: "init-ipc-main-handlers", instantiate: (di) => initIpcMainHandlers({ - electronMenuItems: di.inject(electronMenuItemsInjectable), + applicationMenuItems: di.inject(applicationMenuItemsInjectable), directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable), + getAbsolutePath: di.inject(getAbsolutePathInjectable), }), }); diff --git a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts index 0899052f00..1341ec18d7 100644 --- a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts +++ b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts @@ -14,25 +14,24 @@ import { catalogEntityRegistry } from "../../catalog"; import { pushCatalogToRenderer } from "../../catalog-pusher"; import { ClusterManager } from "../../cluster-manager"; import { ResourceApplier } from "../../resource-applier"; -import { WindowManager } from "../../window-manager"; -import path from "path"; import { remove } from "fs-extra"; -import { getAppMenu } from "../../menu/menu"; -import type { MenuRegistration } from "../../menu/menu-registration"; -import type { IComputedValue } from "mobx"; import { onLocationChange, handleWindowAction } from "../../ipc/window"; import { openFilePickingDialogChannel } from "../../../common/ipc/dialog"; import { showOpenDialog } from "../../ipc/dialog"; import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel } from "../../../common/ipc/window"; import { getNativeColorTheme } from "../../native-theme"; import { getNativeThemeChannel } from "../../../common/ipc/native-theme"; +import type { GetAbsolutePath } from "../../../common/path/get-absolute-path.injectable"; +import type { IComputedValue } from "mobx"; +import type { MenuItemOpts } from "../../menu/application-menu-items.injectable"; interface Dependencies { - electronMenuItems: IComputedValue; directoryForLensLocalStorage: string; + getAbsolutePath: GetAbsolutePath; + applicationMenuItems: IComputedValue; } -export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalStorage }: Dependencies) => () => { +export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLocalStorage, getAbsolutePath }: Dependencies) => () => { ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { return ClusterStore.getInstance() .getById(clusterId) @@ -88,7 +87,7 @@ export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalSt try { // remove the local storage file - const localStorageFilePath = path.resolve(directoryForLensLocalStorage, `${cluster.id}.json`); + const localStorageFilePath = getAbsolutePath(directoryForLensLocalStorage, `${cluster.id}.json`); await remove(localStorageFilePath); } catch { @@ -151,7 +150,9 @@ export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalSt ipcMainHandle(broadcastMainChannel, (event, channel, ...args) => broadcastMessage(channel, ...args)); ipcMainOn(windowOpenAppMenuAsContextMenuChannel, async (event) => { - const menu = Menu.buildFromTemplate(getAppMenu(WindowManager.getInstance(), electronMenuItems.get())); + const appMenu = applicationMenuItems.get(); + + const menu = Menu.buildFromTemplate(appMenu); menu.popup({ ...BrowserWindow.fromWebContents(event.sender), diff --git a/src/main/is-auto-update-enabled.injectable.ts b/src/main/is-auto-update-enabled.injectable.ts new file mode 100644 index 0000000000..4a61318e46 --- /dev/null +++ b/src/main/is-auto-update-enabled.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { isAutoUpdateEnabled } from "./app-updater"; + +const isAutoUpdateEnabledInjectable = getInjectable({ + id: "is-auto-update-enabled", + instantiate: () => isAutoUpdateEnabled, + causesSideEffects: true, +}); + +export default isAutoUpdateEnabledInjectable; diff --git a/src/main/kubectl/create-kubectl.injectable.ts b/src/main/kubectl/create-kubectl.injectable.ts index dac0cf684c..931f777535 100644 --- a/src/main/kubectl/create-kubectl.injectable.ts +++ b/src/main/kubectl/create-kubectl.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { Kubectl } from "./kubectl"; -import directoryForKubectlBinariesInjectable from "./directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable"; +import directoryForKubectlBinariesInjectable from "../../common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable"; const createKubectlInjectable = getInjectable({ diff --git a/src/main/kubectl/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts b/src/main/kubectl/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts deleted file mode 100644 index 75a3cc421d..0000000000 --- a/src/main/kubectl/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import directoryForBinariesInjectable from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable"; -import path from "path"; - -const directoryForKubectlBinariesInjectable = getInjectable({ - id: "directory-for-kubectl-binaries", - - instantiate: (di) => - path.join(di.inject(directoryForBinariesInjectable), "kubectl"), -}); - -export default directoryForKubectlBinariesInjectable; diff --git a/src/main/menu/application-menu-items.injectable.ts b/src/main/menu/application-menu-items.injectable.ts new file mode 100644 index 0000000000..5961d2121b --- /dev/null +++ b/src/main/menu/application-menu-items.injectable.ts @@ -0,0 +1,337 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { checkForUpdates } from "../app-updater"; +import { docsUrl, productName, supportUrl } from "../../common/vars"; +import { exitApp } from "../exit-app"; +import { broadcastMessage } from "../../common/ipc"; +import { openBrowser } from "../../common/utils"; +import { showAbout } from "./menu"; +import windowManagerInjectable from "../window-manager.injectable"; +import { + BrowserWindow, + MenuItem, + MenuItemConstructorOptions, + webContents, +} from "electron"; +import loggerInjectable from "../../common/logger.injectable"; +import appNameInjectable from "../app-paths/app-name/app-name.injectable"; +import electronMenuItemsInjectable from "./electron-menu-items.injectable"; +import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable"; +import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; +import navigateToExtensionsInjectable from "../../common/front-end-routing/routes/extensions/navigate-to-extensions.injectable"; +import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; +import navigateToWelcomeInjectable from "../../common/front-end-routing/routes/welcome/navigate-to-welcome.injectable"; +import navigateToAddClusterInjectable from "../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable"; +import isMacInjectable from "../../common/vars/is-mac.injectable"; +import { computed } from "mobx"; + +function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) { + return check ? [] : menuItems; +} + +export interface MenuItemOpts extends MenuItemConstructorOptions { + submenu?: MenuItemConstructorOptions[]; +} + +const applicationMenuItemsInjectable = getInjectable({ + id: "application-menu-items", + + instantiate: (di) => { + const logger = di.inject(loggerInjectable); + const appName = di.inject(appNameInjectable); + const isMac = di.inject(isMacInjectable); + const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable); + const electronMenuItems = di.inject(electronMenuItemsInjectable); + + return computed((): MenuItemOpts[] => { + + // TODO: These injects should happen outside of the computed. + // TODO: Remove temporal dependencies in WindowManager to make sure timing is correct. + const windowManager = di.inject(windowManagerInjectable); + const navigateToPreferences = di.inject(navigateToPreferencesInjectable); + const navigateToExtensions = di.inject(navigateToExtensionsInjectable); + const navigateToCatalog = di.inject(navigateToCatalogInjectable); + const navigateToWelcome = di.inject(navigateToWelcomeInjectable); + const navigateToAddCluster = di.inject(navigateToAddClusterInjectable); + + const autoUpdateDisabled = !isAutoUpdateEnabled(); + + logger.info(`[MENU]: autoUpdateDisabled=${autoUpdateDisabled}`); + + const macAppMenu: MenuItemOpts = { + label: appName, + id: "root", + submenu: [ + { + label: `About ${productName}`, + id: "about", + click(menuItem: MenuItem, browserWindow: BrowserWindow) { + showAbout(browserWindow); + }, + }, + ...ignoreIf(autoUpdateDisabled, [ + { + label: "Check for updates", + click() { + checkForUpdates().then(() => windowManager.ensureMainWindow()); + }, + }, + ]), + { type: "separator" }, + { + label: "Preferences", + accelerator: "CmdOrCtrl+,", + id: "preferences", + click() { + navigateToPreferences(); + }, + }, + { + label: "Extensions", + accelerator: "CmdOrCtrl+Shift+E", + id: "extensions", + click() { + navigateToExtensions(); + }, + }, + { type: "separator" }, + { role: "services" }, + { type: "separator" }, + { role: "hide" }, + { role: "hideOthers" }, + { role: "unhide" }, + { type: "separator" }, + { + label: "Quit", + accelerator: "Cmd+Q", + id: "quit", + click() { + exitApp(); + }, + }, + ], + }; + const fileMenu: MenuItemOpts = { + label: "File", + id: "file", + submenu: [ + { + label: "Add Cluster", + accelerator: "CmdOrCtrl+Shift+A", + id: "add-cluster", + click() { + navigateToAddCluster(); + }, + }, + ...ignoreIf(isMac, [ + { type: "separator" }, + { + label: "Preferences", + id: "preferences", + accelerator: "Ctrl+,", + click() { + navigateToPreferences(); + }, + }, + { + label: "Extensions", + accelerator: "Ctrl+Shift+E", + click() { + navigateToExtensions(); + }, + }, + ]), + + { type: "separator" }, + + ...(isMac + ? ([ + { + role: "close", + label: "Close Window", + accelerator: "Shift+Cmd+W", + }, + ] as MenuItemConstructorOptions[]) + : []), + + ...ignoreIf(isMac, [ + { + label: "Exit", + accelerator: "Alt+F4", + id: "quit", + click() { + exitApp(); + }, + }, + ]), + ], + }; + const editMenu: MenuItemOpts = { + label: "Edit", + id: "edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "delete" }, + { type: "separator" }, + { role: "selectAll" }, + ], + }; + const viewMenu: MenuItemOpts = { + label: "View", + id: "view", + submenu: [ + { + label: "Catalog", + accelerator: "Shift+CmdOrCtrl+C", + id: "catalog", + click() { + navigateToCatalog(); + }, + }, + { + label: "Command Palette...", + accelerator: "Shift+CmdOrCtrl+P", + id: "command-palette", + click(_m, _b, event) { + /** + * Don't broadcast unless it was triggered by menu iteration so that + * there aren't double events in renderer + * + * NOTE: this `?` is required because of a bug in playwright. https://github.com/microsoft/playwright/issues/10554 + */ + if (!event?.triggeredByAccelerator) { + broadcastMessage("command-palette:open"); + } + }, + }, + { type: "separator" }, + { + label: "Back", + accelerator: "CmdOrCtrl+[", + id: "go-back", + click() { + webContents + .getAllWebContents() + .filter((wc) => wc.getType() === "window") + .forEach((wc) => wc.goBack()); + }, + }, + { + label: "Forward", + accelerator: "CmdOrCtrl+]", + id: "go-forward", + click() { + webContents + .getAllWebContents() + .filter((wc) => wc.getType() === "window") + .forEach((wc) => wc.goForward()); + }, + }, + { + label: "Reload", + accelerator: "CmdOrCtrl+R", + id: "reload", + click() { + windowManager.reload(); + }, + }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }; + const helpMenu: MenuItemOpts = { + role: "help", + id: "help", + submenu: [ + { + label: "Welcome", + id: "welcome", + click() { + navigateToWelcome(); + }, + }, + { + label: "Documentation", + id: "documentation", + click: async () => { + openBrowser(docsUrl).catch((error) => { + logger.error("[MENU]: failed to open browser", { error }); + }); + }, + }, + { + label: "Support", + id: "support", + click: async () => { + openBrowser(supportUrl).catch((error) => { + logger.error("[MENU]: failed to open browser", { error }); + }); + }, + }, + ...ignoreIf(isMac, [ + { + label: `About ${productName}`, + id: "about", + click(menuItem: MenuItem, browserWindow: BrowserWindow) { + showAbout(browserWindow); + }, + }, + ...ignoreIf(autoUpdateDisabled, [ + { + label: "Check for updates", + click() { + checkForUpdates().then(() => + windowManager.ensureMainWindow(), + ); + }, + }, + ]), + ]), + ], + }; + // Prepare menu items order + const appMenu = new Map([ + ["mac", macAppMenu], + ["file", fileMenu], + ["edit", editMenu], + ["view", viewMenu], + ["help", helpMenu], + ]); + + // Modify menu from extensions-api + for (const menuItem of electronMenuItems.get()) { + if (!appMenu.has(menuItem.parentId)) { + logger.error( + `[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`, + { menuItem }, + ); + + continue; + } + + appMenu.get(menuItem.parentId).submenu.push(menuItem); + } + + if (!isMac) { + appMenu.delete("mac"); + } + + return [...appMenu.values()]; + }); + }, +}); + +export default applicationMenuItemsInjectable; diff --git a/src/main/menu/menu.ts b/src/main/menu/menu.ts index 18309cb71b..03f31ecec7 100644 --- a/src/main/menu/menu.ts +++ b/src/main/menu/menu.ts @@ -2,30 +2,18 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents } from "electron"; +import { app, BrowserWindow, dialog, Menu } from "electron"; import { autorun, IComputedValue } from "mobx"; -import type { WindowManager } from "../window-manager"; -import { appName, isMac, isWindows, docsUrl, supportUrl, productName } from "../../common/vars"; -import logger from "../logger"; -import { exitApp } from "../exit-app"; -import { broadcastMessage } from "../../common/ipc"; -import { openBrowser } from "../../common/utils"; +import { appName, isWindows, productName } from "../../common/vars"; import packageJson from "../../../package.json"; -import { preferencesURL, extensionsURL, addClusterURL, catalogURL, welcomeURL } from "../../common/routes"; -import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater"; -import type { MenuRegistration } from "./menu-registration"; +import type { MenuItemOpts } from "./application-menu-items.injectable"; export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"; -interface MenuItemsOpts extends MenuItemConstructorOptions { - submenu?: MenuItemConstructorOptions[]; -} - export function initMenu( - windowManager: WindowManager, - electronMenuItems: IComputedValue, + applicationMenuItems: IComputedValue, ) { - return autorun(() => buildMenu(windowManager, electronMenuItems.get()), { + return autorun(() => buildMenu(applicationMenuItems.get()), { delay: 100, }); } @@ -48,288 +36,10 @@ export function showAbout(browserWindow: BrowserWindow) { }); } -export function getAppMenu( - windowManager: WindowManager, - electronMenuItems: MenuRegistration[], -) { - function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) { - return check ? [] : menuItems; - } - - async function navigate(url: string) { - logger.info(`[MENU]: navigating to ${url}`); - await windowManager.navigate(url); - } - - const autoUpdateDisabled = !isAutoUpdateEnabled(); - - logger.info(`[MENU]: autoUpdateDisabled=${autoUpdateDisabled}`); - - const macAppMenu: MenuItemsOpts = { - label: app.getName(), - id: "root", - submenu: [ - { - label: `About ${productName}`, - id: "about", - click(menuItem: MenuItem, browserWindow: BrowserWindow) { - showAbout(browserWindow); - }, - }, - ...ignoreIf(autoUpdateDisabled, [{ - label: "Check for updates", - click() { - checkForUpdates() - .then(() => windowManager.ensureMainWindow()); - }, - }]), - { type: "separator" }, - { - label: "Preferences", - accelerator: "CmdOrCtrl+,", - id: "preferences", - click() { - navigate(preferencesURL()); - }, - }, - { - label: "Extensions", - accelerator: "CmdOrCtrl+Shift+E", - id: "extensions", - click() { - navigate(extensionsURL()); - }, - }, - { type: "separator" }, - { role: "services" }, - { type: "separator" }, - { role: "hide" }, - { role: "hideOthers" }, - { role: "unhide" }, - { type: "separator" }, - { - label: "Quit", - accelerator: "Cmd+Q", - id: "quit", - click() { - exitApp(); - }, - }, - ], - }; - const fileMenu: MenuItemsOpts = { - label: "File", - id: "file", - submenu: [ - { - label: "Add Cluster", - accelerator: "CmdOrCtrl+Shift+A", - id: "add-cluster", - click() { - navigate(addClusterURL()); - }, - }, - ...ignoreIf(isMac, [ - { type: "separator" }, - { - label: "Preferences", - id: "preferences", - accelerator: "Ctrl+,", - click() { - navigate(preferencesURL()); - }, - }, - { - label: "Extensions", - accelerator: "Ctrl+Shift+E", - click() { - navigate(extensionsURL()); - }, - }, - ]), - - { type: "separator" }, - - ...(isMac ? [ - { - role: "close", - label: "Close Window", - accelerator: "Shift+Cmd+W", - }, - ] as MenuItemConstructorOptions[] : []), - - ...ignoreIf(isMac, [ - { - label: "Exit", - accelerator: "Alt+F4", - id: "quit", - click() { - exitApp(); - }, - }, - ]), - ], - }; - const editMenu: MenuItemsOpts = { - label: "Edit", - id: "edit", - submenu: [ - { role: "undo" }, - { role: "redo" }, - { type: "separator" }, - { role: "cut" }, - { role: "copy" }, - { role: "paste" }, - { role: "delete" }, - { type: "separator" }, - { role: "selectAll" }, - ], - }; - const viewMenu: MenuItemsOpts = { - label: "View", - id: "view", - submenu: [ - { - label: "Catalog", - accelerator: "Shift+CmdOrCtrl+C", - id: "catalog", - click() { - navigate(catalogURL()); - }, - }, - { - label: "Command Palette...", - accelerator: "Shift+CmdOrCtrl+P", - id: "command-palette", - click(_m, _b, event) { - /** - * Don't broadcast unless it was triggered by menu iteration so that - * there aren't double events in renderer - * - * NOTE: this `?` is required because of a bug in playwright. https://github.com/microsoft/playwright/issues/10554 - */ - if (!event?.triggeredByAccelerator) { - broadcastMessage("command-palette:open"); - } - }, - }, - { type: "separator" }, - { - label: "Back", - accelerator: "CmdOrCtrl+[", - id: "go-back", - click() { - webContents.getAllWebContents().filter(wc => wc.getType() === "window").forEach(wc => wc.goBack()); - }, - }, - { - label: "Forward", - accelerator: "CmdOrCtrl+]", - id: "go-forward", - click() { - webContents.getAllWebContents().filter(wc => wc.getType() === "window").forEach(wc => wc.goForward()); - }, - }, - { - label: "Reload", - accelerator: "CmdOrCtrl+R", - id: "reload", - click() { - windowManager.reload(); - }, - }, - { role: "toggleDevTools" }, - { type: "separator" }, - { role: "resetZoom" }, - { role: "zoomIn" }, - { role: "zoomOut" }, - { type: "separator" }, - { role: "togglefullscreen" }, - ], - }; - const helpMenu: MenuItemsOpts = { - role: "help", - id: "help", - submenu: [ - { - label: "Welcome", - id: "welcome", - click() { - navigate(welcomeURL()); - }, - }, - { - label: "Documentation", - id: "documentation", - click: async () => { - openBrowser(docsUrl).catch(error => { - logger.error("[MENU]: failed to open browser", { error }); - }); - }, - }, - { - label: "Support", - id: "support", - click: async () => { - openBrowser(supportUrl).catch(error => { - logger.error("[MENU]: failed to open browser", { error }); - }); - }, - }, - ...ignoreIf(isMac, [ - { - label: `About ${productName}`, - id: "about", - click(menuItem: MenuItem, browserWindow: BrowserWindow) { - showAbout(browserWindow); - }, - }, - ...ignoreIf(autoUpdateDisabled, [{ - label: "Check for updates", - click() { - checkForUpdates() - .then(() => windowManager.ensureMainWindow()); - }, - }]), - ]), - ], - }; - // Prepare menu items order - const appMenu = new Map([ - ["mac", macAppMenu], - ["file", fileMenu], - ["edit", editMenu], - ["view", viewMenu], - ["help", helpMenu], - ]); - - // Modify menu from extensions-api - for (const menuItem of electronMenuItems) { - if (!appMenu.has(menuItem.parentId)) { - logger.error( - `[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`, - { menuItem }, - ); - - continue; - } - - appMenu.get(menuItem.parentId).submenu.push(menuItem); - } - - if (!isMac) { - appMenu.delete("mac"); - } - - return [...appMenu.values()]; - -} - export function buildMenu( - windowManager: WindowManager, - electronMenuItems: MenuRegistration[], + applicationMenuItems: MenuItemOpts[], ) { Menu.setApplicationMenu( - Menu.buildFromTemplate(getAppMenu(windowManager, electronMenuItems)), + Menu.buildFromTemplate(applicationMenuItems), ); } diff --git a/src/main/navigate-to-route/navigate-to-route.injectable.ts b/src/main/navigate-to-route/navigate-to-route.injectable.ts new file mode 100644 index 0000000000..12bf077ced --- /dev/null +++ b/src/main/navigate-to-route/navigate-to-route.injectable.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; +import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token"; +import { buildURL } from "../../common/utils/buildUrl"; + +const navigateToRouteInjectable = getInjectable({ + id: "navigate-to-route", + + instantiate: (di) => { + const navigateToUrl = di.inject(navigateToUrlInjectionToken); + + return (route, options) => { + const url = buildURL(route.path, { + // TODO: enhance typing + params: options?.parameters as any, + query: options?.query, + fragment: options?.fragment, + }); + + navigateToUrl(url, options); + }; + }, + + injectionToken: navigateToRouteInjectionToken, +}); + +export default navigateToRouteInjectable; diff --git a/src/main/navigate-to-url/navigate-to-url.injectable.ts b/src/main/navigate-to-url/navigate-to-url.injectable.ts new file mode 100644 index 0000000000..5a3cd2bf49 --- /dev/null +++ b/src/main/navigate-to-url/navigate-to-url.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import windowManagerInjectable from "../window-manager.injectable"; +import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; + +const navigateToUrlInjectable = getInjectable({ + id: "navigate-to-url", + + instantiate: (di) => { + const windowManager = di.inject(windowManagerInjectable); + + return (url) => { + windowManager.navigateSync(url); + }; + }, + + injectionToken: navigateToUrlInjectionToken, +}); + +export default navigateToUrlInjectable; diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index d96cd342dc..d8c88f7f22 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -13,12 +13,11 @@ import { ExtensionsStore } from "../../../extensions/extensions-store/extensions import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main"; import mockFs from "mock-fs"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; -import extensionLoaderInjectable - from "../../../extensions/extension-loader/extension-loader.injectable"; -import lensProtocolRouterMainInjectable - from "../lens-protocol-router-main/lens-protocol-router-main.injectable"; -import extensionsStoreInjectable - from "../../../extensions/extensions-store/extensions-store.injectable"; +import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; +import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable"; +import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable"; +import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; +import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable"; jest.mock("../../../common/ipc"); @@ -42,11 +41,16 @@ describe("protocol router tests", () => { "tmp": {}, }); + di.override(extensionsStoreInjectable, () => ExtensionsStore.createInstance()); + + di.permitSideEffects(getConfigurationFileModelInjectable); + di.permitSideEffects(appVersionInjectable); + await di.runSetups(); extensionLoader = di.inject(extensionLoaderInjectable); - extensionsStore = di.inject(extensionsStoreInjectable); + extensionsStore = di.inject(extensionsStoreInjectable); lpr = di.inject(lensProtocolRouterMainInjectable); @@ -56,9 +60,6 @@ describe("protocol router tests", () => { afterEach(() => { jest.clearAllMocks(); - // TODO: Remove Singleton from BaseStore to achieve independent unit testing - ExtensionsStore.resetInstance(); - mockFs.restore(); }); diff --git a/src/main/router/router.ts b/src/main/router/router.ts index ca5da23ce1..56455e52e7 100644 --- a/src/main/router/router.ts +++ b/src/main/router/router.ts @@ -7,7 +7,6 @@ import Call from "@hapi/call"; import type http from "http"; import type httpProxy from "http-proxy"; import { toPairs } from "lodash/fp"; -import path from "path"; import type { Cluster } from "../../common/cluster/cluster"; import type { LensApiResultContentType } from "./router-content-types"; import { contentTypes } from "./router-content-types"; @@ -51,7 +50,6 @@ interface Dependencies { export class Router { protected router = new Call.Router(); - protected static rootPath = path.resolve(__static); constructor(routes: Route[], private dependencies: Dependencies) { routes.forEach(route => { diff --git a/src/main/routes/static-file-route.injectable.ts b/src/main/routes/static-file-route.injectable.ts index f35976f320..21b4025a8d 100644 --- a/src/main/routes/static-file-route.injectable.ts +++ b/src/main/routes/static-file-route.injectable.ts @@ -9,23 +9,28 @@ import logger from "../logger"; import { routeInjectionToken } from "../router/router.injectable"; import { appName, publicPath } from "../../common/vars"; import path from "path"; -import readFileInjectable from "../../common/fs/read-file.injectable"; import isDevelopmentInjectable from "../../common/vars/is-development.injectable"; import httpProxy from "http-proxy"; +import readFileBufferInjectable from "../../common/fs/read-file-buffer.injectable"; +import getAbsolutePathInjectable, { GetAbsolutePath } from "../../common/path/get-absolute-path.injectable"; +import type { JoinPaths } from "../../common/path/join-paths.injectable"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; interface ProductionDependencies { - readFile: (path: string) => Promise; + readFileBuffer: (path: string) => Promise; + getAbsolutePath: GetAbsolutePath; + joinPaths: JoinPaths; } const handleStaticFileInProduction = - ({ readFile }: ProductionDependencies) => + ({ readFileBuffer, getAbsolutePath, joinPaths }: ProductionDependencies) => async ({ params }: LensApiRequest) => { - const staticPath = path.resolve(__static); + const staticPath = getAbsolutePath(__static); let filePath = params.path; for (let retryCount = 0; retryCount < 5; retryCount += 1) { - const asset = path.join(staticPath, filePath); - const normalizedFilePath = path.resolve(asset); + const asset = joinPaths(staticPath, filePath); + const normalizedFilePath = getAbsolutePath(asset); if (!normalizedFilePath.startsWith(staticPath)) { return { statusCode: 404 }; @@ -38,7 +43,7 @@ const handleStaticFileInProduction = const contentType = contentTypes[fileExtension] || contentTypes.txt; - return { response: await readFile(asset), contentType }; + return { response: await readFileBuffer(asset), contentType }; } catch (err) { if (retryCount > 5) { logger.error("handleStaticFile:", err.toString()); @@ -78,13 +83,16 @@ const staticFileRouteInjectable = getInjectable({ instantiate: (di): Route => { const isDevelopment = di.inject(isDevelopmentInjectable); + const readFileBuffer = di.inject(readFileBufferInjectable); + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const joinPaths = di.inject(joinPathsInjectable); return { method: "get", path: `/{path*}`, handler: isDevelopment ? handleStaticFileInDevelopment({ proxy: httpProxy.createProxy() }) - : handleStaticFileInProduction({ readFile: di.inject(readFileInjectable) }), + : handleStaticFileInProduction({ readFileBuffer, getAbsolutePath, joinPaths }), }; }, diff --git a/src/main/tray/tray.ts b/src/main/tray/tray.ts index b4e8a24a8c..9bdfa5c082 100644 --- a/src/main/tray/tray.ts +++ b/src/main/tray/tray.ts @@ -13,10 +13,10 @@ import type { WindowManager } from "../window-manager"; import logger from "../logger"; import { isDevelopment, isWindows, productName } from "../../common/vars"; import { exitApp } from "../exit-app"; -import { preferencesURL } from "../../common/routes"; import { toJS } from "../../common/utils"; import type { TrayMenuRegistration } from "./tray-menu-registration"; + const TRAY_LOG_PREFIX = "[TRAY]"; // note: instance of Tray should be saved somewhere, otherwise it disappears @@ -33,6 +33,7 @@ export function getTrayIcon(): string { export function initTray( windowManager: WindowManager, trayMenuItems: IComputedValue, + navigateToPreferences: () => void, ) { const icon = getTrayIcon(); @@ -51,7 +52,7 @@ export function initTray( const disposers = [ autorun(() => { try { - const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get())); + const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()), navigateToPreferences); tray.setContextMenu(menu); } catch (error) { @@ -80,6 +81,7 @@ function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron function createTrayMenu( windowManager: WindowManager, extensionTrayItems: TrayMenuRegistration[], + navigateToPreferences: () => void, ): Menu { let template: Electron.MenuItemConstructorOptions[] = [ { @@ -93,9 +95,7 @@ function createTrayMenu( { label: "Preferences", click() { - windowManager - .navigate(preferencesURL()) - .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to navigate to Preferences`, { error })); + navigateToPreferences(); }, }, ]; diff --git a/src/main/window-manager.injectable.ts b/src/main/window-manager.injectable.ts new file mode 100644 index 0000000000..6a1f54a660 --- /dev/null +++ b/src/main/window-manager.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { WindowManager } from "./window-manager"; + +const windowManagerInjectable = getInjectable({ + id: "window-manager", + + instantiate: () => { + WindowManager.resetInstance(); + + return WindowManager.createInstance(); + }, + + causesSideEffects: true, +}); + +export default windowManagerInjectable; diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index d95e1b69ce..74b80ca563 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -187,7 +187,7 @@ export class WindowManager extends Singleton { return this.mainWindow; } - private sendToView({ channel, frameInfo, data = [] }: SendToViewArgs) { + sendToView({ channel, frameInfo, data = [] }: SendToViewArgs) { if (frameInfo) { this.mainWindow.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data); } else { @@ -210,6 +210,10 @@ export class WindowManager extends Singleton { async navigate(url: string, frameId?: number) { await this.ensureMainWindow(); + this.navigateSync(url, frameId); + } + + navigateSync(url: string, frameId?: number) { const frameInfo = iter.find(clusterFrameMap.values(), frameInfo => frameInfo.frameId === frameId); const channel = frameInfo ? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER diff --git a/src/migrations/cluster-store/5.0.0-beta.10.ts b/src/migrations/cluster-store/5.0.0-beta.10.ts index f087fca2b6..a9fc759ee4 100644 --- a/src/migrations/cluster-store/5.0.0-beta.10.ts +++ b/src/migrations/cluster-store/5.0.0-beta.10.ts @@ -8,8 +8,7 @@ import fse from "fs-extra"; import type { ClusterModel } from "../../common/cluster-types"; import type { MigrationDeclaration } from "../helpers"; import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; -import directoryForUserDataInjectable - from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; interface Pre500WorkspaceStoreModel { workspaces: { diff --git a/src/migrations/hotbar-store/5.0.0-alpha.0.ts b/src/migrations/hotbar-store/5.0.0-alpha.0.ts index eb6c479f8e..3515a927bd 100644 --- a/src/migrations/hotbar-store/5.0.0-alpha.0.ts +++ b/src/migrations/hotbar-store/5.0.0-alpha.0.ts @@ -5,14 +5,18 @@ // Cleans up a store that had the state related data stored import type { MigrationDeclaration } from "../helpers"; -import { catalogEntity } from "../../main/catalog-sources/general"; import { getEmptyHotbar } from "../../common/hotbar-types"; +import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import catalogCatalogEntityInjectable from "../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; export default { version: "5.0.0-alpha.0", run(store) { const hotbar = getEmptyHotbar("default"); - const { metadata: { uid, name, source }} = catalogEntity; + const di = getLegacyGlobalDiForExtensionApi(); + const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable); + + const { metadata: { uid, name, source }} = catalogCatalogEntity; hotbar.items[0] = { entity: { uid, name, source }}; diff --git a/src/migrations/hotbar-store/5.0.0-beta.10.ts b/src/migrations/hotbar-store/5.0.0-beta.10.ts index 5008232e5d..e880310adf 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.10.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.10.ts @@ -9,12 +9,13 @@ import path from "path"; import * as uuid from "uuid"; import type { ClusterStoreModel } from "../../common/cluster-store/cluster-store"; import { defaultHotbarCells, getEmptyHotbar, Hotbar, HotbarItem } from "../../common/hotbar-types"; -import { catalogEntity } from "../../main/catalog-sources/general"; import { MigrationDeclaration, migrationLog } from "../helpers"; import { generateNewIdFor } from "../utils"; import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import catalogCatalogEntityInjectable + from "../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; interface Pre500WorkspaceStoreModel { workspaces: { @@ -42,7 +43,11 @@ export default { // Hotbars might be empty, if some of the previous migrations weren't run if (hotbars.length === 0) { const hotbar = getEmptyHotbar("default"); - const { metadata: { uid, name, source }} = catalogEntity; + + const di = getLegacyGlobalDiForExtensionApi(); + const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable); + + const { metadata: { uid, name, source }} = catalogCatalogEntity; hotbar.items[0] = { entity: { uid, name, source }}; @@ -120,8 +125,11 @@ export default { */ if (hotbars.every(hotbar => hotbar.items.every(item => item?.entity?.uid !== "catalog-entity"))) { // note, we will add a new whole hotbar here called "default" if that was previously removed + const di = getLegacyGlobalDiForExtensionApi(); + const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable); + const defaultHotbar = hotbars.find(hotbar => hotbar.name === "default"); - const { metadata: { uid, name, source }} = catalogEntity; + const { metadata: { uid, name, source }} = catalogCatalogEntity; if (defaultHotbar) { const freeIndex = defaultHotbar.items.findIndex(isNull); diff --git a/src/renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable.ts b/src/renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable.ts new file mode 100644 index 0000000000..151d77f097 --- /dev/null +++ b/src/renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable"; +import type { + IpcChannelListener, +} from "../../ipc-channel-listeners/ipc-channel-listener-injection-token"; + +const registerIpcChannelListenerInjectable = getInjectable({ + id: "register-ipc-channel-listener", + + instantiate: (di) => { + const ipc = di.inject(ipcRendererInjectable); + + return ({ channel, handle }: IpcChannelListener) => { + ipc.on(channel.name, (_, data) => { + handle(data); + }); + }; + }, +}); + +export default registerIpcChannelListenerInjectable; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 91e6892e33..b2a82c34ec 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -20,9 +20,7 @@ import { DefaultProps } from "./mui-base-theme"; import configurePackages from "../common/configure-packages"; import * as initializers from "./initializers"; import logger from "../common/logger"; -import { HotbarStore } from "../common/hotbar-store"; import { WeblinkStore } from "../common/weblink-store"; -import { ThemeStore } from "./theme.store"; import { SentryInit } from "../common/sentry"; import { registerCustomThemes } from "./components/monaco-editor"; import { getDi } from "./getDi"; @@ -36,6 +34,15 @@ import userStoreInjectable from "../common/user-store/user-store.injectable"; import initRootFrameInjectable from "./frames/root-frame/init-root-frame/init-root-frame.injectable"; import initClusterFrameInjectable from "./frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable"; import commandOverlayInjectable from "./components/command-palette/command-overlay.injectable"; +import { Router } from "react-router"; +import historyInjectable from "./navigation/history.injectable"; +import themeStoreInjectable from "./theme-store.injectable"; +import navigateToAddClusterInjectable from "../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable"; +import addSyncEntriesInjectable from "./initializers/add-sync-entries.injectable"; +import hotbarStoreInjectable from "../common/hotbar-store.injectable"; +import { bindEvents } from "./navigation/events"; +import deleteClusterDialogModelInjectable + from "./components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.injectable"; if (process.isMainFrame) { SentryInit(); @@ -58,6 +65,9 @@ async function attachChromeDebugger() { export async function bootstrap(di: DiContainer) { await di.runSetups(); + // TODO: Consolidate import time side-effect to setup time + bindEvents(); + const rootElem = document.getElementById("app"); const logPrefix = `[BOOTSTRAP-${process.isMainFrame ? "ROOT" : "CLUSTER"}-FRAME]:`; @@ -79,12 +89,19 @@ export async function bootstrap(di: DiContainer) { logger.info(`${logPrefix} initializing CatalogEntityDetailRegistry`); initializers.initCatalogEntityDetailRegistry(); + const navigateToAddCluster = di.inject(navigateToAddClusterInjectable); + const addSyncEntries = di.inject(addSyncEntriesInjectable); + logger.info(`${logPrefix} initializing CatalogCategoryRegistryEntries`); - initializers.initCatalogCategoryRegistryEntries(); + initializers.initCatalogCategoryRegistryEntries({ + navigateToAddCluster, + addSyncEntries, + }); logger.info(`${logPrefix} initializing Catalog`); initializers.initCatalog({ openCommandDialog: di.inject(commandOverlayInjectable).open, + deleteClusterDialogModel: di.inject(deleteClusterDialogModelInjectable), }); const extensionLoader = di.inject(extensionLoaderInjectable); @@ -104,10 +121,11 @@ export async function bootstrap(di: DiContainer) { await clusterStore.loadInitialOnRenderer(); // HotbarStore depends on: ClusterStore - HotbarStore.createInstance(); + di.inject(hotbarStoreInjectable); // ThemeStore depends on: UserStore - ThemeStore.createInstance(); + // TODO: Remove temporal dependencies + di.inject(themeStoreInjectable); WeblinkStore.createInstance(); @@ -142,9 +160,13 @@ export async function bootstrap(di: DiContainer) { }); } + const history = di.inject(historyInjectable); + render( - {DefaultProps(App)} + + {DefaultProps(App)} + , rootElem, diff --git a/src/renderer/components/+add-cluster/add-cluster-route-component.injectable.ts b/src/renderer/components/+add-cluster/add-cluster-route-component.injectable.ts new file mode 100644 index 0000000000..5d41fd7b5b --- /dev/null +++ b/src/renderer/components/+add-cluster/add-cluster-route-component.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 } from "@ogre-tools/injectable"; +import { AddCluster } from "./add-cluster"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import addClusterRouteInjectable from "../../../common/front-end-routing/routes/add-cluster/add-cluster-route.injectable"; + +const addClusterRouteComponentInjectable = getInjectable({ + id: "add-cluster-route-component", + + instantiate: (di) => ({ + route: di.inject(addClusterRouteInjectable), + Component: AddCluster, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default addClusterRouteComponentInjectable; diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 76fa6436f3..731ff2aa54 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -13,20 +13,17 @@ import { observer } from "mobx-react"; import path from "path"; import React from "react"; import * as uuid from "uuid"; - -import { catalogURL } from "../../../common/routes"; import { appEventBus } from "../../../common/app-event-bus/event-bus"; import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers"; import { docsUrl } from "../../../common/vars"; -import { navigate } from "../../navigation"; import { iter } from "../../utils"; import { Button } from "../button"; import { Notifications } from "../notifications"; import { SettingLayout } from "../layout/setting-layout"; import { MonacoEditor } from "../monaco-editor"; import { withInjectables } from "@ogre-tools/injectable-react"; -import getCustomKubeConfigDirectoryInjectable - from "../../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; +import getCustomKubeConfigDirectoryInjectable from "../../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; +import navigateToCatalogInjectable, { NavigateToCatalog } from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; interface Option { config: KubeConfig; @@ -35,6 +32,7 @@ interface Option { interface Dependencies { getCustomKubeConfigDirectory: (directoryName: string) => string; + navigateToCatalog: NavigateToCatalog; } function getContexts(config: KubeConfig): Map { @@ -96,7 +94,7 @@ class NonInjectedAddCluster extends React.Component { Notifications.ok(`Successfully added ${this.kubeContexts.size} new cluster(s)`); - return navigate(catalogURL()); + return this.props.navigateToCatalog(); } catch (error) { Notifications.error(`Failed to add clusters: ${error}`); } @@ -104,7 +102,7 @@ class NonInjectedAddCluster extends React.Component { render() { return ( - +

Add Clusters from Kubeconfig

Clusters added here are not merged into the ~/.kube/config file.{" "} @@ -149,5 +147,7 @@ export const AddCluster = withInjectables(NonInjectedAddCluster, { getCustomKubeConfigDirectory: di.inject( getCustomKubeConfigDirectoryInjectable, ), + + navigateToCatalog: di.inject(navigateToCatalogInjectable), }), }); diff --git a/src/renderer/components/+catalog/__tests__/custom-columns.test.ts b/src/renderer/components/+catalog/__tests__/custom-columns.test.ts index 1ef7bef199..e95303f472 100644 --- a/src/renderer/components/+catalog/__tests__/custom-columns.test.ts +++ b/src/renderer/components/+catalog/__tests__/custom-columns.test.ts @@ -12,6 +12,7 @@ import { CatalogCategory } from "../../../api/catalog-entity"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import type { AdditionalCategoryColumnRegistration, CategoryColumnRegistration } from "../custom-category-columns"; import getCategoryColumnsInjectable, { CategoryColumns, GetCategoryColumnsParams } from "../get-category-columns.injectable"; +import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable"; class TestCategory extends CatalogCategory { apiVersion = "catalog.k8slens.dev/v1alpha1"; @@ -39,6 +40,8 @@ describe("Custom Category Columns", () => { beforeEach(() => { di = getDiForUnitTesting(); + + di.override(hotbarStoreInjectable, () => ({})); }); describe("without extensions", () => { diff --git a/src/renderer/components/+workloads/workloads.scss b/src/renderer/components/+catalog/catalog-browse-tab.ts similarity index 77% rename from src/renderer/components/+workloads/workloads.scss rename to src/renderer/components/+catalog/catalog-browse-tab.ts index d05328095c..107e12894e 100644 --- a/src/renderer/components/+workloads/workloads.scss +++ b/src/renderer/components/+catalog/catalog-browse-tab.ts @@ -3,5 +3,4 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -.Workloads { -} +export const browseCatalogTab = "browse"; diff --git a/src/renderer/components/+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable.ts b/src/renderer/components/+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable.ts index c978f8667e..e0ab887393 100644 --- a/src/renderer/components/+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable.ts +++ b/src/renderer/components/+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable.ts @@ -3,8 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { browseCatalogTab } from "../../../../common/routes"; import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; +import { browseCatalogTab } from "../catalog-browse-tab"; const catalogPreviousActiveTabStorageInjectable = getInjectable({ id: "catalog-previous-active-tab-storage", diff --git a/src/renderer/components/+catalog/catalog-route-component.injectable.ts b/src/renderer/components/+catalog/catalog-route-component.injectable.ts new file mode 100644 index 0000000000..feb450328f --- /dev/null +++ b/src/renderer/components/+catalog/catalog-route-component.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 } from "@ogre-tools/injectable"; +import { Catalog } from "./catalog"; +import catalogRouteInjectable from "../../../common/front-end-routing/routes/catalog/catalog-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const catalogRouteComponentInjectable = getInjectable({ + id: "catalog-route-component", + + instantiate: (di) => ({ + route: di.inject(catalogRouteInjectable), + Component: Catalog, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default catalogRouteComponentInjectable; diff --git a/src/renderer/components/+catalog/catalog-route-parameters.injectable.ts b/src/renderer/components/+catalog/catalog-route-parameters.injectable.ts new file mode 100644 index 0000000000..0d9bd1fc4e --- /dev/null +++ b/src/renderer/components/+catalog/catalog-route-parameters.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import routePathParametersInjectable from "../../routes/route-path-parameters.injectable"; +import catalogRouteInjectable from "../../../common/front-end-routing/routes/catalog/catalog-route.injectable"; + +const catalogRouteParametersInjectable = getInjectable({ + id: "catalog-route-parameters", + + instantiate: (di) => { + const route = di.inject(catalogRouteInjectable); + const pathParameters = di.inject(routePathParametersInjectable, route); + + return { + group: computed(() => pathParameters.get().group), + kind: computed(() => pathParameters.get().kind), + }; + }, +}); + +export default catalogRouteParametersInjectable; diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index 5c221bb457..9422adf055 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -7,7 +7,6 @@ import React from "react"; import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Catalog } from "./catalog"; -import { createMemoryHistory } from "history"; import { mockWindow } from "../../../../__mocks__/windowMock"; import { CatalogCategoryRegistry, CatalogEntity, CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog"; import { CatalogEntityRegistry } from "../../api/catalog-entity-registry"; @@ -16,15 +15,15 @@ import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.s import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import type { DiContainer } from "@ogre-tools/injectable"; import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable"; -import catalogEntityRegistryInjectable - from "../../api/catalog-entity-registry/catalog-entity-registry.injectable"; +import catalogEntityRegistryInjectable from "../../api/catalog-entity-registry/catalog-entity-registry.injectable"; import type { DiRender } from "../test-utils/renderFor"; import { renderFor } from "../test-utils/renderFor"; import { ThemeStore } from "../../theme.store"; import { UserStore } from "../../../common/user-store"; import mockFs from "mock-fs"; -import directoryForUserDataInjectable - from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; +import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable"; import type { AppEvent } from "../../../common/app-event-bus/event-bus"; import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable"; @@ -66,25 +65,6 @@ class MockCatalogEntity extends CatalogEntity { } describe("", () => { - const history = createMemoryHistory(); - const mockLocation = { - pathname: "", - search: "", - state: "", - hash: "", - }; - const mockMatch = { - params: { - // will be used to match activeCategory - // need to be the same as property values in kubernetesClusterCategory - group: "entity.k8slens.dev", - kind: "KubernetesCluster", - }, - isExact: true, - path: "", - url: "", - }; - function createMockCatalogEntity(onRun: (context: CatalogEntityActionContext) => void | Promise) { return new MockCatalogEntity({ metadata: { @@ -112,6 +92,9 @@ describe("", () => { di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.permitSideEffects(getConfigurationFileModelInjectable); + di.permitSideEffects(appVersionInjectable); + await di.runSetups(); mockFs(); @@ -169,11 +152,7 @@ describe("", () => { ); render( - , + , ); userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); @@ -198,13 +177,7 @@ describe("", () => { }, ); - render( - , - ); + render(); userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); }); @@ -229,13 +202,7 @@ describe("", () => { }, ); - render( - , - ); + render(); userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); }); @@ -255,13 +222,7 @@ describe("", () => { }, ); - render( - , - ); + render(); userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); }); @@ -288,13 +249,7 @@ describe("", () => { }, ); - render( - , - ); + render(); userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); }); @@ -319,24 +274,14 @@ describe("", () => { }, ); - render( - , - ); + render(); userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); }); it("emits catalog open AppEvent", () => { render( - , + , ); expect(emitEvent).toHaveBeenCalledWith( { @@ -347,11 +292,7 @@ describe("", () => { it("emits catalog change AppEvent when changing the category", () => { render( - , + , ); userEvent.click(screen.getByText("Web Links")); diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index a3af640fc5..784aa1ba45 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -8,21 +8,18 @@ import styles from "./catalog.module.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import { ItemListLayout } from "../item-object-list"; -import { action, IComputedValue, makeObservable, observable, reaction, runInAction, when } from "mobx"; +import { action, computed, IComputedValue, makeObservable, observable, reaction, runInAction, when } from "mobx"; import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store"; import { navigate } from "../../navigation"; import { MenuItem, MenuActions } from "../menu"; import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; -import { HotbarStore } from "../../../common/hotbar-store"; import { ConfirmDialog } from "../confirm-dialog"; import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog"; import { CatalogAddButton } from "./catalog-add-button"; -import type { RouteComponentProps } from "react-router"; import { Notifications } from "../notifications"; import { MainLayout } from "../layout/main-layout"; import { prevDefault } from "../../utils"; import { CatalogEntityDetails } from "./catalog-entity-details"; -import { browseCatalogTab, catalogURL, CatalogViewRouteParam } from "../../../common/routes"; import { CatalogMenu } from "./catalog-menu"; import { RenderDelay } from "../render-delay/render-delay"; import { Icon } from "../icon"; @@ -36,10 +33,13 @@ import getCategoryColumnsInjectable from "./get-category-columns.injectable"; import type { RegisteredCustomCategoryViewDecl } from "./custom-views.injectable"; import customCategoryViewsInjectable from "./custom-views.injectable"; import type { CustomCategoryViewComponents } from "./custom-views"; +import navigateToCatalogInjectable, { NavigateToCatalog } from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; +import catalogRouteParametersInjectable from "./catalog-route-parameters.injectable"; +import { browseCatalogTab } from "./catalog-browse-tab"; import type { AppEvent } from "../../../common/app-event-bus/event-bus"; import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable"; - -export interface CatalogProps extends RouteComponentProps {} +import hotbarStoreInjectable from "../../../common/hotbar-store.injectable"; +import type { HotbarStore } from "../../../common/hotbar-store"; interface Dependencies { catalogPreviousActiveTabStorage: { set: (value: string ) => void }; @@ -47,23 +47,35 @@ interface Dependencies { getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns; customCategoryViews: IComputedValue>>; emitEvent: (event: AppEvent) => void; + + routeParameters: { + group: IComputedValue; + kind: IComputedValue; + }; + + navigateToCatalog: NavigateToCatalog; + hotbarStore: HotbarStore; } @observer -class NonInjectedCatalog extends React.Component { +class NonInjectedCatalog extends React.Component { @observable private contextMenu: CatalogEntityContextMenuContext; @observable activeTab?: string; - constructor(props: CatalogProps & Dependencies) { + constructor(props: Dependencies) { super(props); makeObservable(this); } + @computed get routeActiveTab(): string { - const { group, kind } = this.props.match.params ?? {}; + const { group, kind } = this.props.routeParameters; - if (group && kind) { - return `${group}/${kind}`; + const dereferencedGroup = group.get(); + const dereferencedKind = kind.get(); + + if (dereferencedGroup && dereferencedKind) { + return `${dereferencedGroup}/${dereferencedKind}`; } return browseCatalogTab; @@ -115,11 +127,11 @@ class NonInjectedCatalog extends React.Component { } addToHotbar(entity: CatalogEntity): void { - HotbarStore.getInstance().addToHotbar(entity); + this.props.hotbarStore.addToHotbar(entity); } removeFromHotbar(entity: CatalogEntity): void { - HotbarStore.getInstance().removeFromHotbar(entity.getId()); + this.props.hotbarStore.removeFromHotbar(entity.getId()); } onDetails = (entity: CatalogEntity) => { @@ -163,9 +175,10 @@ class NonInjectedCatalog extends React.Component { }); if (activeCategory) { - navigate(catalogURL({ params: { group: activeCategory.spec.group, kind: activeCategory.spec.names.kind }})); + + this.props.navigateToCatalog({ group: activeCategory.spec.group, kind: activeCategory.spec.names.kind }); } else { - navigate(catalogURL({ params: { group: browseCatalogTab }})); + this.props.navigateToCatalog({ group: browseCatalogTab }); } }); @@ -205,7 +218,7 @@ class NonInjectedCatalog extends React.Component { }; renderName(entity: CatalogEntity) { - const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(entity); + const isItemInHotbar = this.props.hotbarStore.isAddedToActive(entity); return ( <> @@ -321,13 +334,15 @@ class NonInjectedCatalog extends React.Component { } } -export const Catalog = withInjectables( NonInjectedCatalog, { - getProps: (di, props) => ({ +export const Catalog = withInjectables( NonInjectedCatalog, { + getProps: (di) => ({ catalogEntityStore: di.inject(catalogEntityStoreInjectable), catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable), getCategoryColumns: di.inject(getCategoryColumnsInjectable), customCategoryViews: di.inject(customCategoryViewsInjectable), + routeParameters: di.inject(catalogRouteParametersInjectable), + navigateToCatalog: di.inject(navigateToCatalogInjectable), emitEvent: di.inject(appEventBusInjectable).emit, - ...props, + hotbarStore: di.inject(hotbarStoreInjectable), }), }); diff --git a/src/renderer/components/+catalog/get-category-columns.injectable.ts b/src/renderer/components/+catalog/get-category-columns.injectable.ts index ad6984d7c9..bf2cee1bb9 100644 --- a/src/renderer/components/+catalog/get-category-columns.injectable.ts +++ b/src/renderer/components/+catalog/get-category-columns.injectable.ts @@ -9,10 +9,12 @@ import type { CatalogCategory, CatalogEntity } from "../../../common/catalog"; import type { ItemListLayoutProps } from "../item-object-list"; import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns"; import categoryColumnsInjectable from "./custom-category-columns.injectable"; -import { defaultCategoryColumns, browseAllColumns, nameCategoryColumn } from "./internal-category-columns"; +import { defaultCategoryColumns, browseAllColumns } from "./internal-category-columns"; +import nameCategoryColumnInjectable from "./name-category-column.injectable"; interface Dependencies { extensionColumns: IComputedValue>>; + nameCategoryColumn: RegisteredAdditionalCategoryColumn; } export interface GetCategoryColumnsParams { @@ -21,7 +23,7 @@ export interface GetCategoryColumnsParams { export type CategoryColumns = Required, "sortingCallbacks" | "searchFilters" | "renderTableContents" | "renderTableHeader">>; -function getSpecificCategoryColumns(activeCategory: CatalogCategory, extensionColumns: IComputedValue>>): RegisteredAdditionalCategoryColumn[] { +function getSpecificCategoryColumns(activeCategory: CatalogCategory, extensionColumns: IComputedValue>>, nameCategoryColumn: RegisteredAdditionalCategoryColumn): RegisteredAdditionalCategoryColumn[] { const fromExtensions = ( extensionColumns .get() @@ -41,7 +43,7 @@ function getSpecificCategoryColumns(activeCategory: CatalogCategory, extensionCo ]; } -function getBrowseAllColumns(): RegisteredAdditionalCategoryColumn[] { +function getBrowseAllColumns(nameCategoryColumn: RegisteredAdditionalCategoryColumn): RegisteredAdditionalCategoryColumn[] { return [ ...browseAllColumns, nameCategoryColumn, @@ -49,11 +51,11 @@ function getBrowseAllColumns(): RegisteredAdditionalCategoryColumn[] { ]; } -const getCategoryColumns = ({ extensionColumns }: Dependencies) => ({ activeCategory }: GetCategoryColumnsParams): CategoryColumns => { +const getCategoryColumns = ({ extensionColumns, nameCategoryColumn }: Dependencies) => ({ activeCategory }: GetCategoryColumnsParams): CategoryColumns => { const allRegistrations = orderBy( activeCategory - ? getSpecificCategoryColumns(activeCategory, extensionColumns) - : getBrowseAllColumns(), + ? getSpecificCategoryColumns(activeCategory, extensionColumns, nameCategoryColumn) + : getBrowseAllColumns(nameCategoryColumn), "priority", "asc", ); @@ -89,6 +91,7 @@ const getCategoryColumnsInjectable = getInjectable({ instantiate: (di) => getCategoryColumns({ extensionColumns: di.inject(categoryColumnsInjectable), + nameCategoryColumn: di.inject(nameCategoryColumnInjectable), }), }); diff --git a/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx index b0c59b3f85..b48e629c94 100644 --- a/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx +++ b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx @@ -4,26 +4,54 @@ */ import React, { ReactNode, useState } from "react"; -import { HotbarStore } from "../../../common/hotbar-store"; import { MenuItem } from "../menu"; import type { CatalogEntity } from "../../api/catalog-entity"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import hotbarStoreInjectable from "../../../common/hotbar-store.injectable"; +import type { HotbarStore } from "../../../common/hotbar-store"; -export function HotbarToggleMenuItem(props: { entity: CatalogEntity; addContent: ReactNode; removeContent: ReactNode }) { - const store = HotbarStore.getInstance(); - const [itemInHotbar, setItemInHotbar] = useState(store.isAddedToActive(props.entity)); +interface Dependencies { + hotbarStore: HotbarStore; +} + +interface HotbarToggleMenuItemProps { + entity: CatalogEntity; + addContent: ReactNode; + removeContent: ReactNode; +} + +function NonInjectedHotbarToggleMenuItem({ + addContent, + entity, + hotbarStore, + removeContent, +}: Dependencies & HotbarToggleMenuItemProps) { + const [itemInHotbar, setItemInHotbar] = useState(hotbarStore.isAddedToActive(entity)); return ( { if (itemInHotbar) { - store.removeFromHotbar(props.entity.getId()); + hotbarStore.removeFromHotbar(entity.getId()); setItemInHotbar(false); } else { - store.addToHotbar(props.entity); + hotbarStore.addToHotbar(entity); setItemInHotbar(true); } }}> - {itemInHotbar ? props.removeContent : props.addContent } + {itemInHotbar ? removeContent : addContent } ); } + +export const HotbarToggleMenuItem = withInjectables( + NonInjectedHotbarToggleMenuItem, + + { + getProps: (di, props) => ({ + hotbarStore: di.inject(hotbarStoreInjectable), + ...props, + }), + }, +); + diff --git a/src/renderer/components/+catalog/internal-category-columns.tsx b/src/renderer/components/+catalog/internal-category-columns.tsx index f820d40837..0aba81db0b 100644 --- a/src/renderer/components/+catalog/internal-category-columns.tsx +++ b/src/renderer/components/+catalog/internal-category-columns.tsx @@ -6,49 +6,10 @@ import styles from "./catalog.module.scss"; import React from "react"; -import { HotbarStore } from "../../../common/hotbar-store"; -import type { CatalogEntity } from "../../api/catalog-entity"; -import { Avatar } from "../avatar"; import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns"; -import { Icon } from "../icon"; -import { prevDefault } from "../../utils"; import { getLabelBadges } from "./helpers"; import { KubeObject } from "../../../common/k8s-api/kube-object"; -function renderEntityName(entity: CatalogEntity) { - const hotbarStore = HotbarStore.getInstance(); - const isItemInHotbar = hotbarStore.isAddedToActive(entity); - const onClick = prevDefault( - isItemInHotbar - ? () => hotbarStore.removeFromHotbar(entity.getId()) - : () => hotbarStore.addToHotbar(entity), - ); - - return ( - <> - - {entity.spec.icon?.material && } - - {entity.getName()} - - - ); -} - export const browseAllColumns: RegisteredAdditionalCategoryColumn[] = [ { id: "kind", @@ -63,20 +24,6 @@ export const browseAllColumns: RegisteredAdditionalCategoryColumn[] = [ }, ]; -export const nameCategoryColumn: RegisteredAdditionalCategoryColumn = { - id: "name", - priority: 0, - renderCell: renderEntityName, - titleProps: { - title: "Name", - className: styles.entityName, - id: "name", - sortBy: "name", - }, - searchFilter: entity => entity.getName(), - sortCallback: entity => `name=${entity.getName()}`, -}; - export const defaultCategoryColumns: RegisteredAdditionalCategoryColumn[] = [ { id: "source", diff --git a/src/renderer/components/+catalog/name-category-column.injectable.tsx b/src/renderer/components/+catalog/name-category-column.injectable.tsx new file mode 100644 index 0000000000..27fd4072a0 --- /dev/null +++ b/src/renderer/components/+catalog/name-category-column.injectable.tsx @@ -0,0 +1,67 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import styles from "./catalog.module.scss"; +import type { CatalogEntity } from "../../../common/catalog"; +import { prevDefault } from "../../utils"; +import { Avatar } from "../avatar"; +import { Icon } from "../icon"; +import React from "react"; +import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns"; +import hotbarStoreInjectable from "../../../common/hotbar-store.injectable"; +import type { HotbarStore } from "../../../common/hotbar-store"; + +const renderEntityName = (hotbarStore: HotbarStore) => (entity: CatalogEntity) => { + const isItemInHotbar = hotbarStore.isAddedToActive(entity); + const onClick = prevDefault( + isItemInHotbar + ? () => hotbarStore.removeFromHotbar(entity.getId()) + : () => hotbarStore.addToHotbar(entity), + ); + + return ( + <> + + {entity.spec.icon?.material && } + + {entity.getName()} + + + ); +}; + + +const nameCategoryColumnInjectable = getInjectable({ + id: "name-category-column", + instantiate: (di): RegisteredAdditionalCategoryColumn => ({ + id: "name", + priority: 0, + renderCell: renderEntityName(di.inject(hotbarStoreInjectable)), + titleProps: { + title: "Name", + className: styles.entityName, + id: "name", + sortBy: "name", + }, + searchFilter: (entity) => entity.getName(), + sortCallback: (entity) => `name=${entity.getName()}`, + }), +}); + +export default nameCategoryColumnInjectable; diff --git a/src/renderer/components/+cluster/cluster-overview-route-component.injectable.ts b/src/renderer/components/+cluster/cluster-overview-route-component.injectable.ts new file mode 100644 index 0000000000..5f4e5516cd --- /dev/null +++ b/src/renderer/components/+cluster/cluster-overview-route-component.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 } from "@ogre-tools/injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import { ClusterOverview } from "./cluster-overview"; +import clusterOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/overview/cluster-overview-route.injectable"; + +const clusterOverviewRouteComponentInjectable = getInjectable({ + id: "cluster-overview-route-component", + + instantiate: (di) => ({ + route: di.inject(clusterOverviewRouteInjectable), + Component: ClusterOverview, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default clusterOverviewRouteComponentInjectable; diff --git a/src/renderer/components/+cluster/cluster-overview-sidebar-items.injectable.tsx b/src/renderer/components/+cluster/cluster-overview-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..88f5900fed --- /dev/null +++ b/src/renderer/components/+cluster/cluster-overview-sidebar-items.injectable.tsx @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { Icon } from "../icon"; +import React from "react"; +import { + SidebarItemRegistration, + sidebarItemsInjectionToken, +} from "../layout/sidebar-items.injectable"; +import { computed } from "mobx"; +import clusterOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/overview/cluster-overview-route.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToClusterOverviewInjectable from "../../../common/front-end-routing/routes/cluster/overview/navigate-to-cluster-overview.injectable"; + +const clusterOverviewSidebarItemsInjectable = getInjectable({ + id: "cluster-overview-sidebar-items", + + instantiate: (di) => { + const route = di.inject(clusterOverviewRouteInjectable); + const navigateToClusterOverview = di.inject(navigateToClusterOverviewInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed((): SidebarItemRegistration[] => [ + { + id: "cluster-overview", + parentId: null, + title: "Cluster", + getIcon: () => , + onClick: navigateToClusterOverview, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 10, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default clusterOverviewSidebarItemsInjectable; diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx index fcbd73875b..609138c965 100644 --- a/src/renderer/components/+cluster/cluster-overview.tsx +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -11,7 +11,7 @@ import { disposeOnUnmount, observer } from "mobx-react"; import { nodesStore } from "../+nodes/nodes.store"; import { podsStore } from "../+workloads-pods/pods.store"; import { Disposer, getHostedClusterId, interval } from "../../utils"; -import { TabLayout } from "../layout/tab-layout"; +import { TabLayout } from "../layout/tab-layout-2"; import { Spinner } from "../spinner"; import { ClusterIssues } from "./cluster-issues"; import { ClusterMetrics } from "./cluster-metrics"; diff --git a/src/renderer/components/+cluster/sidebar-item.tsx b/src/renderer/components/+cluster/sidebar-item.tsx deleted file mode 100644 index 597118eb02..0000000000 --- a/src/renderer/components/+cluster/sidebar-item.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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-autoscalers/horizontal-pod-auto-scalers-sidebar-items.injectable.tsx b/src/renderer/components/+config-autoscalers/horizontal-pod-auto-scalers-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..99bff705b8 --- /dev/null +++ b/src/renderer/components/+config-autoscalers/horizontal-pod-auto-scalers-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import horizontalPodAutoscalersRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/horizontal-pod-autoscalers-route.injectable"; +import { configSidebarItemId } from "../+config/config-sidebar-items.injectable"; +import { SidebarItemRegistration, sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToHorizontalPodAutoscalersInjectable from "../../../common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/navigate-to-horizontal-pod-autoscalers.injectable"; + +const horizontalPodAutoScalersSidebarItemsInjectable = getInjectable({ + id: "horizontal-pod-auto-scalers-sidebar-items", + + instantiate: (di) => { + const route = di.inject(horizontalPodAutoscalersRouteInjectable); + const navigateToHorizontalPodAutoscalers = di.inject(navigateToHorizontalPodAutoscalersInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed((): SidebarItemRegistration[] => [ + { + id: "horizontal-pod-auto-scalers", + parentId: configSidebarItemId, + title: "HPA", + onClick: navigateToHorizontalPodAutoscalers, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 50, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default horizontalPodAutoScalersSidebarItemsInjectable; diff --git a/src/renderer/components/+config-autoscalers/horizontal-pod-autoscalers-route-component.injectable.ts b/src/renderer/components/+config-autoscalers/horizontal-pod-autoscalers-route-component.injectable.ts new file mode 100644 index 0000000000..991f6cab1b --- /dev/null +++ b/src/renderer/components/+config-autoscalers/horizontal-pod-autoscalers-route-component.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 } from "@ogre-tools/injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import horizontalPodAutoscalersRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/horizontal-pod-autoscalers-route.injectable"; +import { HorizontalPodAutoscalers } from "./hpa"; + +const horizontalPodAutoscalersRouteComponentInjectable = getInjectable({ + id: "horizontal-pod-autoscalers-route-component", + + instantiate: (di) => ({ + route: di.inject(horizontalPodAutoscalersRouteInjectable), + Component: HorizontalPodAutoscalers, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default horizontalPodAutoscalersRouteComponentInjectable; diff --git a/src/renderer/components/+config-autoscalers/hpa.tsx b/src/renderer/components/+config-autoscalers/hpa.tsx index 0f99dd1d78..51866e491e 100644 --- a/src/renderer/components/+config-autoscalers/hpa.tsx +++ b/src/renderer/components/+config-autoscalers/hpa.tsx @@ -7,14 +7,13 @@ import "./hpa.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import type { HorizontalPodAutoscaler } from "../../../common/k8s-api/endpoints/hpa.api"; import { hpaStore } from "./hpa.store"; import { Badge } from "../badge"; import { cssNames } from "../../utils"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { HpaRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -28,11 +27,8 @@ enum columnId { status = "status", } -export interface HorizontalPodAutoscalersProps extends RouteComponentProps { -} - @observer -export class HorizontalPodAutoscalers extends React.Component { +export class HorizontalPodAutoscalers extends React.Component { getTargets(hpa: HorizontalPodAutoscaler) { const metrics = hpa.getMetrics(); @@ -47,57 +43,59 @@ export class HorizontalPodAutoscalers extends React.Component hpa.getName(), - [columnId.namespace]: hpa => hpa.getNs(), - [columnId.minPods]: hpa => hpa.getMinPods(), - [columnId.maxPods]: hpa => hpa.getMaxPods(), - [columnId.replicas]: hpa => hpa.getReplicas(), - [columnId.age]: hpa => -hpa.getCreationTimestamp(), - }} - searchFilters={[ - hpa => hpa.getSearchFields(), - ]} - renderHeaderTitle="Horizontal Pod Autoscalers" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Metrics", className: "metrics", id: columnId.metrics }, - { title: "Min Pods", className: "min-pods", sortBy: columnId.minPods, id: columnId.minPods }, - { title: "Max Pods", className: "max-pods", sortBy: columnId.maxPods, id: columnId.maxPods }, - { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - { title: "Status", className: "status scrollable", id: columnId.status }, - ]} - renderTableContents={hpa => [ - hpa.getName(), - , - hpa.getNs(), - this.getTargets(hpa), - hpa.getMinPods(), - hpa.getMaxPods(), - hpa.getReplicas(), - , - hpa.getConditions() - .filter(({ isReady }) => isReady) - .map(({ type, tooltip }) => ( - - )), - ]} - /> + + hpa.getName(), + [columnId.namespace]: hpa => hpa.getNs(), + [columnId.minPods]: hpa => hpa.getMinPods(), + [columnId.maxPods]: hpa => hpa.getMaxPods(), + [columnId.replicas]: hpa => hpa.getReplicas(), + [columnId.age]: hpa => -hpa.getCreationTimestamp(), + }} + searchFilters={[ + hpa => hpa.getSearchFields(), + ]} + renderHeaderTitle="Horizontal Pod Autoscalers" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Metrics", className: "metrics", id: columnId.metrics }, + { title: "Min Pods", className: "min-pods", sortBy: columnId.minPods, id: columnId.minPods }, + { title: "Max Pods", className: "max-pods", sortBy: columnId.maxPods, id: columnId.maxPods }, + { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status scrollable", id: columnId.status }, + ]} + renderTableContents={hpa => [ + hpa.getName(), + , + hpa.getNs(), + this.getTargets(hpa), + hpa.getMinPods(), + hpa.getMaxPods(), + hpa.getReplicas(), + , + hpa.getConditions() + .filter(({ isReady }) => isReady) + .map(({ type, tooltip }) => ( + + )), + ]} + /> + ); } } diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges-route-component.injectable.ts b/src/renderer/components/+config-limit-ranges/limit-ranges-route-component.injectable.ts new file mode 100644 index 0000000000..b79ffaa8f6 --- /dev/null +++ b/src/renderer/components/+config-limit-ranges/limit-ranges-route-component.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 } from "@ogre-tools/injectable"; +import { LimitRanges } from "./limit-ranges"; +import limitRangesRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/limit-ranges/limit-ranges-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const limitRangesRouteComponentInjectable = getInjectable({ + id: "limit-ranges-route-component", + + instantiate: (di) => ({ + route: di.inject(limitRangesRouteInjectable), + Component: LimitRanges, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default limitRangesRouteComponentInjectable; diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges-sidebar-items.injectable.tsx b/src/renderer/components/+config-limit-ranges/limit-ranges-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..f172cd2e8d --- /dev/null +++ b/src/renderer/components/+config-limit-ranges/limit-ranges-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import limitRangesRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/limit-ranges/limit-ranges-route.injectable"; +import { configSidebarItemId } from "../+config/config-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToLimitRangesInjectable from "../../../common/front-end-routing/routes/cluster/config/limit-ranges/navigate-to-limit-ranges.injectable"; + +const limitRangesSidebarItemsInjectable = getInjectable({ + id: "limit-ranges-sidebar-items", + + instantiate: (di) => { + const route = di.inject(limitRangesRouteInjectable); + const navigateToLimitRanges = di.inject(navigateToLimitRangesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "limit-ranges", + parentId: configSidebarItemId, + title: "Limit Ranges", + onClick: navigateToLimitRanges, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 40, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default limitRangesSidebarItemsInjectable; diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.tsx b/src/renderer/components/+config-limit-ranges/limit-ranges.tsx index 285e9533c7..8bf77cadc1 100644 --- a/src/renderer/components/+config-limit-ranges/limit-ranges.tsx +++ b/src/renderer/components/+config-limit-ranges/limit-ranges.tsx @@ -5,13 +5,12 @@ import "./limit-ranges.scss"; -import type { RouteComponentProps } from "react-router"; import { observer } from "mobx-react"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { limitRangeStore } from "./limit-ranges.store"; import React from "react"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { LimitRangeRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -20,41 +19,40 @@ enum columnId { age = "age", } -export interface LimitRangesProps extends RouteComponentProps { -} - @observer -export class LimitRanges extends React.Component { +export class LimitRanges extends React.Component { render() { return ( - limitRange.getName(), - [columnId.namespace]: limitRange => limitRange.getNs(), - [columnId.age]: limitRange => -limitRange.getCreationTimestamp(), - }} - searchFilters={[ - item => item.getName(), - item => item.getNs(), - ]} - renderHeaderTitle="Limit Ranges" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={limitRange => [ - limitRange.getName(), - , - limitRange.getNs(), - , - ]} - /> + + limitRange.getName(), + [columnId.namespace]: limitRange => limitRange.getNs(), + [columnId.age]: limitRange => -limitRange.getCreationTimestamp(), + }} + searchFilters={[ + item => item.getName(), + item => item.getNs(), + ]} + renderHeaderTitle="Limit Ranges" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={limitRange => [ + limitRange.getName(), + , + limitRange.getNs(), + , + ]} + /> + ); } } diff --git a/src/renderer/components/+config-maps/config-maps-route-component.injectable.ts b/src/renderer/components/+config-maps/config-maps-route-component.injectable.ts new file mode 100644 index 0000000000..fd9c7df320 --- /dev/null +++ b/src/renderer/components/+config-maps/config-maps-route-component.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 } from "@ogre-tools/injectable"; +import { ConfigMaps } from "./config-maps"; +import configMapsRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/config-maps/config-maps-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const configMapsRouteComponentInjectable = getInjectable({ + id: "config-maps-route-component", + + instantiate: (di) => ({ + route: di.inject(configMapsRouteInjectable), + Component: ConfigMaps, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default configMapsRouteComponentInjectable; diff --git a/src/renderer/components/+config-maps/config-maps-sidebar-items.injectable.tsx b/src/renderer/components/+config-maps/config-maps-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..638273b128 --- /dev/null +++ b/src/renderer/components/+config-maps/config-maps-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import configMapsRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/config-maps/config-maps-route.injectable"; +import { configSidebarItemId } from "../+config/config-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToConfigMapsInjectable from "../../../common/front-end-routing/routes/cluster/config/config-maps/navigate-to-config-maps.injectable"; + +const configMapsSidebarItemsInjectable = getInjectable({ + id: "config-maps-sidebar-items", + + instantiate: (di) => { + const route = di.inject(configMapsRouteInjectable); + const navigateToConfigMaps = di.inject(navigateToConfigMapsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "config-maps", + parentId: configSidebarItemId, + title: "ConfigMaps", + onClick: navigateToConfigMaps, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 10, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default configMapsSidebarItemsInjectable; diff --git a/src/renderer/components/+config-maps/config-maps.tsx b/src/renderer/components/+config-maps/config-maps.tsx index 3dc189560e..a7dc001eea 100644 --- a/src/renderer/components/+config-maps/config-maps.tsx +++ b/src/renderer/components/+config-maps/config-maps.tsx @@ -7,11 +7,10 @@ import "./config-maps.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import { configMapsStore } from "./config-maps.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { ConfigMapsRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -21,44 +20,43 @@ enum columnId { age = "age", } -export interface ConfigMapsProps extends RouteComponentProps { -} - @observer -export class ConfigMaps extends React.Component { +export class ConfigMaps extends React.Component { render() { return ( - configMap.getName(), - [columnId.namespace]: configMap => configMap.getNs(), - [columnId.keys]: configMap => configMap.getKeys(), - [columnId.age]: configMap => -configMap.getCreationTimestamp(), - }} - searchFilters={[ - configMap => configMap.getSearchFields(), - configMap => configMap.getKeys(), - ]} - renderHeaderTitle="Config Maps" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Keys", className: "keys", sortBy: columnId.keys, id: columnId.keys }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={configMap => [ - configMap.getName(), - , - configMap.getNs(), - configMap.getKeys().join(", "), - , - ]} - /> + + configMap.getName(), + [columnId.namespace]: configMap => configMap.getNs(), + [columnId.keys]: configMap => configMap.getKeys(), + [columnId.age]: configMap => -configMap.getCreationTimestamp(), + }} + searchFilters={[ + configMap => configMap.getSearchFields(), + configMap => configMap.getKeys(), + ]} + renderHeaderTitle="Config Maps" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Keys", className: "keys", sortBy: columnId.keys, id: columnId.keys }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={configMap => [ + configMap.getName(), + , + configMap.getNs(), + configMap.getKeys().join(", "), + , + ]} + /> + ); } } diff --git a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-route-component.injectable.ts b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-route-component.injectable.ts new file mode 100644 index 0000000000..e963b9408e --- /dev/null +++ b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-route-component.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 } from "@ogre-tools/injectable"; +import { PodDisruptionBudgets } from "./pod-disruption-budgets"; +import podDisruptionBudgetsRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/pod-disruption-budgets/pod-disruption-budgets-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const podDisruptionBudgetsRouteComponentInjectable = getInjectable({ + id: "pod-disruption-budgets-route-component", + + instantiate: (di) => ({ + route: di.inject(podDisruptionBudgetsRouteInjectable), + Component: PodDisruptionBudgets, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default podDisruptionBudgetsRouteComponentInjectable; diff --git a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-sidebar-items.injectable.tsx b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..a4e6092b33 --- /dev/null +++ b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import podDisruptionBudgetsRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/pod-disruption-budgets/pod-disruption-budgets-route.injectable"; +import { configSidebarItemId } from "../+config/config-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToPodDisruptionBudgetsInjectable from "../../../common/front-end-routing/routes/cluster/config/pod-disruption-budgets/navigate-to-pod-disruption-budgets.injectable"; + +const podDisruptionBudgetsSidebarItemsInjectable = getInjectable({ + id: "pod-disruption-budgets-sidebar-items", + + instantiate: (di) => { + const route = di.inject(podDisruptionBudgetsRouteInjectable); + const navigateToPodDisruptionBudgets = di.inject(navigateToPodDisruptionBudgetsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "pod-disruption-budgets", + parentId: configSidebarItemId, + title: "Pod Disruption Budgets", + onClick: navigateToPodDisruptionBudgets, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 60, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default podDisruptionBudgetsSidebarItemsInjectable; diff --git a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx index 74f685df59..f9871f646a 100644 --- a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx +++ b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx @@ -12,6 +12,7 @@ import type { PodDisruptionBudget } from "../../../common/k8s-api/endpoints/podd import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import type { KubeObjectDetailsProps } from "../kube-object-details"; import { KubeObjectListLayout } from "../kube-object-list-layout"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -31,45 +32,47 @@ export interface PodDisruptionBudgetsProps extends KubeObjectDetailsProps { render() { return ( - pdb.getName(), - [columnId.namespace]: pdb => pdb.getNs(), - [columnId.minAvailable]: pdb => pdb.getMinAvailable(), - [columnId.maxUnavailable]: pdb => pdb.getMaxUnavailable(), - [columnId.currentHealthy]: pdb => pdb.getCurrentHealthy(), - [columnId.desiredHealthy]: pdb => pdb.getDesiredHealthy(), - [columnId.age]: pdb => -pdb.getCreationTimestamp(), - }} - searchFilters={[ - pdb => pdb.getSearchFields(), - ]} - renderHeaderTitle="Pod Disruption Budgets" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Min Available", className: "min-available", sortBy: columnId.minAvailable, id: columnId.minAvailable }, - { title: "Max Unavailable", className: "max-unavailable", sortBy: columnId.maxUnavailable, id: columnId.maxUnavailable }, - { title: "Current Healthy", className: "current-healthy", sortBy: columnId.currentHealthy, id: columnId.currentHealthy }, - { title: "Desired Healthy", className: "desired-healthy", sortBy: columnId.desiredHealthy, id: columnId.desiredHealthy }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={pdb => [ - pdb.getName(), - , - pdb.getNs(), - pdb.getMinAvailable(), - pdb.getMaxUnavailable(), - pdb.getCurrentHealthy(), - pdb.getDesiredHealthy(), - , - ]} - /> + + pdb.getName(), + [columnId.namespace]: pdb => pdb.getNs(), + [columnId.minAvailable]: pdb => pdb.getMinAvailable(), + [columnId.maxUnavailable]: pdb => pdb.getMaxUnavailable(), + [columnId.currentHealthy]: pdb => pdb.getCurrentHealthy(), + [columnId.desiredHealthy]: pdb => pdb.getDesiredHealthy(), + [columnId.age]: pdb => -pdb.getCreationTimestamp(), + }} + searchFilters={[ + pdb => pdb.getSearchFields(), + ]} + renderHeaderTitle="Pod Disruption Budgets" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Min Available", className: "min-available", sortBy: columnId.minAvailable, id: columnId.minAvailable }, + { title: "Max Unavailable", className: "max-unavailable", sortBy: columnId.maxUnavailable, id: columnId.maxUnavailable }, + { title: "Current Healthy", className: "current-healthy", sortBy: columnId.currentHealthy, id: columnId.currentHealthy }, + { title: "Desired Healthy", className: "desired-healthy", sortBy: columnId.desiredHealthy, id: columnId.desiredHealthy }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={pdb => [ + pdb.getName(), + , + pdb.getNs(), + pdb.getMinAvailable(), + pdb.getMaxUnavailable(), + pdb.getCurrentHealthy(), + pdb.getDesiredHealthy(), + , + ]} + /> + ); } } diff --git a/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx b/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx index 6746e28fef..e292fd1163 100644 --- a/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx +++ b/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx @@ -168,6 +168,7 @@ export class AddQuotaDialog extends React.Component { {

this.type = value} diff --git a/src/renderer/components/+config-secrets/secrets-route-component.injectable.ts b/src/renderer/components/+config-secrets/secrets-route-component.injectable.ts new file mode 100644 index 0000000000..35dc02d5fe --- /dev/null +++ b/src/renderer/components/+config-secrets/secrets-route-component.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 } from "@ogre-tools/injectable"; +import { Secrets } from "./secrets"; +import secretsRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/secrets/secrets-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const secretsRouteComponentInjectable = getInjectable({ + id: "secrets-route-component", + + instantiate: (di) => ({ + route: di.inject(secretsRouteInjectable), + Component: Secrets, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default secretsRouteComponentInjectable; diff --git a/src/renderer/components/+config-secrets/secrets-sidebar-items.injectable.tsx b/src/renderer/components/+config-secrets/secrets-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..cf8a07dc66 --- /dev/null +++ b/src/renderer/components/+config-secrets/secrets-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import secretsRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/secrets/secrets-route.injectable"; +import { configSidebarItemId } from "../+config/config-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToSecretsInjectable from "../../../common/front-end-routing/routes/cluster/config/secrets/navigate-to-secrets.injectable"; + +const secretsSidebarItemsInjectable = getInjectable({ + id: "secrets-sidebar-items", + + instantiate: (di) => { + const route = di.inject(secretsRouteInjectable); + const navigateToSecrets = di.inject(navigateToSecretsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "secrets", + parentId: configSidebarItemId, + title: "Secrets", + onClick: navigateToSecrets, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 20, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default secretsSidebarItemsInjectable; diff --git a/src/renderer/components/+config-secrets/secrets.tsx b/src/renderer/components/+config-secrets/secrets.tsx index 9df906187e..e519150007 100644 --- a/src/renderer/components/+config-secrets/secrets.tsx +++ b/src/renderer/components/+config-secrets/secrets.tsx @@ -7,13 +7,12 @@ import "./secrets.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import { AddSecretDialog } from "./add-secret-dialog"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { Badge } from "../badge"; import { secretsStore } from "./secrets.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { SecretsRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -25,14 +24,11 @@ enum columnId { age = "age", } -export interface SecretsProps extends RouteComponentProps { -} - @observer -export class Secrets extends React.Component { +export class Secrets extends React.Component { render() { return ( - <> + { }} /> - + ); } } diff --git a/src/renderer/components/+config/config-sidebar-items.injectable.tsx b/src/renderer/components/+config/config-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..60d1820635 --- /dev/null +++ b/src/renderer/components/+config/config-sidebar-items.injectable.tsx @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { Icon } from "../icon"; +import React from "react"; +import { + SidebarItemRegistration, + sidebarItemsInjectionToken, +} from "../layout/sidebar-items.injectable"; +import { computed } from "mobx"; +import { noop } from "lodash/fp"; + +export const configSidebarItemId = "config"; + +const configSidebarItemsInjectable = getInjectable({ + id: "config-sidebar-items", + + instantiate: () => + computed((): SidebarItemRegistration[] => [ + { + id: configSidebarItemId, + parentId: null, + title: "Config", + getIcon: () => , + onClick: noop, + orderNumber: 40, + }, + ]), + + injectionToken: sidebarItemsInjectionToken, +}); + +export default configSidebarItemsInjectable; diff --git a/src/renderer/components/+config/route-tabs.injectable.ts b/src/renderer/components/+config/route-tabs.injectable.ts deleted file mode 100644 index 91bf531eff..0000000000 --- a/src/renderer/components/+config/route-tabs.injectable.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { computed } from "mobx"; -import { HorizontalPodAutoscalers } from "../+config-autoscalers"; -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"; - -interface Dependencies { - isAllowedResource: IsAllowedResource; -} - -function getRouteTabs({ isAllowedResource }: Dependencies) { - return computed(() => { - const tabs: TabLayoutRoute[] = []; - - if (isAllowedResource("configmaps")) { - tabs.push({ - title: "ConfigMaps", - component: ConfigMaps, - url: routes.configMapsURL(), - routePath: routes.configMapsRoute.path.toString(), - }); - } - - if (isAllowedResource("secrets")) { - tabs.push({ - title: "Secrets", - component: Secrets, - url: routes.secretsURL(), - routePath: routes.secretsRoute.path.toString(), - }); - } - - if (isAllowedResource("resourcequotas")) { - tabs.push({ - title: "Resource Quotas", - component: ResourceQuotas, - url: routes.resourceQuotaURL(), - routePath: routes.resourceQuotaRoute.path.toString(), - }); - } - - if (isAllowedResource("limitranges")) { - tabs.push({ - title: "Limit Ranges", - component: LimitRanges, - url: routes.limitRangeURL(), - routePath: routes.limitRangesRoute.path.toString(), - }); - } - - if (isAllowedResource("horizontalpodautoscalers")) { - tabs.push({ - title: "HPA", - component: HorizontalPodAutoscalers, - url: routes.hpaURL(), - routePath: routes.hpaRoute.path.toString(), - }); - } - - if (isAllowedResource("poddisruptionbudgets")) { - tabs.push({ - title: "Pod Disruption Budgets", - component: PodDisruptionBudgets, - url: routes.pdbURL(), - routePath: routes.pdbRoute.path.toString(), - }); - } - - return tabs; - }); -} - -const configRoutesInjectable = getInjectable({ - id: "config-routes", - - instantiate: (di) => getRouteTabs({ - isAllowedResource: di.inject(isAllowedResourceInjectable), - }), -}); - -export default configRoutesInjectable; diff --git a/src/renderer/components/+config/route.tsx b/src/renderer/components/+config/route.tsx deleted file mode 100644 index 44b0934bc6..0000000000 --- a/src/renderer/components/+config/route.tsx +++ /dev/null @@ -1,31 +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 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 deleted file mode 100644 index 4bd8d7b719..0000000000 --- a/src/renderer/components/+config/sidebar-item.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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 { renderTabRoutesSidebarItems } 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 ( - } - > - {renderTabRoutesSidebarItems(tabRoutes)} - - ); -}); - -export const ConfigSidebarItem = withInjectables(NonInjectedConfigSidebarItem, { - getProps: (di, props) => ({ - routes: di.inject(configRoutesInjectable), - ...props, - }), -}); diff --git a/src/renderer/components/+custom-resources/crd-list-route-component.injectable.ts b/src/renderer/components/+custom-resources/crd-list-route-component.injectable.ts new file mode 100644 index 0000000000..55e4a21006 --- /dev/null +++ b/src/renderer/components/+custom-resources/crd-list-route-component.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 } from "@ogre-tools/injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import crdListRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/crd-list/crd-list-route.injectable"; +import { CustomResourceDefinitions } from "./crd-list"; + +const crdListRouteComponentInjectable = getInjectable({ + id: "crd-list-route-component", + + instantiate: (di) => ({ + route: di.inject(crdListRouteInjectable), + Component: CustomResourceDefinitions, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default crdListRouteComponentInjectable; diff --git a/src/renderer/components/+custom-resources/crd-list.tsx b/src/renderer/components/+custom-resources/crd-list.tsx index 8a3277ce21..766559ad18 100644 --- a/src/renderer/components/+custom-resources/crd-list.tsx +++ b/src/renderer/components/+custom-resources/crd-list.tsx @@ -16,6 +16,7 @@ import { Select, SelectOption } from "../select"; import { createPageParam } from "../../navigation"; import { Icon } from "../icon"; import { KubeObjectAge } from "../kube-object/age"; +import { TabLayout } from "../layout/tab-layout-2"; export const crdGroupsUrlParam = createPageParam({ name: "groups", @@ -64,81 +65,84 @@ export class CustomResourceDefinitions extends React.Component { const { items, selectedGroups } = this; return ( - already has and is always mounted - subscribeStores={false} - items={items} - sortingCallbacks={{ - [columnId.kind]: crd => crd.getResourceKind(), - [columnId.group]: crd => crd.getGroup(), - [columnId.version]: crd => crd.getVersion(), - [columnId.scope]: crd => crd.getScope(), - [columnId.age]: crd => -crd.getCreationTimestamp(), - }} - searchFilters={[ - crd => crd.getResourceKind(), - crd => crd.getGroup(), - crd => crd.getVersion(), - crd => crd.getScope(), - crd => -crd.getCreationTimestamp(), - ]} - renderHeaderTitle="Custom Resources" - customizeHeader={({ filters, ...headerPlaceholders }) => { - let placeholder = <>All groups; + + already has and is always mounted + subscribeStores={false} + items={items} + sortingCallbacks={{ + [columnId.kind]: crd => crd.getResourceKind(), + [columnId.group]: crd => crd.getGroup(), + [columnId.version]: crd => crd.getVersion(), + [columnId.scope]: crd => crd.getScope(), + [columnId.age]: crd => -crd.getCreationTimestamp(), + }} + searchFilters={[ + crd => crd.getResourceKind(), + crd => crd.getGroup(), + crd => crd.getVersion(), + crd => crd.getScope(), + crd => -crd.getCreationTimestamp(), + ]} + renderHeaderTitle="Custom Resources" + customizeHeader={({ filters, ...headerPlaceholders }) => { + let placeholder = <>All groups; - if (selectedGroups.length == 1) placeholder = <>Group: {selectedGroups[0]}; - if (selectedGroups.length >= 2) placeholder = <>Groups: {selectedGroups.join(", ")}; + if (selectedGroups.length == 1) placeholder = <>Group: {selectedGroups[0]}; + if (selectedGroups.length >= 2) placeholder = <>Groups: {selectedGroups.join(", ")}; - return { - // todo: move to global filters - filters: ( - <> - {filters} - this.toggleSelection(group)} + closeMenuOnSelect={false} + controlShouldRenderValue={false} + formatOptionLabel={({ value: group }: SelectOption) => { + const isSelected = selectedGroups.includes(group); - return ( -
- - {group} - {isSelected && } -
- ); - }} - /> - - ), - ...headerPlaceholders, - }; - }} - renderTableHeader={[ - { title: "Resource", className: "kind", sortBy: columnId.kind, id: columnId.kind }, - { title: "Group", className: "group", sortBy: columnId.group, id: columnId.group }, - { title: "Version", className: "version", sortBy: columnId.version, id: columnId.version }, - { title: "Scope", className: "scope", sortBy: columnId.scope, id: columnId.scope }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={crd => [ - - {crd.getResourceKind()} - , - crd.getGroup(), - crd.getVersion(), - crd.getScope(), - , - ]} - /> + return ( +
+ + {group} + {isSelected && } +
+ ); + }} + /> + + ), + ...headerPlaceholders, + }; + }} + renderTableHeader={[ + { title: "Resource", className: "kind", sortBy: columnId.kind, id: columnId.kind }, + { title: "Group", className: "group", sortBy: columnId.group, id: columnId.group }, + { title: "Version", className: "version", sortBy: columnId.version, id: columnId.version }, + { title: "Scope", className: "scope", sortBy: columnId.scope, id: columnId.scope }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={crd => [ + + {crd.getResourceKind()} + , + crd.getGroup(), + crd.getVersion(), + crd.getScope(), + , + ]} + /> +
); } } diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index 0669f5ff86..25601b1112 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -8,35 +8,36 @@ import "./crd-resources.scss"; import React from "react"; import { value } from "jsonpath"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import { KubeObjectListLayout } from "../kube-object-list-layout"; -import { computed, makeObservable } from "mobx"; +import { computed, IComputedValue, makeObservable } from "mobx"; import { crdStore } from "./crd.store"; import { apiManager } from "../../../common/k8s-api/api-manager"; import { parseJsonPath } from "../../utils/jsonPath"; -import type { CRDRouteParams } from "../../../common/routes"; +import { TabLayout } from "../layout/tab-layout-2"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import customResourcesRouteParametersInjectable from "./custom-resources-route-parameters.injectable"; import { KubeObjectAge } from "../kube-object/age"; -export interface CustomResourceDefinitionResourcesProps extends RouteComponentProps { -} - enum columnId { name = "name", namespace = "namespace", age = "age", } +interface Dependencies { + group: IComputedValue; + name: IComputedValue; +} + @observer -export class CustomResourceDefinitionResources extends React.Component { - constructor(props: CustomResourceDefinitionResourcesProps) { +class NonInjectedCrdResources extends React.Component { + constructor(props: Dependencies) { super(props); makeObservable(this); } @computed get crd() { - const { group, name } = this.props.match.params; - - return crdStore.getByGroup(group, name); + return crdStore.getByGroup(this.props.group.get(), this.props.name.get()); } @computed get store() { @@ -55,69 +56,87 @@ export class CustomResourceDefinitionResources extends React.Component customResource.getName(), - [columnId.namespace]: customResource => customResource.getNs(), - [columnId.age]: customResource => -customResource.getCreationTimestamp(), - ...Object.fromEntries(extraColumns.map(({ name, jsonPath }) => [ - name, - customResource => value(customResource, parseJsonPath(jsonPath.slice(1))), - ])), - }} - searchFilters={[ - customResource => customResource.getSearchFields(), - ]} - renderHeaderTitle={crd.getResourceKind()} - customizeHeader={({ searchProps, ...headerPlaceholders }) => ({ - searchProps: { - ...searchProps, - placeholder: `${crd.getResourceKind()} search ...`, - }, - ...headerPlaceholders, - })} - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - ...extraColumns.map(({ name }) => ({ - title: name, - className: name.toLowerCase(), - sortBy: name, - id: name, - })), - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={crdInstance => [ - crdInstance.getName(), - isNamespaced && crdInstance.getNs(), - ...extraColumns.map((column) => { - let rawValue = value(crdInstance, parseJsonPath(column.jsonPath.slice(1))); + + customResource.getName(), + [columnId.namespace]: customResource => customResource.getNs(), + [columnId.age]: customResource => -customResource.getCreationTimestamp(), + ...Object.fromEntries(extraColumns.map(({ name, jsonPath }) => [ + name, + customResource => value(customResource, parseJsonPath(jsonPath.slice(1))), + ])), + }} + searchFilters={[ + customResource => customResource.getSearchFields(), + ]} + renderHeaderTitle={crd.getResourceKind()} + customizeHeader={({ searchProps, ...headerPlaceholders }) => ({ + searchProps: { + ...searchProps, + placeholder: `${crd.getResourceKind()} search ...`, + }, + ...headerPlaceholders, + })} + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + ...extraColumns.map(({ name }) => ({ + title: name, + className: name.toLowerCase(), + sortBy: name, + id: name, + })), + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={crdInstance => [ + crdInstance.getName(), + isNamespaced && crdInstance.getNs(), + ...extraColumns.map((column) => { + let rawValue = value(crdInstance, parseJsonPath(column.jsonPath.slice(1))); - if (Array.isArray(rawValue) || typeof rawValue === "object") { - rawValue = JSON.stringify(rawValue); - } + if (Array.isArray(rawValue) || typeof rawValue === "object") { + rawValue = JSON.stringify(rawValue); + } - return { - renderBoolean: true, - children: rawValue, - }; - }), - , - ]} - failedToLoadMessage={( - <> -

Failed to load {crd.getPluralName()}

- {!version.served && ( -

Prefered version ({crd.getGroup()}/{version.name}) is not served

- )} - - )} - /> + return { + renderBoolean: true, + children: rawValue, + }; + }), + , + ]} + failedToLoadMessage={( + <> +

Failed to load {crd.getPluralName()}

+ {!version.served && ( +

Prefered version ({crd.getGroup()}/{version.name}) is not served

+ )} + + )} + /> +
); } } + +export const CrdResources = withInjectables( + NonInjectedCrdResources, + + { + getProps: (di) => { + const routeParameters = di.inject(customResourcesRouteParametersInjectable); + + return { + group: routeParameters.group, + name: routeParameters.name, + }; + }, + }, +); + diff --git a/src/renderer/components/+custom-resources/custom-resource-sidebar-items.injectable.tsx b/src/renderer/components/+custom-resources/custom-resource-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..d9908d6260 --- /dev/null +++ b/src/renderer/components/+custom-resources/custom-resource-sidebar-items.injectable.tsx @@ -0,0 +1,68 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { noop, some } from "lodash/fp"; +import { computed } from "mobx"; + +import { + SidebarItemRegistration, + sidebarItemsInjectionToken, +} from "../layout/sidebar-items.injectable"; +import { Icon } from "../icon"; +import React from "react"; + +import crdListRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/crd-list/crd-list-route.injectable"; +import sidebarItemsForDefinitionGroupsInjectable from "./sidebar-items-for-definition-groups.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToCrdListInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/crd-list/navigate-to-crd-list.injectable"; + +const customResourceSidebarItemsInjectable = getInjectable({ + id: "custom-resource-sidebar-items", + + instantiate: (di) => { + const navigateToCrdList = di.inject(navigateToCrdListInjectable); + const crdListRoute = di.inject(crdListRouteInjectable); + const crdListRouteIsActive = di.inject(routeIsActiveInjectable, crdListRoute); + + const definitionGroupSidebarItems = di.inject( + sidebarItemsForDefinitionGroupsInjectable, + ); + + return computed((): SidebarItemRegistration[] => { + const definitionsItem = { + id: "definitions", + parentId: "custom-resources", + title: "Definitions", + onClick: navigateToCrdList, + isActive: crdListRouteIsActive, + isVisible: crdListRoute.isEnabled, + orderNumber: 10, + }; + + const definitionGroupItems = definitionGroupSidebarItems.get(); + + const childrenAndGrandChildren = [ + definitionsItem, + ...definitionGroupItems, + ]; + + const parentItem: SidebarItemRegistration = { + id: "custom-resources", + parentId: null, + title: "Custom Resources", + getIcon: () => , + onClick: noop, + isVisible: computed(() => some(item => item.isVisible.get(), childrenAndGrandChildren)), + orderNumber: 110, + }; + + return [parentItem, definitionsItem, ...definitionGroupItems]; + }); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default customResourceSidebarItemsInjectable; diff --git a/src/renderer/components/+custom-resources/custom-resources-route-component.injectable.ts b/src/renderer/components/+custom-resources/custom-resources-route-component.injectable.ts new file mode 100644 index 0000000000..1df50fe5df --- /dev/null +++ b/src/renderer/components/+custom-resources/custom-resources-route-component.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 } from "@ogre-tools/injectable"; +import { CrdResources } from "./crd-resources"; +import customResourcesRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const customResourcesRouteComponentInjectable = getInjectable({ + id: "custom-resources-route-component", + + instantiate: (di) => ({ + route: di.inject(customResourcesRouteInjectable), + Component: CrdResources, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default customResourcesRouteComponentInjectable; diff --git a/src/renderer/components/+custom-resources/custom-resources-route-parameters.injectable.ts b/src/renderer/components/+custom-resources/custom-resources-route-parameters.injectable.ts new file mode 100644 index 0000000000..2debbe85bd --- /dev/null +++ b/src/renderer/components/+custom-resources/custom-resources-route-parameters.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import routePathParametersInjectable from "../../routes/route-path-parameters.injectable"; +import customResourcesRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable"; + +const customResourcesRouteParametersInjectable = getInjectable({ + id: "custom-resources-route-parameters", + + instantiate: (di) => { + const route = di.inject(customResourcesRouteInjectable); + const pathParameters = di.inject(routePathParametersInjectable, route); + + return { + group: computed(() => pathParameters.get().group), + name: computed(() => pathParameters.get().name), + }; + }, +}); + +export default customResourcesRouteParametersInjectable; diff --git a/src/renderer/components/+custom-resources/custom-resources.injectable.ts b/src/renderer/components/+custom-resources/custom-resources.injectable.ts index c35a278afa..e1346dcf2b 100644 --- a/src/renderer/components/+custom-resources/custom-resources.injectable.ts +++ b/src/renderer/components/+custom-resources/custom-resources.injectable.ts @@ -6,11 +6,23 @@ import { getInjectable } from "@ogre-tools/injectable"; import { computed } from "mobx"; import { crdStore } from "./crd.store"; +import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; +import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable"; const customResourceDefinitionsInjectable = getInjectable({ id: "custom-resource-definitions", - instantiate: () => computed(() => [...crdStore.items]), + instantiate: (di) => { + const currentlyInClusterFrame = di.inject(currentlyInClusterFrameInjectable); + + if (currentlyInClusterFrame) { + const subscribeStores = di.inject(subscribeStoresInjectable); + + subscribeStores([crdStore]); + } + + return computed(() => [...crdStore.items]); + }, }); export default customResourceDefinitionsInjectable; diff --git a/src/renderer/components/+custom-resources/grouped-custom-resources.injectable.ts b/src/renderer/components/+custom-resources/grouped-custom-resources.injectable.ts deleted file mode 100644 index 0c06b6061b..0000000000 --- a/src/renderer/components/+custom-resources/grouped-custom-resources.injectable.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { 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({ - id: "grouped-custom-resource-definitions", - - instantiate: (di) => getGroupedCustomResourceDefinitions({ - definitions: di.inject(customResourceDefinitionsInjectable), - }), -}); - -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 deleted file mode 100644 index 87fd4b25d3..0000000000 --- a/src/renderer/components/+custom-resources/route-tabs.injectable.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { 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 groupedCustomResourceDefinitionsInjectable from "./grouped-custom-resources.injectable"; - -export interface CustomResourceTabLayoutRoute extends Omit { - 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", - 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 }}), - subRoutes: definitions.map(crd => ({ - id: `crd-resource:${crd.getResourceApiBase()}`, - title: crd.getResourceKind(), - routePath: crd.getResourceUrl(), - })), - }); - } - - return tabs; - }); -} - -const customResourcesRouteTabsInjectable = getInjectable({ - id: "custom-resources-route-tabs", - - instantiate: (di) => getRouteTabs({ - customResourcesDefinitions: di.inject(groupedCustomResourceDefinitionsInjectable), - }), -}); - -export default customResourcesRouteTabsInjectable; diff --git a/src/renderer/components/+custom-resources/route.tsx b/src/renderer/components/+custom-resources/route.tsx deleted file mode 100644 index 805d6216d6..0000000000 --- a/src/renderer/components/+custom-resources/route.tsx +++ /dev/null @@ -1,22 +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 { Redirect, Route, Switch } from "react-router"; -import { TabLayout } from "../layout/tab-layout"; -import { crdDefinitionsRoute, crdResourcesRoute, crdURL } from "../../../common/routes"; -import { CustomResourceDefinitions } from "./crd-list"; -import { CustomResourceDefinitionResources } from "./crd-resources"; - -export const CustomResourcesRoute = () => ( - - - - - - - -); - diff --git a/src/renderer/components/+custom-resources/sidebar-item.tsx b/src/renderer/components/+custom-resources/sidebar-item.tsx deleted file mode 100644 index 40fb76b34a..0000000000 --- a/src/renderer/components/+custom-resources/sidebar-item.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/** - * 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/+custom-resources/sidebar-items-for-definition-groups.injectable.ts b/src/renderer/components/+custom-resources/sidebar-items-for-definition-groups.injectable.ts new file mode 100644 index 0000000000..f05e5df43a --- /dev/null +++ b/src/renderer/components/+custom-resources/sidebar-items-for-definition-groups.injectable.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import crdListRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/crd-list/crd-list-route.injectable"; +import customResourceDefinitionsInjectable from "./custom-resources.injectable"; +import { groupBy, matches, noop, some, toPairs } from "lodash/fp"; +import customResourcesRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable"; +import currentPathParametersInjectable from "../../routes/current-path-parameters.injectable"; +import type { SidebarItemRegistration } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToCustomResourcesInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/custom-resources/navigate-to-custom-resources.injectable"; + +const sidebarItemsForDefinitionGroupsInjectable = getInjectable({ + id: "sidebar-items-for-definition-groups", + + instantiate: (di) => { + const customResourceDefinitions = di.inject( + customResourceDefinitionsInjectable, + ); + + const crdRoute = di.inject(customResourcesRouteInjectable); + const crdRouteIsActive = di.inject(routeIsActiveInjectable, crdRoute); + const crdListRoute = di.inject(crdListRouteInjectable); + const pathParameters = di.inject(currentPathParametersInjectable); + const navigateToCustomResources = di.inject(navigateToCustomResourcesInjectable); + + return computed((): SidebarItemRegistration[] => { + const definitions = customResourceDefinitions.get(); + + const groupedCrds = toPairs( + groupBy((crd) => crd.getGroup(), definitions), + ); + + return groupedCrds.flatMap(([group, definitions]) => { + const childItems = definitions.map((crd) => { + const title = crd.getResourceKind(); + + const crdPathParameters = { + group: crd.getGroup(), + name: crd.getPluralName(), + }; + + return { + id: `custom-resource-definition-group-${group}-crd-${crd.getId()}`, + parentId: `custom-resource-definition-group-${group}`, + title, + + onClick: () => navigateToCustomResources(crdPathParameters), + + isActive: computed( + () => + crdRouteIsActive.get() && + matches(crdPathParameters, pathParameters.get()), + ), + + isVisible: crdListRoute.isEnabled, + orderNumber: 10, + }; + }); + + return [ + { + id: `custom-resource-definition-group-${group}`, + parentId: "custom-resources", + title: group, + onClick: noop, + isVisible: computed(() => some(item => item.isVisible.get(), childItems)), + orderNumber: 10, + }, + + ...childItems, + ]; + }); + }); + }, +}); + +export default sidebarItemsForDefinitionGroupsInjectable; diff --git a/src/renderer/components/+entity-settings/entity-settings-route-component.injectable.ts b/src/renderer/components/+entity-settings/entity-settings-route-component.injectable.ts new file mode 100644 index 0000000000..5e3a2da43f --- /dev/null +++ b/src/renderer/components/+entity-settings/entity-settings-route-component.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 } from "@ogre-tools/injectable"; +import { EntitySettings } from "./entity-settings"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import entitySettingsRouteInjectable from "../../../common/front-end-routing/routes/entity-settings/entity-settings-route.injectable"; + +const entitySettingsRouteComponentInjectable = getInjectable({ + id: "entity-settings-route-component", + + instantiate: (di) => ({ + route: di.inject(entitySettingsRouteInjectable), + Component: EntitySettings, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default entitySettingsRouteComponentInjectable; diff --git a/src/renderer/components/+entity-settings/entity-settings-route-parameters.injectable.ts b/src/renderer/components/+entity-settings/entity-settings-route-parameters.injectable.ts new file mode 100644 index 0000000000..8a1d5600b8 --- /dev/null +++ b/src/renderer/components/+entity-settings/entity-settings-route-parameters.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import routePathParametersInjectable from "../../routes/route-path-parameters.injectable"; +import entitySettingsRouteInjectable from "../../../common/front-end-routing/routes/entity-settings/entity-settings-route.injectable"; + +const entitySettingsRouteParametersInjectable = getInjectable({ + id: "entity-settings-route-parameters", + + instantiate: (di) => { + const route = di.inject(entitySettingsRouteInjectable); + const pathParameters = di.inject(routePathParametersInjectable, route); + + return { + entityId: computed(() => pathParameters.get().entityId), + }; + }, +}); + +export default entitySettingsRouteParametersInjectable; diff --git a/src/renderer/components/+entity-settings/entity-settings.tsx b/src/renderer/components/+entity-settings/entity-settings.tsx index 9b9df16acf..5ec917ceb3 100644 --- a/src/renderer/components/+entity-settings/entity-settings.tsx +++ b/src/renderer/components/+entity-settings/entity-settings.tsx @@ -6,28 +6,29 @@ import styles from "./entity-settings.module.scss"; import React from "react"; -import { observable, makeObservable } from "mobx"; -import type { RouteComponentProps } from "react-router"; +import { observable, makeObservable, IComputedValue, computed } from "mobx"; import { observer } from "mobx-react"; import { navigation } from "../../navigation"; import { Tabs, Tab } from "../tabs"; import type { CatalogEntity } from "../../api/catalog-entity"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { EntitySettingRegistry } from "../../../extensions/registries"; -import type { EntitySettingsRouteParams } from "../../../common/routes"; import { groupBy } from "lodash"; import { SettingLayout } from "../layout/setting-layout"; import logger from "../../../common/logger"; import { Avatar } from "../avatar"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import entitySettingsRouteParametersInjectable from "./entity-settings-route-parameters.injectable"; -export interface EntitySettingsProps extends RouteComponentProps { +interface Dependencies { + entityId: IComputedValue; } @observer -export class EntitySettings extends React.Component { +class NonInjectedEntitySettings extends React.Component { @observable activeTab: string; - constructor(props: EntitySettingsProps) { + constructor(props: Dependencies) { super(props); makeObservable(this); @@ -43,8 +44,9 @@ export class EntitySettings extends React.Component { } } + @computed get entityId() { - return this.props.match.params.entityId; + return this.props.entityId.get(); } get entity(): CatalogEntity { @@ -120,7 +122,6 @@ export class EntitySettings extends React.Component { const { activeSetting } = this; - return ( { ); } } + +export const EntitySettings = withInjectables( + NonInjectedEntitySettings, + + { + getProps: (di) => { + const routeParameters = di.inject(entitySettingsRouteParametersInjectable); + + return { + entityId: routeParameters.entityId, + }; + }, + }, +); diff --git a/src/renderer/components/+events/events-route-component.injectable.ts b/src/renderer/components/+events/events-route-component.injectable.ts new file mode 100644 index 0000000000..b9954b22c0 --- /dev/null +++ b/src/renderer/components/+events/events-route-component.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 } from "@ogre-tools/injectable"; +import { Events } from "./events"; +import eventsRouteInjectable from "../../../common/front-end-routing/routes/cluster/events/events-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const eventsRouteComponentInjectable = getInjectable({ + id: "events-route-component", + + instantiate: (di) => ({ + route: di.inject(eventsRouteInjectable), + Component: Events, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default eventsRouteComponentInjectable; diff --git a/src/renderer/components/+events/events-sidebar-items.injectable.tsx b/src/renderer/components/+events/events-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..90fa787a84 --- /dev/null +++ b/src/renderer/components/+events/events-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import React from "react"; +import { + SidebarItemRegistration, + sidebarItemsInjectionToken, +} from "../layout/sidebar-items.injectable"; +import { Icon } from "../icon"; + +import eventsRouteInjectable from "../../../common/front-end-routing/routes/cluster/events/events-route.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToEventsInjectable from "../../../common/front-end-routing/routes/cluster/events/navigate-to-events.injectable"; + +const eventsSidebarItemsInjectable = getInjectable({ + id: "events-sidebar-items", + + instantiate: (di) => { + const route = di.inject(eventsRouteInjectable); + const navigateToEvents = di.inject(navigateToEventsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed((): SidebarItemRegistration[] => [ + { + id: "events", + parentId: null, + getIcon: () => , + title: "Events", + onClick: navigateToEvents, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 80, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default eventsSidebarItemsInjectable; diff --git a/src/renderer/components/+events/events.tsx b/src/renderer/components/+events/events.tsx index bdf83b750d..fd922c647f 100644 --- a/src/renderer/components/+events/events.tsx +++ b/src/renderer/components/+events/events.tsx @@ -9,7 +9,7 @@ import React, { Fragment } from "react"; import { computed, observable, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { orderBy } from "lodash"; -import { TabLayout } from "../layout/tab-layout"; +import { TabLayout } from "../layout/tab-layout-2"; import { EventStore, eventStore } from "./event.store"; import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object-list-layout"; import type { KubeEvent } from "../../../common/k8s-api/endpoints/events.api"; @@ -19,9 +19,10 @@ import { Tooltip } from "../tooltip"; import { Link } from "react-router-dom"; import { cssNames, IClassName, stopPropagation } from "../../utils"; import { Icon } from "../icon"; -import { eventsURL } from "../../../common/routes"; import { getDetailsUrl } from "../kube-detail-params"; import { apiManager } from "../../../common/k8s-api/api-manager"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import navigateToEventsInjectable from "../../../common/front-end-routing/routes/cluster/events/navigate-to-events.injectable"; import { KubeObjectAge } from "../kube-object/age"; import { ReactiveDuration } from "../duration/reactive-duration"; @@ -46,8 +47,12 @@ const defaultProps: Partial = { compactLimit: 10, }; +interface Dependencies { + navigateToEvents: () => void; +} + @observer -export class Events extends React.Component { +class NonInjectedEvents extends React.Component { static defaultProps = defaultProps as object; @observable sorting: TableSortParams = { @@ -64,7 +69,7 @@ export class Events extends React.Component { [columnId.lastSeen]: event => -new Date(event.lastTimestamp).getTime(), }; - constructor(props: EventsProps) { + constructor(props: Dependencies & EventsProps) { super(props); makeObservable(this); } @@ -105,7 +110,7 @@ export class Events extends React.Component { return { title, - info: ({visibleItems.length} of {items.length}), + info: ({visibleItems.length} of {items.length}), }; } @@ -204,3 +209,14 @@ export class Events extends React.Component { ); } } + +export const Events = withInjectables( + NonInjectedEvents, + + { + getProps: (di, props) => ({ + navigateToEvents: di.inject(navigateToEventsInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/+events/sidebar-item.tsx b/src/renderer/components/+events/sidebar-item.tsx deleted file mode 100644 index 50ffddcefc..0000000000 --- a/src/renderer/components/+events/sidebar-item.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 8589aa80e7..8335e2c24c 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -20,6 +20,10 @@ import { DiRender, renderFor } from "../../test-utils/renderFor"; import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable"; import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForDownloadsInjectable from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable"; +import getConfigurationFileModelInjectable + from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; +import appVersionInjectable + from "../../../../common/get-configuration-file-model/app-version/app-version.injectable"; mockWindow(); @@ -53,6 +57,9 @@ describe("Extensions", () => { di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForDownloadsInjectable, () => "some-directory-for-downloads"); + di.permitSideEffects(getConfigurationFileModelInjectable); + di.permitSideEffects(appVersionInjectable); + mockFs({ "some-directory-for-user-data": {}, }); diff --git a/src/renderer/components/+extensions/extensions-route-component.injectable.ts b/src/renderer/components/+extensions/extensions-route-component.injectable.ts new file mode 100644 index 0000000000..aede3b340a --- /dev/null +++ b/src/renderer/components/+extensions/extensions-route-component.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 } from "@ogre-tools/injectable"; +import { Extensions } from "./extensions"; +import extensionsRouteInjectable from "../../../common/front-end-routing/routes/extensions/extensions-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const extensionsRouteComponentInjectable = getInjectable({ + id: "extensions-route-component", + + instantiate: (di) => ({ + route: di.inject(extensionsRouteInjectable), + Component: Extensions, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default extensionsRouteComponentInjectable; diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index eb61cd2265..a078528d57 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -71,7 +71,7 @@ class NonInjectedExtensions extends React.Component { return ( - +

Extensions

diff --git a/src/renderer/components/+helm-charts/helm-chart-details.tsx b/src/renderer/components/+helm-charts/helm-chart-details.tsx index e5048d64fa..729c31bcb4 100644 --- a/src/renderer/components/+helm-charts/helm-chart-details.tsx +++ b/src/renderer/components/+helm-charts/helm-chart-details.tsx @@ -117,6 +117,7 @@ class NonInjectedHelmChartDetails extends Component { -} - interface Dependencies { releases: IComputedValue; releasesArePending: IComputedValue; selectNamespace: (namespace: string) => void; + namespace: IComputedValue; + navigateToHelmReleases: NavigateToHelmReleases; } -class NonInjectedHelmReleases extends Component { +class NonInjectedHelmReleases extends Component { + // TODO: This side-effect in mount must go. componentDidMount() { - const { match: { params: { namespace }}} = this.props; + const namespace = this.props.namespace.get(); if (namespace) { this.props.selectNamespace(namespace); @@ -60,16 +60,14 @@ class NonInjectedHelmReleases extends Component { - navigation.push(releaseURL({ - params: { - name: item.getName(), - namespace: item.getNs(), - }, - })); + this.props.navigateToHelmReleases({ + name: item.getName(), + namespace: item.getNs(), + }); }; hideDetails = () => { - navigation.push(releaseURL()); + this.props.navigateToHelmReleases(); }; renderRemoveDialogMessage(selectedItems: HelmRelease[]) { @@ -144,7 +142,7 @@ class NonInjectedHelmReleases extends Component; return ( - <> + legacyReleaseStore.items} @@ -171,7 +169,7 @@ class NonInjectedHelmReleases extends Component {filters} - + ), searchProps: { @@ -218,20 +216,25 @@ class NonInjectedHelmReleases extends Component - + ); } } -export const HelmReleases = withInjectables( +export const HelmReleases = withInjectables( NonInjectedHelmReleases, { - getProps: (di, props) => ({ - releases: di.inject(removableReleasesInjectable), - releasesArePending: di.inject(releasesInjectable).pending, - selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces, - ...props, - }), + getProps: (di) => { + const routeParameters = di.inject(helmReleasesRouteParametersInjectable); + + return { + releases: di.inject(removableReleasesInjectable), + releasesArePending: di.inject(releasesInjectable).pending, + selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces, + navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable), + namespace: routeParameters.namespace, + }; + }, }, ); diff --git a/src/renderer/components/+helm/helm-sidebar-items.injectable.tsx b/src/renderer/components/+helm/helm-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..76796bc440 --- /dev/null +++ b/src/renderer/components/+helm/helm-sidebar-items.injectable.tsx @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import React from "react"; +import { + SidebarItemRegistration, + sidebarItemsInjectionToken, +} from "../layout/sidebar-items.injectable"; +import { Icon } from "../icon"; +import { noop } from "lodash/fp"; + +export const helmSidebarItemId = "helm"; + +const helmSidebarItemsInjectable = getInjectable({ + id: "helm-sidebar-items", + + instantiate: () => + computed((): SidebarItemRegistration[] => [ + { + id: helmSidebarItemId, + parentId: null, + getIcon: () => , + title: "Helm", + onClick: noop, + orderNumber: 90, + }, + ]), + + injectionToken: sidebarItemsInjectionToken, +}); + +export default helmSidebarItemsInjectable; diff --git a/src/renderer/components/+helm/route-tabs.injectable.ts b/src/renderer/components/+helm/route-tabs.injectable.ts deleted file mode 100644 index 6b599a368a..0000000000 --- a/src/renderer/components/+helm/route-tabs.injectable.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { 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({ - id: "helm-routes", - instantiate: () => getRouteTabs(), -}); - -export default helmRoutesInjectable; diff --git a/src/renderer/components/+helm/route.tsx b/src/renderer/components/+helm/route.tsx deleted file mode 100644 index da2983a8ba..0000000000 --- a/src/renderer/components/+helm/route.tsx +++ /dev/null @@ -1,31 +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 { 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 deleted file mode 100644 index b01e19d40f..0000000000 --- a/src/renderer/components/+helm/sidebar-item.tsx +++ /dev/null @@ -1,44 +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 { 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 { renderTabRoutesSidebarItems } 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 ( - } - > - {renderTabRoutesSidebarItems(tabRoutes)} - - ); -}); - -export const HelmSidebarItem = withInjectables(NonInjectedHelmSidebarItem, { - getProps: (di, props) => ({ - routes: di.inject(networkRouteTabsInjectable), - ...props, - }), -}); diff --git a/src/renderer/components/+namespaces/namespace-select-filter.tsx b/src/renderer/components/+namespaces/namespace-select-filter.tsx index 9a216a8f9f..b7f8bd7c32 100644 --- a/src/renderer/components/+namespaces/namespace-select-filter.tsx +++ b/src/renderer/components/+namespaces/namespace-select-filter.tsx @@ -30,6 +30,7 @@ const NonInjectedNamespaceSelectFilter = observer(({ model }: SelectProps & Depe onClick={model.onClick} > ({ + route: di.inject(namespacesRouteInjectable), + Component: NamespacesRoute, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default namespacesRouteComponentInjectable; diff --git a/src/renderer/components/+namespaces/namespaces-sidebar-items.injectable.tsx b/src/renderer/components/+namespaces/namespaces-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..296bed7336 --- /dev/null +++ b/src/renderer/components/+namespaces/namespaces-sidebar-items.injectable.tsx @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { + SidebarItemRegistration, + sidebarItemsInjectionToken, +} from "../layout/sidebar-items.injectable"; +import { Icon } from "../icon"; +import React from "react"; +import namespacesRouteInjectable from "../../../common/front-end-routing/routes/cluster/namespaces/namespaces-route.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToNamespacesInjectable from "../../../common/front-end-routing/routes/cluster/namespaces/navigate-to-namespaces.injectable"; + +const namespacesSidebarItemsInjectable = getInjectable({ + id: "namespaces", + + instantiate: (di) => { + const route = di.inject(namespacesRouteInjectable); + const navigateToNamespaces = di.inject(navigateToNamespacesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed((): SidebarItemRegistration[] => [ + { + id: "namespaces", + parentId: null, + getIcon: () => , + title: "Namespaces", + onClick: navigateToNamespaces, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 70, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default namespacesSidebarItemsInjectable; diff --git a/src/renderer/components/+namespaces/route.tsx b/src/renderer/components/+namespaces/route.tsx index b1bf0601d0..00956306ef 100644 --- a/src/renderer/components/+namespaces/route.tsx +++ b/src/renderer/components/+namespaces/route.tsx @@ -8,17 +8,14 @@ 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 { TabLayout } from "../layout/tab-layout-2"; 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"; +import addNamespaceDialogModelInjectable from "./add-namespace-dialog-model/add-namespace-dialog-model.injectable"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -28,15 +25,12 @@ enum columnId { status = "status", } -export interface NamespacesRouteProps extends RouteComponentProps { -} - interface Dependencies { namespaceStore: NamespaceStore; openAddNamespaceDialog: () => void; } -export const NonInjectedNamespacesRoute = ({ namespaceStore, openAddNamespaceDialog }: Dependencies & NamespacesRouteProps) => ( +export const NonInjectedNamespacesRoute = ({ namespaceStore, openAddNamespaceDialog }: Dependencies) => ( (NonInjectedNamespacesRoute, { - getProps: (di, props) => ({ +export const NamespacesRoute = withInjectables(NonInjectedNamespacesRoute, { + getProps: (di) => ({ 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 deleted file mode 100644 index 78bbc8a3ae..0000000000 --- a/src/renderer/components/+namespaces/sidebar-item.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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-endpoints/endpoints-route-component.injectable.ts b/src/renderer/components/+network-endpoints/endpoints-route-component.injectable.ts new file mode 100644 index 0000000000..0236e2be80 --- /dev/null +++ b/src/renderer/components/+network-endpoints/endpoints-route-component.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 } from "@ogre-tools/injectable"; +import { Endpoints } from "./endpoints"; +import endpointsRouteInjectable from "../../../common/front-end-routing/routes/cluster/network/endpoints/endpoints-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const endpointsRouteComponentInjectable = getInjectable({ + id: "endpoints-route-component", + + instantiate: (di) => ({ + route: di.inject(endpointsRouteInjectable), + Component: Endpoints, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default endpointsRouteComponentInjectable; diff --git a/src/renderer/components/+network-endpoints/endpoints-sidebar-items.injectable.tsx b/src/renderer/components/+network-endpoints/endpoints-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..2f8c3adefb --- /dev/null +++ b/src/renderer/components/+network-endpoints/endpoints-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import endpointsRouteInjectable from "../../../common/front-end-routing/routes/cluster/network/endpoints/endpoints-route.injectable"; +import { networkSidebarItemId } from "../+network/network-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToEndpointsInjectable from "../../../common/front-end-routing/routes/cluster/network/endpoints/navigate-to-endpoints.injectable"; + +const endpointsSidebarItemsInjectable = getInjectable({ + id: "endpoints-sidebar-items", + + instantiate: (di) => { + const route = di.inject(endpointsRouteInjectable); + const navigateToEndpoints = di.inject(navigateToEndpointsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "endpoints", + parentId: networkSidebarItemId, + title: "Endpoints", + onClick: navigateToEndpoints, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 20, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default endpointsSidebarItemsInjectable; diff --git a/src/renderer/components/+network-endpoints/endpoints.tsx b/src/renderer/components/+network-endpoints/endpoints.tsx index 0c5595be54..b1411f9681 100644 --- a/src/renderer/components/+network-endpoints/endpoints.tsx +++ b/src/renderer/components/+network-endpoints/endpoints.tsx @@ -7,11 +7,10 @@ import "./endpoints.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router-dom"; import { endpointStore } from "./endpoints.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { EndpointRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -21,48 +20,47 @@ enum columnId { age = "age", } -export interface EndpointsProps extends RouteComponentProps { -} - @observer -export class Endpoints extends React.Component { +export class Endpoints extends React.Component { render() { return ( - endpoint.getName(), - [columnId.namespace]: endpoint => endpoint.getNs(), - [columnId.age]: endpoint => -endpoint.getCreationTimestamp(), - }} - searchFilters={[ - endpoint => endpoint.getSearchFields(), - ]} - renderHeaderTitle="Endpoints" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Endpoints", className: "endpoints", id: columnId.endpoints }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={endpoint => [ - endpoint.getName(), - , - endpoint.getNs(), - endpoint.toString(), - , - ]} - tableProps={{ - customRowHeights: (item, lineHeight, paddings) => { - const lines = item.getEndpointSubsets().length || 1; + + endpoint.getName(), + [columnId.namespace]: endpoint => endpoint.getNs(), + [columnId.age]: endpoint => -endpoint.getCreationTimestamp(), + }} + searchFilters={[ + endpoint => endpoint.getSearchFields(), + ]} + renderHeaderTitle="Endpoints" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Endpoints", className: "endpoints", id: columnId.endpoints }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={endpoint => [ + endpoint.getName(), + , + endpoint.getNs(), + endpoint.toString(), + , + ]} + tableProps={{ + customRowHeights: (item, lineHeight, paddings) => { + const lines = item.getEndpointSubsets().length || 1; - return lines * lineHeight + paddings; - }, - }} - /> + return lines * lineHeight + paddings; + }, + }} + /> + ); } } diff --git a/src/renderer/components/+network-ingresses/ingresses-route-component.injectable.ts b/src/renderer/components/+network-ingresses/ingresses-route-component.injectable.ts new file mode 100644 index 0000000000..17a015ad42 --- /dev/null +++ b/src/renderer/components/+network-ingresses/ingresses-route-component.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 } from "@ogre-tools/injectable"; +import { Ingresses } from "./ingresses"; +import ingressesRouteInjectable from "../../../common/front-end-routing/routes/cluster/network/ingresses/ingresses-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const ingressesRouteComponentInjectable = getInjectable({ + id: "ingresses-route-component", + + instantiate: (di) => ({ + route: di.inject(ingressesRouteInjectable), + Component: Ingresses, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default ingressesRouteComponentInjectable; diff --git a/src/renderer/components/+network-ingresses/ingresses-sidebar-items.injectable.tsx b/src/renderer/components/+network-ingresses/ingresses-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..80e9a7939c --- /dev/null +++ b/src/renderer/components/+network-ingresses/ingresses-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import ingressesRouteInjectable from "../../../common/front-end-routing/routes/cluster/network/ingresses/ingresses-route.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import { networkSidebarItemId } from "../+network/network-sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToIngressesInjectable from "../../../common/front-end-routing/routes/cluster/network/ingresses/navigate-to-ingresses.injectable"; + +const ingressesSidebarItemsInjectable = getInjectable({ + id: "ingresses-sidebar-items", + + instantiate: (di) => { + const route = di.inject(ingressesRouteInjectable); + const navigateToIngresses = di.inject(navigateToIngressesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "ingresses", + parentId: networkSidebarItemId, + title: "Ingresses", + onClick: navigateToIngresses, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 30, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default ingressesSidebarItemsInjectable; diff --git a/src/renderer/components/+network-ingresses/ingresses.tsx b/src/renderer/components/+network-ingresses/ingresses.tsx index 4f4305c64c..a585025324 100644 --- a/src/renderer/components/+network-ingresses/ingresses.tsx +++ b/src/renderer/components/+network-ingresses/ingresses.tsx @@ -7,11 +7,10 @@ import "./ingresses.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router-dom"; import { ingressStore } from "./ingress.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { IngressRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; import { computeRouteDeclarations } from "../../../common/k8s-api/endpoints"; @@ -23,59 +22,65 @@ enum columnId { age = "age", } -export interface IngressesProps extends RouteComponentProps { -} - @observer -export class Ingresses extends React.Component { +export class Ingresses extends React.Component { render() { return ( - ingress.getName(), - [columnId.namespace]: ingress => ingress.getNs(), - [columnId.age]: ingress => -ingress.getCreationTimestamp(), - }} - searchFilters={[ - ingress => ingress.getSearchFields(), - ingress => ingress.getPorts(), - ]} - renderHeaderTitle="Ingresses" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "LoadBalancers", className: "loadbalancers", id: columnId.loadBalancers }, - { title: "Rules", className: "rules", id: columnId.rules }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={ingress => [ - ingress.getName(), - , - ingress.getNs(), - ingress.getLoadBalancers().map(lb =>

{lb}

), - computeRouteDeclarations(ingress).map(decl => ( - decl.displayAsLink - ? ( - - e.stopPropagation()} - > - {decl.url} - ⇢ {decl.service} - - ) - : {decl.url} ⇢ {decl.service} - )), - , - ]} - /> + + ingress.getName(), + [columnId.namespace]: ingress => ingress.getNs(), + [columnId.age]: ingress => -ingress.getCreationTimestamp(), + }} + searchFilters={[ + ingress => ingress.getSearchFields(), + ingress => ingress.getPorts(), + ]} + renderHeaderTitle="Ingresses" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "LoadBalancers", className: "loadbalancers", id: columnId.loadBalancers }, + { title: "Rules", className: "rules", id: columnId.rules }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={ingress => [ + ingress.getName(), + , + ingress.getNs(), + ingress.getLoadBalancers().map(lb =>

{lb}

), + computeRouteDeclarations(ingress).map(decl => ( + decl.displayAsLink + ? ( + + e.stopPropagation()} + > + {decl.url} + ⇢ {decl.service} + + ) + : {decl.url} ⇢ {decl.service} + )), + , + ]} + tableProps={{ + customRowHeights: (item, lineHeight, paddings) => { + const lines = item.getRoutes().length || 1; + + return lines * lineHeight + paddings; + }, + }} + /> +
); } } diff --git a/src/renderer/components/+network-policies/network-policies-route-component.injectable.ts b/src/renderer/components/+network-policies/network-policies-route-component.injectable.ts new file mode 100644 index 0000000000..94d124d94a --- /dev/null +++ b/src/renderer/components/+network-policies/network-policies-route-component.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 } from "@ogre-tools/injectable"; +import { NetworkPolicies } from "./network-policies"; +import networkPoliciesRouteInjectable from "../../../common/front-end-routing/routes/cluster/network/network-policies/network-policies-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const networkPoliciesRouteComponentInjectable = getInjectable({ + id: "network-policies-route-component", + + instantiate: (di) => ({ + route: di.inject(networkPoliciesRouteInjectable), + Component: NetworkPolicies, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default networkPoliciesRouteComponentInjectable; diff --git a/src/renderer/components/+network-policies/network-policies-sidebar-items.injectable.tsx b/src/renderer/components/+network-policies/network-policies-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..6c6df9506d --- /dev/null +++ b/src/renderer/components/+network-policies/network-policies-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import networkPoliciesRouteInjectable from "../../../common/front-end-routing/routes/cluster/network/network-policies/network-policies-route.injectable"; +import { networkSidebarItemId } from "../+network/network-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToNetworkPoliciesInjectable from "../../../common/front-end-routing/routes/cluster/network/network-policies/navigate-to-network-policies.injectable"; + +const networkPoliciesSidebarItemsInjectable = getInjectable({ + id: "network-policies-sidebar-items", + + instantiate: (di) => { + const route = di.inject(networkPoliciesRouteInjectable); + const navigateToNetworkPolicies = di.inject(navigateToNetworkPoliciesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "network-policies", + parentId: networkSidebarItemId, + title: "Network Policies", + onClick: navigateToNetworkPolicies, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 40, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default networkPoliciesSidebarItemsInjectable; diff --git a/src/renderer/components/+network-policies/network-policies.tsx b/src/renderer/components/+network-policies/network-policies.tsx index c226f090d6..9a442677d7 100644 --- a/src/renderer/components/+network-policies/network-policies.tsx +++ b/src/renderer/components/+network-policies/network-policies.tsx @@ -7,11 +7,10 @@ import "./network-policies.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router-dom"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { networkPolicyStore } from "./network-policy.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { NetworkPoliciesRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -21,42 +20,41 @@ enum columnId { age = "age", } -export interface NetworkPoliciesProps extends RouteComponentProps { -} - @observer -export class NetworkPolicies extends React.Component { +export class NetworkPolicies extends React.Component { render() { return ( - networkPolicy.getName(), - [columnId.namespace]: networkPolicy => networkPolicy.getNs(), - [columnId.age]: networkPolicy => -networkPolicy.getCreationTimestamp(), - }} - searchFilters={[ - networkPolicy => networkPolicy.getSearchFields(), - ]} - renderHeaderTitle="Network Policies" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Policy Types", className: "type", id: columnId.types }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={networkPolicy => [ - networkPolicy.getName(), - , - networkPolicy.getNs(), - networkPolicy.getTypes().join(", "), - , - ]} - /> + + networkPolicy.getName(), + [columnId.namespace]: networkPolicy => networkPolicy.getNs(), + [columnId.age]: networkPolicy => -networkPolicy.getCreationTimestamp(), + }} + searchFilters={[ + networkPolicy => networkPolicy.getSearchFields(), + ]} + renderHeaderTitle="Network Policies" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Policy Types", className: "type", id: columnId.types }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={networkPolicy => [ + networkPolicy.getName(), + , + networkPolicy.getNs(), + networkPolicy.getTypes().join(", "), + , + ]} + /> + ); } } diff --git a/src/renderer/components/+network-port-forwards/port-forwards-route-component.injectable.ts b/src/renderer/components/+network-port-forwards/port-forwards-route-component.injectable.ts new file mode 100644 index 0000000000..01c5b3254f --- /dev/null +++ b/src/renderer/components/+network-port-forwards/port-forwards-route-component.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 } from "@ogre-tools/injectable"; +import { PortForwards } from "./port-forwards"; +import portForwardsRouteInjectable from "../../../common/front-end-routing/routes/cluster/network/port-forwards/port-forwards-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const portForwardsRouteComponentInjectable = getInjectable({ + id: "port-forwards-route-component", + + instantiate: (di) => ({ + route: di.inject(portForwardsRouteInjectable), + Component: PortForwards, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default portForwardsRouteComponentInjectable; diff --git a/src/renderer/components/+network-port-forwards/port-forwards-route-parameters.injectable.ts b/src/renderer/components/+network-port-forwards/port-forwards-route-parameters.injectable.ts new file mode 100644 index 0000000000..7b56a14ab3 --- /dev/null +++ b/src/renderer/components/+network-port-forwards/port-forwards-route-parameters.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import routePathParametersInjectable from "../../routes/route-path-parameters.injectable"; +import portForwardsRouteInjectable from "../../../common/front-end-routing/routes/cluster/network/port-forwards/port-forwards-route.injectable"; + +const portForwardsRouteParametersInjectable = getInjectable({ + id: "port-forwards-route-parameters", + + instantiate: (di) => { + const route = di.inject(portForwardsRouteInjectable); + const pathParameters = di.inject(routePathParametersInjectable, route); + + return { + forwardport: computed(() => pathParameters.get().forwardport), + }; + }, +}); + +export default portForwardsRouteParametersInjectable; diff --git a/src/renderer/components/+network-port-forwards/port-forwards-sidebar-items.injectable.tsx b/src/renderer/components/+network-port-forwards/port-forwards-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..271a697faf --- /dev/null +++ b/src/renderer/components/+network-port-forwards/port-forwards-sidebar-items.injectable.tsx @@ -0,0 +1,39 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import portForwardsRouteInjectable from "../../../common/front-end-routing/routes/cluster/network/port-forwards/port-forwards-route.injectable"; +import { networkSidebarItemId } from "../+network/network-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToPortForwardsInjectable from "../../../common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable"; + +const portForwardsSidebarItemsInjectable = getInjectable({ + id: "port-forwards-sidebar-items", + + instantiate: (di) => { + const route = di.inject(portForwardsRouteInjectable); + const navigateToPortForwards = di.inject(navigateToPortForwardsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "port-forwards", + parentId: networkSidebarItemId, + + title: "Port Forwarding", + onClick: navigateToPortForwards, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 50, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default portForwardsSidebarItemsInjectable; diff --git a/src/renderer/components/+network-port-forwards/port-forwards.tsx b/src/renderer/components/+network-port-forwards/port-forwards.tsx index 1a079cd598..7f35bd2373 100644 --- a/src/renderer/components/+network-port-forwards/port-forwards.tsx +++ b/src/renderer/components/+network-port-forwards/port-forwards.tsx @@ -7,15 +7,16 @@ import "./port-forwards.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router-dom"; import { ItemListLayout } from "../item-object-list/list-layout"; import type { PortForwardItem, PortForwardStore } from "../../port-forward"; import { PortForwardMenu } from "./port-forward-menu"; -import { PortForwardsRouteParams, portForwardsURL } from "../../../common/routes"; import { PortForwardDetails } from "./port-forward-details"; -import { navigation } from "../../navigation"; import { withInjectables } from "@ogre-tools/injectable-react"; import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; +import { computed, IComputedValue, makeObservable } from "mobx"; +import portForwardsRouteParametersInjectable from "./port-forwards-route-parameters.injectable"; +import navigateToPortForwardsInjectable, { NavigateToPortForwards } from "../../../common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable"; enum columnId { name = "name", @@ -27,26 +28,30 @@ enum columnId { status = "status", } -export interface PortForwardsProps extends RouteComponentProps { -} - interface Dependencies { portForwardStore: PortForwardStore; + forwardport: IComputedValue; + navigateToPortForwards: NavigateToPortForwards; } @observer -class NonInjectedPortForwards extends React.Component { +class NonInjectedPortForwards extends React.Component { + constructor(props: Dependencies) { + super(props); + + makeObservable(this); + } componentDidMount() { + disposeOnUnmount(this, [ this.props.portForwardStore.watch(), ]); } + @computed get selectedPortForward() { - const { match: { params: { forwardport }}} = this.props; - - return this.props.portForwardStore.getById(forwardport); + return this.props.portForwardStore.getById(this.props.forwardport.get()); } onDetails = (item: PortForwardItem) => { @@ -58,15 +63,13 @@ class NonInjectedPortForwards extends React.Component { - navigation.push(portForwardsURL({ - params: { - forwardport: item.getId(), - }, - })); + this.props.navigateToPortForwards({ + forwardport: item.getId(), + }); }; hideDetails = () => { - navigation.push(portForwardsURL()); + this.props.navigateToPortForwards(); }; renderRemoveDialogMessage(selectedItems: PortForwardItem[]) { @@ -82,7 +85,7 @@ class NonInjectedPortForwards extends React.Component + )} - + ); } } -export const PortForwards = withInjectables( +export const PortForwards = withInjectables( NonInjectedPortForwards, { - getProps: (di, props) => ({ - portForwardStore: di.inject(portForwardStoreInjectable), - ...props, - }), + getProps: (di) => { + const routeParameters = di.inject(portForwardsRouteParametersInjectable); + + return { + portForwardStore: di.inject(portForwardStoreInjectable), + forwardport: routeParameters.forwardport, + navigateToPortForwards: di.inject(navigateToPortForwardsInjectable), + }; + }, }, ); diff --git a/src/renderer/components/+network-services/service-port-component.tsx b/src/renderer/components/+network-services/service-port-component.tsx index 39baf0da09..6945a5dc6a 100644 --- a/src/renderer/components/+network-services/service-port-component.tsx +++ b/src/renderer/components/+network-services/service-port-component.tsx @@ -13,17 +13,14 @@ import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Button } from "../button"; import type { ForwardedPort } from "../../port-forward"; -import { - aboutPortForwarding, - notifyErrorPortForwarding, openPortForward, - PortForwardStore, - predictProtocol, -} from "../../port-forward"; +import { openPortForward, PortForwardStore, predictProtocol } from "../../port-forward"; import { Spinner } from "../spinner"; import { withInjectables } from "@ogre-tools/injectable-react"; import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable"; import portForwardDialogModelInjectable from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable"; import logger from "../../../common/logger"; +import aboutPortForwardingInjectable from "../../port-forward/about-port-forwarding.injectable"; +import notifyErrorPortForwardingInjectable from "../../port-forward/notify-error-port-forwarding.injectable"; export interface ServicePortComponentProps { service: Service; @@ -33,6 +30,8 @@ export interface ServicePortComponentProps { interface Dependencies { portForwardStore: PortForwardStore; openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean; onClose: () => void }) => void; + aboutPortForwarding: () => void; + notifyErrorPortForwarding: (message: string) => void; } @observer @@ -115,10 +114,10 @@ class NonInjectedServicePortComponent extends React.Component ({ portForwardStore: di.inject(portForwardStoreInjectable), openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open, + aboutPortForwarding: di.inject(aboutPortForwardingInjectable), + notifyErrorPortForwarding: di.inject(notifyErrorPortForwardingInjectable), ...props, }), }, diff --git a/src/renderer/components/+network-services/services-route-component.injectable.ts b/src/renderer/components/+network-services/services-route-component.injectable.ts new file mode 100644 index 0000000000..c16b724e0a --- /dev/null +++ b/src/renderer/components/+network-services/services-route-component.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 } from "@ogre-tools/injectable"; +import { Services } from "./services"; +import servicesRouteInjectable from "../../../common/front-end-routing/routes/cluster/network/services/services-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const servicesRouteComponentInjectable = getInjectable({ + id: "services-route-component", + + instantiate: (di) => ({ + route: di.inject(servicesRouteInjectable), + Component: Services, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default servicesRouteComponentInjectable; diff --git a/src/renderer/components/+network-services/services-sidebar-items.injectable.tsx b/src/renderer/components/+network-services/services-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..c4e8d93a99 --- /dev/null +++ b/src/renderer/components/+network-services/services-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import servicesRouteInjectable from "../../../common/front-end-routing/routes/cluster/network/services/services-route.injectable"; +import { networkSidebarItemId } from "../+network/network-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToServicesInjectable from "../../../common/front-end-routing/routes/cluster/network/services/navigate-to-services.injectable"; + +const servicesSidebarItemsInjectable = getInjectable({ + id: "services-sidebar-items", + + instantiate: (di) => { + const route = di.inject(servicesRouteInjectable); + const navigateToServices = di.inject(navigateToServicesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "services", + parentId: networkSidebarItemId, + title: "Services", + onClick: navigateToServices, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 10, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default servicesSidebarItemsInjectable; diff --git a/src/renderer/components/+network-services/services.tsx b/src/renderer/components/+network-services/services.tsx index 57107f1fc2..a05f3c1ab5 100644 --- a/src/renderer/components/+network-services/services.tsx +++ b/src/renderer/components/+network-services/services.tsx @@ -7,12 +7,11 @@ import "./services.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { Badge } from "../badge"; import { serviceStore } from "./services.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { ServicesRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -27,66 +26,65 @@ enum columnId { status = "status", } -export interface ServicesProps extends RouteComponentProps { -} - @observer -export class Services extends React.Component { +export class Services extends React.Component { render() { return ( - service.getName(), - [columnId.namespace]: service => service.getNs(), - [columnId.selector]: service => service.getSelector(), - [columnId.ports]: service => (service.spec.ports || []).map(({ port }) => port)[0], - [columnId.clusterIp]: service => service.getClusterIp(), - [columnId.type]: service => service.getType(), - [columnId.age]: service => -service.getCreationTimestamp(), - [columnId.status]: service => service.getStatus(), - }} - searchFilters={[ - service => service.getSearchFields(), - service => service.getSelector().join(" "), - service => service.getPorts().join(" "), - ]} - renderHeaderTitle="Services" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Type", className: "type", sortBy: columnId.type, id: columnId.type }, - { title: "Cluster IP", className: "clusterIp", sortBy: columnId.clusterIp, id: columnId.clusterIp }, - { title: "Ports", className: "ports", sortBy: columnId.ports, id: columnId.ports }, - { title: "External IP", className: "externalIp", id: columnId.externalIp }, - { title: "Selector", className: "selector", sortBy: columnId.selector, id: columnId.selector }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, - ]} - renderTableContents={service => { - const externalIps = service.getExternalIps(); + + service.getName(), + [columnId.namespace]: service => service.getNs(), + [columnId.selector]: service => service.getSelector(), + [columnId.ports]: service => (service.spec.ports || []).map(({ port }) => port)[0], + [columnId.clusterIp]: service => service.getClusterIp(), + [columnId.type]: service => service.getType(), + [columnId.age]: service => -service.getCreationTimestamp(), + [columnId.status]: service => service.getStatus(), + }} + searchFilters={[ + service => service.getSearchFields(), + service => service.getSelector().join(" "), + service => service.getPorts().join(" "), + ]} + renderHeaderTitle="Services" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Type", className: "type", sortBy: columnId.type, id: columnId.type }, + { title: "Cluster IP", className: "clusterIp", sortBy: columnId.clusterIp, id: columnId.clusterIp }, + { title: "Ports", className: "ports", sortBy: columnId.ports, id: columnId.ports }, + { title: "External IP", className: "externalIp", id: columnId.externalIp }, + { title: "Selector", className: "selector", sortBy: columnId.selector, id: columnId.selector }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, + ]} + renderTableContents={service => { + const externalIps = service.getExternalIps(); - if (externalIps.length === 0 && service.spec?.externalName) { - externalIps.push(service.spec.externalName); - } + if (externalIps.length === 0 && service.spec?.externalName) { + externalIps.push(service.spec.externalName); + } - return [ - service.getName(), - , - service.getNs(), - service.getType(), - service.getClusterIp(), - service.getPorts().join(", "), - externalIps.join(", ") || "-", - service.getSelector().map(label => ), - , - { title: service.getStatus(), className: service.getStatus().toLowerCase() }, - ]; - }} - /> + return [ + service.getName(), + , + service.getNs(), + service.getType(), + service.getClusterIp(), + service.getPorts().join(", "), + externalIps.join(", ") || "-", + service.getSelector().map(label => ), + , + { title: service.getStatus(), className: service.getStatus().toLowerCase() }, + ]; + }} + /> + ); } } diff --git a/src/renderer/components/+network/network-sidebar-items.injectable.tsx b/src/renderer/components/+network/network-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..e0e78a77cf --- /dev/null +++ b/src/renderer/components/+network/network-sidebar-items.injectable.tsx @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { + SidebarItemRegistration, + sidebarItemsInjectionToken, +} from "../layout/sidebar-items.injectable"; +import { Icon } from "../icon"; +import React from "react"; +import { noop } from "lodash/fp"; + +export const networkSidebarItemId = "network"; + +const networkSidebarItemsInjectable = getInjectable({ + id: "network-sidebar-items", + + instantiate: () => + computed((): SidebarItemRegistration[] => [ + { + id: networkSidebarItemId, + parentId: null, + getIcon: () => , + title: "Network", + onClick: noop, + orderNumber: 50, + }, + ]), + + injectionToken: sidebarItemsInjectionToken, +}); + +export default networkSidebarItemsInjectable; diff --git a/src/renderer/components/+network/network.scss b/src/renderer/components/+network/network.scss deleted file mode 100644 index e862468c8a..0000000000 --- a/src/renderer/components/+network/network.scss +++ /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. - */ - -.Network { -} diff --git a/src/renderer/components/+network/route-tabs.injectable.ts b/src/renderer/components/+network/route-tabs.injectable.ts deleted file mode 100644 index bef8a69d67..0000000000 --- a/src/renderer/components/+network/route-tabs.injectable.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { 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 * as routes from "../../../common/routes"; -import type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable"; -import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable"; - -interface Dependencies { - isAllowedResource: IsAllowedResource; -} - -function getRouteTabs({ isAllowedResource }: Dependencies) { - return computed(() => { - const tabs: TabLayoutRoute[] = []; - - if (isAllowedResource("services")) { - tabs.push({ - title: "Services", - component: Services, - url: routes.servicesURL(), - routePath: routes.servicesRoute.path.toString(), - }); - } - - if (isAllowedResource("endpoints")) { - tabs.push({ - title: "Endpoints", - component: Endpoints, - url: routes.endpointURL(), - routePath: routes.endpointRoute.path.toString(), - }); - } - - if (isAllowedResource("ingresses")) { - tabs.push({ - title: "Ingresses", - component: Ingresses, - url: routes.ingressURL(), - routePath: routes.ingressRoute.path.toString(), - }); - } - - if (isAllowedResource("networkpolicies")) { - tabs.push({ - title: "Network Policies", - component: NetworkPolicies, - url: routes.networkPoliciesURL(), - routePath: routes.networkPoliciesRoute.path.toString(), - }); - } - - tabs.push({ - title: "Port Forwarding", - component: PortForwards, - url: routes.portForwardsURL(), - routePath: routes.portForwardsRoute.path.toString(), - }); - - return tabs; - }); -} - -const networkRouteTabsInjectable = getInjectable({ - id: "network-route-tabs", - - instantiate: (di) => getRouteTabs({ - isAllowedResource: di.inject(isAllowedResourceInjectable), - }), -}); - -export default networkRouteTabsInjectable; diff --git a/src/renderer/components/+network/route.tsx b/src/renderer/components/+network/route.tsx deleted file mode 100644 index 1abf1ca445..0000000000 --- a/src/renderer/components/+network/route.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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 deleted file mode 100644 index 6189ef3b13..0000000000 --- a/src/renderer/components/+network/sidebar-item.tsx +++ /dev/null @@ -1,45 +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 { 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 { renderTabRoutesSidebarItems } 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 ( - } - > - {renderTabRoutesSidebarItems(tabRoutes)} - - ); -}); - -export const NetworkSidebarItem = withInjectables(NonInjectedNetworkSidebarItem, { - getProps: (di, props) => ({ - routes: di.inject(networkRouteTabsInjectable), - ...props, - }), -}); diff --git a/src/renderer/components/+nodes/nodes-route-component.injectable.ts b/src/renderer/components/+nodes/nodes-route-component.injectable.ts new file mode 100644 index 0000000000..4db966406d --- /dev/null +++ b/src/renderer/components/+nodes/nodes-route-component.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 } from "@ogre-tools/injectable"; +import { NodesRoute } from "./route"; +import nodesRouteInjectable from "../../../common/front-end-routing/routes/cluster/nodes/nodes-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const nodesRouteComponentInjectable = getInjectable({ + id: "nodes-route-component", + + instantiate: (di) => ({ + route: di.inject(nodesRouteInjectable), + Component: NodesRoute, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default nodesRouteComponentInjectable; diff --git a/src/renderer/components/+nodes/nodes-sidebar-items.injectable.tsx b/src/renderer/components/+nodes/nodes-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..605754a248 --- /dev/null +++ b/src/renderer/components/+nodes/nodes-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { + SidebarItemRegistration, + sidebarItemsInjectionToken, +} from "../layout/sidebar-items.injectable"; +import { Icon } from "../icon"; +import React from "react"; + +import nodesRouteInjectable from "../../../common/front-end-routing/routes/cluster/nodes/nodes-route.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToNodesInjectable from "../../../common/front-end-routing/routes/cluster/nodes/navigate-to-nodes.injectable"; + +const nodesSidebarItemsInjectable = getInjectable({ + id: "nodes-sidebar-items", + + instantiate: (di) => { + const route = di.inject(nodesRouteInjectable); + const navigateToNodes = di.inject(navigateToNodesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed((): SidebarItemRegistration[] => [ + { + id: "nodes", + parentId: null, + getIcon: () => , + title: "Nodes", + onClick: navigateToNodes, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 20, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default nodesSidebarItemsInjectable; diff --git a/src/renderer/components/+nodes/route.tsx b/src/renderer/components/+nodes/route.tsx index 9ca6874743..2fd494a3ae 100644 --- a/src/renderer/components/+nodes/route.tsx +++ b/src/renderer/components/+nodes/route.tsx @@ -6,9 +6,8 @@ import "./nodes.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import { cssNames, interval } from "../../utils"; -import { TabLayout } from "../layout/tab-layout"; +import { TabLayout } from "../layout/tab-layout-2"; import { nodesStore } from "./nodes.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { formatNodeTaint, getMetricsForAllNodes, INodeMetrics, Node } from "../../../common/k8s-api/endpoints/nodes.api"; @@ -20,7 +19,6 @@ import upperFirst from "lodash/upperFirst"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { Badge } from "../badge/badge"; import { eventStore } from "../+events/event.store"; -import type { NodesRouteParams } from "../../../common/routes"; import { makeObservable, observable } from "mobx"; import isEmpty from "lodash/isEmpty"; import { KubeObjectAge } from "../kube-object/age"; @@ -38,9 +36,6 @@ enum columnId { status = "status", } -export interface NodesRouteProps extends RouteComponentProps { -} - type MetricsTooltipFormatter = (metrics: [number, number]) => string; interface UsageArgs { @@ -51,11 +46,11 @@ interface UsageArgs { } @observer -export class NodesRoute extends React.Component { +export class NodesRoute extends React.Component { @observable.ref metrics: Partial = {}; private metricsWatcher = interval(30, async () => this.metrics = await getMetricsForAllNodes()); - constructor(props: NodesRouteProps) { + constructor(props: any) { super(props); makeObservable(this); } diff --git a/src/renderer/components/+nodes/sidebar-item.tsx b/src/renderer/components/+nodes/sidebar-item.tsx deleted file mode 100644 index 1a384cc0fc..0000000000 --- a/src/renderer/components/+nodes/sidebar-item.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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/+pod-security-policies/pod-security-policies-route-component.injectable.ts b/src/renderer/components/+pod-security-policies/pod-security-policies-route-component.injectable.ts new file mode 100644 index 0000000000..58b607e6d8 --- /dev/null +++ b/src/renderer/components/+pod-security-policies/pod-security-policies-route-component.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 } from "@ogre-tools/injectable"; +import { PodSecurityPolicies } from "./pod-security-policies"; +import podSecurityPoliciesRouteInjectable from "../../../common/front-end-routing/routes/cluster/user-management/pod-security-policies/pod-security-policies-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const podSecurityPoliciesRouteComponentInjectable = getInjectable({ + id: "pod-security-policies-route-component", + + instantiate: (di) => ({ + route: di.inject(podSecurityPoliciesRouteInjectable), + Component: PodSecurityPolicies, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default podSecurityPoliciesRouteComponentInjectable; diff --git a/src/renderer/components/+pod-security-policies/pod-security-policies-sidebar-items.injectable.tsx b/src/renderer/components/+pod-security-policies/pod-security-policies-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..711ee8cef4 --- /dev/null +++ b/src/renderer/components/+pod-security-policies/pod-security-policies-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import podSecurityPoliciesRouteInjectable from "../../../common/front-end-routing/routes/cluster/user-management/pod-security-policies/pod-security-policies-route.injectable"; +import { userManagementSidebarItemId } from "../+user-management/user-management-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToPodSecurityPoliciesInjectable from "../../../common/front-end-routing/routes/cluster/user-management/pod-security-policies/navigate-to-pod-security-policies.injectable"; + +const podSecurityPoliciesSidebarItemsInjectable = getInjectable({ + id: "pod-security-policies-sidebar-items", + + instantiate: (di) => { + const route = di.inject(podSecurityPoliciesRouteInjectable); + const navigateToPodSecurityPolicies = di.inject(navigateToPodSecurityPoliciesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "pod-security-policies", + parentId: userManagementSidebarItemId, + title: "Pod Security Policies", + onClick: navigateToPodSecurityPolicies, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 60, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default podSecurityPoliciesSidebarItemsInjectable; diff --git a/src/renderer/components/+pod-security-policies/pod-security-policies.tsx b/src/renderer/components/+pod-security-policies/pod-security-policies.tsx index 341db9cc35..3ccdc9ad7d 100644 --- a/src/renderer/components/+pod-security-policies/pod-security-policies.tsx +++ b/src/renderer/components/+pod-security-policies/pod-security-policies.tsx @@ -10,6 +10,7 @@ import { observer } from "mobx-react"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { podSecurityPoliciesStore } from "./pod-security-policies.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -23,38 +24,40 @@ enum columnId { export class PodSecurityPolicies extends React.Component { render() { return ( - podSecurityPolicy.getName(), - [columnId.volumes]: podSecurityPolicy => podSecurityPolicy.getVolumes(), - [columnId.privileged]: podSecurityPolicy => +podSecurityPolicy.isPrivileged(), - [columnId.age]: podSecurityPolicy => -podSecurityPolicy.getCreationTimestamp(), - }} - searchFilters={[ - podSecurityPolicy => podSecurityPolicy.getSearchFields(), - podSecurityPolicy => podSecurityPolicy.getVolumes(), - podSecurityPolicy => Object.values(podSecurityPolicy.getRules()), - ]} - renderHeaderTitle="Pod Security Policies" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Privileged", className: "privileged", sortBy: columnId.privileged, id: columnId.privileged }, - { title: "Volumes", className: "volumes", sortBy: columnId.volumes, id: columnId.volumes }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={podSecurityPolicy => [ - podSecurityPolicy.getName(), - , - podSecurityPolicy.isPrivileged() ? "Yes" : "No", - podSecurityPolicy.getVolumes().join(", "), - , - ]} - /> + + podSecurityPolicy.getName(), + [columnId.volumes]: podSecurityPolicy => podSecurityPolicy.getVolumes(), + [columnId.privileged]: podSecurityPolicy => +podSecurityPolicy.isPrivileged(), + [columnId.age]: podSecurityPolicy => -podSecurityPolicy.getCreationTimestamp(), + }} + searchFilters={[ + podSecurityPolicy => podSecurityPolicy.getSearchFields(), + podSecurityPolicy => podSecurityPolicy.getVolumes(), + podSecurityPolicy => Object.values(podSecurityPolicy.getRules()), + ]} + renderHeaderTitle="Pod Security Policies" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Privileged", className: "privileged", sortBy: columnId.privileged, id: columnId.privileged }, + { title: "Volumes", className: "volumes", sortBy: columnId.volumes, id: columnId.volumes }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={podSecurityPolicy => [ + podSecurityPolicy.getName(), + , + podSecurityPolicy.isPrivileged() ? "Yes" : "No", + podSecurityPolicy.getVolumes().join(", "), + , + ]} + /> + ); } } diff --git a/src/renderer/components/+preferences/app-preferences-route-component.injectable.ts b/src/renderer/components/+preferences/app-preferences-route-component.injectable.ts new file mode 100644 index 0000000000..aa887c2df9 --- /dev/null +++ b/src/renderer/components/+preferences/app-preferences-route-component.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 } from "@ogre-tools/injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import appPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/app/app-preferences-route.injectable"; +import { Application } from "./application"; + +const appPreferencesRouteComponentInjectable = getInjectable({ + id: "app-preferences-route-component", + + instantiate: (di) => ({ + route: di.inject(appPreferencesRouteInjectable), + Component: Application, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default appPreferencesRouteComponentInjectable; diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx index b85d3e9665..4809590b9f 100644 --- a/src/renderer/components/+preferences/application.tsx +++ b/src/renderer/components/+preferences/application.tsx @@ -7,8 +7,8 @@ import React from "react"; import { observer } from "mobx-react"; import { SubTitle } from "../layout/sub-title"; import { Select, SelectOption } from "../select"; -import { ThemeStore } from "../../theme.store"; -import { UserStore } from "../../../common/user-store"; +import type { ThemeStore } from "../../theme.store"; +import type { UserStore } from "../../../common/user-store"; import { Input } from "../input"; import { Switch } from "../switch"; import moment from "moment-timezone"; @@ -19,6 +19,9 @@ import { ExtensionSettings } from "./extension-settings"; import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration"; import { withInjectables } from "@ogre-tools/injectable-react"; import appPreferencesInjectable from "./app-preferences/app-preferences.injectable"; +import { Preferences } from "./preferences"; +import userStoreInjectable from "../../../common/user-store/user-store.injectable"; +import themeStoreInjectable from "../../theme-store.injectable"; const timezoneOptions: SelectOption[] = moment.tz.names().map(zone => ({ label: zone, @@ -31,100 +34,106 @@ const updateChannelOptions: SelectOption[] = Array.from( interface Dependencies { appPreferenceItems: IComputedValue; + userStore: UserStore; + themeStore: ThemeStore; } -const NonInjectedApplication: React.FC = ({ appPreferenceItems }) => { - const userStore = UserStore.getInstance(); +const NonInjectedApplication: React.FC = ({ appPreferenceItems, userStore, themeStore }) => { const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || ""); const extensionSettings = appPreferenceItems.get().filter((preference) => preference.showInPreferencesTab === "application"); - const themeStore = ThemeStore.getInstance(); return ( -
-

Application

-
- - userStore.colorTheme = value} + themeName="lens" + /> +
+ +
+ +
+ + userStore.extensionRegistryUrl.customUrl = customUrl} + placeholder="Custom Extension Registry URL..." + disabled={userStore.extensionRegistryUrl.location !== ExtensionRegistryLocation.CUSTOM} + /> +
+ +
+ +
+ + userStore.openAtLogin = !userStore.openAtLogin}> + Automatically start Lens on login + +
+ +
+ + {extensionSettings.map(setting => ( + + ))} + +
+ + userStore.setLocaleTimezone(value)} + themeName="lens" + /> +
- -
- -
- - userStore.extensionRegistryUrl.customUrl = customUrl} - placeholder="Custom Extension Registry URL..." - disabled={userStore.extensionRegistryUrl.location !== ExtensionRegistryLocation.CUSTOM} - /> -
- -
- -
- - userStore.openAtLogin = !userStore.openAtLogin}> - Automatically start Lens on login - -
- -
- - {extensionSettings.map(setting => ( - - ))} - -
- - userStore.setLocaleTimezone(value)} - themeName="lens" - /> -
-
+ ); }; @@ -134,6 +143,8 @@ export const Application = withInjectables( { getProps: (di) => ({ appPreferenceItems: di.inject(appPreferencesInjectable), + userStore: di.inject(userStoreInjectable), + themeStore: di.inject(themeStoreInjectable), }), }, ); diff --git a/src/renderer/components/+preferences/close-preferences.injectable.ts b/src/renderer/components/+preferences/close-preferences.injectable.ts new file mode 100644 index 0000000000..61eb3f9042 --- /dev/null +++ b/src/renderer/components/+preferences/close-preferences.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import observableHistoryInjectable from "../../navigation/observable-history.injectable"; +import navigateToFrontPageInjectable from "../../../common/front-end-routing/navigate-to-front-page.injectable"; + +const closePreferencesInjectable = getInjectable({ + id: "close-preferences", + + instantiate: (di) => { + const observableHistory = di.inject(observableHistoryInjectable); + const navigateToFrontPage = di.inject(navigateToFrontPageInjectable); + + return () => { + if (observableHistory.length === 1) { + navigateToFrontPage(); + + return; + } + + observableHistory.goBack(); + }; + }, +}); + +export default closePreferencesInjectable; diff --git a/src/renderer/components/+preferences/default-shell.injectable.ts b/src/renderer/components/+preferences/default-shell.injectable.ts new file mode 100644 index 0000000000..a8d33fe627 --- /dev/null +++ b/src/renderer/components/+preferences/default-shell.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import isWindowsInjectable from "../../../common/vars/is-windows.injectable"; + +const defaultShellInjectable = getInjectable({ + id: "default-shell", + + instantiate: (di) => { + const isWindows = di.inject(isWindowsInjectable); + + return ( + process.env.SHELL || + process.env.PTYSHELL || + (isWindows ? "powershell.exe" : "System default shell") + ); + }, + + causesSideEffects: true, +}); + +export default defaultShellInjectable; diff --git a/src/renderer/components/+preferences/editor-preferences-route-component.injectable.ts b/src/renderer/components/+preferences/editor-preferences-route-component.injectable.ts new file mode 100644 index 0000000000..a51d95438b --- /dev/null +++ b/src/renderer/components/+preferences/editor-preferences-route-component.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 } from "@ogre-tools/injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import { Editor } from "./editor"; +import editorPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/editor/editor-preferences-route.injectable"; + +const editorPreferencesRouteComponentInjectable = getInjectable({ + id: "editor-preferences-route-component", + + instantiate: (di) => ({ + route: di.inject(editorPreferencesRouteInjectable), + Component: Editor, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default editorPreferencesRouteComponentInjectable; diff --git a/src/renderer/components/+preferences/editor.tsx b/src/renderer/components/+preferences/editor.tsx index fe1f4ca602..64d0e9de83 100644 --- a/src/renderer/components/+preferences/editor.tsx +++ b/src/renderer/components/+preferences/editor.tsx @@ -4,12 +4,15 @@ */ import { observer } from "mobx-react"; import React from "react"; -import { UserStore } from "../../../common/user-store"; +import type { UserStore } from "../../../common/user-store"; import { Switch } from "../switch"; import { Select } from "../select"; import { SubTitle } from "../layout/sub-title"; import { SubHeader } from "../layout/sub-header"; import { Input, InputValidators } from "../input"; +import { Preferences } from "./preferences"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import userStoreInjectable from "../../../common/user-store/user-store.injectable"; enum EditorLineNumbersStyles { on = "On", @@ -18,78 +21,100 @@ enum EditorLineNumbersStyles { interval = "Interval", } -export const Editor = observer(() => { - const editorConfiguration = UserStore.getInstance().editorConfiguration; +interface Dependencies { + userStore: UserStore; +} + +const NonInjectedEditor = observer(({ userStore }: Dependencies) => { + const editorConfiguration = userStore.editorConfiguration; return ( -
-

Editor configuration

+ +
+

Editor configuration

- -
-
-
- editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled} - > - Show minimap - + +
+
+
+ editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled} + > + Show minimap + +
+
+ Position + editorConfiguration.minimap.side = value} - /> -
-
-
+
-
- - ({ + label, + value, + }))} + value={editorConfiguration.lineNumbers} + onChange={({ value }) => editorConfiguration.lineNumbers = value} + themeName="lens" + /> +
-
- - editorConfiguration.tabSize = Number(value)} - /> +
+ + editorConfiguration.tabSize = Number(value)} + /> +
+
+ + editorConfiguration.fontSize = Number(value)} + /> +
+
+ + editorConfiguration.fontFamily = value} + /> +
-
- - editorConfiguration.fontSize = Number(value)} - /> -
-
- - editorConfiguration.fontFamily = value} - /> -
-
+
); }); +export const Editor = withInjectables( + NonInjectedEditor, + + { + getProps: (di) => ({ + userStore: di.inject(userStoreInjectable), + }), + }, +); + + diff --git a/src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts b/src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts new file mode 100644 index 0000000000..4aec83f120 --- /dev/null +++ b/src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { filter, map } from "lodash/fp"; +import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token"; +import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; +import { pipeline } from "@ogre-tools/fp"; +import { extensionPreferenceItemInjectionToken } from "./extension-preference-items.injectable"; + +const extensionPreferenceItemRegistratorInjectable = getInjectable({ + id: "extension-preference-item-registrator", + + instantiate: + (di) => + (extension: LensRendererExtension, extensionInstallationCount) => { + const injectables = pipeline( + extension.appPreferences, + + filter( + (registration) => !registration.showInPreferencesTab, + ), + + map((registration) => { + const id = `extension-preferences-item-${registration.id}-for-extension-${extension.sanitizedExtensionId}`; + + return getInjectable({ + id: `${id}-for-instance-${extensionInstallationCount}`, + injectionToken: extensionPreferenceItemInjectionToken, + + instantiate: () => ({ + id: registration.id || id, + title: registration.title, + extension, + + components: { + Hint: registration.components.Hint, + Input: registration.components.Input, + }, + }), + }); + }), + ); + + injectables.forEach(di.register); + + return; + }, + + injectionToken: extensionRegistratorInjectionToken, +}); + +export default extensionPreferenceItemRegistratorInjectable; diff --git a/src/renderer/components/+preferences/extension-preference-items.injectable.ts b/src/renderer/components/+preferences/extension-preference-items.injectable.ts new file mode 100644 index 0000000000..f4d08304d2 --- /dev/null +++ b/src/renderer/components/+preferences/extension-preference-items.injectable.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { pipeline } from "@ogre-tools/fp"; +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; +import { filter, overSome } from "lodash/fp"; +import { computed } from "mobx"; +import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable"; +import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; +import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration"; + +interface ExtensionPreferenceItem extends RegisteredAppPreference { + extension: LensRendererExtension; +} + +export const extensionPreferenceItemInjectionToken = getInjectionToken({ + id: "extension-preference-item-injection-token", +}); + +const extensionsPreferenceItemsInjectable = getInjectable({ + id: "extension-preference-items", + + instantiate: (di) => { + const extensions = di.inject(rendererExtensionsInjectable); + + return computed(() => { + const enabledExtensions = extensions.get(); + + return pipeline( + di.injectMany(extensionPreferenceItemInjectionToken), + + filter((item) => + overSome([ + isNonExtensionItem, + isEnabledExtensionItemFor(enabledExtensions), + ])(item), + ), + ); + }); + }, +}); + +const isNonExtensionItem = (item: ExtensionPreferenceItem) => !item.extension; + +const isEnabledExtensionItemFor = + (enabledExtensions: LensRendererExtension[]) => (item: ExtensionPreferenceItem) => + !!enabledExtensions.find((extension) => extension === item.extension); + +export default extensionsPreferenceItemsInjectable; diff --git a/src/renderer/components/+preferences/extension-preferences-route-component.injectable.ts b/src/renderer/components/+preferences/extension-preferences-route-component.injectable.ts new file mode 100644 index 0000000000..f67e94fd98 --- /dev/null +++ b/src/renderer/components/+preferences/extension-preferences-route-component.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 } from "@ogre-tools/injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import extensionPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable"; +import { Extensions } from "./extensions"; + +const extensionPreferencesRouteComponentInjectable = getInjectable({ + id: "extension-preferences-route-component", + + instantiate: (di) => ({ + route: di.inject(extensionPreferencesRouteInjectable), + Component: Extensions, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default extensionPreferencesRouteComponentInjectable; diff --git a/src/renderer/components/+preferences/extension-settings.tsx b/src/renderer/components/+preferences/extension-settings.tsx index eb4970976b..d796bc35f8 100644 --- a/src/renderer/components/+preferences/extension-settings.tsx +++ b/src/renderer/components/+preferences/extension-settings.tsx @@ -4,15 +4,15 @@ */ import { SubTitle } from "../layout/sub-title"; import type { AppPreferenceRegistration } from "./app-preferences/app-preference-registration"; -import React from "react"; +import React, { DOMAttributes } from "react"; import { cssNames } from "../../../renderer/utils"; -export interface ExtensionSettingsProps { +export interface ExtensionSettingsProps extends DOMAttributes { setting: AppPreferenceRegistration; size: "small" | "normal"; } -export function ExtensionSettings({ setting, size }: ExtensionSettingsProps) { +export function ExtensionSettings({ setting, size, ...props }: ExtensionSettingsProps) { const { title, id, @@ -21,7 +21,7 @@ export function ExtensionSettings({ setting, size }: ExtensionSettingsProps) { return ( -
+
diff --git a/src/renderer/components/+preferences/extension-telemetry-preference-item-registrator.injectable.ts b/src/renderer/components/+preferences/extension-telemetry-preference-item-registrator.injectable.ts new file mode 100644 index 0000000000..555cf829ef --- /dev/null +++ b/src/renderer/components/+preferences/extension-telemetry-preference-item-registrator.injectable.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { filter, map } from "lodash/fp"; +import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token"; +import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; +import { pipeline } from "@ogre-tools/fp"; +import { telemetryPreferenceItemInjectionToken } from "./telemetry-preference-items.injectable"; + +const extensionTelemetryPreferenceItemRegistratorInjectable = getInjectable({ + id: "extension-telemetry-preference-item-registrator", + + instantiate: + (di) => + (extension: LensRendererExtension, extensionInstallationCount) => { + const injectables = pipeline( + extension.appPreferences, + + filter( + (registration) => registration.showInPreferencesTab === "telemetry", + ), + + map((registration) => { + const id = `telemetry-preferences-item-${registration.id}-for-extension-${extension.sanitizedExtensionId}`; + + return getInjectable({ + id: `${id}-for-instance-${extensionInstallationCount}`, + injectionToken: telemetryPreferenceItemInjectionToken, + + instantiate: () => ({ + id: registration.id || id, + title: registration.title, + extension, + + components: { + Hint: registration.components.Hint, + Input: registration.components.Input, + }, + }), + }); + }), + ); + + injectables.forEach(di.register); + + return; + }, + + injectionToken: extensionRegistratorInjectionToken, +}); + +export default extensionTelemetryPreferenceItemRegistratorInjectable; diff --git a/src/renderer/components/+preferences/extensions.tsx b/src/renderer/components/+preferences/extensions.tsx index c3a2632905..930b74c8ee 100644 --- a/src/renderer/components/+preferences/extensions.tsx +++ b/src/renderer/components/+preferences/extensions.tsx @@ -8,33 +8,36 @@ import type { IComputedValue } from "mobx"; import { observer } from "mobx-react"; import React from "react"; import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration"; -import appPreferencesInjectable from "./app-preferences/app-preferences.injectable"; import { ExtensionSettings } from "./extension-settings"; +import { Preferences } from "./preferences"; +import extensionsPreferenceItemsInjectable from "./extension-preference-items.injectable"; interface Dependencies { - appPreferenceItems: IComputedValue; + preferenceItems: IComputedValue; } -const NonInjectedExtensions: React.FC = ({ appPreferenceItems }) => { - - const settings = appPreferenceItems.get(); - - return ( +const NonInjectedExtensions = ({ preferenceItems }: Dependencies) => ( +

Extensions

- {settings.filter(e => !e.showInPreferencesTab).map((setting) => - , - )} + {preferenceItems.get().map((preferenceItem) => ( + + ))}
- ); -}; +
+); export const Extensions = withInjectables( observer(NonInjectedExtensions), { getProps: (di) => ({ - appPreferenceItems: di.inject(appPreferencesInjectable), + preferenceItems: di.inject(extensionsPreferenceItemsInjectable), }), }, ); diff --git a/src/renderer/components/+preferences/helm-charts.tsx b/src/renderer/components/+preferences/helm-charts.tsx index 65c2bb6c3a..bd98679844 100644 --- a/src/renderer/components/+preferences/helm-charts.tsx +++ b/src/renderer/components/+preferences/helm-charts.tsx @@ -144,7 +144,8 @@ export class HelmCharts extends React.Component { return (
- userStore.downloadMirror = value} - disabled={!userStore.downloadKubectlBinaries} - isOptionDisabled={({ platforms }) => !platforms.has(process.platform)} - themeName="lens" - /> -
+ const save = () => { + userStore.downloadBinariesPath = downloadPath; + userStore.kubectlBinariesPath = binariesPath; + }; -
- - -
- The directory to download binaries into. -
-
+ return ( + <> +
+ + userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries} + > + Download kubectl binaries matching the Kubernetes cluster version + +
-
- - -
- - ); -}); +
+ + +
The directory to download binaries into.
+
+ +
+ + +
+ + ); + }, +); + +export const KubectlBinaries = withInjectables( + NonInjectedKubectlBinaries, + { + getProps: (di) => ({ + defaultPathForGeneralBinaries: di.inject(directoryForBinariesInjectable), + defaultPathForKubectlBinaries: di.inject(directoryForKubectlBinariesInjectable), + userStore: di.inject(userStoreInjectable), + }), + }, +); diff --git a/src/renderer/components/+preferences/kubernetes-preferences-route-component.injectable.ts b/src/renderer/components/+preferences/kubernetes-preferences-route-component.injectable.ts new file mode 100644 index 0000000000..51c33f73a6 --- /dev/null +++ b/src/renderer/components/+preferences/kubernetes-preferences-route-component.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 } from "@ogre-tools/injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import kubernetesPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/kubernetes/kubernetes-preferences-route.injectable"; +import { Kubernetes } from "./kubernetes"; + +const kubernetesPreferencesRouteComponentInjectable = getInjectable({ + id: "kubernetes-preferences-route-component", + + instantiate: (di) => ({ + route: di.inject(kubernetesPreferencesRouteInjectable), + Component: Kubernetes, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default kubernetesPreferencesRouteComponentInjectable; diff --git a/src/renderer/components/+preferences/kubernetes.tsx b/src/renderer/components/+preferences/kubernetes.tsx index a7002cacc7..aaaffab0aa 100644 --- a/src/renderer/components/+preferences/kubernetes.tsx +++ b/src/renderer/components/+preferences/kubernetes.tsx @@ -8,24 +8,25 @@ import React from "react"; import { HelmCharts } from "./helm-charts"; import { KubeconfigSyncs } from "./kubeconfig-syncs"; import { KubectlBinaries } from "./kubectl-binaries"; +import { Preferences } from "./preferences"; -export const Kubernetes = observer(() => { - return ( +export const Kubernetes = observer(() => ( +

Kubernetes

-
+

Kubeconfig Syncs

-
+

Helm Charts

- +
- ); -}); +
+)); diff --git a/src/renderer/components/+preferences/preferences-navigation/application-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/application-preferences-navigation-item.injectable.ts new file mode 100644 index 0000000000..0b333f4fed --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/application-preferences-navigation-item.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable"; +import appPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/app/app-preferences-route.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import { computed } from "mobx"; +import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable"; + +const applicationPreferencesNavigationItemInjectable = getInjectable({ + id: "application-preferences-navigation-item", + + instantiate: (di) => { + const route = di.inject(appPreferencesRouteInjectable); + const navigateToPreferenceTab = di.inject(navigateToPreferenceTabInjectable); + + const routeIsActive = di.inject( + routeIsActiveInjectable, + route, + ); + + return { + id: "application", + label: "App", + navigate: navigateToPreferenceTab(route), + isActive: routeIsActive, + isVisible: computed(() => true), + orderNumber: 10, + }; + }, + + injectionToken: preferenceNavigationItemInjectionToken, +}); + +export default applicationPreferencesNavigationItemInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/editor-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/editor-preferences-navigation-item.injectable.ts new file mode 100644 index 0000000000..f5738cf985 --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/editor-preferences-navigation-item.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import { computed } from "mobx"; +import editorPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/editor/editor-preferences-route.injectable"; +import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable"; + +const editorPreferencesNavigationItemInjectable = getInjectable({ + id: "editor-preferences-navigation-item", + + instantiate: (di) => { + const route = di.inject(editorPreferencesRouteInjectable); + const navigateToPreferenceTab = di.inject(navigateToPreferenceTabInjectable); + + const routeIsActive = di.inject( + routeIsActiveInjectable, + route, + ); + + return { + id: "editor", + label: "Editor", + navigate: navigateToPreferenceTab(route), + isActive: routeIsActive, + isVisible: computed(() => true), + orderNumber: 40, + }; + }, + + injectionToken: preferenceNavigationItemInjectionToken, +}); + +export default editorPreferencesNavigationItemInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/extensions-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/extensions-preferences-navigation-item.injectable.ts new file mode 100644 index 0000000000..029e9ba896 --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/extensions-preferences-navigation-item.injectable.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import { computed } from "mobx"; +import extensionsPreferenceItemsInjectable from "../extension-preference-items.injectable"; +import extensionPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable"; +import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable"; + +const extensionsPreferencesNavigationItemInjectable = getInjectable({ + id: "extension-preferences-navigation-item", + + instantiate: (di) => { + const preferenceItems = di.inject( + extensionsPreferenceItemsInjectable, + ); + + const navigateToPreferenceTab = di.inject( + navigateToPreferenceTabInjectable, + ); + + const route = di.inject( + extensionPreferencesRouteInjectable, + ); + + const routeIsActive = di.inject( + routeIsActiveInjectable, + route, + ); + + return { + id: "extensions", + label: "Extensions", + navigate: navigateToPreferenceTab(route), + isActive: routeIsActive, + + isVisible: computed( + () => preferenceItems.get().length > 0, + ), + + orderNumber: 70, + }; + }, + + injectionToken: preferenceNavigationItemInjectionToken, +}); + +export default extensionsPreferencesNavigationItemInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/kubernetes-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/kubernetes-preferences-navigation-item.injectable.ts new file mode 100644 index 0000000000..ec3429a212 --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/kubernetes-preferences-navigation-item.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 } from "@ogre-tools/injectable"; +import { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import { computed } from "mobx"; +import kubernetesPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/kubernetes/kubernetes-preferences-route.injectable"; +import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable"; + +const kubernetesPreferencesNavigationItemInjectable = getInjectable({ + id: "kubernetes-preferences-navigation-item", + + instantiate: (di) => { + const navigateToPreferenceTab = di.inject(navigateToPreferenceTabInjectable); + + const route = di.inject( + kubernetesPreferencesRouteInjectable, + ); + + const routeIsActive = di.inject( + routeIsActiveInjectable, + route, + ); + + return { + id: "kubernetes", + label: "Kubernetes", + navigate: navigateToPreferenceTab(route), + isActive: routeIsActive, + isVisible: computed(() => true), + orderNumber: 30, + }; + }, + + injectionToken: preferenceNavigationItemInjectionToken, +}); + +export default kubernetesPreferencesNavigationItemInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/navigate-to-preference-tab.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/navigate-to-preference-tab.injectable.ts new file mode 100644 index 0000000000..05a20dab29 --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/navigate-to-preference-tab.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 } from "@ogre-tools/injectable"; +import type { Route } from "../../../../common/front-end-routing/route-injection-token"; +import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token"; + +const navigateToPreferenceTabInjectable = getInjectable({ + id: "navigate-to-preference-tab", + + instantiate: (di) => { + const navigateToRoute = di.inject(navigateToRouteInjectionToken); + + return (route: Route) => () => { + navigateToRoute(route, { withoutAffectingBackButton: true }); + }; + }, +}); + +export default navigateToPreferenceTabInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable.ts new file mode 100644 index 0000000000..28eaa06efd --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { pipeline } from "@ogre-tools/fp"; +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; +import { filter, orderBy } from "lodash/fp"; +import { computed, IComputedValue } from "mobx"; + +export const preferenceNavigationItemInjectionToken = + getInjectionToken({ + id: "preference-navigation-item-injection-token", + }); + +export interface PreferenceNavigationItem { + id: string; + label: string; + isActive: IComputedValue; + isVisible: IComputedValue; + navigate: () => void; + orderNumber: number; +} + +const preferenceNavigationItemsInjectable = getInjectable({ + id: "preference-navigation-items", + + instantiate: (di) => + computed((): PreferenceNavigationItem[] => + pipeline( + di.injectMany(preferenceNavigationItemInjectionToken), + filter((item) => !!item.isVisible.get()), + (items) => orderBy([(item) => item.orderNumber], ["asc"], items), + ), + ), +}); + +export default preferenceNavigationItemsInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/preferences-navigation.tsx b/src/renderer/components/+preferences/preferences-navigation/preferences-navigation.tsx new file mode 100644 index 0000000000..ef0f85a263 --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/preferences-navigation.tsx @@ -0,0 +1,61 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { Tab, Tabs } from "../../tabs"; + +import { withInjectables } from "@ogre-tools/injectable-react"; +import type { IComputedValue } from "mobx"; +import React from "react"; +import preferenceNavigationItemsInjectable, { + PreferenceNavigationItem, +} from "./preference-navigation-items.injectable"; + +import { observer } from "mobx-react"; + +interface Dependencies { + navigationItems: IComputedValue; +} + +const NonInjectedPreferencesNavigation = ({ + navigationItems, +}: Dependencies) => ( + item.navigate()} + > +
Preferences
+ + {navigationItems.get().map((item) => ( + + ))} +
+); + +interface PreferenceNavigationTabProps extends React.DOMAttributes { + item: PreferenceNavigationItem; +} + +const PreferencesNavigationTab = observer(({ item }: PreferenceNavigationTabProps) => ( + +)); + +export const PreferencesNavigation = withInjectables( + NonInjectedPreferencesNavigation, + + { + getProps: (di) => ({ + navigationItems: di.inject(preferenceNavigationItemsInjectable), + }), + }, +); diff --git a/src/renderer/components/+preferences/preferences-navigation/proxy-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/proxy-preferences-navigation-item.injectable.ts new file mode 100644 index 0000000000..d4a588db87 --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/proxy-preferences-navigation-item.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import { computed } from "mobx"; +import proxyPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/proxy/proxy-preferences-route.injectable"; +import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable"; + +const proxyPreferencesNavigationItemInjectable = getInjectable({ + id: "proxy-preferences-navigation-item", + + instantiate: (di) => { + const route = di.inject(proxyPreferencesRouteInjectable); + + const navigateToPreferenceTab = di.inject( + navigateToPreferenceTabInjectable, + ); + + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return { + id: "proxy", + label: "Proxy", + navigate: navigateToPreferenceTab(route), + isActive: routeIsActive, + isVisible: computed(() => true), + orderNumber: 20, + }; + }, + + injectionToken: preferenceNavigationItemInjectionToken, +}); + +export default proxyPreferencesNavigationItemInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/telemetry-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/telemetry-preferences-navigation-item.injectable.ts new file mode 100644 index 0000000000..f6576696c6 --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/telemetry-preferences-navigation-item.injectable.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import { computed } from "mobx"; +import telemetryPreferenceItemsInjectable from "../telemetry-preference-items.injectable"; +import telemetryPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/telemetry/telemetry-preferences-route.injectable"; +import sentryDnsUrlInjectable from "../sentry-dns-url.injectable"; +import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable"; + +const terminalPreferencesNavigationItemInjectable = getInjectable({ + id: "telemetry-preferences-navigation-item", + + instantiate: (di) => { + const sentryDnsUrl = di.inject(sentryDnsUrlInjectable); + + const telemetryPreferenceItems = di.inject( + telemetryPreferenceItemsInjectable, + ); + + const navigateToPreferenceTab = di.inject( + navigateToPreferenceTabInjectable, + ); + + const route = di.inject(telemetryPreferencesRouteInjectable); + + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return { + id: "telemetry", + label: "Telemetry", + navigate: navigateToPreferenceTab(route), + isActive: routeIsActive, + + isVisible: computed( + () => !!sentryDnsUrl || telemetryPreferenceItems.get().length > 0, + ), + + orderNumber: 60, + }; + }, + + injectionToken: preferenceNavigationItemInjectionToken, +}); + +export default terminalPreferencesNavigationItemInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/terminal-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/terminal-preferences-navigation-item.injectable.ts new file mode 100644 index 0000000000..b623148c10 --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/terminal-preferences-navigation-item.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import { computed } from "mobx"; +import terminalPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/terminal/terminal-preferences-route.injectable"; +import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable"; + +const terminalPreferencesNavigationItemInjectable = getInjectable({ + id: "terminal-preferences-navigation-item", + + instantiate: (di) => { + const navigateToPreferenceTab = di.inject( + navigateToPreferenceTabInjectable, + ); + + const route = di.inject(terminalPreferencesRouteInjectable); + + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return { + id: "terminal", + label: "Terminal", + navigate: navigateToPreferenceTab(route), + isActive: routeIsActive, + isVisible: computed(() => true), + orderNumber: 50, + }; + }, + + injectionToken: preferenceNavigationItemInjectionToken, +}); + +export default terminalPreferencesNavigationItemInjectable; diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx index cb469b062c..ab40a5ba13 100644 --- a/src/renderer/components/+preferences/preferences.tsx +++ b/src/renderer/components/+preferences/preferences.tsx @@ -3,99 +3,46 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import "./preferences.scss"; - -import type { IComputedValue } from "mobx"; -import { observer } from "mobx-react"; import React from "react"; -import { matchPath, Redirect, Route, RouteProps, Switch } from "react-router"; -import { - appRoute, - appURL, - editorURL, - extensionRoute, - extensionURL, - kubernetesRoute, - kubernetesURL, - preferencesURL, - proxyRoute, - proxyURL, - editorRoute, - telemetryRoute, - telemetryURL, - terminalRoute, - terminalURL, -} from "../../../common/routes"; -import { navigateWithoutHistoryChange, navigation } from "../../navigation"; -import { SettingLayout } from "../layout/setting-layout"; -import { Tab, Tabs } from "../tabs"; -import { Application } from "./application"; -import { Kubernetes } from "./kubernetes"; -import { Editor } from "./editor"; -import { Terminal } from "./terminal"; -import { LensProxy } from "./proxy"; -import { Telemetry } from "./telemetry"; -import { Extensions } from "./extensions"; -import { sentryDsn } from "../../../common/vars"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration"; -import appPreferencesInjectable from "./app-preferences/app-preferences.injectable"; -interface Dependencies { - appPreferenceItems: IComputedValue; +import { SettingLayout } from "../layout/setting-layout"; +import { PreferencesNavigation } from "./preferences-navigation/preferences-navigation"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import closePreferencesInjectable from "./close-preferences.injectable"; + +interface PreferencesProps extends React.DOMAttributes { + children: React.ReactNode; } -const NonInjectedPreferences: React.FC = ({ appPreferenceItems }) => { +interface Dependencies { + closePreferences: () => void; +} - function renderNavigation() { - const extensions = appPreferenceItems.get(); - const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == "telemetry"); - const currentLocation = navigation.location.pathname; - const isActive = (route: RouteProps) => !!matchPath(currentLocation, { path: route.path, exact: route.exact }); +const NonInjectedPreferences = ({ + children, + closePreferences, + ...props +}: PreferencesProps & Dependencies) => ( + } + className="Preferences" + contentGaps={false} + closeButtonProps={{ "data-testid": "close-preferences" }} + back={closePreferences} + {...props} + > + {children} + +); - return ( - navigateWithoutHistoryChange({ pathname: url })}> -
Preferences
- - - - - - {(telemetryExtensions.length > 0 || !!sentryDsn) && - - } - {extensions.filter(e => !e.showInPreferencesTab).length > 0 && - - } -
- ); - } - - return ( - - - - - - - - - - - - - ); -}; - -export const Preferences = withInjectables( - observer(NonInjectedPreferences), +export const Preferences = withInjectables( + NonInjectedPreferences, { - getProps: (di) => ({ - appPreferenceItems: di.inject(appPreferencesInjectable), + getProps: (di, props) => ({ + closePreferences: di.inject(closePreferencesInjectable), + ...props, }), }, ); + diff --git a/src/renderer/components/+preferences/proxy-preferences-route-component.injectable.ts b/src/renderer/components/+preferences/proxy-preferences-route-component.injectable.ts new file mode 100644 index 0000000000..62540dfc42 --- /dev/null +++ b/src/renderer/components/+preferences/proxy-preferences-route-component.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 } from "@ogre-tools/injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import proxyPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/proxy/proxy-preferences-route.injectable"; +import { LensProxy } from "./proxy"; + +const proxyPreferencesRouteComponentInjectable = getInjectable({ + id: "proxy-preferences-route-component", + + instantiate: (di) => ({ + route: di.inject(proxyPreferencesRouteInjectable), + Component: LensProxy, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default proxyPreferencesRouteComponentInjectable; diff --git a/src/renderer/components/+preferences/proxy.tsx b/src/renderer/components/+preferences/proxy.tsx index 69db754d24..1ce4cbc4cf 100644 --- a/src/renderer/components/+preferences/proxy.tsx +++ b/src/renderer/components/+preferences/proxy.tsx @@ -2,48 +2,66 @@ * 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 { UserStore } from "../../../common/user-store"; +import type { UserStore } from "../../../common/user-store"; import { Input } from "../input"; import { SubTitle } from "../layout/sub-title"; import { Switch } from "../switch"; +import { Preferences } from "./preferences"; +import userStoreInjectable from "../../../common/user-store/user-store.injectable"; -export const LensProxy = observer(() => { - const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || ""); - const store = UserStore.getInstance(); +interface Dependencies { + userStore: UserStore; +} + +const NonInjectedLensProxy = observer(({ userStore }: Dependencies) => { + const [proxy, setProxy] = React.useState(userStore.httpsProxy || ""); return ( -
-
-

Proxy

- - setProxy(v)} - onBlur={() => UserStore.getInstance().httpsProxy = proxy} - /> - - Proxy is used only for non-cluster communication. - -
+ +
+
+

Proxy

+ + setProxy(v)} + onBlur={() => userStore.httpsProxy = proxy} + /> + + Proxy is used only for non-cluster communication. + +
-
+
-
- - store.allowUntrustedCAs = !store.allowUntrustedCAs}> - Allow untrusted Certificate Authorities - - - This will make Lens to trust ANY certificate authority without any validations.{" "} - Needed with some corporate proxies that do certificate re-writing.{" "} - Does not affect cluster communications! - +
+ + userStore.allowUntrustedCAs = !userStore.allowUntrustedCAs}> + Allow untrusted Certificate Authorities + + + This will make Lens to trust ANY certificate authority without any validations.{" "} + Needed with some corporate proxies that do certificate re-writing.{" "} + Does not affect cluster communications! + +
-
+
); }); + +export const LensProxy = withInjectables( + NonInjectedLensProxy, + + { + getProps: (di) => ({ + userStore: di.inject(userStoreInjectable), + }), + }, +); diff --git a/src/renderer/components/+preferences/sentry-dns-url.injectable.ts b/src/renderer/components/+preferences/sentry-dns-url.injectable.ts new file mode 100644 index 0000000000..5f337436fc --- /dev/null +++ b/src/renderer/components/+preferences/sentry-dns-url.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { sentryDsn } from "../../../common/vars"; + +const sentryDnsUrlInjectable = getInjectable({ + id: "sentry-dns-url", + instantiate: () => sentryDsn, +}); + +export default sentryDnsUrlInjectable; diff --git a/src/renderer/components/+preferences/telemetry-preference-items.injectable.ts b/src/renderer/components/+preferences/telemetry-preference-items.injectable.ts new file mode 100644 index 0000000000..421a45b83f --- /dev/null +++ b/src/renderer/components/+preferences/telemetry-preference-items.injectable.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; +import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable"; +import { pipeline } from "@ogre-tools/fp"; +import { filter, overSome } from "lodash/fp"; +import { computed } from "mobx"; +import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; +import type { AppPreferenceRegistration } from "./app-preferences/app-preference-registration"; + +export interface ExtensionTelemetryPreferenceRegistration extends AppPreferenceRegistration { + extension?: LensRendererExtension; +} + +export const telemetryPreferenceItemInjectionToken = getInjectionToken({ + id: "telemetry-preference-item-injection-token", +}); + +const telemetryPreferenceItemsInjectable = getInjectable({ + id: "telemetry-preference-items", + instantiate: (di) => { + const extensions = di.inject(rendererExtensionsInjectable); + + return computed(() => { + const enabledExtensions = extensions.get(); + + return pipeline( + di.injectMany(telemetryPreferenceItemInjectionToken), + filter((item) => + overSome([ + isNonExtensionItem, + isEnabledExtensionItemFor(enabledExtensions), + ])(item), + ), + ); + }); + }, +}); + + +const isNonExtensionItem = (item: ExtensionTelemetryPreferenceRegistration) => !item.extension; + +const isEnabledExtensionItemFor = + (enabledExtensions: LensRendererExtension[]) => (item: ExtensionTelemetryPreferenceRegistration) => + !!enabledExtensions.find((extension) => extension === item.extension); + + +export default telemetryPreferenceItemsInjectable; diff --git a/src/renderer/components/+preferences/telemetry-preferences-route-component.injectable.ts b/src/renderer/components/+preferences/telemetry-preferences-route-component.injectable.ts new file mode 100644 index 0000000000..6d78ecfccc --- /dev/null +++ b/src/renderer/components/+preferences/telemetry-preferences-route-component.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 } from "@ogre-tools/injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import telemetryPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/telemetry/telemetry-preferences-route.injectable"; +import { Telemetry } from "./telemetry"; + +const telemetryPreferencesRouteComponentInjectable = getInjectable({ + id: "telemetry-preferences-route-component", + + instantiate: (di) => ({ + route: di.inject(telemetryPreferencesRouteInjectable), + Component: Telemetry, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default telemetryPreferencesRouteComponentInjectable; diff --git a/src/renderer/components/+preferences/telemetry.tsx b/src/renderer/components/+preferences/telemetry.tsx index 984b8e60d5..c568f410c6 100644 --- a/src/renderer/components/+preferences/telemetry.tsx +++ b/src/renderer/components/+preferences/telemetry.tsx @@ -4,52 +4,67 @@ */ import { observer } from "mobx-react"; import React from "react"; -import { UserStore } from "../../../common/user-store"; -import { sentryDsn } from "../../../common/vars"; +import type { UserStore } from "../../../common/user-store"; import { Checkbox } from "../checkbox"; import { SubTitle } from "../layout/sub-title"; import { ExtensionSettings } from "./extension-settings"; -import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration"; -import appPreferencesInjectable from "./app-preferences/app-preferences.injectable"; import type { IComputedValue } from "mobx"; import { withInjectables } from "@ogre-tools/injectable-react"; +import { Preferences } from "./preferences"; +import type { ExtensionTelemetryPreferenceRegistration } from "./telemetry-preference-items.injectable"; +import telemetryPreferenceItemsInjectable from "./telemetry-preference-items.injectable"; +import sentryDnsUrlInjectable from "./sentry-dns-url.injectable"; +import userStoreInjectable from "../../../common/user-store/user-store.injectable"; interface Dependencies { - appPreferenceItems: IComputedValue; + telemetryPreferenceItems: IComputedValue; + sentryDnsUrl: string; + userStore: UserStore; } -const NonInjectedTelemetry: React.FC = ({ appPreferenceItems }) => { - const extensions = appPreferenceItems.get(); - const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == "telemetry"); - +const NonInjectedTelemetry: React.FC = ({ + telemetryPreferenceItems, + sentryDnsUrl, + userStore, +}) => { return ( -
-

Telemetry

- {telemetryExtensions.map((extension) => )} - {sentryDsn ? ( - -
- - { - UserStore.getInstance().allowErrorReporting = value; - }} - /> -
- - Automatic error reports provide vital information about issues and application crashes. - It is highly recommended to keep this feature enabled to ensure fast turnaround for issues you might encounter. - -
-
-
-
) : - // we don't need to shows the checkbox at all if Sentry dsn is not a valid url - null - } -
+ +
+

Telemetry

+ {telemetryPreferenceItems.get().map((item) => ( + + ))} + {sentryDnsUrl ? ( + +
+ + { + userStore.allowErrorReporting = value; + }} + /> +
+ + Automatic error reports provide vital information about issues + and application crashes. It is highly recommended to keep this + feature enabled to ensure fast turnaround for issues you might + encounter. + +
+
+
+
+ ) : // we don't need to shows the checkbox at all if Sentry dsn is not a valid url + null} +
+
); }; @@ -58,7 +73,9 @@ export const Telemetry = withInjectables( { getProps: (di) => ({ - appPreferenceItems: di.inject(appPreferencesInjectable), + telemetryPreferenceItems: di.inject(telemetryPreferenceItemsInjectable), + sentryDnsUrl: di.inject(sentryDnsUrlInjectable), + userStore: di.inject(userStoreInjectable), }), }, ); diff --git a/src/renderer/components/+preferences/terminal-preferences-route-component.injectable.ts b/src/renderer/components/+preferences/terminal-preferences-route-component.injectable.ts new file mode 100644 index 0000000000..e23a4520a8 --- /dev/null +++ b/src/renderer/components/+preferences/terminal-preferences-route-component.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 } from "@ogre-tools/injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; +import terminalPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/terminal/terminal-preferences-route.injectable"; +import { Terminal } from "./terminal"; + +const terminalPreferencesRouteComponentInjectable = getInjectable({ + id: "terminal-preferences-route-component", + + instantiate: (di) => ({ + route: di.inject(terminalPreferencesRouteInjectable), + Component: Terminal, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default terminalPreferencesRouteComponentInjectable; diff --git a/src/renderer/components/+preferences/terminal.tsx b/src/renderer/components/+preferences/terminal.tsx index f9f1226609..08adb55ee3 100644 --- a/src/renderer/components/+preferences/terminal.tsx +++ b/src/renderer/components/+preferences/terminal.tsx @@ -5,80 +5,98 @@ import React from "react"; import { observer } from "mobx-react"; -import { UserStore } from "../../../common/user-store"; +import type { UserStore } from "../../../common/user-store"; import { SubTitle } from "../layout/sub-title"; import { Input, InputValidators } from "../input"; -import { isWindows } from "../../../common/vars"; import { Switch } from "../switch"; import { Select } from "../select"; -import { ThemeStore } from "../../theme.store"; +import type { ThemeStore } from "../../theme.store"; +import { Preferences } from "./preferences"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import userStoreInjectable from "../../../common/user-store/user-store.injectable"; +import themeStoreInjectable from "../../theme-store.injectable"; +import defaultShellInjectable from "./default-shell.injectable"; -export const Terminal = observer(() => { - const userStore = UserStore.getInstance(); - const themeStore = ThemeStore.getInstance(); - const defaultShell = process.env.SHELL - || process.env.PTYSHELL - || ( - isWindows - ? "powershell.exe" - : "System default shell" +interface Dependencies { + userStore: UserStore; + themeStore: ThemeStore; + defaultShell: string; +} + +const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: Dependencies) => { + return ( + +
+

Terminal

+ +
+ + userStore.shell = value} + /> +
+ +
+ + userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect} + > + Copy on select and paste on right-click + +
+ +
+ + userStore.terminalConfig.fontSize = Number(value)} + /> +
+
+ + userStore.terminalConfig.fontFamily = value} + /> +
+
+
); - - return (
-

Terminal

- -
- - userStore.shell = value} - /> -
- -
- - userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect} - > - Copy on select and paste on right-click - -
- -
- - userStore.terminalConfig.fontSize=Number(value)} - /> -
-
- - userStore.terminalConfig.fontFamily=value} - /> -
-
); }); + +export const Terminal = withInjectables( + NonInjectedTerminal, + + { + getProps: (di) => ({ + userStore: di.inject(userStoreInjectable), + themeStore: di.inject(themeStoreInjectable), + defaultShell: di.inject(defaultShellInjectable), + }), + }, +); + diff --git a/src/renderer/components/+storage-classes/storage-classes-route-component.injectable.ts b/src/renderer/components/+storage-classes/storage-classes-route-component.injectable.ts new file mode 100644 index 0000000000..c935983d78 --- /dev/null +++ b/src/renderer/components/+storage-classes/storage-classes-route-component.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 } from "@ogre-tools/injectable"; +import { StorageClasses } from "./storage-classes"; +import storageClassesRouteInjectable from "../../../common/front-end-routing/routes/cluster/storage/storage-classes/storage-classes-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const storageClassesRouteComponentInjectable = getInjectable({ + id: "storage-classes-route-component", + + instantiate: (di) => ({ + route: di.inject(storageClassesRouteInjectable), + Component: StorageClasses, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default storageClassesRouteComponentInjectable; diff --git a/src/renderer/components/+storage-classes/storage-classes-sidebar-items.injectable.tsx b/src/renderer/components/+storage-classes/storage-classes-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..e09d20897e --- /dev/null +++ b/src/renderer/components/+storage-classes/storage-classes-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import storageClassesRouteInjectable from "../../../common/front-end-routing/routes/cluster/storage/storage-classes/storage-classes-route.injectable"; +import { storageSidebarItemId } from "../+storage/storage-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToStorageClassesInjectable from "../../../common/front-end-routing/routes/cluster/storage/storage-classes/navigate-to-storage-classes.injectable"; + +const storageClassesSidebarItemsInjectable = getInjectable({ + id: "storage-classes-sidebar-items", + + instantiate: (di) => { + const route = di.inject(storageClassesRouteInjectable); + const navigateToStorageClasses = di.inject(navigateToStorageClassesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "storage-classes", + parentId: storageSidebarItemId, + title: "Storage Classes", + onClick: navigateToStorageClasses, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 30, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default storageClassesSidebarItemsInjectable; diff --git a/src/renderer/components/+storage-classes/storage-classes.tsx b/src/renderer/components/+storage-classes/storage-classes.tsx index 5da1239b43..3eff2fecd3 100644 --- a/src/renderer/components/+storage-classes/storage-classes.tsx +++ b/src/renderer/components/+storage-classes/storage-classes.tsx @@ -6,12 +6,11 @@ import "./storage-classes.scss"; import React from "react"; -import type { RouteComponentProps } from "react-router-dom"; import { observer } from "mobx-react"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { storageClassStore } from "./storage-class.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { StorageClassesRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -22,46 +21,45 @@ enum columnId { reclaimPolicy = "reclaim", } -export interface StorageClassesProps extends RouteComponentProps { -} - @observer -export class StorageClasses extends React.Component { +export class StorageClasses extends React.Component { render() { return ( - storageClass.getName(), - [columnId.age]: storageClass => -storageClass.getCreationTimestamp(), - [columnId.provisioner]: storageClass => storageClass.provisioner, - [columnId.reclaimPolicy]: storageClass => storageClass.reclaimPolicy, - }} - searchFilters={[ - storageClass => storageClass.getSearchFields(), - storageClass => storageClass.provisioner, - ]} - renderHeaderTitle="Storage Classes" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Provisioner", className: "provisioner", sortBy: columnId.provisioner, id: columnId.provisioner }, - { title: "Reclaim Policy", className: "reclaim-policy", sortBy: columnId.reclaimPolicy, id: columnId.reclaimPolicy }, - { title: "Default", className: "is-default", id: columnId.default }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={storageClass => [ - storageClass.getName(), - , - storageClass.provisioner, - storageClass.getReclaimPolicy(), - storageClass.isDefault() ? "Yes" : null, - , - ]} - /> + + storageClass.getName(), + [columnId.age]: storageClass => -storageClass.getCreationTimestamp(), + [columnId.provisioner]: storageClass => storageClass.provisioner, + [columnId.reclaimPolicy]: storageClass => storageClass.reclaimPolicy, + }} + searchFilters={[ + storageClass => storageClass.getSearchFields(), + storageClass => storageClass.provisioner, + ]} + renderHeaderTitle="Storage Classes" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Provisioner", className: "provisioner", sortBy: columnId.provisioner, id: columnId.provisioner }, + { title: "Reclaim Policy", className: "reclaim-policy", sortBy: columnId.reclaimPolicy, id: columnId.reclaimPolicy }, + { title: "Default", className: "is-default", id: columnId.default }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={storageClass => [ + storageClass.getName(), + , + storageClass.provisioner, + storageClass.getReclaimPolicy(), + storageClass.isDefault() ? "Yes" : null, + , + ]} + /> + ); } } diff --git a/src/renderer/components/+storage-volume-claims/persistent-volume-claims-route-component.injectable.ts b/src/renderer/components/+storage-volume-claims/persistent-volume-claims-route-component.injectable.ts new file mode 100644 index 0000000000..2c5cef3fcb --- /dev/null +++ b/src/renderer/components/+storage-volume-claims/persistent-volume-claims-route-component.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 } from "@ogre-tools/injectable"; +import { PersistentVolumeClaims } from "./volume-claims"; +import persistentVolumeClaimsRouteInjectable from "../../../common/front-end-routing/routes/cluster/storage/persistent-volume-claims/persistent-volume-claims-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const persistentVolumeClaimsRouteComponentInjectable = getInjectable({ + id: "persistent-volume-claims-route-component", + + instantiate: (di) => ({ + route: di.inject(persistentVolumeClaimsRouteInjectable), + Component: PersistentVolumeClaims, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default persistentVolumeClaimsRouteComponentInjectable; diff --git a/src/renderer/components/+storage-volume-claims/persistent-volume-claims-sidebar-items.injectable.tsx b/src/renderer/components/+storage-volume-claims/persistent-volume-claims-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..a3b6742347 --- /dev/null +++ b/src/renderer/components/+storage-volume-claims/persistent-volume-claims-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import persistentVolumeClaimsRouteInjectable from "../../../common/front-end-routing/routes/cluster/storage/persistent-volume-claims/persistent-volume-claims-route.injectable"; +import { storageSidebarItemId } from "../+storage/storage-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToPersistentVolumeClaimsInjectable from "../../../common/front-end-routing/routes/cluster/storage/persistent-volume-claims/navigate-to-persistent-volume-claims.injectable"; + +const persistentVolumeClaimsSidebarItemsInjectable = getInjectable({ + id: "persistent-volume-claims-sidebar-items", + + instantiate: (di) => { + const route = di.inject(persistentVolumeClaimsRouteInjectable); + const navigateToPersistentVolumeClaims = di.inject(navigateToPersistentVolumeClaimsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "persistent-volume-claims", + parentId: storageSidebarItemId, + title: "Persistent Volume Claims", + onClick: navigateToPersistentVolumeClaims, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 10, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default persistentVolumeClaimsSidebarItemsInjectable; diff --git a/src/renderer/components/+storage-volume-claims/volume-claims.tsx b/src/renderer/components/+storage-volume-claims/volume-claims.tsx index 4e3da7fa24..2365d107fc 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claims.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claims.tsx @@ -7,7 +7,7 @@ import "./volume-claims.scss"; import React from "react"; import { observer } from "mobx-react"; -import { Link, RouteComponentProps } from "react-router-dom"; +import { Link } from "react-router-dom"; import { volumeClaimStore } from "./volume-claim.store"; import { podsStore } from "../+workloads-pods/pods.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; @@ -15,8 +15,8 @@ import { unitsToBytes } from "../../../common/utils/convertMemory"; import { stopPropagation } from "../../utils"; import { storageClassApi } from "../../../common/k8s-api/endpoints"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { VolumeClaimsRouteParams } from "../../../common/routes"; import { getDetailsUrl } from "../kube-detail-params"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -29,68 +29,67 @@ enum columnId { age = "age", } -export interface PersistentVolumeClaimsProps extends RouteComponentProps { -} - @observer -export class PersistentVolumeClaims extends React.Component { +export class PersistentVolumeClaims extends React.Component { render() { return ( - pvc.getName(), - [columnId.namespace]: pvc => pvc.getNs(), - [columnId.pods]: pvc => pvc.getPods(podsStore.items).map(pod => pod.getName()), - [columnId.status]: pvc => pvc.getStatus(), - [columnId.size]: pvc => unitsToBytes(pvc.getStorage()), - [columnId.storageClass]: pvc => pvc.spec.storageClassName, - [columnId.age]: pvc => -pvc.getCreationTimestamp(), - }} - searchFilters={[ - pvc => pvc.getSearchFields(), - pvc => pvc.getPods(podsStore.items).map(pod => pod.getName()), - ]} - renderHeaderTitle="Persistent Volume Claims" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Storage class", className: "storageClass", sortBy: columnId.storageClass, id: columnId.storageClass }, - { title: "Size", className: "size", sortBy: columnId.size, id: columnId.size }, - { title: "Pods", className: "pods", sortBy: columnId.pods, id: columnId.pods }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, - ]} - renderTableContents={pvc => { - const pods = pvc.getPods(podsStore.items); - const { storageClassName } = pvc.spec; - const storageClassDetailsUrl = getDetailsUrl(storageClassApi.getUrl({ - name: storageClassName, - })); + + pvc.getName(), + [columnId.namespace]: pvc => pvc.getNs(), + [columnId.pods]: pvc => pvc.getPods(podsStore.items).map(pod => pod.getName()), + [columnId.status]: pvc => pvc.getStatus(), + [columnId.size]: pvc => unitsToBytes(pvc.getStorage()), + [columnId.storageClass]: pvc => pvc.spec.storageClassName, + [columnId.age]: pvc => -pvc.getCreationTimestamp(), + }} + searchFilters={[ + pvc => pvc.getSearchFields(), + pvc => pvc.getPods(podsStore.items).map(pod => pod.getName()), + ]} + renderHeaderTitle="Persistent Volume Claims" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Storage class", className: "storageClass", sortBy: columnId.storageClass, id: columnId.storageClass }, + { title: "Size", className: "size", sortBy: columnId.size, id: columnId.size }, + { title: "Pods", className: "pods", sortBy: columnId.pods, id: columnId.pods }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, + ]} + renderTableContents={pvc => { + const pods = pvc.getPods(podsStore.items); + const { storageClassName } = pvc.spec; + const storageClassDetailsUrl = getDetailsUrl(storageClassApi.getUrl({ + name: storageClassName, + })); - return [ - pvc.getName(), - , - pvc.getNs(), - - {storageClassName} - , - pvc.getStorage(), - pods.map(pod => ( - - {pod.getName()} - - )), - , - { title: pvc.getStatus(), className: pvc.getStatus().toLowerCase() }, - ]; - }} - /> + return [ + pvc.getName(), + , + pvc.getNs(), + + {storageClassName} + , + pvc.getStorage(), + pods.map(pod => ( + + {pod.getName()} + + )), + , + { title: pvc.getStatus(), className: pvc.getStatus().toLowerCase() }, + ]; + }} + /> + ); } } diff --git a/src/renderer/components/+storage-volumes/persistent-volumes-route-component.injectable.ts b/src/renderer/components/+storage-volumes/persistent-volumes-route-component.injectable.ts new file mode 100644 index 0000000000..4ef6443384 --- /dev/null +++ b/src/renderer/components/+storage-volumes/persistent-volumes-route-component.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 } from "@ogre-tools/injectable"; +import { PersistentVolumes } from "./volumes"; + +import persistentVolumesRouteInjectable from "../../../common/front-end-routing/routes/cluster/storage/persistent-volumes/persistent-volumes-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const persistentVolumesRouteComponentInjectable = getInjectable({ + id: "persistent-volumes-route-component", + + instantiate: (di) => ({ + route: di.inject(persistentVolumesRouteInjectable), + Component: PersistentVolumes, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default persistentVolumesRouteComponentInjectable; diff --git a/src/renderer/components/+storage-volumes/persistent-volumes-sidebar-items.injectable.tsx b/src/renderer/components/+storage-volumes/persistent-volumes-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..950882383d --- /dev/null +++ b/src/renderer/components/+storage-volumes/persistent-volumes-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import persistentVolumesRouteInjectable from "../../../common/front-end-routing/routes/cluster/storage/persistent-volumes/persistent-volumes-route.injectable"; +import { storageSidebarItemId } from "../+storage/storage-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToPersistentVolumesInjectable from "../../../common/front-end-routing/routes/cluster/storage/persistent-volumes/navigate-to-persistent-volumes.injectable"; + +const persistentVolumesSidebarItemsInjectable = getInjectable({ + id: "persistent-volumes-sidebar-items", + + instantiate: (di) => { + const route = di.inject(persistentVolumesRouteInjectable); + const navigateToPersistentVolumes = di.inject(navigateToPersistentVolumesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "persistent-volumes", + parentId: storageSidebarItemId, + title: "Persistent Volumes", + onClick: navigateToPersistentVolumes, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 20, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default persistentVolumesSidebarItemsInjectable; diff --git a/src/renderer/components/+storage-volumes/volumes.tsx b/src/renderer/components/+storage-volumes/volumes.tsx index ab44b4cf3f..1f5a744f0d 100644 --- a/src/renderer/components/+storage-volumes/volumes.tsx +++ b/src/renderer/components/+storage-volumes/volumes.tsx @@ -7,14 +7,14 @@ import "./volumes.scss"; import React from "react"; import { observer } from "mobx-react"; -import { Link, RouteComponentProps } from "react-router-dom"; +import { Link } from "react-router-dom"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { getDetailsUrl } from "../kube-detail-params"; import { stopPropagation } from "../../utils"; import { volumesStore } from "./volumes.store"; import { pvcApi, storageClassApi } from "../../../common/k8s-api/endpoints"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { VolumesRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -26,62 +26,61 @@ enum columnId { age = "age", } -export interface PersistentVolumesProps extends RouteComponentProps { -} - @observer -export class PersistentVolumes extends React.Component { +export class PersistentVolumes extends React.Component { render() { return ( - volume.getName(), - [columnId.storageClass]: volume => volume.getStorageClass(), - [columnId.capacity]: volume => volume.getCapacity(true), - [columnId.status]: volume => volume.getStatus(), - [columnId.age]: volume => -volume.getCreationTimestamp(), - }} - searchFilters={[ - volume => volume.getSearchFields(), - volume => volume.getClaimRefName(), - ]} - renderHeaderTitle="Persistent Volumes" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Storage Class", className: "storageClass", sortBy: columnId.storageClass, id: columnId.storageClass }, - { title: "Capacity", className: "capacity", sortBy: columnId.capacity, id: columnId.capacity }, - { title: "Claim", className: "claim", id: columnId.claim }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, - ]} - renderTableContents={volume => { - const { claimRef, storageClassName } = volume.spec; - const storageClassDetailsUrl = getDetailsUrl(storageClassApi.getUrl({ - name: storageClassName, - })); + + volume.getName(), + [columnId.storageClass]: volume => volume.getStorageClass(), + [columnId.capacity]: volume => volume.getCapacity(true), + [columnId.status]: volume => volume.getStatus(), + [columnId.age]: volume => -volume.getCreationTimestamp(), + }} + searchFilters={[ + volume => volume.getSearchFields(), + volume => volume.getClaimRefName(), + ]} + renderHeaderTitle="Persistent Volumes" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Storage Class", className: "storageClass", sortBy: columnId.storageClass, id: columnId.storageClass }, + { title: "Capacity", className: "capacity", sortBy: columnId.capacity, id: columnId.capacity }, + { title: "Claim", className: "claim", id: columnId.claim }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, + ]} + renderTableContents={volume => { + const { claimRef, storageClassName } = volume.spec; + const storageClassDetailsUrl = getDetailsUrl(storageClassApi.getUrl({ + name: storageClassName, + })); - return [ - volume.getName(), - , - - {storageClassName} - , - volume.getCapacity(), - claimRef && ( - - {claimRef.name} - - ), - , - { title: volume.getStatus(), className: volume.getStatus().toLowerCase() }, - ]; - }} - /> + return [ + volume.getName(), + , + + {storageClassName} + , + volume.getCapacity(), + claimRef && ( + + {claimRef.name} + + ), + , + { title: volume.getStatus(), className: volume.getStatus().toLowerCase() }, + ]; + }} + /> + ); } } diff --git a/src/renderer/components/+storage/route-tabs.injectable.ts b/src/renderer/components/+storage/route-tabs.injectable.ts deleted file mode 100644 index 9c7acf50a9..0000000000 --- a/src/renderer/components/+storage/route-tabs.injectable.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { 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 type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable"; -import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable"; -import * as routes from "../../../common/routes"; - -interface Dependencies { - isAllowedResource: IsAllowedResource; -} - -function getRouteTabs({ isAllowedResource }: Dependencies) { - return computed(() => { - const tabs: TabLayoutRoute[] = []; - - if (isAllowedResource("persistentvolumeclaims")) { - tabs.push({ - title: "Persistent Volume Claims", - component: PersistentVolumeClaims, - url: routes.volumeClaimsURL(), - routePath: routes.volumeClaimsRoute.path.toString(), - }); - } - - if (isAllowedResource("persistentvolumes")) { - tabs.push({ - title: "Persistent Volumes", - component: PersistentVolumes, - url: routes.volumesURL(), - routePath: routes.volumesRoute.path.toString(), - }); - } - - if (isAllowedResource("storageclasses")) { - tabs.push({ - title: "Storage Classes", - component: StorageClasses, - url: routes.storageClassesURL(), - routePath: routes.storageClassesRoute.path.toString(), - }); - } - - return tabs; - }); -} - -const storageRouteTabsInjectable = getInjectable({ - id: "storage-route-tabs", - - instantiate: (di) => getRouteTabs({ - isAllowedResource: di.inject(isAllowedResourceInjectable), - }), -}); - -export default storageRouteTabsInjectable; diff --git a/src/renderer/components/+storage/route.tsx b/src/renderer/components/+storage/route.tsx deleted file mode 100644 index b11d643186..0000000000 --- a/src/renderer/components/+storage/route.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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 deleted file mode 100644 index 96447ab5de..0000000000 --- a/src/renderer/components/+storage/sidebar-item.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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 { renderTabRoutesSidebarItems } 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 ( - } - > - {renderTabRoutesSidebarItems(tabRoutes)} - - ); -}); - -export const StorageSidebarItem = withInjectables(NonInjectedStorageSidebarItem, { - getProps: (di, props) => ({ - routes: di.inject(storageRouteTabsInjectable), - ...props, - }), -}); diff --git a/src/renderer/components/+storage/storage-sidebar-items.injectable.tsx b/src/renderer/components/+storage/storage-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..3faebf6835 --- /dev/null +++ b/src/renderer/components/+storage/storage-sidebar-items.injectable.tsx @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { + SidebarItemRegistration, + sidebarItemsInjectionToken, +} from "../layout/sidebar-items.injectable"; +import { Icon } from "../icon"; +import React from "react"; +import { noop } from "lodash/fp"; + +export const storageSidebarItemId = "storage"; + +const storageSidebarItemsInjectable = getInjectable({ + id: "storage-sidebar-items", + + instantiate: () => + computed((): SidebarItemRegistration[] => [ + { + id: storageSidebarItemId, + parentId: null, + getIcon: () => , + title: "Storage", + onClick: noop, + orderNumber: 60, + }, + ]), + + injectionToken: sidebarItemsInjectionToken, +}); + +export default storageSidebarItemsInjectable; diff --git a/src/renderer/components/+storage/storage.scss b/src/renderer/components/+storage/storage.scss deleted file mode 100644 index 62aa8d4294..0000000000 --- a/src/renderer/components/+storage/storage.scss +++ /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. - */ - -.Storage { -} diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/cluster-role-bindings-route-component.injectable.ts b/src/renderer/components/+user-management/+cluster-role-bindings/cluster-role-bindings-route-component.injectable.ts new file mode 100644 index 0000000000..47de616795 --- /dev/null +++ b/src/renderer/components/+user-management/+cluster-role-bindings/cluster-role-bindings-route-component.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 } from "@ogre-tools/injectable"; +import { ClusterRoleBindings } from "./view"; +import clusterRoleBindingsRouteInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/cluster-role-bindings/cluster-role-bindings-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../../routes/route-specific-component-injection-token"; + +const clusterRoleBindingsRouteComponentInjectable = getInjectable({ + id: "cluster-role-bindings-route-component", + + instantiate: (di) => ({ + route: di.inject(clusterRoleBindingsRouteInjectable), + Component: ClusterRoleBindings, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default clusterRoleBindingsRouteComponentInjectable; diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/cluster-role-bindings-sidebar-items.injectable.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/cluster-role-bindings-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..bb4ed2a928 --- /dev/null +++ b/src/renderer/components/+user-management/+cluster-role-bindings/cluster-role-bindings-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import clusterRoleBindingsRouteInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/cluster-role-bindings/cluster-role-bindings-route.injectable"; +import { userManagementSidebarItemId } from "../user-management-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import navigateToClusterRoleBindingsInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/cluster-role-bindings/navigate-to-cluster-role-bindings.injectable"; + +const clusterRoleBindingsSidebarItemsInjectable = getInjectable({ + id: "cluster-role-bindings-sidebar-items", + + instantiate: (di) => { + const route = di.inject(clusterRoleBindingsRouteInjectable); + const navigateToClusterRoleBindings = di.inject(navigateToClusterRoleBindingsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "cluster-role-bindings", + parentId: userManagementSidebarItemId, + title: "Cluster Role Bindings", + onClick: navigateToClusterRoleBindings, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 40, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default clusterRoleBindingsSidebarItemsInjectable; diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx index 54f626397e..3a83329723 100644 --- a/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx +++ b/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx @@ -172,6 +172,7 @@ export class ClusterRoleBindingDialog extends React.Component { -} - @observer -export class ClusterRoleBindings extends React.Component { +export class ClusterRoleBindings extends React.Component { render() { return ( - <> + - + ); } } diff --git a/src/renderer/components/+user-management/+cluster-roles/cluster-roles-route-component.injectable.ts b/src/renderer/components/+user-management/+cluster-roles/cluster-roles-route-component.injectable.ts new file mode 100644 index 0000000000..829ae16baa --- /dev/null +++ b/src/renderer/components/+user-management/+cluster-roles/cluster-roles-route-component.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 } from "@ogre-tools/injectable"; +import { ClusterRoles } from "./view"; +import clusterRolesRouteInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/cluster-roles/cluster-roles-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../../routes/route-specific-component-injection-token"; + +const clusterRolesRouteComponentInjectable = getInjectable({ + id: "cluster-roles-route-component", + + instantiate: (di) => ({ + route: di.inject(clusterRolesRouteInjectable), + Component: ClusterRoles, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default clusterRolesRouteComponentInjectable; diff --git a/src/renderer/components/+user-management/+cluster-roles/cluster-roles-sidebar-items.injectable.tsx b/src/renderer/components/+user-management/+cluster-roles/cluster-roles-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..54ae68ef44 --- /dev/null +++ b/src/renderer/components/+user-management/+cluster-roles/cluster-roles-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import clusterRolesRouteInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/cluster-roles/cluster-roles-route.injectable"; +import { userManagementSidebarItemId } from "../user-management-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import navigateToClusterRolesInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/cluster-roles/navigate-to-cluster-roles.injectable"; + +const clusterRolesSidebarItemsInjectable = getInjectable({ + id: "cluster-roles-sidebar-items", + + instantiate: (di) => { + const route = di.inject(clusterRolesRouteInjectable); + const navigateToClusterRoles = di.inject(navigateToClusterRolesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "cluster-roles", + parentId: userManagementSidebarItemId, + title: "Cluster Roles", + onClick: navigateToClusterRoles, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 20, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default clusterRolesSidebarItemsInjectable; diff --git a/src/renderer/components/+user-management/+cluster-roles/view.tsx b/src/renderer/components/+user-management/+cluster-roles/view.tsx index 0b979145d6..735d523227 100644 --- a/src/renderer/components/+user-management/+cluster-roles/view.tsx +++ b/src/renderer/components/+user-management/+cluster-roles/view.tsx @@ -7,12 +7,11 @@ import "./view.scss"; import { observer } from "mobx-react"; import React from "react"; -import type { RouteComponentProps } from "react-router"; import { KubeObjectListLayout } from "../../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../../kube-object-status-icon"; import { AddClusterRoleDialog } from "./add-dialog"; import { clusterRolesStore } from "./store"; -import type { ClusterRolesRouteParams } from "../../../../common/routes"; +import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../../kube-object/age"; enum columnId { @@ -21,14 +20,11 @@ enum columnId { age = "age", } -export interface ClusterRolesProps extends RouteComponentProps { -} - @observer -export class ClusterRoles extends React.Component { +export class ClusterRoles extends React.Component { render() { return ( - <> + { }} /> - + ); } } diff --git a/src/renderer/components/+user-management/+role-bindings/dialog.tsx b/src/renderer/components/+user-management/+role-bindings/dialog.tsx index 3add013dc3..8d1e40d741 100644 --- a/src/renderer/components/+user-management/+role-bindings/dialog.tsx +++ b/src/renderer/components/+user-management/+role-bindings/dialog.tsx @@ -176,6 +176,7 @@ export class RoleBindingDialog extends React.Component { <> { ({ + route: di.inject(roleBindingsRouteInjectable), + Component: RoleBindings, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default roleBindingsRouteComponentInjectable; diff --git a/src/renderer/components/+user-management/+role-bindings/role-bindings-sidebar-items.injectable.tsx b/src/renderer/components/+user-management/+role-bindings/role-bindings-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..f2eb240839 --- /dev/null +++ b/src/renderer/components/+user-management/+role-bindings/role-bindings-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import roleBindingsRouteInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/role-bindings/role-bindings-route.injectable"; +import { userManagementSidebarItemId } from "../user-management-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import navigateToRoleBindingsInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/role-bindings/navigate-to-role-bindings.injectable"; + +const roleBindingsSidebarItemsInjectable = getInjectable({ + id: "role-bindings-sidebar-items", + + instantiate: (di) => { + const route = di.inject(roleBindingsRouteInjectable); + const navigateToRoleBindings = di.inject(navigateToRoleBindingsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "role-bindings", + parentId: userManagementSidebarItemId, + title: "Role Bindings", + onClick: navigateToRoleBindings, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 50, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default roleBindingsSidebarItemsInjectable; diff --git a/src/renderer/components/+user-management/+role-bindings/view.tsx b/src/renderer/components/+user-management/+role-bindings/view.tsx index 5ef4099afc..fbf5d11081 100644 --- a/src/renderer/components/+user-management/+role-bindings/view.tsx +++ b/src/renderer/components/+user-management/+role-bindings/view.tsx @@ -6,7 +6,6 @@ import "./view.scss"; import { observer } from "mobx-react"; import React from "react"; -import type { RouteComponentProps } from "react-router"; import { KubeObjectListLayout } from "../../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../../kube-object-status-icon"; import { RoleBindingDialog } from "./dialog"; @@ -14,7 +13,7 @@ import { roleBindingsStore } from "./store"; import { rolesStore } from "../+roles/store"; import { clusterRolesStore } from "../+cluster-roles/store"; import { serviceAccountsStore } from "../+service-accounts/store"; -import type { RoleBindingsRouteParams } from "../../../../common/routes"; +import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../../kube-object/age"; enum columnId { @@ -24,14 +23,11 @@ enum columnId { age = "age", } -export interface RoleBindingsProps extends RouteComponentProps { -} - @observer -export class RoleBindings extends React.Component { +export class RoleBindings extends React.Component { render() { return ( - <> + { }} /> - + ); } } diff --git a/src/renderer/components/+user-management/+roles/add-dialog.tsx b/src/renderer/components/+user-management/+roles/add-dialog.tsx index 6bf6b6ef1f..ac9b7262e7 100644 --- a/src/renderer/components/+user-management/+roles/add-dialog.tsx +++ b/src/renderer/components/+user-management/+roles/add-dialog.tsx @@ -85,6 +85,7 @@ export class AddRoleDialog extends React.Component { /> this.namespace = value} diff --git a/src/renderer/components/+user-management/+roles/roles-route-component.injectable.ts b/src/renderer/components/+user-management/+roles/roles-route-component.injectable.ts new file mode 100644 index 0000000000..980b2499de --- /dev/null +++ b/src/renderer/components/+user-management/+roles/roles-route-component.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 } from "@ogre-tools/injectable"; +import { Roles } from "./view"; +import rolesRouteInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/roles/roles-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../../routes/route-specific-component-injection-token"; + +const rolesRouteComponentInjectable = getInjectable({ + id: "roles-route-component", + + instantiate: (di) => ({ + route: di.inject(rolesRouteInjectable), + Component: Roles, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default rolesRouteComponentInjectable; diff --git a/src/renderer/components/+user-management/+roles/roles-sidebar-items.injectable.tsx b/src/renderer/components/+user-management/+roles/roles-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..97ee9715a2 --- /dev/null +++ b/src/renderer/components/+user-management/+roles/roles-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import rolesRouteInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/roles/roles-route.injectable"; +import { userManagementSidebarItemId } from "../user-management-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import navigateToRolesInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/roles/navigate-to-roles.injectable"; + +const rolesSidebarItemsInjectable = getInjectable({ + id: "roles-sidebar-items", + + instantiate: (di) => { + const route = di.inject(rolesRouteInjectable); + const navigateToRoles = di.inject(navigateToRolesInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "roles", + parentId: userManagementSidebarItemId, + title: "Roles", + onClick: navigateToRoles, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 30, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default rolesSidebarItemsInjectable; diff --git a/src/renderer/components/+user-management/+roles/view.tsx b/src/renderer/components/+user-management/+roles/view.tsx index 63e77c3e04..2f8316c089 100644 --- a/src/renderer/components/+user-management/+roles/view.tsx +++ b/src/renderer/components/+user-management/+roles/view.tsx @@ -7,12 +7,11 @@ import "./view.scss"; import { observer } from "mobx-react"; import React from "react"; -import type { RouteComponentProps } from "react-router"; import { KubeObjectListLayout } from "../../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../../kube-object-status-icon"; import { AddRoleDialog } from "./add-dialog"; import { rolesStore } from "./store"; -import type { RolesRouteParams } from "../../../../common/routes"; +import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../../kube-object/age"; enum columnId { @@ -21,14 +20,11 @@ enum columnId { age = "age", } -export interface RolesProps extends RouteComponentProps { -} - @observer -export class Roles extends React.Component { +export class Roles extends React.Component { render() { return ( - <> + { }} /> - + ); } } diff --git a/src/renderer/components/+user-management/+service-accounts/create-dialog.tsx b/src/renderer/components/+user-management/+service-accounts/create-dialog.tsx index c041d502f4..7c77ed41b3 100644 --- a/src/renderer/components/+user-management/+service-accounts/create-dialog.tsx +++ b/src/renderer/components/+user-management/+service-accounts/create-dialog.tsx @@ -81,6 +81,7 @@ export class CreateServiceAccountDialog extends React.Component this.namespace = value} diff --git a/src/renderer/components/+user-management/+service-accounts/service-accounts-route-component.injectable.ts b/src/renderer/components/+user-management/+service-accounts/service-accounts-route-component.injectable.ts new file mode 100644 index 0000000000..5903af5194 --- /dev/null +++ b/src/renderer/components/+user-management/+service-accounts/service-accounts-route-component.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 } from "@ogre-tools/injectable"; +import { ServiceAccounts } from "./view"; +import serviceAccountsRouteInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/service-accounts/service-accounts-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../../routes/route-specific-component-injection-token"; + +const serviceAccountsRouteComponentInjectable = getInjectable({ + id: "service-accounts-route-component", + + instantiate: (di) => ({ + route: di.inject(serviceAccountsRouteInjectable), + Component: ServiceAccounts, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default serviceAccountsRouteComponentInjectable; diff --git a/src/renderer/components/+user-management/+service-accounts/service-accounts-sidebar-items.injectable.tsx b/src/renderer/components/+user-management/+service-accounts/service-accounts-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..d01b8d6480 --- /dev/null +++ b/src/renderer/components/+user-management/+service-accounts/service-accounts-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import serviceAccountsRouteInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/service-accounts/service-accounts-route.injectable"; +import { userManagementSidebarItemId } from "../user-management-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import navigateToServiceAccountsInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/service-accounts/navigate-to-service-accounts.injectable"; + +const serviceAccountsSidebarItemsInjectable = getInjectable({ + id: "service-accounts-sidebar-items", + + instantiate: (di) => { + const route = di.inject(serviceAccountsRouteInjectable); + const navigateToServiceAccounts = di.inject(navigateToServiceAccountsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "service-accounts", + parentId: userManagementSidebarItemId, + title: "Service Accounts", + onClick: navigateToServiceAccounts, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 10, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default serviceAccountsSidebarItemsInjectable; diff --git a/src/renderer/components/+user-management/+service-accounts/view.tsx b/src/renderer/components/+user-management/+service-accounts/view.tsx index 5c43716ed5..bce7955469 100644 --- a/src/renderer/components/+user-management/+service-accounts/view.tsx +++ b/src/renderer/components/+user-management/+service-accounts/view.tsx @@ -7,12 +7,11 @@ import "./view.scss"; import { observer } from "mobx-react"; import React from "react"; -import type { RouteComponentProps } from "react-router"; import { KubeObjectListLayout } from "../../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../../kube-object-status-icon"; import { CreateServiceAccountDialog } from "./create-dialog"; import { serviceAccountsStore } from "./store"; -import type { ServiceAccountsRouteParams } from "../../../../common/routes"; +import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../../kube-object/age"; enum columnId { @@ -21,14 +20,11 @@ enum columnId { age = "age", } -export interface ServiceAccountsProps extends RouteComponentProps { -} - @observer -export class ServiceAccounts extends React.Component { +export class ServiceAccounts extends React.Component { render() { return ( - <> + { }} /> - + ); } } diff --git a/src/renderer/components/+user-management/route-tabs.injectable.ts b/src/renderer/components/+user-management/route-tabs.injectable.ts deleted file mode 100644 index c4c9f5c134..0000000000 --- a/src/renderer/components/+user-management/route-tabs.injectable.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { 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 { ClusterRoles } from "./+cluster-roles"; -import { RoleBindings } from "./+role-bindings"; -import { Roles } from "./+roles"; -import { ServiceAccounts } from "./+service-accounts"; - -interface Dependencies { - isAllowedResource: IsAllowedResource; -} - -function getRouteTabs({ isAllowedResource }: Dependencies) { - return computed(() => { - const tabs: TabLayoutRoute[] = []; - - if (isAllowedResource("serviceaccounts")) { - tabs.push({ - title: "Service Accounts", - component: ServiceAccounts, - url: routes.serviceAccountsURL(), - routePath: routes.serviceAccountsRoute.path.toString(), - }); - } - - if (isAllowedResource("clusterroles")) { - tabs.push({ - title: "Cluster Roles", - component: ClusterRoles, - url: routes.clusterRolesURL(), - routePath: routes.clusterRolesRoute.path.toString(), - }); - } - - if (isAllowedResource("roles")) { - tabs.push({ - title: "Roles", - component: Roles, - url: routes.rolesURL(), - routePath: routes.rolesRoute.path.toString(), - }); - } - - if (isAllowedResource("clusterrolebindings")) { - tabs.push({ - title: "Cluster Role Bindings", - component: ClusterRoleBindings, - url: routes.clusterRoleBindingsURL(), - routePath: routes.clusterRoleBindingsRoute.path.toString(), - }); - } - - if (isAllowedResource("rolebindings")) { - tabs.push({ - title: "Role Bindings", - component: RoleBindings, - url: routes.roleBindingsURL(), - routePath: routes.roleBindingsRoute.path.toString(), - }); - } - - if (isAllowedResource("podsecuritypolicies")) { - tabs.push({ - title: "Pod Security Policies", - component: PodSecurityPolicies, - url: routes.podSecurityPoliciesURL(), - routePath: routes.podSecurityPoliciesRoute.path.toString(), - }); - } - - return tabs; - }); -} - -const userManagementRouteTabsInjectable = getInjectable({ - id: "user-management-route-tabs", - - instantiate: (di) => getRouteTabs({ - isAllowedResource: di.inject(isAllowedResourceInjectable), - }), -}); - -export default userManagementRouteTabsInjectable; diff --git a/src/renderer/components/+user-management/route.tsx b/src/renderer/components/+user-management/route.tsx deleted file mode 100644 index 360b0f57d0..0000000000 --- a/src/renderer/components/+user-management/route.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/** - * 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 deleted file mode 100644 index 20d9500e2b..0000000000 --- a/src/renderer/components/+user-management/sidebar-item.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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 { renderTabRoutesSidebarItems } 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 ( - } - > - {renderTabRoutesSidebarItems(tabRoutes)} - - ); -}); - -export const UserManagementSidebarItem = withInjectables(NonInjectedUserManagementSidebarItem, { - getProps: (di, props) => ({ - routes: di.inject(userManagementRouteTabsInjectable), - ...props, - }), -}); diff --git a/src/renderer/components/+user-management/user-management-sidebar-items.injectable.tsx b/src/renderer/components/+user-management/user-management-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..f07fcaf6c8 --- /dev/null +++ b/src/renderer/components/+user-management/user-management-sidebar-items.injectable.tsx @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { + SidebarItemRegistration, + sidebarItemsInjectionToken, +} from "../layout/sidebar-items.injectable"; +import { Icon } from "../icon"; +import React from "react"; +import { noop } from "lodash/fp"; + +export const userManagementSidebarItemId = "user-management"; + +const userManagementSidebarItemsInjectable = getInjectable({ + id: "user-management-sidebar-items", + + instantiate: () => + computed((): SidebarItemRegistration[] => [ + { + id: userManagementSidebarItemId, + parentId: null, + getIcon: () => , + title: "Access Control", + onClick: noop, + orderNumber: 100, + }, + ]), + + injectionToken: sidebarItemsInjectionToken, +}); + +export default userManagementSidebarItemsInjectable; diff --git a/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts b/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts index a4073afe1e..4d01e7e9cb 100644 --- a/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts +++ b/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts @@ -4,23 +4,26 @@ */ import { computed, IComputedValue } from "mobx"; import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; -import { navigate } from "../../../navigation"; -import { catalogURL } from "../../../../common/routes"; +import type { NavigateToCatalog } from "../../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; interface Dependencies { extensions: IComputedValue; + navigateToCatalog: NavigateToCatalog; } -export const getWelcomeMenuItems = ({ extensions }: Dependencies) => { +export const getWelcomeMenuItems = ({ + extensions, + navigateToCatalog, +}: Dependencies) => { const browseClusters = { title: "Browse Clusters in Catalog", icon: "view_list", + click: () => - navigate( - catalogURL({ - params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }, - }), - ), + navigateToCatalog({ + group: "entity.k8slens.dev", + kind: "KubernetesCluster", + }), }; return computed(() => [ diff --git a/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts index 1fbfcffed6..c5234ac6f8 100644 --- a/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts +++ b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts @@ -5,6 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; import { getWelcomeMenuItems } from "./get-welcome-menu-items"; +import navigateToCatalogInjectable from "../../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; const welcomeMenuItemsInjectable = getInjectable({ id: "welcome-menu-items", @@ -12,6 +13,7 @@ const welcomeMenuItemsInjectable = getInjectable({ instantiate: (di) => getWelcomeMenuItems({ extensions: di.inject(rendererExtensionsInjectable), + navigateToCatalog: di.inject(navigateToCatalogInjectable), }), }); diff --git a/src/renderer/components/+welcome/welcome-route-component.injectable.ts b/src/renderer/components/+welcome/welcome-route-component.injectable.ts new file mode 100644 index 0000000000..65fa0dfb00 --- /dev/null +++ b/src/renderer/components/+welcome/welcome-route-component.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 } from "@ogre-tools/injectable"; +import { Welcome } from "./welcome"; +import welcomeRouteInjectable from "../../../common/front-end-routing/routes/welcome/welcome-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const welcomeRouteComponentInjectable = getInjectable({ + id: "welcome-route-component", + + instantiate: (di) => ({ + route: di.inject(welcomeRouteInjectable), + Component: Welcome, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default welcomeRouteComponentInjectable; diff --git a/src/renderer/components/+welcome/welcome.tsx b/src/renderer/components/+welcome/welcome.tsx index 20f6ed1a27..b7390f6776 100644 --- a/src/renderer/components/+welcome/welcome.tsx +++ b/src/renderer/components/+welcome/welcome.tsx @@ -38,7 +38,7 @@ const NonInjectedWelcome: React.FC = ({ welcomeMenuItems, welcomeB }, defaultWidth); return ( -
+
({ + route: di.inject(cronJobsRouteInjectable), + Component: CronJobs, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default cronJobsRouteComponentInjectable; diff --git a/src/renderer/components/+workloads-cronjobs/cron-jobs-sidebar-items.injectable.tsx b/src/renderer/components/+workloads-cronjobs/cron-jobs-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..cc7c4a7140 --- /dev/null +++ b/src/renderer/components/+workloads-cronjobs/cron-jobs-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import cronJobsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/cron-jobs/cron-jobs-route.injectable"; +import { workloadsSidebarItemId } from "../+workloads/workloads-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToCronJobsInjectable from "../../../common/front-end-routing/routes/cluster/workloads/cron-jobs/navigate-to-cron-jobs.injectable"; + +const cronJobsSidebarItemsInjectable = getInjectable({ + id: "cron-jobs-sidebar-items", + + instantiate: (di) => { + const route = di.inject(cronJobsRouteInjectable); + const navigateToCronJobs = di.inject(navigateToCronJobsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "cron-jobs", + parentId: workloadsSidebarItemId, + title: "CronJobs", + onClick: navigateToCronJobs, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 80, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default cronJobsSidebarItemsInjectable; diff --git a/src/renderer/components/+workloads-cronjobs/cron-jobs-store.injectable.ts b/src/renderer/components/+workloads-cronjobs/cron-jobs-store.injectable.ts new file mode 100644 index 0000000000..a05ca0fc2a --- /dev/null +++ b/src/renderer/components/+workloads-cronjobs/cron-jobs-store.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { cronJobStore } from "./cronjob.store"; + +const cronJobsStoreInjectable = getInjectable({ + id: "cron-jobs-store", + instantiate: () => cronJobStore, + causesSideEffects: true, +}); + +export default cronJobsStoreInjectable; diff --git a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx index 7a818543af..41812f3a9b 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx @@ -7,14 +7,13 @@ import "./cronjobs.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import { cronJobStore } from "./cronjob.store"; import { jobStore } from "../+workloads-jobs/job.store"; import { eventStore } from "../+events/event.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { CronJobsRouteParams } from "../../../common/routes"; import moment from "moment"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -27,57 +26,56 @@ enum columnId { age = "age", } -export interface CronJobsProps extends RouteComponentProps { -} - @observer -export class CronJobs extends React.Component { +export class CronJobs extends React.Component { render() { return ( - cronJob.getName(), - [columnId.namespace]: cronJob => cronJob.getNs(), - [columnId.suspend]: cronJob => cronJob.getSuspendFlag(), - [columnId.active]: cronJob => cronJobStore.getActiveJobsNum(cronJob), - [columnId.lastSchedule]: cronJob => ( - cronJob.status?.lastScheduleTime - ? moment().diff(cronJob.status.lastScheduleTime) - : 0 - ), - [columnId.age]: cronJob => -cronJob.getCreationTimestamp(), - }} - searchFilters={[ - cronJob => cronJob.getSearchFields(), - cronJob => cronJob.getSchedule(), - ]} - renderHeaderTitle="Cron Jobs" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Schedule", className: "schedule", id: columnId.schedule }, - { title: "Suspend", className: "suspend", sortBy: columnId.suspend, id: columnId.suspend }, - { title: "Active", className: "active", sortBy: columnId.active, id: columnId.active }, - { title: "Last schedule", className: "last-schedule", sortBy: columnId.lastSchedule, id: columnId.lastSchedule }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={cronJob => [ - cronJob.getName(), - , - cronJob.getNs(), - cronJob.isNeverRun() ? "never" : cronJob.getSchedule(), - cronJob.getSuspendFlag(), - cronJobStore.getActiveJobsNum(cronJob), - cronJob.getLastScheduleTime(), - , - ]} - /> + + cronJob.getName(), + [columnId.namespace]: cronJob => cronJob.getNs(), + [columnId.suspend]: cronJob => cronJob.getSuspendFlag(), + [columnId.active]: cronJob => cronJobStore.getActiveJobsNum(cronJob), + [columnId.lastSchedule]: cronJob => ( + cronJob.status?.lastScheduleTime + ? moment().diff(cronJob.status.lastScheduleTime) + : 0 + ), + [columnId.age]: cronJob => -cronJob.getCreationTimestamp(), + }} + searchFilters={[ + cronJob => cronJob.getSearchFields(), + cronJob => cronJob.getSchedule(), + ]} + renderHeaderTitle="Cron Jobs" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Schedule", className: "schedule", id: columnId.schedule }, + { title: "Suspend", className: "suspend", sortBy: columnId.suspend, id: columnId.suspend }, + { title: "Active", className: "active", sortBy: columnId.active, id: columnId.active }, + { title: "Last schedule", className: "last-schedule", sortBy: columnId.lastSchedule, id: columnId.lastSchedule }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={cronJob => [ + cronJob.getName(), + , + cronJob.getNs(), + cronJob.isNeverRun() ? "never" : cronJob.getSchedule(), + cronJob.getSuspendFlag(), + cronJobStore.getActiveJobsNum(cronJob), + cronJob.getLastScheduleTime(), + , + ]} + /> + ); } } diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets-route-component.injectable.ts b/src/renderer/components/+workloads-daemonsets/daemonsets-route-component.injectable.ts new file mode 100644 index 0000000000..329c36943c --- /dev/null +++ b/src/renderer/components/+workloads-daemonsets/daemonsets-route-component.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 } from "@ogre-tools/injectable"; +import { DaemonSets } from "./daemonsets"; +import daemonsetsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/daemonsets/daemonsets-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const daemonsetsRouteComponentInjectable = getInjectable({ + id: "daemonsets-route-component", + + instantiate: (di) => ({ + route: di.inject(daemonsetsRouteInjectable), + Component: DaemonSets, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default daemonsetsRouteComponentInjectable; diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets-sidebar-items.injectable.tsx b/src/renderer/components/+workloads-daemonsets/daemonsets-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..3ec7c3a143 --- /dev/null +++ b/src/renderer/components/+workloads-daemonsets/daemonsets-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import daemonsetsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/daemonsets/daemonsets-route.injectable"; +import { workloadsSidebarItemId } from "../+workloads/workloads-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToDaemonsetsInjectable from "../../../common/front-end-routing/routes/cluster/workloads/daemonsets/navigate-to-daemonsets.injectable"; + +const daemonsetsSidebarItemsInjectable = getInjectable({ + id: "daemonsets-sidebar-items", + + instantiate: (di) => { + const route = di.inject(daemonsetsRouteInjectable); + const navigateToDaemonsets = di.inject(navigateToDaemonsetsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "daemon-sets", + parentId: workloadsSidebarItemId, + title: "DaemonSets", + onClick: () => navigateToDaemonsets(), + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 40, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default daemonsetsSidebarItemsInjectable; diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets-store.injectable.ts b/src/renderer/components/+workloads-daemonsets/daemonsets-store.injectable.ts new file mode 100644 index 0000000000..044750d493 --- /dev/null +++ b/src/renderer/components/+workloads-daemonsets/daemonsets-store.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { daemonSetStore } from "./daemonsets.store"; + +const daemonsetsStoreInjectable = getInjectable({ + id: "daemonsets-store", + instantiate: () => daemonSetStore, + causesSideEffects: true, +}); + +export default daemonsetsStoreInjectable; + diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx index 37293daa05..286cd947c9 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx @@ -7,7 +7,6 @@ import "./daemonsets.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import type { DaemonSet } from "../../../common/k8s-api/endpoints"; import { eventStore } from "../+events/event.store"; import { daemonSetStore } from "./daemonsets.store"; @@ -15,7 +14,7 @@ import { podsStore } from "../+workloads-pods/pods.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { Badge } from "../badge"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { DaemonSetsRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -26,53 +25,52 @@ enum columnId { age = "age", } -export interface DaemonSetsProps extends RouteComponentProps { -} - @observer -export class DaemonSets extends React.Component { +export class DaemonSets extends React.Component { getPodsLength(daemonSet: DaemonSet) { return daemonSetStore.getChildPods(daemonSet).length; } render() { return ( - daemonSet.getName(), - [columnId.namespace]: daemonSet => daemonSet.getNs(), - [columnId.pods]: daemonSet => this.getPodsLength(daemonSet), - [columnId.age]: daemonSet => -daemonSet.getCreationTimestamp(), - }} - searchFilters={[ - daemonSet => daemonSet.getSearchFields(), - daemonSet => daemonSet.getLabels(), - ]} - renderHeaderTitle="Daemon Sets" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Pods", className: "pods", sortBy: columnId.pods, id: columnId.pods }, - { className: "warning", showWithColumn: columnId.pods }, - { title: "Node Selector", className: "labels scrollable", id: columnId.labels }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={daemonSet => [ - daemonSet.getName(), - daemonSet.getNs(), - this.getPodsLength(daemonSet), - , - daemonSet.getNodeSelectors().map(selector => ( - - )), - , - ]} - /> + + daemonSet.getName(), + [columnId.namespace]: daemonSet => daemonSet.getNs(), + [columnId.pods]: daemonSet => this.getPodsLength(daemonSet), + [columnId.age]: daemonSet => -daemonSet.getCreationTimestamp(), + }} + searchFilters={[ + daemonSet => daemonSet.getSearchFields(), + daemonSet => daemonSet.getLabels(), + ]} + renderHeaderTitle="Daemon Sets" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Pods", className: "pods", sortBy: columnId.pods, id: columnId.pods }, + { className: "warning", showWithColumn: columnId.pods }, + { title: "Node Selector", className: "labels scrollable", id: columnId.labels }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={daemonSet => [ + daemonSet.getName(), + daemonSet.getNs(), + this.getPodsLength(daemonSet), + , + daemonSet.getNodeSelectors().map(selector => ( + + )), + , + ]} + /> + ); } } diff --git a/src/renderer/components/+workloads-deployments/deployments-route-component.injectable.ts b/src/renderer/components/+workloads-deployments/deployments-route-component.injectable.ts new file mode 100644 index 0000000000..65ec34acb2 --- /dev/null +++ b/src/renderer/components/+workloads-deployments/deployments-route-component.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 } from "@ogre-tools/injectable"; +import { Deployments } from "./deployments"; +import deploymentsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/deployments/deployments-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const deploymentsRouteComponentInjectable = getInjectable({ + id: "deployments-route-component", + + instantiate: (di) => ({ + route: di.inject(deploymentsRouteInjectable), + Component: Deployments, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default deploymentsRouteComponentInjectable; diff --git a/src/renderer/components/+workloads-deployments/deployments-sidebar-items.injectable.tsx b/src/renderer/components/+workloads-deployments/deployments-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..9f06301b8c --- /dev/null +++ b/src/renderer/components/+workloads-deployments/deployments-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import deploymentsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/deployments/deployments-route.injectable"; +import { workloadsSidebarItemId } from "../+workloads/workloads-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToDeploymentsInjectable from "../../../common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable"; + +const deploymentsSidebarItemsInjectable = getInjectable({ + id: "deployments-sidebar-items", + + instantiate: (di) => { + const route = di.inject(deploymentsRouteInjectable); + const navigateToDeployments = di.inject(navigateToDeploymentsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "deployments", + parentId: workloadsSidebarItemId, + title: "Deployments", + onClick: navigateToDeployments, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 30, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default deploymentsSidebarItemsInjectable; diff --git a/src/renderer/components/+workloads-deployments/deployments-store.injectable.ts b/src/renderer/components/+workloads-deployments/deployments-store.injectable.ts new file mode 100644 index 0000000000..38068fdf00 --- /dev/null +++ b/src/renderer/components/+workloads-deployments/deployments-store.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { deploymentStore } from "./deployments.store"; + +const deploymentsStoreInjectable = getInjectable({ + id: "deployments-store", + instantiate: () => deploymentStore, + causesSideEffects: true, +}); + +export default deploymentsStoreInjectable; diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index 44f1d55b8e..9a2b7f239e 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -7,7 +7,6 @@ import "./deployments.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import type { Deployment } from "../../../common/k8s-api/endpoints"; import { deploymentStore } from "./deployments.store"; import { eventStore } from "../+events/event.store"; @@ -16,7 +15,7 @@ import { cssNames } from "../../utils"; import kebabCase from "lodash/kebabCase"; import orderBy from "lodash/orderBy"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { DeploymentsRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -28,11 +27,8 @@ enum columnId { condition = "condition", } -export interface DeploymentsProps extends RouteComponentProps { -} - @observer -export class Deployments extends React.Component { +export class Deployments extends React.Component { renderPods(deployment: Deployment) { const { replicas, availableReplicas } = deployment.status; @@ -51,42 +47,44 @@ export class Deployments extends React.Component { render() { return ( - deployment.getName(), - [columnId.namespace]: deployment => deployment.getNs(), - [columnId.replicas]: deployment => deployment.getReplicas(), - [columnId.age]: deployment => -deployment.getCreationTimestamp(), - [columnId.condition]: deployment => deployment.getConditionsText(), - }} - searchFilters={[ - deployment => deployment.getSearchFields(), - deployment => deployment.getConditionsText(), - ]} - renderHeaderTitle="Deployments" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Pods", className: "pods", id: columnId.pods }, - { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - { title: "Conditions", className: "conditions", sortBy: columnId.condition, id: columnId.condition }, - ]} - renderTableContents={deployment => [ - deployment.getName(), - , - deployment.getNs(), - this.renderPods(deployment), - deployment.getReplicas(), - , - this.renderConditions(deployment), - ]} - /> + + deployment.getName(), + [columnId.namespace]: deployment => deployment.getNs(), + [columnId.replicas]: deployment => deployment.getReplicas(), + [columnId.age]: deployment => -deployment.getCreationTimestamp(), + [columnId.condition]: deployment => deployment.getConditionsText(), + }} + searchFilters={[ + deployment => deployment.getSearchFields(), + deployment => deployment.getConditionsText(), + ]} + renderHeaderTitle="Deployments" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Pods", className: "pods", id: columnId.pods }, + { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Conditions", className: "conditions", sortBy: columnId.condition, id: columnId.condition }, + ]} + renderTableContents={deployment => [ + deployment.getName(), + , + deployment.getNs(), + this.renderPods(deployment), + deployment.getReplicas(), + , + this.renderConditions(deployment), + ]} + /> + ); } } diff --git a/src/renderer/components/+workloads-jobs/jobs-route-component.injectable.ts b/src/renderer/components/+workloads-jobs/jobs-route-component.injectable.ts new file mode 100644 index 0000000000..e060cf95ee --- /dev/null +++ b/src/renderer/components/+workloads-jobs/jobs-route-component.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 } from "@ogre-tools/injectable"; +import { Jobs } from "./jobs"; +import jobsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/jobs/jobs-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const jobsRouteComponentInjectable = getInjectable({ + id: "jobs-route-component", + + instantiate: (di) => ({ + route: di.inject(jobsRouteInjectable), + Component: Jobs, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default jobsRouteComponentInjectable; diff --git a/src/renderer/components/+workloads-jobs/jobs-sidebar-items.injectable.tsx b/src/renderer/components/+workloads-jobs/jobs-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..d89d6a2874 --- /dev/null +++ b/src/renderer/components/+workloads-jobs/jobs-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import jobsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/jobs/jobs-route.injectable"; +import { workloadsSidebarItemId } from "../+workloads/workloads-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToJobsInjectable from "../../../common/front-end-routing/routes/cluster/workloads/jobs/navigate-to-jobs.injectable"; + +const jobsSidebarItemsInjectable = getInjectable({ + id: "jobs-sidebar-items", + + instantiate: (di) => { + const route = di.inject(jobsRouteInjectable); + const navigateToJobs = di.inject(navigateToJobsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "jobs", + parentId: workloadsSidebarItemId, + title: "Jobs", + onClick: navigateToJobs, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 70, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default jobsSidebarItemsInjectable; diff --git a/src/renderer/components/+workloads-jobs/jobs-store.injectable.ts b/src/renderer/components/+workloads-jobs/jobs-store.injectable.ts new file mode 100644 index 0000000000..773706b8d6 --- /dev/null +++ b/src/renderer/components/+workloads-jobs/jobs-store.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { jobStore } from "./job.store"; + +const jobsStoreInjectable = getInjectable({ + id: "jobs-store", + instantiate: () => jobStore, + causesSideEffects: true, +}); + +export default jobsStoreInjectable; diff --git a/src/renderer/components/+workloads-jobs/jobs.tsx b/src/renderer/components/+workloads-jobs/jobs.tsx index d6d1e47c5b..0c94808014 100644 --- a/src/renderer/components/+workloads-jobs/jobs.tsx +++ b/src/renderer/components/+workloads-jobs/jobs.tsx @@ -7,13 +7,12 @@ import "./jobs.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import { jobStore } from "./job.store"; import { eventStore } from "../+events/event.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import kebabCase from "lodash/kebabCase"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { JobsRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -24,52 +23,51 @@ enum columnId { age = "age", } -export interface JobsProps extends RouteComponentProps { -} - @observer -export class Jobs extends React.Component { +export class Jobs extends React.Component { render() { return ( - job.getName(), - [columnId.namespace]: job => job.getNs(), - [columnId.conditions]: job => job.getCondition() != null ? job.getCondition().type : "", - [columnId.age]: job => -job.getCreationTimestamp(), - }} - searchFilters={[ - job => job.getSearchFields(), - ]} - renderHeaderTitle="Jobs" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Completions", className: "completions", id: columnId.completions }, - { className: "warning", showWithColumn: columnId.completions }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - { title: "Conditions", className: "conditions", sortBy: columnId.conditions, id: columnId.conditions }, - ]} - renderTableContents={job => { - const condition = job.getCondition(); + + job.getName(), + [columnId.namespace]: job => job.getNs(), + [columnId.conditions]: job => job.getCondition() != null ? job.getCondition().type : "", + [columnId.age]: job => -job.getCreationTimestamp(), + }} + searchFilters={[ + job => job.getSearchFields(), + ]} + renderHeaderTitle="Jobs" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Completions", className: "completions", id: columnId.completions }, + { className: "warning", showWithColumn: columnId.completions }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Conditions", className: "conditions", sortBy: columnId.conditions, id: columnId.conditions }, + ]} + renderTableContents={job => { + const condition = job.getCondition(); - return [ - job.getName(), - job.getNs(), - `${job.getCompletions()} / ${job.getDesiredCompletions()}`, - , - , - condition && { - title: condition.type, - className: kebabCase(condition.type), - }, - ]; - }} - /> + return [ + job.getName(), + job.getNs(), + `${job.getCompletions()} / ${job.getDesiredCompletions()}`, + , + , + condition && { + title: condition.type, + className: kebabCase(condition.type), + }, + ]; + }} + /> + ); } } diff --git a/src/renderer/components/+workloads-overview/overview-statuses.tsx b/src/renderer/components/+workloads-overview/overview-statuses.tsx index 1eb684b553..c69944d0a5 100644 --- a/src/renderer/components/+workloads-overview/overview-statuses.tsx +++ b/src/renderer/components/+workloads-overview/overview-statuses.tsx @@ -8,22 +8,13 @@ import "./overview-statuses.scss"; import React from "react"; import { observer } from "mobx-react"; import { OverviewWorkloadStatus } from "./overview-workload-status"; -import { Link } from "react-router-dom"; -import type { KubeResource } from "../../../common/rbac"; import { withInjectables } from "@ogre-tools/injectable-react"; import type { IComputedValue } from "mobx"; -import workloadsInjectable from "./workloads.injectable"; +import workloadsInjectable from "./workloads/workloads.injectable"; +import type { Workload } from "./workloads/workload-injection-token"; export interface OverviewStatusesProps {} -interface Workload { - resource: KubeResource; - amountOfItems: number; - href: string; - status: Record; - title: string; -} - interface Dependencies { workloads: IComputedValue; } @@ -32,18 +23,17 @@ const NonInjectedOverviewStatuses = observer( ({ workloads }: Dependencies & OverviewStatusesProps) => (
- {workloads.get() - .map(({ resource, title, href, status, amountOfItems }) => ( -
-
- - {title} ({amountOfItems}) - -
- - + {workloads.get().map((workload) => ( + + ))}
), diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index 527fc547b7..e71636c463 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -7,7 +7,6 @@ import "./overview.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import { eventStore } from "../+events/event.store"; import { podsStore } from "../+workloads-pods/pods.store"; import { deploymentStore } from "../+workloads-deployments/deployments.store"; @@ -16,7 +15,6 @@ 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 type { WorkloadsOverviewRouteParams } from "../../../common/routes"; import { IComputedValue, makeObservable, observable, reaction } from "mobx"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; import { Icon } from "../icon"; @@ -30,9 +28,7 @@ import type { Disposer } from "../../../common/utils"; import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; import type { KubeWatchSubscribeStoreOptions } from "../../kube-watch-api/kube-watch-api"; import detailComponentsInjectable from "./detail-components.injectable"; - -export interface WorkloadsOverviewProps extends RouteComponentProps { -} +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; interface Dependencies { detailComponents: IComputedValue[]>; @@ -41,10 +37,10 @@ interface Dependencies { } @observer -class NonInjectedWorkloadsOverview extends React.Component { +class NonInjectedWorkloadsOverview extends React.Component { @observable loadErrors: string[] = []; - constructor(props: WorkloadsOverviewProps & Dependencies) { + constructor(props: Dependencies) { super(props); makeObservable(this); } @@ -93,30 +89,31 @@ class NonInjectedWorkloadsOverview extends React.Component -
-
Overview
- {this.renderLoadErrors()} - -
+ +
+
+
Overview
+ {this.renderLoadErrors()} + +
- {this.props.detailComponents.get().map((Details, index) => ( -
- ))} -
+ {this.props.detailComponents.get().map((Details, index) => ( +
+ ))} +
+ ); } } -export const WorkloadsOverview = withInjectables( +export const WorkloadsOverview = withInjectables( NonInjectedWorkloadsOverview, { - getProps: (di, props) => ({ + getProps: (di) => ({ detailComponents: di.inject(detailComponentsInjectable), clusterFrameContext: di.inject(clusterFrameContextInjectable), subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, - ...props, }), }, ); diff --git a/src/renderer/components/+workloads-overview/workloads-overview-route-component.injectable.ts b/src/renderer/components/+workloads-overview/workloads-overview-route-component.injectable.ts new file mode 100644 index 0000000000..59a2a6d7ae --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads-overview-route-component.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 } from "@ogre-tools/injectable"; +import { WorkloadsOverview } from "./overview"; +import workloadsOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/overview/workloads-overview-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const workloadsOverviewRouteComponentInjectable = getInjectable({ + id: "workloads-overview-route-component", + + instantiate: (di) => ({ + route: di.inject(workloadsOverviewRouteInjectable), + Component: WorkloadsOverview, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default workloadsOverviewRouteComponentInjectable; diff --git a/src/renderer/components/+workloads-overview/workloads-overview-sidebar-items.injectable.tsx b/src/renderer/components/+workloads-overview/workloads-overview-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..c82c1e6e75 --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads-overview-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import workloadsOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/overview/workloads-overview-route.injectable"; +import { workloadsSidebarItemId } from "../+workloads/workloads-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToWorkloadsOverviewInjectable from "../../../common/front-end-routing/routes/cluster/workloads/overview/navigate-to-workloads-overview.injectable"; + +const workloadsOverviewSidebarItemsInjectable = getInjectable({ + id: "workloads-overview-sidebar-items", + + instantiate: (di) => { + const route = di.inject(workloadsOverviewRouteInjectable); + const navigateToWorkloadsOverview = di.inject(navigateToWorkloadsOverviewInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "overview", + parentId: workloadsSidebarItemId, + title: "Overview", + onClick: navigateToWorkloadsOverview, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 10, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default workloadsOverviewSidebarItemsInjectable; diff --git a/src/renderer/components/+workloads-overview/workloads.injectable.ts b/src/renderer/components/+workloads-overview/workloads.injectable.ts deleted file mode 100644 index 646dcb6325..0000000000 --- a/src/renderer/components/+workloads-overview/workloads.injectable.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import 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({ - id: "workloads", - - 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], - ]), - }), -}); - -export default workloadsInjectable; diff --git a/src/renderer/components/+workloads-overview/workloads.ts b/src/renderer/components/+workloads-overview/workloads.ts deleted file mode 100644 index f3db713e63..0000000000 --- a/src/renderer/components/+workloads-overview/workloads.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * 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-overview/workloads/implementations/cron-jobs-workload.injectable.ts b/src/renderer/components/+workloads-overview/workloads/implementations/cron-jobs-workload.injectable.ts new file mode 100644 index 0000000000..8171c17625 --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads/implementations/cron-jobs-workload.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { workloadInjectionToken } from "../workload-injection-token"; +import { ResourceNames } from "../../../../utils/rbac"; +import navigateToPodsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable"; +import namespaceStoreInjectable from "../../../+namespaces/namespace-store/namespace-store.injectable"; +import cronJobsStoreInjectable from "../../../+workloads-cronjobs/cron-jobs-store.injectable"; +import { computed } from "mobx"; + +const cronJobsWorkloadInjectable = getInjectable({ + id: "cron-jobs-workload", + + instantiate: (di) => { + const navigate = di.inject(navigateToPodsInjectable); + const namespaceStore = di.inject(namespaceStoreInjectable); + const store = di.inject(cronJobsStoreInjectable); + + return { + resourceName: "cronjobs", + open: navigate, + + amountOfItems: computed( + () => store.getAllByNs(namespaceStore.contextNamespaces).length, + ), + + status: computed(() => + store.getStatuses(store.getAllByNs(namespaceStore.contextNamespaces)), + ), + + title: ResourceNames.cronjobs, + orderNumber: 70, + }; + }, + + injectionToken: workloadInjectionToken, +}); + +export default cronJobsWorkloadInjectable; diff --git a/src/renderer/components/+workloads-overview/workloads/implementations/daemonsets-workload.injectable.ts b/src/renderer/components/+workloads-overview/workloads/implementations/daemonsets-workload.injectable.ts new file mode 100644 index 0000000000..3d94bed421 --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads/implementations/daemonsets-workload.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { workloadInjectionToken } from "../workload-injection-token"; +import { ResourceNames } from "../../../../utils/rbac"; +import namespaceStoreInjectable from "../../../+namespaces/namespace-store/namespace-store.injectable"; +import daemonsetsStoreInjectable from "../../../+workloads-daemonsets/daemonsets-store.injectable"; +import navigateToDaemonsetsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/daemonsets/navigate-to-daemonsets.injectable"; +import { computed } from "mobx"; + +const daemonsetsWorkloadInjectable = getInjectable({ + id: "daemonsets-workload", + + instantiate: (di) => { + const navigate = di.inject(navigateToDaemonsetsInjectable); + const namespaceStore = di.inject(namespaceStoreInjectable); + const store = di.inject(daemonsetsStoreInjectable); + + return { + resourceName: "daemonsets", + open: navigate, + + amountOfItems: computed( + () => store.getAllByNs(namespaceStore.contextNamespaces).length, + ), + + status: computed(() => + store.getStatuses(store.getAllByNs(namespaceStore.contextNamespaces)), + ), + + title: ResourceNames.daemonsets, + orderNumber: 30, + }; + }, + + injectionToken: workloadInjectionToken, +}); + +export default daemonsetsWorkloadInjectable; diff --git a/src/renderer/components/+workloads-overview/workloads/implementations/deployments-workload.injectable.ts b/src/renderer/components/+workloads-overview/workloads/implementations/deployments-workload.injectable.ts new file mode 100644 index 0000000000..8bd680a88e --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads/implementations/deployments-workload.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { workloadInjectionToken } from "../workload-injection-token"; +import { ResourceNames } from "../../../../utils/rbac"; +import namespaceStoreInjectable from "../../../+namespaces/namespace-store/namespace-store.injectable"; +import deploymentsStoreInjectable from "../../../+workloads-deployments/deployments-store.injectable"; +import navigateToDeploymentsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable"; +import { computed } from "mobx"; + +const deploymentsWorkloadInjectable = getInjectable({ + id: "deployments-workload", + + instantiate: (di) => { + const navigate = di.inject(navigateToDeploymentsInjectable); + const namespaceStore = di.inject(namespaceStoreInjectable); + const store = di.inject(deploymentsStoreInjectable); + + return { + resourceName: "deployments", + open: navigate, + + amountOfItems: computed( + () => store.getAllByNs(namespaceStore.contextNamespaces).length, + ), + + status: computed(() => + store.getStatuses(store.getAllByNs(namespaceStore.contextNamespaces)), + ), + + title: ResourceNames.deployments, + orderNumber: 20, + }; + }, + + injectionToken: workloadInjectionToken, +}); + +export default deploymentsWorkloadInjectable; diff --git a/src/renderer/components/+workloads-overview/workloads/implementations/jobs-workload.injectable.ts b/src/renderer/components/+workloads-overview/workloads/implementations/jobs-workload.injectable.ts new file mode 100644 index 0000000000..19966b952b --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads/implementations/jobs-workload.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { workloadInjectionToken } from "../workload-injection-token"; +import { ResourceNames } from "../../../../utils/rbac"; +import namespaceStoreInjectable from "../../../+namespaces/namespace-store/namespace-store.injectable"; +import jobsStoreInjectable from "../../../+workloads-jobs/jobs-store.injectable"; +import navigateToJobsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/jobs/navigate-to-jobs.injectable"; +import { computed } from "mobx"; + +const jobsWorkloadInjectable = getInjectable({ + id: "jobs-workload", + + instantiate: (di) => { + const navigate = di.inject(navigateToJobsInjectable); + const namespaceStore = di.inject(namespaceStoreInjectable); + const store = di.inject(jobsStoreInjectable); + + return { + resourceName: "jobs", + open: navigate, + + amountOfItems: computed( + () => store.getAllByNs(namespaceStore.contextNamespaces).length, + ), + + status: computed(() => + store.getStatuses(store.getAllByNs(namespaceStore.contextNamespaces)), + ), + + title: ResourceNames.jobs, + orderNumber: 60, + }; + }, + + injectionToken: workloadInjectionToken, +}); + +export default jobsWorkloadInjectable; diff --git a/src/renderer/components/+workloads-overview/workloads/implementations/pods-workload.injectable.ts b/src/renderer/components/+workloads-overview/workloads/implementations/pods-workload.injectable.ts new file mode 100644 index 0000000000..334c20ba44 --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads/implementations/pods-workload.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { workloadInjectionToken } from "../workload-injection-token"; +import { ResourceNames } from "../../../../utils/rbac"; +import navigateToPodsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/pods/navigate-to-pods.injectable"; +import podsStoreInjectable from "../../../+workloads-pods/pods-store.injectable"; +import namespaceStoreInjectable from "../../../+namespaces/namespace-store/namespace-store.injectable"; +import { computed } from "mobx"; + +const podsWorkloadInjectable = getInjectable({ + id: "pods-workload", + + instantiate: (di) => { + const navigate = di.inject(navigateToPodsInjectable); + const namespaceStore = di.inject(namespaceStoreInjectable); + const store = di.inject(podsStoreInjectable); + + return { + resourceName: "pods", + open: navigate, + + amountOfItems: computed( + () => store.getAllByNs(namespaceStore.contextNamespaces).length, + ), + + status: computed(() => + store.getStatuses(store.getAllByNs(namespaceStore.contextNamespaces)), + ), + + title: ResourceNames.pods, + orderNumber: 10, + }; + }, + + injectionToken: workloadInjectionToken, +}); + +export default podsWorkloadInjectable; diff --git a/src/renderer/components/+workloads-overview/workloads/implementations/replicasets-workload.injectable.ts b/src/renderer/components/+workloads-overview/workloads/implementations/replicasets-workload.injectable.ts new file mode 100644 index 0000000000..71eed58284 --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads/implementations/replicasets-workload.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { workloadInjectionToken } from "../workload-injection-token"; +import { ResourceNames } from "../../../../utils/rbac"; +import navigateToPodsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable"; +import namespaceStoreInjectable from "../../../+namespaces/namespace-store/namespace-store.injectable"; +import replicasetsStoreInjectable from "../../../+workloads-replicasets/replicasets-store.injectable"; +import { computed } from "mobx"; + +const replicasetsWorkloadInjectable = getInjectable({ + id: "replicasets-workload", + + instantiate: (di) => { + const navigate = di.inject(navigateToPodsInjectable); + const namespaceStore = di.inject(namespaceStoreInjectable); + const store = di.inject(replicasetsStoreInjectable); + + return { + resourceName: "replicasets", + open: navigate, + + amountOfItems: computed( + () => store.getAllByNs(namespaceStore.contextNamespaces).length, + ), + + status: computed(() => + store.getStatuses(store.getAllByNs(namespaceStore.contextNamespaces)), + ), + + title: ResourceNames.replicasets, + orderNumber: 50, + }; + }, + + injectionToken: workloadInjectionToken, +}); + +export default replicasetsWorkloadInjectable; diff --git a/src/renderer/components/+workloads-overview/workloads/implementations/statefulsets-workload.injectable.ts b/src/renderer/components/+workloads-overview/workloads/implementations/statefulsets-workload.injectable.ts new file mode 100644 index 0000000000..d7908990bf --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads/implementations/statefulsets-workload.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { workloadInjectionToken } from "../workload-injection-token"; +import { ResourceNames } from "../../../../utils/rbac"; +import navigateToPodsInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable"; +import namespaceStoreInjectable from "../../../+namespaces/namespace-store/namespace-store.injectable"; +import statefulsetsStoreInjectable from "../../../+workloads-statefulsets/statefulsets-store.injectable"; +import { computed } from "mobx"; + +const statefulsetsWorkloadInjectable = getInjectable({ + id: "statefulsets-workload", + + instantiate: (di) => { + const navigate = di.inject(navigateToPodsInjectable); + const namespaceStore = di.inject(namespaceStoreInjectable); + const store = di.inject(statefulsetsStoreInjectable); + + return { + resourceName: "statefulsets", + open: navigate, + + amountOfItems: computed( + () => store.getAllByNs(namespaceStore.contextNamespaces).length, + ), + + status: computed(() => + store.getStatuses(store.getAllByNs(namespaceStore.contextNamespaces)), + ), + + title: ResourceNames.statefulsets, + orderNumber: 40, + }; + }, + + injectionToken: workloadInjectionToken, +}); + +export default statefulsetsWorkloadInjectable; diff --git a/src/renderer/components/+workloads-overview/workloads/workload-injection-token.ts b/src/renderer/components/+workloads-overview/workloads/workload-injection-token.ts new file mode 100644 index 0000000000..6e7bfcd74a --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads/workload-injection-token.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { IComputedValue } from "mobx"; + +export interface Workload { + resourceName: string; + open: () => void; + amountOfItems: IComputedValue; + status: IComputedValue>; + title: string; + orderNumber: number; +} + +export const workloadInjectionToken = getInjectionToken({ + id: "workload-injection-token", +}); diff --git a/src/renderer/components/+workloads-overview/workloads/workloads.injectable.ts b/src/renderer/components/+workloads-overview/workloads/workloads.injectable.ts new file mode 100644 index 0000000000..330c9ae8ae --- /dev/null +++ b/src/renderer/components/+workloads-overview/workloads/workloads.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { pipeline } from "@ogre-tools/fp"; +import { getInjectable } from "@ogre-tools/injectable"; +import { filter, sortBy as sortByWithBadTyping } from "lodash/fp"; +import { computed } from "mobx"; +import { Workload, workloadInjectionToken } from "./workload-injection-token"; +import isAllowedResourceInjectable from "../../../../common/utils/is-allowed-resource.injectable"; + +const sortBy = + (propertyPath: string) => + (collection: Collection[]) => + sortByWithBadTyping(propertyPath, collection); + +const workloadsInjectable = getInjectable({ + id: "workloads", + + instantiate: (di) => { + const workloads = di.injectMany(workloadInjectionToken); + + const isAllowedResource = (resourceName: string) => + di.inject(isAllowedResourceInjectable, resourceName); + + return computed(() => + pipeline( + workloads, + + filter((workload: Workload) => { + const isAllowed = isAllowedResource(workload.resourceName); + + return isAllowed.get(); + }), + + sortBy("orderNumber"), + ), + ); + }, +}); + +export default workloadsInjectable; diff --git a/src/renderer/components/+workloads-pods/pod-container-port.tsx b/src/renderer/components/+workloads-pods/pod-container-port.tsx index 386554bec3..1e38f377a4 100644 --- a/src/renderer/components/+workloads-pods/pod-container-port.tsx +++ b/src/renderer/components/+workloads-pods/pod-container-port.tsx @@ -13,19 +13,14 @@ import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Button } from "../button"; import type { ForwardedPort } from "../../port-forward"; -import { - aboutPortForwarding, - notifyErrorPortForwarding, - openPortForward, - PortForwardStore, - predictProtocol, -} from "../../port-forward"; - +import { openPortForward, PortForwardStore, predictProtocol } from "../../port-forward"; import { Spinner } from "../spinner"; import { withInjectables } from "@ogre-tools/injectable-react"; import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable"; import portForwardDialogModelInjectable from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable"; import logger from "../../../common/logger"; +import aboutPortForwardingInjectable from "../../port-forward/about-port-forwarding.injectable"; +import notifyErrorPortForwardingInjectable from "../../port-forward/notify-error-port-forwarding.injectable"; export interface PodContainerPortProps { pod: Pod; @@ -39,6 +34,8 @@ export interface PodContainerPortProps { interface Dependencies { portForwardStore: PortForwardStore; openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean; onClose: () => void }) => void; + aboutPortForwarding: () => void; + notifyErrorPortForwarding: (message: string) => void; } @observer @@ -119,10 +116,10 @@ class NonInjectedPodContainerPort extends React.Component ({ portForwardStore: di.inject(portForwardStoreInjectable), openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open, + aboutPortForwarding: di.inject(aboutPortForwardingInjectable), + notifyErrorPortForwarding: di.inject(notifyErrorPortForwardingInjectable), ...props, }), }, diff --git a/src/renderer/components/+workloads-pods/pods-route-component.injectable.ts b/src/renderer/components/+workloads-pods/pods-route-component.injectable.ts new file mode 100644 index 0000000000..6ab45945cb --- /dev/null +++ b/src/renderer/components/+workloads-pods/pods-route-component.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 } from "@ogre-tools/injectable"; +import { Pods } from "./pods"; +import podsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/pods/pods-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const podsRouteComponentInjectable = getInjectable({ + id: "pods-route-component", + + instantiate: (di) => ({ + route: di.inject(podsRouteInjectable), + Component: Pods, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default podsRouteComponentInjectable; diff --git a/src/renderer/components/+workloads-pods/pods-sidebar-items.injectable.tsx b/src/renderer/components/+workloads-pods/pods-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..849db460e7 --- /dev/null +++ b/src/renderer/components/+workloads-pods/pods-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import podsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/pods/pods-route.injectable"; +import { workloadsSidebarItemId } from "../+workloads/workloads-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToPodsInjectable from "../../../common/front-end-routing/routes/cluster/workloads/pods/navigate-to-pods.injectable"; + +const podsSidebarItemsInjectable = getInjectable({ + id: "pods-sidebar-items", + + instantiate: (di) => { + const route = di.inject(podsRouteInjectable); + const navigateToPods = di.inject(navigateToPodsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "pods", + parentId: workloadsSidebarItemId, + title: "Pods", + onClick: navigateToPods, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 20, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default podsSidebarItemsInjectable; diff --git a/src/renderer/components/+workloads-pods/pods-store.injectable.ts b/src/renderer/components/+workloads-pods/pods-store.injectable.ts new file mode 100644 index 0000000000..7515931f80 --- /dev/null +++ b/src/renderer/components/+workloads-pods/pods-store.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { podsStore } from "./pods.store"; + +const podsStoreInjectable = getInjectable({ + id: "pods-store", + instantiate: () => podsStore, + causesSideEffects: true, +}); + +export default podsStoreInjectable; diff --git a/src/renderer/components/+workloads-pods/pods.tsx b/src/renderer/components/+workloads-pods/pods.tsx index b3f57db369..fb3e571d84 100644 --- a/src/renderer/components/+workloads-pods/pods.tsx +++ b/src/renderer/components/+workloads-pods/pods.tsx @@ -9,7 +9,6 @@ import React, { Fragment } from "react"; import { observer } from "mobx-react"; import { Link } from "react-router-dom"; import { podsStore } from "./pods.store"; -import type { RouteComponentProps } from "react-router"; import { eventStore } from "../+events/event.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { nodesApi, Pod } from "../../../common/k8s-api/endpoints"; @@ -21,8 +20,8 @@ import kebabCase from "lodash/kebabCase"; import { apiManager } from "../../../common/k8s-api/api-manager"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { Badge } from "../badge"; -import type { PodsRouteParams } from "../../../common/routes"; import { getDetailsUrl } from "../kube-detail-params"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -37,11 +36,8 @@ enum columnId { status = "status", } -export interface PodsProps extends RouteComponentProps { -} - @observer -export class Pods extends React.Component { +export class Pods extends React.Component { renderContainersStatus(pod: Pod) { return pod.getContainerStatuses().map(containerStatus => { const { name, state, ready } = containerStatus; @@ -76,72 +72,74 @@ export class Pods extends React.Component { render() { return ( - getConvertedParts(pod.getName()), - [columnId.namespace]: pod => pod.getNs(), - [columnId.containers]: pod => pod.getContainers().length, - [columnId.restarts]: pod => pod.getRestartsCount(), - [columnId.owners]: pod => pod.getOwnerRefs().map(ref => ref.kind), - [columnId.qos]: pod => pod.getQosClass(), - [columnId.node]: pod => pod.getNodeName(), - [columnId.age]: pod => -pod.getCreationTimestamp(), - [columnId.status]: pod => pod.getStatusMessage(), - }} - searchFilters={[ - pod => pod.getSearchFields(), - pod => pod.getStatusMessage(), - pod => pod.status.podIP, - pod => pod.getNodeName(), - ]} - renderHeaderTitle="Pods" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Containers", className: "containers", sortBy: columnId.containers, id: columnId.containers }, - { title: "Restarts", className: "restarts", sortBy: columnId.restarts, id: columnId.restarts }, - { title: "Controlled By", className: "owners", sortBy: columnId.owners, id: columnId.owners }, - { title: "Node", className: "node", sortBy: columnId.node, id: columnId.node }, - { title: "QoS", className: "qos", sortBy: columnId.qos, id: columnId.qos }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, - ]} - renderTableContents={pod => [ - , - , - pod.getNs(), - this.renderContainersStatus(pod), - pod.getRestartsCount(), - pod.getOwnerRefs().map(ref => { - const { kind, name } = ref; - const detailsLink = getDetailsUrl(apiManager.lookupApiLink(ref, pod)); + + getConvertedParts(pod.getName()), + [columnId.namespace]: pod => pod.getNs(), + [columnId.containers]: pod => pod.getContainers().length, + [columnId.restarts]: pod => pod.getRestartsCount(), + [columnId.owners]: pod => pod.getOwnerRefs().map(ref => ref.kind), + [columnId.qos]: pod => pod.getQosClass(), + [columnId.node]: pod => pod.getNodeName(), + [columnId.age]: pod => -pod.getCreationTimestamp(), + [columnId.status]: pod => pod.getStatusMessage(), + }} + searchFilters={[ + pod => pod.getSearchFields(), + pod => pod.getStatusMessage(), + pod => pod.status.podIP, + pod => pod.getNodeName(), + ]} + renderHeaderTitle="Pods" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Containers", className: "containers", sortBy: columnId.containers, id: columnId.containers }, + { title: "Restarts", className: "restarts", sortBy: columnId.restarts, id: columnId.restarts }, + { title: "Controlled By", className: "owners", sortBy: columnId.owners, id: columnId.owners }, + { title: "Node", className: "node", sortBy: columnId.node, id: columnId.node }, + { title: "QoS", className: "qos", sortBy: columnId.qos, id: columnId.qos }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, + ]} + renderTableContents={pod => [ + , + , + pod.getNs(), + this.renderContainersStatus(pod), + pod.getRestartsCount(), + pod.getOwnerRefs().map(ref => { + const { kind, name } = ref; + const detailsLink = getDetailsUrl(apiManager.lookupApiLink(ref, pod)); - return ( - - - {kind} + return ( + + + {kind} + + + ); + }), + pod.getNodeName() ? + + + {pod.getNodeName()} - ); - }), - pod.getNodeName() ? - - - {pod.getNodeName()} - - - : "", - pod.getQosClass(), - , - { title: pod.getStatusMessage(), className: kebabCase(pod.getStatusMessage()) }, - ]} - /> + : "", + pod.getQosClass(), + , + { title: pod.getStatusMessage(), className: kebabCase(pod.getStatusMessage()) }, + ]} + /> + ); } } diff --git a/src/renderer/components/+workloads-replicasets/replicasets-route-component.injectable.ts b/src/renderer/components/+workloads-replicasets/replicasets-route-component.injectable.ts new file mode 100644 index 0000000000..092d81f6d8 --- /dev/null +++ b/src/renderer/components/+workloads-replicasets/replicasets-route-component.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 } from "@ogre-tools/injectable"; +import { ReplicaSets } from "./replicasets"; +import replicasetsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/replicasets/replicasets-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const replicasetsRouteComponentInjectable = getInjectable({ + id: "replicasets-route-component", + + instantiate: (di) => ({ + route: di.inject(replicasetsRouteInjectable), + Component: ReplicaSets, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default replicasetsRouteComponentInjectable; diff --git a/src/renderer/components/+workloads-replicasets/replicasets-sidebar-items.injectable.tsx b/src/renderer/components/+workloads-replicasets/replicasets-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..3fe3c0dca6 --- /dev/null +++ b/src/renderer/components/+workloads-replicasets/replicasets-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import replicasetsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/replicasets/replicasets-route.injectable"; +import { workloadsSidebarItemId } from "../+workloads/workloads-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToReplicasetsInjectable from "../../../common/front-end-routing/routes/cluster/workloads/replicasets/navigate-to-replicasets.injectable"; + +const replicasetsSidebarItemsInjectable = getInjectable({ + id: "replicasets-sidebar-items", + + instantiate: (di) => { + const route = di.inject(replicasetsRouteInjectable); + const navigateToReplicasets = di.inject(navigateToReplicasetsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "replica-sets", + parentId: workloadsSidebarItemId, + title: "ReplicaSets", + onClick: navigateToReplicasets, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 60, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default replicasetsSidebarItemsInjectable; diff --git a/src/renderer/components/+workloads-replicasets/replicasets-store.injectable.ts b/src/renderer/components/+workloads-replicasets/replicasets-store.injectable.ts new file mode 100644 index 0000000000..5b66d75ca8 --- /dev/null +++ b/src/renderer/components/+workloads-replicasets/replicasets-store.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { replicaSetStore } from "./replicasets.store"; + +const replicasetsStoreInjectable = getInjectable({ + id: "replicasets-store", + instantiate: () => replicaSetStore, + causesSideEffects: true, +}); + +export default replicasetsStoreInjectable; diff --git a/src/renderer/components/+workloads-replicasets/replicasets.tsx b/src/renderer/components/+workloads-replicasets/replicasets.tsx index 08d597efb6..834b33aecb 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.tsx +++ b/src/renderer/components/+workloads-replicasets/replicasets.tsx @@ -9,10 +9,9 @@ import React from "react"; import { observer } from "mobx-react"; import { replicaSetStore } from "./replicasets.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { RouteComponentProps } from "react-router"; import { KubeObjectListLayout } from "../kube-object-list-layout"; -import type { ReplicaSetsRouteParams } from "../../../common/routes"; import { eventStore } from "../+events/event.store"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -24,49 +23,48 @@ enum columnId { age = "age", } -export interface ReplicaSetsProps extends RouteComponentProps { -} - @observer -export class ReplicaSets extends React.Component { +export class ReplicaSets extends React.Component { render() { return ( - replicaSet.getName(), - [columnId.namespace]: replicaSet => replicaSet.getNs(), - [columnId.desired]: replicaSet => replicaSet.getDesired(), - [columnId.current]: replicaSet => replicaSet.getCurrent(), - [columnId.ready]: replicaSet => replicaSet.getReady(), - [columnId.age]: replicaSet => -replicaSet.getCreationTimestamp(), - }} - searchFilters={[ - replicaSet => replicaSet.getSearchFields(), - ]} - renderHeaderTitle="Replica Sets" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { className: "warning", showWithColumn: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Desired", className: "desired", sortBy: columnId.desired, id: columnId.desired }, - { title: "Current", className: "current", sortBy: columnId.current, id: columnId.current }, - { title: "Ready", className: "ready", sortBy: columnId.ready, id: columnId.ready }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={replicaSet => [ - replicaSet.getName(), - , - replicaSet.getNs(), - replicaSet.getDesired(), - replicaSet.getCurrent(), - replicaSet.getReady(), - , - ]} - /> + + replicaSet.getName(), + [columnId.namespace]: replicaSet => replicaSet.getNs(), + [columnId.desired]: replicaSet => replicaSet.getDesired(), + [columnId.current]: replicaSet => replicaSet.getCurrent(), + [columnId.ready]: replicaSet => replicaSet.getReady(), + [columnId.age]: replicaSet => -replicaSet.getCreationTimestamp(), + }} + searchFilters={[ + replicaSet => replicaSet.getSearchFields(), + ]} + renderHeaderTitle="Replica Sets" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Desired", className: "desired", sortBy: columnId.desired, id: columnId.desired }, + { title: "Current", className: "current", sortBy: columnId.current, id: columnId.current }, + { title: "Ready", className: "ready", sortBy: columnId.ready, id: columnId.ready }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={replicaSet => [ + replicaSet.getName(), + , + replicaSet.getNs(), + replicaSet.getDesired(), + replicaSet.getCurrent(), + replicaSet.getReady(), + , + ]} + /> + ); } } diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets-route-component.injectable.ts b/src/renderer/components/+workloads-statefulsets/statefulsets-route-component.injectable.ts new file mode 100644 index 0000000000..0f461e4735 --- /dev/null +++ b/src/renderer/components/+workloads-statefulsets/statefulsets-route-component.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 } from "@ogre-tools/injectable"; +import { StatefulSets } from "./statefulsets"; +import statefulsetsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/statefulsets/statefulsets-route.injectable"; +import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token"; + +const statefulsetsRouteComponentInjectable = getInjectable({ + id: "statefulsets-route-component", + + instantiate: (di) => ({ + route: di.inject(statefulsetsRouteInjectable), + Component: StatefulSets, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default statefulsetsRouteComponentInjectable; diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets-sidebar-items.injectable.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..db78a8eac8 --- /dev/null +++ b/src/renderer/components/+workloads-statefulsets/statefulsets-sidebar-items.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; + +import statefulsetsRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/statefulsets/statefulsets-route.injectable"; +import { workloadsSidebarItemId } from "../+workloads/workloads-sidebar-items.injectable"; +import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable"; +import routeIsActiveInjectable from "../../routes/route-is-active.injectable"; +import navigateToStatefulsetsInjectable from "../../../common/front-end-routing/routes/cluster/workloads/statefulsets/navigate-to-statefulsets.injectable"; + +const statefulsetsSidebarItemsInjectable = getInjectable({ + id: "statefulsets-sidebar-items", + + instantiate: (di) => { + const route = di.inject(statefulsetsRouteInjectable); + const navigateToStatefulsets = di.inject(navigateToStatefulsetsInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + + return computed(() => [ + { + id: "stateful-sets", + parentId: workloadsSidebarItemId, + title: "StatefulSets", + onClick: navigateToStatefulsets, + isActive: routeIsActive, + isVisible: route.isEnabled, + orderNumber: 50, + }, + ]); + }, + + injectionToken: sidebarItemsInjectionToken, +}); + +export default statefulsetsSidebarItemsInjectable; diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets-store.injectable.ts b/src/renderer/components/+workloads-statefulsets/statefulsets-store.injectable.ts new file mode 100644 index 0000000000..832da43271 --- /dev/null +++ b/src/renderer/components/+workloads-statefulsets/statefulsets-store.injectable.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { statefulSetStore } from "./statefulset.store"; + +const statefulsetsStoreInjectable = getInjectable({ + id: "statefulset-store", + instantiate: () => statefulSetStore, + causesSideEffects: true, +}); + +export default statefulsetsStoreInjectable; diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx index 4aed8d3a6f..43963a798c 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx @@ -7,14 +7,13 @@ import "./statefulsets.scss"; import React from "react"; import { observer } from "mobx-react"; -import type { RouteComponentProps } from "react-router"; import type { StatefulSet } from "../../../common/k8s-api/endpoints"; import { podsStore } from "../+workloads-pods/pods.store"; import { statefulSetStore } from "./statefulset.store"; import { eventStore } from "../+events/event.store"; import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; -import type { StatefulSetsRouteParams } from "../../../common/routes"; +import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; enum columnId { @@ -25,11 +24,8 @@ enum columnId { replicas = "replicas", } -export interface StatefulSetsProps extends RouteComponentProps { -} - @observer -export class StatefulSets extends React.Component { +export class StatefulSets extends React.Component { renderPods(statefulSet: StatefulSet) { const { readyReplicas, currentReplicas } = statefulSet.status; @@ -38,38 +34,40 @@ export class StatefulSets extends React.Component { render() { return ( - statefulSet.getName(), - [columnId.namespace]: statefulSet => statefulSet.getNs(), - [columnId.age]: statefulSet => -statefulSet.getCreationTimestamp(), - [columnId.replicas]: statefulSet => statefulSet.getReplicas(), - }} - searchFilters={[ - statefulSet => statefulSet.getSearchFields(), - ]} - renderHeaderTitle="Stateful Sets" - renderTableHeader={[ - { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, - { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, - { title: "Pods", className: "pods", id: columnId.pods }, - { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas }, - { className: "warning", showWithColumn: columnId.replicas }, - { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, - ]} - renderTableContents={statefulSet => [ - statefulSet.getName(), - statefulSet.getNs(), - this.renderPods(statefulSet), - statefulSet.getReplicas(), - , - , - ]} - /> + + statefulSet.getName(), + [columnId.namespace]: statefulSet => statefulSet.getNs(), + [columnId.age]: statefulSet => -statefulSet.getCreationTimestamp(), + [columnId.replicas]: statefulSet => statefulSet.getReplicas(), + }} + searchFilters={[ + statefulSet => statefulSet.getSearchFields(), + ]} + renderHeaderTitle="Stateful Sets" + renderTableHeader={[ + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Pods", className: "pods", id: columnId.pods }, + { title: "Replicas", className: "replicas", sortBy: columnId.replicas, id: columnId.replicas }, + { className: "warning", showWithColumn: columnId.replicas }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + ]} + renderTableContents={statefulSet => [ + statefulSet.getName(), + statefulSet.getNs(), + this.renderPods(statefulSet), + statefulSet.getReplicas(), + , + , + ]} + /> + ); } } diff --git a/src/renderer/components/+workloads/route-tabs.injectable.ts b/src/renderer/components/+workloads/route-tabs.injectable.ts deleted file mode 100644 index c263734af1..0000000000 --- a/src/renderer/components/+workloads/route-tabs.injectable.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import { 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"; -import { DaemonSets } from "../+workloads-daemonsets"; -import { StatefulSets } from "../+workloads-statefulsets"; -import { Jobs } from "../+workloads-jobs"; -import { CronJobs } from "../+workloads-cronjobs"; -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"; - -interface Dependencies { - isAllowedResource: IsAllowedResource; -} - -function getRouteTabs({ isAllowedResource }: Dependencies) { - return computed(() => { - const tabs: TabLayoutRoute[] = [ - { - title: "Overview", - component: WorkloadsOverview, - url: routes.overviewURL(), - routePath: routes.overviewRoute.path.toString(), - }, - ]; - - if (isAllowedResource("pods")) { - tabs.push({ - title: "Pods", - component: Pods, - url: routes.podsURL(), - routePath: routes.podsRoute.path.toString(), - }); - } - - if (isAllowedResource("deployments")) { - tabs.push({ - title: "Deployments", - component: Deployments, - url: routes.deploymentsURL(), - routePath: routes.deploymentsRoute.path.toString(), - }); - } - - if (isAllowedResource("daemonsets")) { - tabs.push({ - title: "DaemonSets", - component: DaemonSets, - url: routes.daemonSetsURL(), - routePath: routes.daemonSetsRoute.path.toString(), - }); - } - - if (isAllowedResource("statefulsets")) { - tabs.push({ - title: "StatefulSets", - component: StatefulSets, - url: routes.statefulSetsURL(), - routePath: routes.statefulSetsRoute.path.toString(), - }); - } - - if (isAllowedResource("replicasets")) { - tabs.push({ - title: "ReplicaSets", - component: ReplicaSets, - url: routes.replicaSetsURL(), - routePath: routes.replicaSetsRoute.path.toString(), - }); - } - - if (isAllowedResource("jobs")) { - tabs.push({ - title: "Jobs", - component: Jobs, - url: routes.jobsURL(), - routePath: routes.jobsRoute.path.toString(), - }); - } - - if (isAllowedResource("cronjobs")) { - tabs.push({ - title: "CronJobs", - component: CronJobs, - url: routes.cronJobsURL(), - routePath: routes.cronJobsRoute.path.toString(), - }); - } - - return tabs; - }); -} - -const workloadsRouteTabsInjectable = getInjectable({ - id: "workloads-route-tabs", - - instantiate: (di) => getRouteTabs({ - isAllowedResource: di.inject(isAllowedResourceInjectable), - }), -}); - -export default workloadsRouteTabsInjectable; diff --git a/src/renderer/components/+workloads/route.tsx b/src/renderer/components/+workloads/route.tsx deleted file mode 100644 index 9778ef5ee9..0000000000 --- a/src/renderer/components/+workloads/route.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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 deleted file mode 100644 index 4301994990..0000000000 --- a/src/renderer/components/+workloads/sidebar-item.tsx +++ /dev/null @@ -1,45 +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 { 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 { renderTabRoutesSidebarItems } 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 ( - } - > - {renderTabRoutesSidebarItems(tabRoutes)} - - ); -}); - -export const WorkloadsSidebarItem = withInjectables(NonInjectedWorkloadsSidebarItem, { - getProps: (di, props) => ({ - routes: di.inject(workloadsRouteTabsInjectable), - ...props, - }), -}); diff --git a/src/renderer/components/+workloads/workloads-sidebar-items.injectable.tsx b/src/renderer/components/+workloads/workloads-sidebar-items.injectable.tsx new file mode 100644 index 0000000000..9a0dfc86af --- /dev/null +++ b/src/renderer/components/+workloads/workloads-sidebar-items.injectable.tsx @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { + SidebarItemRegistration, + sidebarItemsInjectionToken, +} from "../layout/sidebar-items.injectable"; +import { Icon } from "../icon"; +import React from "react"; +import { noop } from "lodash/fp"; + +export const workloadsSidebarItemId = "workloads"; + +const workloadsSidebarItemsInjectable = getInjectable({ + id: "workloads-sidebar-items", + + instantiate: () => + computed((): SidebarItemRegistration[] => [ + { + id: workloadsSidebarItemId, + parentId: null, + title: "Workloads", + getIcon: () => , + onClick: noop, + orderNumber: 20, + }, + ]), + + injectionToken: sidebarItemsInjectionToken, +}); + +export default workloadsSidebarItemsInjectable; diff --git a/src/renderer/components/activate-entity-command/activate-entity-command.tsx b/src/renderer/components/activate-entity-command/activate-entity-command.tsx index 939d750da4..bb353d294e 100644 --- a/src/renderer/components/activate-entity-command/activate-entity-command.tsx +++ b/src/renderer/components/activate-entity-command/activate-entity-command.tsx @@ -32,6 +32,7 @@ const NonInjectedActivateEntityCommand = observer(({ closeCommandOverlay, entiti return ( : <> executeAction(v.value)} components={{ diff --git a/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx b/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx index e951c7a713..cf544f7569 100644 --- a/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx +++ b/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx @@ -4,7 +4,6 @@ */ import React from "react"; -import * as routes from "../../../../common/routes"; import { EntitySettingRegistry, RegisteredEntitySetting } from "../../../../extensions/registries"; import { HotbarAddCommand } from "../../hotbar/hotbar-add-command"; import { HotbarRemoveCommand } from "../../hotbar/hotbar-remove-command"; @@ -16,6 +15,28 @@ import { getInjectable } from "@ogre-tools/injectable"; import commandOverlayInjectable from "../command-overlay.injectable"; import createTerminalTabInjectable from "../../dock/terminal/create-terminal-tab.injectable"; import type { DockTabCreate } from "../../dock/dock/store"; +import navigateToPreferencesInjectable from "../../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; +import navigateToHelmChartsInjectable from "../../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable"; +import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable"; +import navigateToConfigMapsInjectable from "../../../../common/front-end-routing/routes/cluster/config/config-maps/navigate-to-config-maps.injectable"; +import navigateToSecretsInjectable from "../../../../common/front-end-routing/routes/cluster/config/secrets/navigate-to-secrets.injectable"; +import navigateToResourceQuotasInjectable from "../../../../common/front-end-routing/routes/cluster/config/resource-quotas/navigate-to-resource-quotas.injectable"; +import navigateToLimitRangesInjectable from "../../../../common/front-end-routing/routes/cluster/config/limit-ranges/navigate-to-limit-ranges.injectable"; +import navigateToHorizontalPodAutoscalersInjectable from "../../../../common/front-end-routing/routes/cluster/config/horizontal-pod-autoscalers/navigate-to-horizontal-pod-autoscalers.injectable"; +import navigateToPodDisruptionBudgetsInjectable from "../../../../common/front-end-routing/routes/cluster/config/pod-disruption-budgets/navigate-to-pod-disruption-budgets.injectable"; +import navigateToServicesInjectable from "../../../../common/front-end-routing/routes/cluster/network/services/navigate-to-services.injectable"; +import navigateToEndpointsInjectable from "../../../../common/front-end-routing/routes/cluster/network/endpoints/navigate-to-endpoints.injectable"; +import navigateToIngressesInjectable from "../../../../common/front-end-routing/routes/cluster/network/ingresses/navigate-to-ingresses.injectable"; +import navigateToNetworkPoliciesInjectable from "../../../../common/front-end-routing/routes/cluster/network/network-policies/navigate-to-network-policies.injectable"; +import navigateToNodesInjectable from "../../../../common/front-end-routing/routes/cluster/nodes/navigate-to-nodes.injectable"; +import navigateToPodsInjectable from "../../../../common/front-end-routing/routes/cluster/workloads/pods/navigate-to-pods.injectable"; +import navigateToDeploymentsInjectable from "../../../../common/front-end-routing/routes/cluster/workloads/deployments/navigate-to-deployments.injectable"; +import navigateToDaemonsetsInjectable from "../../../../common/front-end-routing/routes/cluster/workloads/daemonsets/navigate-to-daemonsets.injectable"; +import navigateToStatefulsetsInjectable from "../../../../common/front-end-routing/routes/cluster/workloads/statefulsets/navigate-to-statefulsets.injectable"; +import navigateToJobsInjectable from "../../../../common/front-end-routing/routes/cluster/workloads/jobs/navigate-to-jobs.injectable"; +import navigateToCronJobsInjectable from "../../../../common/front-end-routing/routes/cluster/workloads/cron-jobs/navigate-to-cron-jobs.injectable"; +import navigateToCustomResourcesInjectable from "../../../../common/front-end-routing/routes/cluster/custom-resources/custom-resources/navigate-to-custom-resources.injectable"; +import navigateToEntitySettingsInjectable from "../../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; export function isKubernetesClusterActive(context: CommandContext): boolean { return context.entity?.kind === "KubernetesCluster"; @@ -25,181 +46,200 @@ interface Dependencies { openCommandDialog: (component: React.ReactElement) => void; getEntitySettingItems: (kind: string, apiVersion: string, source?: string) => RegisteredEntitySetting[]; createTerminalTab: () => DockTabCreate; + + navigateToPreferences: () => void; + navigateToHelmCharts: () => void; + navigateToHelmReleases: () => void; + navigateToConfigMaps: () => void; + navigateToSecrets: () => void; + navigateToResourceQuotas: () => void; + navigateToLimitRanges: () => void; + navigateToHorizontalPodAutoscalers: () => void; + navigateToPodDisruptionBudgets: () => void; + navigateToServices: () => void; + navigateToEndpoints: () => void; + navigateToIngresses: () => void; + navigateToNetworkPolicies: () => void; + navigateToNodes: () => void; + navigateToPods: () => void; + navigateToDeployments: () => void; + navigateToDaemonsets: () => void; + navigateToStatefulsets: () => void; + navigateToJobs: () => void; + navigateToCronJobs: () => void; + navigateToCustomResources: () => void; + navigateToEntitySettings: (entityId: string) => void; } -function getInternalCommands({ openCommandDialog, getEntitySettingItems, createTerminalTab }: Dependencies): CommandRegistration[] { +function getInternalCommands(dependencies: Dependencies): CommandRegistration[] { return [ { id: "app.showPreferences", title: "Preferences: Open", - action: ({ navigate }) => navigate(routes.preferencesURL(), { - forceRootFrame: true, - }), + action: () => dependencies.navigateToPreferences(), }, { id: "cluster.viewHelmCharts", title: "Cluster: View Helm Charts", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.helmChartsURL()), + action: () => dependencies.navigateToHelmCharts(), }, { id: "cluster.viewHelmReleases", title: "Cluster: View Helm Releases", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.releaseURL()), + action: () => dependencies.navigateToHelmReleases(), }, { id: "cluster.viewConfigMaps", title: "Cluster: View ConfigMaps", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.configMapsURL()), + action: () => dependencies.navigateToConfigMaps(), }, { id: "cluster.viewSecrets", title: "Cluster: View Secrets", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.secretsURL()), + action: () => dependencies.navigateToSecrets(), }, { id: "cluster.viewResourceQuotas", title: "Cluster: View ResourceQuotas", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.resourceQuotaURL()), + action: () => dependencies.navigateToResourceQuotas(), }, { id: "cluster.viewLimitRanges", title: "Cluster: View LimitRanges", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.limitRangeURL()), + action: () => dependencies.navigateToLimitRanges(), }, { id: "cluster.viewHorizontalPodAutoscalers", title: "Cluster: View HorizontalPodAutoscalers (HPA)", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.hpaURL()), + action: () => dependencies.navigateToHorizontalPodAutoscalers(), }, { id: "cluster.viewPodDisruptionBudget", title: "Cluster: View PodDisruptionBudgets", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.pdbURL()), + action: () => dependencies.navigateToPodDisruptionBudgets(), }, { id: "cluster.viewServices", title: "Cluster: View Services", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.servicesURL()), + action: () => dependencies.navigateToServices(), }, { id: "cluster.viewEndpoints", title: "Cluster: View Endpoints", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.endpointURL()), + action: () => dependencies.navigateToEndpoints(), }, { id: "cluster.viewIngresses", title: "Cluster: View Ingresses", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.ingressURL()), + action: () => dependencies.navigateToIngresses(), }, { id: "cluster.viewNetworkPolicies", title: "Cluster: View NetworkPolicies", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.networkPoliciesURL()), + action: () => dependencies.navigateToNetworkPolicies, }, { id: "cluster.viewNodes", title: "Cluster: View Nodes", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.nodesURL()), + action: () => dependencies.navigateToNodes(), }, { id: "cluster.viewPods", title: "Cluster: View Pods", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.podsURL()), + action: () => dependencies.navigateToPods(), }, { id: "cluster.viewDeployments", title: "Cluster: View Deployments", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.deploymentsURL()), + action: () => dependencies.navigateToDeployments(), }, { id: "cluster.viewDaemonSets", title: "Cluster: View DaemonSets", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.daemonSetsURL()), + action: () => dependencies.navigateToDaemonsets(), }, { id: "cluster.viewStatefulSets", title: "Cluster: View StatefulSets", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.statefulSetsURL()), + action: () => dependencies.navigateToStatefulsets(), }, { id: "cluster.viewJobs", title: "Cluster: View Jobs", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.jobsURL()), + action: () => dependencies.navigateToJobs(), }, { id: "cluster.viewCronJobs", title: "Cluster: View CronJobs", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.cronJobsURL()), + action: () => dependencies.navigateToCronJobs(), }, { id: "cluster.viewCustomResourceDefinitions", title: "Cluster: View Custom Resource Definitions", isActive: isKubernetesClusterActive, - action: ({ navigate }) => navigate(routes.crdURL()), + action: () => dependencies.navigateToCustomResources(), }, { id: "entity.viewSettings", title: ({ entity }) => `${entity.kind}/${entity.getName()}: View Settings`, - action: ({ entity, navigate }) => navigate(`/entity/${entity.getId()}/settings`, { - forceRootFrame: true, - }), + action: ({ entity }) => dependencies.navigateToEntitySettings(entity.getId()), isActive: ({ entity }) => { if (!entity) { return false; } - return getEntitySettingItems(entity.kind, entity.apiVersion, entity.metadata.source).length > 0; + return dependencies.getEntitySettingItems(entity.kind, entity.apiVersion, entity.metadata.source).length > 0; }, }, { id: "cluster.openTerminal", title: "Cluster: Open terminal", - action: () => createTerminalTab(), + action: () => dependencies.createTerminalTab(), isActive: isKubernetesClusterActive, }, { id: "hotbar.switchHotbar", title: "Hotbar: Switch ...", - action: () => openCommandDialog(), + action: () => dependencies.openCommandDialog(), }, { id: "hotbar.addHotbar", title: "Hotbar: Add Hotbar ...", - action: () => openCommandDialog(), + action: () => dependencies.openCommandDialog(), }, { id: "hotbar.removeHotbar", title: "Hotbar: Remove Hotbar ...", - action: () => openCommandDialog(), + action: () => dependencies.openCommandDialog(), }, { id: "hotbar.renameHotbar", title: "Hotbar: Rename Hotbar ...", - action: () => openCommandDialog(), + action: () => dependencies.openCommandDialog(), }, { id: "catalog.searchEntities", title: "Catalog: Activate Entity ...", - action: () => openCommandDialog(), + action: () => dependencies.openCommandDialog(), }, ]; } @@ -213,6 +253,29 @@ const internalCommandsInjectable = getInjectable({ .getInstance() .getItemsForKind, createTerminalTab: di.inject(createTerminalTabInjectable), + + navigateToPreferences: di.inject(navigateToPreferencesInjectable), + navigateToHelmCharts: di.inject(navigateToHelmChartsInjectable), + navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable), + navigateToConfigMaps: di.inject(navigateToConfigMapsInjectable), + navigateToSecrets: di.inject(navigateToSecretsInjectable), + navigateToResourceQuotas: di.inject(navigateToResourceQuotasInjectable), + navigateToLimitRanges: di.inject(navigateToLimitRangesInjectable), + navigateToHorizontalPodAutoscalers: di.inject(navigateToHorizontalPodAutoscalersInjectable), + navigateToPodDisruptionBudgets: di.inject(navigateToPodDisruptionBudgetsInjectable), + navigateToServices: di.inject(navigateToServicesInjectable), + navigateToEndpoints: di.inject(navigateToEndpointsInjectable), + navigateToIngresses: di.inject(navigateToIngressesInjectable), + navigateToNetworkPolicies: di.inject(navigateToNetworkPoliciesInjectable), + navigateToNodes: di.inject(navigateToNodesInjectable), + navigateToPods: di.inject(navigateToPodsInjectable), + navigateToDeployments: di.inject(navigateToDeploymentsInjectable), + navigateToDaemonsets: di.inject(navigateToDaemonsetsInjectable), + navigateToStatefulsets: di.inject(navigateToStatefulsetsInjectable), + navigateToJobs: di.inject(navigateToJobsInjectable), + navigateToCronJobs: di.inject(navigateToCronJobsInjectable), + navigateToCustomResources: di.inject(navigateToCustomResourcesInjectable), + navigateToEntitySettings: di.inject(navigateToEntitySettingsInjectable), }), }); diff --git a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx index f341faa185..1fe1d6e339 100644 --- a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx +++ b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx @@ -4,7 +4,7 @@ */ import "@testing-library/jest-dom/extend-expect"; import { KubeConfig } from "@kubernetes/client-node"; -import { fireEvent, render } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; import mockFs from "mock-fs"; import React from "react"; import * as selectEvent from "react-select-event"; @@ -16,6 +16,11 @@ import type { ClusterModel } from "../../../../common/cluster-types"; import { getDisForUnitTesting } from "../../../../test-utils/get-dis-for-unit-testing"; import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token"; import createContextHandlerInjectable from "../../../../main/context-handler/create-context-handler.injectable"; +import deleteClusterDialogModelInjectable from "../delete-cluster-dialog-model/delete-cluster-dialog-model.injectable"; +import type { DeleteClusterDialogModel } from "../delete-cluster-dialog-model/delete-cluster-dialog-model"; +import type { DiRender } from "../../test-utils/renderFor"; +import { renderFor } from "../../test-utils/renderFor"; +import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable"; jest.mock("electron", () => ({ app: { @@ -88,16 +93,23 @@ let config: KubeConfig; describe("", () => { let createCluster: (model: ClusterModel) => Cluster; + let deleteClusterDialogModel: DeleteClusterDialogModel; + let render: DiRender; beforeEach(async () => { - const { mainDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true }); + const { mainDi, rendererDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true }); + + render = renderFor(rendererDi); mainDi.override(createContextHandlerInjectable, () => () => undefined); mockFs(); + rendererDi.override(hotbarStoreInjectable, () => ({})); + await runSetups(); + deleteClusterDialogModel = rendererDi.inject(deleteClusterDialogModelInjectable); createCluster = mainDi.inject(createClusterInjectionToken); }); @@ -137,7 +149,7 @@ describe("", () => { kubeConfigPath: "./temp-kube-config", }); - DeleteClusterDialog.open({ cluster, config }); + deleteClusterDialogModel.open({ cluster, config }); const { getByText } = render(); const message = "The contents of kubeconfig file will be changed!"; @@ -155,7 +167,7 @@ describe("", () => { kubeConfigPath: "./temp-kube-config", }); - DeleteClusterDialog.open({ cluster, config }); + deleteClusterDialogModel.open({ cluster, config }); const { getByTestId } = render(); @@ -172,7 +184,7 @@ describe("", () => { kubeConfigPath: "./temp-kube-config", }); - DeleteClusterDialog.open({ cluster, config }); + deleteClusterDialogModel.open({ cluster, config }); const { getByText } = render(); @@ -193,7 +205,7 @@ describe("", () => { kubeConfigPath: "./temp-kube-config", }); - DeleteClusterDialog.open({ cluster, config }); + deleteClusterDialogModel.open({ cluster, config }); const { getByText, getByTestId } = render(); const link = getByTestId("context-switch"); @@ -220,7 +232,7 @@ describe("", () => { const spy = jest.spyOn(cluster, "isInLocalKubeconfig").mockImplementation(() => true); - DeleteClusterDialog.open({ cluster, config }); + deleteClusterDialogModel.open({ cluster, config }); const { getByTestId } = render(); @@ -256,7 +268,7 @@ describe("", () => { kubeConfigPath: "./temp-kube-config", }); - DeleteClusterDialog.open({ cluster, config }); + deleteClusterDialogModel.open({ cluster, config }); const { getByTestId } = render(); diff --git a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.injectable.ts b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.injectable.ts new file mode 100644 index 0000000000..c5ddcfd43b --- /dev/null +++ b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { DeleteClusterDialogModel } from "./delete-cluster-dialog-model"; + +const deleteClusterDialogModelInjectable = getInjectable({ + id: "delete-cluster-dialog-model", + instantiate: () => new DeleteClusterDialogModel(), +}); + +export default deleteClusterDialogModelInjectable; diff --git a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.ts b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.ts new file mode 100644 index 0000000000..55be1e592a --- /dev/null +++ b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { KubeConfig } from "@kubernetes/client-node"; +import { observable, makeObservable, action } from "mobx"; +import type { Cluster } from "../../../../common/cluster/cluster"; + +export class DeleteClusterDialogModel { + isOpen = false; + + constructor() { + makeObservable(this, { + isOpen: observable, + open: action, + close: action, + }); + } + + cluster: Cluster; + config: KubeConfig; + + open = ({ cluster, config }: { cluster: Cluster; config: KubeConfig }) => { + this.isOpen = true; + + this.cluster = cluster; + this.config = config; + }; + + close = () => { + this.isOpen = false; + }; +} diff --git a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx index a49dcde13e..0fd44c48c2 100644 --- a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx +++ b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx @@ -9,52 +9,35 @@ import { observer } from "mobx-react"; import React from "react"; import { Button } from "../button"; -import type { KubeConfig } from "@kubernetes/client-node"; -import type { Cluster } from "../../../common/cluster/cluster"; import { saveKubeconfig } from "./save-config"; import { Notifications } from "../notifications"; -import { HotbarStore } from "../../../common/hotbar-store"; import { boundMethod } from "autobind-decorator"; import { Dialog } from "../dialog"; import { Icon } from "../icon"; import { Select } from "../select"; import { Checkbox } from "../checkbox"; import { requestClearClusterAsDeleting, requestDeleteCluster, requestSetClusterAsDeleting } from "../../ipc"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import hotbarStoreInjectable from "../../../common/hotbar-store.injectable"; +import type { DeleteClusterDialogModel } from "./delete-cluster-dialog-model/delete-cluster-dialog-model"; +import deleteClusterDialogModelInjectable from "./delete-cluster-dialog-model/delete-cluster-dialog-model.injectable"; +import type { HotbarStore } from "../../../common/hotbar-store"; -interface DialogState { - isOpen: boolean; - config?: KubeConfig; - cluster?: Cluster; +interface Dependencies { + hotbarStore: HotbarStore; + model: DeleteClusterDialogModel; } -const dialogState: DialogState = observable({ - isOpen: false, -}); - -export interface DeleteClusterDialogProps {} - @observer -export class DeleteClusterDialog extends React.Component { +class NonInjectedDeleteClusterDialog extends React.Component { @observable showContextSwitch = false; @observable newCurrentContext = ""; - constructor(props: DeleteClusterDialogProps) { + constructor(props: Dependencies) { super(props); makeObservable(this); } - static open({ config, cluster }: Partial) { - dialogState.isOpen = true; - dialogState.config = config; - dialogState.cluster = cluster; - } - - static close() { - dialogState.isOpen = false; - dialogState.cluster = null; - dialogState.config = null; - } - @boundMethod onOpen() { this.newCurrentContext = ""; @@ -66,25 +49,25 @@ export class DeleteClusterDialog extends React.Component - item.name !== dialogState.cluster.contextName, + this.props.model.config.contexts = this.props.model.config.contexts.filter(item => + item.name !== this.props.model.cluster.contextName, ); } changeCurrentContext() { if (this.newCurrentContext && this.showContextSwitch) { - dialogState.config.currentContext = this.newCurrentContext; + this.props.model.config.currentContext = this.newCurrentContext; } } @boundMethod async onDelete() { - const { cluster, config } = dialogState; + const { cluster, config } = this.props.model; await requestSetClusterAsDeleting(cluster.id); this.removeContext(); @@ -92,7 +75,7 @@ export class DeleteClusterDialog extends React.Component context.name !== cluster.contextName).length == 0; const newContextNotSelected = this.newCurrentContext === ""; @@ -116,12 +99,12 @@ export class DeleteClusterDialog extends React.Component context.name !== cluster.contextName); const options = [ @@ -134,6 +117,7 @@ export class DeleteClusterDialog extends React.Component Promise; installChartStore: InstallChartTabStore; dockStore: DockStore; + navigateToHelmReleases: NavigateToHelmReleases; } @observer @@ -74,12 +75,11 @@ class NonInjectedInstallChart extends Component viewRelease = () => { const { release } = this.releaseDetails; - navigate(releaseURL({ - params: { - name: release.name, - namespace: release.namespace, - }, - })); + this.props.navigateToHelmReleases({ + name: release.name, + namespace: release.namespace, + }); + this.props.dockStore.closeTab(this.tabId); }; @@ -171,6 +171,7 @@ class NonInjectedInstallChart extends Component Version opt.value === pod)} formatOptionLabel={option => option.label} @@ -93,6 +94,7 @@ export const LogResourceSelector = observer(({ model }: LogResourceSelectorProps /> Container ", () => { di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.permitSideEffects(hotbarStoreInjectable); + di.permitSideEffects(getConfigurationFileModelInjectable); + di.permitSideEffects(appVersionInjectable); + render = renderFor(di); UserStore.createInstance(); @@ -57,7 +63,7 @@ describe("", () => { }); it("renders w/o errors", async () => { - di.override(hotbarManagerInjectable, () => ({ + di.override(hotbarStoreInjectable, () => ({ hotbars: [mockHotbars["1"]], getById: (id: string) => mockHotbars[id], remove: () => { @@ -76,7 +82,7 @@ describe("", () => { it("calls remove if you click on the entry", async () => { const removeMock = jest.fn(); - di.override(hotbarManagerInjectable, () => ({ + di.override(hotbarStoreInjectable, () => ({ hotbars: [mockHotbars["1"]], getById: (id: string) => mockHotbars[id], remove: removeMock, diff --git a/src/renderer/components/hotbar/hotbar-add-command.tsx b/src/renderer/components/hotbar/hotbar-add-command.tsx index c283a6dc05..ee270dd866 100644 --- a/src/renderer/components/hotbar/hotbar-add-command.tsx +++ b/src/renderer/components/hotbar/hotbar-add-command.tsx @@ -9,7 +9,7 @@ import { Input, InputValidator } from "../input"; import type { CreateHotbarData, CreateHotbarOptions } from "../../../common/hotbar-types"; import { withInjectables } from "@ogre-tools/injectable-react"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; -import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; +import hotbarStoreInjectable from "../../../common/hotbar-store.injectable"; import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable"; interface Dependencies { @@ -50,7 +50,7 @@ const NonInjectedHotbarAddCommand = observer(({ closeCommandOverlay, addHotbar, export const HotbarAddCommand = withInjectables(NonInjectedHotbarAddCommand, { getProps: (di, props) => ({ closeCommandOverlay: di.inject(commandOverlayInjectable).close, - addHotbar: di.inject(hotbarManagerInjectable).add, + addHotbar: di.inject(hotbarStoreInjectable).add, uniqueHotbarName: di.inject(uniqueHotbarNameInjectable), ...props, }), diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index d994d7536c..f40ddd8108 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -10,7 +10,6 @@ import { observer } from "mobx-react"; import { HotbarEntityIcon } from "./hotbar-entity-icon"; import { cssNames, IClassName } from "../../utils"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -import { HotbarStore } from "../../../common/hotbar-store"; import type { CatalogEntity } from "../../api/catalog-entity"; import { DragDropContext, Draggable, Droppable, type DropResult } from "react-beautiful-dnd"; import { HotbarSelector } from "./hotbar-selector"; @@ -18,26 +17,33 @@ import { HotbarCell } from "./hotbar-cell"; import { HotbarIcon } from "./hotbar-icon"; import { defaultHotbarCells, HotbarItem } from "../../../common/hotbar-types"; import { action, makeObservable, observable } from "mobx"; +import hotbarStoreInjectable from "../../../common/hotbar-store.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import type { HotbarStore } from "../../../common/hotbar-store"; export interface HotbarMenuProps { className?: IClassName; } +interface Dependencies { + hotbarStore: HotbarStore; +} + @observer -export class HotbarMenu extends React.Component { +class NonInjectedHotbarMenu extends React.Component { @observable draggingOver = false; - constructor(props: HotbarMenuProps) { + constructor(props: Dependencies & HotbarMenuProps) { super(props); makeObservable(this); } get hotbar() { - return HotbarStore.getInstance().getActive(); + return this.props.hotbarStore.getActive(); } getEntity(item: HotbarItem) { - const hotbar = HotbarStore.getInstance().getActive(); + const hotbar = this.props.hotbarStore.getActive(); if (!hotbar) { return null; @@ -64,17 +70,17 @@ export class HotbarMenu extends React.Component { const from = parseInt(source.droppableId); const to = parseInt(destination.droppableId); - HotbarStore.getInstance().restackItems(from, to); + this.props.hotbarStore.restackItems(from, to); } removeItem(uid: string) { - const hotbar = HotbarStore.getInstance(); + const hotbar = this.props.hotbarStore; hotbar.removeFromHotbar(uid); } addItem(entity: CatalogEntity, index = -1) { - const hotbar = HotbarStore.getInstance(); + const hotbar = this.props.hotbarStore; hotbar.addToHotbar(entity, index); } @@ -160,8 +166,7 @@ export class HotbarMenu extends React.Component { } render() { - const { className } = this.props; - const hotbarStore = HotbarStore.getInstance(); + const { className, hotbarStore } = this.props; const hotbar = hotbarStore.getActive(); return ( @@ -176,3 +181,14 @@ export class HotbarMenu extends React.Component { ); } } + +export const HotbarMenu = withInjectables( + NonInjectedHotbarMenu, + + { + getProps: (di, props) => ({ + hotbarStore: di.inject(hotbarStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/hotbar/hotbar-remove-command.tsx b/src/renderer/components/hotbar/hotbar-remove-command.tsx index ed3cd275ca..27189bdebb 100644 --- a/src/renderer/components/hotbar/hotbar-remove-command.tsx +++ b/src/renderer/components/hotbar/hotbar-remove-command.tsx @@ -6,7 +6,7 @@ import React from "react"; import { observer } from "mobx-react"; import { Select } from "../select"; -import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; +import hotbarStoreInjectable from "../../../common/hotbar-store.injectable"; import { ConfirmDialog } from "../confirm-dialog"; import { withInjectables } from "@ogre-tools/injectable-react"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; @@ -14,7 +14,7 @@ import type { Hotbar } from "../../../common/hotbar-types"; interface Dependencies { closeCommandOverlay: () => void; - hotbarManager: { + hotbarStore: { hotbars: Hotbar[]; getById: (id: string) => Hotbar | undefined; remove: (hotbar: Hotbar) => void; @@ -22,14 +22,14 @@ interface Dependencies { }; } -const NonInjectedHotbarRemoveCommand = observer(({ closeCommandOverlay, hotbarManager }: Dependencies) => { - const options = hotbarManager.hotbars.map(hotbar => ({ +const NonInjectedHotbarRemoveCommand = observer(({ closeCommandOverlay, hotbarStore }: Dependencies) => { + const options = hotbarStore.hotbars.map(hotbar => ({ value: hotbar.id, - label: hotbarManager.getDisplayLabel(hotbar), + label: hotbarStore.getDisplayLabel(hotbar), })); const onChange = (id: string): void => { - const hotbar = hotbarManager.getById(id); + const hotbar = hotbarStore.getById(id); if (!hotbar) { return; @@ -43,7 +43,7 @@ const NonInjectedHotbarRemoveCommand = observer(({ closeCommandOverlay, hotbarMa primary: false, accent: true, }, - ok: () => hotbarManager.remove(hotbar), + ok: () => hotbarStore.remove(hotbar), message: (

@@ -56,6 +56,7 @@ const NonInjectedHotbarRemoveCommand = observer(({ closeCommandOverlay, hotbarMa return ( onSelect(v.value)} components={{ DropdownIndicator: null, IndicatorSeparator: null }} @@ -84,7 +85,7 @@ const NonInjectedHotbarRenameCommand = observer(({ closeCommandOverlay, hotbarMa export const HotbarRenameCommand = withInjectables(NonInjectedHotbarRenameCommand, { getProps: (di, props) => ({ closeCommandOverlay: di.inject(commandOverlayInjectable).close, - hotbarManager: di.inject(hotbarManagerInjectable), + hotbarStore: di.inject(hotbarStoreInjectable), uniqueHotbarName: di.inject(uniqueHotbarNameInjectable), ...props, }), diff --git a/src/renderer/components/hotbar/hotbar-selector.tsx b/src/renderer/components/hotbar/hotbar-selector.tsx index 68a8f0e81b..59e39605e2 100644 --- a/src/renderer/components/hotbar/hotbar-selector.tsx +++ b/src/renderer/components/hotbar/hotbar-selector.tsx @@ -7,7 +7,7 @@ import styles from "./hotbar-selector.module.scss"; import React, { useRef, useState } from "react"; import { Icon } from "../icon"; import { Badge } from "../badge"; -import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; +import hotbarStoreInjectable from "../../../common/hotbar-store.injectable"; import { HotbarSwitchCommand } from "./hotbar-switch-command"; import { Tooltip, TooltipPosition } from "../tooltip"; import { observer } from "mobx-react"; @@ -17,7 +17,7 @@ import commandOverlayInjectable from "../command-palette/command-overlay.injecta import { cssNames } from "../../utils"; interface Dependencies { - hotbarManager: { + hotbarStore: { switchToPrevious: () => void; switchToNext: () => void; getActive: () => Hotbar; @@ -30,7 +30,7 @@ export interface HotbarSelectorProps extends Partial { hotbar: Hotbar; } -const NonInjectedHotbarSelector = observer(({ hotbar, hotbarManager, openCommandOverlay }: HotbarSelectorProps & Dependencies) => { +const NonInjectedHotbarSelector = observer(({ hotbar, hotbarStore, openCommandOverlay }: HotbarSelectorProps & Dependencies) => { const [tooltipVisible, setTooltipVisible] = useState(false); const tooltipTimeout = useRef(); @@ -59,13 +59,13 @@ const NonInjectedHotbarSelector = observer(({ hotbar, hotbarManager, openCommand onArrowClick(hotbarManager.switchToPrevious)} + onClick={() => onArrowClick(hotbarStore.switchToPrevious)} />

openCommandOverlay()} className={styles.Badge} onMouseEnter={onMouseEvent} @@ -79,14 +79,14 @@ const NonInjectedHotbarSelector = observer(({ hotbar, hotbarManager, openCommand {hotbar.name}
- onArrowClick(hotbarManager.switchToNext)} /> + onArrowClick(hotbarStore.switchToNext)} />
); }); export const HotbarSelector = withInjectables(NonInjectedHotbarSelector, { getProps: (di, props) => ({ - hotbarManager: di.inject(hotbarManagerInjectable), + hotbarStore: di.inject(hotbarStoreInjectable), openCommandOverlay: di.inject(commandOverlayInjectable).open, ...props, }), diff --git a/src/renderer/components/hotbar/hotbar-switch-command.tsx b/src/renderer/components/hotbar/hotbar-switch-command.tsx index d9e9d76c00..c4100ba483 100644 --- a/src/renderer/components/hotbar/hotbar-switch-command.tsx +++ b/src/renderer/components/hotbar/hotbar-switch-command.tsx @@ -6,39 +6,33 @@ import React from "react"; import { observer } from "mobx-react"; import { Select } from "../select"; -import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; +import hotbarStoreInjectable from "../../../common/hotbar-store.injectable"; import type { CommandOverlay } from "../command-palette"; import { HotbarAddCommand } from "./hotbar-add-command"; import { HotbarRemoveCommand } from "./hotbar-remove-command"; import { HotbarRenameCommand } from "./hotbar-rename-command"; -import type { Hotbar } from "../../../common/hotbar-types"; import { withInjectables } from "@ogre-tools/injectable-react"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; +import type { HotbarStore } from "../../../common/hotbar-store"; const addActionId = "__add__"; const removeActionId = "__remove__"; const renameActionId = "__rename__"; -interface HotbarManager { - hotbars: Hotbar[]; - setActiveHotbar: (id: string) => void; - getDisplayLabel: (hotbar: Hotbar) => string; -} - interface Dependencies { - hotbarManager: HotbarManager; + hotbarStore: HotbarStore; commandOverlay: CommandOverlay; } -function getHotbarSwitchOptions(hotbarManager: HotbarManager) { - const options = hotbarManager.hotbars.map(hotbar => ({ +function getHotbarSwitchOptions(hotbarStore: HotbarStore) { + const options = hotbarStore.hotbars.map(hotbar => ({ value: hotbar.id, - label: hotbarManager.getDisplayLabel(hotbar), + label: hotbarStore.getDisplayLabel(hotbar), })); options.push({ value: addActionId, label: "Add hotbar ..." }); - if (hotbarManager.hotbars.length > 1) { + if (hotbarStore.hotbars.length > 1) { options.push({ value: removeActionId, label: "Remove hotbar ..." }); } @@ -47,8 +41,8 @@ function getHotbarSwitchOptions(hotbarManager: HotbarManager) { return options; } -const NonInjectedHotbarSwitchCommand = observer(({ hotbarManager, commandOverlay }: Dependencies) => { - const options = getHotbarSwitchOptions(hotbarManager); +const NonInjectedHotbarSwitchCommand = observer(({ hotbarStore, commandOverlay }: Dependencies) => { + const options = getHotbarSwitchOptions(hotbarStore); const onChange = (idOrAction: string): void => { switch (idOrAction) { @@ -59,13 +53,14 @@ const NonInjectedHotbarSwitchCommand = observer(({ hotbarManager, commandOverlay case renameActionId: return commandOverlay.open(); default: - hotbarManager.setActiveHotbar(idOrAction); + hotbarStore.setActiveHotbar(idOrAction); commandOverlay.close(); } }; return ( ", () => { let render: DiRender; beforeEach(async () => { - di = getDiForUnitTesting({ doGeneralOverrides: true }); render = renderFor(di); @@ -39,6 +39,9 @@ describe("", () => { const onChange = jest.fn(); - const { container } = render(); expect(container).toBeInstanceOf(HTMLElement); }); @@ -83,7 +86,7 @@ describe("); + const { container } = render(", () => { const onChange = jest.fn(); - const { container, rerender } = render(); const selectedValueContainer = container.querySelector(".Select__single-value"); expect(selectedValueContainer.textContent).toBe(options[0].label); - rerender(); expect(container.querySelector(".Select__single-value").textContent).toBe(options[1].label); }); @@ -127,12 +130,12 @@ describe("); + const { container, rerender } = render(); + rerender(", () => { const onChange = jest.fn(); - const { container, rerender } = render(); const selectedValueContainer = container.querySelector(".Select__single-value"); expect(selectedValueContainer.textContent).toBe(options[0].label); - rerender(); expect(container.querySelector(".Select__single-value")).not.toBeInTheDocument(); }); diff --git a/src/renderer/components/select/select.tsx b/src/renderer/components/select/select.tsx index c2712cf25f..cf22511ce2 100644 --- a/src/renderer/components/select/select.tsx +++ b/src/renderer/components/select/select.tsx @@ -31,6 +31,7 @@ export interface SelectOption { } export interface SelectProps extends ReactSelectProps, CreatableProps { + id: string; value?: T; themeName?: "dark" | "light" | "outlined" | "lens"; menuClass?: string; @@ -41,7 +42,7 @@ export interface SelectProps extends ReactSelectProps, Crea @observer export class Select extends React.Component { - static defaultProps: SelectProps = { + static defaultProps: Omit = { autoConvertOptions: true, menuPortalTarget: document.body, menuPlacement: "auto", @@ -115,12 +116,13 @@ export class Select extends React.Component { render() { const { className, menuClass, isCreatable, autoConvertOptions, - value, options, components = {}, ...props + value, options, components = {}, id: inputId, ...props } = this.props; const WrappedMenu = components.Menu ?? Menu; const selectProps: Partial = { ...props, + inputId, styles: this.styles, value: autoConvertOptions ? this.selectedOption : value, options: autoConvertOptions ? this.options : options, diff --git a/src/renderer/components/tabs/tabs.tsx b/src/renderer/components/tabs/tabs.tsx index be70447f5a..9eb5c67471 100644 --- a/src/renderer/components/tabs/tabs.tsx +++ b/src/renderer/components/tabs/tabs.tsx @@ -60,7 +60,7 @@ export interface TabProps extends DOMAttributes { disabled?: boolean; icon?: React.ReactNode | string; // material-ui name or custom icon label?: React.ReactNode; - value: D; + value?: D; } export class Tab extends React.PureComponent { @@ -79,9 +79,8 @@ export class Tab extends React.PureComponent { } scrollIntoView() { - if (typeof this.ref.current?.scrollIntoViewIfNeeded === "function") { - this.ref.current.scrollIntoViewIfNeeded(); - } + // Note: .scrollIntoViewIfNeeded() is non-standard and thus not present in js-dom. + this.ref.current?.scrollIntoViewIfNeeded?.(); } @boundMethod diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx new file mode 100644 index 0000000000..ef0fbdafb2 --- /dev/null +++ b/src/renderer/components/test-utils/get-application-builder.tsx @@ -0,0 +1,314 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; +import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable"; +import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable"; +import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token"; +import { computed, IObservableArray, observable, runInAction } from "mobx"; +import { renderFor } from "./renderFor"; +import observableHistoryInjectable from "../../navigation/observable-history.injectable"; +import React from "react"; +import { Router } from "react-router"; +import { Observer } from "mobx-react"; +import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; +import allowedResourcesInjectable from "../../../common/cluster-store/allowed-resources.injectable"; +import { fireEvent, RenderResult } from "@testing-library/react"; +import type { KubeResource } from "../../../common/rbac"; +import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; +import { Sidebar } from "../layout/sidebar"; +import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing"; +import type { DiContainer } from "@ogre-tools/injectable"; +import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable"; +import type { ClusterStore } from "../../../common/cluster-store/cluster-store"; +import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable"; +import type { LensMainExtension } from "../../../extensions/lens-main-extension"; +import currentRouteComponentInjectable from "../../routes/current-route-component.injectable"; +import { pipeline } from "@ogre-tools/fp"; +import { flatMap, compact, join, get, filter } from "lodash/fp"; +import preferenceNavigationItemsInjectable from "../+preferences/preferences-navigation/preference-navigation-items.injectable"; +import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; +import applicationMenuItemsInjectable, { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable"; + +type Callback = (dis: DiContainers) => void | Promise; + +export interface ApplicationBuilder { + dis: DiContainers; + setEnvironmentToClusterFrame: () => ApplicationBuilder; + addExtensions: (...extensions: LensRendererExtension[]) => Promise; + allowKubeResource: (resourceName: KubeResource) => ApplicationBuilder; + beforeSetups: (callback: Callback) => ApplicationBuilder; + beforeRender: (callback: Callback) => ApplicationBuilder; + render: () => Promise; + + applicationMenu: { + click: (path: string) => void; + }; + + preferences: { + close: () => void; + navigate: () => void; + navigation: { + click: (id: string) => void; + }; + }; +} + +interface DiContainers { + rendererDi: DiContainer; + mainDi: DiContainer; +} + +interface Environment { + renderSidebar: () => React.ReactNode; + onAllowKubeResource: () => void; +} + +export const getApplicationBuilder = () => { + const { rendererDi, mainDi, runSetups } = getDisForUnitTesting({ + doGeneralOverrides: true, + }); + + const dis = { rendererDi, mainDi }; + + const clusterStoreStub = { + getById: (): null => null, + } as unknown as ClusterStore; + + rendererDi.override(clusterStoreInjectable, () => clusterStoreStub); + mainDi.override(clusterStoreInjectable, () => clusterStoreStub); + + const beforeSetupsCallbacks: Callback[] = []; + const beforeRenderCallbacks: Callback[] = []; + + const extensionsState = observable.array(); + + rendererDi.override(subscribeStoresInjectable, () => () => () => {}); + + const environments: Record = { + application: { + renderSidebar: () => null, + + onAllowKubeResource: () => { + throw new Error( + "Tried to allow kube resource when environment is not cluster frame.", + ); + }, + }, + + clusterFrame: { + renderSidebar: () => , + onAllowKubeResource: () => {}, + }, + }; + + let environment = environments.application; + + rendererDi.override( + currentlyInClusterFrameInjectable, + () => environment === environments.clusterFrame, + ); + + rendererDi.override(rendererExtensionsInjectable, () => + computed(() => extensionsState), + ); + + mainDi.override(mainExtensionsInjectable, () => + computed((): LensMainExtension[] => []), + ); + + let allowedResourcesState: IObservableArray; + let rendered: RenderResult; + + const builder: ApplicationBuilder = { + dis, + + applicationMenu: { + click: (path: string) => { + const applicationMenuItems = mainDi.inject( + applicationMenuItemsInjectable, + ); + + const menuItems = pipeline( + applicationMenuItems.get(), + flatMap(toFlatChildren(null)), + filter((menuItem) => !!menuItem.click), + ); + + const menuItem = menuItems.find((menuItem) => menuItem.path === path); + + if (!menuItem) { + const availableIds = menuItems.map(get("path")).join('", "'); + + throw new Error( + `Tried to click application menu item with ID "${path}" which does not exist. Available IDs are: "${availableIds}"`, + ); + } + + menuItem.click(undefined, undefined, undefined); + }, + }, + + preferences: { + close: () => { + const link = rendered.getByTestId("close-preferences"); + + fireEvent.click(link); + }, + + navigate: () => { + const navigateToPreferences = rendererDi.inject(navigateToPreferencesInjectable); + + navigateToPreferences(); + }, + + navigation: { + click: (id: string) => { + const link = rendered.queryByTestId(`tab-link-for-${id}`); + + if (!link) { + const preferencesNavigationItems = rendererDi.inject( + preferenceNavigationItemsInjectable, + ); + + const availableIds = preferencesNavigationItems + .get() + .map(get("id")); + + throw new Error( + `Tried to click navigation item "${id}" which does not exist in preferences. Available IDs are "${availableIds.join( + '", "', + )}"`, + ); + } + + fireEvent.click(link); + }, + }, + }, + + setEnvironmentToClusterFrame: () => { + environment = environments.clusterFrame; + + allowedResourcesState = observable.array(); + + rendererDi.override(allowedResourcesInjectable, () => + computed(() => new Set([...allowedResourcesState])), + ); + + rendererDi.override( + directoryForLensLocalStorageInjectable, + () => "/irrelevant", + ); + + return builder; + }, + + addExtensions: async (...extensions) => { + const extensionRegistrators = rendererDi.injectMany( + extensionRegistratorInjectionToken, + ); + + const addAndEnableExtensions = async () => { + const registratorPromises = extensions.flatMap((extension) => + extensionRegistrators.map((registrator) => registrator(extension, 1)), + ); + + await Promise.all(registratorPromises); + + runInAction(() => { + extensions.forEach((extension) => { + extensionsState.push(extension); + }); + }); + }; + + if (rendered) { + await addAndEnableExtensions(); + } else { + builder.beforeRender(addAndEnableExtensions); + } + + return builder; + }, + + allowKubeResource: (resourceName) => { + environment.onAllowKubeResource(); + + runInAction(() => { + allowedResourcesState.push(resourceName); + }); + + return builder; + }, + + beforeSetups(callback: (dis: DiContainers) => void) { + beforeSetupsCallbacks.push(callback); + + return builder; + }, + + beforeRender(callback: (dis: DiContainers) => void) { + beforeRenderCallbacks.push(callback); + + return builder; + }, + + async render() { + for (const callback of beforeSetupsCallbacks) { + await callback(dis); + } + + await runSetups(); + + const render = renderFor(rendererDi); + + const history = rendererDi.inject(observableHistoryInjectable); + + const currentRouteComponent = rendererDi.inject( + currentRouteComponentInjectable, + ); + + for (const callback of beforeRenderCallbacks) { + await callback(dis); + } + + rendered = render( + + {environment.renderSidebar()} + + + {() => { + const Component = currentRouteComponent.get(); + + if (!Component) { + return null; + } + + return ; + }} + + , + ); + + return rendered; + }, + }; + + return builder; +}; + +const toFlatChildren = + (parentId: string) => + ({ + submenu = [], + ...menuItem + }: MenuItemOpts): (MenuItemOpts & { path: string })[] => + [ + { + ...menuItem, + path: pipeline([parentId, menuItem.id], compact, join(".")), + }, + ...submenu.flatMap(toFlatChildren(menuItem.id)), + ]; diff --git a/src/renderer/components/test-utils/get-renderer-extension-fake.ts b/src/renderer/components/test-utils/get-renderer-extension-fake.ts new file mode 100644 index 0000000000..7f007b3bc3 --- /dev/null +++ b/src/renderer/components/test-utils/get-renderer-extension-fake.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; + +export class TestExtension extends LensRendererExtension {} + +export const getRendererExtensionFake = ({ id, ...rest }: Partial) => { + const instance = new TestExtension({ + id, + absolutePath: "irrelevant", + isBundled: false, + isCompatible: false, + isEnabled: false, + manifest: { name: id, version: "some-version" }, + manifestPath: "irrelevant", + }); + + Object.assign(instance, rest); + + return instance; +}; diff --git a/src/renderer/components/tooltip/withTooltip.tsx b/src/renderer/components/tooltip/withTooltip.tsx index cf75517160..adf3029e62 100644 --- a/src/renderer/components/tooltip/withTooltip.tsx +++ b/src/renderer/components/tooltip/withTooltip.tsx @@ -24,6 +24,7 @@ export function withTooltip>(Target: T): T { const DecoratedComponent = class extends React.Component & TooltipDecoratorProps> { static displayName = `withTooltip(${Target.displayName || Target.name})`; + // TODO: Remove side-effect to allow deterministic unit testing protected tooltipId = uniqueId("tooltip_target_"); render() { diff --git a/src/renderer/frames/cluster-frame/cluster-frame.tsx b/src/renderer/frames/cluster-frame/cluster-frame.tsx index 9392b9f4c8..e2fe33bacc 100755 --- a/src/renderer/frames/cluster-frame/cluster-frame.tsx +++ b/src/renderer/frames/cluster-frame/cluster-frame.tsx @@ -3,23 +3,15 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import React from "react"; -import { makeObservable, computed } from "mobx"; +import type { IComputedValue } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; -import { Redirect, Route, Router, Switch } from "react-router"; -import { UserManagementRoute } from "../../components/+user-management/route"; +import { Redirect } from "react-router"; 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 { 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"; import { ReplicaSetScaleDialog } from "../../components/+workloads-replicasets/replicaset-scale-dialog"; import { CommandContainer } from "../../components/command-palette/command-container"; -import * as routes from "../../../common/routes"; -import { TabLayout, TabLayoutRoute } from "../../components/layout/tab-layout"; import { ErrorBoundary } from "../../components/error-boundary"; import { MainLayout } from "../../components/layout/main-layout"; import { Notifications } from "../../components/notifications"; @@ -27,37 +19,24 @@ 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 { 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"; import type { NamespaceStore } from "../../components/+namespaces/namespace-store/namespace.store"; 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.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"; +import currentRouteComponentInjectable from "../../routes/current-route-component.injectable"; +import startUrlInjectable from "./start-url.injectable"; interface Dependencies { - history: History; namespaceStore: NamespaceStore; - hostedClusterId: ClusterId; subscribeStores: (stores: KubeObjectStore[]) => Disposer; - isAllowedResource: IsAllowedResource; + currentRouteComponent: IComputedValue; + startUrl: IComputedValue; } @observer @@ -66,7 +45,6 @@ class NonInjectedClusterFrame extends React.Component { constructor(props: Dependencies) { super(props); - makeObservable(this); } componentDidMount() { @@ -78,121 +56,40 @@ class NonInjectedClusterFrame extends React.Component { ]); } - @computed get startUrl() { - const resources : KubeResource[] = ["events", "nodes", "pods"]; + render() { + const Component = this.props.currentRouteComponent.get(); - return resources.every(x => this.props.isAllowedResource(x)) - ? routes.clusterURL() - : routes.workloadsURL(); - } - - getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) { - const routes: TabLayoutRoute[] = []; - - if (!menuItem.id) { - return routes; + if (!Component) { + return ; } - ClusterPageMenuRegistry.getInstance().getSubItems(menuItem).forEach((subMenu) => { - const page = ClusterPageRegistry.getInstance().getByPageTarget(subMenu.target); - - if (page) { - routes.push({ - routePath: page.url, - url: getExtensionPageUrl(subMenu.target), - title: subMenu.title, - component: page.components.Page, - }); - } - }); - - return routes; - } - - renderExtensionTabLayoutRoutes() { - return ClusterPageMenuRegistry.getInstance().getRootItems().map((menu, index) => { - const tabRoutes = this.getTabLayoutRoutes(menu); - - if (tabRoutes.length > 0) { - const pageComponent = () => ; - - return tab.routePath)}/>; - } else { - const page = ClusterPageRegistry.getInstance().getByPageTarget(menu.target); - - if (page) { - return ; - } - } - - return null; - }); - } - - renderExtensionRoutes() { - return ClusterPageRegistry.getInstance().getItems().map((page, index) => { - const menu = ClusterPageMenuRegistry.getInstance().getByPage(page); - - if (!menu) { - return ; - } - - return null; - }); - } - - render() { return ( - - - } footer={}> - - - - - - - - - - - - - {this.renderExtensionTabLayoutRoutes()} - {this.renderExtensionRoutes()} - + + } footer={}> + + - { - Notifications.error(`Unknown location ${location.pathname}, redirecting to main page.`); - - return ; - }} /> - - - - - - - - - - - - - - - - + + + + + + + + + + + + ); } } export const ClusterFrame = withInjectables(NonInjectedClusterFrame, { getProps: di => ({ - history: di.inject(historyInjectable), namespaceStore: di.inject(namespaceStoreInjectable), - hostedClusterId: di.inject(hostedClusterInjectable).id, subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, - isAllowedResource: di.inject(isAllowedResourceInjectable), + startUrl: di.inject(startUrlInjectable), + currentRouteComponent: di.inject(currentRouteComponentInjectable), }), }); diff --git a/src/renderer/frames/cluster-frame/start-url.injectable.ts b/src/renderer/frames/cluster-frame/start-url.injectable.ts new file mode 100644 index 0000000000..cd88250174 --- /dev/null +++ b/src/renderer/frames/cluster-frame/start-url.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 } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import type { KubeResource } from "../../../common/rbac"; +import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable"; +import clusterOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/overview/cluster-overview-route.injectable"; +import workloadsOverviewRouteInjectable from "../../../common/front-end-routing/routes/cluster/workloads/overview/workloads-overview-route.injectable"; +import { buildURL } from "../../../common/utils/buildUrl"; + +const startUrlInjectable = getInjectable({ + id: "start-url", + + instantiate: (di) => { + const isAllowedResource = (resourceName: any) => di.inject(isAllowedResourceInjectable, resourceName); + + const clusterOverviewRoute = di.inject(clusterOverviewRouteInjectable); + const workloadOverviewRoute = di.inject(workloadsOverviewRouteInjectable); + const clusterOverviewUrl = buildURL(clusterOverviewRoute.path); + const workloadOverviewUrl = buildURL(workloadOverviewRoute.path); + + return computed(() => { + const resources: KubeResource[] = ["events", "nodes", "pods"]; + + return resources.every((resourceName) => isAllowedResource(resourceName)) + ? clusterOverviewUrl + : workloadOverviewUrl; + }); + }, +}); + +export default startUrlInjectable; diff --git a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts index 4d78260c19..03ab72ddb6 100644 --- a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts +++ b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts @@ -9,6 +9,7 @@ import ipcRendererInjectable from "../../../app-paths/get-value-from-registered- import bindProtocolAddRouteHandlersInjectable from "../../../protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable"; import lensProtocolRouterRendererInjectable from "../../../protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable"; import catalogEntityRegistryInjectable from "../../../api/catalog-entity-registry/catalog-entity-registry.injectable"; +import registerIpcListenersInjectable from "../../../ipc/register-ipc-listeners.injectable"; const initRootFrameInjectable = getInjectable({ id: "init-root-frame", @@ -19,6 +20,8 @@ const initRootFrameInjectable = getInjectable({ return initRootFrame({ loadExtensions: extensionLoader.loadOnClusterManagerRenderer, + registerIpcListeners: di.inject(registerIpcListenersInjectable), + ipcRenderer: di.inject(ipcRendererInjectable), bindProtocolAddRouteHandlers: di.inject( diff --git a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts index 511c0baab0..15d4ec1be7 100644 --- a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts +++ b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts @@ -4,16 +4,18 @@ */ import { delay } from "../../../../common/utils"; import { broadcastMessage } from "../../../../common/ipc"; -import { registerIpcListeners } from "../../../ipc/register-listeners"; import logger from "../../../../common/logger"; import { unmountComponentAtNode } from "react-dom"; import type { ExtensionLoading } from "../../../../extensions/extension-loader"; import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry"; import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling"; + interface Dependencies { loadExtensions: () => Promise; + registerIpcListeners: () => void; + // TODO: Move usages of third party library behind abstraction ipcRenderer: { send: (name: string) => void }; @@ -31,6 +33,7 @@ export const initRootFrame = bindProtocolAddRouteHandlers, lensProtocolRouterRenderer, ipcRenderer, + registerIpcListeners, catalogEntityRegistry, }: Dependencies) => diff --git a/src/renderer/frames/root-frame/root-frame.tsx b/src/renderer/frames/root-frame/root-frame.tsx index 2dce9a61b0..53c4a0ec62 100644 --- a/src/renderer/frames/root-frame/root-frame.tsx +++ b/src/renderer/frames/root-frame/root-frame.tsx @@ -5,7 +5,6 @@ import { injectSystemCAs } from "../../../common/system-ca"; import React from "react"; -import { Route, Router, Switch } from "react-router"; import { observer } from "mobx-react"; import { ClusterManager } from "../../components/cluster-manager"; import { ErrorBoundary } from "../../components/error-boundary"; @@ -15,21 +14,14 @@ import { CommandContainer } from "../../components/command-palette/command-conta import { ipcRenderer } from "electron"; import { IpcRendererNavigationEvents } from "../../navigation/events"; import { ClusterFrameHandler } from "../../components/cluster-manager/lens-views"; -import historyInjectable from "../../navigation/history.injectable"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import type { History } from "history"; injectSystemCAs(); -interface Dependencies { - history: History; -} - @observer -class NonInjectedRootFrame extends React.Component { +export class RootFrame extends React.Component { static displayName = "RootFrame"; - constructor(props: Dependencies) { + constructor(props: any) { super(props); ClusterFrameHandler.createInstance(); @@ -41,20 +33,14 @@ class NonInjectedRootFrame extends React.Component { render() { return ( - + <> - - - + - + ); } } - -export const RootFrame = withInjectables(NonInjectedRootFrame, { - getProps: (di) => ({ history: di.inject(historyInjectable) }), -}); diff --git a/src/renderer/getDi.tsx b/src/renderer/getDi.tsx index 6c3a9a82d7..8735eabc6d 100644 --- a/src/renderer/getDi.tsx +++ b/src/renderer/getDi.tsx @@ -4,7 +4,7 @@ */ import { createContainer } from "@ogre-tools/injectable"; -import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; export const getDi = () => { const di = createContainer( @@ -13,7 +13,7 @@ export const getDi = () => { getRequireContextForCommonCode, ); - setLegacyGlobalDiForExtensionApi(di); + setLegacyGlobalDiForExtensionApi(di, Environments.renderer); return di; }; diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index bc9821d4df..6a752dfd85 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -4,19 +4,42 @@ */ import glob from "glob"; -import { memoize } from "lodash/fp"; +import { memoize, noop } from "lodash/fp"; import { createContainer } from "@ogre-tools/injectable"; -import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import getValueFromRegisteredChannelInjectable from "./app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable"; -import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; -import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; -import readDirInjectable from "../common/fs/read-dir.injectable"; -import readFileInjectable from "../common/fs/read-file.injectable"; +import loggerInjectable from "../common/logger.injectable"; +import { overrideFsWithFakes } from "../test-utils/override-fs-with-fakes"; +import observableHistoryInjectable from "./navigation/observable-history.injectable"; +import { searchParamsOptions } from "./navigation"; +import { createMemoryHistory } from "history"; +import { createObservableHistory } from "mobx-observable-history"; +import registerIpcChannelListenerInjectable from "./app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; +import focusWindowInjectable from "./ipc-channel-listeners/focus-window.injectable"; +import extensionsStoreInjectable from "../extensions/extensions-store/extensions-store.injectable"; +import type { ExtensionsStore } from "../extensions/extensions-store/extensions-store"; +import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable"; +import type { FileSystemProvisionerStore } from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store"; +import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; +import type { ClusterStore } from "../common/cluster-store/cluster-store"; +import type { Cluster } from "../common/cluster/cluster"; +import userStoreInjectable from "../common/user-store/user-store.injectable"; +import type { UserStore } from "../common/user-store"; +import isMacInjectable from "../common/vars/is-mac.injectable"; +import isWindowsInjectable from "../common/vars/is-windows.injectable"; +import isLinuxInjectable from "../common/vars/is-linux.injectable"; +import getAbsolutePathInjectable from "../common/path/get-absolute-path.injectable"; +import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake"; +import joinPathsInjectable from "../common/path/join-paths.injectable"; +import { joinPathsFake } from "../common/test-utils/join-paths-fake"; +import hotbarStoreInjectable from "../common/hotbar-store.injectable"; -export const getDiForUnitTesting = ({ doGeneralOverrides } = { doGeneralOverrides: false }) => { +export const getDiForUnitTesting = ( + { doGeneralOverrides } = { doGeneralOverrides: false }, +) => { const di = createContainer(); - setLegacyGlobalDiForExtensionApi(di); + setLegacyGlobalDiForExtensionApi(di, Environments.renderer); for (const filePath of getInjectableFilePaths()) { const injectableInstance = require(filePath).default; @@ -30,23 +53,45 @@ export const getDiForUnitTesting = ({ doGeneralOverrides } = { doGeneralOverride di.preventSideEffects(); if (doGeneralOverrides) { + di.override(isMacInjectable, () => true); + di.override(isWindowsInjectable, () => false); + di.override(isLinuxInjectable, () => false); + + di.override(getAbsolutePathInjectable, () => getAbsolutePathFake); + di.override(joinPathsInjectable, () => joinPathsFake); + + // eslint-disable-next-line unused-imports/no-unused-vars-ts + di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore); + + di.override(hotbarStoreInjectable, () => ({})); + + di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore); + + // eslint-disable-next-line unused-imports/no-unused-vars-ts + di.override(clusterStoreInjectable, () => ({ getById: (id): Cluster => ({}) as Cluster }) as ClusterStore); + di.override(userStoreInjectable, () => ({}) as UserStore); + di.override(getValueFromRegisteredChannelInjectable, () => () => undefined); + di.override(registerIpcChannelListenerInjectable, () => () => undefined); - di.override(readDirInjectable, () => () => { - throw new Error("Tried to read contents of a directory from file system without specifying explicit override."); + overrideFsWithFakes(di); + + di.override(observableHistoryInjectable, () => { + const historyFake = createMemoryHistory(); + + return createObservableHistory(historyFake, { + searchParams: searchParamsOptions, + }); }); - di.override(readFileInjectable, () => () => { - throw new Error("Tried to read a file from file system without specifying explicit override."); - }); + di.override(focusWindowInjectable, () => () => {}); - di.override(writeJsonFileInjectable, () => () => { - throw new Error("Tried to write JSON file to file system without specifying explicit override."); - }); - - di.override(readJsonFileInjectable, () => () => { - throw new Error("Tried to read JSON file from file system without specifying explicit override."); - }); + di.override(loggerInjectable, () => ({ + warn: noop, + debug: noop, + error: (message: string, ...args: any) => console.error(message, ...args), + info: noop, + })); } return di; diff --git a/src/renderer/initializers/add-sync-entries.injectable.tsx b/src/renderer/initializers/add-sync-entries.injectable.tsx new file mode 100644 index 0000000000..418759bfe7 --- /dev/null +++ b/src/renderer/initializers/add-sync-entries.injectable.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 { getAllEntries } from "../components/+preferences/kubeconfig-syncs"; +import { Notifications } from "../components/notifications"; +import { getInjectable } from "@ogre-tools/injectable"; +import userStoreInjectable from "../../common/user-store/user-store.injectable"; +import React from "react"; +import navigateToKubernetesPreferencesInjectable from "../../common/front-end-routing/routes/preferences/kubernetes/navigate-to-kubernetes-preferences.injectable"; +import loggerInjectable from "../../common/logger.injectable"; + +const addSyncEntriesInjectable = getInjectable({ + id: "add-sync-entries", + + instantiate: (di) => { + const userStore = di.inject(userStoreInjectable); + const navigateToKubernetesPreferences = di.inject(navigateToKubernetesPreferencesInjectable); + const logger = di.inject(loggerInjectable); + + return async (filePaths: string[]) => { + userStore.syncKubeconfigEntries.merge( + await getAllEntries(filePaths, logger), + ); + + Notifications.ok( +
+

Selected items has been added to Kubeconfig Sync.


+

Check the Preferences{" "} + to see full list.

+
, + ); + }; + }, +}); + +export default addSyncEntriesInjectable; + diff --git a/src/renderer/initializers/catalog-category-registry.tsx b/src/renderer/initializers/catalog-category-registry.tsx index fc41f31f36..bef9fd240e 100644 --- a/src/renderer/initializers/catalog-category-registry.tsx +++ b/src/renderer/initializers/catalog-category-registry.tsx @@ -3,35 +3,22 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import React from "react"; import { kubernetesClusterCategory } from "../../common/catalog-entities"; -import { addClusterURL, kubernetesURL } from "../../common/routes"; -import { UserStore } from "../../common/user-store"; -import { getAllEntries } from "../components/+preferences/kubeconfig-syncs"; import { isLinux, isWindows } from "../../common/vars"; import { PathPicker } from "../components/path-picker"; -import { Notifications } from "../components/notifications"; -import { Link } from "react-router-dom"; -async function addSyncEntries(filePaths: string[]) { - UserStore.getInstance().syncKubeconfigEntries.merge(await getAllEntries(filePaths)); - - Notifications.ok( -
-

Selected items has been added to Kubeconfig Sync.


-

Check the Preferences{" "} - to see full list.

-
, - ); +interface Dependencies { + navigateToAddCluster: () => void; + addSyncEntries: (filePaths: string[]) => void; } -export function initCatalogCategoryRegistryEntries() { +export function initCatalogCategoryRegistryEntries({ navigateToAddCluster, addSyncEntries } : Dependencies) { kubernetesClusterCategory.on("catalogAddMenu", ctx => { ctx.menuItems.push( { icon: "text_snippet", title: "Add from kubeconfig", - onClick: () => ctx.navigate(addClusterURL()), + onClick: navigateToAddCluster, }, ); diff --git a/src/renderer/initializers/catalog.tsx b/src/renderer/initializers/catalog.tsx index e55614d132..f287aa8360 100644 --- a/src/renderer/initializers/catalog.tsx +++ b/src/renderer/initializers/catalog.tsx @@ -10,9 +10,9 @@ import { ClusterStore } from "../../common/cluster-store/cluster-store"; import { catalogCategoryRegistry } from "../api/catalog-category-registry"; import { WeblinkAddCommand } from "../components/catalog-entities/weblink-add-command"; import { loadConfigFromString } from "../../common/kube-helpers"; -import { DeleteClusterDialog } from "../components/delete-cluster-dialog"; +import type { DeleteClusterDialogModel } from "../components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model"; -async function onClusterDelete(clusterId: string) { +async function onClusterDelete(clusterId: string, deleteClusterDialogModel: DeleteClusterDialogModel) { const cluster = ClusterStore.getInstance().getById(clusterId); if (!cluster) { @@ -25,14 +25,15 @@ async function onClusterDelete(clusterId: string) { throw error; } - DeleteClusterDialog.open({ cluster, config }); + deleteClusterDialogModel.open({ cluster, config }); } interface Dependencies { openCommandDialog: (component: React.ReactElement) => void; + deleteClusterDialogModel: DeleteClusterDialogModel; } -export function initCatalog({ openCommandDialog }: Dependencies) { +export function initCatalog({ openCommandDialog, deleteClusterDialogModel }: Dependencies) { catalogCategoryRegistry .getForGroupKind("entity.k8slens.dev", "WebLink") .on("catalogAddMenu", ctx => { @@ -50,7 +51,7 @@ export function initCatalog({ openCommandDialog }: Dependencies) { context.menuItems.push({ title: "Remove", icon: "delete", - onClick: () => onClusterDelete(entity.getId()), + onClick: () => onClusterDelete(entity.getId(), deleteClusterDialogModel), }); } }); diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts index 7362bdfbd7..1caac13959 100644 --- a/src/renderer/initializers/registries.ts +++ b/src/renderer/initializers/registries.ts @@ -7,9 +7,6 @@ import * as registries from "../../extensions/registries"; export function initRegistries() { registries.CatalogEntityDetailRegistry.createInstance(); - registries.ClusterPageMenuRegistry.createInstance(); - registries.ClusterPageRegistry.createInstance(); - registries.EntitySettingRegistry.createInstance(); - registries.GlobalPageRegistry.createInstance(); registries.KubeObjectDetailRegistry.createInstance(); + registries.EntitySettingRegistry.createInstance(); } diff --git a/src/renderer/initializers/workload-events.tsx b/src/renderer/initializers/workload-events.tsx index be58a5e39f..734b5105f0 100644 --- a/src/renderer/initializers/workload-events.tsx +++ b/src/renderer/initializers/workload-events.tsx @@ -4,20 +4,20 @@ */ import { withInjectables } from "@ogre-tools/injectable-react"; +import type { IComputedValue } from "mobx"; 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; + workloadEventsAreAllowed: IComputedValue; } -const NonInjectedWorkloadEvents = observer(({ isAllowedResource }: Dependencies & WorkloadEventsProps) => { - if (!isAllowedResource("events")) { +const NonInjectedWorkloadEvents = observer(({ workloadEventsAreAllowed }: Dependencies & WorkloadEventsProps) => { + if (!workloadEventsAreAllowed.get()) { return null; } @@ -32,7 +32,7 @@ const NonInjectedWorkloadEvents = observer(({ isAllowedResource }: Dependencies export const WorkloadEvents = withInjectables(NonInjectedWorkloadEvents, { getProps: (di, props) => ({ - isAllowedResource: di.inject(isAllowedResourceInjectable), + workloadEventsAreAllowed: di.inject(isAllowedResourceInjectable, "events"), ...props, }), }); diff --git a/src/renderer/ipc-channel-listeners/focus-window.injectable.ts b/src/renderer/ipc-channel-listeners/focus-window.injectable.ts new file mode 100644 index 0000000000..a9ce2b9b5e --- /dev/null +++ b/src/renderer/ipc-channel-listeners/focus-window.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; + +const focusWindowInjectable = getInjectable({ + id: "focus-window", + instantiate: () => () => window.focus(), + causesSideEffects: true, +}); + +export default focusWindowInjectable; diff --git a/src/renderer/ipc-channel-listeners/ipc-channel-listener-injection-token.ts b/src/renderer/ipc-channel-listeners/ipc-channel-listener-injection-token.ts new file mode 100644 index 0000000000..235b90873c --- /dev/null +++ b/src/renderer/ipc-channel-listeners/ipc-channel-listener-injection-token.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { Channel } from "../../common/ipc-channel/channel"; + + +export interface IpcChannelListener { + channel: Channel; + handle: (value: any) => void; +} + +export const ipcChannelListenerInjectionToken = + getInjectionToken({ + id: "ipc-channel-listener-injection-token", + }); diff --git a/src/renderer/ipc-channel-listeners/navigation-listener.injectable.ts b/src/renderer/ipc-channel-listeners/navigation-listener.injectable.ts new file mode 100644 index 0000000000..36a1d02559 --- /dev/null +++ b/src/renderer/ipc-channel-listeners/navigation-listener.injectable.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { ipcChannelListenerInjectionToken } from "./ipc-channel-listener-injection-token"; +import { appNavigationIpcChannel, clusterFrameNavigationIpcChannel } from "../../common/front-end-routing/navigation-ipc-channel"; +import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable"; +import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; +import focusWindowInjectable from "./focus-window.injectable"; + +const navigationListenerInjectable = getInjectable({ + id: "navigation-listener", + + instantiate: (di) => { + const navigateToUrl = di.inject(navigateToUrlInjectionToken); + const currentlyInClusterFrame = di.inject(currentlyInClusterFrameInjectable); + const focusWindow = di.inject(focusWindowInjectable); + + return { + channel: currentlyInClusterFrame + ? clusterFrameNavigationIpcChannel + : appNavigationIpcChannel, + + handle: (url: string) => { + navigateToUrl(url); + + if (!currentlyInClusterFrame) { + focusWindow(); // make sure that the main frame is focused + } + }, + }; + }, + + injectionToken: ipcChannelListenerInjectionToken, +}); + +export default navigationListenerInjectable; diff --git a/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts b/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts new file mode 100644 index 0000000000..9ce36a787b --- /dev/null +++ b/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { noop } from "lodash/fp"; +import { ipcChannelListenerInjectionToken } from "./ipc-channel-listener-injection-token"; +import registerIpcChannelListenerInjectable + from "../app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; + +const registerIpcChannelListenersInjectable = getInjectable({ + id: "register-ipc-channel-listeners", + + setup: async di => { + const registerIpcChannelListener = await di.inject(registerIpcChannelListenerInjectable); + + const listeners = await di.injectMany(ipcChannelListenerInjectionToken); + + listeners.forEach(listener => { + registerIpcChannelListener(listener); + }); + }, + + instantiate: () => noop, +}); + +export default registerIpcChannelListenersInjectable; diff --git a/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx b/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx new file mode 100644 index 0000000000..43b0eb8701 --- /dev/null +++ b/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx @@ -0,0 +1,81 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import navigateToEntitySettingsInjectable from "../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; +import type { ListNamespaceForbiddenArgs } from "../../common/ipc/cluster"; +import { Notifications, notificationsStore } from "../components/notifications"; +import { ClusterStore } from "../../common/cluster-store/cluster-store"; +import { Button } from "../components/button"; +import type { IpcRendererEvent } from "electron"; +import React from "react"; + +const listNamespacesForbiddenHandlerInjectable = getInjectable({ + id: "list-namespaces-forbidden-handler", + + instantiate: (di) => { + const navigateToEntitySettings = di.inject(navigateToEntitySettingsInjectable); + + const notificationLastDisplayedAt = new Map(); + const intervalBetweenNotifications = 1000 * 60; // 60s + + return ( + event: IpcRendererEvent, + ...[clusterId]: ListNamespaceForbiddenArgs + ): void => { + const lastDisplayedAt = notificationLastDisplayedAt.get(clusterId); + const now = Date.now(); + + if ( + !notificationLastDisplayedAt.has(clusterId) || + now - lastDisplayedAt > intervalBetweenNotifications + ) { + notificationLastDisplayedAt.set(clusterId, now); + } else { + // don't bother the user too often + return; + } + + const notificationId = `list-namespaces-forbidden:${clusterId}`; + + if (notificationsStore.getById(notificationId)) { + // notification is still visible + return; + } + + Notifications.info( + ( +
+ Add Accessible Namespaces +

+ Cluster {ClusterStore.getInstance().getById(clusterId).name} does not have permissions to list namespaces.{" "} + Please add the namespaces you have access to. +

+
+
+
+ ), + { + id: notificationId, + /** + * Set the time when the notification is closed as well so that there is at + * least a minute between closing the notification as seeing it again + */ + onClose: () => notificationLastDisplayedAt.set(clusterId, Date.now()), + }, + ); + }; + }, +}); + +export default listNamespacesForbiddenHandlerInjectable; diff --git a/src/renderer/ipc/register-ipc-listeners.injectable.ts b/src/renderer/ipc/register-ipc-listeners.injectable.ts new file mode 100644 index 0000000000..a37f983769 --- /dev/null +++ b/src/renderer/ipc/register-ipc-listeners.injectable.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { registerIpcListeners } from "./register-listeners"; +import listNamespacesForbiddenHandlerInjectable from "./list-namespaces-forbidden-handler.injectable"; + +const registerIpcListenersInjectable = getInjectable({ + id: "register-ipc-listeners", + + instantiate: (di) => registerIpcListeners({ + listNamespacesForbiddenHandler: di.inject(listNamespacesForbiddenHandlerInjectable), + }), +}); + +export default registerIpcListenersInjectable; diff --git a/src/renderer/ipc/register-listeners.tsx b/src/renderer/ipc/register-listeners.tsx index 5a5cc509bf..6e71231224 100644 --- a/src/renderer/ipc/register-listeners.tsx +++ b/src/renderer/ipc/register-listeners.tsx @@ -9,9 +9,6 @@ import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, Upda import { Notifications, notificationsStore } from "../components/notifications"; import { Button } from "../components/button"; import { isMac } from "../../common/vars"; -import { ClusterStore } from "../../common/cluster-store/cluster-store"; -import { navigate } from "../navigation"; -import { entitySettingsURL } from "../../common/routes"; import { defaultHotbarCells } from "../../common/hotbar-types"; import { type ListNamespaceForbiddenArgs, clusterListNamespaceForbiddenChannel, isListNamespaceForbiddenArgs } from "../../common/ipc/cluster"; import { hotbarTooManyItemsChannel } from "../../common/ipc/hotbar"; @@ -63,64 +60,18 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update ); } -const notificationLastDisplayedAt = new Map(); -const intervalBetweenNotifications = 1000 * 60; // 60s - -function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: ListNamespaceForbiddenArgs): void { - const lastDisplayedAt = notificationLastDisplayedAt.get(clusterId); - const now = Date.now(); - - if (!notificationLastDisplayedAt.has(clusterId) || (now - lastDisplayedAt) > intervalBetweenNotifications) { - notificationLastDisplayedAt.set(clusterId, now); - } else { - // don't bother the user too often - return; - } - - const notificationId = `list-namespaces-forbidden:${clusterId}`; - - if (notificationsStore.getById(notificationId)) { - // notification is still visible - return; - } - - Notifications.info( - ( -
- Add Accessible Namespaces -

- Cluster {ClusterStore.getInstance().getById(clusterId).name} does not have permissions to list namespaces.{" "} - Please add the namespaces you have access to. -

-
-
-
- ), - { - id: notificationId, - /** - * Set the time when the notification is closed as well so that there is at - * least a minute between closing the notification as seeing it again - */ - onClose: () => notificationLastDisplayedAt.set(clusterId, Date.now()), - }, - ); -} - function HotbarTooManyItemsHandler(): void { Notifications.error(`Cannot have more than ${defaultHotbarCells} items pinned to a hotbar`); } -export function registerIpcListeners() { +interface Dependencies { + listNamespacesForbiddenHandler: ( + event: IpcRendererEvent, + ...[clusterId]: ListNamespaceForbiddenArgs + ) => void; +} + +export const registerIpcListeners = ({ listNamespacesForbiddenHandler }: Dependencies) => () => { onCorrect({ source: ipcRenderer, channel: UpdateAvailableChannel, @@ -130,7 +81,7 @@ export function registerIpcListeners() { onCorrect({ source: ipcRenderer, channel: clusterListNamespaceForbiddenChannel, - listener: ListNamespacesForbiddenHandler, + listener: listNamespacesForbiddenHandler, verifier: isListNamespaceForbiddenArgs, }); onCorrect({ @@ -145,4 +96,4 @@ export function registerIpcListeners() { ipcRendererOn(AutoUpdateNoUpdateAvailable, () => { Notifications.shortInfo("No update is currently available"); }); -} +}; diff --git a/src/renderer/kube-watch-api/subscribe-stores.injectable.ts b/src/renderer/kube-watch-api/subscribe-stores.injectable.ts index 81c38c387e..f34a3d21f4 100644 --- a/src/renderer/kube-watch-api/subscribe-stores.injectable.ts +++ b/src/renderer/kube-watch-api/subscribe-stores.injectable.ts @@ -7,6 +7,7 @@ import kubeWatchApiInjectable from "./kube-watch-api.injectable"; const subscribeStoresInjectable = getInjectable({ id: "subscribe-stores", + causesSideEffects: true, instantiate: (di) => di.inject(kubeWatchApiInjectable).subscribeStores, }); diff --git a/src/renderer/navigation/events.ts b/src/renderer/navigation/events.ts index c2ac5670b1..35021e1d72 100644 --- a/src/renderer/navigation/events.ts +++ b/src/renderer/navigation/events.ts @@ -5,9 +5,11 @@ import { ipcRenderer } from "electron"; import { reaction } from "mobx"; -import { getMatchedClusterId, navigate } from "./helpers"; import { broadcastMessage, ipcRendererOn } from "../../common/ipc"; -import logger from "../../main/logger"; +import { + getLegacyGlobalDiForExtensionApi, +} from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import matchedClusterIdInjectable from "./matched-cluster-id.injectable"; export const enum IpcRendererNavigationEvents { RELOAD_PAGE = "renderer:page-reload", @@ -24,8 +26,6 @@ export function bindEvents() { if (process.isMainFrame) { bindClusterManagerRouteEvents(); - } else { - bindClusterFrameRouteEvents(); } // Reload dashboard window @@ -36,25 +36,14 @@ export function bindEvents() { // Handle events only in main window renderer process (see also: cluster-manager.tsx) function bindClusterManagerRouteEvents() { + const di = getLegacyGlobalDiForExtensionApi(); + + const matchedClusterId = di.inject(matchedClusterIdInjectable); + // Keep track of active cluster-id for handling IPC/menus/etc. - reaction(() => getMatchedClusterId(), clusterId => { + reaction(() => matchedClusterId.get(), clusterId => { broadcastMessage(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, clusterId); }, { fireImmediately: true, }); - - // Handle navigation via IPC - ipcRendererOn(IpcRendererNavigationEvents.NAVIGATE_IN_APP, (event, url: string) => { - logger.info(`[IPC]: navigate to ${url}`, { currentLocation: location.href }); - navigate(url); - window.focus(); // make sure that the main frame is focused - }); -} - -// Handle cluster-view renderer process events within iframes -function bindClusterFrameRouteEvents() { - ipcRendererOn(IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER, (event, url: string) => { - logger.info(`[IPC]: navigate to ${url}`, { currentLocation: location.href }); - navigate(url); - }); } diff --git a/src/renderer/navigation/helpers.ts b/src/renderer/navigation/helpers.ts index 05fa983e2e..14fa76e503 100644 --- a/src/renderer/navigation/helpers.ts +++ b/src/renderer/navigation/helpers.ts @@ -7,7 +7,6 @@ import type { LocationDescriptor } from "history"; import { createPath } from "history"; import { matchPath, RouteProps } from "react-router"; import { navigation } from "./history"; -import { ClusterViewRouteParams, clusterViewRoute } from "../../common/routes"; import { PageParam, PageParamInit } from "./page-param"; export function navigate(location: LocationDescriptor) { @@ -22,10 +21,6 @@ export function navigate(location: LocationDescriptor) { } } -export function navigateWithoutHistoryChange(location: Partial) { - navigation.merge(location, true); -} - export function createPageParam(init: PageParamInit) { return new PageParam(init, navigation); } @@ -37,12 +32,3 @@ export function matchRoute

(route: string | string[] | RouteProps) { export function isActiveRoute(route: string | string[] | RouteProps): boolean { return !!matchRoute(route); } - -export function getMatchedClusterId(): string | undefined { - const matched = matchPath(navigation.location.pathname, { - exact: true, - path: clusterViewRoute.path, - }); - - return matched?.params.clusterId; -} diff --git a/src/renderer/navigation/index.ts b/src/renderer/navigation/index.ts index cdc18bbb69..f49f7672ea 100644 --- a/src/renderer/navigation/index.ts +++ b/src/renderer/navigation/index.ts @@ -5,10 +5,6 @@ // Navigation (renderer) -import { bindEvents } from "./events"; - export * from "./history"; export * from "./helpers"; export * from "./page-param"; - -bindEvents(); diff --git a/src/renderer/navigation/matched-cluster-id.injectable.ts b/src/renderer/navigation/matched-cluster-id.injectable.ts new file mode 100644 index 0000000000..2c2bb01f75 --- /dev/null +++ b/src/renderer/navigation/matched-cluster-id.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import clusterViewRouteParametersInjectable from "../components/cluster-manager/cluster-view-route-parameters.injectable"; + +const matchedClusterIdInjectable = getInjectable({ + id: "matched-cluster-id", + + instantiate: (di) => { + const routeParameters = di.inject(clusterViewRouteParametersInjectable); + + return routeParameters.clusterId; + }, +}); + +export default matchedClusterIdInjectable; diff --git a/src/renderer/navigation/observable-history.injectable.ts b/src/renderer/navigation/observable-history.injectable.ts index b4995c67c2..a3f2d42163 100644 --- a/src/renderer/navigation/observable-history.injectable.ts +++ b/src/renderer/navigation/observable-history.injectable.ts @@ -8,6 +8,7 @@ import { navigation as observableHistory } from "./history"; const observableHistoryInjectable = getInjectable({ id: "observable-history", + causesSideEffects: true, instantiate: () => observableHistory, }); diff --git a/src/renderer/navigation/page-param.ts b/src/renderer/navigation/page-param.ts index 280d12d55a..a5e6d4c6ed 100644 --- a/src/renderer/navigation/page-param.ts +++ b/src/renderer/navigation/page-param.ts @@ -10,7 +10,6 @@ import type { ObservableHistory } from "mobx-observable-history"; export interface PageParamInit { name: string; defaultValue?: V; // multi-values param must be defined with array-value, e.g. [] - prefix?: string; // name prefix, for extensions it's `${extension.id}:` parse?(value: string | string[]): V; // from URL stringify?(value: V): string | string[]; // to URL } @@ -22,9 +21,9 @@ export class PageParam { constructor(private init: PageParamInit, private history: ObservableHistory) { makeObservable(this); - const { prefix, name, defaultValue } = init; + const { name, defaultValue } = init; - this.name = `${prefix ?? ""}${name}`; // actual prefixed URL-name + this.name = name; this.isMulti = Array.isArray(defaultValue); // multi-values param } @@ -97,7 +96,7 @@ export class PageParam { this.history.searchParams.delete(this.name); } - toString({ withPrefix = true, mergeGlobals = true, value = this.get() } = {}): string { + toString({ mergeGlobals = true, value = this.get() } = {}): string { let searchParams = new URLSearchParams(); if (mergeGlobals) { @@ -109,6 +108,6 @@ export class PageParam { searchParams.append(this.name, value); }); - return `${withPrefix ? "?" : ""}${searchParams}`; + return searchParams.toString(); } } diff --git a/src/renderer/port-forward/about-port-forwarding.injectable.ts b/src/renderer/port-forward/about-port-forwarding.injectable.ts new file mode 100644 index 0000000000..505de71920 --- /dev/null +++ b/src/renderer/port-forward/about-port-forwarding.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { aboutPortForwarding } from "./port-forward-notify"; +import navigateToPortForwardsInjectable from "../../common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable"; + +const aboutPortForwardingInjectable = getInjectable({ + id: "about-port-forwarding", + + instantiate: (di) => + aboutPortForwarding({ + navigateToPortForwards: di.inject(navigateToPortForwardsInjectable), + }), +}); + +export default aboutPortForwardingInjectable; diff --git a/src/renderer/port-forward/notify-error-port-forwarding.injectable.ts b/src/renderer/port-forward/notify-error-port-forwarding.injectable.ts new file mode 100644 index 0000000000..68e98a1213 --- /dev/null +++ b/src/renderer/port-forward/notify-error-port-forwarding.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { notifyErrorPortForwarding } from "./port-forward-notify"; +import navigateToPortForwardsInjectable from "../../common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable"; + +const notifyErrorPortForwardingInjectable = getInjectable({ + id: "notify-error-port-forwarding", + + instantiate: (di) => + notifyErrorPortForwarding({ + navigateToPortForwards: di.inject(navigateToPortForwardsInjectable), + }), +}); + +export default notifyErrorPortForwardingInjectable; diff --git a/src/renderer/port-forward/port-forward-dialog.tsx b/src/renderer/port-forward/port-forward-dialog.tsx index 70c9910652..65ace376f4 100644 --- a/src/renderer/port-forward/port-forward-dialog.tsx +++ b/src/renderer/port-forward/port-forward-dialog.tsx @@ -14,19 +14,22 @@ import { Input } from "../components/input"; import { cssNames } from "../utils"; import type { PortForwardStore } from "./port-forward-store/port-forward-store"; import { openPortForward } from "./port-forward-utils"; -import { aboutPortForwarding, notifyErrorPortForwarding } from "./port-forward-notify"; import { Checkbox } from "../components/checkbox"; import { withInjectables } from "@ogre-tools/injectable-react"; import type { PortForwardDialogModel } from "./port-forward-dialog-model/port-forward-dialog-model"; import portForwardDialogModelInjectable from "./port-forward-dialog-model/port-forward-dialog-model.injectable"; import logger from "../../common/logger"; import portForwardStoreInjectable from "./port-forward-store/port-forward-store.injectable"; +import aboutPortForwardingInjectable from "./about-port-forwarding.injectable"; +import notifyErrorPortForwardingInjectable from "./notify-error-port-forwarding.injectable"; export interface PortForwardDialogProps extends Partial {} interface Dependencies { portForwardStore: PortForwardStore; model: PortForwardDialogModel; + aboutPortForwarding: () => void; + notifyErrorPortForwarding: (message: string) => void; } @observer @@ -68,18 +71,18 @@ class NonInjectedPortForwardDialog extends Component ({ portForwardStore: di.inject(portForwardStoreInjectable), model: di.inject(portForwardDialogModelInjectable), + aboutPortForwarding: di.inject(aboutPortForwardingInjectable), + notifyErrorPortForwarding: di.inject(notifyErrorPortForwardingInjectable), ...props, }), }, diff --git a/src/renderer/port-forward/port-forward-notify.tsx b/src/renderer/port-forward/port-forward-notify.tsx index 3b21817be1..004eedac25 100644 --- a/src/renderer/port-forward/port-forward-notify.tsx +++ b/src/renderer/port-forward/port-forward-notify.tsx @@ -4,14 +4,16 @@ */ import React from "react"; -import { portForwardsURL } from "../../common/routes/port-forwards"; import { Button } from "../components/button"; import { Notifications, notificationsStore } from "../components/notifications"; -import { navigate } from "../navigation"; import { getHostedClusterId } from "../utils"; +import type { NavigateToPortForwards } from "../../common/front-end-routing/routes/cluster/network/port-forwards/navigate-to-port-forwards.injectable"; +interface AboutPortForwardingDependencies { + navigateToPortForwards: NavigateToPortForwards; +} -export function aboutPortForwarding() { +export const aboutPortForwarding = ({ navigateToPortForwards }: AboutPortForwardingDependencies) => () => { const notificationId = `port-forward-notification-${getHostedClusterId()}`; Notifications.info( @@ -27,7 +29,7 @@ export function aboutPortForwarding() { outlined label="Go to Port Forwarding" onClick={() => { - navigate(portForwardsURL()); + navigateToPortForwards(); notificationsStore.remove(notificationId); }} /> @@ -39,9 +41,14 @@ export function aboutPortForwarding() { timeout: 10_000, }, ); +}; + +interface NotifyErrorPortForwardingDependencies { + navigateToPortForwards: NavigateToPortForwards; } -export function notifyErrorPortForwarding(msg: string) { + +export const notifyErrorPortForwarding = ({ navigateToPortForwards }: NotifyErrorPortForwardingDependencies) => (msg: string) => { const notificationId = `port-forward-error-notification-${getHostedClusterId()}`; Notifications.error( @@ -57,7 +64,7 @@ export function notifyErrorPortForwarding(msg: string) { outlined label="Check Port Forwarding" onClick={() => { - navigate(portForwardsURL()); + navigateToPortForwards(); notificationsStore.remove(notificationId); }} /> @@ -69,5 +76,5 @@ export function notifyErrorPortForwarding(msg: string) { timeout: 10_000, }, ); -} +}; diff --git a/src/renderer/port-forward/port-forward-store/port-forward-store.injectable.ts b/src/renderer/port-forward/port-forward-store/port-forward-store.injectable.ts index 0f90c2d705..4e834d0650 100644 --- a/src/renderer/port-forward/port-forward-store/port-forward-store.injectable.ts +++ b/src/renderer/port-forward/port-forward-store/port-forward-store.injectable.ts @@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import { PortForwardStore } from "./port-forward-store"; import type { ForwardedPort } from "../port-forward-item"; import createStorageInjectable from "../../utils/create-storage/create-storage.injectable"; +import notifyErrorPortForwardingInjectable from "../notify-error-port-forwarding.injectable"; const portForwardStoreInjectable = getInjectable({ id: "port-forward-store", @@ -18,7 +19,10 @@ const portForwardStoreInjectable = getInjectable({ undefined, ); - return new PortForwardStore({ storage }); + return new PortForwardStore({ + storage, + notifyErrorPortForwarding: di.inject(notifyErrorPortForwardingInjectable), + }); }, }); 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 9ac59b9814..d3a4f833a3 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 @@ -7,13 +7,13 @@ import { action, makeObservable, observable, reaction } from "mobx"; import { ItemStore } from "../../../common/item.store"; import { autoBind, disposer, StorageHelper } from "../../utils"; import { ForwardedPort, PortForwardItem } from "../port-forward-item"; -import { notifyErrorPortForwarding } from "../port-forward-notify"; import { apiBase } from "../../api"; import { waitUntilFree } from "tcp-port-used"; import logger from "../../../common/logger"; interface Dependencies { storage: StorageHelper; + notifyErrorPortForwarding: (message: string) => void; } export class PortForwardStore extends ItemStore { @@ -43,7 +43,7 @@ export class PortForwardStore extends ItemStore { for (const result of results) { if (result.status === "rejected" || result.value.status === "Disabled") { - notifyErrorPortForwarding("One or more port-forwards could not be started"); + this.dependencies.notifyErrorPortForwarding("One or more port-forwards could not be started"); return; } diff --git a/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable.ts b/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable.ts index 26d7968044..575dc4ec82 100644 --- a/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable.ts +++ b/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable.ts @@ -6,6 +6,12 @@ import { getInjectable } from "@ogre-tools/injectable"; import attemptInstallByInfoInjectable from "../../components/+extensions/attempt-install-by-info/attempt-install-by-info.injectable"; import { bindProtocolAddRouteHandlers } from "./bind-protocol-add-route-handlers"; import lensProtocolRouterRendererInjectable from "../lens-protocol-router-renderer/lens-protocol-router-renderer.injectable"; +import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; +import navigateToAddClusterInjectable from "../../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable"; +import navigateToExtensionsInjectable from "../../../common/front-end-routing/routes/extensions/navigate-to-extensions.injectable"; +import navigateToEntitySettingsInjectable from "../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; +import navigateToClusterViewInjectable from "../../../common/front-end-routing/routes/cluster-view/navigate-to-cluster-view.injectable"; +import navigateToPreferenceTabIdInjectable from "./navigate-to-preference-tab-id.injectable"; const bindProtocolAddRouteHandlersInjectable = getInjectable({ id: "bind-protocol-add-route-handlers", @@ -13,9 +19,17 @@ const bindProtocolAddRouteHandlersInjectable = getInjectable({ instantiate: (di) => bindProtocolAddRouteHandlers({ attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable), + lensProtocolRouterRenderer: di.inject( lensProtocolRouterRendererInjectable, ), + + navigateToCatalog: di.inject(navigateToCatalogInjectable), + navigateToAddCluster: di.inject(navigateToAddClusterInjectable), + navigateToExtensions: di.inject(navigateToExtensionsInjectable), + navigateToEntitySettings: di.inject(navigateToEntitySettingsInjectable), + navigateToClusterView: di.inject(navigateToClusterViewInjectable), + navigateToPreferenceTabId: di.inject(navigateToPreferenceTabIdInjectable), }), }); diff --git a/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.tsx b/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.tsx index 2e69506c20..69954b8e70 100644 --- a/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.tsx +++ b/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.tsx @@ -5,7 +5,6 @@ import React from "react"; import type { LensProtocolRouterRenderer } from "../lens-protocol-router-renderer/lens-protocol-router-renderer"; -import { navigate } from "../../navigation/helpers"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import { @@ -14,20 +13,28 @@ import { LensProtocolRouter, } from "../../../common/protocol-handler"; import { Notifications } from "../../components/notifications"; -import * as routes from "../../../common/routes"; import type { ExtensionInfo } from "../../components/+extensions/attempt-install-by-info/attempt-install-by-info"; +import type { NavigateToCatalog } from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; +import type { NavigateToEntitySettings } from "../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; +import type { NavigateToClusterView } from "../../../common/front-end-routing/routes/cluster-view/navigate-to-cluster-view.injectable"; interface Dependencies { attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise; lensProtocolRouterRenderer: LensProtocolRouterRenderer; + navigateToCatalog: NavigateToCatalog; + navigateToAddCluster: () => void; + navigateToExtensions: () => void; + navigateToEntitySettings: NavigateToEntitySettings; + navigateToClusterView: NavigateToClusterView; + navigateToPreferenceTabId: (tabId: string) => void; } export const bindProtocolAddRouteHandlers = - ({ attemptInstallByInfo, lensProtocolRouterRenderer }: Dependencies) => + ({ attemptInstallByInfo, lensProtocolRouterRenderer, navigateToCatalog, navigateToAddCluster, navigateToExtensions, navigateToEntitySettings, navigateToClusterView, navigateToPreferenceTabId }: Dependencies) => () => { lensProtocolRouterRenderer - .addInternalHandler("/preferences", ({ search: { highlight }}) => { - navigate(routes.preferencesURL({ fragment: highlight })); + .addInternalHandler("/preferences", ({ search: { highlight: tabId }}) => { + navigateToPreferenceTabId(tabId); }) .addInternalHandler("/", ({ tail }) => { if (tail) { @@ -39,26 +46,19 @@ export const bindProtocolAddRouteHandlers = ); } - navigate(routes.catalogURL()); + navigateToCatalog(); }) .addInternalHandler("/landing", () => { - navigate(routes.catalogURL()); + navigateToCatalog(); }) .addInternalHandler( "/landing/view/:group/:kind", ({ pathname: { group, kind }}) => { - navigate( - routes.catalogURL({ - params: { - group, - kind, - }, - }), - ); + navigateToCatalog({ group, kind }); }, ) .addInternalHandler("/cluster", () => { - navigate(routes.addClusterURL()); + navigateToAddCluster(); }) .addInternalHandler( "/entity/:entityId/settings", @@ -66,7 +66,7 @@ export const bindProtocolAddRouteHandlers = const entity = catalogEntityRegistry.getById(entityId); if (entity) { - navigate(routes.entitySettingsURL({ params: { entityId }})); + navigateToEntitySettings(entityId); } else { Notifications.shortInfo(

@@ -83,7 +83,7 @@ export const bindProtocolAddRouteHandlers = const cluster = ClusterStore.getInstance().getById(clusterId); if (cluster) { - navigate(routes.clusterViewURL({ params: { clusterId }})); + navigateToClusterView(clusterId); } else { Notifications.shortInfo(

@@ -99,9 +99,7 @@ export const bindProtocolAddRouteHandlers = const cluster = ClusterStore.getInstance().getById(clusterId); if (cluster) { - navigate( - routes.entitySettingsURL({ params: { entityId: clusterId }}), - ); + navigateToEntitySettings(clusterId); } else { Notifications.shortInfo(

@@ -112,7 +110,7 @@ export const bindProtocolAddRouteHandlers = }, ) .addInternalHandler("/extensions", () => { - navigate(routes.extensionsURL()); + navigateToExtensions(); }) .addInternalHandler( `/extensions/install${LensProtocolRouter.ExtensionUrlSchema}`, @@ -124,7 +122,7 @@ export const bindProtocolAddRouteHandlers = .filter(Boolean) .join("/"); - navigate(routes.extensionsURL()); + navigateToExtensions(); attemptInstallByInfo({ name, version, requireConfirmation: true }); }, ); diff --git a/src/renderer/protocol-handler/bind-protocol-add-route-handlers/navigate-to-preference-tab-id.injectable.ts b/src/renderer/protocol-handler/bind-protocol-add-route-handlers/navigate-to-preference-tab-id.injectable.ts new file mode 100644 index 0000000000..5c6aa2177e --- /dev/null +++ b/src/renderer/protocol-handler/bind-protocol-add-route-handlers/navigate-to-preference-tab-id.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import preferenceNavigationItemsInjectable from "../../components/+preferences/preferences-navigation/preference-navigation-items.injectable"; +import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; + +const navigateToPreferenceTabIdInjectable = getInjectable({ + id: "navigate-to-preference-tab-id", + + instantiate: (di) => { + const preferenceNavigationItems = di.inject(preferenceNavigationItemsInjectable); + const navigateToPreferences = di.inject(navigateToPreferencesInjectable); + + return (tabId: string) => { + const targetTab = preferenceNavigationItems.get().find(item => item.id === tabId); + + if (targetTab) { + targetTab.navigate(); + + return; + } + + navigateToPreferences(); + }; + }, +}); + +export default navigateToPreferenceTabIdInjectable; diff --git a/src/renderer/routes/all-routes.injectable.ts b/src/renderer/routes/all-routes.injectable.ts new file mode 100644 index 0000000000..75a69bb31c --- /dev/null +++ b/src/renderer/routes/all-routes.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { overSome } from "lodash/fp"; +import { computed } from "mobx"; +import rendererExtensionsInjectable from "../../extensions/renderer-extensions.injectable"; +import type { LensRendererExtension } from "../../extensions/lens-renderer-extension"; +import { Route, routeInjectionToken } from "../../common/front-end-routing/route-injection-token"; + +const allRoutesInjectable = getInjectable({ + id: "all-routes", + + instantiate: (di) => { + const extensions = di.inject(rendererExtensionsInjectable); + + return computed(() => { + const enabledExtensions = extensions.get(); + + return di + .injectMany(routeInjectionToken) + .filter((route) => + overSome([ + isNonExtensionRoute, + isEnabledExtensionRouteFor(enabledExtensions), + ])(route), + ); + }); + }, +}); + +const isNonExtensionRoute = (route: Route) => !route.extension; + +const isEnabledExtensionRouteFor = + (enabledExtensions: LensRendererExtension[]) => (route: Route) => + !!enabledExtensions.find((x) => x === route.extension); + +export default allRoutesInjectable; diff --git a/src/renderer/routes/current-path-parameters.injectable.ts b/src/renderer/routes/current-path-parameters.injectable.ts new file mode 100644 index 0000000000..8aceb17036 --- /dev/null +++ b/src/renderer/routes/current-path-parameters.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import matchingRouteInjectable from "./matching-route.injectable"; + +const currentPathParametersInjectable = getInjectable({ + id: "current-path-parameters", + + instantiate: (di) => { + const matchingRoute = di.inject(matchingRouteInjectable); + + return computed((): Record => { + const match = matchingRoute.get(); + + return match ? match.pathParameters: {}; + }); + }, +}); + +export default currentPathParametersInjectable; diff --git a/src/renderer/routes/current-path.injectable.ts b/src/renderer/routes/current-path.injectable.ts new file mode 100644 index 0000000000..4ca6d64e8f --- /dev/null +++ b/src/renderer/routes/current-path.injectable.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import observableHistoryInjectable from "../navigation/observable-history.injectable"; + +const currentPathInjectable = getInjectable({ + id: "current-path", + + instantiate: (di) => { + const observableHistory = di.inject(observableHistoryInjectable); + + return computed(() => observableHistory.location.pathname); + }, +}); + +export default currentPathInjectable; diff --git a/src/renderer/routes/current-route-component.injectable.ts b/src/renderer/routes/current-route-component.injectable.ts new file mode 100644 index 0000000000..2421ba97c6 --- /dev/null +++ b/src/renderer/routes/current-route-component.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { matches } from "lodash/fp"; +import { computed } from "mobx"; +import currentRouteInjectable from "./current-route.injectable"; +import { routeSpecificComponentInjectionToken } from "./route-specific-component-injection-token"; + +const currentRouteComponentInjectable = getInjectable({ + id: "current-route-component", + + instantiate: (di) => { + const route = di.inject(currentRouteInjectable); + + return computed(() => { + const currentRoute = route.get(); + + if (!currentRoute) { + return null; + } + + const routeSpecificComponent = di + .injectMany(routeSpecificComponentInjectionToken) + .find(matches({ route: currentRoute })); + + return routeSpecificComponent.Component; + }); + }, +}); + +export default currentRouteComponentInjectable; diff --git a/src/renderer/routes/current-route.injectable.ts b/src/renderer/routes/current-route.injectable.ts new file mode 100644 index 0000000000..fad42b6c36 --- /dev/null +++ b/src/renderer/routes/current-route.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import matchingRouteInjectable from "./matching-route.injectable"; + +const currentRouteInjectable = getInjectable({ + id: "current-route", + + instantiate: (di) => { + const matchingRoute = di.inject(matchingRouteInjectable); + + return computed(() => { + const match = matchingRoute.get(); + + if (match && match.route.isEnabled.get()) { + return match.route; + } + + return null; + }); + }, +}); + +export default currentRouteInjectable; diff --git a/src/renderer/routes/currently-in-cluster-frame.injectable.ts b/src/renderer/routes/currently-in-cluster-frame.injectable.ts new file mode 100644 index 0000000000..b1d901fb91 --- /dev/null +++ b/src/renderer/routes/currently-in-cluster-frame.injectable.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; + +const currentlyInClusterFrameInjectable = getInjectable({ + id: "currently-in-cluster-frame", + instantiate: () => !process.isMainFrame, +}); + +export default currentlyInClusterFrameInjectable; diff --git a/src/renderer/routes/extension-page-parameters.injectable.ts b/src/renderer/routes/extension-page-parameters.injectable.ts new file mode 100644 index 0000000000..1fe2d65857 --- /dev/null +++ b/src/renderer/routes/extension-page-parameters.injectable.ts @@ -0,0 +1,68 @@ +/** + * 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 { pipeline } from "@ogre-tools/fp"; +import { PageParam, PageParamInit } from "../navigation"; +import observableHistoryInjectable from "../navigation/observable-history.injectable"; +import type { LensRendererExtension } from "../../extensions/lens-renderer-extension"; +import type { PageRegistration } from "../../extensions/registries"; +import { fromPairs, map, toPairs } from "lodash/fp"; + +interface InstantiationParameter { + extension: LensRendererExtension; + registration: PageRegistration; +} + +const extensionPageParametersInjectable = getInjectable({ + id: "extension-page-parameters", + + instantiate: (di, { registration }: InstantiationParameter) => { + const observableHistory = di.inject(observableHistoryInjectable); + + return pipeline( + registration.params, + (params) => toPairs(params), + + map(([key, value]): [string, PageParamInit] => [ + key, + + typeof value === "string" + ? convertStringToPageParamInit(key, value) + : convertPartialPageParamInitToFull(key, value as PageParamInit), + ]), + + map(([key, value]) => [key, new PageParam(value, observableHistory)]), + + (paramsTuple) => fromPairs(paramsTuple), + ); + }, + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: ( + di, + { extension, registration }: InstantiationParameter, + ) => `${extension.sanitizedExtensionId}-${registration?.id}`, + }), +}); + +const convertPartialPageParamInitToFull = ( + key: string, + value: PageParamInit, +): PageParamInit => ({ + name: key, + defaultValue: value.defaultValue, + stringify: value.stringify, + parse: value.parse, +}); + +const convertStringToPageParamInit = ( + key: string, + value: string, +): PageParamInit => ({ + name: key, + defaultValue: value, +}); + +export default extensionPageParametersInjectable; diff --git a/src/renderer/routes/extension-route-registrator.injectable.tsx b/src/renderer/routes/extension-route-registrator.injectable.tsx new file mode 100644 index 0000000000..172da10e26 --- /dev/null +++ b/src/renderer/routes/extension-route-registrator.injectable.tsx @@ -0,0 +1,116 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { DiContainer, getInjectable } from "@ogre-tools/injectable"; + +import type { LensRendererExtension } from "../../extensions/lens-renderer-extension"; +import { observer } from "mobx-react"; +import React from "react"; +import { isEmpty, matches } from "lodash/fp"; +import type { PageRegistration } from "../../extensions/registries"; +import observableHistoryInjectable from "../navigation/observable-history.injectable"; +import type { ObservableHistory } from "mobx-observable-history"; +import { extensionRegistratorInjectionToken } from "../../extensions/extension-loader/extension-registrator-injection-token"; +import { SiblingsInTabLayout } from "../components/layout/siblings-in-tab-layout"; +import extensionPageParametersInjectable from "./extension-page-parameters.injectable"; +import { routeSpecificComponentInjectionToken } from "./route-specific-component-injection-token"; +import { computed } from "mobx"; +import { getExtensionRoutePath } from "./get-extension-route-path"; +import { routeInjectionToken } from "../../common/front-end-routing/route-injection-token"; + +const extensionRouteRegistratorInjectable = getInjectable({ + id: "extension-route-registrator", + + instantiate: (di: DiContainer) => { + const observableHistory = di.inject(observableHistoryInjectable); + + return ( + extension: LensRendererExtension, + extensionInstallationCount: number, + ) => { + const toRouteInjectable = toRouteInjectableFor( + di, + extension, + observableHistory, + extensionInstallationCount, + ); + + const routeInjectables = [ + ...extension.globalPages.map(toRouteInjectable(false)), + ...extension.clusterPages.map(toRouteInjectable(true)), + ].flat(); + + routeInjectables.forEach(di.register); + }; + }, + + injectionToken: extensionRegistratorInjectionToken, +}); + +export default extensionRouteRegistratorInjectable; + +const toRouteInjectableFor = + ( + di: DiContainer, + extension: LensRendererExtension, + observableHistory: ObservableHistory, + extensionInstallationCount: number, + ) => + (clusterFrame: boolean) => + (registration: PageRegistration) => { + const routeInjectable = getInjectable({ + id: `route-${registration.id}-for-extension-${extension.sanitizedExtensionId}-installation-${extensionInstallationCount}`, + + instantiate: () => ({ + path: getExtensionRoutePath(extension, registration.id), + clusterFrame, + isEnabled: computed(() => true), + extension, + }), + + injectionToken: routeInjectionToken, + }); + + const normalizedParams = di.inject(extensionPageParametersInjectable, { + extension, + registration, + }); + + const currentSidebarRegistration = extension.clusterPageMenus.find( + matches({ target: { pageId: registration.id }}), + ); + + const siblingRegistrations = currentSidebarRegistration?.parentId + ? extension.clusterPageMenus.filter( + matches({ parentId: currentSidebarRegistration.parentId }), + ) + : []; + + const ObserverPage = observer(registration.components.Page); + + const Component = () => { + if (isEmpty(siblingRegistrations)) { + return ; + } + + return ( + + + + ); + }; + + const routeSpecificComponentInjectable = getInjectable({ + id: `route-${registration.id}-component-for-extension-${extension.sanitizedExtensionId}-installation-${extensionInstallationCount}`, + + instantiate: (di) => ({ + route: di.inject(routeInjectable), + Component, + }), + + injectionToken: routeSpecificComponentInjectionToken, + }); + + return [routeInjectable, routeSpecificComponentInjectable]; + }; diff --git a/src/renderer/routes/get-extension-route-id.ts b/src/renderer/routes/get-extension-route-id.ts new file mode 100644 index 0000000000..213489d34c --- /dev/null +++ b/src/renderer/routes/get-extension-route-id.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export const getExtensionRouteId = ( + extensionId: string, + registrationId: string, +) => (registrationId ? `${extensionId}/${registrationId}` : extensionId); diff --git a/src/renderer/routes/get-extension-route-path.ts b/src/renderer/routes/get-extension-route-path.ts new file mode 100644 index 0000000000..e50028060c --- /dev/null +++ b/src/renderer/routes/get-extension-route-path.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getExtensionRouteId } from "./get-extension-route-id"; +import { getSanitizedPath } from "../../extensions/lens-extension"; +import type { LensRendererExtension } from "../../extensions/lens-renderer-extension"; + +export const getExtensionRoutePath = ( + extension: LensRendererExtension, + pageId?: string, +) => { + const routeId = getExtensionRouteId( + extension.sanitizedExtensionId, + pageId, + ); + + return getSanitizedPath("/extension", routeId); +}; diff --git a/src/renderer/routes/matching-route.injectable.ts b/src/renderer/routes/matching-route.injectable.ts new file mode 100644 index 0000000000..66cf12af57 --- /dev/null +++ b/src/renderer/routes/matching-route.injectable.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import routesInjectable from "./routes.injectable"; +import { matches } from "lodash/fp"; +import { computed } from "mobx"; +import { matchPath } from "react-router"; +import currentPathInjectable from "./current-path.injectable"; + +const matchingRouteInjectable = getInjectable({ + id: "matching-route", + + instantiate: (di) => { + const routes = di.inject(routesInjectable); + const currentPath = di.inject(currentPathInjectable); + + return computed(() => { + const matchedRoutes = routes.get().map((route) => { + const match = matchPath(currentPath.get(), { + path: route.path, + exact: true, + }); + + return { + route, + isMatching: !!match, + pathParameters: match ? match.params : {}, + }; + }); + + return matchedRoutes.find(matches({ isMatching: true })); + }); + }, +}); + +export default matchingRouteInjectable; diff --git a/src/renderer/routes/navigate-to-route.injectable.ts b/src/renderer/routes/navigate-to-route.injectable.ts new file mode 100644 index 0000000000..8b57a02613 --- /dev/null +++ b/src/renderer/routes/navigate-to-route.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; +import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token"; +import currentlyInClusterFrameInjectable from "./currently-in-cluster-frame.injectable"; +import { buildURL } from "../../common/utils/buildUrl"; + +const navigateToRouteInjectable = getInjectable({ + id: "navigate-to-route", + + instantiate: (di) => { + const navigateToUrl = di.inject(navigateToUrlInjectionToken); + + const currentlyInClusterFrame = di.inject( + currentlyInClusterFrameInjectable, + ); + + return (route, options) => { + const url = buildURL(route.path, { + // TODO: enhance typing + params: options?.parameters as any, + query: options?.query, + fragment: options?.fragment, + }); + + navigateToUrl(url, { + ...options, + forceRootFrame: currentlyInClusterFrame && route.clusterFrame === false, + }); + }; + }, + + injectionToken: navigateToRouteInjectionToken, +}); + +export default navigateToRouteInjectable; diff --git a/src/renderer/routes/navigate-to-url.injectable.ts b/src/renderer/routes/navigate-to-url.injectable.ts new file mode 100644 index 0000000000..468543d100 --- /dev/null +++ b/src/renderer/routes/navigate-to-url.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 } from "@ogre-tools/injectable"; +import observableHistoryInjectable from "../navigation/observable-history.injectable"; +import { runInAction } from "mobx"; +import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; +import { broadcastMessage } from "../../common/ipc"; +import { IpcRendererNavigationEvents } from "../navigation/events"; + +const navigateToUrlInjectable = getInjectable({ + id: "navigate-to-url", + + instantiate: (di) => { + const observableHistory = di.inject(observableHistoryInjectable); + + return (url, options = {}) => { + if (options.forceRootFrame) { + broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, url); + + return; + } + + runInAction(() => { + if (options.withoutAffectingBackButton) { + observableHistory.replace(url); + + return; + } + + observableHistory.push(url); + }); + }; + }, + + injectionToken: navigateToUrlInjectionToken, +}); + +export default navigateToUrlInjectable; diff --git a/src/renderer/routes/query-parameters.injectable.ts b/src/renderer/routes/query-parameters.injectable.ts new file mode 100644 index 0000000000..d8a5c3fec5 --- /dev/null +++ b/src/renderer/routes/query-parameters.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { parse as parseQueryString } from "query-string"; +import observableHistoryInjectable from "../navigation/observable-history.injectable"; + +const queryParametersInjectable = getInjectable({ + id: "query-parameters", + + instantiate: (di) => { + const observableHistory = di.inject(observableHistoryInjectable); + + return computed(() => { + const queryString = observableHistory.location.search; + + return parseQueryString(queryString); + }); + }, +}); + +export default queryParametersInjectable; diff --git a/src/renderer/routes/route-is-active.injectable.ts b/src/renderer/routes/route-is-active.injectable.ts new file mode 100644 index 0000000000..0bf4eb8f4b --- /dev/null +++ b/src/renderer/routes/route-is-active.injectable.ts @@ -0,0 +1,24 @@ +/** + * 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 currentRouteInjectable from "./current-route.injectable"; +import type { Route } from "../../common/front-end-routing/route-injection-token"; + +const routeIsActiveInjectable = getInjectable({ + id: "route-is-active", + + instantiate: (di, route: Route) => { + const currentRoute = di.inject(currentRouteInjectable); + + return computed(() => currentRoute.get() === route); + }, + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, route: Route) => route.path, + }), +}); + +export default routeIsActiveInjectable; diff --git a/src/renderer/routes/route-path-parameters.injectable.ts b/src/renderer/routes/route-path-parameters.injectable.ts new file mode 100644 index 0000000000..e99c48314a --- /dev/null +++ b/src/renderer/routes/route-path-parameters.injectable.ts @@ -0,0 +1,33 @@ +/** + * 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 { matchPath } from "react-router"; +import currentPathInjectable from "./current-path.injectable"; +import type { Route } from "../../common/front-end-routing/route-injection-token"; + +const routePathParametersInjectable = getInjectable({ + id: "route-path-parameters", + + instantiate: (di, route: Route) => { + const currentPath = di.inject(currentPathInjectable); + + // TODO: Reuse typing from route for accuracy + return computed((): Record => { + const match = matchPath(currentPath.get(), { + path: route.path, + exact: true, + }); + + return match ? match.params : {}; + }); + }, + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, route: Route) => route, + }), +}); + +export default routePathParametersInjectable; diff --git a/src/renderer/routes/route-specific-component-injection-token.ts b/src/renderer/routes/route-specific-component-injection-token.ts new file mode 100644 index 0000000000..5d3f58e3bb --- /dev/null +++ b/src/renderer/routes/route-specific-component-injection-token.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { Route } from "../../common/front-end-routing/route-injection-token"; + +export const routeSpecificComponentInjectionToken = getInjectionToken<{ + route: Route; + Component: React.ElementType; +}>({ + id: "route-specific-component-injection-token", +}); diff --git a/src/renderer/routes/routes.injectable.ts b/src/renderer/routes/routes.injectable.ts new file mode 100644 index 0000000000..ee93d61ee1 --- /dev/null +++ b/src/renderer/routes/routes.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import currentlyInClusterFrameInjectable from "./currently-in-cluster-frame.injectable"; +import allRoutesInjectable from "./all-routes.injectable"; +import { matches } from "lodash/fp"; + +const routesInjectable = getInjectable({ + id: "routes", + + instantiate: (di) => { + const allRoutes = di.inject(allRoutesInjectable); + + const currentlyInClusterFrame = di.inject( + currentlyInClusterFrameInjectable, + ); + + return computed(() => + allRoutes + .get() + .filter(matches({ clusterFrame: currentlyInClusterFrame })) + .filter((route) => route.isEnabled.get()), + ); + }, +}); + +export default routesInjectable; diff --git a/src/renderer/routes/sibling-tabs.injectable.ts b/src/renderer/routes/sibling-tabs.injectable.ts new file mode 100644 index 0000000000..e126fa0b74 --- /dev/null +++ b/src/renderer/routes/sibling-tabs.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { computed } from "mobx"; +import { getInjectable } from "@ogre-tools/injectable"; + +import sidebarItemsInjectable, { HierarchicalSidebarItem } from "../components/layout/sidebar-items.injectable"; +import { find } from "lodash/fp"; +import { pipeline } from "@ogre-tools/fp"; + +const siblingTabsInjectable = getInjectable({ + id: "sibling-tabs", + + instantiate: (di) => { + const sidebarItems = di.inject(sidebarItemsInjectable); + + return computed((): HierarchicalSidebarItem[] => + pipeline( + sidebarItems.get(), + find(({ isActive }) => isActive.get()), + ({ children = [] }) => children, + ), + ); + }, +}); + +export default siblingTabsInjectable; diff --git a/src/renderer/theme-store.injectable.ts b/src/renderer/theme-store.injectable.ts new file mode 100644 index 0000000000..40e8a6fc59 --- /dev/null +++ b/src/renderer/theme-store.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { ThemeStore } from "./theme.store"; + +const themeStoreInjectable = getInjectable({ + id: "theme-store", + + instantiate: () => { + ThemeStore.resetInstance(); + + return ThemeStore.createInstance(); + }, + + causesSideEffects: true, +}); + +export default themeStoreInjectable; diff --git a/src/renderer/utils/create-storage/create-storage.injectable.ts b/src/renderer/utils/create-storage/create-storage.injectable.ts index 69a999c809..f9df0315a9 100644 --- a/src/renderer/utils/create-storage/create-storage.injectable.ts +++ b/src/renderer/utils/create-storage/create-storage.injectable.ts @@ -7,18 +7,30 @@ import directoryForLensLocalStorageInjectable from "../../../common/directory-fo import { createStorage } from "./create-storage"; import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable"; import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable"; +import { observable } from "mobx"; +import loggerInjectable from "../../../common/logger.injectable"; +import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable"; const createStorageInjectable = getInjectable({ id: "create-storage", instantiate: (di) => createStorage({ + storage: observable({ + initialized: false, + loaded: false, + data: {} as Record, // json-serializable + }), + readJsonFile: di.inject(readJsonFileInjectable), writeJsonFile: di.inject(writeJsonFileInjectable), + logger: di.inject(loggerInjectable), directoryForLensLocalStorage: di.inject( directoryForLensLocalStorageInjectable, ), + + getAbsolutePath: di.inject(getAbsolutePathInjectable), }), }); diff --git a/src/renderer/utils/create-storage/create-storage.ts b/src/renderer/utils/create-storage/create-storage.ts index 8382d5accf..295e88fda7 100755 --- a/src/renderer/utils/create-storage/create-storage.ts +++ b/src/renderer/utils/create-storage/create-storage.ts @@ -5,41 +5,38 @@ // Keeps window.localStorage state in external JSON-files. // Because app creates random port between restarts => storage session wiped out each time. -import path from "path"; -import { comparer, observable, reaction, toJS, when } from "mobx"; +import { comparer, reaction, toJS, when } from "mobx"; import { StorageHelper } from "../storageHelper"; -import logger from "../../../main/logger"; import { isTestEnv } from "../../../common/vars"; import { getHostedClusterId } from "../../../common/utils"; -import type { JsonObject } from "type-fest"; - -const storage = observable({ - initialized: false, - loaded: false, - data: {} as Record, // json-serializable -}); +import type { JsonObject, JsonValue } from "type-fest"; +import type { Logger } from "../../../common/logger"; +import type { GetAbsolutePath } from "../../../common/path/get-absolute-path.injectable"; interface Dependencies { + storage: { initialized: boolean; loaded: boolean; data: Record }; + logger: Logger; directoryForLensLocalStorage: string; - readJsonFile: (filePath: string) => Promise; + readJsonFile: (filePath: string) => Promise; writeJsonFile: (filePath: string, contentObject: JsonObject) => Promise; + getAbsolutePath: GetAbsolutePath; } /** * Creates a helper for saving data under the "key" intended for window.localStorage */ -export const createStorage = ({ directoryForLensLocalStorage, readJsonFile, writeJsonFile }: Dependencies) => (key: string, defaultValue: T) => { +export const createStorage = ({ storage, getAbsolutePath, logger, directoryForLensLocalStorage, readJsonFile, writeJsonFile }: Dependencies) => (key: string, defaultValue: T) => { const { logPrefix } = StorageHelper; if (!storage.initialized) { storage.initialized = true; (async () => { - const filePath = path.resolve(directoryForLensLocalStorage, `${getHostedClusterId() || "app"}.json`); + const filePath = getAbsolutePath(directoryForLensLocalStorage, `${getHostedClusterId() || "app"}.json`); try { - storage.data = await readJsonFile(filePath); + storage.data = (await readJsonFile(filePath)) as JsonObject; } // eslint-disable-next-line no-empty diff --git a/src/test-utils/override-fs-with-fakes.ts b/src/test-utils/override-fs-with-fakes.ts new file mode 100644 index 0000000000..1f0d0321c7 --- /dev/null +++ b/src/test-utils/override-fs-with-fakes.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 type { DiContainer } from "@ogre-tools/injectable"; +import readFileInjectable from "../common/fs/read-file.injectable"; +import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; +import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; +import pathExistsInjectable from "../common/fs/path-exists.injectable"; + +type Override = (di: DiContainer) => void; + +export const overrideFsWithFakes = (di: DiContainer) => { + const state = new Map(); + + const readFile = readFileFor(state); + + const overrides: Override[] = [ + (di) => { + di.override(readFileInjectable, () => readFile); + }, + + (di) => { + di.override( + writeJsonFileInjectable, + () => (filePath: string, contents: object) => { + state.set(filePath, JSON.stringify(contents)); + + return Promise.resolve(); + }, + ); + }, + + (di) => { + di.override(readJsonFileInjectable, () => async (filePath: string) => { + const fileContent = await readFile(filePath); + + return JSON.parse(fileContent.toString()); + }); + }, + + (di) => { + di.override( + pathExistsInjectable, + () => async (filePath: string) => Promise.resolve(state.has(filePath)), + ); + }, + ]; + + overrides.forEach(callback => callback(di)); +}; + +const readFileFor = (state: Map) => (filePath: string) => { + const fileContent = state.get(filePath); + + if (!fileContent) { + const existingFilePaths = [...state.keys()].join('", "'); + + throw new Error( + `Tried to access file ${filePath} which does not exist. Existing file paths are: "${existingFilePaths}"`, + ); + } + + return Promise.resolve(fileContent); +}; diff --git a/src/test-utils/override-ipc-bridge.ts b/src/test-utils/override-ipc-bridge.ts index ff948fd8a5..efcb48a115 100644 --- a/src/test-utils/override-ipc-bridge.ts +++ b/src/test-utils/override-ipc-bridge.ts @@ -7,6 +7,10 @@ import type { Channel } from "../common/ipc-channel/channel"; import getValueFromRegisteredChannelInjectable from "../renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable"; import registerChannelInjectable from "../main/app-paths/register-channel/register-channel.injectable"; import asyncFn from "@async-fn/jest"; +import registerIpcChannelListenerInjectable from "../renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; +import windowManagerInjectable from "../main/window-manager.injectable"; +import type { SendToViewArgs, WindowManager } from "../main/window-manager"; +import { appNavigationIpcChannel } from "../common/front-end-routing/navigation-ipc-channel"; export const overrideIpcBridge = ({ rendererDi, @@ -63,4 +67,33 @@ export const overrideIpcBridge = ({ mainDi.override(registerChannelInjectable, () => (channel, callback) => { mainIpcRegistrations.set(channel, callback); }); + + const rendererIpcFakeHandles = new Map void)[]>(); + + rendererDi.override( + registerIpcChannelListenerInjectable, + () => + ({ channel, handle }) => { + const existingHandles = rendererIpcFakeHandles.get(channel.name) || []; + + rendererIpcFakeHandles.set(channel.name, [...existingHandles, handle]); + }, + ); + + mainDi.override( + windowManagerInjectable, + () => + ({ + sendToView: ({ channel: channelName, data }: SendToViewArgs) => { + const handles = rendererIpcFakeHandles.get(channelName); + + handles.forEach(handle => handle(...data)); + }, + + navigateSync(url: string) { + this.sendToView({ channel: appNavigationIpcChannel.name, data: [url] }); + }, + + } as unknown as WindowManager), + ); }; diff --git a/yarn.lock b/yarn.lock index 2ef9e574d8..fb2265da92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -956,28 +956,28 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" -"@ogre-tools/fp@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-5.1.2.tgz#8648bcd2e942211b8e94882afbfdf00ae2d07310" - integrity sha512-xWXg51KKoRX5p1czSkhUnWoRvzzM83V9bThWGjZKg5opZMUPmZI/GZ3uSALq0484lDr1rQCK1DaEDy4VDB7tWg== +"@ogre-tools/fp@5.2.0", "@ogre-tools/fp@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-5.2.0.tgz#75d46f5c4304242663c5a9c26544583c107a4755" + integrity sha512-Tm8RfJUmY+Xq8R4XA+B100PI4K0MTpsMq1Li16N3MENFpNP2eGmp1cLR8YhxahMtPPuzKScvlaIybnP6pX/c6Q== dependencies: lodash "^4.17.21" -"@ogre-tools/injectable-react@5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-5.1.2.tgz#95cf37881b026a18f3625fbfe896e987cb108f36" - integrity sha512-D/B4Tyh5KuEnPP73QTZpbErDtCHGDbYPp6KvOU4t+TnNUCX0fw2ImPSa09YmjtyLh3hr4DZVyf/QpamY6HEjMw== +"@ogre-tools/injectable-react@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-5.2.0.tgz#ec55cd88d3b7e778952d77e6fcb4771f315430d2" + integrity sha512-V1aMIKGIXrhZe6OwhcZ5xm4PLfE9F21KXOXhhz7dXcECWKh0+tHUJaG3MaMPzJqloRSE9SNbMz5nCOv172xRvg== dependencies: - "@ogre-tools/fp" "^5.1.2" - "@ogre-tools/injectable" "^5.1.2" + "@ogre-tools/fp" "^5.2.0" + "@ogre-tools/injectable" "^5.2.0" lodash "^4.17.21" -"@ogre-tools/injectable@5.1.2", "@ogre-tools/injectable@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-5.1.2.tgz#7fa0673c447e493ea8bc0c6821cfe6e6a79ce8fe" - integrity sha512-dP4T/vHy6HlfAJPzXtzGRlgwMDx7SqnAJKvDuhDPpsMWqEGPxouVMMJIi7aF2lvZtnDj9xJK/VfAk7oXPcMXAA== +"@ogre-tools/injectable@5.2.0", "@ogre-tools/injectable@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-5.2.0.tgz#40c1e3eedc6103d985b6daa124ef6d3eaff0c82c" + integrity sha512-pREfT/51AAWrqBerFc/UCvFN+Xa3U2sgkC0KzHGP5kogjuI1UecWTTChoiO01yoqgfVffqfBoe9uOFs/TzSogQ== dependencies: - "@ogre-tools/fp" "^5.1.2" + "@ogre-tools/fp" "^5.2.0" lodash "^4.17.21" "@panva/asn1.js@^1.0.0":