1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>
This commit is contained in:
Jim Ehrismann 2021-09-29 22:35:00 -04:00
parent ff7084ec81
commit cd943bd34c
11 changed files with 424 additions and 375 deletions

View File

@ -138,6 +138,7 @@ export class PortForwardRoute {
let thePort: string; // undefined let thePort: string; // undefined
const portNum = Number(forwardPort); const portNum = Number(forwardPort);
if (portNum > 0 && portNum < 65536) { if (portNum > 0 && portNum < 65536) {
thePort = forwardPort; thePort = forwardPort;
} }
@ -201,7 +202,8 @@ export class PortForwardRoute {
port: f.port, port: f.port,
forwardPort: f.forwardPort, forwardPort: f.forwardPort,
}; };
portForwards.push(pf)
portForwards.push(pf);
}); });
respondJson(response, {portForwards}); respondJson(response, {portForwards});

View File

@ -23,7 +23,7 @@ import "./port-forward-details.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { PortForwardItem } from "./port-forward.store"; import type { PortForwardItem } from "../../port-forward";
interface Props { interface Props {
portForward: PortForwardItem; portForward: PortForwardItem;

View File

@ -30,73 +30,73 @@ import { Icon } from "../icon";
import { Input } from "../input"; import { Input } from "../input";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { PortForwardItem, portForwardStore } from "./port-forward.store"; import { modifyPortForward, PortForwardItem } from "../../port-forward";
import { isNumber } from "../input/input_validators"; import { isNumber } from "../input/input_validators";
interface Props extends Partial<DialogProps> { interface Props extends Partial<DialogProps> {
} }
const dialogState = observable.object({ const dialogState = observable.object({
isOpen: false, isOpen: false,
data: null as PortForwardItem, data: null as PortForwardItem,
}); });
@observer @observer
export class PortForwardDialog extends Component<Props> { export class PortForwardDialog extends Component<Props> {
@observable ready = false; @observable ready = false;
@observable currentPort = 0; @observable currentPort = 0;
@observable desiredPort = 0; @observable desiredPort = 0;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
makeObservable(this); makeObservable(this);
} }
static open(portForward: PortForwardItem) { static open(portForward: PortForwardItem) {
dialogState.isOpen = true; dialogState.isOpen = true;
dialogState.data = portForward; dialogState.data = portForward;
} }
static close() { static close() {
dialogState.isOpen = false; dialogState.isOpen = false;
} }
get portForward() { get portForward() {
return dialogState.data; return dialogState.data;
} }
close = () => { close = () => {
PortForwardDialog.close(); PortForwardDialog.close();
}; };
@computed get scaleMax() { @computed get scaleMax() {
const { currentPort } = this; const { currentPort } = this;
const defaultMax = 50; const defaultMax = 50;
return currentPort <= defaultMax return currentPort <= defaultMax
? defaultMax * 2 ? defaultMax * 2
: currentPort * 2; : currentPort * 2;
} }
onOpen = async () => { onOpen = async () => {
const { portForward } = this; const { portForward } = this;
this.currentPort = +portForward.forwardPort; this.currentPort = +portForward.forwardPort;
this.desiredPort = this.currentPort; this.desiredPort = this.currentPort;
this.ready = true; this.ready = true;
}; };
onClose = () => { onClose = () => {
this.ready = false; this.ready = false;
}; };
changePort = async () => { changePort = async () => {
const { portForward } = this; const { portForward } = this;
const { currentPort, desiredPort, close } = this; const { currentPort, desiredPort, close } = this;
try { try {
if (currentPort !== desiredPort) { if (currentPort !== desiredPort) {
await portForwardStore.modify(portForward, desiredPort); await modifyPortForward(portForward, desiredPort);
} }
close(); close();
} catch (err) { } catch (err) {
@ -104,13 +104,13 @@ const dialogState = observable.object({
} }
}; };
renderContents() { renderContents() {
return ( return (
<> <>
<div className="flex gaps align-center"> <div className="flex gaps align-center">
<div className="input-container flex align-center"> <div className="input-container flex align-center">
<div className="current-port" data-testid="current-port"> <div className="current-port" data-testid="current-port">
Current port: {this.currentPort} Current port: {this.currentPort}
</div> </div>
<Input <Input
required autoFocus required autoFocus
@ -123,43 +123,42 @@ const dialogState = observable.object({
</div> </div>
</div> </div>
<div className="warning" data-testid="warning"> <div className="warning" data-testid="warning">
<Icon material="warning"/> <Icon material="warning" />
Current port-forwarding will be removed and a new one will be started Current port-forwarding will be removed and a new one will be started
</div> </div>
</> </>
); );
} }
render() { render() {
const { className, ...dialogProps } = this.props; const { className, ...dialogProps } = this.props;
const resourceName = this.portForward ? this.portForward.getName() : ""; const resourceName = this.portForward ? this.portForward.getName() : "";
const header = ( const header = (
<h5> <h5>
Change Port <span>{resourceName}</span> Change Port <span>{resourceName}</span>
</h5> </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>
);
}
}
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>
);
}
}

View File

@ -21,7 +21,7 @@
import React from "react"; import React from "react";
import { boundMethod, cssNames, openExternal } from "../../utils"; import { boundMethod, cssNames, openExternal } from "../../utils";
import { PortForwardItem, portForwardStore } from "./port-forward.store"; import { PortForwardItem, removePortForward } from "../../port-forward";
import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
import { Icon } from "../icon"; import { Icon } from "../icon";
@ -36,7 +36,7 @@ interface Props extends MenuActionsProps {
export class PortForwardMenu extends React.Component<Props> { export class PortForwardMenu extends React.Component<Props> {
@boundMethod @boundMethod
remove() { remove() {
return portForwardStore.remove(this.props.portForward); return removePortForward(this.props.portForward);
} }
@boundMethod @boundMethod
@ -55,7 +55,7 @@ export class PortForwardMenu extends React.Component<Props> {
}); });
Notifications.error(`Failed to open ${browseTo} in browser`); Notifications.error(`Failed to open ${browseTo} in browser`);
} }
); );
} }
renderContent() { renderContent() {
@ -66,29 +66,28 @@ export class PortForwardMenu extends React.Component<Props> {
return ( return (
<> <>
<MenuItem onClick={this.openInBrowser}> <MenuItem onClick={this.openInBrowser}>
<Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser"/> <Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser" />
<span className="title">Open</span> <span className="title">Open</span>
</MenuItem> </MenuItem>
<MenuItem onClick={() => PortForwardDialog.open(portForward)}> <MenuItem onClick={() => PortForwardDialog.open(portForward)}>
<Icon material="edit" tooltip="Change port" interactive={toolbar}/> <Icon material="edit" tooltip="Change port" interactive={toolbar} />
<span className="title">Edit</span> <span className="title">Edit</span>
</MenuItem> </MenuItem>
</> </>
); );
} }
render() { render() {
const { className, ...menuProps } = this.props; const { className, ...menuProps } = this.props;
return (
<MenuActions
{...menuProps}
className={cssNames("PortForwardMenu", className)}
removeAction={this.remove}
>
{this.renderContent()}
</MenuActions>
);
}
}
return (
<MenuActions
{...menuProps}
className={cssNames("PortForwardMenu", className)}
removeAction={this.remove}
>
{this.renderContent()}
</MenuActions>
);
}
}

