(res: any): T {
+ // for get endpoints that might return {data: {...}}
+ if (res?.data && typeof res.data === "object") return res.data as T
+ return res as T
+}
+
// --- UI helpers ---
function StatusBadge({ status }: { status?: string | null }) {
@@ -133,6 +116,61 @@ function StatusBadge({ status }: { status?: string | null }) {
)
}
+function RunStatusBadge({ status }: { status?: string | null }) {
+ const s = (status ?? "").toLowerCase()
+
+ if (!s)
+ return (
+
+ unknown
+
+ )
+
+ if (s === "succeeded" || s === "success") {
+ return (
+
+
+ succeeded
+
+ )
+ }
+
+ if (s === "failed" || s === "error") {
+ return (
+
+
+ failed
+
+ )
+ }
+
+ if (s === "queued" || s === "running") {
+ return (
+
+
+ {s}
+
+ )
+ }
+
+ return (
+
+ {s}
+
+ )
+}
+
+function fmtTime(v: any): string {
+ if (!v) return "-"
+ try {
+ const d = v instanceof Date ? v : new Date(v)
+ if (Number.isNaN(d.getTime())) return "-"
+ return d.toLocaleString()
+ } catch {
+ return "-"
+ }
+}
+
function ClusterSummary({ c }: { c: DtoClusterResponse }) {
return (
@@ -173,7 +211,7 @@ export const ClustersPage = () => {
const [deleteId, setDeleteId] = useState(null)
const [editingId, setEditingId] = useState(null)
- // Config dialog state
+ // Configure dialog state
const [configCluster, setConfigCluster] = useState(null)
const [captainDomainId, setCaptainDomainId] = useState("")
@@ -193,36 +231,69 @@ export const ClustersPage = () => {
const clustersQ = useQuery({
queryKey: ["clusters"],
- queryFn: () => clustersApi.listClusters(),
+ queryFn: async () => asArray(await clustersApi.listClusters()),
})
const lbsQ = useQuery({
queryKey: ["load-balancers"],
- queryFn: () => loadBalancersApi.listLoadBalancers(),
+ queryFn: async () =>
+ asArray(await loadBalancersApi.listLoadBalancers()),
})
const domainsQ = useQuery({
queryKey: ["domains"],
- queryFn: () => dnsApi.listDomains(),
+ queryFn: async () => asArray(await dnsApi.listDomains()),
})
- // record sets fetched per captain domain
const recordSetsQ = useQuery({
queryKey: ["record-sets", captainDomainId],
enabled: !!captainDomainId,
- queryFn: () => dnsApi.listRecordSetsByDomain(captainDomainId),
+ queryFn: async () =>
+ asArray(await dnsApi.listRecordSetsByDomain(captainDomainId)),
})
const serversQ = useQuery({
queryKey: ["servers"],
- queryFn: () => serversApi.listServers(),
+ queryFn: async () => asArray(await serversApi.listServers()),
})
const npQ = useQuery({
queryKey: ["node-pools"],
- queryFn: () => nodePoolsApi.listNodePools(),
+ queryFn: async () => asArray(await nodePoolsApi.listNodePools()),
})
+ const actionsQ = useQuery({
+ queryKey: ["actions"],
+ queryFn: async () => asArray(await actionsApi.listActions()),
+ })
+
+ const runsQ = useQuery({
+ queryKey: ["cluster-runs", configCluster?.id],
+ enabled: !!configCluster?.id,
+ queryFn: async () =>
+ asArray(await clustersApi.listClusterRuns(configCluster!.id!)),
+ refetchInterval: (data) => {
+ // IMPORTANT: data might not be array if queryFn isn't normalizing. But it is here anyway.
+ const rows = Array.isArray(data) ? data : []
+ const active = rows.some((r: any) => {
+ const s = String(r?.status ?? "").toLowerCase()
+ return s === "queued" || s === "running"
+ })
+ return active ? 2000 : false
+ },
+ })
+
+ const actionLabelByTarget = useMemo(() => {
+ const m = new Map()
+ ;(actionsQ.data ?? []).forEach((a) => {
+ if (a.make_target) m.set(a.make_target, a.label ?? a.make_target)
+ })
+ return m
+ }, [actionsQ.data])
+
+ const runDisplayName = (r: DtoClusterRunResponse) =>
+ actionLabelByTarget.get(r.action ?? "") ?? r.action ?? "unknown"
+
// --- Create ---
const createForm = useForm({
@@ -244,15 +315,10 @@ export const ClustersPage = () => {
setCreateOpen(false)
toast.success("Cluster created successfully.")
},
- onError: (err: any) => {
- toast.error(err?.message ?? "There was an error while creating the cluster")
- },
+ onError: (err: any) =>
+ toast.error(err?.message ?? "There was an error while creating the cluster"),
})
- const onCreateSubmit = (values: CreateClusterInput) => {
- createMut.mutate(values)
- }
-
// --- Update basic details ---
const updateForm = useForm({
@@ -269,9 +335,8 @@ export const ClustersPage = () => {
setUpdateOpen(false)
toast.success("Cluster updated successfully.")
},
- onError: (err: any) => {
- toast.error(err?.message ?? "There was an error while updating the cluster")
- },
+ onError: (err: any) =>
+ toast.error(err?.message ?? "There was an error while updating the cluster"),
})
const openEdit = (cluster: DtoClusterResponse) => {
@@ -296,11 +361,32 @@ export const ClustersPage = () => {
setDeleteId(null)
toast.success("Cluster deleted successfully.")
},
- onError: (err: any) => {
- toast.error(err?.message ?? "There was an error while deleting the cluster")
- },
+ onError: (err: any) =>
+ toast.error(err?.message ?? "There was an error while deleting the cluster"),
})
+ // --- Run Action ---
+
+ const runActionMut = useMutation({
+ mutationFn: ({ clusterID, actionID }: { clusterID: string; actionID: string }) =>
+ clustersApi.runClusterAction(clusterID, actionID),
+ onSuccess: async () => {
+ await qc.invalidateQueries({ queryKey: ["cluster-runs", configCluster?.id] })
+ toast.success("Action enqueued.")
+ },
+ onError: (err: any) => toast.error(err?.message ?? "Failed to enqueue action."),
+ })
+
+ async function handleRunAction(actionID: string) {
+ if (!configCluster?.id) return
+ setBusyKey(`run:${actionID}`)
+ try {
+ await runActionMut.mutateAsync({ clusterID: configCluster.id, actionID })
+ } finally {
+ setBusyKey(null)
+ }
+ }
+
// --- Filter ---
const filtered = useMemo(() => {
@@ -333,30 +419,23 @@ export const ClustersPage = () => {
return
}
- // Prefill IDs from current attachments
- if (configCluster.captain_domain?.id) {
- setCaptainDomainId(configCluster.captain_domain.id)
- }
- if (configCluster.control_plane_record_set?.id) {
+ if (configCluster.captain_domain?.id) setCaptainDomainId(configCluster.captain_domain.id)
+ if (configCluster.control_plane_record_set?.id)
setRecordSetId(configCluster.control_plane_record_set.id)
- }
- if (configCluster.apps_load_balancer?.id) {
- setAppsLbId(configCluster.apps_load_balancer.id)
- }
- if (configCluster.glueops_load_balancer?.id) {
+ if (configCluster.apps_load_balancer?.id) setAppsLbId(configCluster.apps_load_balancer.id)
+ if (configCluster.glueops_load_balancer?.id)
setGlueopsLbId(configCluster.glueops_load_balancer.id)
- }
- if (configCluster.bastion_server?.id) {
- setBastionId(configCluster.bastion_server.id)
- }
+ if (configCluster.bastion_server?.id) setBastionId(configCluster.bastion_server.id)
}, [configCluster])
async function refreshConfigCluster() {
if (!configCluster?.id) return
try {
- const updated = await clustersApi.getCluster(configCluster.id)
+ const updatedRaw = await clustersApi.getCluster(configCluster.id)
+ const updated = asObject(updatedRaw)
setConfigCluster(updated)
await qc.invalidateQueries({ queryKey: ["clusters"] })
+ await qc.invalidateQueries({ queryKey: ["cluster-runs", configCluster.id] })
} catch {
// ignore
}
@@ -364,15 +443,10 @@ export const ClustersPage = () => {
async function handleAttachCaptain() {
if (!configCluster?.id) return
- if (!captainDomainId) {
- toast.error("Domain is required")
- return
- }
+ if (!captainDomainId) return toast.error("Domain is required")
setBusyKey("captain")
try {
- await clustersApi.attachCaptainDomain(configCluster.id, {
- domain_id: captainDomainId,
- })
+ await clustersApi.attachCaptainDomain(configCluster.id, { domain_id: captainDomainId })
toast.success("Captain domain attached.")
await refreshConfigCluster()
} catch (err: any) {
@@ -398,10 +472,7 @@ export const ClustersPage = () => {
async function handleAttachRecordSet() {
if (!configCluster?.id) return
- if (!recordSetId) {
- toast.error("Record set is required")
- return
- }
+ if (!recordSetId) return toast.error("Record set is required")
setBusyKey("recordset")
try {
await clustersApi.attachControlPlaneRecordSet(configCluster.id, {
@@ -432,15 +503,10 @@ export const ClustersPage = () => {
async function handleAttachAppsLb() {
if (!configCluster?.id) return
- if (!appsLbId) {
- toast.error("Load balancer is required")
- return
- }
+ if (!appsLbId) return toast.error("Load balancer is required")
setBusyKey("apps-lb")
try {
- await clustersApi.attachAppsLoadBalancer(configCluster.id, {
- load_balancer_id: appsLbId,
- })
+ await clustersApi.attachAppsLoadBalancer(configCluster.id, { load_balancer_id: appsLbId })
toast.success("Apps load balancer attached.")
await refreshConfigCluster()
} catch (err: any) {
@@ -466,10 +532,7 @@ export const ClustersPage = () => {
async function handleAttachGlueopsLb() {
if (!configCluster?.id) return
- if (!glueopsLbId) {
- toast.error("Load balancer is required")
- return
- }
+ if (!glueopsLbId) return toast.error("Load balancer is required")
setBusyKey("glueops-lb")
try {
await clustersApi.attachGlueOpsLoadBalancer(configCluster.id, {
@@ -500,15 +563,10 @@ export const ClustersPage = () => {
async function handleAttachBastion() {
if (!configCluster?.id) return
- if (!bastionId) {
- toast.error("Server is required")
- return
- }
+ if (!bastionId) return toast.error("Server is required")
setBusyKey("bastion")
try {
- await clustersApi.attachBastion(configCluster.id, {
- server_id: bastionId,
- })
+ await clustersApi.attachBastion(configCluster.id, { server_id: bastionId })
toast.success("Bastion server attached.")
await refreshConfigCluster()
} catch (err: any) {
@@ -534,10 +592,7 @@ export const ClustersPage = () => {
async function handleAttachNodePool() {
if (!configCluster?.id) return
- if (!nodePoolId) {
- toast.error("Node pool is required")
- return
- }
+ if (!nodePoolId) return toast.error("Node pool is required")
setBusyKey("nodepool")
try {
await clustersApi.attachNodePool(configCluster.id, nodePoolId)
@@ -567,15 +622,10 @@ export const ClustersPage = () => {
async function handleSetKubeconfig() {
if (!configCluster?.id) return
- if (!kubeconfigText.trim()) {
- toast.error("Kubeconfig is required")
- return
- }
+ if (!kubeconfigText.trim()) return toast.error("Kubeconfig is required")
setBusyKey("kubeconfig")
try {
- await clustersApi.setKubeconfig(configCluster.id, {
- kubeconfig: kubeconfigText,
- })
+ await clustersApi.setKubeconfig(configCluster.id, { kubeconfig: kubeconfigText })
toast.success("Kubeconfig updated.")
setKubeconfigText("")
await refreshConfigCluster()
@@ -636,7 +686,10 @@ export const ClustersPage = () => {
)}
- {c.docker_image + ":" + c.docker_tag}
+ {(c.docker_image ?? "") + ":" + (c.docker_tag ?? "")}
{c.id && (
@@ -782,7 +835,7 @@ export const ClustersPage = () => {
{filtered.length === 0 && (
-
+
No clusters match your search.
@@ -799,6 +852,7 @@ export const ClustersPage = () => {
Edit Cluster
+