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:
Janne SavolainenandMikko Aspiala committed 2022-01-05 13:26:49 +02:00
1 parent 908a40975e
commit 9d33fff906
336 files changed
+9105 -3307

No files matched your search

+2 -2
View File
@@ -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
View File
@@ -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;
@@ -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 {
-251
View File
@@ -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,
}),
},
);
@@ -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;
@@ -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,
}),
},
);
@@ -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;
@@ -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);
}
}
@@ -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}
/>,
);
+45 -31
View File
@@ -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),
}),
},
);
@@ -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;
@@ -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: {
@@ -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,
@@ -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(
", ",
@@ -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;
@@ -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,
}),
},
);
@@ -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;
@@ -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,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,
}),
},
);
@@ -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;
@@ -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;
@@ -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);
@@ -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";
@@ -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";
@@ -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";
@@ -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", () => {
@@ -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;
};
@@ -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;
@@ -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,
});
@@ -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;
@@ -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