@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
227 lines (226 loc) • 11.5 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/*
* Copyright 2025 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Icon, InlineIcon } from '@iconify/react';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import MuiLink from '@mui/material/Link';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { useTheme } from '@mui/system';
import { useSnackbar } from 'notistack';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { isDockerDesktop } from '../../helpers/isDockerDesktop';
import { listPortForward, startPortForward, stopOrDeletePortForward } from '../../lib/k8s/apiProxy';
import { getCluster } from '../../lib/util';
import { Link, Loader, SectionBox, SimpleTable, StatusLabel } from '../common';
import { PORT_FORWARD_RUNNING_STATUS, PORT_FORWARD_STOP_STATUS, PORT_FORWARDS_STORAGE_KEY, } from '../common/Resource/PortForward';
var PortForwardAction;
(function (PortForwardAction) {
PortForwardAction["Start"] = "Start";
PortForwardAction["Stop"] = "Stop";
PortForwardAction["Delete"] = "Delete";
})(PortForwardAction || (PortForwardAction = {}));
export default function PortForwardingList() {
const [portforwards, setPortForwards] = React.useState([]);
const theme = useTheme();
const [anchorEl, setAnchorEl] = React.useState(null);
const [portForwardInAction, setPortForwardInAction] = React.useState(null);
const { enqueueSnackbar } = useSnackbar();
const cluster = getCluster();
const { t, i18n } = useTranslation(['translation', 'glossary']);
const optionsTranslated = React.useMemo(() => ({
[PortForwardAction.Start]: t('translation|Start'),
[PortForwardAction.Stop]: t('translation|Stop'),
[PortForwardAction.Delete]: t('translation|Delete'),
}), [i18n.language]);
const options = Object.keys(optionsTranslated);
function fetchPortForwardList(showError) {
const cluster = getCluster();
if (!cluster)
return;
// fetch port forwarding list
listPortForward(cluster).then(portforwards => {
const massagedPortForwards = portforwards === null ? [] : portforwards;
massagedPortForwards.forEach((portforward) => {
if (portForwardInAction?.id === portforward.id) {
if (portforward.Error && showError) {
enqueueSnackbar(portforward.Error, {
key: 'portforward-error',
preventDuplicate: true,
autoHideDuration: 3000,
variant: 'error',
});
}
}
});
// sync portforwards from backend with localStorage
const portforwardInStorage = localStorage.getItem(PORT_FORWARDS_STORAGE_KEY);
const parsedPortForwards = JSON.parse(portforwardInStorage || '[]');
parsedPortForwards.forEach((portforward) => {
const index = massagedPortForwards.findIndex((pf) => pf.id === portforward.id);
if (index === -1) {
portforward.status = PORT_FORWARD_STOP_STATUS;
massagedPortForwards.push(portforward);
}
});
localStorage.setItem(PORT_FORWARDS_STORAGE_KEY, JSON.stringify(
// in the locaStorage we store portforward status as stop
// this is because the correct status is always present on the backend
// the localStorage portforwards are used specifically when the user relaunches the app
massagedPortForwards.map((portforward) => {
const newPortforward = { ...portforward };
newPortforward.status = PORT_FORWARD_STOP_STATUS;
return newPortforward;
})));
setPortForwards(massagedPortForwards);
});
}
React.useEffect(() => {
fetchPortForwardList();
}, []);
const handleClick = (event, portforward) => {
setPortForwardInAction(portforward);
setAnchorEl(event.currentTarget);
};
const handleClose = (option) => {
setAnchorEl(null);
if (!option || typeof option !== 'string') {
return;
}
const { id, namespace, cluster, port, targetPort, pod, service, serviceNamespace } = portForwardInAction;
let address = 'localhost';
if (isDockerDesktop()) {
address = '0.0.0.0';
}
portForwardInAction.loading = true;
setPortForwardInAction(portForwardInAction);
if (option === PortForwardAction.Start) {
// start portforward
startPortForward(cluster, namespace, pod, targetPort, service, serviceNamespace, port, address, id).then(() => {
portForwardInAction.loading = false;
setPortForwardInAction(portForwardInAction);
// update portforward list item
fetchPortForwardList(true);
});
}
if (option === PortForwardAction.Stop) {
// stop portforward
stopOrDeletePortForward(cluster, id, true).finally(() => {
portForwardInAction.loading = false;
setPortForwardInAction(portForwardInAction);
// update portforward list item
fetchPortForwardList(true);
});
}
if (option === PortForwardAction.Delete) {
// delete portforward
stopOrDeletePortForward(cluster, id, false).finally(() => {
portForwardInAction.loading = false;
setPortForwardInAction(portForwardInAction);
// remove portforward from storage too
const portforwardInStorage = localStorage.getItem(PORT_FORWARDS_STORAGE_KEY);
const parsedPortForwards = JSON.parse(portforwardInStorage || '[]');
const index = parsedPortForwards.findIndex((pf) => pf.id === id);
if (index !== -1) {
parsedPortForwards.splice(index, 1);
}
localStorage.setItem(PORT_FORWARDS_STORAGE_KEY, JSON.stringify(parsedPortForwards));
// update portforward list item
fetchPortForwardList(true);
});
}
};
function prepareStatusLabel(portforward) {
if (portForwardInAction?.id === portforward.id && portForwardInAction.loading) {
return _jsx(Loader, { noContainer: true, title: t('translation|Loading port forwarding'), size: 30 });
}
const error = portforward.error;
if (error) {
return _jsx(StatusLabel, { status: "error", children: t('translation|Error') });
}
return (_jsx(StatusLabel, { status: portforward.status === PORT_FORWARD_RUNNING_STATUS ? 'success' : '', children: portforward.status }));
}
return (_jsx(SectionBox, { title: t('glossary|Port Forwarding'), children: _jsx(SimpleTable, { columns: [
{
label: t('translation|Name'),
getter: portforward => {
const podOrService = portforward.service ? 'service' : 'pod';
const name = portforward.service || portforward.pod;
const namespace = portforward.serviceNamespace || portforward.namespace;
return (_jsx(Link, { routeName: podOrService, params: { name, namespace }, children: name }));
},
},
{
label: t('glossary|Namespace'),
getter: portforward => {
return portforward.serviceNamespace || portforward.namespace;
},
},
{
label: t('glossary|Kind'),
getter: portforward => {
return !!portforward.service ? 'Service' : 'Pod';
},
},
{
label: t('translation|Pod Port'),
getter: portforward => {
return portforward.targetPort;
},
},
{
label: t('translation|Local Port'),
getter: portforward => {
return (_jsx(Box, { display: 'flex', alignItems: "center", children: _jsxs(MuiLink, { onClick: () => {
window.open(`http://localhost:${portforward.port}`, '_blank');
}, sx: theme => portforward.status === PORT_FORWARD_RUNNING_STATUS
? {
cursor: 'pointer',
marginRight: theme.spacing(1),
}
: {
pointerEvents: 'none',
color: theme.palette.text.disabled,
}, children: [portforward.port, _jsx(InlineIcon, { icon: 'mdi:open-in-new', style: { marginLeft: theme.spacing(0.5) } })] }) }));
},
},
{
label: t('translation|Status'),
getter: portforward => {
return prepareStatusLabel(portforward);
},
},
{
label: t('translation|Actions'),
getter: portforward => {
const filteredOptions = options.filter(option => {
if (portForwardInAction?.error) {
return option === PortForwardAction.Delete;
}
if (portForwardInAction?.status === PORT_FORWARD_RUNNING_STATUS) {
return option !== PortForwardAction.Start;
}
else if (portForwardInAction?.status === PORT_FORWARD_STOP_STATUS) {
return option !== PortForwardAction.Stop;
}
});
return (_jsxs(_Fragment, { children: [_jsx(IconButton, { "aria-label": t('translation|More'), onClick: e => handleClick(e, portforward), size: "medium", children: _jsx(Icon, { icon: 'mdi:dots-vertical' }) }), _jsx(Menu, { anchorEl: anchorEl, open: Boolean(anchorEl), onClose: handleClose, children: filteredOptions.map(option => (_jsx(MenuItem, { onClick: () => handleClose(option), children: optionsTranslated[option] }))) })] }));
},
},
], data: portforwards.filter((pf) => pf.cluster === cluster) }) }));
}