UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

271 lines (270 loc) 14.7 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } 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 ListItemText from '@mui/material/ListItemText'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; 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/api/v1/portForward'; import { getCluster } from '../../lib/util'; import { StatusLabel } from '../common/Label'; import Link from '../common/Link'; import Loader from '../common/Loader'; import { PORT_FORWARD_RUNNING_STATUS, PORT_FORWARD_STOP_STATUS, PORT_FORWARDS_STORAGE_KEY, } from '../common/Resource/PortForward'; import SectionBox from '../common/SectionBox'; import Table from '../common/Table'; import PortForwardStartDialog from './PortForwardStartDialog'; 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 [portForwardInAction, setPortForwardInAction] = React.useState(null); const [startDialogOpen, setStartDialogOpen] = React.useState(false); const [selectedForStart, setSelectedForStart] = 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'), }), // eslint-disable-next-line react-hooks/exhaustive-deps [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(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleAction = (option, portforward, closeMenu) => { closeMenu(); if (!option || typeof option !== 'string') { return; } const { id, namespace, cluster } = portforward; if (option === PortForwardAction.Start) { let address = 'localhost'; if (isDockerDesktop()) { address = '0.0.0.0'; } setSelectedForStart({ ...portforward, cluster, namespace, address }); setStartDialogOpen(true); return; } if (option === PortForwardAction.Stop) { setPortForwardInAction({ ...portforward, loading: true }); // stop portforward stopOrDeletePortForward(cluster, id, true).finally(() => { setPortForwardInAction(null); // update portforward list item fetchPortForwardList(true); }); } if (option === PortForwardAction.Delete) { setPortForwardInAction({ ...portforward, loading: true }); // delete portforward stopOrDeletePortForward(cluster, id, false).finally(() => { setPortForwardInAction(null); // 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); }); } }; const PortForwardContextMenu = ({ portforward }) => { const [anchorEl, setAnchorEl] = React.useState(null); const menuId = `pf-actions-${portforward.id}`; const filteredOptions = options.filter(option => { if (portforward.error) { return option === PortForwardAction.Delete; } if (portforward.status === PORT_FORWARD_RUNNING_STATUS) { return option !== PortForwardAction.Start; } else if (portforward.status === PORT_FORWARD_STOP_STATUS) { return option !== PortForwardAction.Stop; } return true; }); function closeMenu() { setAnchorEl(null); } return (_jsxs(_Fragment, { children: [_jsx(IconButton, { size: "small", onClick: event => setAnchorEl(event.currentTarget), "aria-haspopup": "menu", "aria-controls": menuId, "aria-label": t('Actions'), children: _jsx(Icon, { icon: "mdi:more-vert" }) }), _jsx(Menu, { id: menuId, anchorEl: anchorEl, open: Boolean(anchorEl), onClose: closeMenu, children: filteredOptions.map(option => (_jsx(MenuItem, { onClick: () => handleAction(option, portforward, closeMenu), children: _jsx(ListItemText, { children: optionsTranslated[option] }) }, option))) })] })); }; 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 (_jsxs(SectionBox, { title: t('glossary|Port Forwarding'), children: [_jsx(Table, { columns: [ { id: 'name', header: t('translation|Name'), accessorFn: portforward => portforward.service || portforward.pod, Cell: ({ row }) => { const portforward = row.original; 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 })); }, }, { id: 'namespace', header: t('glossary|Namespace'), accessorFn: portforward => portforward.serviceNamespace || portforward.namespace, }, { id: 'kind', header: t('glossary|Kind'), accessorFn: portforward => (!!portforward.service ? 'Service' : 'Pod'), }, { id: 'podPort', header: t('translation|Pod Port'), accessorFn: portforward => portforward.targetPort, }, { id: 'localPort', header: t('translation|Local Port'), accessorFn: portforward => portforward.port, Cell: ({ row }) => { const portforward = row.original; 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: '4px' } })] }) })); }, }, { id: 'status', header: t('translation|Status'), accessorFn: portforward => portforward.status, Cell: ({ row }) => prepareStatusLabel(row.original), }, { id: 'actions', header: t('translation|Actions'), gridTemplate: 'min-content', muiTableBodyCellProps: { align: 'right' }, accessorFn: portforward => portforward.status, Cell: ({ row }) => _jsx(PortForwardContextMenu, { portforward: row.original }), enableSorting: false, enableColumnFilter: false, }, ], data: portforwards.filter((pf) => pf.cluster === cluster), getRowId: row => row.id }), _jsx(PortForwardStartDialog, { open: startDialogOpen, defaultPort: selectedForStart?.port, podName: selectedForStart?.pod || selectedForStart?.service || '', namespace: selectedForStart?.serviceNamespace || selectedForStart?.namespace || '', containerPort: selectedForStart?.targetPort || '', isDockerDesktop: isDockerDesktop(), onCancel: () => { setStartDialogOpen(false); setSelectedForStart(null); }, onConfirm: portInput => { if (!selectedForStart) return; const { cluster, namespace, pod, targetPort, service, serviceNamespace, id } = selectedForStart; const chosenPort = portInput || selectedForStart.port; const address = selectedForStart.address || 'localhost'; setStartDialogOpen(false); setSelectedForStart(null); setPortForwardInAction({ ...selectedForStart, loading: true }); startPortForward(cluster, namespace, pod, targetPort, service, serviceNamespace, chosenPort, address, id) .then(() => { setPortForwardInAction(null); fetchPortForwardList(true); }) .catch(error => { setPortForwardInAction(null); const errorMessage = error instanceof Error ? error.message : typeof error === 'string' ? error : ''; const displayMessage = errorMessage ? `${t('translation|Error starting port forward')}: ${errorMessage}` : t('translation|Error starting port forward'); enqueueSnackbar(displayMessage, { key: 'portforward-error', preventDuplicate: true, variant: 'error', }); console.error('Error starting port forward:', error); }); } })] })); }