merge develop into release
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-14 13:36:16 +09:00
2 changed files with 71 additions and 19 deletions
@@ -1,12 +1,12 @@
import { RotateCcw } from 'lucide-react'; import { Braces, RotateCcw } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../../components/data-table'; import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../../components/data-table';
import { Badge, Button, Card, CardHeader } from '../../components/ui'; import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui';
import { PageTitle } from '../../layout/AppLayout'; import { PageTitle } from '../../layout/AppLayout';
import { runtimeStatusDotClass } from '../../lib/status-dot'; import { runtimeStatusDotClass } from '../../lib/status-dot';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import type { RuntimePodMetric, RuntimeResource, RuntimeResourceResponse } from '../../types/runtime'; import type { RuntimePodMetric, RuntimeResource, RuntimeResourceResponse } from '../../types/runtime';
import { loadApps, loadRuntimeEvents, loadRuntimePodMetrics, loadRuntimeResources } from '../runtime/runtime-api'; import { loadApps, loadRuntimePodMetrics, loadRuntimeResourceYaml, loadRuntimeResources } from '../runtime/runtime-api';
import { LoadingModal, useMinimumLoading } from '../operations/operation-utils'; import { LoadingModal, useMinimumLoading } from '../operations/operation-utils';
const defaultNamespaces = ['studio-system', 'stdo-lms', 'stdo-ads-ide']; const defaultNamespaces = ['studio-system', 'stdo-lms', 'stdo-ads-ide'];
@@ -40,7 +40,7 @@ function isNetwork(resource: RuntimeResource) {
} }
function isStorage(resource: RuntimeResource) { function isStorage(resource: RuntimeResource) {
return ['PersistentVolumeClaim', 'PersistentVolume', 'StorageClass'].includes(resource.kind); return ['PersistentVolumeClaim', 'PVC', 'PersistentVolume', 'StorageClass'].includes(resource.kind);
} }
function resourceStatusTone(status: string): 'neutral' | 'green' | 'amber' | 'red' | 'blue' { function resourceStatusTone(status: string): 'neutral' | 'green' | 'amber' | 'red' | 'blue' {
@@ -191,6 +191,22 @@ function ResourceTable({
loading: boolean; loading: boolean;
emptyMessage: string; emptyMessage: string;
}) { }) {
const [yamlTarget, setYamlTarget] = useState<ResourceRow | null>(null);
const [yaml, setYaml] = useState('');
const [yamlLoading, setYamlLoading] = useState(false);
const [yamlError, setYamlError] = useState('');
const openYaml = (row: ResourceRow) => {
setYamlTarget(row);
setYaml('');
setYamlError('');
setYamlLoading(true);
loadRuntimeResourceYaml(row.namespace, row.kind, row.name)
.then((response) => setYaml(response.yaml))
.catch((error: unknown) => setYamlError(error instanceof Error ? error.message : 'YAML을 불러오지 못했습니다.'))
.finally(() => setYamlLoading(false));
};
const columns = useMemo<Array<DataTableColumn<ResourceRow>>>( const columns = useMemo<Array<DataTableColumn<ResourceRow>>>(
() => [ () => [
{ key: 'namespace', header: '네임스페이스', className: 'w-44 px-5 py-3', cell: (row) => <span className="font-medium">{row.namespace}</span> }, { key: 'namespace', header: '네임스페이스', className: 'w-44 px-5 py-3', cell: (row) => <span className="font-medium">{row.namespace}</span> },
@@ -201,7 +217,23 @@ function ResourceTable({
{ key: 'restarts', header: '재시작', className: 'w-24 px-5 py-3', cell: (row) => <span className="text-muted-foreground">{row.restarts ?? '-'}</span> }, { key: 'restarts', header: '재시작', className: 'w-24 px-5 py-3', cell: (row) => <span className="text-muted-foreground">{row.restarts ?? '-'}</span> },
{ key: 'metric', header: '사용량', className: 'w-40 px-5 py-3', cell: (row) => <span className="text-xs text-muted-foreground">{row.metric ? `${row.metric.cpu} / ${row.metric.memory}` : '-'}</span> }, { key: 'metric', header: '사용량', className: 'w-40 px-5 py-3', cell: (row) => <span className="text-xs text-muted-foreground">{row.metric ? `${row.metric.cpu} / ${row.metric.memory}` : '-'}</span> },
{ key: 'age', header: 'Age', className: 'w-28 px-5 py-3', cell: (row) => <span className="text-muted-foreground">{row.age || '-'}</span> }, { key: 'age', header: 'Age', className: 'w-28 px-5 py-3', cell: (row) => <span className="text-muted-foreground">{row.age || '-'}</span> },
{ key: 'detail', header: '상세', className: 'min-w-[260px] px-5 py-3', cell: (row) => <span className="text-xs text-muted-foreground">{row.detail || '-'}</span> } { key: 'detail', header: '상세', className: 'min-w-[220px] px-5 py-3', cell: (row) => <span className="text-xs text-muted-foreground">{row.detail || '-'}</span> },
{
key: 'yaml',
header: '',
className: 'w-14 px-3 py-3 text-right',
cell: (row) => (
<Button
variant="ghost"
className="size-8 px-0"
title="YAML 보기"
aria-label="YAML 보기"
onClick={() => openYaml(row)}
>
<Braces className="size-4" />
</Button>
)
}
], ],
[] []
); );
@@ -209,6 +241,7 @@ function ResourceTable({
const filters = useMemo(() => [NamespaceFilter(rows), KindFilter(rows)], [rows]); const filters = useMemo(() => [NamespaceFilter(rows), KindFilter(rows)], [rows]);
return ( return (
<>
<DataTableCard <DataTableCard
title={title} title={title}
description={description} description={description}
@@ -219,10 +252,23 @@ function ResourceTable({
getSearchText={(row) => `${row.namespace} ${row.kind} ${row.name} ${row.status} ${row.detail}`} getSearchText={(row) => `${row.namespace} ${row.kind} ${row.name} ${row.status} ${row.detail}`}
getRowKey={(row) => row.id} getRowKey={(row) => row.id}
pageSize={10} pageSize={10}
minWidth={1280} minWidth={1320}
loading={loading} loading={loading}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
/> />
<Modal
open={Boolean(yamlTarget)}
title={yamlTarget ? `${yamlTarget.kind}/${yamlTarget.name}` : 'YAML'}
onClose={() => setYamlTarget(null)}
className="!max-w-5xl"
>
{yamlLoading ? <p className="py-10 text-center text-sm text-muted-foreground">불러오는 중...</p> : null}
{yamlError ? <p className="py-4 text-sm text-rose-300">{yamlError}</p> : null}
{!yamlLoading && !yamlError ? (
<pre className="max-h-[65vh] overflow-auto rounded-md border border-border bg-black/30 p-4 text-xs leading-6 text-slate-200">{yaml}</pre>
) : null}
</Modal>
</>
); );
} }
+6
View File
@@ -66,6 +66,12 @@ export function loadRuntimeResources(namespace: string) {
return apiRequest<RuntimeResourceResponse>(`/apps/runtime-namespaces/${encodeURIComponent(namespace)}/resources`); return apiRequest<RuntimeResourceResponse>(`/apps/runtime-namespaces/${encodeURIComponent(namespace)}/resources`);
} }
export function loadRuntimeResourceYaml(namespace: string, kind: string, name: string) {
return apiRequest<{ namespace: string; kind: string; name: string; yaml: string }>(
`/apps/runtime-namespaces/${encodeURIComponent(namespace)}/resources/${encodeURIComponent(kind)}/${encodeURIComponent(name)}/yaml`
);
}
export function loadRuntimePodMetrics(namespace?: string) { export function loadRuntimePodMetrics(namespace?: string) {
const query = namespace ? `?namespace=${encodeURIComponent(namespace)}` : ''; const query = namespace ? `?namespace=${encodeURIComponent(namespace)}` : '';
return apiRequest<RuntimePodMetric[]>(`/runtime/metrics/pods${query}`); return apiRequest<RuntimePodMetric[]>(`/runtime/metrics/pods${query}`);