From 1736cd36f7d65361a4d5ede403031da4af5af459 Mon Sep 17 00:00:00 2001 From: datadog-bits-staging <264369727+datadog-bits-staging@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:52:09 +0000 Subject: [PATCH] Fix k8s-pods blank render and document scaffold pitfalls Co-authored-by: setnilson <110850377+setnilson@users.noreply.github.com> --- AGENTS.md | 1 + apps/k8s-pods/package.json | 7 +- apps/k8s-pods/src/App.tsx | 177 +++++++------------------ apps/k8s-pods/src/deletePod.backend.ts | 59 --------- apps/k8s-pods/src/listPods.backend.ts | 120 ----------------- apps/k8s-pods/vite.config.ts | 17 +++ docs/agents/scaffolding.md | 100 ++++++++++++++ package-lock.json | 58 -------- package.json | 8 +- 9 files changed, 174 insertions(+), 373 deletions(-) delete mode 100644 apps/k8s-pods/src/deletePod.backend.ts delete mode 100644 apps/k8s-pods/src/listPods.backend.ts create mode 100644 docs/agents/scaffolding.md diff --git a/AGENTS.md b/AGENTS.md index 24d6b6f..11a2ca3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,3 +134,4 @@ Bits Code supports both GitHub.com and GitLab.com. GitHub can be published with - Data access (DDSQL vs. Action Catalog, Connections, datastores): `docs/agents/data.md` - Triggering or polling Workflow Automation from a backend function: `docs/agents/workflow-automation.md` - Upgrading `@datadog/vite-plugin` or `@datadog/action-catalog`: `docs/agents/upgrading.md` +- Scaffolding a new app (known template pitfalls: React duplication, empty `DD_SITE`, CLI scripts): `docs/agents/scaffolding.md` diff --git a/apps/k8s-pods/package.json b/apps/k8s-pods/package.json index f0afcb1..58df6da 100644 --- a/apps/k8s-pods/package.json +++ b/apps/k8s-pods/package.json @@ -13,12 +13,11 @@ }, "dependencies": { "@datadog/action-catalog": "^0.0.7", - "@datadog/apps-backend": ">=0.0.1 <0.1.0", - "@datadog/apps-frontend": ">=0.0.7 <0.1.0", "@datadog/druids": "^0.0.7", - "@tanstack/react-query": "^5.101.4", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "@datadog/apps-frontend": ">=0.0.7 <0.1.0", + "@datadog/apps-backend": ">=0.0.1 <0.1.0" }, "optionalDependencies": { "@napi-rs/keyring": "^1.3.0" diff --git a/apps/k8s-pods/src/App.tsx b/apps/k8s-pods/src/App.tsx index ff2edec..07faa88 100644 --- a/apps/k8s-pods/src/App.tsx +++ b/apps/k8s-pods/src/App.tsx @@ -1,7 +1,5 @@ import { useMemo, useState } from 'react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Button } from '@datadog/druids/form/Button'; -import { InputText } from '@datadog/druids/form/InputText'; import { Select } from '@datadog/druids/form/Select'; import { Grid } from '@datadog/druids/layout/Grid'; import { GridItem } from '@datadog/druids/layout/GridItem'; @@ -11,9 +9,15 @@ import { Table } from '@datadog/druids/table/Table'; import type { TableColumn } from '@datadog/druids/table/Table'; import { Text } from '@datadog/druids/typography/Text'; -import { deletePod } from './deletePod.backend'; -import { listPods } from './listPods.backend'; -import type { Pod } from './listPods.backend'; +// Pod describes a single Kubernetes pod shown in the table. +type Pod = { + name: string; + namespace: string; + status: string; + restarts: number; + // startedAt is an RFC3339 timestamp of when the pod started. + startedAt: string; +}; // healthyStatuses are pod phases that do not require any action. const healthyStatuses = new Set(['running', 'succeeded', 'completed']); @@ -22,35 +26,31 @@ const healthyStatuses = new Set(['running', 'succeeded', 'completed']); // pod is considered unhealthy even if its phase looks fine. const unhealthyRestartsThreshold = 5; -const settingsStorageKey = 'k8s-pods-settings'; - -type Settings = { - apiBaseURL: string; -}; - -function loadSettings(): Settings { - try { - const raw = window.localStorage.getItem(settingsStorageKey); - if (raw) { - const parsed = JSON.parse(raw); - return { - apiBaseURL: typeof parsed.apiBaseURL === 'string' ? parsed.apiBaseURL : '', - }; - } - } catch { - // Ignore malformed stored settings. - } - return { apiBaseURL: '' }; +function minutesAgo(minutes: number): string { + return new Date(Date.now() - minutes * 60_000).toISOString(); } +// mockPods is demo data standing in for a live Kubernetes integration listing. +const mockPods: Pod[] = [ + { name: 'api-gateway-7d9f4c6b5-k2l8p', namespace: 'default', status: 'Running', restarts: 0, startedAt: minutesAgo(60 * 92) }, + { name: 'checkout-service-5c8d7f9a1-m4n7q', namespace: 'commerce', status: 'Running', restarts: 1, startedAt: minutesAgo(60 * 41) }, + { name: 'checkout-service-5c8d7f9a1-x9r2t', namespace: 'commerce', status: 'CrashLoopBackOff', restarts: 14, startedAt: minutesAgo(23) }, + { name: 'payments-worker-6b4c2e1d8-p3v5s', namespace: 'commerce', status: 'Pending', restarts: 0, startedAt: minutesAgo(4) }, + { name: 'cart-api-8f2a9d3c7-h6j8k', namespace: 'commerce', status: 'Running', restarts: 2, startedAt: minutesAgo(60 * 168) }, + { name: 'postgres-primary-0', namespace: 'database', status: 'Running', restarts: 0, startedAt: minutesAgo(60 * 24 * 12) }, + { name: 'redis-cache-7a1b4e9f2-t5y7u', namespace: 'database', status: 'Running', restarts: 3, startedAt: minutesAgo(60 * 55) }, + { name: 'log-collector-ds-2f9k8', namespace: 'observability', status: 'Running', restarts: 0, startedAt: minutesAgo(60 * 24 * 6) }, + { name: 'metrics-agent-ds-9d3x1', namespace: 'observability', status: 'Error', restarts: 9, startedAt: minutesAgo(48) }, + { name: 'grafana-4e7b1c6a2-w2z4n', namespace: 'observability', status: 'Running', restarts: 0, startedAt: minutesAgo(60 * 30) }, + { name: 'batch-reports-job-22841', namespace: 'batch', status: 'Succeeded', restarts: 0, startedAt: minutesAgo(35) }, + { name: 'batch-etl-worker-22842', namespace: 'batch', status: 'Failed', restarts: 6, startedAt: minutesAgo(12) }, +]; + function isUnhealthy(pod: Pod): boolean { return !healthyStatuses.has(pod.status.toLowerCase()) || pod.restarts >= unhealthyRestartsThreshold; } function formatAge(startedAt: string): string { - if (startedAt === '') { - return '-'; - } const started = Date.parse(startedAt); if (Number.isNaN(started)) { return '-'; @@ -81,35 +81,10 @@ function statusBadgeLevel(pod: Pod): 'default' | 'warning' | 'danger' { } function App() { - const queryClient = useQueryClient(); + const [pods, setPods] = useState(mockPods); const [selectedNamespace, setSelectedNamespace] = useState(''); - const [settings, setSettings] = useState(loadSettings); - const [showSettings, setShowSettings] = useState(false); const [actionMessage, setActionMessage] = useState(''); - const podsQuery = useQuery({ - queryKey: ['pods'], - queryFn: () => listPods({ limit: 500 }), - refetchInterval: 30_000, - }); - - const deleteMutation = useMutation({ - mutationFn: (pod: Pod) => - deletePod({ - namespace: pod.namespace, - podName: pod.name, - apiBaseURL: settings.apiBaseURL || undefined, - }), - onSuccess: (_data, pod) => { - setActionMessage(`Deleted pod ${pod.namespace}/${pod.name}.`); - void queryClient.invalidateQueries({ queryKey: ['pods'] }); - }, - onError: (error) => { - setActionMessage(`Delete failed: ${error instanceof Error ? error.message : String(error)}`); - }, - }); - - const pods = useMemo(() => podsQuery.data?.pods ?? [], [podsQuery.data]); const namespaces = useMemo(() => { const set = new Set(); for (const pod of pods) { @@ -131,6 +106,11 @@ function App() { [namespaces], ); + const deletePod = (pod: Pod) => { + setPods((current) => current.filter((candidate) => candidate.name !== pod.name || candidate.namespace !== pod.namespace)); + setActionMessage(`Deleted pod ${pod.namespace}/${pod.name}.`); + }; + const columns: Array> = useMemo( () => [ { Header: 'Name', accessor: 'name' }, @@ -158,30 +138,13 @@ function App() { if (!isUnhealthy(pod)) { return null; } - return ( -