kubeops 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +219 -0
  2. package/electron/main.js +429 -0
  3. package/electron/preload.js +13 -0
  4. package/electron-builder.yml +60 -0
  5. package/next.config.ts +8 -0
  6. package/package.json +98 -0
  7. package/postcss.config.mjs +7 -0
  8. package/resources/icon.icns +0 -0
  9. package/resources/icon.ico +0 -0
  10. package/resources/icon.png +0 -0
  11. package/resources/icon.svg +86 -0
  12. package/scripts/build-server.mjs +20 -0
  13. package/scripts/generate-icons.mjs +61 -0
  14. package/server.ts +58 -0
  15. package/src/app/api/clusters/[clusterId]/health/route.ts +27 -0
  16. package/src/app/api/clusters/[clusterId]/metrics/route.ts +196 -0
  17. package/src/app/api/clusters/[clusterId]/namespaces/route.ts +48 -0
  18. package/src/app/api/clusters/[clusterId]/nodes/[nodeName]/route.ts +21 -0
  19. package/src/app/api/clusters/[clusterId]/nodes/route.ts +31 -0
  20. package/src/app/api/clusters/[clusterId]/resources/[namespace]/[...resourcePath]/route.ts +204 -0
  21. package/src/app/api/clusters/route.ts +48 -0
  22. package/src/app/api/kubeconfig/route.ts +25 -0
  23. package/src/app/api/port-forward/route.ts +143 -0
  24. package/src/app/api/tsh/login/route.ts +50 -0
  25. package/src/app/clusters/[clusterId]/app-map/page.tsx +42 -0
  26. package/src/app/clusters/[clusterId]/clusterrolebindings/[name]/page.tsx +5 -0
  27. package/src/app/clusters/[clusterId]/clusterrolebindings/page.tsx +5 -0
  28. package/src/app/clusters/[clusterId]/clusterroles/[name]/page.tsx +5 -0
  29. package/src/app/clusters/[clusterId]/clusterroles/page.tsx +5 -0
  30. package/src/app/clusters/[clusterId]/layout.tsx +9 -0
  31. package/src/app/clusters/[clusterId]/namespaces/[namespace]/configmaps/[name]/page.tsx +5 -0
  32. package/src/app/clusters/[clusterId]/namespaces/[namespace]/configmaps/page.tsx +5 -0
  33. package/src/app/clusters/[clusterId]/namespaces/[namespace]/cronjobs/[name]/page.tsx +5 -0
  34. package/src/app/clusters/[clusterId]/namespaces/[namespace]/cronjobs/page.tsx +5 -0
  35. package/src/app/clusters/[clusterId]/namespaces/[namespace]/daemonsets/[name]/page.tsx +5 -0
  36. package/src/app/clusters/[clusterId]/namespaces/[namespace]/daemonsets/page.tsx +5 -0
  37. package/src/app/clusters/[clusterId]/namespaces/[namespace]/deployments/[name]/page.tsx +457 -0
  38. package/src/app/clusters/[clusterId]/namespaces/[namespace]/deployments/page.tsx +5 -0
  39. package/src/app/clusters/[clusterId]/namespaces/[namespace]/endpoints/[name]/page.tsx +5 -0
  40. package/src/app/clusters/[clusterId]/namespaces/[namespace]/endpoints/page.tsx +5 -0
  41. package/src/app/clusters/[clusterId]/namespaces/[namespace]/events/page.tsx +5 -0
  42. package/src/app/clusters/[clusterId]/namespaces/[namespace]/ingresses/[name]/page.tsx +5 -0
  43. package/src/app/clusters/[clusterId]/namespaces/[namespace]/ingresses/page.tsx +5 -0
  44. package/src/app/clusters/[clusterId]/namespaces/[namespace]/jobs/[name]/page.tsx +5 -0
  45. package/src/app/clusters/[clusterId]/namespaces/[namespace]/jobs/page.tsx +5 -0
  46. package/src/app/clusters/[clusterId]/namespaces/[namespace]/networkpolicies/[name]/page.tsx +5 -0
  47. package/src/app/clusters/[clusterId]/namespaces/[namespace]/networkpolicies/page.tsx +5 -0
  48. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/exec/page.tsx +173 -0
  49. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/logs/page.tsx +137 -0
  50. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/page.tsx +448 -0
  51. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/page.tsx +5 -0
  52. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pvcs/[name]/page.tsx +5 -0
  53. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pvcs/page.tsx +5 -0
  54. package/src/app/clusters/[clusterId]/namespaces/[namespace]/replicasets/[name]/page.tsx +5 -0
  55. package/src/app/clusters/[clusterId]/namespaces/[namespace]/replicasets/page.tsx +5 -0
  56. package/src/app/clusters/[clusterId]/namespaces/[namespace]/rolebindings/[name]/page.tsx +5 -0
  57. package/src/app/clusters/[clusterId]/namespaces/[namespace]/rolebindings/page.tsx +5 -0
  58. package/src/app/clusters/[clusterId]/namespaces/[namespace]/roles/[name]/page.tsx +5 -0
  59. package/src/app/clusters/[clusterId]/namespaces/[namespace]/roles/page.tsx +5 -0
  60. package/src/app/clusters/[clusterId]/namespaces/[namespace]/secrets/[name]/page.tsx +168 -0
  61. package/src/app/clusters/[clusterId]/namespaces/[namespace]/secrets/page.tsx +5 -0
  62. package/src/app/clusters/[clusterId]/namespaces/[namespace]/serviceaccounts/[name]/page.tsx +5 -0
  63. package/src/app/clusters/[clusterId]/namespaces/[namespace]/serviceaccounts/page.tsx +5 -0
  64. package/src/app/clusters/[clusterId]/namespaces/[namespace]/services/[name]/page.tsx +5 -0
  65. package/src/app/clusters/[clusterId]/namespaces/[namespace]/services/page.tsx +5 -0
  66. package/src/app/clusters/[clusterId]/namespaces/[namespace]/statefulsets/[name]/page.tsx +302 -0
  67. package/src/app/clusters/[clusterId]/namespaces/[namespace]/statefulsets/page.tsx +5 -0
  68. package/src/app/clusters/[clusterId]/nodes/[nodeName]/page.tsx +5 -0
  69. package/src/app/clusters/[clusterId]/nodes/page.tsx +5 -0
  70. package/src/app/clusters/[clusterId]/page.tsx +635 -0
  71. package/src/app/clusters/[clusterId]/port-forwarding/page.tsx +145 -0
  72. package/src/app/clusters/[clusterId]/pvs/[name]/page.tsx +5 -0
  73. package/src/app/clusters/[clusterId]/pvs/page.tsx +5 -0
  74. package/src/app/clusters/page.tsx +166 -0
  75. package/src/app/favicon.ico +0 -0
  76. package/src/app/globals.css +167 -0
  77. package/src/app/layout.tsx +48 -0
  78. package/src/app/page.tsx +5 -0
  79. package/src/components/clusters/cluster-selector.tsx +64 -0
  80. package/src/components/layout/app-shell.tsx +26 -0
  81. package/src/components/layout/breadcrumbs.tsx +97 -0
  82. package/src/components/layout/command-palette.tsx +112 -0
  83. package/src/components/layout/header.tsx +184 -0
  84. package/src/components/layout/sidebar.tsx +84 -0
  85. package/src/components/layout/theme-toggle.tsx +21 -0
  86. package/src/components/namespaces/namespace-selector.tsx +165 -0
  87. package/src/components/panel/bottom-panel.tsx +127 -0
  88. package/src/components/panel/logs-tab.tsx +109 -0
  89. package/src/components/panel/terminal-tab.tsx +180 -0
  90. package/src/components/pods/pod-watch-button.tsx +44 -0
  91. package/src/components/resources/resource-columns.tsx +320 -0
  92. package/src/components/resources/resource-detail-page.tsx +191 -0
  93. package/src/components/resources/resource-list-page.tsx +78 -0
  94. package/src/components/resources/scale-dialog.tsx +107 -0
  95. package/src/components/settings/settings-dialog.tsx +103 -0
  96. package/src/components/shared/age-display.tsx +27 -0
  97. package/src/components/shared/confirm-dialog.tsx +52 -0
  98. package/src/components/shared/data-table.tsx +149 -0
  99. package/src/components/shared/env-value-resolver.tsx +570 -0
  100. package/src/components/shared/error-display.tsx +109 -0
  101. package/src/components/shared/loading-skeleton.tsx +25 -0
  102. package/src/components/shared/metrics-charts-impl.tsx +434 -0
  103. package/src/components/shared/metrics-charts.tsx +24 -0
  104. package/src/components/shared/port-forward-btn.tsx +60 -0
  105. package/src/components/shared/resource-info-drawer.tsx +542 -0
  106. package/src/components/shared/resource-node.tsx +157 -0
  107. package/src/components/shared/resource-tree-impl.tsx +228 -0
  108. package/src/components/shared/resource-tree.tsx +20 -0
  109. package/src/components/shared/status-badge.tsx +35 -0
  110. package/src/components/shared/yaml-editor.tsx +438 -0
  111. package/src/components/ui/badge.tsx +48 -0
  112. package/src/components/ui/button.tsx +64 -0
  113. package/src/components/ui/card.tsx +92 -0
  114. package/src/components/ui/command.tsx +184 -0
  115. package/src/components/ui/dialog.tsx +158 -0
  116. package/src/components/ui/dropdown-menu.tsx +257 -0
  117. package/src/components/ui/input.tsx +21 -0
  118. package/src/components/ui/popover.tsx +89 -0
  119. package/src/components/ui/scroll-area.tsx +58 -0
  120. package/src/components/ui/select.tsx +190 -0
  121. package/src/components/ui/separator.tsx +28 -0
  122. package/src/components/ui/sheet.tsx +143 -0
  123. package/src/components/ui/skeleton.tsx +13 -0
  124. package/src/components/ui/sonner.tsx +40 -0
  125. package/src/components/ui/table.tsx +116 -0
  126. package/src/components/ui/tabs.tsx +91 -0
  127. package/src/components/ui/tooltip.tsx +57 -0
  128. package/src/hooks/use-age-tick.ts +40 -0
  129. package/src/hooks/use-auto-update.ts +100 -0
  130. package/src/hooks/use-clusters.ts +32 -0
  131. package/src/hooks/use-namespaces.ts +17 -0
  132. package/src/hooks/use-pod-watcher.ts +79 -0
  133. package/src/hooks/use-port-forwards.ts +18 -0
  134. package/src/hooks/use-resource-detail.ts +28 -0
  135. package/src/hooks/use-resource-list.ts +26 -0
  136. package/src/hooks/use-resource-tree.ts +440 -0
  137. package/src/lib/api-client.ts +31 -0
  138. package/src/lib/constants.ts +126 -0
  139. package/src/lib/k8s/client-factory.ts +57 -0
  140. package/src/lib/k8s/kubeconfig-manager.ts +43 -0
  141. package/src/lib/k8s/resource-api.ts +223 -0
  142. package/src/lib/k8s/types.ts +29 -0
  143. package/src/lib/utils.ts +6 -0
  144. package/src/providers/swr-provider.tsx +20 -0
  145. package/src/providers/theme-provider.tsx +17 -0
  146. package/src/stores/cluster-store.ts +32 -0
  147. package/src/stores/namespace-store.ts +27 -0
  148. package/src/stores/panel-store.ts +61 -0
  149. package/src/stores/pod-watcher-store.ts +69 -0
  150. package/src/stores/settings-store.ts +24 -0
  151. package/src/stores/sidebar-store.ts +22 -0
  152. package/src/types/cluster.ts +19 -0
  153. package/src/types/css.d.ts +6 -0
  154. package/src/types/electron.d.ts +25 -0
  155. package/src/types/navigation.ts +4 -0
  156. package/src/types/resource.ts +27 -0
  157. package/tsconfig.json +34 -0
  158. package/ws/exec-handler.ts +112 -0
  159. package/ws/index.ts +2 -0
  160. package/ws/logs-handler.ts +70 -0
