mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Cascade different refactorings to eliminate global shared state from app paths
Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
1 parent
908a40975e
commit
9d33fff906
336 files changed
+9105
-3307
No files matched your search
@@ -23,8 +23,8 @@ import { computed, observable, makeObservable, action } from "mobx";
|
||||
import { ipcRendererOn } from "../../common/ipc";
|
||||
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
|
||||
import "../../common/catalog-entities";
|
||||
import type { Cluster } from "../../main/cluster";
|
||||
import { ClusterStore } from "../../common/cluster-store";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { ClusterStore } from "../../common/cluster-store/cluster-store";
|
||||
import { Disposer, iter } from "../utils";
|
||||
import { once } from "lodash";
|
||||
import logger from "../../common/logger";
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { catalogEntityRegistry } from "../catalog-entity-registry";
|
||||
|
||||
const catalogEntityRegistryInjectable = getInjectable({
|
||||
instantiate: () => catalogEntityRegistry,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default catalogEntityRegistryInjectable;
|
||||
+32
-43
@@ -31,8 +31,6 @@ import * as LensExtensionsRendererApi from "../extensions/renderer-api";
|
||||
import { render } from "react-dom";
|
||||
import { delay } from "../common/utils";
|
||||
import { isMac, isDevelopment } from "../common/vars";
|
||||
import { ClusterStore } from "../common/cluster-store";
|
||||
import { UserStore } from "../common/user-store";
|
||||
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
|
||||
import { DefaultProps } from "./mui-base-theme";
|
||||
import configurePackages from "../common/configure-packages";
|
||||
@@ -40,26 +38,27 @@ import * as initializers from "./initializers";
|
||||
import logger from "../common/logger";
|
||||
import { HotbarStore } from "../common/hotbar-store";
|
||||
import { WeblinkStore } from "../common/weblink-store";
|
||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import { ThemeStore } from "./theme.store";
|
||||
import { SentryInit } from "../common/sentry";
|
||||
import { TerminalStore } from "./components/dock/terminal.store";
|
||||
import { AppPaths } from "../common/app-paths";
|
||||
import { registerCustomThemes } from "./components/monaco-editor";
|
||||
import { getDi } from "./components/getDi";
|
||||
import { DiContextProvider } from "@ogre-tools/injectable-react";
|
||||
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
||||
import type { ExtensionLoader } from "../extensions/extension-loader";
|
||||
import bindProtocolAddRouteHandlersInjectable
|
||||
from "./protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable";
|
||||
import type { LensProtocolRouterRenderer } from "./protocol-handler";
|
||||
import lensProtocolRouterRendererInjectable
|
||||
from "./protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable";
|
||||
|
||||
|
||||
import extensionDiscoveryInjectable
|
||||
from "../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import extensionInstallationStateStoreInjectable
|
||||
from "../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
|
||||
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 createTerminalTabInjectable
|
||||
from "./components/dock/create-terminal-tab/create-terminal-tab.injectable";
|
||||
|
||||
if (process.isMainFrame) {
|
||||
SentryInit();
|
||||
@@ -79,23 +78,14 @@ async function attachChromeDebugger() {
|
||||
}
|
||||
}
|
||||
|
||||
type AppComponent = React.ComponentType & {
|
||||
|
||||
// TODO: This static method is criminal as it has no direct relation with component
|
||||
init(
|
||||
rootElem: HTMLElement,
|
||||
extensionLoader: ExtensionLoader,
|
||||
bindProtocolAddRouteHandlers?: () => void,
|
||||
lensProtocolRouterRendererInjectable?: LensProtocolRouterRenderer
|
||||
): Promise<void>;
|
||||
};
|
||||
|
||||
export async function bootstrap(comp: () => Promise<AppComponent>, di: DependencyInjectionContainer) {
|
||||
export async function bootstrap(di: DependencyInjectionContainer) {
|
||||
await di.runSetups();
|
||||
|
||||
const rootElem = document.getElementById("app");
|
||||
const logPrefix = `[BOOTSTRAP-${process.isMainFrame ? "ROOT" : "CLUSTER"}-FRAME]:`;
|
||||
|
||||
await AppPaths.init();
|
||||
UserStore.createInstance();
|
||||
// TODO: Remove temporal dependencies to make timing of initialization not important
|
||||
di.inject(userStoreInjectable);
|
||||
|
||||
await attachChromeDebugger();
|
||||
rootElem.classList.toggle("is-mac", isMac);
|
||||
@@ -103,8 +93,10 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
||||
logger.info(`${logPrefix} initializing Registries`);
|
||||
initializers.initRegistries();
|
||||
|
||||
const createTerminalTab = di.inject(createTerminalTabInjectable);
|
||||
|
||||
logger.info(`${logPrefix} initializing CommandRegistry`);
|
||||
initializers.initCommandRegistry();
|
||||
initializers.initCommandRegistry(createTerminalTab);
|
||||
|
||||
logger.info(`${logPrefix} initializing EntitySettingsRegistry`);
|
||||
initializers.initEntitySettingsRegistry();
|
||||
@@ -145,19 +137,16 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
||||
extensionDiscovery.init();
|
||||
|
||||
// ClusterStore depends on: UserStore
|
||||
const clusterStore = ClusterStore.createInstance();
|
||||
const clusterStore = di.inject(clusterStoreInjectable);
|
||||
|
||||
await clusterStore.loadInitialOnRenderer();
|
||||
|
||||
// HotbarStore depends on: ClusterStore
|
||||
HotbarStore.createInstance();
|
||||
FilesystemProvisionerStore.createInstance();
|
||||
|
||||
// ThemeStore depends on: UserStore
|
||||
ThemeStore.createInstance();
|
||||
|
||||
// TerminalStore depends on: ThemeStore
|
||||
TerminalStore.createInstance();
|
||||
WeblinkStore.createInstance();
|
||||
|
||||
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
@@ -169,13 +158,20 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
||||
// Register additional store listeners
|
||||
clusterStore.registerIpcListener();
|
||||
|
||||
// init app's dependencies if any
|
||||
const App = await comp();
|
||||
let App;
|
||||
let initializeApp;
|
||||
|
||||
const bindProtocolAddRouteHandlers = di.inject(bindProtocolAddRouteHandlersInjectable);
|
||||
const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable);
|
||||
// TODO: Introduce proper architectural boundaries between root and cluster iframes
|
||||
if (process.isMainFrame) {
|
||||
initializeApp = di.inject(initRootFrameInjectable);
|
||||
|
||||
await App.init(rootElem, extensionLoader, bindProtocolAddRouteHandlers, lensProtocolRouterRenderer);
|
||||
App = (await import("./frames/root-frame/root-frame")).RootFrame;
|
||||
} else {
|
||||
initializeApp = di.inject(initClusterFrameInjectable);
|
||||
App = (await import("./frames/cluster-frame/cluster-frame")).ClusterFrame;
|
||||
}
|
||||
|
||||
await initializeApp(rootElem);
|
||||
|
||||
render(
|
||||
<DiContextProvider value={{ di }}>
|
||||
@@ -189,14 +185,7 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
||||
const di = getDi();
|
||||
|
||||
// run
|
||||
bootstrap(
|
||||
async () =>
|
||||
process.isMainFrame
|
||||
? (await import("./root-frame")).RootFrame
|
||||
: (await import("./cluster-frame")).ClusterFrame,
|
||||
di,
|
||||
);
|
||||
|
||||
bootstrap(di);
|
||||
|
||||
/**
|
||||
* Exports for virtual package "@k8slens/extensions" for renderer-process.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { ClusterFrameContext } from "./cluster-frame-context";
|
||||
import namespaceStoreInjectable from "../components/+namespaces/namespace-store/namespace-store.injectable";
|
||||
import hostedClusterInjectable from "../../common/cluster-store/hosted-cluster/hosted-cluster.injectable";
|
||||
|
||||
const clusterFrameContextInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const cluster = di.inject(hostedClusterInjectable);
|
||||
|
||||
return new ClusterFrameContext(
|
||||
cluster,
|
||||
|
||||
{
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default clusterFrameContextInjectable;
|
||||
+11
-7
@@ -19,13 +19,17 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { Cluster } from "../../main/cluster";
|
||||
import { namespaceStore } from "./+namespaces/namespace.store";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { NamespaceStore } from "../components/+namespaces/namespace-store/namespace.store";
|
||||
import type { ClusterContext } from "../../common/k8s-api/cluster-context";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
|
||||
export class FrameContext implements ClusterContext {
|
||||
constructor(public cluster: Cluster) {
|
||||
interface Dependencies {
|
||||
namespaceStore: NamespaceStore
|
||||
}
|
||||
|
||||
export class ClusterFrameContext implements ClusterContext {
|
||||
constructor(public cluster: Cluster, private dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@@ -35,9 +39,9 @@ export class FrameContext implements ClusterContext {
|
||||
return this.cluster.accessibleNamespaces;
|
||||
}
|
||||
|
||||
if (namespaceStore.items.length > 0) {
|
||||
if (this.dependencies.namespaceStore.items.length > 0) {
|
||||
// namespaces from kubernetes api
|
||||
return namespaceStore.items.map((namespace) => namespace.getName());
|
||||
return this.dependencies.namespaceStore.items.map((namespace) => namespace.getName());
|
||||
} else {
|
||||
// fallback to cluster resolved namespaces because we could not load list
|
||||
return this.cluster.allowedNamespaces || [];
|
||||
@@ -45,7 +49,7 @@ export class FrameContext implements ClusterContext {
|
||||
}
|
||||
|
||||
@computed get contextNamespaces(): string[] {
|
||||
return namespaceStore.contextNamespaces;
|
||||
return this.dependencies.namespaceStore.contextNamespaces;
|
||||
}
|
||||
|
||||
@computed get hasSelectedAll(): boolean {
|
||||
@@ -1,251 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { observable, makeObservable, when } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Redirect, Route, Router, Switch } from "react-router";
|
||||
import { history } from "./navigation";
|
||||
import { NotFound } from "./components/+404";
|
||||
import { UserManagement } from "./components/+user-management/user-management";
|
||||
import { ConfirmDialog } from "./components/confirm-dialog";
|
||||
import { ClusterOverview } from "./components/+cluster/cluster-overview";
|
||||
import { Events } from "./components/+events/events";
|
||||
import { DeploymentScaleDialog } from "./components/+workloads-deployments/deployment-scale-dialog";
|
||||
import { CronJobTriggerDialog } from "./components/+workloads-cronjobs/cronjob-trigger-dialog";
|
||||
import { CustomResources } from "./components/+custom-resources/custom-resources";
|
||||
import { isAllowedResource } from "../common/utils/allowed-resource";
|
||||
import logger from "../main/logger";
|
||||
import { webFrame } from "electron";
|
||||
import { ClusterPageRegistry, getExtensionPageUrl } from "../extensions/registries/page-registry";
|
||||
import type { ExtensionLoader } from "../extensions/extension-loader";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { requestMain } from "../common/ipc";
|
||||
import { clusterSetFrameIdHandler } from "../common/cluster-ipc";
|
||||
import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../extensions/registries";
|
||||
import { StatefulSetScaleDialog } from "./components/+workloads-statefulsets/statefulset-scale-dialog";
|
||||
import { KubeWatchApi, kubeWatchApi } from "../common/k8s-api/kube-watch-api";
|
||||
import { ReplicaSetScaleDialog } from "./components/+workloads-replicasets/replicaset-scale-dialog";
|
||||
import { CommandContainer } from "./components/command-palette/command-container";
|
||||
import { KubeObjectStore } from "../common/k8s-api/kube-object.store";
|
||||
import { FrameContext } from "./components/context";
|
||||
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";
|
||||
import { KubeObjectDetails } from "./components/kube-object-details";
|
||||
import { KubeConfigDialog } from "./components/kubeconfig-dialog";
|
||||
import { Terminal } from "./components/dock/terminal";
|
||||
import { namespaceStore } from "./components/+namespaces/namespace.store";
|
||||
import { Sidebar } from "./components/layout/sidebar";
|
||||
import { Dock } from "./components/dock";
|
||||
import { Apps } from "./components/+apps";
|
||||
import { Namespaces } from "./components/+namespaces";
|
||||
import { Network } from "./components/+network";
|
||||
import { Nodes } from "./components/+nodes";
|
||||
import { Workloads } from "./components/+workloads";
|
||||
import { Config } from "./components/+config";
|
||||
import { Storage } from "./components/+storage";
|
||||
import { catalogEntityRegistry } from "./api/catalog-entity-registry";
|
||||
import { getHostedClusterId } from "./utils";
|
||||
import { ClusterStore } from "../common/cluster-store";
|
||||
import type { ClusterId } from "../common/cluster-types";
|
||||
import { watchHistoryState } from "./remote-helpers/history-updater";
|
||||
import { unmountComponentAtNode } from "react-dom";
|
||||
import { PortForwardDialog } from "./port-forward";
|
||||
import { DeleteClusterDialog } from "./components/delete-cluster-dialog";
|
||||
import { WorkloadsOverview } from "./components/+workloads-overview/overview";
|
||||
import { KubeObjectListLayout } from "./components/kube-object-list-layout";
|
||||
import type { KubernetesCluster } from "../common/catalog-entities";
|
||||
|
||||
@observer
|
||||
export class ClusterFrame extends React.Component {
|
||||
static clusterId: ClusterId;
|
||||
static readonly logPrefix = "[CLUSTER-FRAME]:";
|
||||
static displayName = "ClusterFrame";
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
static async init(rootElem: HTMLElement, extensionLoader: ExtensionLoader) {
|
||||
catalogEntityRegistry.init();
|
||||
const frameId = webFrame.routingId;
|
||||
|
||||
ClusterFrame.clusterId = getHostedClusterId();
|
||||
|
||||
const cluster = ClusterStore.getInstance().getById(ClusterFrame.clusterId);
|
||||
|
||||
logger.info(`${ClusterFrame.logPrefix} Init dashboard, clusterId=${ClusterFrame.clusterId}, frameId=${frameId}`);
|
||||
await Terminal.preloadFonts();
|
||||
await requestMain(clusterSetFrameIdHandler, ClusterFrame.clusterId);
|
||||
await cluster.whenReady; // cluster.activate() is done at this point
|
||||
|
||||
catalogEntityRegistry.activeEntity = ClusterFrame.clusterId;
|
||||
|
||||
// Only load the extensions once the catalog has been populated
|
||||
when(
|
||||
() => Boolean(catalogEntityRegistry.activeEntity),
|
||||
() => extensionLoader.loadOnClusterRenderer(catalogEntityRegistry.activeEntity as KubernetesCluster),
|
||||
{
|
||||
timeout: 15_000,
|
||||
onError: (error) => {
|
||||
console.warn("[CLUSTER-FRAME]: error from activeEntity when()", error);
|
||||
Notifications.error("Failed to get KubernetesCluster for this view. Extensions will not be loaded.");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
appEventBus.emit({
|
||||
name: "cluster",
|
||||
action: "open",
|
||||
params: {
|
||||
clusterId: ClusterFrame.clusterId,
|
||||
},
|
||||
});
|
||||
});
|
||||
window.addEventListener("online", () => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
window.onbeforeunload = () => {
|
||||
logger.info(`${ClusterFrame.logPrefix} Unload dashboard, clusterId=${ClusterFrame.clusterId}, frameId=${frameId}`);
|
||||
|
||||
unmountComponentAtNode(rootElem);
|
||||
};
|
||||
|
||||
const clusterContext = new FrameContext(cluster);
|
||||
|
||||
// Setup hosted cluster context
|
||||
KubeObjectStore.defaultContext.set(clusterContext);
|
||||
WorkloadsOverview.clusterContext
|
||||
= KubeObjectListLayout.clusterContext
|
||||
= KubeWatchApi.context
|
||||
= clusterContext;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
kubeWatchApi.subscribeStores([
|
||||
namespaceStore,
|
||||
]),
|
||||
|
||||
watchHistoryState(),
|
||||
]);
|
||||
}
|
||||
|
||||
@observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? routes.clusterURL() : routes.workloadsURL();
|
||||
|
||||
getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) {
|
||||
const routes: TabLayoutRoute[] = [];
|
||||
|
||||
if (!menuItem.id) {
|
||||
return routes;
|
||||
}
|
||||
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 = () => <TabLayout tabs={tabRoutes}/>;
|
||||
|
||||
return <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)}/>;
|
||||
} else {
|
||||
const page = ClusterPageRegistry.getInstance().getByPageTarget(menu.target);
|
||||
|
||||
if (page) {
|
||||
return <Route key={`extension-tab-layout-route-${index}`} path={page.url} component={page.components.Page}/>;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
renderExtensionRoutes() {
|
||||
return ClusterPageRegistry.getInstance().getItems().map((page, index) => {
|
||||
const menu = ClusterPageMenuRegistry.getInstance().getByPage(page);
|
||||
|
||||
if (!menu) {
|
||||
return <Route key={`extension-route-${index}`} path={page.url} component={page.components.Page}/>;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Router history={history}>
|
||||
<ErrorBoundary>
|
||||
<MainLayout sidebar={<Sidebar/>} footer={<Dock/>}>
|
||||
<Switch>
|
||||
<Route component={ClusterOverview} {...routes.clusterRoute}/>
|
||||
<Route component={Nodes} {...routes.nodesRoute}/>
|
||||
<Route component={Workloads} {...routes.workloadsRoute}/>
|
||||
<Route component={Config} {...routes.configRoute}/>
|
||||
<Route component={Network} {...routes.networkRoute}/>
|
||||
<Route component={Storage} {...routes.storageRoute}/>
|
||||
<Route component={Namespaces} {...routes.namespacesRoute}/>
|
||||
<Route component={Events} {...routes.eventRoute}/>
|
||||
<Route component={CustomResources} {...routes.crdRoute}/>
|
||||
<Route component={UserManagement} {...routes.usersManagementRoute}/>
|
||||
<Route component={Apps} {...routes.appsRoute}/>
|
||||
{this.renderExtensionTabLayoutRoutes()}
|
||||
{this.renderExtensionRoutes()}
|
||||
<Redirect exact from="/" to={this.startUrl}/>
|
||||
<Route component={NotFound}/>
|
||||
</Switch>
|
||||
</MainLayout>
|
||||
<Notifications/>
|
||||
<ConfirmDialog/>
|
||||
<KubeObjectDetails/>
|
||||
<KubeConfigDialog/>
|
||||
<DeploymentScaleDialog/>
|
||||
<StatefulSetScaleDialog/>
|
||||
<ReplicaSetScaleDialog/>
|
||||
<CronJobTriggerDialog/>
|
||||
<PortForwardDialog/>
|
||||
<DeleteClusterDialog/>
|
||||
<CommandContainer clusterId={ClusterFrame.clusterId}/>
|
||||
</ErrorBoundary>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -28,23 +28,31 @@ import { action, computed, makeObservable, observable } from "mobx";
|
||||
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/event-bus";
|
||||
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 { getCustomKubeConfigPath, iter } from "../../utils";
|
||||
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";
|
||||
|
||||
interface Option {
|
||||
config: KubeConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
getCustomKubeConfigDirectory: (directoryName: string) => string
|
||||
}
|
||||
|
||||
function getContexts(config: KubeConfig): Map<string, Option> {
|
||||
return new Map(
|
||||
splitConfig(config)
|
||||
@@ -56,14 +64,14 @@ function getContexts(config: KubeConfig): Map<string, Option> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class AddCluster extends React.Component {
|
||||
class NonInjectedAddCluster extends React.Component<Dependencies> {
|
||||
@observable kubeContexts = observable.map<string, Option>();
|
||||
@observable customConfig = "";
|
||||
@observable isWaiting = false;
|
||||
@observable errors: string[] = [];
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
constructor(dependencies: Dependencies) {
|
||||
super(dependencies);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@@ -99,7 +107,7 @@ export class AddCluster extends React.Component {
|
||||
appEventBus.emit({ name: "cluster-add", action: "click" });
|
||||
|
||||
try {
|
||||
const absPath = getCustomKubeConfigPath();
|
||||
const absPath = this.props.getCustomKubeConfigDirectory(uuid.v4());
|
||||
|
||||
await fse.ensureDir(path.dirname(absPath));
|
||||
await fse.writeFile(absPath, this.customConfig.trim(), { encoding: "utf-8", mode: 0o600 });
|
||||
@@ -153,3 +161,11 @@ export class AddCluster extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const AddCluster = withInjectables<Dependencies>(NonInjectedAddCluster, {
|
||||
getProps: (di) => ({
|
||||
getCustomKubeConfigDirectory: di.inject(
|
||||
getCustomKubeConfigDirectoryInjectable,
|
||||
),
|
||||
}),
|
||||
});
|
||||
@@ -31,9 +31,11 @@ import { MarkdownViewer } from "../markdown-viewer";
|
||||
import { Spinner } from "../spinner";
|
||||
import { Button } from "../button";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { createInstallChartTab } from "../dock/install-chart.store";
|
||||
import { Badge } from "../badge";
|
||||
import { Tooltip, withStyles } from "@material-ui/core";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import createInstallChartTabInjectable
|
||||
from "../dock/create-install-chart-tab/create-install-chart-tab.injectable";
|
||||
|
||||
interface Props {
|
||||
chart: HelmChart;
|
||||
@@ -46,8 +48,12 @@ const LargeTooltip = withStyles({
|
||||
},
|
||||
})(Tooltip);
|
||||
|
||||
interface Dependencies {
|
||||
createInstallChartTab: (helmChart: HelmChart) => void
|
||||
}
|
||||
|
||||
@observer
|
||||
export class HelmChartDetails extends Component<Props> {
|
||||
class NonInjectedHelmChartDetails extends Component<Props & Dependencies> {
|
||||
@observable chartVersions: HelmChart[];
|
||||
@observable selectedChart?: HelmChart;
|
||||
@observable readme?: string;
|
||||
@@ -55,7 +61,7 @@ export class HelmChartDetails extends Component<Props> {
|
||||
|
||||
private abortController?: AbortController;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -106,7 +112,7 @@ export class HelmChartDetails extends Component<Props> {
|
||||
|
||||
@boundMethod
|
||||
install() {
|
||||
createInstallChartTab(this.selectedChart);
|
||||
this.props.createInstallChartTab(this.selectedChart);
|
||||
this.props.hideDetails();
|
||||
}
|
||||
|
||||
@@ -215,3 +221,14 @@ export class HelmChartDetails extends Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const HelmChartDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedHelmChartDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
createInstallChartTab: di.inject(createInstallChartTabInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -36,9 +36,8 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Spinner } from "../spinner";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { Button } from "../button";
|
||||
import { releaseStore } from "./release.store";
|
||||
import type { ReleaseStore } from "./release.store";
|
||||
import { Notifications } from "../notifications";
|
||||
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
@@ -47,14 +46,23 @@ import { Secret } from "../../../common/k8s-api/endpoints";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { MonacoEditor } from "../monaco-editor";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import releaseStoreInjectable from "./release-store.injectable";
|
||||
import createUpgradeChartTabInjectable
|
||||
from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable";
|
||||
|
||||
interface Props {
|
||||
release: HelmRelease;
|
||||
hideDetails(): void;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
releaseStore: ReleaseStore
|
||||
createUpgradeChartTab: (release: HelmRelease) => void
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ReleaseDetails extends Component<Props> {
|
||||
class NonInjectedReleaseDetails extends Component<Props & Dependencies> {
|
||||
@observable details: IReleaseDetails | null = null;
|
||||
@observable values = "";
|
||||
@observable valuesLoading = false;
|
||||
@@ -73,7 +81,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
}),
|
||||
reaction(() => secretsStore.getItems(), () => {
|
||||
if (!this.props.release) return;
|
||||
const { getReleaseSecret } = releaseStore;
|
||||
const { getReleaseSecret } = this.props.releaseStore;
|
||||
const { release } = this.props;
|
||||
const secret = getReleaseSecret(release);
|
||||
|
||||
@@ -89,7 +97,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
]);
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -133,7 +141,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
await releaseStore.update(name, namespace, data);
|
||||
await this.props.releaseStore.update(name, namespace, data);
|
||||
Notifications.ok(
|
||||
<p>Release <b>{name}</b> successfully updated!</p>,
|
||||
);
|
||||
@@ -146,7 +154,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
upgradeVersion = () => {
|
||||
const { release, hideDetails } = this.props;
|
||||
|
||||
createUpgradeChartTab(release);
|
||||
this.props.createUpgradeChartTab(release);
|
||||
hideDetails();
|
||||
};
|
||||
|
||||
@@ -315,3 +323,15 @@ export class ReleaseDetails extends Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ReleaseDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedReleaseDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
releaseStore: di.inject(releaseStoreInjectable),
|
||||
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -22,32 +22,42 @@
|
||||
import React from "react";
|
||||
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { cssNames } from "../../utils";
|
||||
import { releaseStore } from "./release.store";
|
||||
import type { ReleaseStore } from "./release.store";
|
||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
|
||||
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import releaseStoreInjectable from "./release-store.injectable";
|
||||
import createUpgradeChartTabInjectable
|
||||
from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable";
|
||||
import releaseRollbackDialogModelInjectable
|
||||
from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable";
|
||||
|
||||
interface Props extends MenuActionsProps {
|
||||
release: HelmRelease;
|
||||
hideDetails?(): void;
|
||||
}
|
||||
|
||||
export class HelmReleaseMenu extends React.Component<Props> {
|
||||
interface Dependencies {
|
||||
releaseStore: ReleaseStore
|
||||
createUpgradeChartTab: (release: HelmRelease) => void
|
||||
openRollbackDialog: (release: HelmRelease) => void
|
||||
}
|
||||
|
||||
class NonInjectedHelmReleaseMenu extends React.Component<Props & Dependencies> {
|
||||
remove = () => {
|
||||
return releaseStore.remove(this.props.release);
|
||||
return this.props.releaseStore.remove(this.props.release);
|
||||
};
|
||||
|
||||
upgrade = () => {
|
||||
const { release, hideDetails } = this.props;
|
||||
|
||||
createUpgradeChartTab(release);
|
||||
this.props.createUpgradeChartTab(release);
|
||||
hideDetails?.();
|
||||
};
|
||||
|
||||
rollback = () => {
|
||||
ReleaseRollbackDialog.open(this.props.release);
|
||||
this.props.openRollbackDialog(this.props.release);
|
||||
};
|
||||
|
||||
renderContent() {
|
||||
@@ -87,3 +97,17 @@ export class HelmReleaseMenu extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const HelmReleaseMenu = withInjectables<Dependencies, Props>(
|
||||
NonInjectedHelmReleaseMenu,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
releaseStore: di.inject(releaseStoreInjectable),
|
||||
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
|
||||
openRollbackDialog: di.inject(releaseRollbackDialogModelInjectable).open,
|
||||
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { ReleaseRollbackDialogModel } from "./release-rollback-dialog-model";
|
||||
|
||||
const releaseRollbackDialogModelInjectable = getInjectable({
|
||||
instantiate: () => new ReleaseRollbackDialogModel(),
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default releaseRollbackDialogModelInjectable;
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { computed, observable, makeObservable, action } from "mobx";
|
||||
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
|
||||
export class ReleaseRollbackDialogModel {
|
||||
release: HelmRelease | null = null;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
isOpen: computed,
|
||||
release: observable,
|
||||
open: action,
|
||||
close: action,
|
||||
});
|
||||
}
|
||||
|
||||
get isOpen() {
|
||||
return !!this.release;
|
||||
}
|
||||
|
||||
open = (release: HelmRelease) => {
|
||||
this.release = release;
|
||||
};
|
||||
|
||||
close = () => {
|
||||
this.release = null;
|
||||
};
|
||||
}
|
||||
@@ -27,41 +27,36 @@ import { observer } from "mobx-react";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { getReleaseHistory, HelmRelease, IReleaseRevision } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { releaseStore } from "./release.store";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { Notifications } from "../notifications";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import releaseStoreInjectable from "./release-store.injectable";
|
||||
import releaseRollbackDialogModelInjectable
|
||||
from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable";
|
||||
import type { ReleaseRollbackDialogModel } from "./release-rollback-dialog-model/release-rollback-dialog-model";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
}
|
||||
|
||||
const dialogState = observable.object({
|
||||
isOpen: false,
|
||||
release: null as HelmRelease,
|
||||
});
|
||||
interface Dependencies {
|
||||
rollbackRelease: (releaseName: string, namespace: string, revisionNumber: number) => Promise<any>
|
||||
model: ReleaseRollbackDialogModel
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
class NonInjectedReleaseRollbackDialog extends React.Component<Props & Dependencies> {
|
||||
@observable isLoading = false;
|
||||
@observable revision: IReleaseRevision;
|
||||
@observable revisions = observable.array<IReleaseRevision>();
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
static open(release: HelmRelease) {
|
||||
dialogState.isOpen = true;
|
||||
dialogState.release = release;
|
||||
}
|
||||
|
||||
static close() {
|
||||
dialogState.isOpen = false;
|
||||
}
|
||||
|
||||
get release(): HelmRelease {
|
||||
return dialogState.release;
|
||||
return this.props.model.release;
|
||||
}
|
||||
|
||||
onOpen = async () => {
|
||||
@@ -78,17 +73,13 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
const revisionNumber = this.revision.revision;
|
||||
|
||||
try {
|
||||
await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber);
|
||||
this.close();
|
||||
await this.props.rollbackRelease(this.release.getName(), this.release.getNs(), revisionNumber);
|
||||
this.props.model.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
close = () => {
|
||||
ReleaseRollbackDialog.close();
|
||||
};
|
||||
|
||||
renderContent() {
|
||||
const { revision, revisions } = this;
|
||||
|
||||
@@ -120,11 +111,11 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="ReleaseRollbackDialog"
|
||||
isOpen={dialogState.isOpen}
|
||||
isOpen={this.props.model.isOpen}
|
||||
onOpen={this.onOpen}
|
||||
close={this.close}
|
||||
close={this.props.model.close}
|
||||
>
|
||||
<Wizard header={header} done={this.close}>
|
||||
<Wizard header={header} done={this.props.model.close}>
|
||||
<WizardStep
|
||||
scrollable={false}
|
||||
nextLabel="Rollback"
|
||||
@@ -138,3 +129,15 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ReleaseRollbackDialog = withInjectables<Dependencies, Props>(
|
||||
NonInjectedReleaseRollbackDialog,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
rollbackRelease: di.inject(releaseStoreInjectable).rollback,
|
||||
model: di.inject(releaseRollbackDialogModelInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { ReleaseStore } from "./release.store";
|
||||
import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable";
|
||||
|
||||
const releaseStoreInjectable = getInjectable({
|
||||
instantiate: (di) => new ReleaseStore({
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default releaseStoreInjectable;
|
||||
@@ -26,13 +26,17 @@ import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IRele
|
||||
import { ItemStore } from "../../../common/item.store";
|
||||
import type { Secret } from "../../../common/k8s-api/endpoints";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store";
|
||||
import { Notifications } from "../notifications";
|
||||
|
||||
interface Dependencies {
|
||||
namespaceStore: NamespaceStore
|
||||
}
|
||||
|
||||
export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
releaseSecrets = observable.map<string, Secret>();
|
||||
|
||||
constructor() {
|
||||
constructor(private dependencies: Dependencies ) {
|
||||
super();
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
@@ -61,7 +65,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
}
|
||||
|
||||
watchSelectedNamespaces(): (() => void) {
|
||||
return reaction(() => namespaceStore.context.contextNamespaces, namespaces => {
|
||||
return reaction(() => this.dependencies.namespaceStore.context.contextNamespaces, namespaces => {
|
||||
this.loadAll(namespaces);
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
@@ -106,13 +110,13 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
}
|
||||
|
||||
async loadFromContextNamespaces(): Promise<void> {
|
||||
return this.loadAll(namespaceStore.context.contextNamespaces);
|
||||
return this.loadAll(this.dependencies.namespaceStore.context.contextNamespaces);
|
||||
}
|
||||
|
||||
async loadItems(namespaces: string[]) {
|
||||
const isLoadingAll = namespaceStore.context.allNamespaces?.length > 1
|
||||
&& namespaceStore.context.cluster.accessibleNamespaces.length === 0
|
||||
&& namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns));
|
||||
const isLoadingAll = this.dependencies.namespaceStore.context.allNamespaces?.length > 1
|
||||
&& this.dependencies.namespaceStore.context.cluster.accessibleNamespaces.length === 0
|
||||
&& this.dependencies.namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns));
|
||||
|
||||
if (isLoadingAll) {
|
||||
return listReleases();
|
||||
@@ -123,13 +127,13 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
.then(items => items.flat());
|
||||
}
|
||||
|
||||
async create(payload: IReleaseCreatePayload) {
|
||||
create = async (payload: IReleaseCreatePayload) => {
|
||||
const response = await createRelease(payload);
|
||||
|
||||
if (this.isLoaded) this.loadFromContextNamespaces();
|
||||
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
||||
const response = await updateRelease(name, namespace, payload);
|
||||
@@ -139,13 +143,13 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
return response;
|
||||
}
|
||||
|
||||
async rollback(name: string, namespace: string, revision: number) {
|
||||
rollback = async (name: string, namespace: string, revision: number) => {
|
||||
const response = await rollbackRelease(name, namespace, revision);
|
||||
|
||||
if (this.isLoaded) this.loadFromContextNamespaces();
|
||||
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
async remove(release: HelmRelease) {
|
||||
return super.removeItem(release, () => deleteRelease(release.getName(), release.getNs()));
|
||||
@@ -155,5 +159,3 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
return Promise.all(this.selectedItems.map(this.remove));
|
||||
}
|
||||
}
|
||||
|
||||
export const releaseStore = new ReleaseStore();
|
||||
@@ -25,7 +25,7 @@ import React, { Component } from "react";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import { releaseStore } from "./release.store";
|
||||
import type { ReleaseStore } from "./release.store";
|
||||
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { ReleaseDetails } from "./release-details";
|
||||
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
|
||||
@@ -36,7 +36,9 @@ import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||
import type { ReleaseRouteParams } from "../../../common/routes";
|
||||
import { releaseURL } from "../../../common/routes";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import releaseStoreInjectable from "./release-store.injectable";
|
||||
import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@@ -52,25 +54,30 @@ enum columnId {
|
||||
interface Props extends RouteComponentProps<ReleaseRouteParams> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
releaseStore: ReleaseStore
|
||||
selectNamespace: (namespace: string) => void
|
||||
}
|
||||
|
||||
@observer
|
||||
export class HelmReleases extends Component<Props> {
|
||||
class NonInjectedHelmReleases extends Component<Dependencies & Props> {
|
||||
componentDidMount() {
|
||||
const { match: { params: { namespace }}} = this.props;
|
||||
|
||||
if (namespace) {
|
||||
namespaceStore.selectNamespaces(namespace);
|
||||
this.props.selectNamespace(namespace);
|
||||
}
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
releaseStore.watchAssociatedSecrets(),
|
||||
releaseStore.watchSelectedNamespaces(),
|
||||
this.props.releaseStore.watchAssociatedSecrets(),
|
||||
this.props.releaseStore.watchSelectedNamespaces(),
|
||||
]);
|
||||
}
|
||||
|
||||
get selectedRelease() {
|
||||
const { match: { params: { name, namespace }}} = this.props;
|
||||
|
||||
return releaseStore.items.find(release => {
|
||||
return this.props.releaseStore.items.find(release => {
|
||||
return release.getName() == name && release.getNs() == namespace;
|
||||
});
|
||||
}
|
||||
@@ -116,7 +123,7 @@ export class HelmReleases extends Component<Props> {
|
||||
isConfigurable
|
||||
tableId="helm_releases"
|
||||
className="HelmReleases"
|
||||
store={releaseStore}
|
||||
store={this.props.releaseStore}
|
||||
dependentStores={[secretsStore]}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: release => release.getName(),
|
||||
@@ -188,3 +195,15 @@ export class HelmReleases extends Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const HelmReleases = withInjectables<Dependencies, Props>(
|
||||
NonInjectedHelmReleases,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
releaseStore: di.inject(releaseStoreInjectable),
|
||||
selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { CatalogEntityStore } from "./catalog-entity.store";
|
||||
import catalogEntityRegistryInjectable from "../../../api/catalog-entity-registry/catalog-entity-registry.injectable";
|
||||
|
||||
const catalogEntityStoreInjectable = getInjectable({
|
||||
instantiate: (di) => new CatalogEntityStore({
|
||||
registry: di.inject(catalogEntityRegistryInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default catalogEntityStoreInjectable;
|
||||
+13
-9
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
|
||||
import { computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { catalogEntityRegistry, CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { ItemStore } from "../../../common/item.store";
|
||||
import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog";
|
||||
import { autoBind, disposer } from "../../../common/utils";
|
||||
import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry";
|
||||
import type { CatalogEntity } from "../../../api/catalog-entity";
|
||||
import { ItemStore } from "../../../../common/item.store";
|
||||
import { CatalogCategory, catalogCategoryRegistry } from "../../../../common/catalog";
|
||||
import { autoBind, disposer } from "../../../../common/utils";
|
||||
|
||||
interface Dependencies {
|
||||
registry: CatalogEntityRegistry
|
||||
}
|
||||
|
||||
export class CatalogEntityStore extends ItemStore<CatalogEntity> {
|
||||
constructor(private registry: CatalogEntityRegistry = catalogEntityRegistry) {
|
||||
constructor(private dependencies: Dependencies) {
|
||||
super();
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
@@ -38,10 +42,10 @@ export class CatalogEntityStore extends ItemStore<CatalogEntity> {
|
||||
|
||||
@computed get entities() {
|
||||
if (!this.activeCategory) {
|
||||
return this.registry.filteredItems;
|
||||
return this.dependencies.registry.filteredItems;
|
||||
}
|
||||
|
||||
return this.registry.getItemsForCategory(this.activeCategory, { filtered: true });
|
||||
return this.dependencies.registry.getItemsForCategory(this.activeCategory, { filtered: true });
|
||||
}
|
||||
|
||||
@computed get selectedItem() {
|
||||
@@ -69,6 +73,6 @@ export class CatalogEntityStore extends ItemStore<CatalogEntity> {
|
||||
}
|
||||
|
||||
onRun(entity: CatalogEntity): void {
|
||||
this.registry.onRun(entity);
|
||||
this.dependencies.registry.onRun(entity);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { browseCatalogTab } from "../../../../common/routes";
|
||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||
|
||||
const catalogPreviousActiveTabStorageInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const createStorage = di.inject(createStorageInjectable);
|
||||
|
||||
return createStorage(
|
||||
"catalog-previous-active-tab",
|
||||
browseCatalogTab,
|
||||
);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default catalogPreviousActiveTabStorageInjectable;
|
||||
@@ -20,17 +20,27 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/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 { kubernetesClusterCategory } from "../../../common/catalog-entities/kubernetes-cluster";
|
||||
import { catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntity, CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog";
|
||||
import { CatalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry";
|
||||
import { CatalogCategoryRegistry, CatalogEntity, CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog";
|
||||
import { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
|
||||
import { CatalogEntityStore } from "./catalog-entity.store";
|
||||
import { AppPaths } from "../../../common/app-paths";
|
||||
import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import type { DependencyInjectionContainer } 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 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";
|
||||
|
||||
mockWindow();
|
||||
jest.mock("electron", () => ({
|
||||
@@ -49,8 +59,6 @@ jest.mock("electron", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
AppPaths.init();
|
||||
|
||||
jest.mock("./hotbar-toggle-menu-item", () => ({
|
||||
HotbarToggleMenuItem: () => <div>menu item</div>,
|
||||
}));
|
||||
@@ -103,31 +111,46 @@ describe("<Catalog />", () => {
|
||||
}, onRun);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
CatalogEntityDetailRegistry.createInstance();
|
||||
// mock the return of getting CatalogCategoryRegistry.filteredItems
|
||||
jest
|
||||
.spyOn(catalogCategoryRegistry, "filteredItems", "get")
|
||||
.mockImplementation(() => {
|
||||
return [kubernetesClusterCategory];
|
||||
});
|
||||
let di: DependencyInjectionContainer;
|
||||
let catalogEntityStore: CatalogEntityStore;
|
||||
let catalogEntityRegistry: CatalogEntityRegistry;
|
||||
let render: DiRender;
|
||||
|
||||
// we don't care what this.renderList renders in this test case.
|
||||
jest.spyOn(Catalog.prototype, "renderList").mockImplementation(() => {
|
||||
return <span>empty renderList</span>;
|
||||
});
|
||||
beforeEach(async () => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
mockFs();
|
||||
|
||||
UserStore.createInstance();
|
||||
ThemeStore.createInstance();
|
||||
CatalogEntityDetailRegistry.createInstance();
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
|
||||
catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
|
||||
di.override(catalogEntityRegistryInjectable, () => catalogEntityRegistry);
|
||||
|
||||
catalogEntityStore = di.inject(catalogEntityStoreInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
UserStore.resetInstance();
|
||||
ThemeStore.resetInstance();
|
||||
CatalogEntityDetailRegistry.resetInstance();
|
||||
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", (done) => {
|
||||
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
@@ -153,7 +176,6 @@ describe("<Catalog />", () => {
|
||||
history={history}
|
||||
location={mockLocation}
|
||||
match={mockMatch}
|
||||
catalogEntityStore={catalogEntityStore}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -161,9 +183,6 @@ describe("<Catalog />", () => {
|
||||
});
|
||||
|
||||
it("onBeforeRun prevents event => onRun wont be triggered", (done) => {
|
||||
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
@@ -187,7 +206,6 @@ describe("<Catalog />", () => {
|
||||
history={history}
|
||||
location={mockLocation}
|
||||
match={mockMatch}
|
||||
catalogEntityStore={catalogEntityStore}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -195,9 +213,6 @@ describe("<Catalog />", () => {
|
||||
});
|
||||
|
||||
it("addOnBeforeRun throw an exception => onRun will be triggered", (done) => {
|
||||
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
@@ -222,7 +237,6 @@ describe("<Catalog />", () => {
|
||||
history={history}
|
||||
location={mockLocation}
|
||||
match={mockMatch}
|
||||
catalogEntityStore={catalogEntityStore}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -230,9 +244,6 @@ describe("<Catalog />", () => {
|
||||
});
|
||||
|
||||
it("addOnRunHook return a promise and does not prevent run event => onRun()", (done) => {
|
||||
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn(() => done());
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
@@ -252,7 +263,6 @@ describe("<Catalog />", () => {
|
||||
history={history}
|
||||
location={mockLocation}
|
||||
match={mockMatch}
|
||||
catalogEntityStore={catalogEntityStore}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -260,9 +270,6 @@ describe("<Catalog />", () => {
|
||||
});
|
||||
|
||||
it("addOnRunHook return a promise and prevents event wont be triggered", (done) => {
|
||||
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
@@ -289,7 +296,6 @@ describe("<Catalog />", () => {
|
||||
history={history}
|
||||
location={mockLocation}
|
||||
match={mockMatch}
|
||||
catalogEntityStore={catalogEntityStore}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -297,9 +303,6 @@ describe("<Catalog />", () => {
|
||||
});
|
||||
|
||||
it("addOnRunHook return a promise and reject => onRun will be triggered", (done) => {
|
||||
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
|
||||
@@ -324,7 +327,6 @@ describe("<Catalog />", () => {
|
||||
history={history}
|
||||
location={mockLocation}
|
||||
match={mockMatch}
|
||||
catalogEntityStore={catalogEntityStore}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { ItemListLayout } from "../item-object-list";
|
||||
import { action, makeObservable, observable, reaction, runInAction, when } from "mobx";
|
||||
import { CatalogEntityStore } from "./catalog-entity.store";
|
||||
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";
|
||||
@@ -36,7 +36,7 @@ import { CatalogAddButton } from "./catalog-add-button";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import { Notifications } from "../notifications";
|
||||
import { MainLayout } from "../layout/main-layout";
|
||||
import { createStorage, prevDefault } from "../../utils";
|
||||
import { prevDefault } from "../../utils";
|
||||
import { CatalogEntityDetails } from "./catalog-entity-details";
|
||||
import { browseCatalogTab, catalogURL, CatalogViewRouteParam } from "../../../common/routes";
|
||||
import { CatalogMenu } from "./catalog-menu";
|
||||
@@ -46,8 +46,10 @@ import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
|
||||
import { Avatar } from "../avatar";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { getLabelBadges } from "./helpers";
|
||||
|
||||
export const previousActiveTab = createStorage("catalog-previous-active-tab", browseCatalogTab);
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import catalogPreviousActiveTabStorageInjectable
|
||||
from "./catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
|
||||
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
@@ -56,26 +58,23 @@ enum sortBy {
|
||||
status = "status",
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<CatalogViewRouteParam> {
|
||||
catalogEntityStore?: CatalogEntityStore;
|
||||
interface Props extends RouteComponentProps<CatalogViewRouteParam> {}
|
||||
|
||||
interface Dependencies {
|
||||
catalogPreviousActiveTabStorage: { set: (value: string ) => void }
|
||||
catalogEntityStore: CatalogEntityStore
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Catalog extends React.Component<Props> {
|
||||
@observable private catalogEntityStore?: CatalogEntityStore;
|
||||
class NonInjectedCatalog extends React.Component<Props & Dependencies> {
|
||||
@observable private contextMenu: CatalogEntityContextMenuContext;
|
||||
@observable activeTab?: string;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
this.catalogEntityStore = props.catalogEntityStore;
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
catalogEntityStore: new CatalogEntityStore(),
|
||||
};
|
||||
|
||||
|
||||
get routeActiveTab(): string {
|
||||
const { group, kind } = this.props.match.params ?? {};
|
||||
|
||||
@@ -92,9 +91,9 @@ export class Catalog extends React.Component<Props> {
|
||||
navigate: (url: string) => navigate(url),
|
||||
};
|
||||
disposeOnUnmount(this, [
|
||||
this.catalogEntityStore.watch(),
|
||||
this.props.catalogEntityStore.watch(),
|
||||
reaction(() => this.routeActiveTab, async (routeTab) => {
|
||||
previousActiveTab.set(this.routeActiveTab);
|
||||
this.props.catalogPreviousActiveTabStorage.set(this.routeActiveTab);
|
||||
|
||||
try {
|
||||
await when(() => (routeTab === browseCatalogTab || !!catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab)), { timeout: 5_000 }); // we need to wait because extensions might take a while to load
|
||||
@@ -102,7 +101,7 @@ export class Catalog extends React.Component<Props> {
|
||||
|
||||
runInAction(() => {
|
||||
this.activeTab = routeTab;
|
||||
this.catalogEntityStore.activeCategory = item;
|
||||
this.props.catalogEntityStore.activeCategory = item;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -113,13 +112,13 @@ export class Catalog extends React.Component<Props> {
|
||||
|
||||
// If active category is filtered out, automatically switch to the first category
|
||||
disposeOnUnmount(this, reaction(() => catalogCategoryRegistry.filteredItems, () => {
|
||||
if (!catalogCategoryRegistry.filteredItems.find(item => item.getId() === this.catalogEntityStore.activeCategory.getId())) {
|
||||
if (!catalogCategoryRegistry.filteredItems.find(item => item.getId() === this.props.catalogEntityStore.activeCategory.getId())) {
|
||||
const item = catalogCategoryRegistry.filteredItems[0];
|
||||
|
||||
runInAction(() => {
|
||||
if (item) {
|
||||
this.activeTab = item.getId();
|
||||
this.catalogEntityStore.activeCategory = item;
|
||||
this.props.catalogEntityStore.activeCategory = item;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -135,10 +134,10 @@ export class Catalog extends React.Component<Props> {
|
||||
}
|
||||
|
||||
onDetails = (entity: CatalogEntity) => {
|
||||
if (this.catalogEntityStore.selectedItemId) {
|
||||
this.catalogEntityStore.selectedItemId = null;
|
||||
if (this.props.catalogEntityStore.selectedItemId) {
|
||||
this.props.catalogEntityStore.selectedItemId = null;
|
||||
} else {
|
||||
this.catalogEntityStore.onRun(entity);
|
||||
this.props.catalogEntityStore.onRun(entity);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -189,7 +188,7 @@ export class Catalog extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<MenuActions onOpen={onOpen}>
|
||||
<MenuItem key="open-details" onClick={() => this.catalogEntityStore.selectedItemId = entity.getId()}>
|
||||
<MenuItem key="open-details" onClick={() => this.props.catalogEntityStore.selectedItemId = entity.getId()}>
|
||||
View Details
|
||||
</MenuItem>
|
||||
{
|
||||
@@ -238,7 +237,7 @@ export class Catalog extends React.Component<Props> {
|
||||
}
|
||||
|
||||
renderList() {
|
||||
const { activeCategory } = this.catalogEntityStore;
|
||||
const { activeCategory } = this.props.catalogEntityStore;
|
||||
const tableId = activeCategory ? `catalog-items-${activeCategory.metadata.name.replace(" ", "")}` : "catalog-items";
|
||||
|
||||
if (this.activeTab === undefined) {
|
||||
@@ -252,7 +251,7 @@ export class Catalog extends React.Component<Props> {
|
||||
renderHeaderTitle={activeCategory?.metadata.name || "Browse All"}
|
||||
isSelectable={false}
|
||||
isConfigurable={true}
|
||||
store={this.catalogEntityStore}
|
||||
store={this.props.catalogEntityStore}
|
||||
sortingCallbacks={{
|
||||
[sortBy.name]: entity => entity.getName(),
|
||||
[sortBy.source]: entity => entity.getSource(),
|
||||
@@ -292,11 +291,11 @@ export class Catalog extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.catalogEntityStore) {
|
||||
if (!this.props.catalogEntityStore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedEntity = this.catalogEntityStore.selectedItem;
|
||||
const selectedEntity = this.props.catalogEntityStore.selectedItem;
|
||||
|
||||
return (
|
||||
<MainLayout sidebar={this.renderNavigation()}>
|
||||
@@ -307,13 +306,13 @@ export class Catalog extends React.Component<Props> {
|
||||
selectedEntity
|
||||
? <CatalogEntityDetails
|
||||
entity={selectedEntity}
|
||||
hideDetails={() => this.catalogEntityStore.selectedItemId = null}
|
||||
onRun={() => this.catalogEntityStore.onRun(selectedEntity)}
|
||||
hideDetails={() => this.props.catalogEntityStore.selectedItemId = null}
|
||||
onRun={() => this.props.catalogEntityStore.onRun(selectedEntity)}
|
||||
/>
|
||||
: (
|
||||
<RenderDelay>
|
||||
<CatalogAddButton
|
||||
category={this.catalogEntityStore.activeCategory}
|
||||
category={this.props.catalogEntityStore.activeCategory}
|
||||
/>
|
||||
</RenderDelay>
|
||||
)
|
||||
@@ -322,3 +321,18 @@ export class Catalog extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const Catalog = withInjectables<Dependencies, Props>(
|
||||
NonInjectedCatalog,
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
catalogEntityStore: di.inject(catalogEntityStoreInjectable),
|
||||
|
||||
catalogPreviousActiveTabStorage: di.inject(
|
||||
catalogPreviousActiveTabStorageInjectable,
|
||||
),
|
||||
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -24,10 +24,15 @@ import { observer } from "mobx-react";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Radio, RadioGroup } from "../radio";
|
||||
import { clusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview.store";
|
||||
import { ClusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview-store/cluster-overview-store";
|
||||
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
|
||||
export const ClusterMetricSwitchers = observer(() => {
|
||||
const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterOverviewStore;
|
||||
interface Dependencies {
|
||||
clusterOverviewStore: ClusterOverviewStore
|
||||
}
|
||||
|
||||
const NonInjectedClusterMetricSwitchers = observer(({ clusterOverviewStore: { metricType, metricNodeRole, getMetricsValues, metrics }}: Dependencies) => {
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
const metricsValues = getMetricsValues(metrics);
|
||||
const disableRoles = !masterNodes.length || !workerNodes.length;
|
||||
@@ -40,7 +45,7 @@ export const ClusterMetricSwitchers = observer(() => {
|
||||
asButtons
|
||||
className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })}
|
||||
value={metricNodeRole}
|
||||
onChange={(metric: MetricNodeRole) => clusterOverviewStore.metricNodeRole = metric}
|
||||
onChange={(metric: MetricNodeRole) => metricNodeRole = metric}
|
||||
>
|
||||
<Radio label="Master" value={MetricNodeRole.MASTER}/>
|
||||
<Radio label="Worker" value={MetricNodeRole.WORKER}/>
|
||||
@@ -51,7 +56,7 @@ export const ClusterMetricSwitchers = observer(() => {
|
||||
asButtons
|
||||
className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })}
|
||||
value={metricType}
|
||||
onChange={(value: MetricType) => clusterOverviewStore.metricType = value}
|
||||
onChange={(value: MetricType) => metricType = value}
|
||||
>
|
||||
<Radio label="CPU" value={MetricType.CPU}/>
|
||||
<Radio label="Memory" value={MetricType.MEMORY}/>
|
||||
@@ -60,3 +65,14 @@ export const ClusterMetricSwitchers = observer(() => {
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const ClusterMetricSwitchers = withInjectables<Dependencies>(
|
||||
NonInjectedClusterMetricSwitchers,
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ import styles from "./cluster-metrics.module.scss";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { ChartOptions, ChartPoint } from "chart.js";
|
||||
import { clusterOverviewStore, MetricType } from "./cluster-overview.store";
|
||||
import { ClusterOverviewStore, MetricType } from "./cluster-overview-store/cluster-overview-store";
|
||||
import { BarChart } from "../chart";
|
||||
import { bytesToUnits, cssNames } from "../../utils";
|
||||
import { Spinner } from "../spinner";
|
||||
@@ -32,10 +32,16 @@ import { ZebraStripes } from "../chart/zebra-stripes.plugin";
|
||||
import { ClusterNoMetrics } from "./cluster-no-metrics";
|
||||
import { ClusterMetricSwitchers } from "./cluster-metric-switchers";
|
||||
import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import clusterOverviewStoreInjectable
|
||||
from "./cluster-overview-store/cluster-overview-store.injectable";
|
||||
|
||||
export const ClusterMetrics = observer(() => {
|
||||
const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = clusterOverviewStore;
|
||||
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterOverviewStore.metrics);
|
||||
interface Dependencies {
|
||||
clusterOverviewStore: ClusterOverviewStore
|
||||
}
|
||||
|
||||
const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics }}: Dependencies) => {
|
||||
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(metrics);
|
||||
const metricValues = getMetricsValues(metrics);
|
||||
const colors = { cpu: "#3D90CE", memory: "#C93DCE" };
|
||||
const data = metricValues.map(value => ({
|
||||
@@ -118,3 +124,13 @@ export const ClusterMetrics = observer(() => {
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const ClusterMetrics = withInjectables<Dependencies>(
|
||||
NonInjectedClusterMetrics,
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import {
|
||||
ClusterOverviewStorageState,
|
||||
ClusterOverviewStore,
|
||||
MetricNodeRole,
|
||||
MetricType,
|
||||
} from "./cluster-overview-store";
|
||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||
import apiManagerInjectable from "../../kube-object-menu/dependencies/api-manager.injectable";
|
||||
|
||||
const clusterOverviewStoreInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const createStorage = di.inject(createStorageInjectable);
|
||||
|
||||
const storage = createStorage<ClusterOverviewStorageState>(
|
||||
"cluster_overview",
|
||||
{
|
||||
metricType: MetricType.CPU, // setup defaults
|
||||
metricNodeRole: MetricNodeRole.WORKER,
|
||||
},
|
||||
);
|
||||
|
||||
const store = new ClusterOverviewStore({
|
||||
storage,
|
||||
});
|
||||
|
||||
const apiManager = di.inject(apiManagerInjectable);
|
||||
|
||||
apiManager.registerStore(store);
|
||||
|
||||
return store;
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default clusterOverviewStoreInjectable;
|
||||
+15
-20
@@ -20,12 +20,11 @@
|
||||
*/
|
||||
|
||||
import { action, observable, reaction, when, makeObservable } from "mobx";
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import { Cluster, clusterApi, getMetricsByNodeNames, IClusterMetrics } from "../../../common/k8s-api/endpoints";
|
||||
import { autoBind, createStorage } from "../../utils";
|
||||
import { IMetricsReqParams, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
|
||||
import { Cluster, clusterApi, getMetricsByNodeNames, IClusterMetrics } from "../../../../common/k8s-api/endpoints";
|
||||
import { autoBind, StorageHelper } from "../../../utils";
|
||||
import { IMetricsReqParams, normalizeMetrics } from "../../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { nodesStore } from "../../+nodes/nodes.store";
|
||||
|
||||
export enum MetricType {
|
||||
MEMORY = "memory",
|
||||
@@ -42,34 +41,33 @@ export interface ClusterOverviewStorageState {
|
||||
metricNodeRole: MetricNodeRole,
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
storage: StorageHelper<ClusterOverviewStorageState>
|
||||
}
|
||||
|
||||
export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements ClusterOverviewStorageState {
|
||||
api = clusterApi;
|
||||
|
||||
@observable metrics: Partial<IClusterMetrics> = {};
|
||||
@observable metricsLoaded = false;
|
||||
|
||||
private storage = createStorage<ClusterOverviewStorageState>("cluster_overview", {
|
||||
metricType: MetricType.CPU, // setup defaults
|
||||
metricNodeRole: MetricNodeRole.WORKER,
|
||||
});
|
||||
|
||||
get metricType(): MetricType {
|
||||
return this.storage.get().metricType;
|
||||
return this.dependencies.storage.get().metricType;
|
||||
}
|
||||
|
||||
set metricType(value: MetricType) {
|
||||
this.storage.merge({ metricType: value });
|
||||
this.dependencies.storage.merge({ metricType: value });
|
||||
}
|
||||
|
||||
get metricNodeRole(): MetricNodeRole {
|
||||
return this.storage.get().metricNodeRole;
|
||||
return this.dependencies.storage.get().metricNodeRole;
|
||||
}
|
||||
|
||||
set metricNodeRole(value: MetricNodeRole) {
|
||||
this.storage.merge({ metricNodeRole: value });
|
||||
this.dependencies.storage.merge({ metricNodeRole: value });
|
||||
}
|
||||
|
||||
constructor() {
|
||||
constructor(private dependencies: Dependencies ) {
|
||||
super();
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
@@ -125,9 +123,6 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
|
||||
reset() {
|
||||
super.reset();
|
||||
this.resetMetrics();
|
||||
this.storage?.reset();
|
||||
this.dependencies.storage?.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export const clusterOverviewStore = new ClusterOverviewStore();
|
||||
apiManager.registerStore(clusterOverviewStore);
|
||||
@@ -26,28 +26,37 @@ import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { getHostedClusterId, interval } from "../../utils";
|
||||
import { Disposer, getHostedClusterId, interval } from "../../utils";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { Spinner } from "../spinner";
|
||||
import { ClusterIssues } from "./cluster-issues";
|
||||
import { ClusterMetrics } from "./cluster-metrics";
|
||||
import { clusterOverviewStore } from "./cluster-overview.store";
|
||||
import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store";
|
||||
import { ClusterPieCharts } from "./cluster-pie-charts";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import { eventStore } from "../+events/event.store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer,
|
||||
clusterOverviewStore: ClusterOverviewStore
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterOverview extends React.Component {
|
||||
class NonInjectedClusterOverview extends React.Component<Dependencies> {
|
||||
private metricPoller = interval(60, () => this.loadMetrics());
|
||||
|
||||
loadMetrics() {
|
||||
const cluster = ClusterStore.getInstance().getById(getHostedClusterId());
|
||||
|
||||
if (cluster.available) {
|
||||
clusterOverviewStore.loadMetrics();
|
||||
this.props.clusterOverviewStore.loadMetrics();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,13 +64,14 @@ export class ClusterOverview extends React.Component {
|
||||
this.metricPoller.start(true);
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
kubeWatchApi.subscribeStores([
|
||||
this.props.subscribeStores([
|
||||
podsStore,
|
||||
eventStore,
|
||||
nodesStore,
|
||||
]),
|
||||
|
||||
reaction(
|
||||
() => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
|
||||
() => this.props.clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
|
||||
() => this.metricPoller.restart(true),
|
||||
),
|
||||
]);
|
||||
@@ -110,3 +120,14 @@ export class ClusterOverview extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ClusterOverview = withInjectables<Dependencies>(
|
||||
NonInjectedClusterOverview,
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -23,7 +23,7 @@ import styles from "./cluster-pie-charts.module.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { clusterOverviewStore, MetricNodeRole } from "./cluster-overview.store";
|
||||
import { ClusterOverviewStore, MetricNodeRole } from "./cluster-overview-store/cluster-overview-store";
|
||||
import { Spinner } from "../spinner";
|
||||
import { Icon } from "../icon";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
@@ -32,12 +32,18 @@ import { ClusterNoMetrics } from "./cluster-no-metrics";
|
||||
import { bytesToUnits, cssNames } from "../../utils";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
|
||||
|
||||
function createLabels(rawLabelData: [string, number | undefined][]): string[] {
|
||||
return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`);
|
||||
}
|
||||
|
||||
export const ClusterPieCharts = observer(() => {
|
||||
interface Dependencies {
|
||||
clusterOverviewStore: ClusterOverviewStore
|
||||
}
|
||||
|
||||
const NonInjectedClusterPieCharts = observer(({ clusterOverviewStore }: Dependencies) => {
|
||||
const renderLimitWarning = () => {
|
||||
return (
|
||||
<div className="node-warning flex gaps align-center">
|
||||
@@ -213,9 +219,8 @@ export const ClusterPieCharts = observer(() => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
const renderContent = ({ metricNodeRole, metricsLoaded }: ClusterOverviewStore) => {
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
const { metricNodeRole, metricsLoaded } = clusterOverviewStore;
|
||||
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
|
||||
|
||||
if (!nodes.length) {
|
||||
@@ -245,7 +250,17 @@ export const ClusterPieCharts = observer(() => {
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
{renderContent()}
|
||||
{renderContent(clusterOverviewStore)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const ClusterPieCharts = withInjectables<Dependencies>(
|
||||
NonInjectedClusterPieCharts,
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -25,21 +25,28 @@ import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { cssNames } from "../../utils";
|
||||
import { cssNames, Disposer } from "../../utils";
|
||||
import { LocaleDate } from "../locale-date";
|
||||
import { eventStore } from "./event.store";
|
||||
import logger from "../../../common/logger";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable
|
||||
from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
|
||||
export interface KubeEventDetailsProps {
|
||||
object: KubeObject;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
||||
}
|
||||
|
||||
@observer
|
||||
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||
class NonInjectedKubeEventDetails extends React.Component<KubeEventDetailsProps & Dependencies> {
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
kubeWatchApi.subscribeStores([
|
||||
this.props.subscribeStores([
|
||||
eventStore,
|
||||
]),
|
||||
]);
|
||||
@@ -102,3 +109,17 @@ export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const KubeEventDetails = withInjectables<Dependencies, KubeEventDetailsProps>(
|
||||
NonInjectedKubeEventDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -30,13 +30,13 @@ import { ConfirmDialog } from "../../confirm-dialog";
|
||||
import { Extensions } from "../extensions";
|
||||
import mockFs from "mock-fs";
|
||||
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
||||
import { AppPaths } from "../../../../common/app-paths";
|
||||
import extensionLoaderInjectable
|
||||
from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||
import extensionDiscoveryInjectable
|
||||
from "../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
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";
|
||||
|
||||
mockWindow();
|
||||
|
||||
@@ -59,42 +59,28 @@ jest.mock("../../../../common/utils/downloadFile", () => ({
|
||||
|
||||
jest.mock("../../../../common/utils/tar");
|
||||
|
||||
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(),
|
||||
},
|
||||
}));
|
||||
|
||||
AppPaths.init();
|
||||
|
||||
describe("Extensions", () => {
|
||||
let extensionLoader: ExtensionLoader;
|
||||
let extensionDiscovery: ExtensionDiscovery;
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting();
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
di.override(directoryForDownloadsInjectable, () => "some-directory-for-downloads");
|
||||
|
||||
mockFs({
|
||||
"some-directory-for-user-data": {},
|
||||
});
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
|
||||
mockFs({
|
||||
"tmp": {},
|
||||
});
|
||||
|
||||
extensionLoader.addExtension({
|
||||
id: "extensionId",
|
||||
manifest: {
|
||||
|
||||
+2
@@ -21,11 +21,13 @@
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { installFromSelectFileDialog } from "./install-from-select-file-dialog";
|
||||
import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable";
|
||||
import directoryForDownloadsInjectable from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable";
|
||||
|
||||
const installFromSelectFileDialogInjectable = getInjectable({
|
||||
instantiate: (di) =>
|
||||
installFromSelectFileDialog({
|
||||
attemptInstalls: di.inject(attemptInstallsInjectable),
|
||||
directoryForDownloads: di.inject(directoryForDownloadsInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
|
||||
+3
-3
@@ -19,18 +19,18 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { dialog } from "../../../remote-helpers";
|
||||
import { AppPaths } from "../../../../common/app-paths";
|
||||
import { supportedExtensionFormats } from "../supported-extension-formats";
|
||||
|
||||
interface Dependencies {
|
||||
attemptInstalls: (filePaths: string[]) => Promise<void>
|
||||
directoryForDownloads: string
|
||||
}
|
||||
|
||||
export const installFromSelectFileDialog =
|
||||
({ attemptInstalls }: Dependencies) =>
|
||||
({ attemptInstalls, directoryForDownloads }: Dependencies) =>
|
||||
async () => {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
defaultPath: AppPaths.get("downloads"),
|
||||
defaultPath: directoryForDownloads,
|
||||
properties: ["openFile", "multiSelections"],
|
||||
message: `Select extensions to install (formats: ${supportedExtensionFormats.join(
|
||||
", ",
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { AddNamespaceDialogModel } from "./add-namespace-dialog-model";
|
||||
|
||||
const addNamespaceDialogModelInjectable = getInjectable({
|
||||
instantiate: () => new AddNamespaceDialogModel(),
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default addNamespaceDialogModelInjectable;
|
||||
+16
-9
@@ -18,17 +18,24 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { observable, makeObservable, action } from "mobx";
|
||||
|
||||
import { useState } from "react";
|
||||
import { createStorage } from "../utils";
|
||||
export class AddNamespaceDialogModel {
|
||||
isOpen = false;
|
||||
|
||||
export function useStorage<T>(key: string, initialValue: T) {
|
||||
const storage = createStorage(key, initialValue);
|
||||
const [storageValue, setStorageValue] = useState(storage.get());
|
||||
const setValue = (value: T) => {
|
||||
setStorageValue(value);
|
||||
storage.set(value);
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
isOpen: observable,
|
||||
open: action,
|
||||
close: action,
|
||||
});
|
||||
}
|
||||
|
||||
open = () => {
|
||||
this.isOpen = true;
|
||||
};
|
||||
|
||||
return [storageValue, setValue] as [T, (value: T) => void];
|
||||
close = () => {
|
||||
this.isOpen = false;
|
||||
};
|
||||
}
|
||||
@@ -26,38 +26,35 @@ import { observable, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { namespaceStore } from "./namespace.store";
|
||||
import type { Namespace } from "../../../common/k8s-api/endpoints";
|
||||
import { Input } from "../input";
|
||||
import { systemName } from "../input/input_validators";
|
||||
import { Notifications } from "../notifications";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable";
|
||||
import type { AddNamespaceDialogModel } from "./add-namespace-dialog-model/add-namespace-dialog-model";
|
||||
import addNamespaceDialogModelInjectable
|
||||
from "./add-namespace-dialog-model/add-namespace-dialog-model.injectable";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
onSuccess?(ns: Namespace): void;
|
||||
onError?(error: any): void;
|
||||
}
|
||||
|
||||
const dialogState = observable.object({
|
||||
isOpen: false,
|
||||
});
|
||||
interface Dependencies {
|
||||
createNamespace: (params: { name: string }) => Promise<Namespace>,
|
||||
model: AddNamespaceDialogModel
|
||||
}
|
||||
|
||||
@observer
|
||||
export class AddNamespaceDialog extends React.Component<Props> {
|
||||
class NonInjectedAddNamespaceDialog extends React.Component<Props & Dependencies> {
|
||||
@observable namespace = "";
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
static open() {
|
||||
dialogState.isOpen = true;
|
||||
}
|
||||
|
||||
static close() {
|
||||
dialogState.isOpen = false;
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
this.namespace = "";
|
||||
};
|
||||
@@ -67,10 +64,10 @@ export class AddNamespaceDialog extends React.Component<Props> {
|
||||
const { onSuccess, onError } = this.props;
|
||||
|
||||
try {
|
||||
const created = await namespaceStore.create({ name: namespace });
|
||||
const created = await this.props.createNamespace({ name: namespace });
|
||||
|
||||
onSuccess?.(created);
|
||||
AddNamespaceDialog.close();
|
||||
this.props.close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
onError?.(err);
|
||||
@@ -78,7 +75,7 @@ export class AddNamespaceDialog extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { ...dialogProps } = this.props;
|
||||
const { model, createNamespace, ...dialogProps } = this.props;
|
||||
const { namespace } = this;
|
||||
const header = <h5>Create Namespace</h5>;
|
||||
|
||||
@@ -86,11 +83,11 @@ export class AddNamespaceDialog extends React.Component<Props> {
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="AddNamespaceDialog"
|
||||
isOpen={dialogState.isOpen}
|
||||
isOpen={this.props.model.isOpen}
|
||||
onOpen={this.reset}
|
||||
close={AddNamespaceDialog.close}
|
||||
close={this.props.model.close}
|
||||
>
|
||||
<Wizard header={header} done={AddNamespaceDialog.close}>
|
||||
<Wizard header={header} done={this.props.model.close}>
|
||||
<WizardStep
|
||||
contentClass="flex gaps column"
|
||||
nextLabel="Create"
|
||||
@@ -110,3 +107,16 @@ export class AddNamespaceDialog extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const AddNamespaceDialog = withInjectables<Dependencies, Props>(
|
||||
NonInjectedAddNamespaceDialog,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
createNamespace: di.inject(namespaceStoreInjectable).create,
|
||||
model: di.inject(addNamespaceDialogModelInjectable),
|
||||
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -25,7 +25,7 @@ import React from "react";
|
||||
import { computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { DrawerItem } from "../drawer";
|
||||
import { boundMethod, cssNames } from "../../utils";
|
||||
import { boundMethod, cssNames, Disposer } from "../../utils";
|
||||
import { getMetricsForNamespace, IPodMetrics, Namespace } from "../../../common/k8s-api/endpoints";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -39,16 +39,24 @@ import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import logger from "../../../common/logger";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable
|
||||
from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Namespace> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
||||
}
|
||||
|
||||
@observer
|
||||
export class NamespaceDetails extends React.Component<Props> {
|
||||
class NonInjectedNamespaceDetails extends React.Component<Props & Dependencies> {
|
||||
@observable metrics: IPodMetrics = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -58,7 +66,8 @@ export class NamespaceDetails extends React.Component<Props> {
|
||||
reaction(() => this.props.object, () => {
|
||||
this.metrics = null;
|
||||
}),
|
||||
kubeWatchApi.subscribeStores([
|
||||
|
||||
this.props.subscribeStores([
|
||||
resourceQuotaStore,
|
||||
limitRangeStore,
|
||||
]),
|
||||
@@ -138,3 +147,15 @@ export class NamespaceDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const NamespaceDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedNamespaceDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { NamespaceSelectFilterModel } from "./namespace-select-filter-model";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
|
||||
const NamespaceSelectFilterModelInjectable = getInjectable({
|
||||
instantiate: () => new NamespaceSelectFilterModel(),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default NamespaceSelectFilterModelInjectable;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { observable, makeObservable, action } from "mobx";
|
||||
|
||||
export class NamespaceSelectFilterModel {
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
menuIsOpen: observable,
|
||||
closeMenu: action,
|
||||
openMenu: action,
|
||||
toggleMenu: action,
|
||||
isMultiSelection: observable,
|
||||
setIsMultiSelection: action,
|
||||
});
|
||||
}
|
||||
|
||||
menuIsOpen = false;
|
||||
|
||||
closeMenu = () => {
|
||||
this.menuIsOpen = false;
|
||||
};
|
||||
|
||||
openMenu = () => {
|
||||
this.menuIsOpen = true;
|
||||
};
|
||||
|
||||
toggleMenu = () => {
|
||||
this.menuIsOpen = !this.menuIsOpen;
|
||||
};
|
||||
|
||||
isMultiSelection = false;
|
||||
|
||||
setIsMultiSelection = (isMultiSelection: boolean) => {
|
||||
this.isMultiSelection = isMultiSelection;
|
||||
};
|
||||
}
|
||||
@@ -24,41 +24,26 @@ import "./namespace-select-filter.scss";
|
||||
import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { components, PlaceholderProps } from "react-select";
|
||||
import { action, computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { action, makeObservable, observable, reaction } from "mobx";
|
||||
|
||||
import { Icon } from "../icon";
|
||||
import { NamespaceSelect } from "./namespace-select";
|
||||
import { namespaceStore } from "./namespace.store";
|
||||
import type { NamespaceStore } from "./namespace-store/namespace.store";
|
||||
|
||||
import type { SelectOption, SelectProps } from "../select";
|
||||
import { isMac } from "../../../common/vars";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable";
|
||||
import type { NamespaceSelectFilterModel } from "./namespace-select-filter-model/namespace-select-filter-model";
|
||||
import namespaceSelectFilterModelInjectable from "./namespace-select-filter-model/namespace-select-filter-model.injectable";
|
||||
|
||||
const Placeholder = observer((props: PlaceholderProps<any, boolean>) => {
|
||||
const getPlaceholder = (): React.ReactNode => {
|
||||
const namespaces = namespaceStore.contextNamespaces;
|
||||
|
||||
if (namespaceStore.areAllSelectedImplicitly || !namespaces.length) {
|
||||
return <>All namespaces</>;
|
||||
}
|
||||
|
||||
if (namespaces.length === 1) {
|
||||
return <>Namespace: {namespaces[0]}</>;
|
||||
}
|
||||
|
||||
return <>Namespaces: {namespaces.join(", ")}</>;
|
||||
};
|
||||
|
||||
return (
|
||||
<components.Placeholder {...props}>
|
||||
{getPlaceholder()}
|
||||
</components.Placeholder>
|
||||
);
|
||||
});
|
||||
interface Dependencies {
|
||||
model: NamespaceSelectFilterModel,
|
||||
namespaceStore: NamespaceStore
|
||||
}
|
||||
|
||||
@observer
|
||||
export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
||||
static isMultiSelection = observable.box(false);
|
||||
static isMenuOpen = observable.box(false);
|
||||
class NonInjectedNamespaceSelectFilter extends React.Component<SelectProps & Dependencies> {
|
||||
|
||||
/**
|
||||
* Only updated on every open
|
||||
@@ -66,32 +51,20 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
||||
private selected = observable.set<string>();
|
||||
private didToggle = false;
|
||||
|
||||
constructor(props: SelectProps) {
|
||||
constructor(props: SelectProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@computed get isMultiSelection() {
|
||||
return NamespaceSelectFilter.isMultiSelection.get();
|
||||
}
|
||||
|
||||
set isMultiSelection(val: boolean) {
|
||||
NamespaceSelectFilter.isMultiSelection.set(val);
|
||||
}
|
||||
|
||||
@computed get isMenuOpen() {
|
||||
return NamespaceSelectFilter.isMenuOpen.get();
|
||||
}
|
||||
|
||||
set isMenuOpen(val: boolean) {
|
||||
NamespaceSelectFilter.isMenuOpen.set(val);
|
||||
get model() {
|
||||
return this.props.model;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.isMenuOpen, newVal => {
|
||||
reaction(() => this.model.menuIsOpen, newVal => {
|
||||
if (newVal) { // rising edge of selection
|
||||
this.selected.replace(namespaceStore.selectedNames);
|
||||
this.selected.replace(this.props.namespaceStore.selectedNames);
|
||||
this.didToggle = false;
|
||||
}
|
||||
}),
|
||||
@@ -100,7 +73,7 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
||||
|
||||
formatOptionLabel({ value: namespace, label }: SelectOption) {
|
||||
if (namespace) {
|
||||
const isSelected = namespaceStore.hasContext(namespace);
|
||||
const isSelected = this.props.namespaceStore.hasContext(namespace);
|
||||
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
@@ -117,14 +90,14 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
||||
@action
|
||||
onChange = ([{ value: namespace }]: SelectOption[]) => {
|
||||
if (namespace) {
|
||||
if (this.isMultiSelection) {
|
||||
if (this.model.isMultiSelection) {
|
||||
this.didToggle = true;
|
||||
namespaceStore.toggleSingle(namespace);
|
||||
this.props.namespaceStore.toggleSingle(namespace);
|
||||
} else {
|
||||
namespaceStore.selectSingle(namespace);
|
||||
this.props.namespaceStore.selectSingle(namespace);
|
||||
}
|
||||
} else {
|
||||
namespaceStore.selectAll();
|
||||
this.props.namespaceStore.selectAll();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,33 +112,33 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
||||
@action
|
||||
onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (this.isSelectionKey(e)) {
|
||||
this.isMultiSelection = true;
|
||||
this.model.setIsMultiSelection(true);
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
onKeyUp = (e: React.KeyboardEvent) => {
|
||||
if (this.isSelectionKey(e)) {
|
||||
this.isMultiSelection = false;
|
||||
this.model.setIsMultiSelection(false);
|
||||
}
|
||||
|
||||
if (!this.isMultiSelection && this.didToggle) {
|
||||
this.isMenuOpen = false;
|
||||
if (!this.model.isMultiSelection && this.didToggle) {
|
||||
this.model.closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
onClick = () => {
|
||||
if (!this.isMenuOpen) {
|
||||
this.isMenuOpen = true;
|
||||
} else if (!this.isMultiSelection) {
|
||||
this.isMenuOpen = !this.isMenuOpen;
|
||||
if (!this.model.menuIsOpen) {
|
||||
this.model.openMenu();
|
||||
} else if (!this.model.isMultiSelection) {
|
||||
this.model.toggleMenu();
|
||||
}
|
||||
};
|
||||
|
||||
reset = () => {
|
||||
this.isMultiSelection = false;
|
||||
this.isMenuOpen = false;
|
||||
this.model.setIsMultiSelection(true);
|
||||
this.model.closeMenu();
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -173,7 +146,7 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
||||
<div onKeyUp={this.onKeyUp} onKeyDown={this.onKeyDown} onClick={this.onClick}>
|
||||
<NamespaceSelect
|
||||
isMulti={true}
|
||||
menuIsOpen={this.isMenuOpen}
|
||||
menuIsOpen={this.model.menuIsOpen}
|
||||
components={{ Placeholder }}
|
||||
showAllNamespacesOption={true}
|
||||
closeMenuOnSelect={false}
|
||||
@@ -190,3 +163,58 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const NamespaceSelectFilter = withInjectables<Dependencies, SelectProps>(
|
||||
NonInjectedNamespaceSelectFilter,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
model: di.inject(namespaceSelectFilterModelInjectable),
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
type CustomPlaceholderProps = PlaceholderProps<any, boolean>;
|
||||
|
||||
interface PlaceholderDependencies {
|
||||
namespaceStore: NamespaceStore
|
||||
}
|
||||
|
||||
const NonInjectedPlaceholder = observer(
|
||||
({ namespaceStore, ...props }: CustomPlaceholderProps & PlaceholderDependencies) => {
|
||||
const getPlaceholder = (): React.ReactNode => {
|
||||
const namespaces = namespaceStore.contextNamespaces;
|
||||
|
||||
if (namespaceStore.areAllSelectedImplicitly || !namespaces.length) {
|
||||
return <>All namespaces</>;
|
||||
}
|
||||
|
||||
if (namespaces.length === 1) {
|
||||
return <>Namespace: {namespaces[0]}</>;
|
||||
}
|
||||
|
||||
return <>Namespaces: {namespaces.join(", ")}</>;
|
||||
};
|
||||
|
||||
return (
|
||||
<components.Placeholder {...props}>
|
||||
{getPlaceholder()}
|
||||
</components.Placeholder>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
const Placeholder = withInjectables<PlaceholderDependencies, CustomPlaceholderProps>(
|
||||
NonInjectedPlaceholder,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -27,7 +27,9 @@ import { observer } from "mobx-react";
|
||||
import { Select, SelectOption, SelectProps } from "../select";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import { namespaceStore } from "./namespace.store";
|
||||
import type { NamespaceStore } from "./namespace-store/namespace.store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable";
|
||||
|
||||
interface Props extends SelectProps {
|
||||
showIcons?: boolean;
|
||||
@@ -40,11 +42,15 @@ const defaultProps: Partial<Props> = {
|
||||
showIcons: true,
|
||||
};
|
||||
|
||||
interface Dependencies {
|
||||
namespaceStore: NamespaceStore
|
||||
}
|
||||
|
||||
@observer
|
||||
export class NamespaceSelect extends React.Component<Props> {
|
||||
class NonInjectedNamespaceSelect extends React.Component<Props & Dependencies> {
|
||||
static defaultProps = defaultProps as object;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -53,7 +59,7 @@ export class NamespaceSelect extends React.Component<Props> {
|
||||
|
||||
@computed.struct get options(): SelectOption[] {
|
||||
const { customizeOptions, showAllNamespacesOption, sort } = this.props;
|
||||
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
|
||||
let options: SelectOption[] = this.props.namespaceStore.items.map(ns => ({ value: ns.getName() }));
|
||||
|
||||
if (sort) {
|
||||
options.sort(sort);
|
||||
@@ -97,3 +103,14 @@ export class NamespaceSelect extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const NamespaceSelect = withInjectables<Dependencies, Props>(
|
||||
NonInjectedNamespaceSelect,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { NamespaceStore } from "./namespace.store";
|
||||
import apiManagerInjectable from "../../kube-object-menu/dependencies/api-manager.injectable";
|
||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||
|
||||
const namespaceStoreInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const createStorage = di.inject(createStorageInjectable);
|
||||
|
||||
const storage = createStorage<string[] | undefined>(
|
||||
"selected_namespaces",
|
||||
undefined,
|
||||
);
|
||||
|
||||
const namespaceStore = new NamespaceStore({
|
||||
storage,
|
||||
});
|
||||
|
||||
const apiManager = di.inject(apiManagerInjectable);
|
||||
|
||||
apiManager.registerStore(namespaceStore);
|
||||
|
||||
return namespaceStore;
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default namespaceStoreInjectable;
|
||||
+20
-22
@@ -20,16 +20,18 @@
|
||||
*/
|
||||
|
||||
import { action, comparer, computed, IReactionDisposer, makeObservable, reaction } from "mobx";
|
||||
import { autoBind, createStorage, noop, ToggleSet } from "../../utils";
|
||||
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../../common/k8s-api/kube-object.store";
|
||||
import { Namespace, namespacesApi } from "../../../common/k8s-api/endpoints/namespaces.api";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { autoBind, noop, StorageHelper, ToggleSet } from "../../../utils";
|
||||
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../../../common/k8s-api/kube-object.store";
|
||||
import { Namespace, namespacesApi } from "../../../../common/k8s-api/endpoints/namespaces.api";
|
||||
|
||||
interface Dependencies {
|
||||
storage: StorageHelper<string[] | undefined>
|
||||
}
|
||||
|
||||
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
api = namespacesApi;
|
||||
private storage = createStorage<string[] | undefined>("selected_namespaces", undefined);
|
||||
|
||||
constructor() {
|
||||
constructor(private dependencies: Dependencies) {
|
||||
super();
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
@@ -39,7 +41,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
|
||||
private async init() {
|
||||
await this.contextReady;
|
||||
await this.storage.whenReady;
|
||||
await this.dependencies.storage.whenReady;
|
||||
|
||||
this.selectNamespaces(this.initialNamespaces);
|
||||
this.autoLoadAllowedNamespaces();
|
||||
@@ -61,7 +63,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
|
||||
private get initialNamespaces(): string[] {
|
||||
const { allowedNamespaces } = this;
|
||||
const selectedNamespaces = this.storage.get(); // raw namespaces, undefined on first load
|
||||
const selectedNamespaces = this.dependencies.storage.get(); // raw namespaces, undefined on first load
|
||||
|
||||
// return previously saved namespaces from local-storage (if any)
|
||||
if (Array.isArray(selectedNamespaces)) {
|
||||
@@ -83,7 +85,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
* The current value (list of namespaces names) in the storage layer
|
||||
*/
|
||||
@computed private get selectedNamespaces(): string[] {
|
||||
return this.storage.get() ?? [];
|
||||
return this.dependencies.storage.get() ?? [];
|
||||
}
|
||||
|
||||
@computed get allowedNamespaces(): string[] {
|
||||
@@ -147,22 +149,21 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
return namespaces;
|
||||
}
|
||||
|
||||
@action
|
||||
selectNamespaces(namespace: string | string[]) {
|
||||
@action selectNamespaces = (namespace: string | string[]) => {
|
||||
const namespaces = Array.from(new Set([namespace].flat()));
|
||||
|
||||
this.storage.set(namespaces);
|
||||
}
|
||||
this.dependencies.storage.set(namespaces);
|
||||
};
|
||||
|
||||
@action
|
||||
clearSelected(namespaces?: string | string[]) {
|
||||
if (namespaces) {
|
||||
const resettingNamespaces = [namespaces].flat();
|
||||
const newNamespaces = this.storage.get().filter(ns => !resettingNamespaces.includes(ns));
|
||||
const newNamespaces = this.dependencies.storage.get().filter(ns => !resettingNamespaces.includes(ns));
|
||||
|
||||
this.storage.set(newNamespaces);
|
||||
this.dependencies.storage.set(newNamespaces);
|
||||
} else {
|
||||
this.storage.reset();
|
||||
this.dependencies.storage.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +197,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
nextState.toggle(namespace);
|
||||
}
|
||||
|
||||
this.storage.set([...nextState]);
|
||||
this.dependencies.storage.set([...nextState]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,14 +210,14 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
const nextState = new ToggleSet(this.contextNamespaces);
|
||||
|
||||
nextState.toggle(namespace);
|
||||
this.storage.set([...nextState]);
|
||||
this.dependencies.storage.set([...nextState]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the given namespace the sole selected namespace
|
||||
*/
|
||||
selectSingle(namespace: string) {
|
||||
this.storage.set([namespace]);
|
||||
this.dependencies.storage.set([namespace]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,9 +248,6 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
}
|
||||
}
|
||||
|
||||
export const namespaceStore = new NamespaceStore();
|
||||
apiManager.registerStore(namespaceStore);
|
||||
|
||||
export function getDummyNamespace(name: string) {
|
||||
return new Namespace({
|
||||
kind: Namespace.kind,
|
||||
@@ -28,9 +28,13 @@ import { TabLayout } from "../layout/tab-layout";
|
||||
import { Badge } from "../badge";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import { namespaceStore } from "./namespace.store";
|
||||
import type { NamespaceStore } from "./namespace-store/namespace.store";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { NamespacesRouteParams } from "../../../common/routes";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable";
|
||||
import addNamespaceDialogModelInjectable
|
||||
from "./add-namespace-dialog-model/add-namespace-dialog-model.injectable";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@@ -42,7 +46,12 @@ enum columnId {
|
||||
interface Props extends RouteComponentProps<NamespacesRouteParams> {
|
||||
}
|
||||
|
||||
export class Namespaces extends React.Component<Props> {
|
||||
interface Dependencies {
|
||||
namespaceStore: NamespaceStore
|
||||
openAddNamespaceDialog: () => void
|
||||
}
|
||||
|
||||
class NonInjectedNamespaces extends React.Component<Props & Dependencies> {
|
||||
render() {
|
||||
return (
|
||||
<TabLayout>
|
||||
@@ -50,7 +59,7 @@ export class Namespaces extends React.Component<Props> {
|
||||
isConfigurable
|
||||
tableId="namespaces"
|
||||
className="Namespaces"
|
||||
store={namespaceStore}
|
||||
store={this.props.namespaceStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: ns => ns.getName(),
|
||||
[columnId.labels]: ns => ns.getLabels(),
|
||||
@@ -78,7 +87,7 @@ export class Namespaces extends React.Component<Props> {
|
||||
]}
|
||||
addRemoveButtons={{
|
||||
addTooltip: "Add Namespace",
|
||||
onAdd: () => AddNamespaceDialog.open(),
|
||||
onAdd: () => this.props.openAddNamespaceDialog(),
|
||||
}}
|
||||
customizeTableRowProps={item => ({
|
||||
disabled: item.getStatus() === NamespaceStatus.TERMINATING,
|
||||
@@ -89,3 +98,15 @@ export class Namespaces extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const Namespaces = withInjectables<Dependencies, Props>(
|
||||
NonInjectedNamespaces,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
openAddNamespaceDialog: di.inject(addNamespaceDialogModelInjectable).open,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -21,25 +21,34 @@
|
||||
|
||||
import React from "react";
|
||||
import { boundMethod, cssNames } from "../../utils";
|
||||
import { openPortForward, PortForwardItem, removePortForward } from "../../port-forward";
|
||||
import { openPortForward, PortForwardItem } from "../../port-forward";
|
||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { PortForwardDialog } from "../../port-forward";
|
||||
import { Notifications } from "../notifications";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import removePortForwardInjectable
|
||||
from "../../port-forward/port-forward-store/remove-port-forward/remove-port-forward.injectable";
|
||||
import portForwardDialogModelInjectable
|
||||
from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable";
|
||||
|
||||
interface Props extends MenuActionsProps {
|
||||
portForward: PortForwardItem;
|
||||
hideDetails?(): void;
|
||||
}
|
||||
|
||||
export class PortForwardMenu extends React.Component<Props> {
|
||||
interface Dependencies {
|
||||
removePortForward: (item: PortForwardItem) => Promise<void>,
|
||||
openPortForwardDialog: (item: PortForwardItem) => void
|
||||
}
|
||||
|
||||
class NonInjectedPortForwardMenu extends React.Component<Props & Dependencies> {
|
||||
@boundMethod
|
||||
remove() {
|
||||
const { portForward } = this.props;
|
||||
|
||||
try {
|
||||
removePortForward(portForward);
|
||||
this.props.removePortForward(portForward);
|
||||
} catch (error) {
|
||||
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}. The port-forward may still be active.`);
|
||||
}
|
||||
@@ -56,7 +65,7 @@ export class PortForwardMenu extends React.Component<Props> {
|
||||
<Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser" />
|
||||
<span className="title">Open</span>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => PortForwardDialog.open(portForward)}>
|
||||
<MenuItem onClick={() => this.props.openPortForwardDialog(portForward)}>
|
||||
<Icon material="edit" tooltip="Change port or protocol" interactive={toolbar} />
|
||||
<span className="title">Edit</span>
|
||||
</MenuItem>
|
||||
@@ -78,3 +87,15 @@ export class PortForwardMenu extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const PortForwardMenu = withInjectables<Dependencies, Props>(
|
||||
NonInjectedPortForwardMenu,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
removePortForward: di.inject(removePortForwardInjectable),
|
||||
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -25,11 +25,13 @@ import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import type { RouteComponentProps } from "react-router-dom";
|
||||
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
||||
import { PortForwardItem, portForwardStore } from "../../port-forward";
|
||||
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";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@@ -44,19 +46,23 @@ enum columnId {
|
||||
interface Props extends RouteComponentProps<PortForwardsRouteParams> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
portForwardStore: PortForwardStore
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PortForwards extends React.Component<Props> {
|
||||
class NonInjectedPortForwards extends React.Component<Props & Dependencies> {
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
portForwardStore.watch(),
|
||||
this.props.portForwardStore.watch(),
|
||||
]);
|
||||
}
|
||||
|
||||
get selectedPortForward() {
|
||||
const { match: { params: { forwardport }}} = this.props;
|
||||
|
||||
return portForwardStore.getById(forwardport);
|
||||
return this.props.portForwardStore.getById(forwardport);
|
||||
}
|
||||
|
||||
onDetails = (item: PortForwardItem) => {
|
||||
@@ -96,7 +102,7 @@ export class PortForwards extends React.Component<Props> {
|
||||
<ItemListLayout
|
||||
isConfigurable
|
||||
tableId="port_forwards"
|
||||
className="PortForwards" store={portForwardStore}
|
||||
className="PortForwards" store={this.props.portForwardStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: item => item.getName(),
|
||||
[columnId.namespace]: item => item.getNs(),
|
||||
@@ -150,3 +156,15 @@ export class PortForwards extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const PortForwards = withInjectables<Dependencies, Props>(
|
||||
NonInjectedPortForwards,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
portForwardStore: di.inject(portForwardStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -31,25 +31,37 @@ import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { ServicePortComponent } from "./service-port-component";
|
||||
import { endpointStore } from "../+network-endpoints/endpoints.store";
|
||||
import { ServiceDetailsEndpoint } from "./service-details-endpoint";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
import { portForwardStore } from "../../port-forward";
|
||||
import type { PortForwardStore } from "../../port-forward";
|
||||
import logger from "../../../common/logger";
|
||||
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 { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable
|
||||
from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable";
|
||||
import type { KubeWatchSubscribeStoreOptions } from "../../kube-watch-api/kube-watch-api";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Service> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[], options: KubeWatchSubscribeStoreOptions) => Disposer
|
||||
portForwardStore: PortForwardStore
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ServiceDetails extends React.Component<Props> {
|
||||
class NonInjectedServiceDetails extends React.Component<Props & Dependencies> {
|
||||
componentDidMount() {
|
||||
const { object: service } = this.props;
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
kubeWatchApi.subscribeStores([
|
||||
this.props.subscribeStores([
|
||||
endpointStore,
|
||||
], {
|
||||
namespaces: [service.getNs()],
|
||||
}),
|
||||
portForwardStore.watch(),
|
||||
this.props.portForwardStore.watch(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -140,3 +152,15 @@ export class ServiceDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ServiceDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedServiceDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
portForwardStore: di.inject(portForwardStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -28,22 +28,36 @@ import { observable, makeObservable, reaction } from "mobx";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Button } from "../button";
|
||||
import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward";
|
||||
import { aboutPortForwarding, getPortForward, getPortForwards, openPortForward, PortForwardStore, predictProtocol } from "../../port-forward";
|
||||
import type { ForwardedPort } 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 removePortForwardInjectable from "../../port-forward/port-forward-store/remove-port-forward/remove-port-forward.injectable";
|
||||
import portForwardDialogModelInjectable
|
||||
from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable";
|
||||
import addPortForwardInjectable
|
||||
from "../../port-forward/port-forward-store/add-port-forward/add-port-forward.injectable";
|
||||
|
||||
interface Props {
|
||||
service: Service;
|
||||
port: ServicePort;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
portForwardStore: PortForwardStore
|
||||
removePortForward: (item: ForwardedPort) => Promise<void>
|
||||
addPortForward: (item: ForwardedPort) => Promise<number>
|
||||
openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean }) => void
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ServicePortComponent extends React.Component<Props> {
|
||||
class NonInjectedServicePortComponent extends React.Component<Props & Dependencies> {
|
||||
@observable waiting = false;
|
||||
@observable forwardPort = 0;
|
||||
@observable isPortForwarded = false;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
this.checkExistingPortForwarding();
|
||||
@@ -51,7 +65,7 @@ export class ServicePortComponent extends React.Component<Props> {
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => [portForwardStore.portForwards, this.props.service], () => this.checkExistingPortForwarding()),
|
||||
reaction(() => [this.props.portForwardStore.portForwards, this.props.service], () => this.checkExistingPortForwarding()),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -96,7 +110,7 @@ export class ServicePortComponent extends React.Component<Props> {
|
||||
// determine how many port-forwards are already active
|
||||
const { length } = await getPortForwards();
|
||||
|
||||
this.forwardPort = await addPortForward(portForward);
|
||||
this.forwardPort = await this.props.addPortForward(portForward);
|
||||
|
||||
if (this.forwardPort) {
|
||||
portForward.forwardPort = this.forwardPort;
|
||||
@@ -129,7 +143,7 @@ export class ServicePortComponent extends React.Component<Props> {
|
||||
this.waiting = true;
|
||||
|
||||
try {
|
||||
await removePortForward(portForward);
|
||||
await this.props.removePortForward(portForward);
|
||||
this.isPortForwarded = false;
|
||||
} catch (error) {
|
||||
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
|
||||
@@ -155,7 +169,7 @@ export class ServicePortComponent extends React.Component<Props> {
|
||||
protocol: predictProtocol(port.name),
|
||||
};
|
||||
|
||||
PortForwardDialog.open(portForward, { openInBrowser: true });
|
||||
this.props.openPortForwardDialog(portForward, { openInBrowser: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -172,3 +186,18 @@ export class ServicePortComponent extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ServicePortComponent = withInjectables<Dependencies, Props>(
|
||||
NonInjectedServicePortComponent,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
portForwardStore: di.inject(portForwardStoreInjectable),
|
||||
removePortForward: di.inject(removePortForwardInjectable),
|
||||
addPortForward: di.inject(addPortForwardInjectable),
|
||||
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -39,18 +39,26 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { NodeDetailsResources } from "./node-details-resources";
|
||||
import { DrawerTitle } from "../drawer/drawer-title";
|
||||
import { boundMethod } from "../../utils";
|
||||
import { boundMethod, Disposer } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable
|
||||
from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Node> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
||||
}
|
||||
|
||||
@observer
|
||||
export class NodeDetails extends React.Component<Props> {
|
||||
class NonInjectedNodeDetails extends React.Component<Props & Dependencies> {
|
||||
@observable metrics: Partial<IClusterMetrics>;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -60,7 +68,8 @@ export class NodeDetails extends React.Component<Props> {
|
||||
reaction(() => this.props.object.getName(), () => {
|
||||
this.metrics = null;
|
||||
}),
|
||||
kubeWatchApi.subscribeStores([
|
||||
|
||||
this.props.subscribeStores([
|
||||
podsStore,
|
||||
]),
|
||||
]);
|
||||
@@ -190,3 +199,15 @@ export class NodeDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const NodeDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedNodeDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -22,14 +22,20 @@
|
||||
import React, { useState } from "react";
|
||||
import { Input, InputValidators } from "../input";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { getDefaultKubectlDownloadPath, UserStore } from "../../../common/user-store";
|
||||
import { observer } from "mobx-react";
|
||||
import { bundledKubectlPath } from "../../../main/kubectl";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { bundledKubectlPath } from "../../../main/kubectl/kubectl";
|
||||
import { SelectOption, Select } from "../select";
|
||||
import { FormSwitch, Switcher } from "../switch";
|
||||
import { packageMirrors } from "../../../common/user-store/preferences-helpers";
|
||||
import directoryForBinariesInjectable
|
||||
from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
|
||||
export const KubectlBinaries = observer(() => {
|
||||
interface Dependencies {
|
||||
defaultPathForKubectlBinaries: string
|
||||
}
|
||||
|
||||
const NonInjectedKubectlBinaries: React.FC<Dependencies> = (({ defaultPathForKubectlBinaries }) => {
|
||||
const userStore = UserStore.getInstance();
|
||||
const [downloadPath, setDownloadPath] = useState(userStore.downloadBinariesPath || "");
|
||||
const [binariesPath, setBinariesPath] = useState(userStore.kubectlBinariesPath || "");
|
||||
@@ -78,7 +84,7 @@ export const KubectlBinaries = observer(() => {
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={downloadPath}
|
||||
placeholder={getDefaultKubectlDownloadPath()}
|
||||
placeholder={defaultPathForKubectlBinaries}
|
||||
validators={pathValidator}
|
||||
onChange={setDownloadPath}
|
||||
onBlur={save}
|
||||
@@ -104,3 +110,9 @@ export const KubectlBinaries = observer(() => {
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const KubectlBinaries = withInjectables<Dependencies>(NonInjectedKubectlBinaries, {
|
||||
getProps: (di) => ({
|
||||
defaultPathForKubectlBinaries: di.inject(directoryForBinariesInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -33,16 +33,25 @@ import { storageClassStore } from "./storage-class.store";
|
||||
import { VolumeDetailsList } from "../+storage-volumes/volume-details-list";
|
||||
import { volumesStore } from "../+storage-volumes/volumes.store";
|
||||
import logger from "../../../common/logger";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
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 { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable
|
||||
from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<StorageClass> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
||||
}
|
||||
|
||||
@observer
|
||||
export class StorageClassDetails extends React.Component<Props> {
|
||||
class NonInjectedStorageClassDetails extends React.Component<Props & Dependencies> {
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
kubeWatchApi.subscribeStores([
|
||||
this.props.subscribeStores([
|
||||
volumesStore,
|
||||
]),
|
||||
]);
|
||||
@@ -102,3 +111,15 @@ export class StorageClassDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const StorageClassDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedStorageClassDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
+11
-2
@@ -19,17 +19,26 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { ClusterRoleBindingDialog } from "../dialog";
|
||||
import { clusterRolesStore } from "../../+cluster-roles/store";
|
||||
import { ClusterRole } from "../../../../../common/k8s-api/endpoints";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import { DiRender, renderFor } from "../../../test-utils/renderFor";
|
||||
|
||||
jest.mock("../../+cluster-roles/store");
|
||||
|
||||
describe("ClusterRoleBindingDialog tests", () => {
|
||||
beforeEach(() => {
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
(clusterRolesStore as any).items = [new ClusterRole({
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "ClusterRole",
|
||||
|
||||
@@ -19,17 +19,31 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { clusterRolesStore } from "../../+cluster-roles/store";
|
||||
import { ClusterRole } from "../../../../../common/k8s-api/endpoints";
|
||||
import { RoleBindingDialog } from "../dialog";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import type { DiRender } from "../../../test-utils/renderFor";
|
||||
import { renderFor } from "../../../test-utils/renderFor";
|
||||
import directoryForUserDataInjectable
|
||||
from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
|
||||
jest.mock("../../+cluster-roles/store");
|
||||
|
||||
describe("RoleBindingDialog tests", () => {
|
||||
beforeEach(() => {
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
(clusterRolesStore as any).items = [new ClusterRole({
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "ClusterRole",
|
||||
|
||||
@@ -34,16 +34,25 @@ import { getDetailsUrl } from "../kube-detail-params";
|
||||
import { CronJob, Job } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import logger from "../../../common/logger";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
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 { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable
|
||||
from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<CronJob> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
||||
}
|
||||
|
||||
@observer
|
||||
export class CronJobDetails extends React.Component<Props> {
|
||||
class NonInjectedCronJobDetails extends React.Component<Props & Dependencies> {
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
kubeWatchApi.subscribeStores([
|
||||
this.props.subscribeStores([
|
||||
jobStore,
|
||||
]),
|
||||
]);
|
||||
@@ -119,3 +128,14 @@ export class CronJobDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const CronJobDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedCronJobDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -39,18 +39,26 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import { boundMethod, Disposer } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable
|
||||
from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<DaemonSet> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
||||
}
|
||||
|
||||
@observer
|
||||
export class DaemonSetDetails extends React.Component<Props> {
|
||||
class NonInjectedDaemonSetDetails extends React.Component<Props & Dependencies> {
|
||||
@observable metrics: IPodMetrics = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -60,7 +68,7 @@ export class DaemonSetDetails extends React.Component<Props> {
|
||||
reaction(() => this.props.object, () => {
|
||||
this.metrics = null;
|
||||
}),
|
||||
kubeWatchApi.subscribeStores([
|
||||
this.props.subscribeStores([
|
||||
podsStore,
|
||||
]),
|
||||
]);
|
||||
@@ -139,3 +147,14 @@ export class DaemonSetDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DaemonSetDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedDaemonSetDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -41,18 +41,26 @@ import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
|
||||
import { DeploymentReplicaSets } from "./deployment-replicasets";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import { boundMethod, Disposer } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable
|
||||
from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Deployment> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
||||
}
|
||||
|
||||
@observer
|
||||
export class DeploymentDetails extends React.Component<Props> {
|
||||
class NonInjectedDeploymentDetails extends React.Component<Props & Dependencies> {
|
||||
@observable metrics: IPodMetrics = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -62,7 +70,8 @@ export class DeploymentDetails extends React.Component<Props> {
|
||||
reaction(() => this.props.object, () => {
|
||||
this.metrics = null;
|
||||
}),
|
||||
kubeWatchApi.subscribeStores([
|
||||
|
||||
this.props.subscribeStores([
|
||||
podsStore,
|
||||
replicaSetStore,
|
||||
]),
|
||||
@@ -162,3 +171,15 @@ export class DeploymentDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DeploymentDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedDeploymentDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -45,16 +45,25 @@ import { boundMethod } from "autobind-decorator";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import logger from "../../../common/logger";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
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 { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable
|
||||
from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Job> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
||||
}
|
||||
|
||||
@observer
|
||||
export class JobDetails extends React.Component<Props> {
|
||||
class NonInjectedJobDetails extends React.Component<Props & Dependencies> {
|
||||
@observable metrics: IPodMetrics = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -64,7 +73,7 @@ export class JobDetails extends React.Component<Props> {
|
||||
reaction(() => this.props.object, () => {
|
||||
this.metrics = null;
|
||||
}),
|
||||
kubeWatchApi.subscribeStores([
|
||||
this.props.subscribeStores([
|
||||
podsStore,
|
||||
]),
|
||||
]);
|
||||
@@ -171,3 +180,15 @@ export class JobDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const JobDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedJobDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -26,12 +26,14 @@ import { observer } from "mobx-react";
|
||||
import { OverviewWorkloadStatus } from "./overview-workload-status";
|
||||
import { Link } from "react-router-dom";
|
||||
import { workloadStores } from "../+workloads";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store";
|
||||
import type { KubeResource } from "../../../common/rbac";
|
||||
import { ResourceNames } from "../../utils/rbac";
|
||||
import { boundMethod } from "../../utils";
|
||||
import { workloadURL } from "../../../common/routes";
|
||||
import { isAllowedResource } from "../../../common/utils/allowed-resource";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable";
|
||||
|
||||
const resources: KubeResource[] = [
|
||||
"pods",
|
||||
@@ -43,8 +45,12 @@ const resources: KubeResource[] = [
|
||||
"cronjobs",
|
||||
];
|
||||
|
||||
interface Dependencies {
|
||||
namespaceStore: NamespaceStore
|
||||
}
|
||||
|
||||
@observer
|
||||
export class OverviewStatuses extends React.Component {
|
||||
class NonInjectedOverviewStatuses extends React.Component<Dependencies> {
|
||||
@boundMethod
|
||||
renderWorkload(resource: KubeResource): React.ReactElement {
|
||||
const store = workloadStores.get(resource);
|
||||
@@ -53,7 +59,7 @@ export class OverviewStatuses extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = store.getAllByNs(namespaceStore.contextNamespaces);
|
||||
const items = store.getAllByNs(this.props.namespaceStore.contextNamespaces);
|
||||
|
||||
return (
|
||||
<div className="workload" key={resource}>
|
||||
@@ -79,3 +85,13 @@ export class OverviewStatuses extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const OverviewStatuses = withInjectables<Dependencies>(
|
||||
NonInjectedOverviewStatuses,
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -32,32 +32,41 @@ 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 { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
import { WorkloadsOverviewDetailRegistry } from "../../../extensions/registries";
|
||||
import type { WorkloadsOverviewRouteParams } from "../../../common/routes";
|
||||
import { makeObservable, observable, reaction } from "mobx";
|
||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||
import { Icon } from "../icon";
|
||||
import { TooltipPosition } from "../tooltip";
|
||||
import type { ClusterContext } from "../../../common/k8s-api/cluster-context";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable";
|
||||
import type { ClusterFrameContext } from "../../cluster-frame-context/cluster-frame-context";
|
||||
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 type { KubeWatchSubscribeStoreOptions } from "../../kube-watch-api/kube-watch-api";
|
||||
|
||||
interface Props extends RouteComponentProps<WorkloadsOverviewRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class WorkloadsOverview extends React.Component<Props> {
|
||||
static clusterContext: ClusterContext;
|
||||
interface Dependencies {
|
||||
clusterFrameContext: ClusterFrameContext
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[], options: KubeWatchSubscribeStoreOptions) => Disposer
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedWorkloadsOverview extends React.Component<Props & Dependencies> {
|
||||
@observable loadErrors: string[] = [];
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
kubeWatchApi.subscribeStores([
|
||||
this.props.subscribeStores([
|
||||
cronJobStore,
|
||||
daemonSetStore,
|
||||
deploymentStore,
|
||||
@@ -69,7 +78,7 @@ export class WorkloadsOverview extends React.Component<Props> {
|
||||
], {
|
||||
onLoadFailure: error => this.loadErrors.push(String(error)),
|
||||
}),
|
||||
reaction(() => WorkloadsOverview.clusterContext.contextNamespaces.slice(), () => {
|
||||
reaction(() => this.props.clusterFrameContext.contextNamespaces.slice(), () => {
|
||||
// clear load errors
|
||||
this.loadErrors.length = 0;
|
||||
}),
|
||||
@@ -117,3 +126,15 @@ export class WorkloadsOverview extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const WorkloadsOverview = withInjectables<Dependencies, Props>(
|
||||
NonInjectedWorkloadsOverview,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
clusterFrameContext: di.inject(clusterFrameContextInjectable),
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -21,9 +21,13 @@
|
||||
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import type { IToleration } from "../../../../common/k8s-api/workload-kube-object";
|
||||
import { PodTolerations } from "../pod-tolerations";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||
import directoryForLensLocalStorageInjectable
|
||||
from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
@@ -47,6 +51,21 @@ const tolerations: IToleration[] =[
|
||||
];
|
||||
|
||||
describe("<PodTolerations />", () => {
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(
|
||||
directoryForLensLocalStorageInjectable,
|
||||
() => "some-directory-for-lens-local-storage",
|
||||
);
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
render = renderFor(di);
|
||||
});
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
const { container } = render(<PodTolerations tolerations={tolerations} />);
|
||||
|
||||
|
||||
@@ -28,9 +28,16 @@ import { observable, makeObservable, reaction } from "mobx";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Button } from "../button";
|
||||
import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward";
|
||||
import { aboutPortForwarding, getPortForward, getPortForwards, openPortForward, PortForwardStore, predictProtocol } from "../../port-forward";
|
||||
import type { ForwardedPort } 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 removePortForwardInjectable from "../../port-forward/port-forward-store/remove-port-forward/remove-port-forward.injectable";
|
||||
import portForwardDialogModelInjectable
|
||||
from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable";
|
||||
import addPortForwardInjectable
|
||||
from "../../port-forward/port-forward-store/add-port-forward/add-port-forward.injectable";
|
||||
|
||||
interface Props {
|
||||
pod: Pod;
|
||||
@@ -41,13 +48,20 @@ interface Props {
|
||||
}
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
portForwardStore: PortForwardStore;
|
||||
removePortForward: (item: ForwardedPort) => Promise<void>;
|
||||
addPortForward: (item: ForwardedPort) => Promise<number>;
|
||||
openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean }) => void;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PodContainerPort extends React.Component<Props> {
|
||||
class NonInjectedPodContainerPort extends React.Component<Props & Dependencies> {
|
||||
@observable waiting = false;
|
||||
@observable forwardPort = 0;
|
||||
@observable isPortForwarded = false;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
this.checkExistingPortForwarding();
|
||||
@@ -55,7 +69,7 @@ export class PodContainerPort extends React.Component<Props> {
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => [portForwardStore.portForwards, this.props.pod], () => this.checkExistingPortForwarding()),
|
||||
reaction(() => [this.props.portForwardStore.portForwards, this.props.pod], () => this.checkExistingPortForwarding()),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -100,7 +114,7 @@ export class PodContainerPort extends React.Component<Props> {
|
||||
// determine how many port-forwards are already active
|
||||
const { length } = await getPortForwards();
|
||||
|
||||
this.forwardPort = await addPortForward(portForward);
|
||||
this.forwardPort = await this.props.addPortForward(portForward);
|
||||
|
||||
if (this.forwardPort) {
|
||||
portForward.forwardPort = this.forwardPort;
|
||||
@@ -133,7 +147,7 @@ export class PodContainerPort extends React.Component<Props> {
|
||||
this.waiting = true;
|
||||
|
||||
try {
|
||||
await removePortForward(portForward);
|
||||
await this.props.removePortForward(portForward);
|
||||
this.isPortForwarded = false;
|
||||
} catch (error) {
|
||||
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
|
||||
@@ -161,7 +175,7 @@ export class PodContainerPort extends React.Component<Props> {
|
||||
protocol: predictProtocol(port.name),
|
||||
};
|
||||
|
||||
PortForwardDialog.open(portForward, { openInBrowser: true });
|
||||
this.props.openPortForwardDialog(portForward, { openInBrowser: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,3 +192,17 @@ export class PodContainerPort extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const PodContainerPort = withInjectables<Dependencies, Props>(
|
||||
NonInjectedPodContainerPort,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
portForwardStore: di.inject(portForwardStoreInjectable),
|
||||
removePortForward: di.inject(removePortForwardInjectable),
|
||||
addPortForward: di.inject(addPortForwardInjectable),
|
||||
openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -35,8 +35,10 @@ import { ContainerCharts } from "./container-charts";
|
||||
import { LocaleDate } from "../locale-date";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { portForwardStore } from "../../port-forward/port-forward.store";
|
||||
import type { PortForwardStore } from "../../port-forward";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable";
|
||||
|
||||
interface Props {
|
||||
pod: Pod;
|
||||
@@ -44,12 +46,16 @@ interface Props {
|
||||
metrics?: { [key: string]: IMetrics };
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
portForwardStore: PortForwardStore
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PodDetailsContainer extends React.Component<Props> {
|
||||
class NonInjectedPodDetailsContainer extends React.Component<Props & Dependencies> {
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
portForwardStore.watch(),
|
||||
this.props.portForwardStore.watch(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -200,3 +206,14 @@ export class PodDetailsContainer extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const PodDetailsContainer = withInjectables<Dependencies, Props>(
|
||||
NonInjectedPodDetailsContainer,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
portForwardStore: di.inject(portForwardStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -38,18 +38,26 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import { boundMethod, Disposer } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable
|
||||
from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<ReplicaSet> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ReplicaSetDetails extends React.Component<Props> {
|
||||
class NonInjectedReplicaSetDetails extends React.Component<Props & Dependencies> {
|
||||
@observable metrics: IPodMetrics = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -59,7 +67,8 @@ export class ReplicaSetDetails extends React.Component<Props> {
|
||||
reaction(() => this.props.object, () => {
|
||||
this.metrics = null;
|
||||
}),
|
||||
kubeWatchApi.subscribeStores([
|
||||
|
||||
this.props.subscribeStores([
|
||||
podsStore,
|
||||
]),
|
||||
]);
|
||||
@@ -140,3 +149,14 @@ export class ReplicaSetDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ReplicaSetDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedReplicaSetDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -39,18 +39,26 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import { boundMethod, Disposer } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import kubeWatchApiInjectable
|
||||
from "../../kube-watch-api/kube-watch-api.injectable";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<StatefulSet> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
||||
}
|
||||
|
||||
@observer
|
||||
export class StatefulSetDetails extends React.Component<Props> {
|
||||
class NonInjectedStatefulSetDetails extends React.Component<Props & Dependencies> {
|
||||
@observable metrics: IPodMetrics = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -60,7 +68,8 @@ export class StatefulSetDetails extends React.Component<Props> {
|
||||
reaction(() => this.props.object, () => {
|
||||
this.metrics = null;
|
||||
}),
|
||||
kubeWatchApi.subscribeStores([
|
||||
|
||||
this.props.subscribeStores([
|
||||
podsStore,
|
||||
]),
|
||||
]);
|
||||
@@ -137,3 +146,15 @@ export class StatefulSetDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const StatefulSetDetails = withInjectables<Dependencies, Props>(
|
||||
NonInjectedStatefulSetDetails,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import {
|
||||
AppPaths,
|
||||
appPathsInjectionToken,
|
||||
appPathsIpcChannel,
|
||||
} from "../../../common/app-paths/app-path-injection-token";
|
||||
import getValueFromRegisteredChannelInjectable from "./get-value-from-registered-channel/get-value-from-registered-channel.injectable";
|
||||
|
||||
let syncAppPaths: AppPaths;
|
||||
|
||||
const appPathsInjectable = getInjectable({
|
||||
setup: async (di) => {
|
||||
const getValueFromRegisteredChannel = di.inject(
|
||||
getValueFromRegisteredChannelInjectable,
|
||||
);
|
||||
|
||||
syncAppPaths = await getValueFromRegisteredChannel(appPathsIpcChannel);
|
||||
},
|
||||
|
||||
instantiate: () => syncAppPaths,
|
||||
|
||||
injectionToken: appPathsInjectionToken,
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default appPathsInjectable;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable";
|
||||
import { getValueFromRegisteredChannel } from "./get-value-from-registered-channel";
|
||||
|
||||
const getValueFromRegisteredChannelInjectable = getInjectable({
|
||||
instantiate: (di) =>
|
||||
getValueFromRegisteredChannel({ ipcRenderer: di.inject(ipcRendererInjectable) }),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default getValueFromRegisteredChannelInjectable;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import type { IpcRenderer } from "electron";
|
||||
import type { Channel } from "../../../../common/ipc-channel/channel";
|
||||
|
||||
interface Dependencies {
|
||||
ipcRenderer: IpcRenderer;
|
||||
}
|
||||
|
||||
export const getValueFromRegisteredChannel =
|
||||
({ ipcRenderer }: Dependencies) =>
|
||||
<TChannel extends Channel<TInstance>, TInstance>(
|
||||
channel: TChannel,
|
||||
): Promise<TChannel["_template"]> =>
|
||||
ipcRenderer.invoke(channel.name);
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
const ipcRendererInjectable = getInjectable({
|
||||
instantiate: () => ipcRenderer,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default ipcRendererInjectable;
|
||||
@@ -26,10 +26,10 @@ import "@testing-library/jest-dom/extend-expect";
|
||||
import { BottomBar } from "./bottom-bar";
|
||||
import { StatusBarRegistry } from "../../../extensions/registries";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { AppPaths } from "../../../common/app-paths";
|
||||
import { CommandOverlay } from "../command-palette";
|
||||
import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command";
|
||||
import { ActiveHotbarName } from "./active-hotbar-name";
|
||||
import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing";
|
||||
|
||||
jest.mock("../command-palette", () => ({
|
||||
CommandOverlay: {
|
||||
@@ -37,8 +37,6 @@ jest.mock("../command-palette", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
AppPaths.init();
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
getName: () => "lens",
|
||||
@@ -56,7 +54,11 @@ jest.mock("electron", () => ({
|
||||
}));
|
||||
|
||||
describe("<BottomBar />", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
await dis.runSetups();
|
||||
|
||||
const mockOpts = {
|
||||
"tmp": {
|
||||
"test-store.json": JSON.stringify({}),
|
||||
|
||||
@@ -25,7 +25,7 @@ import React from "react";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { BottomBar } from "./bottom-bar";
|
||||
import { Catalog, previousActiveTab } from "../+catalog";
|
||||
import { Catalog } from "../+catalog";
|
||||
import { Preferences } from "../+preferences";
|
||||
import { AddCluster } from "../+add-cluster";
|
||||
import { ClusterView } from "./cluster-view";
|
||||
@@ -41,9 +41,16 @@ import { navigation } from "../../navigation";
|
||||
import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync";
|
||||
import { TopBar } from "../layout/topbar";
|
||||
import { catalogURL, getPreviousTabUrl } from "../../../common/routes";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import catalogPreviousActiveTabStorageInjectable
|
||||
from "../+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
catalogPreviousActiveTabStorage: { get: () => string }
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterManager extends React.Component {
|
||||
class NonInjectedClusterManager extends React.Component<Dependencies> {
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => navigation.location, () => setEntityOnRouteMatch(), { fireImmediately: true }),
|
||||
@@ -53,11 +60,18 @@ export class ClusterManager extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="ClusterManager">
|
||||
<TopBar/>
|
||||
<TopBar />
|
||||
<main>
|
||||
<div id="lens-views"/>
|
||||
<div id="lens-views" />
|
||||
<Switch>
|
||||
<Redirect exact from={catalogURL()} to={getPreviousTabUrl(previousActiveTab.get())}/>
|
||||
<Redirect
|
||||
exact
|
||||
from={catalogURL()}
|
||||
to={getPreviousTabUrl(
|
||||
this.props.catalogPreviousActiveTabStorage.get(),
|
||||
)}
|
||||
/>
|
||||
|
||||
<Route component={Welcome} {...routes.welcomeRoute} />
|
||||
<Route component={Catalog} {...routes.catalogRoute} />
|
||||
<Route component={Preferences} {...routes.preferencesRoute} />
|
||||
@@ -65,19 +79,24 @@ export class ClusterManager extends React.Component {
|
||||
<Route component={AddCluster} {...routes.addClusterRoute} />
|
||||
<Route component={ClusterView} {...routes.clusterViewRoute} />
|
||||
<Route component={EntitySettings} {...routes.entitySettingsRoute} />
|
||||
{
|
||||
GlobalPageRegistry.getInstance().getItems()
|
||||
.map(({ url, components: { Page }}) => (
|
||||
<Route key={url} path={url} component={Page} />
|
||||
))
|
||||
}
|
||||
<Redirect exact to={routes.welcomeURL()}/>
|
||||
{GlobalPageRegistry.getInstance()
|
||||
.getItems()
|
||||
.map(({ url, components: { Page }}) => (
|
||||
<Route key={url} path={url} component={Page} />
|
||||
))}
|
||||
<Redirect exact to={routes.welcomeURL()} />
|
||||
</Switch>
|
||||
</main>
|
||||
<HotbarMenu/>
|
||||
<BottomBar/>
|
||||
<DeleteClusterDialog/>
|
||||
<HotbarMenu />
|
||||
<BottomBar />
|
||||
<DeleteClusterDialog />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ClusterManager = withInjectables<Dependencies>(NonInjectedClusterManager, {
|
||||
getProps: di => ({
|
||||
catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -26,7 +26,7 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||
import { ipcRendererOn, requestMain } from "../../../common/ipc";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
|
||||
@@ -26,8 +26,8 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import type { RouteComponentProps } from "react-router";
|
||||
import { ClusterStatus } from "./cluster-status";
|
||||
import { ClusterFrameHandler } from "./lens-views";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import { requestMain } from "../../../common/ipc";
|
||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import { action, IReactionDisposer, makeObservable, observable, when } from "mobx";
|
||||
import logger from "../../../main/logger";
|
||||
import { clusterVisibilityHandler } from "../../../common/cluster-ipc";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import type { ClusterId } from "../../../common/cluster-types";
|
||||
import { getClusterFrameUrl, Singleton } from "../../utils";
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import React from "react";
|
||||
import type { KubernetesCluster } from "../../../common/catalog-entities";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import type { EntitySettingViewProps } from "../../../extensions/registries";
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import * as components from "./components";
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { Cluster } from "../../../../main/cluster";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { EditableList } from "../../editable-list";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import type { Cluster } from "../../../../main/cluster";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { boundMethod } from "../../../utils";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import type { Cluster } from "../../../../main/cluster";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { observer } from "mobx-react";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { boundMethod } from "../../../../common/utils";
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { Cluster } from "../../../../main/cluster";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { Input } from "../../input";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { stat } from "fs/promises";
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Select, SelectOption } from "../../select/select";
|
||||
import { Icon } from "../../icon/icon";
|
||||
import { Button } from "../../button/button";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import type { Cluster } from "../../../../main/cluster";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { observable, reaction, makeObservable } from "mobx";
|
||||
import { ClusterMetricsResourceType } from "../../../../common/cluster-types";
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import type { Cluster } from "../../../../main/cluster";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { Input } from "../../input";
|
||||
import { observable, autorun, makeObservable } from "mobx";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { Cluster } from "../../../../main/cluster";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import React from "react";
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import type { Cluster } from "../../../../main/cluster";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Select, SelectOption } from "../../select";
|
||||
import { Input } from "../../input";
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import React from "react";
|
||||
import { observable, autorun, makeObservable } from "mobx";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import type { Cluster } from "../../../../main/cluster";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { Input, InputValidators } from "../../input";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import type { Cluster } from "../../../../main/cluster";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { observable, reaction, makeObservable } from "mobx";
|
||||
import { Badge } from "../../badge/badge";
|
||||
import { Icon } from "../../icon/icon";
|
||||
|
||||
+27
-10
@@ -25,9 +25,12 @@ import mockFs from "mock-fs";
|
||||
import React from "react";
|
||||
import selectEvent from "react-select-event";
|
||||
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { DeleteClusterDialog } from "../delete-cluster-dialog";
|
||||
import { AppPaths } from "../../../../common/app-paths";
|
||||
|
||||
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";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
@@ -45,8 +48,6 @@ jest.mock("electron", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
AppPaths.init();
|
||||
|
||||
const kubeconfig = `
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
@@ -101,6 +102,22 @@ users:
|
||||
let config: KubeConfig;
|
||||
|
||||
describe("<DeleteClusterDialog />", () => {
|
||||
let createCluster: (model: ClusterModel) => Cluster;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { mainDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs();
|
||||
|
||||
await runSetups();
|
||||
|
||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
describe("Kubeconfig with different clusters", () => {
|
||||
beforeEach(async () => {
|
||||
const mockOpts = {
|
||||
@@ -124,7 +141,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
});
|
||||
|
||||
it("shows warning when deleting non-current-context cluster", () => {
|
||||
const cluster = new Cluster({
|
||||
const cluster = createCluster({
|
||||
id: "test",
|
||||
contextName: "test",
|
||||
preferences: {
|
||||
@@ -142,7 +159,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
});
|
||||
|
||||
it("shows warning when deleting current-context cluster", () => {
|
||||
const cluster = new Cluster({
|
||||
const cluster = createCluster({
|
||||
id: "other-cluster",
|
||||
contextName: "other-context",
|
||||
preferences: {
|
||||
@@ -159,7 +176,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
});
|
||||
|
||||
it("shows context switcher when deleting current cluster", async () => {
|
||||
const cluster = new Cluster({
|
||||
const cluster = createCluster({
|
||||
id: "other-cluster",
|
||||
contextName: "other-context",
|
||||
preferences: {
|
||||
@@ -180,7 +197,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
});
|
||||
|
||||
it("shows context switcher after checkbox click", async () => {
|
||||
const cluster = new Cluster({
|
||||
const cluster = createCluster({
|
||||
id: "some-cluster",
|
||||
contextName: "test",
|
||||
preferences: {
|
||||
@@ -205,7 +222,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
});
|
||||
|
||||
it("shows warning for internal kubeconfig cluster", () => {
|
||||
const cluster = new Cluster({
|
||||
const cluster = createCluster({
|
||||
id: "some-cluster",
|
||||
contextName: "test",
|
||||
preferences: {
|
||||
@@ -243,7 +260,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
});
|
||||
|
||||
it("shows warning if no other contexts left", () => {
|
||||
const cluster = new Cluster({
|
||||
const cluster = createCluster({
|
||||
id: "other-cluster",
|
||||
contextName: "other-context",
|
||||
preferences: {
|
||||
|
||||
@@ -26,7 +26,7 @@ import React from "react";
|
||||
|
||||
import { Button } from "../button";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import type { Cluster } from "../../../main/cluster";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { saveKubeconfig } from "./save-config";
|
||||
import { requestMain } from "../../../common/ipc";
|
||||
import { clusterClearDeletingHandler, clusterDeleteHandler, clusterSetDeletingHandler } from "../../../common/cluster-ipc";
|
||||
|
||||
@@ -20,16 +20,20 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import fse from "fs-extra";
|
||||
import { DockTabs } from "../dock-tabs";
|
||||
import { dockStore, DockTab, TabKind } from "../dock.store";
|
||||
import { DockStore, DockTab, TabKind } from "../dock-store/dock.store";
|
||||
import { noop } from "../../../utils";
|
||||
import { ThemeStore } from "../../../theme.store";
|
||||
import { TerminalStore } from "../terminal.store";
|
||||
import { UserStore } from "../../../../common/user-store";
|
||||
import { AppPaths } from "../../../../common/app-paths";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||
import type { DiRender } from "../../test-utils/renderFor";
|
||||
import { renderFor } from "../../test-utils/renderFor";
|
||||
import directoryForUserDataInjectable
|
||||
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
@@ -46,7 +50,6 @@ jest.mock("electron", () => ({
|
||||
handle: jest.fn(),
|
||||
},
|
||||
}));
|
||||
AppPaths.init();
|
||||
|
||||
const initialTabs: DockTab[] = [
|
||||
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false },
|
||||
@@ -56,7 +59,7 @@ const initialTabs: DockTab[] = [
|
||||
{ id: "logs", kind: TabKind.POD_LOGS, title: "Logs", pinned: false },
|
||||
];
|
||||
|
||||
const getComponent = () => (
|
||||
const getComponent = (dockStore: DockStore) => (
|
||||
<DockTabs
|
||||
tabs={dockStore.tabs}
|
||||
selectedTab={dockStore.selectedTab}
|
||||
@@ -65,40 +68,56 @@ const getComponent = () => (
|
||||
/>
|
||||
);
|
||||
|
||||
const renderTabs = () => render(getComponent());
|
||||
const getTabKinds = () => dockStore.tabs.map(tab => tab.kind);
|
||||
const getTabKinds = (dockStore: DockStore) => dockStore.tabs.map((tab) => tab.kind);
|
||||
|
||||
describe("<DockTabs />", () => {
|
||||
let dockStore: DockStore;
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
di.override(
|
||||
directoryForUserDataInjectable,
|
||||
() => "some-test-suite-specific-directory-for-user-data",
|
||||
);
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
dockStore = di.inject(dockStoreInjectable);
|
||||
|
||||
UserStore.createInstance();
|
||||
ThemeStore.createInstance();
|
||||
TerminalStore.createInstance();
|
||||
await dockStore.whenReady;
|
||||
dockStore.tabs = initialTabs;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ThemeStore.resetInstance();
|
||||
TerminalStore.resetInstance();
|
||||
UserStore.resetInstance();
|
||||
fse.remove("tmp");
|
||||
|
||||
// TODO: A unit test may not cause side effects. Here accessing file system is a side effect.
|
||||
fse.remove("some-test-suite-specific-directory-for-user-data");
|
||||
});
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
const { container } = renderTabs();
|
||||
const { container } = render(getComponent(dockStore));
|
||||
|
||||
expect(container).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
it("has 6 tabs (1 tab is initial terminal)", () => {
|
||||
const { container } = renderTabs();
|
||||
const { container } = render(getComponent(dockStore));
|
||||
const tabs = container.querySelectorAll(".Tab");
|
||||
|
||||
expect(tabs.length).toBe(initialTabs.length);
|
||||
});
|
||||
|
||||
it("opens a context menu", () => {
|
||||
const { container, getByText } = renderTabs();
|
||||
const { container, getByText } = render(getComponent(dockStore));
|
||||
const tab = container.querySelector(".Tab");
|
||||
|
||||
fireEvent.contextMenu(tab);
|
||||
@@ -106,17 +125,22 @@ describe("<DockTabs />", () => {
|
||||
});
|
||||
|
||||
it("closes selected tab", () => {
|
||||
const { container, getByText, rerender } = renderTabs();
|
||||
const { container, getByText, rerender } = render(
|
||||
getComponent(dockStore),
|
||||
);
|
||||
|
||||
const tab = container.querySelector(".Tab");
|
||||
|
||||
fireEvent.contextMenu(tab);
|
||||
fireEvent.click(getByText("Close"));
|
||||
rerender(getComponent());
|
||||
|
||||
rerender(getComponent(dockStore));
|
||||
|
||||
const tabs = container.querySelectorAll(".Tab");
|
||||
|
||||
expect(tabs.length).toBe(initialTabs.length - 1);
|
||||
expect(getTabKinds()).toEqual([
|
||||
|
||||
expect(getTabKinds(dockStore)).toEqual([
|
||||
TabKind.CREATE_RESOURCE,
|
||||
TabKind.EDIT_RESOURCE,
|
||||
TabKind.INSTALL_CHART,
|
||||
@@ -125,42 +149,42 @@ describe("<DockTabs />", () => {
|
||||
});
|
||||
|
||||
it("closes other tabs", () => {
|
||||
const { container, getByText, rerender } = renderTabs();
|
||||
const { container, getByText, rerender } = render(getComponent(dockStore));
|
||||
const tab = container.querySelectorAll(".Tab")[3];
|
||||
|
||||
fireEvent.contextMenu(tab);
|
||||
fireEvent.click(getByText("Close other tabs"));
|
||||
rerender(getComponent());
|
||||
rerender(getComponent(dockStore));
|
||||
|
||||
const tabs = container.querySelectorAll(".Tab");
|
||||
|
||||
expect(tabs.length).toBe(1);
|
||||
expect(getTabKinds()).toEqual([initialTabs[3].kind]);
|
||||
expect(getTabKinds(dockStore)).toEqual([initialTabs[3].kind]);
|
||||
});
|
||||
|
||||
it("closes all tabs", () => {
|
||||
const { container, getByText, rerender } = renderTabs();
|
||||
const { container, getByText, rerender } = render(getComponent(dockStore));
|
||||
const tab = container.querySelector(".Tab");
|
||||
|
||||
fireEvent.contextMenu(tab);
|
||||
const command = getByText("Close all tabs");
|
||||
|
||||
fireEvent.click(command);
|
||||
rerender(getComponent());
|
||||
rerender(getComponent(dockStore));
|
||||
const tabs = container.querySelectorAll(".Tab");
|
||||
|
||||
expect(tabs.length).toBe(0);
|
||||
});
|
||||
|
||||
it("closes tabs to the right", () => {
|
||||
const { container, getByText, rerender } = renderTabs();
|
||||
const { container, getByText, rerender } = render(getComponent(dockStore));
|
||||
const tab = container.querySelectorAll(".Tab")[3]; // 4th of 5
|
||||
|
||||
fireEvent.contextMenu(tab);
|
||||
fireEvent.click(getByText("Close tabs to the right"));
|
||||
rerender(getComponent());
|
||||
rerender(getComponent(dockStore));
|
||||
|
||||
expect(getTabKinds()).toEqual(
|
||||
expect(getTabKinds(dockStore)).toEqual(
|
||||
initialTabs.slice(0, 4).map(tab => tab.kind),
|
||||
);
|
||||
});
|
||||
@@ -169,7 +193,7 @@ describe("<DockTabs />", () => {
|
||||
dockStore.tabs = [{
|
||||
id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false,
|
||||
}];
|
||||
const { container, getByText } = renderTabs();
|
||||
const { container, getByText } = render(getComponent(dockStore));
|
||||
const tab = container.querySelector(".Tab");
|
||||
|
||||
fireEvent.contextMenu(tab);
|
||||
@@ -185,7 +209,7 @@ describe("<DockTabs />", () => {
|
||||
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false },
|
||||
{ id: "logs", kind: TabKind.POD_LOGS, title: "Pod Logs", pinned: false },
|
||||
];
|
||||
const { container, getByText } = renderTabs();
|
||||
const { container, getByText } = render(getComponent(dockStore));
|
||||
const tab = container.querySelectorAll(".Tab")[1];
|
||||
|
||||
fireEvent.contextMenu(tab);
|
||||
|
||||
@@ -21,17 +21,20 @@
|
||||
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { render } from "@testing-library/react";
|
||||
import selectEvent from "react-select-event";
|
||||
|
||||
import { Pod } from "../../../../common/k8s-api/endpoints";
|
||||
import { LogResourceSelector } from "../log-resource-selector";
|
||||
import type { LogTabData } from "../log-tab.store";
|
||||
import type { LogTabData } from "../log-tab-store/log-tab.store";
|
||||
import { dockerPod, deploymentPod1 } from "./pod.mock";
|
||||
import { ThemeStore } from "../../../theme.store";
|
||||
import { UserStore } from "../../../../common/user-store";
|
||||
import mockFs from "mock-fs";
|
||||
import { AppPaths } from "../../../../common/app-paths";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import type { DiRender } from "../../test-utils/renderFor";
|
||||
import { renderFor } from "../../test-utils/renderFor";
|
||||
import directoryForUserDataInjectable
|
||||
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
@@ -49,8 +52,6 @@ jest.mock("electron", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
AppPaths.init();
|
||||
|
||||
const getComponent = (tabData: LogTabData) => {
|
||||
return (
|
||||
<LogResourceSelector
|
||||
@@ -84,10 +85,21 @@ const getFewPodsTabData = (): LogTabData => {
|
||||
};
|
||||
|
||||
describe("<LogResourceSelector />", () => {
|
||||
beforeEach(() => {
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
mockFs({
|
||||
"tmp": {},
|
||||
});
|
||||
|
||||
UserStore.createInstance();
|
||||
ThemeStore.createInstance();
|
||||
});
|
||||
|
||||
@@ -23,49 +23,47 @@ import { podsStore } from "../../+workloads-pods/pods.store";
|
||||
import { UserStore } from "../../../../common/user-store";
|
||||
import { Pod } from "../../../../common/k8s-api/endpoints";
|
||||
import { ThemeStore } from "../../../theme.store";
|
||||
import { dockStore } from "../dock.store";
|
||||
import { logTabStore } from "../log-tab.store";
|
||||
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
|
||||
import fse from "fs-extra";
|
||||
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
||||
import { AppPaths } from "../../../../common/app-paths";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable";
|
||||
import type { LogTabStore } from "../log-tab-store/log-tab.store";
|
||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||
import type { DockStore } from "../dock-store/dock.store";
|
||||
import directoryForUserDataInjectable
|
||||
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import mockFs from "mock-fs";
|
||||
|
||||
mockWindow();
|
||||
|
||||
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(),
|
||||
},
|
||||
}));
|
||||
|
||||
AppPaths.init();
|
||||
|
||||
podsStore.items.push(new Pod(dockerPod));
|
||||
podsStore.items.push(new Pod(deploymentPod1));
|
||||
podsStore.items.push(new Pod(deploymentPod2));
|
||||
|
||||
describe("log tab store", () => {
|
||||
beforeEach(() => {
|
||||
let logTabStore: LogTabStore;
|
||||
let dockStore: DockStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs();
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
dockStore = di.inject(dockStoreInjectable);
|
||||
logTabStore = di.inject(logTabStoreInjectable);
|
||||
|
||||
UserStore.createInstance();
|
||||
ThemeStore.createInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
logTabStore.reset();
|
||||
dockStore.reset();
|
||||
UserStore.resetInstance();
|
||||
ThemeStore.resetInstance();
|
||||
fse.remove("tmp");
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("creates log tab without sibling pods", () => {
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { createInstallChartTab } from "./create-install-chart-tab";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import installChartStoreInjectable from "../install-chart-store/install-chart-store.injectable";
|
||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||
|
||||
const createInstallChartTabInjectable = getInjectable({
|
||||
instantiate: (di) => createInstallChartTab({
|
||||
installChartStore: di.inject(installChartStoreInjectable),
|
||||
createDockTab: di.inject(dockStoreInjectable).createTab,
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createInstallChartTabInjectable;
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import {
|
||||
DockTab,
|
||||
DockTabCreate,
|
||||
DockTabCreateSpecific,
|
||||
TabKind,
|
||||
} from "../dock-store/dock.store";
|
||||
|
||||
import type { InstallChartStore } from "../install-chart-store/install-chart.store";
|
||||
|
||||
interface Dependencies {
|
||||
createDockTab: (rawTab: DockTabCreate, addNumber: boolean) => DockTab;
|
||||
installChartStore: InstallChartStore;
|
||||
}
|
||||
|
||||
export const createInstallChartTab =
|
||||
({ createDockTab, installChartStore }: Dependencies) =>
|
||||
(chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => {
|
||||
const { name, repo, version } = chart;
|
||||
|
||||
const tab = createDockTab(
|
||||
{
|
||||
title: `Helm Install: ${repo}/${name}`,
|
||||
...tabParams,
|
||||
kind: TabKind.INSTALL_CHART,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
installChartStore.setData(tab.id, {
|
||||
name,
|
||||
repo,
|
||||
version,
|
||||
namespace: "default",
|
||||
releaseName: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
return tab;
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||
import { CreateResourceStore } from "./create-resource.store";
|
||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||
|
||||
const createResourceStoreInjectable = getInjectable({
|
||||
instantiate: (di) => new CreateResourceStore({
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
createStorage: di.inject(createStorageInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createResourceStoreInjectable;
|
||||
+11
-15
@@ -25,15 +25,21 @@ import os from "os";
|
||||
import groupBy from "lodash/groupBy";
|
||||
import filehound from "filehound";
|
||||
import { watch } from "chokidar";
|
||||
import { autoBind } from "../../utils";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store";
|
||||
import { autoBind, StorageHelper } from "../../../utils";
|
||||
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
|
||||
import type { DockStore } from "../dock-store/dock.store";
|
||||
|
||||
interface Dependencies {
|
||||
dockStore: DockStore,
|
||||
createStorage:<T> (storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
|
||||
}
|
||||
|
||||
export class CreateResourceStore extends DockTabStore<string> {
|
||||
constructor() {
|
||||
super({
|
||||
constructor(protected dependencies: Dependencies) {
|
||||
super(dependencies, {
|
||||
storageKey: "create_resource",
|
||||
});
|
||||
|
||||
autoBind(this);
|
||||
fs.ensureDirSync(this.userTemplatesFolder);
|
||||
}
|
||||
@@ -78,13 +84,3 @@ export class CreateResourceStore extends DockTabStore<string> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const createResourceStore = new CreateResourceStore();
|
||||
|
||||
export function createResourceTab(tabParams: DockTabCreateSpecific = {}) {
|
||||
return dockStore.createTab({
|
||||
title: "Create resource",
|
||||
...tabParams,
|
||||
kind: TabKind.CREATE_RESOURCE,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { createResourceTab } from "./create-resource-tab";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||
|
||||
const createResourceTabInjectable = getInjectable({
|
||||
instantiate: (di) => createResourceTab({
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createResourceTabInjectable;
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store";
|
||||
|
||||
interface Dependencies {
|
||||
dockStore: DockStore
|
||||
}
|
||||
|
||||
export const createResourceTab =
|
||||
({ dockStore }: Dependencies) =>
|
||||
(tabParams: DockTabCreateSpecific = {}) =>
|
||||
dockStore.createTab({
|
||||
title: "Create resource",
|
||||
...tabParams,
|
||||
kind: TabKind.CREATE_RESOURCE,
|
||||
});
|
||||
@@ -28,8 +28,8 @@ import { GroupSelectOption, Select, SelectOption } from "../select";
|
||||
import yaml from "js-yaml";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { createResourceStore } from "./create-resource.store";
|
||||
import type { DockTab } from "./dock.store";
|
||||
import type { CreateResourceStore } from "./create-resource-store/create-resource.store";
|
||||
import type { DockTab } from "./dock-store/dock.store";
|
||||
import { EditorPanel } from "./editor-panel";
|
||||
import { InfoPanel } from "./info-panel";
|
||||
import * as resourceApplierApi from "../../../common/k8s-api/endpoints/resource-applier.api";
|
||||
@@ -40,25 +40,32 @@ import { getDetailsUrl } from "../kube-detail-params";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { prevDefault } from "../../utils";
|
||||
import { navigate } from "../../navigation";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import createResourceStoreInjectable
|
||||
from "./create-resource-store/create-resource-store.injectable";
|
||||
|
||||
interface Props {
|
||||
tab: DockTab;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
createResourceStore: CreateResourceStore
|
||||
}
|
||||
|
||||
@observer
|
||||
export class CreateResource extends React.Component<Props> {
|
||||
class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
|
||||
@observable currentTemplates: Map<string, SelectOption> = new Map();
|
||||
@observable error = "";
|
||||
@observable templates: GroupSelectOption<SelectOption>[] = [];
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props: Props & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v));
|
||||
createResourceStore.watchUserTemplates(() => createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v)));
|
||||
this.props.createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v));
|
||||
this.props.createResourceStore.watchUserTemplates(() => this.props.createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v)));
|
||||
}
|
||||
|
||||
updateGroupSelectOptions(templates: Record<string, string[]>) {
|
||||
@@ -77,7 +84,7 @@ export class CreateResource extends React.Component<Props> {
|
||||
}
|
||||
|
||||
get data() {
|
||||
return createResourceStore.getData(this.tabId);
|
||||
return this.props.createResourceStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
get currentTemplate() {
|
||||
@@ -86,7 +93,7 @@ export class CreateResource extends React.Component<Props> {
|
||||
|
||||
onChange = (value: string) => {
|
||||
this.error = ""; // reset first, validation goes later
|
||||
createResourceStore.setData(this.tabId, value);
|
||||
this.props.createResourceStore.setData(this.tabId, value);
|
||||
};
|
||||
|
||||
onError = (error: Error | string) => {
|
||||
@@ -96,7 +103,7 @@ export class CreateResource extends React.Component<Props> {
|
||||
onSelectTemplate = (item: SelectOption) => {
|
||||
this.currentTemplates.set(this.tabId, item);
|
||||
fs.readFile(item.value, "utf8").then(v => {
|
||||
createResourceStore.setData(this.tabId, v);
|
||||
this.props.createResourceStore.setData(this.tabId, v);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -180,3 +187,14 @@ export class CreateResource extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const CreateResource = withInjectables<Dependencies, Props>(
|
||||
NonInjectedCreateResource,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
createResourceStore: di.inject(createResourceStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { createTerminalTab } from "./create-terminal-tab";
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||
|
||||
const createTerminalTabInjectable = getInjectable({
|
||||
instantiate: (di) => createTerminalTab({
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createTerminalTabInjectable;
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import {
|
||||
DockStore,
|
||||
DockTabCreateSpecific,
|
||||
TabKind,
|
||||
} from "../dock-store/dock.store";
|
||||
|
||||
interface Dependencies {
|
||||
dockStore: DockStore;
|
||||
}
|
||||
|
||||
export const createTerminalTab =
|
||||
({ dockStore }: Dependencies) =>
|
||||
(tabParams: DockTabCreateSpecific = {}) =>
|
||||
dockStore.createTab({
|
||||
title: `Terminal`,
|
||||
...tabParams,
|
||||
kind: TabKind.TERMINAL,
|
||||
});
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { createUpgradeChartTab } from "./create-upgrade-chart-tab";
|
||||
import upgradeChartStoreInjectable from "../upgrade-chart-store/upgrade-chart-store.injectable";
|
||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||
|
||||
const createUpgradeChartTabInjectable = getInjectable({
|
||||
instantiate: (di) => createUpgradeChartTab({
|
||||
upgradeChartStore: di.inject(upgradeChartStoreInjectable),
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default createUpgradeChartTabInjectable;
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store";
|
||||
import type { UpgradeChartStore } from "../upgrade-chart-store/upgrade-chart.store";
|
||||
|
||||
interface Dependencies {
|
||||
upgradeChartStore: UpgradeChartStore;
|
||||
dockStore: DockStore
|
||||
}
|
||||
|
||||
export const createUpgradeChartTab =
|
||||
({ upgradeChartStore, dockStore }: Dependencies) =>
|
||||
(release: HelmRelease, tabParams: DockTabCreateSpecific = {}) => {
|
||||
let tab = upgradeChartStore.getTabByRelease(release.getName());
|
||||
|
||||
if (tab) {
|
||||
dockStore.open();
|
||||
dockStore.selectTab(tab.id);
|
||||
}
|
||||
|
||||
if (!tab) {
|
||||
tab = dockStore.createTab(
|
||||
{
|
||||
title: `Helm Upgrade: ${release.getName()}`,
|
||||
...tabParams,
|
||||
kind: TabKind.UPGRADE_CHART,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
upgradeChartStore.setData(tab.id, {
|
||||
releaseName: release.getName(),
|
||||
releaseNamespace: release.getNs(),
|
||||
});
|
||||
}
|
||||
|
||||
return tab;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { DockStorageState, DockStore, TabKind } from "./dock.store";
|
||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||
|
||||
const dockStoreInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const createStorage = di.inject(createStorageInjectable);
|
||||
|
||||
const storage = createStorage<DockStorageState>("dock", {
|
||||
height: 300,
|
||||
tabs: [
|
||||
{
|
||||
id: "terminal",
|
||||
kind: TabKind.TERMINAL,
|
||||
title: "Terminal",
|
||||
pinned: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return new DockStore({
|
||||
storage,
|
||||
});
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default dockStoreInjectable;
|
||||
+19
-24
@@ -21,7 +21,7 @@
|
||||
|
||||
import * as uuid from "uuid";
|
||||
import { action, comparer, computed, makeObservable, observable, reaction, runInAction } from "mobx";
|
||||
import { autoBind, createStorage } from "../../utils";
|
||||
import { autoBind, StorageHelper } from "../../../utils";
|
||||
import throttle from "lodash/throttle";
|
||||
|
||||
export type TabId = string;
|
||||
@@ -113,8 +113,12 @@ export interface DockTabCloseEvent {
|
||||
tabId: TabId; // closed tab id
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
storage: StorageHelper<DockStorageState>
|
||||
}
|
||||
|
||||
export class DockStore implements DockStorageState {
|
||||
constructor() {
|
||||
constructor(private dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
this.init();
|
||||
@@ -123,45 +127,38 @@ export class DockStore implements DockStorageState {
|
||||
readonly minHeight = 100;
|
||||
@observable fullSize = false;
|
||||
|
||||
private storage = createStorage<DockStorageState>("dock", {
|
||||
height: 300,
|
||||
tabs: [
|
||||
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false },
|
||||
],
|
||||
});
|
||||
|
||||
get whenReady() {
|
||||
return this.storage.whenReady;
|
||||
return this.dependencies.storage.whenReady;
|
||||
}
|
||||
|
||||
get isOpen(): boolean {
|
||||
return this.storage.get().isOpen;
|
||||
return this.dependencies.storage.get().isOpen;
|
||||
}
|
||||
|
||||
set isOpen(isOpen: boolean) {
|
||||
this.storage.merge({ isOpen });
|
||||
this.dependencies.storage.merge({ isOpen });
|
||||
}
|
||||
|
||||
get height(): number {
|
||||
return this.storage.get().height;
|
||||
return this.dependencies.storage.get().height;
|
||||
}
|
||||
|
||||
set height(height: number) {
|
||||
this.storage.merge({
|
||||
this.dependencies.storage.merge({
|
||||
height: Math.max(this.minHeight, Math.min(height || this.minHeight, this.maxHeight)),
|
||||
});
|
||||
}
|
||||
|
||||
get tabs(): DockTab[] {
|
||||
return this.storage.get().tabs;
|
||||
return this.dependencies.storage.get().tabs;
|
||||
}
|
||||
|
||||
set tabs(tabs: DockTab[]) {
|
||||
this.storage.merge({ tabs });
|
||||
this.dependencies.storage.merge({ tabs });
|
||||
}
|
||||
|
||||
get selectedTabId(): TabId | undefined {
|
||||
return this.storage.get().selectedTabId
|
||||
return this.dependencies.storage.get().selectedTabId
|
||||
|| (
|
||||
this.tabs.length > 0
|
||||
? this.tabs[0]?.id
|
||||
@@ -172,7 +169,7 @@ export class DockStore implements DockStorageState {
|
||||
set selectedTabId(tabId: TabId) {
|
||||
if (tabId && !this.getTabById(tabId)) return; // skip invalid ids
|
||||
|
||||
this.storage.merge({ selectedTabId: tabId });
|
||||
this.dependencies.storage.merge({ selectedTabId: tabId });
|
||||
}
|
||||
|
||||
@computed get selectedTab() {
|
||||
@@ -206,7 +203,7 @@ export class DockStore implements DockStorageState {
|
||||
}
|
||||
|
||||
onTabClose(callback: (evt: DockTabCloseEvent) => void, opts: { fireImmediately?: boolean } = {}) {
|
||||
return reaction(() => dockStore.tabs.map(tab => tab.id), (tabs: TabId[], prevTabs?: TabId[]) => {
|
||||
return reaction(() => this.tabs.map(tab => tab.id), (tabs: TabId[], prevTabs?: TabId[]) => {
|
||||
if (!Array.isArray(prevTabs)) {
|
||||
return; // tabs not yet modified
|
||||
}
|
||||
@@ -292,7 +289,7 @@ export class DockStore implements DockStorageState {
|
||||
}
|
||||
|
||||
@action
|
||||
createTab(rawTabDesc: DockTabCreate, addNumber = true): DockTab {
|
||||
createTab = (rawTabDesc: DockTabCreate, addNumber = true): DockTab => {
|
||||
const {
|
||||
id = uuid.v4(),
|
||||
kind,
|
||||
@@ -322,7 +319,7 @@ export class DockStore implements DockStorageState {
|
||||
this.open();
|
||||
|
||||
return tab;
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
closeTab(tabId: TabId) {
|
||||
@@ -381,8 +378,6 @@ export class DockStore implements DockStorageState {
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.storage?.reset();
|
||||
this.dependencies.storage?.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export const dockStore = new DockStore();
|
||||
Loaded 100 of 336 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user