@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
311 lines (310 loc) • 15.7 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 } 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,
},
} })) })) })] }) }));
}