View File

@ -1,242 +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 { 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();

View File

@ -25,7 +25,7 @@ import React from "react";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import type { RouteComponentProps } from "react-router-dom"; import type { RouteComponentProps } from "react-router-dom";
import { ItemListLayout } from "../item-object-list/item-list-layout"; import { ItemListLayout } from "../item-object-list/item-list-layout";
import { PortForwardItem, portForwardStore } from "./port-forward.store"; import { PortForwardItem, portForwardStore } from "../../port-forward";
import { PortForwardsRouteParams, portForwardsURL } from "../../../common/routes"; import { PortForwardsRouteParams, portForwardsURL } from "../../../common/routes";
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { PortForwardDetails } from "./port-forward-details"; import { PortForwardDetails } from "./port-forward-details";

View File

@ -30,6 +30,7 @@ import { cssNames } from "../../utils";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { Button } from "../button"; import { Button } from "../button";
import { Input } from "../input"; import { Input } from "../input";
import { portForwardStore } from "../../port-forward/port-forward.store";
interface Props { interface Props {
pod: Pod; pod: Pod;
@ -88,6 +89,7 @@ export class PodContainerPort extends React.Component<Props> {
this.forwardPort = response.port; this.forwardPort = response.port;
this.isPortForwarded = true; this.isPortForwarded = true;
portForwardStore.reset();
} catch(error) { } catch(error) {
Notifications.error(error); Notifications.error(error);
} finally { } finally {
@ -103,6 +105,7 @@ export class PodContainerPort extends React.Component<Props> {
try { try {
await apiBase.del(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}/${this.forwardPort}`, {}); await apiBase.del(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}/${this.forwardPort}`, {});
this.isPortForwarded = false; this.isPortForwarded = false;
portForwardStore.reset();
} catch(error) { } catch(error) {
Notifications.error(error); Notifications.error(error);
} finally { } finally {

View File

@ -35,6 +35,8 @@ import { ContainerCharts } from "./container-charts";
import { LocaleDate } from "../locale-date"; import { LocaleDate } from "../locale-date";
import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { portForwardStore } from "../../port-forward/port-forward.store";
import { disposeOnUnmount } from "mobx-react";
interface Props { interface Props {
pod: Pod; pod: Pod;
@ -44,6 +46,12 @@ interface Props {
export class PodDetailsContainer extends React.Component<Props> { export class PodDetailsContainer extends React.Component<Props> {
componentDidMount() {
disposeOnUnmount(this, [
portForwardStore.watch(),
]);
}
renderStatus(state: string, status: IPodContainerStatus) { renderStatus(state: string, status: IPodContainerStatus) {
const ready = status ? status.ready : ""; const ready = status ? status.ready : "";

View 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-forward.store";
export * from "./port-forward-item";

View File

@ -0,0 +1,75 @@
/**
* 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 { ItemObject } from "../../common/item.store";
import { autoBind } from "../../common/utils";
export class PortForwardItem implements ItemObject {
clusterId: string;
kind: string;
namespace: string;
name: string;
port: string;
forwardPort: string;
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,
];
}
}

View File

@ -0,0 +1,182 @@
/**
* 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 } from "../utils";
import { getHostedClusterId } from "../utils";
import { PortForwardItem } from "./port-forward-item";
import { apiBase } from "../api";
import { waitUntilFree } from "tcp-port-used";
export interface ForwardedPort {
clusterId: string;
kind: string;
namespace: string;
name: string;
port: string;
forwardPort: string;
}
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 addPortForward(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 () => {
let portForwards = await getPortForwards();
// filter out any not for this cluster
portForwards = 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;
this.portForwards.push(port);
});
return this.portForwards;
});
}
reset() {
this.portForwards = [];
}
async removeSelectedItems() {
return Promise.all(this.selectedItems.map(removePortForward));
}
}
interface PortForwardResult {
port: number;
}
interface PortForwardsResult {
portForwards: ForwardedPort[];
}
export async function addPortForward(portForward: PortForwardItem): Promise<number> {
let response;
try {
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}`);
}
} catch (error) {
console.error(error);
}
portForwardStore.reset();
return response?.port;
}
export async function modifyPortForward(portForward: PortForwardItem, desiredPort: number) {
try {
await removePortForward(portForward);
portForward.forwardPort = desiredPort.toString();
await addPortForward(portForward);
} catch (error) {
console.error(error);
}
portForwardStore.reset();
}
export async function removePortForward(portForward: PortForwardItem) {
try {
await apiBase.del(`/pods/${portForward.getNs()}/${portForward.getKind()}/${portForward.getName()}/port-forward/${portForward.getPort()}/${portForward.forwardPort}`, {});
await waitUntilFree(+portForward.getForwardPort(), 200, 1000);
} catch (error) {
console.error(error);
}
portForwardStore.reset();
}
export async function getPortForwards(): Promise<ForwardedPort[]> {
try {
const response = await apiBase.get<PortForwardsResult>(`/port-forwards`, {});
return response.portForwards;
} catch (error) {
console.error(error);
return [];
}
}
export const portForwardStore = new PortForwardStore();