mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
cherry-pick of hackweek work plus merge conflicts/build errors
added a route to get all port forwards
Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>
Added Forwarded Ports to cluster dashboard
Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>
working port-forward page (open, edit, remove)
Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>
added local storage to the port-forward store
Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>
automatically restore port-forward after pod is restarted
Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>
start port-forwards using random local port by default, rearranged pod and service port-forward UI
Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>
Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>
This commit is contained in:
parent
ef52e4e9e2
commit
ff7084ec81
@ -40,6 +40,7 @@ export * from "./network-policies";
|
|||||||
export * from "./network";
|
export * from "./network";
|
||||||
export * from "./nodes";
|
export * from "./nodes";
|
||||||
export * from "./pod-disruption-budgets";
|
export * from "./pod-disruption-budgets";
|
||||||
|
export * from "./port-forwards";
|
||||||
export * from "./preferences";
|
export * from "./preferences";
|
||||||
export * from "./releases";
|
export * from "./releases";
|
||||||
export * from "./resource-quotas";
|
export * from "./resource-quotas";
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { endpointRoute } from "./endpoints";
|
|||||||
import { ingressRoute } from "./ingresses";
|
import { ingressRoute } from "./ingresses";
|
||||||
import { networkPoliciesRoute } from "./network-policies";
|
import { networkPoliciesRoute } from "./network-policies";
|
||||||
import { servicesRoute, servicesURL } from "./services";
|
import { servicesRoute, servicesURL } from "./services";
|
||||||
|
import { portForwardsRoute } from "./port-forwards";
|
||||||
|
|
||||||
export const networkRoute: RouteProps = {
|
export const networkRoute: RouteProps = {
|
||||||
path: [
|
path: [
|
||||||
@ -32,6 +33,7 @@ export const networkRoute: RouteProps = {
|
|||||||
endpointRoute,
|
endpointRoute,
|
||||||
ingressRoute,
|
ingressRoute,
|
||||||
networkPoliciesRoute,
|
networkPoliciesRoute,
|
||||||
|
portForwardsRoute,
|
||||||
].map(route => route.path.toString())
|
].map(route => route.path.toString())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
33
src/common/routes/port-forwards.ts
Normal file
33
src/common/routes/port-forwards.ts
Normal file
@ -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 { RouteProps } from "react-router";
|
||||||
|
import { buildURL } from "../utils/buildUrl";
|
||||||
|
|
||||||
|
export const portForwardsRoute: RouteProps = {
|
||||||
|
path: "/port-forwards/:forwardport?"
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PortForwardsRouteParams {
|
||||||
|
forwardport?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const portForwardsURL = buildURL<PortForwardsRouteParams>(portForwardsRoute.path);
|
||||||
@ -182,6 +182,7 @@ export class Router {
|
|||||||
// Port-forward API
|
// Port-forward API
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}/{forwardPort}` }, PortForwardRoute.routePortForward);
|
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}/{forwardPort}` }, PortForwardRoute.routePortForward);
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}/{forwardPort}` }, PortForwardRoute.routeCurrentPortForward);
|
this.router.add({ method: "get", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}/{forwardPort}` }, PortForwardRoute.routeCurrentPortForward);
|
||||||
|
this.router.add({ method: "get", path: `${apiPrefix}/port-forwards` }, PortForwardRoute.routeAllPortForwards);
|
||||||
this.router.add({ method: "delete", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}/{forwardPort}` }, PortForwardRoute.routeCurrentPortForwardStop);
|
this.router.add({ method: "delete", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}/{forwardPort}` }, PortForwardRoute.routeCurrentPortForwardStop);
|
||||||
|
|
||||||
// Helm API
|
// Helm API
|
||||||
|
|||||||
@ -99,6 +99,8 @@ class PortForward {
|
|||||||
try {
|
try {
|
||||||
await tcpPortUsed.waitUntilUsed(this.internalPort, 500, 15000);
|
await tcpPortUsed.waitUntilUsed(this.internalPort, 500, 15000);
|
||||||
|
|
||||||
|
this.forwardPort = String(this.internalPort);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.process.kill();
|
this.process.kill();
|
||||||
@ -134,6 +136,12 @@ export class PortForwardRoute {
|
|||||||
namespace, port, forwardPort,
|
namespace, port, forwardPort,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let thePort: string; // undefined
|
||||||
|
const portNum = Number(forwardPort);
|
||||||
|
if (portNum > 0 && portNum < 65536) {
|
||||||
|
thePort = forwardPort;
|
||||||
|
}
|
||||||
|
|
||||||
if (!portForward) {
|
if (!portForward) {
|
||||||
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
|
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
|
||||||
portForward = new PortForward(await cluster.getProxyKubeconfigPath(), {
|
portForward = new PortForward(await cluster.getProxyKubeconfigPath(), {
|
||||||
@ -142,7 +150,7 @@ export class PortForwardRoute {
|
|||||||
namespace,
|
namespace,
|
||||||
name: resourceName,
|
name: resourceName,
|
||||||
port,
|
port,
|
||||||
forwardPort
|
forwardPort: thePort,
|
||||||
});
|
});
|
||||||
|
|
||||||
const started = await portForward.start();
|
const started = await portForward.start();
|
||||||
@ -151,7 +159,7 @@ export class PortForwardRoute {
|
|||||||
logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { namespace, port, resourceType, resourceName });
|
logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { namespace, port, resourceType, resourceName });
|
||||||
|
|
||||||
return respondJson(response, {
|
return respondJson(response, {
|
||||||
message: `Failed to forward port ${port}`
|
message: `Failed to forward port ${port} to ${thePort ? forwardPort : "random port"}`
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,6 +187,26 @@ export class PortForwardRoute {
|
|||||||
respondJson(response, {port: portForward?.internalPort?? null});
|
respondJson(response, {port: portForward?.internalPort?? null});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async routeAllPortForwards(request: LensApiRequest) {
|
||||||
|
const { response } = request;
|
||||||
|
|
||||||
|
const portForwards: PortForwardArgs[] = [];
|
||||||
|
|
||||||
|
PortForward.portForwards.forEach(f => {
|
||||||
|
const pf: PortForwardArgs = {
|
||||||
|
clusterId: f.clusterId,
|
||||||
|
kind: f.kind,
|
||||||
|
namespace: f.namespace,
|
||||||
|
name: f.name,
|
||||||
|
port: f.port,
|
||||||
|
forwardPort: f.forwardPort,
|
||||||
|
};
|
||||||
|
portForwards.push(pf)
|
||||||
|
});
|
||||||
|
|
||||||
|
respondJson(response, {portForwards});
|
||||||
|
}
|
||||||
|
|
||||||
static async routeCurrentPortForwardStop(request: LensApiRequest) {
|
static async routeCurrentPortForwardStop(request: LensApiRequest) {
|
||||||
const { params, response, cluster} = request;
|
const { params, response, cluster} = request;
|
||||||
const { namespace, port, forwardPort, resourceType, resourceName } = params;
|
const { namespace, port, forwardPort, resourceType, resourceName } = params;
|
||||||
|
|||||||
23
src/renderer/components/+network-port-forwards/index.ts
Normal file
23
src/renderer/components/+network-port-forwards/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./port-forwards";
|
||||||
|
export * from "./port-forward-details";
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.PortForwardDetails {
|
||||||
|
.SubTitle {
|
||||||
|
text-transform: none
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 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 "./port-forward-details.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import type { PortForwardItem } from "./port-forward.store";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
portForward: PortForwardItem;
|
||||||
|
hideDetails(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class PortForwardDetails extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="PortForwardDetails">
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.PortForwardDialog {
|
||||||
|
.Wizard {
|
||||||
|
.header {
|
||||||
|
span {
|
||||||
|
color: #a0a0a0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.WizardStep {
|
||||||
|
.step-content {
|
||||||
|
min-height: 90px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-scale {
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
|
||||||
|
.desired-scale {
|
||||||
|
flex: 1.1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
flex: 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-minus-container {
|
||||||
|
margin-left: $margin * 2;
|
||||||
|
.Icon {
|
||||||
|
--color-active: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: $colorSoftError;
|
||||||
|
font-size: small;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.Icon {
|
||||||
|
margin: 0;
|
||||||
|
margin-right: $margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* 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 "./port-forward-dialog.scss";
|
||||||
|
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { computed, observable, makeObservable } from "mobx";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { Dialog, DialogProps } from "../dialog";
|
||||||
|
import { Wizard, WizardStep } from "../wizard";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { Input } from "../input";
|
||||||
|
import { Notifications } from "../notifications";
|
||||||
|
import { cssNames } from "../../utils";
|
||||||
|
import { PortForwardItem, portForwardStore } from "./port-forward.store";
|
||||||
|
import { isNumber } from "../input/input_validators";
|
||||||
|
|
||||||
|
interface Props extends Partial<DialogProps> {
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogState = observable.object({
|
||||||
|
isOpen: false,
|
||||||
|
data: null as PortForwardItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class PortForwardDialog extends Component<Props> {
|
||||||
|
@observable ready = false;
|
||||||
|
@observable currentPort = 0;
|
||||||
|
@observable desiredPort = 0;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static open(portForward: PortForwardItem) {
|
||||||
|
dialogState.isOpen = true;
|
||||||
|
dialogState.data = portForward;
|
||||||
|
}
|
||||||
|
|
||||||
|
static close() {
|
||||||
|
dialogState.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get portForward() {
|
||||||
|
return dialogState.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
close = () => {
|
||||||
|
PortForwardDialog.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
@computed get scaleMax() {
|
||||||
|
const { currentPort } = this;
|
||||||
|
const defaultMax = 50;
|
||||||
|
|
||||||
|
return currentPort <= defaultMax
|
||||||
|
? defaultMax * 2
|
||||||
|
: currentPort * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen = async () => {
|
||||||
|
const { portForward } = this;
|
||||||
|
|
||||||
|
this.currentPort = +portForward.forwardPort;
|
||||||
|
this.desiredPort = this.currentPort;
|
||||||
|
this.ready = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
onClose = () => {
|
||||||
|
this.ready = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
changePort = async () => {
|
||||||
|
const { portForward } = this;
|
||||||
|
const { currentPort, desiredPort, close } = this;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (currentPort !== desiredPort) {
|
||||||
|
await portForwardStore.modify(portForward, desiredPort);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
} catch (err) {
|
||||||
|
Notifications.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderContents() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex gaps align-center">
|
||||||
|
<div className="input-container flex align-center">
|
||||||
|
<div className="current-port" data-testid="current-port">
|
||||||
|
Current port: {this.currentPort}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
required autoFocus
|
||||||
|
iconLeft="import_export"
|
||||||
|
placeholder="Desired port"
|
||||||
|
trim
|
||||||
|
validators={isNumber}
|
||||||
|
onChange={v => this.desiredPort = +v}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="warning" data-testid="warning">
|
||||||
|
<Icon material="warning"/>
|
||||||
|
Current port-forwarding will be removed and a new one will be started
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { className, ...dialogProps } = this.props;
|
||||||
|
const resourceName = this.portForward ? this.portForward.getName() : "";
|
||||||
|
const header = (
|
||||||
|
<h5>
|
||||||
|
Change Port <span>{resourceName}</span>
|
||||||
|
</h5>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
{...dialogProps}
|
||||||
|
isOpen={dialogState.isOpen}
|
||||||
|
className={cssNames("PortForwardDialog", className)}
|
||||||
|
onOpen={this.onOpen}
|
||||||
|
onClose={this.onClose}
|
||||||
|
close={this.close}
|
||||||
|
>
|
||||||
|
<Wizard header={header} done={this.close}>
|
||||||
|
<WizardStep
|
||||||
|
contentClass="flex gaps column"
|
||||||
|
next={this.changePort}
|
||||||
|
nextLabel="Change Port"
|
||||||
|
disabledNext={!this.ready}
|
||||||
|
>
|
||||||
|
{this.renderContents()}
|
||||||
|
</WizardStep>
|
||||||
|
</Wizard>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* 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 { boundMethod, cssNames, openExternal } from "../../utils";
|
||||||
|
import { PortForwardItem, portForwardStore } from "./port-forward.store";
|
||||||
|
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||||
|
import { MenuItem } from "../menu";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { Notifications } from "../notifications";
|
||||||
|
import { PortForwardDialog } from "./port-forward-dialog";
|
||||||
|
|
||||||
|
interface Props extends MenuActionsProps {
|
||||||
|
portForward: PortForwardItem;
|
||||||
|
hideDetails?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PortForwardMenu extends React.Component<Props> {
|
||||||
|
@boundMethod
|
||||||
|
remove() {
|
||||||
|
return portForwardStore.remove(this.props.portForward);
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
openInBrowser() {
|
||||||
|
const { portForward } = this.props;
|
||||||
|
const browseTo = `http://localhost:${portForward.forwardPort}`;
|
||||||
|
|
||||||
|
openExternal(browseTo)
|
||||||
|
.catch(error => {
|
||||||
|
console.error(`failed to open in browser: ${error}`, {
|
||||||
|
clusterId: portForward.clusterId,
|
||||||
|
port: portForward.port,
|
||||||
|
kind: portForward.kind,
|
||||||
|
namespace: portForward.namespace,
|
||||||
|
name: portForward.name,
|
||||||
|
});
|
||||||
|
Notifications.error(`Failed to open ${browseTo} in browser`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
const { portForward, toolbar } = this.props;
|
||||||
|
|
||||||
|
if (!portForward) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={this.openInBrowser}>
|
||||||
|
<Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser"/>
|
||||||
|
<span className="title">Open</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => PortForwardDialog.open(portForward)}>
|
||||||
|
<Icon material="edit" tooltip="Change port" interactive={toolbar}/>
|
||||||
|
<span className="title">Edit</span>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { className, ...menuProps } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuActions
|
||||||
|
{...menuProps}
|
||||||
|
className={cssNames("PortForwardMenu", className)}
|
||||||
|
removeAction={this.remove}
|
||||||
|
>
|
||||||
|
{this.renderContent()}
|
||||||
|
</MenuActions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,242 @@
|
|||||||
|
/**
|
||||||
|
* 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, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
|
||||||
|
import { ItemObject, ItemStore } from "../../../common/item.store";
|
||||||
|
import { autoBind, createStorage, delay } from "../../utils";
|
||||||
|
import { apiBase } from "../../api";
|
||||||
|
import { waitUntilFree } from "tcp-port-used";
|
||||||
|
import { getHostedClusterId } from "../../utils";
|
||||||
|
import { podsApi } from "../../../common/k8s-api/endpoints";
|
||||||
|
|
||||||
|
interface PortForwardResult {
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForwardedPort {
|
||||||
|
clusterId: string;
|
||||||
|
kind: string;
|
||||||
|
namespace: string;
|
||||||
|
name: string;
|
||||||
|
port: string;
|
||||||
|
forwardPort: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PortForwardsResult {
|
||||||
|
portForwards: ForwardedPort[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PortForwardItem implements ItemObject {
|
||||||
|
clusterId: string;
|
||||||
|
kind: string;
|
||||||
|
namespace: string;
|
||||||
|
name: string;
|
||||||
|
port: string;
|
||||||
|
forwardPort: string;
|
||||||
|
|
||||||
|
interval: NodeJS.Timeout;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
autoBind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNs() {
|
||||||
|
return this.namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this.forwardPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.forwardPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
getKind() {
|
||||||
|
return this.kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPort() {
|
||||||
|
return this.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
getForwardPort() {
|
||||||
|
return this.forwardPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchFields() {
|
||||||
|
return [
|
||||||
|
this.name,
|
||||||
|
this.id,
|
||||||
|
this.kind,
|
||||||
|
this.port,
|
||||||
|
this.forwardPort,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor() {
|
||||||
|
// for now only support pods
|
||||||
|
if (this.kind === "pod") {
|
||||||
|
this.interval = setInterval( this.keepAlive, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async keepAlive() {
|
||||||
|
console.log("keepAlive() called");
|
||||||
|
let pod = await podsApi.get({name: this.getName(), namespace: this.getNs()});
|
||||||
|
if (!pod || pod.metadata.deletionTimestamp) {
|
||||||
|
console.log("keepAlive() pod lost!");
|
||||||
|
clearInterval(this.interval);
|
||||||
|
this.interval = undefined;
|
||||||
|
// wait for resource
|
||||||
|
while (true) {
|
||||||
|
await delay(1000);
|
||||||
|
pod = await podsApi.get({name: this.getName(), namespace: this.getNs()});
|
||||||
|
if (pod) {
|
||||||
|
if (pod.getContainerStatuses()?.[0]?.ready) {
|
||||||
|
console.log("keepAlive() pod found!");
|
||||||
|
// restart port forward
|
||||||
|
await portForwardStore.modify(this, +this.getForwardPort());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PortForwardStore extends ItemStore<PortForwardItem> {
|
||||||
|
private storage = createStorage<ForwardedPort[] | undefined>("port_forwards", undefined);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
makeObservable(this);
|
||||||
|
autoBind(this);
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
await this.storage.whenReady;
|
||||||
|
|
||||||
|
const savedPortForwards = this.storage.get(); // undefined on first load
|
||||||
|
|
||||||
|
if (Array.isArray(savedPortForwards)) {
|
||||||
|
await Promise.all(savedPortForwards.map(pf => {
|
||||||
|
const port = new PortForwardItem;
|
||||||
|
port.clusterId = pf.clusterId;
|
||||||
|
port.kind = pf.kind;
|
||||||
|
port.namespace = pf.namespace;
|
||||||
|
port.name = pf.name;
|
||||||
|
port.port = pf.port;
|
||||||
|
port.forwardPort = pf.forwardPort;
|
||||||
|
return this.add(port);
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.portForwards = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@observable selectedItemId?: string;
|
||||||
|
@observable portForwards: PortForwardItem[];
|
||||||
|
|
||||||
|
@computed get selectedItem() {
|
||||||
|
return this.portForwards.find((e: ItemObject) => e.getId() === this.selectedItemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch() {
|
||||||
|
const disposers: IReactionDisposer[] = [
|
||||||
|
reaction(() => this.portForwards, () => this.loadAll()),
|
||||||
|
];
|
||||||
|
|
||||||
|
return () => disposers.forEach((dispose) => dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAll() {
|
||||||
|
return this.loadItems(async () => {
|
||||||
|
const response = await apiBase.get<PortForwardsResult>(`/port-forwards`, {});
|
||||||
|
|
||||||
|
const portForwards = response.portForwards?.filter(pf => pf.clusterId == getHostedClusterId());
|
||||||
|
this.storage.set(portForwards);
|
||||||
|
|
||||||
|
this.reset();
|
||||||
|
portForwards?.forEach(pf => {
|
||||||
|
const port = new PortForwardItem;
|
||||||
|
port.clusterId = pf.clusterId;
|
||||||
|
port.kind = pf.kind;
|
||||||
|
port.namespace = pf.namespace;
|
||||||
|
port.name = pf.name;
|
||||||
|
port.port = pf.port;
|
||||||
|
port.forwardPort = pf.forwardPort;
|
||||||
|
port.monitor();
|
||||||
|
this.portForwards.push(port);
|
||||||
|
})
|
||||||
|
return this.portForwards;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.portForwards?.forEach(pf => {
|
||||||
|
if (pf.interval) clearInterval(pf.interval)
|
||||||
|
});
|
||||||
|
this.portForwards = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(portForward: PortForwardItem) {
|
||||||
|
await add(portForward);
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async modify(portForward: PortForwardItem, desiredPort: number) {
|
||||||
|
await remove(portForward);
|
||||||
|
portForward.forwardPort = desiredPort.toString();
|
||||||
|
await add(portForward);
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(portForward: PortForwardItem) {
|
||||||
|
await remove(portForward);
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeSelectedItems() {
|
||||||
|
return Promise.all(this.selectedItems.map(this.remove));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function add(portForward: PortForwardItem) {
|
||||||
|
const response = await apiBase.post<PortForwardResult>(`/pods/${portForward.getNs()}/${portForward.getKind()}/${portForward.getName()}/port-forward/${portForward.getPort()}/${portForward.getForwardPort()}`, {});
|
||||||
|
if (response?.port != +portForward.getForwardPort() ) {
|
||||||
|
console.log(`specified ${portForward.getForwardPort()} got ${response.port}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(portForward: PortForwardItem) {
|
||||||
|
await apiBase.del(`/pods/${portForward.getNs()}/${portForward.getKind()}/${portForward.getName()}/port-forward/${portForward.getPort()}/${portForward.forwardPort}`, {})
|
||||||
|
await waitUntilFree(+portForward.getForwardPort(), 200, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const portForwardStore = new PortForwardStore();
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.PortForwards {
|
||||||
|
.TableCell {
|
||||||
|
&.warning {
|
||||||
|
@include table-cell-warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
src/renderer/components/+network-port-forwards/port-forwards.tsx
Normal file
136
src/renderer/components/+network-port-forwards/port-forwards.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* 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 "./port-forwards.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
import type { RouteComponentProps } from "react-router-dom";
|
||||||
|
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
||||||
|
import { PortForwardItem, portForwardStore } from "./port-forward.store";
|
||||||
|
import { PortForwardsRouteParams, portForwardsURL } from "../../../common/routes";
|
||||||
|
import { navigation } from "../../navigation";
|
||||||
|
import { PortForwardDetails } from "./port-forward-details";
|
||||||
|
import { PortForwardMenu } from "./port-forward-menu";
|
||||||
|
|
||||||
|
enum columnId {
|
||||||
|
name = "name",
|
||||||
|
namespace = "namespace",
|
||||||
|
kind = "kind",
|
||||||
|
port = "port",
|
||||||
|
forwardPort = "forwardPort",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends RouteComponentProps<PortForwardsRouteParams> {
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class PortForwards extends React.Component<Props> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
portForwardStore.watch(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedPortForward() {
|
||||||
|
const { match: { params: { forwardport } } } = this.props;
|
||||||
|
|
||||||
|
return portForwardStore.items.find(pf => {
|
||||||
|
return pf.getForwardPort() == forwardport;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showDetails = (item: PortForwardItem) => {
|
||||||
|
navigation.push(portForwardsURL({
|
||||||
|
params: {
|
||||||
|
forwardport: item.getForwardPort(),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
hideDetails = () => {
|
||||||
|
navigation.push(portForwardsURL());
|
||||||
|
};
|
||||||
|
|
||||||
|
renderRemoveDialogMessage(selectedItems: PortForwardItem[]) {
|
||||||
|
const forwardPorts = selectedItems.map(item => item.getForwardPort()).join(", ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<>Stop forwarding from <b>{forwardPorts}</b>?</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ItemListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="port_forwards"
|
||||||
|
className="PortForwards" store={portForwardStore}
|
||||||
|
sortingCallbacks={{
|
||||||
|
[columnId.name]: item => item.getName(),
|
||||||
|
[columnId.namespace]: item => item.getNs(),
|
||||||
|
[columnId.kind]: item => item.getKind(),
|
||||||
|
[columnId.port]: item => item.getPort(),
|
||||||
|
[columnId.forwardPort]: item => item.getForwardPort(),
|
||||||
|
}}
|
||||||
|
searchFilters={[
|
||||||
|
item => item.getSearchFields(),
|
||||||
|
]}
|
||||||
|
renderHeaderTitle="Forwarded Ports"
|
||||||
|
renderTableHeader={[
|
||||||
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
|
{ title: "Kind", className: "kind", sortBy: columnId.kind, id: columnId.kind },
|
||||||
|
{ title: "Pod Port", className: "port", sortBy: columnId.port, id: columnId.port },
|
||||||
|
{ title: "Local Port", className: "forwardPort", sortBy: columnId.forwardPort, id: columnId.forwardPort },
|
||||||
|
]}
|
||||||
|
renderTableContents={item => [
|
||||||
|
item.getName(),
|
||||||
|
item.getNs(),
|
||||||
|
item.getKind(),
|
||||||
|
item.getPort(),
|
||||||
|
item.getForwardPort(),
|
||||||
|
]}
|
||||||
|
renderItemMenu={pf => (
|
||||||
|
<PortForwardMenu
|
||||||
|
portForward={pf}
|
||||||
|
removeConfirmationMessage={this.renderRemoveDialogMessage([pf])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
customizeRemoveDialog={selectedItems => ({
|
||||||
|
message: this.renderRemoveDialogMessage(selectedItems)
|
||||||
|
})}
|
||||||
|
detailsItem={this.selectedPortForward}
|
||||||
|
onDetails={this.showDetails}
|
||||||
|
/>
|
||||||
|
<PortForwardDetails
|
||||||
|
portForward={this.selectedPortForward}
|
||||||
|
hideDetails={this.hideDetails}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -75,6 +75,10 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
|
|
||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
|
if (!this.forwardPort) {
|
||||||
|
this.forwardPort = 0;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiBase.post<PortForwardResult>(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}/${this.forwardPort}`, {});
|
const response = await apiBase.post<PortForwardResult>(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}/${this.forwardPort}`, {});
|
||||||
|
|
||||||
@ -117,7 +121,8 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
||||||
{port.toString()}
|
{port.toString()}
|
||||||
{" "}
|
{" "}
|
||||||
<text>to</text>
|
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop":"Forward"} </Button>
|
||||||
|
<text> local port:</text>
|
||||||
<Input className={"portInput"}
|
<Input className={"portInput"}
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
@ -127,7 +132,6 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
placeholder={"Random"}
|
placeholder={"Random"}
|
||||||
onChange={(value) => this.forwardPort = Number(value)}
|
onChange={(value) => this.forwardPort = Number(value)}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop":"Forward"} </Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import { Services } from "../+network-services";
|
|||||||
import { Endpoints } from "../+network-endpoints";
|
import { Endpoints } from "../+network-endpoints";
|
||||||
import { Ingresses } from "../+network-ingresses";
|
import { Ingresses } from "../+network-ingresses";
|
||||||
import { NetworkPolicies } from "../+network-policies";
|
import { NetworkPolicies } from "../+network-policies";
|
||||||
|
import { PortForwards } from "../+network-port-forwards";
|
||||||
import { isAllowedResource } from "../../../common/utils/allowed-resource";
|
import { isAllowedResource } from "../../../common/utils/allowed-resource";
|
||||||
import * as routes from "../../../common/routes";
|
import * as routes from "../../../common/routes";
|
||||||
|
|
||||||
@ -72,6 +73,13 @@ export class Network extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tabs.push({
|
||||||
|
title: "Forwarded Ports",
|
||||||
|
component: PortForwards,
|
||||||
|
url: routes.portForwardsURL(),
|
||||||
|
routePath: routes.portForwardsRoute.path.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -79,6 +79,10 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
|
|
||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
|
if (!this.forwardPort) {
|
||||||
|
this.forwardPort = 0;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiBase.post<PortForwardResult>(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}/${this.forwardPort}`, {});
|
const response = await apiBase.post<PortForwardResult>(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}/${this.forwardPort}`, {});
|
||||||
|
|
||||||
@ -123,7 +127,8 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
||||||
{text}
|
{text}
|
||||||
{" "}
|
{" "}
|
||||||
<text>to</text>
|
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop":"Forward"} </Button>
|
||||||
|
<text> local port:</text>
|
||||||
<Input className={"portInput"}
|
<Input className={"portInput"}
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
@ -133,7 +138,6 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
placeholder={"Random"}
|
placeholder={"Random"}
|
||||||
onChange={(value) => this.forwardPort = Number(value)}
|
onChange={(value) => this.forwardPort = Number(value)}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop":"Forward"} </Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,6 +74,7 @@ import { ClusterStore } from "../../common/cluster-store";
|
|||||||
import type { ClusterId } from "../../common/cluster-types";
|
import type { ClusterId } from "../../common/cluster-types";
|
||||||
import { watchHistoryState } from "../remote-helpers/history-updater";
|
import { watchHistoryState } from "../remote-helpers/history-updater";
|
||||||
import { unmountComponentAtNode } from "react-dom";
|
import { unmountComponentAtNode } from "react-dom";
|
||||||
|
import { PortForwardDialog } from "./+network-port-forwards/port-forward-dialog";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class App extends React.Component {
|
export class App extends React.Component {
|
||||||
@ -231,6 +232,7 @@ export class App extends React.Component {
|
|||||||
<StatefulSetScaleDialog/>
|
<StatefulSetScaleDialog/>
|
||||||
<ReplicaSetScaleDialog/>
|
<ReplicaSetScaleDialog/>
|
||||||
<CronJobTriggerDialog/>
|
<CronJobTriggerDialog/>
|
||||||
|
<PortForwardDialog/>
|
||||||
<CommandContainer clusterId={App.clusterId}/>
|
<CommandContainer clusterId={App.clusterId}/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user