UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

206 lines (205 loc) 10.7 kB
import { jsx as _jsx, 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 } from '@iconify/react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import { useTheme } from '@mui/material/styles'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath, useHistory } from 'react-router-dom'; import { getClusterAppearanceFromMeta } from '../../../helpers/clusterAppearance'; import { isElectron } from '../../../helpers/isElectron'; import { loadTableSettings, storeTableSettings } from '../../../helpers/tableSettings'; import { formatClusterPathParam } from '../../../lib/cluster'; import { createRouteURL } from '../../../lib/router/createRouteURL'; import { getClusterPrefixedPath } from '../../../lib/util'; import { useTypedSelector } from '../../../redux/hooks'; import { Loader } from '../../common'; import Link from '../../common/Link'; import Table from '../../common/Table'; import { useLocalStorageState } from '../../globalSearch/useLocalStorageState'; import ClusterBadge from '../../Sidebar/ClusterBadge'; import ClusterContextMenu from './ClusterContextMenu'; import { MULTI_HOME_ENABLED } from './config'; /** * ClusterStatus component displays the status of a cluster. * It shows an icon and a message indicating whether the cluster is active, unknown, or has an error. * * @param {Object} props - The component props. * @param {ApiError|null} [props.error] - The error object if there is an error with the cluster. */ function ClusterStatus({ error, cluster }) { const { t } = useTranslation(['translation']); const theme = useTheme(); const customStatuses = useTypedSelector(state => state.clusterProvider.clusterStatuses); const renderedCustomStatus = useMemo(() => { for (const Status of customStatuses) { const renderedStatus = _jsx(Status, { cluster: cluster, error: error }); if (renderedStatus !== null) { return renderedStatus; } } return null; }, [customStatuses, cluster, error]); if (renderedCustomStatus !== null) { return renderedCustomStatus; } const stateUnknown = error === undefined; const hasReachError = error && error.status !== 401 && error.status !== 403; return (_jsx(Box, { width: "fit-content", children: _jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "center", children: [hasReachError ? (_jsx(Icon, { icon: "mdi:cloud-off", width: 16, color: theme.palette.home.status.error })) : stateUnknown ? (_jsx(Icon, { icon: "mdi:cloud-question", width: 16, color: theme.palette.home.status.unknown })) : (_jsx(Icon, { icon: "mdi:cloud-check-variant", width: 16, color: theme.palette.home.status.success })), _jsx(Typography, { variant: "body2", style: { marginLeft: theme.spacing(1), color: hasReachError ? theme.palette.home.status.error : !stateUnknown ? theme.palette.home.status.success : undefined, }, children: hasReachError ? error.message : stateUnknown ? '⋯' : t('translation|Active') })] }) })); } /** * ClusterTable component displays a table of clusters with their status, origin, and version. */ const CLUSTER_TABLE_ID = 'home-clusters'; export default function ClusterTable({ customNameClusters, versions, errors, clusters, warningLabels, }) { const history = useHistory(); const { t } = useTranslation(['translation']); const [columnVisibility, setColumnVisibility] = useState(() => { const visibility = {}; const stored = loadTableSettings(CLUSTER_TABLE_ID); stored.forEach(({ id, show }) => (visibility[id] = show)); return visibility; }); const [sorting, setSorting] = useLocalStorageState(`table_sorting.${CLUSTER_TABLE_ID}`, [{ id: 'name', desc: false }]); const [columnFilters, setColumnFilters] = useLocalStorageState(`table_filters.${CLUSTER_TABLE_ID}`, []); const handleColumnVisibilityChange = useCallback((updater) => { setColumnVisibility(oldCols => { const newCols = typeof updater === 'function' ? updater(oldCols) : updater; const colsToStore = Object.entries(newCols).map(([id, show]) => ({ id, show: (show ?? true), })); storeTableSettings(CLUSTER_TABLE_ID, colsToStore); return newCols; }); }, []); const handleSortingChange = useCallback((updater) => { setSorting(old => (typeof updater === 'function' ? updater(old) : updater)); }, [setSorting]); const handleColumnFiltersChange = useCallback((updater) => { setColumnFilters(old => (typeof updater === 'function' ? updater(old) : updater)); }, [setColumnFilters]); /** * Gets the origin of a cluster. * * @param cluster * @returns A description of where the cluster is picked up from: dynamic, in-cluster, or from a kubeconfig file. */ function getOrigin(cluster) { if (cluster?.meta_data?.source === 'kubeconfig') { const sourcePath = cluster?.meta_data?.origin?.kubeconfig; return sourcePath ? `Kubeconfig: ${sourcePath}` : 'Kubeconfig'; } else if (cluster?.meta_data?.source === 'dynamic_cluster') { return t('translation|Plugin'); } else if (cluster?.meta_data?.source === 'in_cluster') { return t('translation|In-cluster'); } return 'Unknown'; } const viewClusters = t('View Clusters'); const loading = clusters === null; if (loading) { return _jsx(Loader, { title: t('Loading...') }); } const clustersList = Object.values(customNameClusters); if (clustersList.length === 0) { return (_jsxs(Box, { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "400px", textAlign: "center", children: [_jsx(Icon, { icon: "mdi:hexagon-multiple-outline", style: { fontSize: 64, color: '#ccc', marginBottom: 16 } }), _jsx(Typography, { variant: "h6", gutterBottom: true, children: t('No clusters found') }), _jsx(Typography, { variant: "body2", color: "text.secondary", paragraph: true, children: t('Add a cluster to get started.') }), isElectron() && (_jsx(Button, { variant: "contained", startIcon: _jsx(Icon, { icon: "mdi:plus" }), onClick: () => { history.push(createRouteURL('addCluster')); }, children: t('Add Cluster') }))] })); } return (_jsx(Table, { columns: [ { id: 'name', header: t('Name'), accessorKey: 'name', gridTemplate: 2, Cell: ({ row: { original } }) => { const appearance = getClusterAppearanceFromMeta(original.name); return (_jsx(Tooltip, { title: original.name, arrow: true, children: _jsx("span", { children: _jsx(Link, { routeName: "cluster", params: { cluster: original.name }, children: _jsx(ClusterBadge, { name: original.name, icon: appearance.icon, accentColor: appearance.accentColor }) }) }) })); }, }, { id: 'origin', header: t('Origin'), accessorFn: cluster => getOrigin(cluster), Cell: ({ row: { original } }) => (_jsx(Typography, { variant: "body2", children: getOrigin((clusters || {})[original.name]) })), }, { id: 'status', header: t('Status'), accessorFn: cluster => errors[cluster?.name] === null ? 'Active' : errors[cluster?.name]?.message, Cell: ({ row: { original } }) => (_jsx(ClusterStatus, { error: errors[original.name], cluster: original })), }, { id: 'warnings', header: t('Warnings'), accessorFn: cluster => warningLabels[cluster?.name], }, { id: 'version', header: t('glossary|Kubernetes Version'), accessorFn: ({ name }) => versions[name]?.gitVersion || '⋯', }, { id: 'actions', header: t('Actions'), gridTemplate: 'min-content', muiTableBodyCellProps: { align: 'right', }, accessorFn: cluster => errors[cluster?.name] === null ? 'Active' : errors[cluster?.name]?.message, Cell: ({ row: { original: cluster } }) => { return _jsx(ClusterContextMenu, { cluster: cluster }); }, enableSorting: false, enableColumnFilter: false, }, ], data: clustersList, enableRowSelection: MULTI_HOME_ENABLED ? row => { // Only allow selection if the cluster is working return !errors[row.original.name]; } : false, state: { columnVisibility, sorting, columnFilters, }, onColumnVisibilityChange: handleColumnVisibilityChange, onSortingChange: handleSortingChange, onColumnFiltersChange: handleColumnFiltersChange, muiToolbarAlertBannerProps: { sx: theme => ({ background: theme.palette.background.muted, }), }, renderToolbarAlertBannerContent: ({ table }) => (_jsx(Button, { variant: "contained", sx: { marginLeft: 1, }, onClick: () => { history.push({ pathname: generatePath(getClusterPrefixedPath(), { cluster: formatClusterPathParam(table.getSelectedRowModel().rows.map(it => it.original.name)), }), }); }, children: viewClusters })) })); }