UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

311 lines (310 loc) 15.7 kB
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 } from '@iconify/react'; import { Box, Typography } from '@mui/material'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getKubeObjectCategory } from '../../lib/k8s/ResourceCategory'; import { Activity } from '../activity/Activity'; import { StatusLabel } from '../common'; import ActionButton from '../common/ActionButton/ActionButton'; import Link from '../common/Link'; import AuthVisible from '../common/Resource/AuthVisible'; import DeleteButton from '../common/Resource/DeleteButton'; import ScaleButton from '../common/Resource/ScaleButton'; import Table from '../common/Table'; import Terminal from '../common/Terminal'; import { PodLogViewer } from '../pod/Details'; import { getStatus } from '../resourceMap/nodes/KubeObjectStatus'; import { getResourcesHealth } from './projectUtils'; import { ResourceCategoriesList } from './ResourceCategoriesList'; export const useResourceCategoriesList = (resources) => { return React.useMemo(() => { const groups = new Map(); // Place items per group resources.forEach(r => { const category = getKubeObjectCategory(r); if (!groups.has(category)) { groups.set(category, { items: [], health: {} }); } const group = groups.get(category); group.items.push(r); }); // Calculate health per group groups.forEach(value => { value.health = getResourcesHealth(value.items); }); return [...groups.entries()].map(it => ({ category: it[0], items: it[1].items, health: it[1].health, })); }, [resources]); }; export function ProjectResourcesTab({ projectResources, showClusterColumn, selectedCategoryName, setSelectedCategoryName, }) { const { t } = useTranslation(); const resourceCategories = useResourceCategoriesList(projectResources); const { selectedCategory, selectedResources } = useMemo(() => { const group = selectedCategoryName && resourceCategories.find(({ category }) => category.label === selectedCategoryName); if (group) { return { selectedCategory: group.category, selectedResources: group.items }; } return { selectedCategory: undefined, selectedResources: undefined }; }, [resourceCategories, selectedCategoryName]); const columns = React.useMemo(() => [ { id: 'kind', accessorFn: item => item.kind, header: t('Kind'), gridTemplate: 'min-content', }, { id: 'name', accessorFn: item => item.metadata.uid + item.metadata.name, header: t('Name'), Cell: ({ row }) => { const resource = row.original; return _jsx(Link, { kubeObject: resource, children: resource.metadata?.name }); }, }, { id: 'health', gridTemplate: 'min-content', accessorFn: resource => { const kind = resource.kind; if (kind === 'Deployment') { const deployment = resource; const spec = deployment.spec; const status = deployment.status; if (status?.readyReplicas === 0) return 'Unhealthy'; if ((status?.readyReplicas || 0) < (spec?.replicas || 0)) return 'Degraded'; } else if (kind === 'StatefulSet') { const statefulSet = resource; const spec = statefulSet.spec; const status = statefulSet.status; if (status?.readyReplicas === 0) return 'Unhealthy'; if ((status?.readyReplicas || 0) < (spec?.replicas || 0)) return 'Degraded'; } else if (kind === 'DaemonSet') { const daemonSet = resource; const status = daemonSet.status; if (status?.numberReady === 0) return 'Unhealthy'; if ((status?.numberReady || 0) < (status?.desiredNumberScheduled || 0)) return 'Degraded'; } else if (kind === 'Pod') { const pod = resource; const phase = pod.status?.phase; const conditions = pod.status?.conditions || []; const ready = conditions.find((c) => c.type === 'Ready')?.status === 'True'; if (phase === 'Failed' || phase === 'CrashLoopBackOff') return 'Failed'; if (phase === 'Pending' || !ready) return 'Pending'; } return 'Healthy'; }, header: t('Health'), Cell: ({ row }) => { const resource = row.original; const kind = resource.kind; let healthText = 'Healthy'; const status = getStatus(resource); if (kind === 'Deployment') { const deployment = resource; const spec = deployment.spec; const status = deployment.status; if (status?.readyReplicas === 0) { healthText = 'Unhealthy'; } else if ((status?.readyReplicas || 0) < (spec?.replicas || 0)) { healthText = 'Degraded'; } } else if (kind === 'StatefulSet') { const statefulSet = resource; const spec = statefulSet.spec; const status = statefulSet.status; if (status?.readyReplicas === 0) { healthText = 'Unhealthy'; } else if ((status?.readyReplicas || 0) < (spec?.replicas || 0)) { healthText = 'Degraded'; } } else if (kind === 'DaemonSet') { const daemonSet = resource; const status = daemonSet.status; if (status?.numberReady === 0) { healthText = 'Unhealthy'; } else if ((status?.numberReady || 0) < (status?.desiredNumberScheduled || 0)) { healthText = 'Degraded'; } } else if (kind === 'Pod') { const pod = resource; const phase = pod.status?.phase; const conditions = pod.status?.conditions || []; const ready = conditions.find((c) => c.type === 'Ready')?.status === 'True'; if (phase === 'Failed' || phase === 'CrashLoopBackOff') { healthText = 'Failed'; } else if (phase === 'Pending' || !ready) { healthText = 'Pending'; } } return (_jsxs(StatusLabel, { status: status, sx: { alignItems: 'center' }, children: [_jsx(Icon, { icon: status === 'error' ? 'mdi:alert' : status === 'warning' ? 'mdi:alert-circle' : 'mdi:check-circle', style: { fontSize: 16, } }), healthText] })); }, }, { id: 'namespace', accessorFn: item => item.metadata.namespace || 'default', header: t('Namespace'), gridTemplate: 'min-content', }, { id: 'cluster', accessorFn: item => item.cluster, header: t('Cluster'), gridTemplate: 'min-content', }, { id: 'details', gridTemplate: 'min-content', accessorFn: resource => { const kind = resource.kind; if (resource.isScalable) { const res = resource; return `Replicas: ${res.status?.readyReplicas || res.status?.availableReplicas || 0}/${res.spec?.replicas || 0}`; } if (kind === 'DaemonSet') { const res = resource; return `Ready: ${res.status?.numberReady || 0}/${res.status?.desiredNumberScheduled || 0}`; } if (kind === 'Pod') { const res = resource; return `Phase: ${res.status?.phase}`; } return ''; }, header: t('Details'), Cell: ({ row }) => { const resource = row.original; const kind = resource.kind; if (resource.isScalable) { const res = resource; return (_jsx(Typography, { variant: "body2", color: "text.secondary", whiteSpace: "nowrap", children: `Replicas: ${res.status?.readyReplicas || res.status?.availableReplicas || 0}/${res.spec?.replicas || 0}` })); } if (kind === 'DaemonSet') { const res = resource; return (_jsx(Typography, { variant: "body2", color: "text.secondary", whiteSpace: "nowrap", children: `Ready: ${res.status?.numberReady || 0}/${res.status?.desiredNumberScheduled || 0}` })); } if (kind === 'Pod') { const res = resource; return (_jsx(Typography, { variant: "body2", color: "text.secondary", whiteSpace: "nowrap", children: `Phase: ${res.status?.phase}` })); } return null; }, }, { id: 'age', accessorFn: item => item.metadata.creationTimestamp, header: t('Age'), gridTemplate: 'min-content', Cell: ({ row }) => { const resource = row.original; const createdDate = resource.metadata?.creationTimestamp ? new Date(resource.metadata.creationTimestamp) : null; const ageText = createdDate ? Math.floor((Date.now() - createdDate.getTime()) / (1000 * 60 * 60 * 24)) + 'd' : 'Unknown'; return (_jsx(Typography, { variant: "caption", color: "text.secondary", children: ageText })); }, }, { id: 'actions', header: t('Actions'), gridTemplate: 'min-content', accessorFn: item => item.metadata.uid, Cell: ({ row }) => { const resource = row.original; const kind = resource.kind; const isScalable = resource.isScalable; const isPod = kind === 'Pod'; return (_jsxs(Box, { display: "flex", alignItems: "center", gap: 1, justifyContent: "flex-end", children: [isScalable && (_jsx(ScaleButton, { item: resource })), isPod && (_jsxs(_Fragment, { children: [_jsx(AuthVisible, { item: resource, authVerb: "get", subresource: "log", children: _jsx(ActionButton, { description: t('Show Logs'), icon: "mdi:file-document-box-outline", onClick: () => { const id = 'logs-' + resource.metadata.uid; Activity.launch({ id, title: t('Logs: {{ itemName }}', { itemName: resource.metadata.name }), cluster: resource.cluster, icon: (_jsx(Icon, { icon: "mdi:file-document-box-outline", width: "100%", height: "100%" })), location: 'full', content: (_jsx(PodLogViewer, { noDialog: true, open: true, item: resource, onClose: () => Activity.close(id) })), }); } }) }), _jsx(AuthVisible, { item: resource, authVerb: "create", subresource: "exec", children: _jsx(ActionButton, { description: t('Terminal / Exec'), icon: "mdi:console", onClick: () => { const id = 'terminal-' + resource.metadata.uid; Activity.launch({ id, title: resource.metadata.name, cluster: resource.cluster, icon: _jsx(Icon, { icon: "mdi:console", width: "100%", height: "100%" }), location: 'full', content: (_jsx(Terminal, { open: true, item: resource, onClose: () => Activity.close(id) })), }); } }) }), _jsx(DeleteButton, { item: resource })] }))] })); }, }, ], // eslint-disable-next-line react-hooks/exhaustive-deps [t, showClusterColumn]); return (_jsx(_Fragment, { children: _jsxs(Box, { sx: theme => ({ display: 'flex', border: '1px solid', borderColor: theme.palette.divider, borderTop: 0, flexGrow: 1, minHeight: 0, flexBasis: 0, }), children: [_jsx(ResourceCategoriesList, { categoryList: resourceCategories, selectedCategoryName: selectedCategoryName, onCategoryClick: setSelectedCategoryName }), _jsx(Box, { sx: theme => ({ flexGrow: 1, p: 1, overflowY: 'auto', borderLeft: '1px solid', borderColor: theme.palette.divider, }), children: selectedCategory && (_jsx(Box, { children: selectedResources.length === 0 ? (_jsx(Typography, { variant: "body2", color: "text.secondary", children: t('No {{category}} resources found for this project.', { category: selectedCategory.label.toLowerCase(), }) })) : (_jsx(Table, { columns: columns, data: selectedResources, state: { columnVisibility: { cluster: !!showClusterColumn, }, } })) })) })] }) })); }