@@ -0,0 +1,57 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Tooltip as TooltipPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function TooltipProvider({
9
+ delayDuration = 0,
10
+ ...props
11
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
12
+ return (
13
+ <TooltipPrimitive.Provider
14
+ data-slot="tooltip-provider"
15
+ delayDuration={delayDuration}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ function Tooltip({
22
+ ...props
23
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
24
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
25
+ }
26
+
27
+ function TooltipTrigger({
28
+ ...props
29
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
30
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
31
+ }
32
+
33
+ function TooltipContent({
34
+ className,
35
+ sideOffset = 0,
36
+ children,
37
+ ...props
38
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
39
+ return (
40
+ <TooltipPrimitive.Portal>
41
+ <TooltipPrimitive.Content
42
+ data-slot="tooltip-content"
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ {children}
51
+ <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
52
+ </TooltipPrimitive.Content>
53
+ </TooltipPrimitive.Portal>
54
+ )
55
+ }
56
+
57
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,40 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ const INTERVAL_MS = 30_000;
4
+
5
+ let tick = 0;
6
+ let listeners = new Set<() => void>();
7
+ let timer: ReturnType<typeof setInterval> | null = null;
8
+
9
+ function startTimer() {
10
+ if (timer) return;
11
+ timer = setInterval(() => {
12
+ tick++;
13
+ listeners.forEach((l) => l());
14
+ }, INTERVAL_MS);
15
+ }
16
+
17
+ function stopTimer() {
18
+ if (timer) {
19
+ clearInterval(timer);
20
+ timer = null;
21
+ }
22
+ }
23
+
24
+ function subscribe(listener: () => void) {
25
+ listeners.add(listener);
26
+ if (listeners.size === 1) startTimer();
27
+ return () => {
28
+ listeners.delete(listener);
29
+ if (listeners.size === 0) stopTimer();
30
+ };
31
+ }
32
+
33
+ function getSnapshot() {
34
+ return tick;
35
+ }
36
+
37
+ /** Returns a counter that increments every 30s. All consumers share a single timer. */
38
+ export function useAgeTick(): number {
39
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
40
+ }
@@ -0,0 +1,100 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import type { UpdateStatus } from '@/types/electron';
5
+
6
+ export type UpdatePhase =
7
+ | 'idle'
8
+ | 'checking'
9
+ | 'available'
10
+ | 'not-available'
11
+ | 'downloading'
12
+ | 'downloaded'
13
+ | 'error';
14
+
15
+ export function useAutoUpdate() {
16
+ const [phase, setPhase] = useState<UpdatePhase>('idle');
17
+ const [version, setVersion] = useState<string | null>(null);
18
+ const [percent, setPercent] = useState(0);
19
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
20
+ const [appVersion, setAppVersion] = useState<string | null>(null);
21
+
22
+ const api = typeof window !== 'undefined' ? window.electronUpdater : undefined;
23
+ const isAvailable = !!api;
24
+ const apiRef = useRef(api);
25
+ apiRef.current = api;
26
+
27
+ useEffect(() => {
28
+ apiRef.current?.getAppVersion().then(setAppVersion).catch(() => {});
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ if (!apiRef.current) return;
33
+
34
+ const unsubscribe = apiRef.current.onUpdateStatus((status: UpdateStatus) => {
35
+ switch (status.status) {
36
+ case 'checking':
37
+ setPhase('checking');
38
+ break;
39
+ case 'available':
40
+ setPhase('available');
41
+ setVersion(status.version ?? null);
42
+ break;
43
+ case 'not-available':
44
+ setPhase('not-available');
45
+ setTimeout(() => setPhase('idle'), 3000);
46
+ break;
47
+ case 'downloading':
48
+ setPhase('downloading');
49
+ setPercent(status.percent ?? 0);
50
+ break;
51
+ case 'downloaded':
52
+ setPhase('downloaded');
53
+ setVersion(status.version ?? null);
54
+ break;
55
+ case 'error':
56
+ setPhase('error');
57
+ setErrorMessage(status.message ?? 'Unknown error');
58
+ break;
59
+ }
60
+ });
61
+
62
+ return unsubscribe;
63
+ }, []);
64
+
65
+ const checkForUpdates = useCallback(() => {
66
+ if (!apiRef.current) return;
67
+ setPhase('checking');
68
+ setErrorMessage(null);
69
+ apiRef.current.checkForUpdates().catch(() => {});
70
+ }, []);
71
+
72
+ const downloadUpdate = useCallback(() => {
73
+ if (!apiRef.current) return;
74
+ setPercent(0);
75
+ apiRef.current.downloadUpdate().catch(() => {});
76
+ }, []);
77
+
78
+ const quitAndInstall = useCallback(() => {
79
+ if (!apiRef.current) return;
80
+ apiRef.current.quitAndInstall().catch(() => {});
81
+ }, []);
82
+
83
+ const dismiss = useCallback(() => {
84
+ setPhase('idle');
85
+ setErrorMessage(null);
86
+ }, []);
87
+
88
+ return {
89
+ phase,
90
+ version,
91
+ percent,
92
+ errorMessage,
93
+ isAvailable,
94
+ appVersion,
95
+ checkForUpdates,
96
+ downloadUpdate,
97
+ quitAndInstall,
98
+ dismiss,
99
+ };
100
+ }
@@ -0,0 +1,32 @@
1
+ import useSWR from 'swr';
2
+ import { useEffect } from 'react';
3
+ import { ClusterInfo } from '@/types/cluster';
4
+ import { useNamespaceStore } from '@/stores/namespace-store';
5
+
6
+ export function useClusters() {
7
+ const { data, error, isLoading, mutate } = useSWR<{ clusters: ClusterInfo[] }>(
8
+ '/api/clusters',
9
+ {
10
+ refreshInterval: 30000,
11
+ }
12
+ );
13
+
14
+ const { activeNamespaces, setActiveNamespace } = useNamespaceStore();
15
+
16
+ // Auto-set namespace from kubeconfig context when not yet configured
17
+ useEffect(() => {
18
+ if (!data?.clusters) return;
19
+ for (const cluster of data.clusters) {
20
+ if (cluster.namespace && !activeNamespaces[cluster.name]) {
21
+ setActiveNamespace(cluster.name, cluster.namespace);
22
+ }
23
+ }
24
+ }, [data?.clusters, activeNamespaces, setActiveNamespace]);
25
+
26
+ return {
27
+ clusters: data?.clusters || [],
28
+ error,
29
+ isLoading,
30
+ mutate,
31
+ };
32
+ }
@@ -0,0 +1,17 @@
1
+ import useSWR from 'swr';
2
+
3
+ export function useNamespaces(clusterId: string | null) {
4
+ const key = clusterId
5
+ ? `/api/clusters/${encodeURIComponent(clusterId)}/namespaces`
6
+ : null;
7
+
8
+ const { data, error, isLoading } = useSWR(key, {
9
+ refreshInterval: 30000,
10
+ });
11
+
12
+ return {
13
+ namespaces: data?.namespaces || [],
14
+ error,
15
+ isLoading,
16
+ };
17
+ }
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { usePodWatcherStore } from '@/stores/pod-watcher-store';
5
+ import { toast } from 'sonner';
6
+
7
+ interface PodLike {
8
+ metadata?: {
9
+ name?: string;
10
+ namespace?: string;
11
+ };
12
+ status?: {
13
+ containerStatuses?: Array<{
14
+ restartCount?: number;
15
+ }>;
16
+ };
17
+ }
18
+
19
+ export function usePodRestartWatcher(
20
+ clusterId: string,
21
+ pods: PodLike[] | undefined
22
+ ) {
23
+ const { watchedPods, notificationsEnabled, getSnapshot, updateSnapshot } =
24
+ usePodWatcherStore();
25
+ const initializedRef = useRef<Set<string>>(new Set());
26
+
27
+ useEffect(() => {
28
+ if (!pods || pods.length === 0) return;
29
+
30
+ for (const pod of pods) {
31
+ const name = pod.metadata?.name;
32
+ const namespace = pod.metadata?.namespace;
33
+ if (!name || !namespace) continue;
34
+
35
+ const watchKey = { clusterId, namespace, name };
36
+ const isWatched = watchedPods.some(
37
+ (w) =>
38
+ w.clusterId === clusterId &&
39
+ w.namespace === namespace &&
40
+ w.name === name
41
+ );
42
+ if (!isWatched) continue;
43
+
44
+ const totalRestarts = (pod.status?.containerStatuses || []).reduce(
45
+ (sum, cs) => sum + (cs.restartCount || 0),
46
+ 0
47
+ );
48
+
49
+ const snapshotKey = `${clusterId}/${namespace}/${name}`;
50
+ const previousSnapshot = getSnapshot(watchKey);
51
+
52
+ if (previousSnapshot === undefined) {
53
+ // First time seeing this pod — set baseline, no notification
54
+ updateSnapshot(watchKey, totalRestarts);
55
+ initializedRef.current.add(snapshotKey);
56
+ continue;
57
+ }
58
+
59
+ if (totalRestarts > previousSnapshot) {
60
+ const delta = totalRestarts - previousSnapshot;
61
+ const msg = `Pod ${name} restarted ${delta} time${delta > 1 ? 's' : ''} (total: ${totalRestarts})`;
62
+
63
+ if (notificationsEnabled) {
64
+ toast.warning(msg, { duration: 8000 });
65
+
66
+ if (
67
+ typeof window !== 'undefined' &&
68
+ 'Notification' in window &&
69
+ Notification.permission === 'granted'
70
+ ) {
71
+ new Notification('Pod Restart Detected', { body: msg });
72
+ }
73
+ }
74
+
75
+ updateSnapshot(watchKey, totalRestarts);
76
+ }
77
+ }
78
+ }, [pods, clusterId, watchedPods, notificationsEnabled, getSnapshot, updateSnapshot]);
79
+ }
@@ -0,0 +1,18 @@
1
+ import useSWR from 'swr';
2
+
3
+ interface PortForward {
4
+ id: string;
5
+ localPort: number;
6
+ containerPort: number;
7
+ status: string;
8
+ }
9
+
10
+ /**
11
+ * Shared hook for port-forward data.
12
+ * No polling — relies on globalMutate('/api/port-forward') calls after start/stop actions.
13
+ */
14
+ export function usePortForwards() {
15
+ const { data, mutate } = useSWR<{ forwards: PortForward[] }>('/api/port-forward');
16
+ const forwards: PortForward[] = data?.forwards || [];
17
+ return { forwards, mutate };
18
+ }
@@ -0,0 +1,28 @@
1
+ import useSWR from 'swr';
2
+
3
+ interface UseResourceDetailOptions {
4
+ clusterId: string | null;
5
+ namespace: string;
6
+ resourceType: string;
7
+ name: string;
8
+ refreshInterval?: number;
9
+ enabled?: boolean;
10
+ }
11
+
12
+ export function useResourceDetail({
13
+ clusterId,
14
+ namespace,
15
+ resourceType,
16
+ name,
17
+ refreshInterval = 5000,
18
+ enabled = true,
19
+ }: UseResourceDetailOptions) {
20
+ const key =
21
+ enabled && clusterId
22
+ ? `/api/clusters/${encodeURIComponent(clusterId)}/resources/${namespace}/${resourceType}/${name}`
23
+ : null;
24
+
25
+ return useSWR(key, {
26
+ refreshInterval,
27
+ });
28
+ }
@@ -0,0 +1,26 @@
1
+ import useSWR from 'swr';
2
+
3
+ interface UseResourceListOptions {
4
+ clusterId: string | null;
5
+ namespace: string;
6
+ resourceType: string;
7
+ refreshInterval?: number;
8
+ enabled?: boolean;
9
+ }
10
+
11
+ export function useResourceList({
12
+ clusterId,
13
+ namespace,
14
+ resourceType,
15
+ refreshInterval = 5000,
16
+ enabled = true,
17
+ }: UseResourceListOptions) {
18
+ const key =
19
+ enabled && clusterId
20
+ ? `/api/clusters/${encodeURIComponent(clusterId)}/resources/${namespace}/${resourceType}`
21
+ : null;
22
+
23
+ return useSWR(key, {
24
+ refreshInterval,
25
+ });
26
+ }