Explorar o código

feat: add management tabs component

Thomas Zhang hai 3 meses
pai
achega
5847728955

+ 12 - 0
package-lock.json

@@ -28,6 +28,7 @@
 				"bcryptjs": "^3.0.3",
 				"bits-ui": "^2.16.5",
 				"clsx": "^2.1.1",
+				"date-fns": "^4.3.0",
 				"eslint": "^9.39.2",
 				"eslint-config-prettier": "^10.1.8",
 				"eslint-plugin-simple-import-sort": "^13.0.0",
@@ -4019,6 +4020,17 @@
 				"node": ">=12"
 			}
 		},
+		"node_modules/date-fns": {
+			"version": "4.3.0",
+			"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.3.0.tgz",
+			"integrity": "sha512-OYcL+3N/jyWbYdFGqoMAhytDgxP9pbYPUUiRCOgn4Fewaadk9l/Wam4Avciiyp2BgkpfQyBV9B+ehnVJych+eQ==",
+			"dev": true,
+			"license": "MIT",
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/kossnocorp"
+			}
+		},
 		"node_modules/dayjs": {
 			"version": "1.11.20",
 			"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",

+ 1 - 0
package.json

@@ -38,6 +38,7 @@
 		"bcryptjs": "^3.0.3",
 		"bits-ui": "^2.16.5",
 		"clsx": "^2.1.1",
+		"date-fns": "^4.3.0",
 		"eslint": "^9.39.2",
 		"eslint-config-prettier": "^10.1.8",
 		"eslint-plugin-simple-import-sort": "^13.0.0",

+ 3 - 0
src/lib/components/management/change-list/index.ts

@@ -0,0 +1,3 @@
+import ChangeList from './list.svelte';
+
+export { ChangeList };

+ 77 - 0
src/lib/components/management/change-list/list.svelte

@@ -0,0 +1,77 @@
+<script lang="ts">
+	import { GitCommitVertical, GitPullRequestIcon } from '@lucide/svelte';
+	import { formatDistanceToNow } from 'date-fns';
+
+	import { GlassPane } from '$lib/components/common/glass-pane';
+	import type { GitCommit, GitPullRequest } from '$lib/types/workspace';
+	import type { Changes } from '$lib/types/workspace';
+
+	let { onloadchanges }: { onloadchanges?: () => Promise<Changes> } = $props();
+
+	const PAGE_SIZE = 5;
+	let loading = $state(true);
+	let expanded = $state(false);
+	let prs = $state<GitPullRequest[]>([]);
+	let commits = $state<GitCommit[]>([]);
+
+	$effect(() => {
+		fetchData();
+	});
+
+	const fetchData = async () => {
+		loading = true;
+		try {
+			const changes = await onloadchanges?.();
+			if (changes) {
+				prs = changes.gitPullRequests;
+				commits = changes.gitCommits;
+			}
+		} catch (err) {
+			console.error('Failed to fetch changes:', err);
+		}
+		loading = false;
+	};
+
+	const openPrs = $derived(prs.filter((p) => !p.merged && p.state === 'open'));
+	const total = $derived(openPrs.length + commits.length);
+	const visibleCommits = $derived(
+		expanded ? commits : commits.slice(0, Math.max(0, PAGE_SIZE - openPrs.length)),
+	);
+
+	const formatDate = (date?: Date) => (date ? formatDistanceToNow(date, { addSuffix: true }) : '');
+</script>
+
+<div>
+	<h2 class="mb-2 text-lg font-thin text-white/75">History</h2>
+	<div class="space-y-2">
+		{#each openPrs as pr (pr.id)}
+			<GlassPane class="flex items-center gap-2 px-3 py-2">
+				<GitPullRequestIcon class="shrink-0 text-blue-400/70" size={14} strokeWidth={2} />
+				<span class="flex-1 text-sm text-white/80">{pr.title}</span>
+				<span class="shrink-0 text-xs text-white/30">{formatDate(pr.createdAt)}</span>
+			</GlassPane>
+		{/each}
+		{#each visibleCommits as commit (commit.sha)}
+			<GlassPane class="flex items-center gap-2 px-3 py-2">
+				<GitCommitVertical class="shrink-0 text-white/30" size={14} strokeWidth={2} />
+				<code class="w-14 shrink-0 font-mono text-xs text-white/30">
+					{commit.sha.substring(0, 7)}
+				</code>
+				<span class="flex-1 text-sm text-white/80">{commit.message}</span>
+				<span class="shrink-0 text-xs text-white/30">{formatDate(commit.createdAt)}</span>
+			</GlassPane>
+		{/each}
+		{#if openPrs.length === 0 && commits.length === 0 && !loading}
+			<p class="text-sm text-white/25">No changes found</p>
+		{/if}
+	</div>
+	{#if total > PAGE_SIZE}
+		<button
+			class="mt-2 w-full py-1.5 text-center text-xs text-white/30 hover:text-white/60"
+			type="button"
+			onclick={() => (expanded = !expanded)}
+		>
+			{expanded ? 'Show Less' : 'Show More…'}
+		</button>
+	{/if}
+</div>

+ 3 - 0
src/lib/components/management/custom-resource/index.ts

@@ -0,0 +1,3 @@
+import CustomResourcePane from './pane.svelte';
+
+export { CustomResourcePane };

+ 22 - 0
src/lib/components/management/custom-resource/pane.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+	import { CopyButton } from '$lib/components/common/copy';
+	import { GlassPane } from '$lib/components/common/glass-pane';
+
+	type Props = {
+		yaml: string;
+	};
+
+	let { yaml }: Props = $props();
+</script>
+
+<div>
+	<h2 class="mb-2 text-lg font-thin text-white/75">Custom Resource</h2>
+	<div class="relative">
+		<GlassPane class="overflow-x-auto bg-black/25 p-4">
+			<pre class="font-mono text-sm leading-relaxed text-white/70">{yaml}</pre>
+		</GlassPane>
+		<div class="absolute top-2 right-2">
+			<CopyButton content={yaml} />
+		</div>
+	</div>
+</div>

+ 4 - 0
src/lib/components/management/index.ts

@@ -0,0 +1,4 @@
+import ManagementTabs from './tabs.svelte';
+
+export type { Condition } from './types';
+export { ManagementTabs };

+ 3 - 0
src/lib/components/management/resource-list/index.ts

@@ -0,0 +1,3 @@
+import ResourceList from './list.svelte';
+
+export { ResourceList };

+ 76 - 0
src/lib/components/management/resource-list/list.svelte

@@ -0,0 +1,76 @@
+<script lang="ts">
+	import type { V1Deployment, V1Pod, V1Service } from '@kubernetes/client-node';
+
+	import { GlassPane } from '$lib/components/common/glass-pane';
+	import { Spinner } from '$lib/components/controls/spinner';
+	import type { CoreResources } from '$lib/types/workspace';
+
+	let {
+		onloadresources,
+	}: {
+		onloadresources?: () => Promise<CoreResources>;
+	} = $props();
+
+	let loading = $state(true);
+	let pods = $state<V1Pod[]>([]);
+	let deployments = $state<V1Deployment[]>([]);
+	let services = $state<V1Service[]>([]);
+
+	$effect(() => {
+		fetchData();
+	});
+
+	const fetchData = async () => {
+		loading = true;
+		try {
+			const resources = await onloadresources?.();
+			if (resources) {
+				pods = resources.pods;
+				deployments = resources.deployments;
+				services = resources.services;
+			}
+		} catch (err) {
+			console.error('Failed to fetch resources:', err);
+		}
+		loading = false;
+	};
+</script>
+
+<div class="space-y-2">
+	<h2 class="mb-2 text-lg font-thin text-white/75">Pods</h2>
+	{#if loading}
+		<Spinner class="size-4" />
+	{:else}
+		{#each pods as pod (pod.metadata?.uid)}
+			<GlassPane class="flex items-center gap-2 px-3 py-2">
+				<span class="flex-1 font-mono text-sm text-white/70">{pod.metadata?.name}</span>
+			</GlassPane>
+		{:else}
+			<p class="text-sm text-white/25">No pods found</p>
+		{/each}
+	{/if}
+	<h2 class="mt-4 mb-2 text-lg font-thin text-white/75">Deployments</h2>
+	{#if loading}
+		<Spinner class="size-4" />
+	{:else}
+		{#each deployments as deployment (deployment.metadata?.uid)}
+			<GlassPane class="flex items-center gap-2 px-3 py-2">
+				<span class="flex-1 font-mono text-sm text-white/70">{deployment.metadata?.name}</span>
+			</GlassPane>
+		{:else}
+			<p class="text-sm text-white/25">No deployments found</p>
+		{/each}
+	{/if}
+	<h2 class="mt-4 mb-2 text-lg font-thin text-white/75">Services</h2>
+	{#if loading}
+		<Spinner class="size-4" />
+	{:else}
+		{#each services as service (service.metadata?.uid)}
+			<GlassPane class="flex items-center gap-2 px-3 py-2">
+				<span class="flex-1 font-mono text-sm text-white/70">{service.metadata?.name}</span>
+			</GlassPane>
+		{:else}
+			<p class="text-sm text-white/25">No services found</p>
+		{/each}
+	{/if}
+</div>

+ 3 - 0
src/lib/components/management/status-list/index.ts

@@ -0,0 +1,3 @@
+import StatusList from './list.svelte';
+
+export { StatusList };

+ 27 - 0
src/lib/components/management/status-list/list.svelte

@@ -0,0 +1,27 @@
+<script lang="ts">
+	import { GlassPane } from '$lib/components/common/glass-pane';
+	import type { Condition } from '$lib/components/management/types';
+
+	let {
+		conditions,
+	}: {
+		conditions: Condition[];
+	} = $props();
+</script>
+
+<div>
+	<h2 class="mb-2 text-lg font-thin text-white/75">Conditions</h2>
+	<div class="space-y-2">
+		{#each conditions as condition, idx (idx)}
+			<GlassPane class="flex items-center justify-between gap-4 px-4 py-2">
+				<div>
+					<div class="text-sm font-medium">{condition.type}</div>
+					<div class="text-xs text-white/50">{condition.message}</div>
+				</div>
+				<div>{condition.status}</div>
+			</GlassPane>
+		{:else}
+			<p class="text-sm text-white/25">No conditions found</p>
+		{/each}
+	</div>
+</div>

+ 85 - 0
src/lib/components/management/tabs.svelte

@@ -0,0 +1,85 @@
+<script lang="ts">
+	import { GlassPane } from '$lib/components/common/glass-pane';
+	import * as Tabs from '$lib/components/controls/tabs';
+	import { ChangeList } from '$lib/components/management/change-list';
+	import { CustomResourcePane } from '$lib/components/management/custom-resource';
+	import { ResourceList } from '$lib/components/management/resource-list';
+	import { StatusList } from '$lib/components/management/status-list';
+	import type { Condition } from '$lib/components/management/types';
+	import type { Changes, CoreResources } from '$lib/types/workspace';
+
+	let {
+		tab = $bindable('status'),
+		conditions,
+		onloadresources,
+		onloadchanges,
+		cr,
+	}: {
+		tab?: string;
+		conditions?: Condition[];
+		onloadresources?: () => Promise<CoreResources>;
+		onloadchanges?: () => Promise<Changes>;
+		cr?: string;
+	} = $props();
+</script>
+
+<Tabs.Root bind:value={tab}>
+	<div class="border-b border-white/10">
+		<Tabs.List variant="line">
+			<Tabs.Trigger value="status">Status</Tabs.Trigger>
+			<Tabs.Trigger value="resources">Resources</Tabs.Trigger>
+			<Tabs.Trigger value="changes">Changes</Tabs.Trigger>
+			<Tabs.Trigger value="custom-resource">Definitions</Tabs.Trigger>
+		</Tabs.List>
+	</div>
+
+	<Tabs.Content value="status">
+		<div class="my-4 min-h-96">
+			{#if conditions}
+				<StatusList {conditions} />
+			{:else}
+				<GlassPane>
+					<p class="py-6 text-center text-sm text-white/30">No status information available.</p>
+				</GlassPane>
+			{/if}
+		</div>
+	</Tabs.Content>
+
+	<Tabs.Content value="resources">
+		<div class="my-4 min-h-96">
+			{#if onloadresources}
+				<ResourceList {onloadresources} />
+			{:else}
+				<GlassPane>
+					<p class="py-6 text-center text-sm text-white/30">No resource information available.</p>
+				</GlassPane>
+			{/if}
+		</div>
+	</Tabs.Content>
+
+	<Tabs.Content value="changes">
+		<div class="my-4 min-h-96">
+			{#if onloadchanges}
+				<ChangeList {onloadchanges} />
+			{:else}
+				<GlassPane>
+					<p class="py-6 text-center text-sm text-white/30">No git information available.</p>
+				</GlassPane>
+			{/if}
+		</div>
+	</Tabs.Content>
+
+	<Tabs.Content value="custom-resource">
+		<div class="my-4 min-h-96">
+			{#if cr}
+				<CustomResourcePane yaml={cr} />
+			{:else}
+				<GlassPane>
+					<p class="py-6 text-center text-sm text-white/30">
+						No custom resource information available.
+					</p>
+				</GlassPane>
+			{/if}
+		</div>
+	</Tabs.Content>
+</Tabs.Root>

+ 7 - 0
src/lib/components/management/types.ts

@@ -0,0 +1,7 @@
+interface Condition {
+	type: string;
+	status: string;
+	message?: string;
+}
+
+export type { Condition };