style: format all source files with Prettier

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DevContainer User
2026-03-04 00:55:39 +00:00
parent 49c5cdbe86
commit c0389c0302
16 changed files with 533 additions and 200 deletions
+25 -9
View File
@@ -166,7 +166,9 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// Operator pods
try {
const opList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_OPERATOR_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_OPERATOR_SELECTOR
)}`
);
if (!cancelled && isKubeList(opList)) setOperatorPods(opList.items as RookCephPod[]);
} catch {
@@ -176,7 +178,9 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// MON pods
try {
const monList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_MON_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_MON_SELECTOR
)}`
);
if (!cancelled && isKubeList(monList)) setMonPods(monList.items as RookCephPod[]);
} catch {
@@ -186,7 +190,9 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// OSD pods
try {
const osdList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_OSD_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_OSD_SELECTOR
)}`
);
if (!cancelled && isKubeList(osdList)) setOsdPods(osdList.items as RookCephPod[]);
} catch {
@@ -196,7 +202,9 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// MGR pods
try {
const mgrList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_MGR_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_MGR_SELECTOR
)}`
);
if (!cancelled && isKubeList(mgrList)) setMgrPods(mgrList.items as RookCephPod[]);
} catch {
@@ -206,9 +214,12 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// CSI RBD provisioner pods
try {
const csiRbdList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_CSI_RBD_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_CSI_RBD_SELECTOR
)}`
);
if (!cancelled && isKubeList(csiRbdList)) setCsiRbdPods(csiRbdList.items as RookCephPod[]);
if (!cancelled && isKubeList(csiRbdList))
setCsiRbdPods(csiRbdList.items as RookCephPod[]);
} catch {
if (!cancelled) setCsiRbdPods([]);
}
@@ -216,9 +227,12 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// CSI CephFS provisioner pods
try {
const csiCephfsList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_CSI_CEPHFS_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_CSI_CEPHFS_SELECTOR
)}`
);
if (!cancelled && isKubeList(csiCephfsList)) setCsiCephfsPods(csiCephfsList.items as RookCephPod[]);
if (!cancelled && isKubeList(csiCephfsList))
setCsiCephfsPods(csiCephfsList.items as RookCephPod[]);
} catch {
if (!cancelled) setCsiCephfsPods([]);
}
@@ -232,7 +246,9 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
}
void fetchAsync();
return () => { cancelled = true; };
return () => {
cancelled = true;
};
}, [refreshKey]);
// ---------------------------------------------------------------------------
+25 -20
View File
@@ -129,9 +129,12 @@ export interface CephCluster extends KubeObject {
export function healthToStatus(health: string | undefined): 'success' | 'warning' | 'error' {
switch (health) {
case 'HEALTH_OK': return 'success';
case 'HEALTH_WARN': return 'warning';
default: return 'error';
case 'HEALTH_OK':
return 'success';
case 'HEALTH_WARN':
return 'warning';
default:
return 'error';
}
}
@@ -331,9 +334,7 @@ export function findBoundPv(
): RookCephPersistentVolume | undefined {
const ns = pvc.metadata.namespace ?? '';
const name = pvc.metadata.name;
return rookPvs.find(
pv => pv.spec.claimRef?.namespace === ns && pv.spec.claimRef?.name === name
);
return rookPvs.find(pv => pv.spec.claimRef?.namespace === ns && pv.spec.claimRef?.name === name);
}
// ---------------------------------------------------------------------------
@@ -368,15 +369,11 @@ export interface RookCephPod extends KubeObject {
}
export function isPodReady(pod: RookCephPod): boolean {
return (
pod.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false
);
return pod.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false;
}
export function getPodRestarts(pod: RookCephPod): number {
return (
pod.status?.containerStatuses?.reduce((sum, c) => sum + c.restartCount, 0) ?? 0
);
return pod.status?.containerStatuses?.reduce((sum, c) => sum + c.restartCount, 0) ?? 0;
}
export function getPodImage(pod: RookCephPod): string {
@@ -441,11 +438,16 @@ export function parseStorageToBytes(storage: string): number {
const suffix = match[2] ?? '';
const multipliers: Record<string, number> = {
'': 1,
K: 1e3, Ki: 1024,
M: 1e6, Mi: 1024 ** 2,
G: 1e9, Gi: 1024 ** 3,
T: 1e12, Ti: 1024 ** 4,
P: 1e15, Pi: 1024 ** 5,
K: 1e3,
Ki: 1024,
M: 1e6,
Mi: 1024 ** 2,
G: 1e9,
Gi: 1024 ** 3,
T: 1e12,
Ti: 1024 ** 4,
P: 1e15,
Pi: 1024 ** 5,
};
return value * (multipliers[suffix] ?? 1);
}
@@ -453,9 +455,12 @@ export function parseStorageToBytes(storage: string): number {
/** Returns display label for storage type (rbd → Block, cephfs → Filesystem). */
export function formatStorageType(type: 'rbd' | 'cephfs' | 'unknown'): string {
switch (type) {
case 'rbd': return 'Block (RBD)';
case 'cephfs': return 'Filesystem (CephFS)';
default: return 'Unknown';
case 'rbd':
return 'Block (RBD)';
case 'cephfs':
return 'Filesystem (CephFS)';
default:
return 'Unknown';
}
}
+8 -4
View File
@@ -14,10 +14,14 @@ import { useRookCephContext } from '../api/RookCephDataContext';
function getHealthColor(health: string | undefined): string {
switch (health) {
case 'HEALTH_OK': return '#4caf50';
case 'HEALTH_WARN': return '#ff9800';
case 'HEALTH_ERR': return '#f44336';
default: return '#9e9e9e';
case 'HEALTH_OK':
return '#4caf50';
case 'HEALTH_WARN':
return '#ff9800';
case 'HEALTH_ERR':
return '#f44336';
default:
return '#9e9e9e';
}
}
+49 -10
View File
@@ -19,7 +19,10 @@ function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () =
<div
style={{
position: 'fixed',
top: 0, right: 0, bottom: 0, width: '480px',
top: 0,
right: 0,
bottom: 0,
width: '480px',
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
boxShadow: '-4px 0 16px rgba(0,0,0,0.15)',
zIndex: 1300,
@@ -27,7 +30,14 @@ function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () =
padding: '24px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
<strong>{pool.metadata.name}</strong>
<button
onClick={onClose}
@@ -99,14 +109,18 @@ export default function BlockPoolsPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
{blockPools.length === 0 ? (
<SectionBox title="No Block Pools">
<NameValueTable
rows={[{ name: 'Status', value: 'No CephBlockPool resources found in rook-ceph namespace.' }]}
rows={[
{ name: 'Status', value: 'No CephBlockPool resources found in rook-ceph namespace.' },
]}
/>
</SectionBox>
) : (
@@ -118,7 +132,15 @@ export default function BlockPoolsPage() {
getter: (p: CephBlockPool) => (
<button
onClick={() => setSelected(p)}
style={{ border: 'none', background: 'transparent', color: 'var(--link-color, #1976d2)', cursor: 'pointer', textDecoration: 'underline', padding: 0, font: 'inherit' }}
style={{
border: 'none',
background: 'transparent',
color: 'var(--link-color, #1976d2)',
cursor: 'pointer',
textDecoration: 'underline',
padding: 0,
font: 'inherit',
}}
>
{p.metadata.name}
</button>
@@ -132,10 +154,22 @@ export default function BlockPoolsPage() {
</StatusLabel>
),
},
{ label: 'Replicas', getter: (p: CephBlockPool) => String(p.spec?.replicated?.size ?? '—') },
{ label: 'Failure Domain', getter: (p: CephBlockPool) => p.spec?.failureDomain ?? '—' },
{ label: 'Mirroring', getter: (p: CephBlockPool) => p.spec?.mirroring?.enabled ? 'Enabled' : 'Disabled' },
{ label: 'Age', getter: (p: CephBlockPool) => formatAge(p.metadata.creationTimestamp) },
{
label: 'Replicas',
getter: (p: CephBlockPool) => String(p.spec?.replicated?.size ?? '—'),
},
{
label: 'Failure Domain',
getter: (p: CephBlockPool) => p.spec?.failureDomain ?? '—',
},
{
label: 'Mirroring',
getter: (p: CephBlockPool) => (p.spec?.mirroring?.enabled ? 'Enabled' : 'Disabled'),
},
{
label: 'Age',
getter: (p: CephBlockPool) => formatAge(p.metadata.creationTimestamp),
},
]}
data={blockPools}
/>
@@ -145,7 +179,12 @@ export default function BlockPoolsPage() {
{selected && (
<>
<div
style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.3)', zIndex: 1299 }}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
zIndex: 1299,
}}
onClick={() => setSelected(null)}
/>
<BlockPoolDetail pool={selected} onClose={() => setSelected(null)} />
+7 -10
View File
@@ -80,16 +80,17 @@ export default function CephPodDetailSection({ resource }: CephPodDetailSectionP
const role = ROLE_LABELS[appLabel] ?? appLabel;
const phase = raw.status?.phase ?? 'Unknown';
const isReady =
raw.status?.conditions?.some((c) => c.type === 'Ready' && c.status === 'True') ?? false;
const restarts =
raw.status?.containerStatuses?.reduce((s, c) => s + c.restartCount, 0) ?? 0;
raw.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false;
const restarts = raw.status?.containerStatuses?.reduce((s, c) => s + c.restartCount, 0) ?? 0;
const containerRows = (raw.status?.containerStatuses ?? []).map((cs) => {
const containerRows = (raw.status?.containerStatuses ?? []).map(cs => {
let stateStr = 'Unknown';
if (cs.state?.running) stateStr = 'Running';
else if (cs.state?.waiting) stateStr = `Waiting: ${cs.state.waiting.reason ?? ''}`;
else if (cs.state?.terminated)
stateStr = `Terminated: ${cs.state.terminated.reason ?? ''} (exit ${cs.state.terminated.exitCode ?? ''})`;
stateStr = `Terminated: ${cs.state.terminated.reason ?? ''} (exit ${
cs.state.terminated.exitCode ?? ''
})`;
return {
name: cs.name,
@@ -111,11 +112,7 @@ export default function CephPodDetailSection({ resource }: CephPodDetailSectionP
},
{
name: 'Phase',
value: (
<StatusLabel status={isReady ? 'success' : 'error'}>
{phase}
</StatusLabel>
),
value: <StatusLabel status={isReady ? 'success' : 'error'}>{phase}</StatusLabel>,
},
{ name: 'Node', value: raw.spec?.nodeName ?? '—' },
{ name: 'Restarts', value: String(restarts) },
+24 -13
View File
@@ -11,7 +11,15 @@ import {
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import type { CephCluster, RookCephPod } from '../api/k8s';
import { formatAge, formatBytes, getPodImage, getPodRestarts, healthToStatus, isPodReady, phaseToStatus } from '../api/k8s';
import {
formatAge,
formatBytes,
getPodImage,
getPodRestarts,
healthToStatus,
isPodReady,
phaseToStatus,
} from '../api/k8s';
interface ClusterStatusCardProps {
cephClusters: CephCluster[];
@@ -26,17 +34,14 @@ interface ClusterStatusCardProps {
function PodStatusBadge({ pod }: { pod: RookCephPod }) {
const ready = isPodReady(pod);
const phase = pod.status?.phase ?? 'Unknown';
return (
<StatusLabel status={ready ? 'success' : 'error'}>
{phase}
</StatusLabel>
);
return <StatusLabel status={ready ? 'success' : 'error'}>{phase}</StatusLabel>;
}
function PodSummaryRow({ pods, label }: { pods: RookCephPod[]; label: string }) {
const ready = pods.filter(isPodReady).length;
const total = pods.length;
const status = total === 0 ? 'error' : ready === total ? 'success' : ready > 0 ? 'warning' : 'error';
const status =
total === 0 ? 'error' : ready === total ? 'success' : ready > 0 ? 'warning' : 'error';
return {
name: label,
value: (
@@ -84,12 +89,12 @@ export default function ClusterStatusCard({
{
name: 'Phase',
value: (
<StatusLabel status={phaseToStatus(phase)}>
{phase ?? 'Unknown'}
</StatusLabel>
<StatusLabel status={phaseToStatus(phase)}>{phase ?? 'Unknown'}</StatusLabel>
),
},
...(cluster.status?.message ? [{ name: 'Message', value: cluster.status.message }] : []),
...(cluster.status?.message
? [{ name: 'Message', value: cluster.status.message }]
: []),
{ name: 'Ceph Version', value: version },
{ name: 'Namespace', value: cluster.metadata.namespace ?? '—' },
{ name: 'Age', value: formatAge(cluster.metadata.creationTimestamp) },
@@ -102,7 +107,11 @@ export default function ClusterStatusCard({
<div style={{ marginBottom: '12px' }}>
<PercentageBar
data={[
{ name: 'Used', value: bytesUsed, fill: usedPct > 80 ? '#f44336' : '#1976d2' },
{
name: 'Used',
value: bytesUsed,
fill: usedPct > 80 ? '#f44336' : '#1976d2',
},
{ name: 'Free', value: bytesAvail, fill: '#e0e0e0' },
]}
total={bytesTotal}
@@ -142,7 +151,9 @@ export function PodDetailRows({ pods, label }: { pods: RookCephPod[]; label: str
return (
<SectionBox title={label}>
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">No pods found</StatusLabel> }]}
rows={[
{ name: 'Status', value: <StatusLabel status="error">No pods found</StatusLabel> },
]}
/>
</SectionBox>
);
+56 -11
View File
@@ -19,7 +19,10 @@ function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () =>
<div
style={{
position: 'fixed',
top: 0, right: 0, bottom: 0, width: '480px',
top: 0,
right: 0,
bottom: 0,
width: '480px',
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
boxShadow: '-4px 0 16px rgba(0,0,0,0.15)',
zIndex: 1300,
@@ -27,7 +30,14 @@ function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () =>
padding: '24px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
<strong>{fs.metadata.name}</strong>
<button
onClick={onClose}
@@ -58,7 +68,10 @@ function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () =>
<NameValueTable
rows={[
{ name: 'Active Count', value: String(fs.spec?.metadataServer?.activeCount ?? '—') },
{ name: 'Active Standby', value: String(fs.spec?.metadataServer?.activeStandby ?? '—') },
{
name: 'Active Standby',
value: String(fs.spec?.metadataServer?.activeStandby ?? '—'),
},
]}
/>
</SectionBox>
@@ -107,14 +120,21 @@ export default function FilesystemsPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
{filesystems.length === 0 ? (
<SectionBox title="No Filesystems">
<NameValueTable
rows={[{ name: 'Status', value: 'No CephFilesystem resources found in rook-ceph namespace.' }]}
rows={[
{
name: 'Status',
value: 'No CephFilesystem resources found in rook-ceph namespace.',
},
]}
/>
</SectionBox>
) : (
@@ -126,7 +146,15 @@ export default function FilesystemsPage() {
getter: (f: CephFilesystem) => (
<button
onClick={() => setSelected(f)}
style={{ border: 'none', background: 'transparent', color: 'var(--link-color, #1976d2)', cursor: 'pointer', textDecoration: 'underline', padding: 0, font: 'inherit' }}
style={{
border: 'none',
background: 'transparent',
color: 'var(--link-color, #1976d2)',
cursor: 'pointer',
textDecoration: 'underline',
padding: 0,
font: 'inherit',
}}
>
{f.metadata.name}
</button>
@@ -140,10 +168,22 @@ export default function FilesystemsPage() {
</StatusLabel>
),
},
{ label: 'Active MDS', getter: (f: CephFilesystem) => String(f.spec?.metadataServer?.activeCount ?? '—') },
{ label: 'Active Standby', getter: (f: CephFilesystem) => String(f.spec?.metadataServer?.activeStandby ?? '—') },
{ label: 'Data Pools', getter: (f: CephFilesystem) => String(f.spec?.dataPools?.length ?? 0) },
{ label: 'Age', getter: (f: CephFilesystem) => formatAge(f.metadata.creationTimestamp) },
{
label: 'Active MDS',
getter: (f: CephFilesystem) => String(f.spec?.metadataServer?.activeCount ?? '—'),
},
{
label: 'Active Standby',
getter: (f: CephFilesystem) => String(f.spec?.metadataServer?.activeStandby ?? '—'),
},
{
label: 'Data Pools',
getter: (f: CephFilesystem) => String(f.spec?.dataPools?.length ?? 0),
},
{
label: 'Age',
getter: (f: CephFilesystem) => formatAge(f.metadata.creationTimestamp),
},
]}
data={filesystems}
/>
@@ -153,7 +193,12 @@ export default function FilesystemsPage() {
{selected && (
<>
<div
style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.3)', zIndex: 1299 }}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
zIndex: 1299,
}}
onClick={() => setSelected(null)}
/>
<FilesystemDetail fs={selected} onClose={() => setSelected(null)} />
+49 -10
View File
@@ -23,7 +23,10 @@ function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose
<div
style={{
position: 'fixed',
top: 0, right: 0, bottom: 0, width: '480px',
top: 0,
right: 0,
bottom: 0,
width: '480px',
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
boxShadow: '-4px 0 16px rgba(0,0,0,0.15)',
zIndex: 1300,
@@ -31,7 +34,14 @@ function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose
padding: '24px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
<strong>{store.metadata.name}</strong>
<button
onClick={onClose}
@@ -67,7 +77,7 @@ function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose
]}
/>
</SectionBox>
{(endpoints?.insecure?.length || endpoints?.secure?.length) ? (
{endpoints?.insecure?.length || endpoints?.secure?.length ? (
<SectionBox title="Endpoints">
<NameValueTable
rows={[
@@ -104,14 +114,21 @@ export default function ObjectStoresPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
{objectStores.length === 0 ? (
<SectionBox title="No Object Stores">
<NameValueTable
rows={[{ name: 'Status', value: 'No CephObjectStore resources found in rook-ceph namespace.' }]}
rows={[
{
name: 'Status',
value: 'No CephObjectStore resources found in rook-ceph namespace.',
},
]}
/>
</SectionBox>
) : (
@@ -123,7 +140,15 @@ export default function ObjectStoresPage() {
getter: (o: CephObjectStore) => (
<button
onClick={() => setSelected(o)}
style={{ border: 'none', background: 'transparent', color: 'var(--link-color, #1976d2)', cursor: 'pointer', textDecoration: 'underline', padding: 0, font: 'inherit' }}
style={{
border: 'none',
background: 'transparent',
color: 'var(--link-color, #1976d2)',
cursor: 'pointer',
textDecoration: 'underline',
padding: 0,
font: 'inherit',
}}
>
{o.metadata.name}
</button>
@@ -137,9 +162,18 @@ export default function ObjectStoresPage() {
</StatusLabel>
),
},
{ label: 'Gateway Port', getter: (o: CephObjectStore) => String(o.spec?.gateway?.port ?? '—') },
{ label: 'Instances', getter: (o: CephObjectStore) => String(o.spec?.gateway?.instances ?? '—') },
{ label: 'Age', getter: (o: CephObjectStore) => formatAge(o.metadata.creationTimestamp) },
{
label: 'Gateway Port',
getter: (o: CephObjectStore) => String(o.spec?.gateway?.port ?? '—'),
},
{
label: 'Instances',
getter: (o: CephObjectStore) => String(o.spec?.gateway?.instances ?? '—'),
},
{
label: 'Age',
getter: (o: CephObjectStore) => formatAge(o.metadata.creationTimestamp),
},
]}
data={objectStores}
/>
@@ -149,7 +183,12 @@ export default function ObjectStoresPage() {
{selected && (
<>
<div
style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.3)', zIndex: 1299 }}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
zIndex: 1299,
}}
onClick={() => setSelected(null)}
/>
<ObjectStoreDetail store={selected} onClose={() => setSelected(null)} />
+84 -35
View File
@@ -15,7 +15,13 @@ import {
StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { formatAge, formatBytes, healthToStatus, phaseToStatus, storageClassType } from '../api/k8s';
import {
formatAge,
formatBytes,
healthToStatus,
phaseToStatus,
storageClassType,
} from '../api/k8s';
import { useRookCephContext } from '../api/RookCephDataContext';
import ClusterStatusCard from './ClusterStatusCard';
@@ -70,7 +76,14 @@ export default function OverviewPage() {
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
}}
>
<SectionHeader title="Rook-Ceph — Overview" />
<button
onClick={refresh}
@@ -97,11 +110,16 @@ export default function OverviewPage() {
rows={[
{
name: 'Status',
value: <StatusLabel status="error">No CephCluster found in namespace rook-ceph</StatusLabel>,
value: (
<StatusLabel status="error">
No CephCluster found in namespace rook-ceph
</StatusLabel>
),
},
{
name: 'Install',
value: 'helm install rook-ceph rook-release/rook-ceph -n rook-ceph --create-namespace',
value:
'helm install rook-ceph rook-release/rook-ceph -n rook-ceph --create-namespace',
},
{
name: 'Docs',
@@ -129,9 +147,7 @@ export default function OverviewPage() {
{
name: 'Health',
value: (
<StatusLabel status={healthToStatus(primaryHealth)}>
{primaryHealth}
</StatusLabel>
<StatusLabel status={healthToStatus(primaryHealth)}>{primaryHealth}</StatusLabel>
),
},
{
@@ -148,7 +164,13 @@ export default function OverviewPage() {
<SectionBox title="Storage Summary">
{storageClasses.length > 0 && (
<div style={{ marginBottom: '16px' }}>
<div style={{ marginBottom: '8px', fontSize: '14px', color: 'var(--mui-palette-text-secondary)' }}>
<div
style={{
marginBottom: '8px',
fontSize: '14px',
color: 'var(--mui-palette-text-secondary)',
}}
>
StorageClass Type Distribution
</div>
<PercentageBar
@@ -157,7 +179,13 @@ export default function OverviewPage() {
? [{ name: 'Block (RBD)', value: rbdClasses.length, fill: '#1976d2' }]
: []),
...(cephfsClasses.length > 0
? [{ name: 'Filesystem (CephFS)', value: cephfsClasses.length, fill: '#9c27b0' }]
? [
{
name: 'Filesystem (CephFS)',
value: cephfsClasses.length,
fill: '#9c27b0',
},
]
: []),
]}
total={storageClasses.length}
@@ -166,7 +194,10 @@ export default function OverviewPage() {
)}
<NameValueTable
rows={[
{ name: 'Storage Classes', value: `${storageClasses.length} (${rbdClasses.length} RBD, ${cephfsClasses.length} CephFS)` },
{
name: 'Storage Classes',
value: `${storageClasses.length} (${rbdClasses.length} RBD, ${cephfsClasses.length} CephFS)`,
},
{ name: 'Block Pools', value: String(blockPools.length) },
{ name: 'Filesystems', value: String(filesystems.length) },
{ name: 'Object Stores', value: String(objectStores.length) },
@@ -177,10 +208,20 @@ export default function OverviewPage() {
value: <StatusLabel status="success">{pvcStatusCounts.Bound}</StatusLabel>,
},
...(pvcStatusCounts.Pending > 0
? [{ name: 'PVCs (Pending)', value: <StatusLabel status="warning">{pvcStatusCounts.Pending}</StatusLabel> }]
? [
{
name: 'PVCs (Pending)',
value: <StatusLabel status="warning">{pvcStatusCounts.Pending}</StatusLabel>,
},
]
: []),
...(pvcStatusCounts.Lost > 0
? [{ name: 'PVCs (Lost)', value: <StatusLabel status="error">{pvcStatusCounts.Lost}</StatusLabel> }]
? [
{
name: 'PVCs (Lost)',
value: <StatusLabel status="error">{pvcStatusCounts.Lost}</StatusLabel>,
},
]
: []),
]}
/>
@@ -203,18 +244,18 @@ export default function OverviewPage() {
<SectionBox title="Block Pools">
<SimpleTable
columns={[
{ label: 'Name', getter: (p) => p.metadata.name },
{ label: 'Name', getter: p => p.metadata.name },
{
label: 'Phase',
getter: (p) => (
getter: p => (
<StatusLabel status={phaseToStatus(p.status?.phase)}>
{p.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Replicas', getter: (p) => String(p.spec?.replicated?.size ?? '—') },
{ label: 'Failure Domain', getter: (p) => p.spec?.failureDomain ?? '—' },
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Replicas', getter: p => String(p.spec?.replicated?.size ?? '—') },
{ label: 'Failure Domain', getter: p => p.spec?.failureDomain ?? '—' },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={blockPools}
/>
@@ -226,17 +267,20 @@ export default function OverviewPage() {
<SectionBox title="Filesystems">
<SimpleTable
columns={[
{ label: 'Name', getter: (f) => f.metadata.name },
{ label: 'Name', getter: f => f.metadata.name },
{
label: 'Phase',
getter: (f) => (
getter: f => (
<StatusLabel status={phaseToStatus(f.status?.phase)}>
{f.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Active MDS', getter: (f) => String(f.spec?.metadataServer?.activeCount ?? '—') },
{ label: 'Age', getter: (f) => formatAge(f.metadata.creationTimestamp) },
{
label: 'Active MDS',
getter: f => String(f.spec?.metadataServer?.activeCount ?? '—'),
},
{ label: 'Age', getter: f => formatAge(f.metadata.creationTimestamp) },
]}
data={filesystems}
/>
@@ -248,18 +292,18 @@ export default function OverviewPage() {
<SectionBox title="Object Stores">
<SimpleTable
columns={[
{ label: 'Name', getter: (o) => o.metadata.name },
{ label: 'Name', getter: o => o.metadata.name },
{
label: 'Phase',
getter: (o) => (
getter: o => (
<StatusLabel status={phaseToStatus(o.status?.phase)}>
{o.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Gateway Port', getter: (o) => String(o.spec?.gateway?.port ?? '—') },
{ label: 'Instances', getter: (o) => String(o.spec?.gateway?.instances ?? '—') },
{ label: 'Age', getter: (o) => formatAge(o.metadata.creationTimestamp) },
{ label: 'Gateway Port', getter: o => String(o.spec?.gateway?.port ?? '—') },
{ label: 'Instances', getter: o => String(o.spec?.gateway?.instances ?? '—') },
{ label: 'Age', getter: o => formatAge(o.metadata.creationTimestamp) },
]}
data={objectStores}
/>
@@ -271,17 +315,17 @@ export default function OverviewPage() {
<SectionBox title="Attention: Non-Bound PVCs">
<SimpleTable
columns={[
{ label: 'Name', getter: (pvc) => pvc.metadata.name },
{ label: 'Namespace', getter: (pvc) => pvc.metadata.namespace ?? '—' },
{ label: 'Name', getter: pvc => pvc.metadata.name },
{ label: 'Namespace', getter: pvc => pvc.metadata.namespace ?? '—' },
{
label: 'Status',
getter: (pvc) => (
getter: pvc => (
<StatusLabel status={phaseToStatus(pvc.status?.phase)}>
{pvc.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Age', getter: (pvc) => formatAge(pvc.metadata.creationTimestamp) },
{ label: 'Age', getter: pvc => formatAge(pvc.metadata.creationTimestamp) },
]}
data={nonBoundPvcs}
/>
@@ -298,11 +342,16 @@ function parseStorageToBytes(storage: string): number {
const suffix = match[2] ?? '';
const multipliers: Record<string, number> = {
'': 1,
K: 1e3, Ki: 1024,
M: 1e6, Mi: 1024 ** 2,
G: 1e9, Gi: 1024 ** 3,
T: 1e12, Ti: 1024 ** 4,
P: 1e15, Pi: 1024 ** 5,
K: 1e3,
Ki: 1024,
M: 1e6,
Mi: 1024 ** 2,
G: 1e9,
Gi: 1024 ** 3,
T: 1e12,
Ti: 1024 ** 4,
P: 1e15,
Pi: 1024 ** 5,
};
return value * (multipliers[suffix] ?? 1);
}
+6 -5
View File
@@ -5,10 +5,7 @@
* Uses registerDetailsViewSection in index.tsx.
*/
import {
NameValueTable,
SectionBox,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { findBoundPv, formatStorageType } from '../api/k8s';
import { useRookCephContext } from '../api/RookCephDataContext';
@@ -40,7 +37,11 @@ export default function PVCDetailSection({ resource }: PVCDetailSectionProps) {
// Determine storage type from driver name
const driver = boundPv.spec.csi?.driver ?? '';
const type = driver.includes('.rbd.') ? 'rbd' : driver.includes('.cephfs.') ? 'cephfs' : 'unknown';
const type = driver.includes('.rbd.')
? 'rbd'
: driver.includes('.cephfs.')
? 'cephfs'
: 'unknown';
return (
<SectionBox title="Rook-Ceph Storage Details">
+10 -6
View File
@@ -4,17 +4,17 @@
* Shown only when the PV uses a Rook-Ceph CSI driver.
*/
import {
NameValueTable,
SectionBox,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { formatStorageType, isRookCephPersistentVolume } from '../api/k8s';
interface PVDetailSectionProps {
resource: {
metadata?: { name?: string };
spec?: { csi?: { driver?: string; volumeHandle?: string; volumeAttributes?: Record<string, string> }; storageClassName?: string };
spec?: {
csi?: { driver?: string; volumeHandle?: string; volumeAttributes?: Record<string, string> };
storageClassName?: string;
};
jsonData?: unknown;
};
}
@@ -34,7 +34,11 @@ export default function PVDetailSection({ resource }: PVDetailSectionProps) {
}
const attrs = spec?.csi?.volumeAttributes ?? {};
const type = driver.includes('.rbd.') ? 'rbd' : driver.includes('.cephfs.') ? 'cephfs' : 'unknown';
const type = driver.includes('.rbd.')
? 'rbd'
: driver.includes('.cephfs.')
? 'cephfs'
: 'unknown';
return (
<SectionBox title="Rook-Ceph Volume Details">
+37 -32
View File
@@ -20,18 +20,18 @@ function PodTable({ pods, title }: { pods: RookCephPod[]; title: string }) {
<SectionBox title={`${title} (${pods.length})`}>
<SimpleTable
columns={[
{ label: 'Name', getter: (p) => p.metadata.name },
{ label: 'Name', getter: p => p.metadata.name },
{
label: 'Status',
getter: (p) => (
getter: p => (
<StatusLabel status={isPodReady(p) ? 'success' : 'error'}>
{p.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Node', getter: (p) => p.spec?.nodeName ?? '—' },
{ label: 'Restarts', getter: (p) => String(getPodRestarts(p)) },
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Node', getter: p => p.spec?.nodeName ?? '—' },
{ label: 'Restarts', getter: p => String(getPodRestarts(p)) },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={pods}
/>
@@ -45,27 +45,27 @@ function OsdTable({ pods }: { pods: RookCephPod[] }) {
<SectionBox title={`OSDs (${pods.length})`}>
<SimpleTable
columns={[
{ label: 'OSD ID', getter: (p) => p.metadata.labels?.['osd'] ?? p.metadata.name },
{ label: 'OSD ID', getter: p => p.metadata.labels?.['osd'] ?? p.metadata.name },
{
label: 'Status',
getter: (p) => {
const st = isPodReady(p) ? 'success' : p.status?.phase === 'Pending' ? 'warning' : 'error';
return (
<StatusLabel status={st}>
{p.status?.phase ?? 'Unknown'}
</StatusLabel>
);
getter: p => {
const st = isPodReady(p)
? 'success'
: p.status?.phase === 'Pending'
? 'warning'
: 'error';
return <StatusLabel status={st}>{p.status?.phase ?? 'Unknown'}</StatusLabel>;
},
},
{
label: 'Node',
getter: (p) => p.spec?.nodeName ?? p.metadata.labels?.['topology-location-host'] ?? '—',
getter: p => p.spec?.nodeName ?? p.metadata.labels?.['topology-location-host'] ?? '—',
},
{ label: 'Device Class', getter: (p) => p.metadata.labels?.['device-class'] ?? '—' },
{ label: 'Store', getter: (p) => p.metadata.labels?.['osd-store'] ?? '—' },
{ label: 'Failure Domain', getter: (p) => p.metadata.labels?.['failure-domain'] ?? '—' },
{ label: 'Restarts', getter: (p) => String(getPodRestarts(p)) },
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Device Class', getter: p => p.metadata.labels?.['device-class'] ?? '—' },
{ label: 'Store', getter: p => p.metadata.labels?.['osd-store'] ?? '—' },
{ label: 'Failure Domain', getter: p => p.metadata.labels?.['failure-domain'] ?? '—' },
{ label: 'Restarts', getter: p => String(getPodRestarts(p)) },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={pods}
/>
@@ -74,20 +74,19 @@ function OsdTable({ pods }: { pods: RookCephPod[] }) {
}
export default function PodsPage() {
const {
operatorPods,
monPods,
osdPods,
mgrPods,
csiRbdPods,
csiCephfsPods,
loading,
error,
} = useRookCephContext();
const { operatorPods, monPods, osdPods, mgrPods, csiRbdPods, csiCephfsPods, loading, error } =
useRookCephContext();
if (loading) return <Loader title="Loading Rook-Ceph pods..." />;
const allPods = [...operatorPods, ...monPods, ...osdPods, ...mgrPods, ...csiRbdPods, ...csiCephfsPods];
const allPods = [
...operatorPods,
...monPods,
...osdPods,
...mgrPods,
...csiRbdPods,
...csiCephfsPods,
];
const totalReady = allPods.filter(isPodReady).length;
return (
@@ -96,7 +95,9 @@ export default function PodsPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
@@ -106,7 +107,11 @@ export default function PodsPage() {
{
name: 'Overall Health',
value: (
<StatusLabel status={totalReady === allPods.length && allPods.length > 0 ? 'success' : 'warning'}>
<StatusLabel
status={
totalReady === allPods.length && allPods.length > 0 ? 'success' : 'warning'
}
>
{totalReady}/{allPods.length} pods ready
</StatusLabel>
),
+67 -12
View File
@@ -14,13 +14,24 @@ import React, { useState } from 'react';
import { formatAge, formatStorageType, RookCephStorageClass, storageClassType } from '../api/k8s';
import { useRookCephContext } from '../api/RookCephDataContext';
function StorageClassDetail({ sc, pvCount, onClose }: { sc: RookCephStorageClass; pvCount: number; onClose: () => void }) {
function StorageClassDetail({
sc,
pvCount,
onClose,
}: {
sc: RookCephStorageClass;
pvCount: number;
onClose: () => void;
}) {
const type = storageClassType(sc);
return (
<div
style={{
position: 'fixed',
top: 0, right: 0, bottom: 0, width: '480px',
top: 0,
right: 0,
bottom: 0,
width: '480px',
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
boxShadow: '-4px 0 16px rgba(0,0,0,0.15)',
zIndex: 1300,
@@ -28,7 +39,14 @@ function StorageClassDetail({ sc, pvCount, onClose }: { sc: RookCephStorageClass
padding: '24px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
<strong>{sc.metadata.name}</strong>
<button
onClick={onClose}
@@ -46,7 +64,10 @@ function StorageClassDetail({ sc, pvCount, onClose }: { sc: RookCephStorageClass
{ name: 'Type', value: formatStorageType(type) },
{ name: 'Reclaim Policy', value: sc.reclaimPolicy ?? '—' },
{ name: 'Volume Binding Mode', value: sc.volumeBindingMode ?? '—' },
{ name: 'Volume Expansion', value: sc.allowVolumeExpansion ? 'Allowed' : 'Not allowed' },
{
name: 'Volume Expansion',
value: sc.allowVolumeExpansion ? 'Allowed' : 'Not allowed',
},
{ name: 'Age', value: formatAge(sc.metadata.creationTimestamp) },
{ name: 'Bound PVs', value: String(pvCount) },
]}
@@ -81,14 +102,22 @@ export default function StorageClassesPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
{storageClasses.length === 0 ? (
<SectionBox title="No Storage Classes">
<NameValueTable
rows={[{ name: 'Status', value: 'No Rook-Ceph StorageClasses found. Ensure CephBlockPool and CephFilesystem resources exist.' }]}
rows={[
{
name: 'Status',
value:
'No Rook-Ceph StorageClasses found. Ensure CephBlockPool and CephFilesystem resources exist.',
},
]}
/>
</SectionBox>
) : (
@@ -100,7 +129,15 @@ export default function StorageClassesPage() {
getter: (sc: RookCephStorageClass) => (
<button
onClick={() => setSelected(sc)}
style={{ border: 'none', background: 'transparent', color: 'var(--link-color, #1976d2)', cursor: 'pointer', textDecoration: 'underline', padding: 0, font: 'inherit' }}
style={{
border: 'none',
background: 'transparent',
color: 'var(--link-color, #1976d2)',
cursor: 'pointer',
textDecoration: 'underline',
padding: 0,
font: 'inherit',
}}
>
{sc.metadata.name}
</button>
@@ -115,11 +152,24 @@ export default function StorageClassesPage() {
),
},
{ label: 'Provisioner', getter: (sc: RookCephStorageClass) => sc.provisioner },
{ label: 'Pool', getter: (sc: RookCephStorageClass) => sc.parameters?.['pool'] ?? '—' },
{
label: 'Pool',
getter: (sc: RookCephStorageClass) => sc.parameters?.['pool'] ?? '—',
},
{ label: 'Reclaim', getter: (sc: RookCephStorageClass) => sc.reclaimPolicy ?? '—' },
{ label: 'Expansion', getter: (sc: RookCephStorageClass) => sc.allowVolumeExpansion ? 'Yes' : 'No' },
{ label: 'PVs', getter: (sc: RookCephStorageClass) => String(pvCountByClass.get(sc.metadata.name) ?? 0) },
{ label: 'Age', getter: (sc: RookCephStorageClass) => formatAge(sc.metadata.creationTimestamp) },
{
label: 'Expansion',
getter: (sc: RookCephStorageClass) => (sc.allowVolumeExpansion ? 'Yes' : 'No'),
},
{
label: 'PVs',
getter: (sc: RookCephStorageClass) =>
String(pvCountByClass.get(sc.metadata.name) ?? 0),
},
{
label: 'Age',
getter: (sc: RookCephStorageClass) => formatAge(sc.metadata.creationTimestamp),
},
]}
data={storageClasses}
/>
@@ -129,7 +179,12 @@ export default function StorageClassesPage() {
{selected && (
<>
<div
style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.3)', zIndex: 1299 }}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
zIndex: 1299,
}}
onClick={() => setSelected(null)}
/>
<StorageClassDetail
+57 -11
View File
@@ -20,7 +20,10 @@ function PVDetail({ pv, onClose }: { pv: RookCephPersistentVolume; onClose: () =
<div
style={{
position: 'fixed',
top: 0, right: 0, bottom: 0, width: '520px',
top: 0,
right: 0,
bottom: 0,
width: '520px',
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
boxShadow: '-4px 0 16px rgba(0,0,0,0.15)',
zIndex: 1300,
@@ -28,7 +31,14 @@ function PVDetail({ pv, onClose }: { pv: RookCephPersistentVolume; onClose: () =
padding: '24px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
<strong>{pv.metadata.name}</strong>
<button
onClick={onClose}
@@ -89,7 +99,9 @@ export default function VolumesPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
@@ -108,14 +120,28 @@ export default function VolumesPage() {
getter: (pv: RookCephPersistentVolume) => (
<button
onClick={() => setSelected(pv)}
style={{ border: 'none', background: 'transparent', color: 'var(--link-color, #1976d2)', cursor: 'pointer', textDecoration: 'underline', padding: 0, font: 'inherit' }}
style={{
border: 'none',
background: 'transparent',
color: 'var(--link-color, #1976d2)',
cursor: 'pointer',
textDecoration: 'underline',
padding: 0,
font: 'inherit',
}}
>
{pv.metadata.name}
</button>
),
},
{ label: 'Capacity', getter: (pv: RookCephPersistentVolume) => pv.spec.capacity?.storage ?? '—' },
{ label: 'Access Modes', getter: (pv: RookCephPersistentVolume) => formatAccessModes(pv.spec.accessModes) },
{
label: 'Capacity',
getter: (pv: RookCephPersistentVolume) => pv.spec.capacity?.storage ?? '—',
},
{
label: 'Access Modes',
getter: (pv: RookCephPersistentVolume) => formatAccessModes(pv.spec.accessModes),
},
{
label: 'Phase',
getter: (pv: RookCephPersistentVolume) => (
@@ -124,10 +150,25 @@ export default function VolumesPage() {
</StatusLabel>
),
},
{ label: 'Reclaim', getter: (pv: RookCephPersistentVolume) => pv.spec.persistentVolumeReclaimPolicy ?? '—' },
{ label: 'Pool', getter: (pv: RookCephPersistentVolume) => pv.spec.csi?.volumeAttributes?.['pool'] ?? '—' },
{ label: 'Claim', getter: (pv: RookCephPersistentVolume) => pv.spec.claimRef ? `${pv.spec.claimRef.namespace}/${pv.spec.claimRef.name}` : '—' },
{ label: 'Age', getter: (pv: RookCephPersistentVolume) => formatAge(pv.metadata.creationTimestamp) },
{
label: 'Reclaim',
getter: (pv: RookCephPersistentVolume) =>
pv.spec.persistentVolumeReclaimPolicy ?? '—',
},
{
label: 'Pool',
getter: (pv: RookCephPersistentVolume) =>
pv.spec.csi?.volumeAttributes?.['pool'] ?? '—',
},
{
label: 'Claim',
getter: (pv: RookCephPersistentVolume) =>
pv.spec.claimRef ? `${pv.spec.claimRef.namespace}/${pv.spec.claimRef.name}` : '—',
},
{
label: 'Age',
getter: (pv: RookCephPersistentVolume) => formatAge(pv.metadata.creationTimestamp),
},
]}
data={persistentVolumes}
/>
@@ -137,7 +178,12 @@ export default function VolumesPage() {
{selected && (
<>
<div
style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.3)', zIndex: 1299 }}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
zIndex: 1299,
}}
onClick={() => setSelected(null)}
/>
<PVDetail pv={selected} onClose={() => setSelected(null)} />
@@ -64,7 +64,7 @@ export function buildStorageClassColumns() {
},
{
label: 'Pool',
getValue: (item: unknown) => getField(item, 'parameters', 'pool') as string | null ?? null,
getValue: (item: unknown) => (getField(item, 'parameters', 'pool') as string | null) ?? null,
render: (item: unknown) => {
if (!isRookRow(item)) return <span></span>;
const pool = getField(item, 'parameters', 'pool') as string | undefined;
@@ -73,12 +73,17 @@ export function buildStorageClassColumns() {
},
{
label: 'Cluster',
getValue: (item: unknown) => getField(item, 'parameters', 'clusterID') as string | null ?? null,
getValue: (item: unknown) =>
(getField(item, 'parameters', 'clusterID') as string | null) ?? null,
render: (item: unknown) => {
if (!isRookRow(item)) return <span></span>;
const clusterID = getField(item, 'parameters', 'clusterID') as string | undefined;
if (!clusterID) return <span></span>;
return <span title={clusterID}>{clusterID.length > 16 ? `${clusterID.slice(0, 16)}` : clusterID}</span>;
return (
<span title={clusterID}>
{clusterID.length > 16 ? `${clusterID.slice(0, 16)}` : clusterID}
</span>
);
},
},
];
@@ -101,10 +106,13 @@ export function buildPVColumns() {
},
{
label: 'Pool',
getValue: (item: unknown) => getField(item, 'spec', 'csi', 'volumeAttributes', 'pool') as string | null ?? null,
getValue: (item: unknown) =>
(getField(item, 'spec', 'csi', 'volumeAttributes', 'pool') as string | null) ?? null,
render: (item: unknown) => {
if (!isRookPvRow(item)) return <span></span>;
const pool = getField(item, 'spec', 'csi', 'volumeAttributes', 'pool') as string | undefined;
const pool = getField(item, 'spec', 'csi', 'volumeAttributes', 'pool') as
| string
| undefined;
return <span>{pool ?? '—'}</span>;
},
},
+16 -7
View File
@@ -16,7 +16,10 @@ import { RookCephDataProvider } from './api/RookCephDataContext';
import BlockPoolsPage from './components/BlockPoolsPage';
import CephPodDetailSection from './components/CephPodDetailSection';
import FilesystemsPage from './components/FilesystemsPage';
import { buildPVColumns, buildStorageClassColumns } from './components/integrations/StorageClassColumns';
import {
buildPVColumns,
buildStorageClassColumns,
} from './components/integrations/StorageClassColumns';
import ObjectStoresPage from './components/ObjectStoresPage';
import OverviewPage from './components/OverviewPage';
import PodsPage from './components/PodsPage';
@@ -207,11 +210,18 @@ registerDetailsViewSection(({ resource }) => {
// takes priority and falls back to the existing one (for mixed-driver tables).
function mergeColumns<T>(
existing: T[],
incoming: Array<{ label: string; getValue: (r: unknown) => unknown; render: (r: unknown) => React.ReactNode }>
incoming: Array<{
label: string;
getValue: (r: unknown) => unknown;
render: (r: unknown) => React.ReactNode;
}>
): T[] {
type ObjCol = { label: string; getValue: (r: unknown) => unknown; render: (r: unknown) => React.ReactNode };
const isObjCol = (c: unknown): c is ObjCol =>
typeof c === 'object' && c !== null && 'label' in c;
type ObjCol = {
label: string;
getValue: (r: unknown) => unknown;
render: (r: unknown) => React.ReactNode;
};
const isObjCol = (c: unknown): c is ObjCol => typeof c === 'object' && c !== null && 'label' in c;
const result = [...existing];
const toAppend: typeof incoming = [];
for (const col of incoming) {
@@ -221,7 +231,7 @@ function mergeColumns<T>(
result[idx] = {
label: col.label,
getValue: (r: unknown) => col.getValue(r) ?? prev.getValue(r),
render: (r: unknown) => col.getValue(r) !== null ? col.render(r) : prev.render(r),
render: (r: unknown) => (col.getValue(r) !== null ? col.render(r) : prev.render(r)),
} as unknown as T;
} else {
toAppend.push(col);
@@ -239,4 +249,3 @@ registerResourceTableColumnsProcessor(({ id, columns }) => {
}
return columns;
});