Skip to content

Commit dc5c719

Browse files
authored
feat(dynamodb): expand dashboard management UI (#10)
* feat(dynamodb): expand dashboard inspection tools * chore(dynamodb): add dashboard management autoloop * feat(dynamodb): add dashboard management flows * feat(dynamodb): add advanced dashboard management UI
1 parent 96c8879 commit dc5c719

28 files changed

Lines changed: 3387 additions & 72 deletions

File tree

.agents/nexus.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
| 2026-05-02 | Nexus | drafted SQS compatibility design | `docs/design-sqs-compat.md` | scoped as design-only groundwork for SQS local compatibility server |
44
| 2026-05-02 | Nexus | drafted Google Cloud Pub/Sub compatibility design | `docs/design-pubsub-compat.md` | keeps Pub/Sub on shared Message Core with SQS-compatible delivery streams |
55
| 2026-05-04 | Nexus | drafted Amazon Redshift compatibility design | `docs/design-redshift-compat.md` | aligns PostgreSQL wire protocol, Data API, management API, and shared Query Engine phases |
6+
| 2026-05-05 | Nexus | routed DynamoDB dashboard management UI upgrade | `web/dashboard/src/app/services/dynamodb/`, `web/dashboard/src/styles/globals.css` | selected explorer + local implementation; non-destructive UI metadata/detail expansion |

.agents/orbit.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@
1212
| 2026-05-05 | Orbit | split remaining Redshift PostgreSQL default-backend tasks into a Codex autoloop | `scripts/redshift-remaining-autoloop/`, `.gitignore` | ready for bounded default backend flip, docs status, and full-remaining gates |
1313
| 2026-05-05 | Orbit | split Redshift dashboard SQL query runner UI into a Codex autoloop | `scripts/redshift-query-runner-ui-autoloop/`, `.gitignore` | ready for bounded Redshift query runner UI, frontend, dashboard API, e2e, and full-query-runner-ui gates |
1414
| 2026-05-05 | Orbit | split Redshift advanced compatibility into a Codex autoloop | `scripts/redshift-advanced-compat-autoloop/`, `.gitignore` | ready for staged extended protocol, advanced SQL, serverless metadata, snapshots, introspection, procedures, dashboard/e2e, and full-advanced gates |
15+
| 2026-05-05 | Orbit | split DynamoDB dashboard management UI tasks into a Codex autoloop | `scripts/dynamodb-dashboard-management-autoloop/`, `.gitignore` | ready for guarded operations UI, Query/Scan UI, E2E, docs, and full-management-ui gates |
16+
| 2026-05-06 | Orbit | split DynamoDB dashboard advanced UI tasks into a Codex autoloop | `scripts/dynamodb-dashboard-advanced-autoloop/`, `.gitignore` | ready for pagination, saved/recent operations, table wizard, validation, delete confirmation, e2e, docs, and full-advanced-ui gates |

.gitignore

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@ scripts/dynamodb-autoloop/progress.md
3535
scripts/dynamodb-autoloop/done.md
3636
scripts/dynamodb-autoloop/iteration-*.out
3737
scripts/dynamodb-autoloop/prompt-*.*
38+
scripts/dynamodb-dashboard-management-autoloop/.run-loop.lock
39+
scripts/dynamodb-dashboard-management-autoloop/.circuit-state
40+
scripts/dynamodb-dashboard-management-autoloop/runner.jsonl
41+
scripts/dynamodb-dashboard-management-autoloop/runner.log
42+
scripts/dynamodb-dashboard-management-autoloop/state.env
43+
scripts/dynamodb-dashboard-management-autoloop/state.env.sha256
44+
scripts/dynamodb-dashboard-management-autoloop/progress.md
45+
scripts/dynamodb-dashboard-management-autoloop/done.md
46+
scripts/dynamodb-dashboard-management-autoloop/iteration-*.out
47+
scripts/dynamodb-dashboard-management-autoloop/verify-*.out
48+
scripts/dynamodb-dashboard-management-autoloop/prompt-*.*
49+
scripts/dynamodb-dashboard-advanced-autoloop/.run-loop.lock
50+
scripts/dynamodb-dashboard-advanced-autoloop/.circuit-state
51+
scripts/dynamodb-dashboard-advanced-autoloop/runner.jsonl
52+
scripts/dynamodb-dashboard-advanced-autoloop/runner.log
53+
scripts/dynamodb-dashboard-advanced-autoloop/state.env
54+
scripts/dynamodb-dashboard-advanced-autoloop/state.env.sha256
55+
scripts/dynamodb-dashboard-advanced-autoloop/progress.md
56+
scripts/dynamodb-dashboard-advanced-autoloop/done.md
57+
scripts/dynamodb-dashboard-advanced-autoloop/iteration-*.out
58+
scripts/dynamodb-dashboard-advanced-autoloop/verify-*.out
59+
scripts/dynamodb-dashboard-advanced-autoloop/prompt-*.*
3860
scripts/bigquery-autoloop/.run-loop.lock
3961
scripts/bigquery-autoloop/.circuit-state
4062
scripts/bigquery-autoloop/runner.jsonl

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ Legend:
293293
| Real capacity accounting/throttling | No | Local deterministic behavior, not AWS capacity simulation. |
294294
| IAM condition enforcement | No | Not implemented. |
295295

296+
DynamoDB dashboard management is available under `/dashboard/dynamodb` and the local `/api/dynamodb/*` dashboard API. The dashboard can inspect tables, items, indexes, TTL, and streams, and can run guarded local management flows for `CreateTable`, `PutItem`, `UpdateItem`, `UpdateTimeToLive`, `DeleteItem`, `DeleteTable`, `Query`, and `Scan`. Query and Scan expose result pagination with count, scanned count, next/previous controls, and selected result item JSON. Recent operation history is stored only in browser `localStorage` as metadata; it does not persist item payloads, credentials, full request payloads, or pagination keys. Dashboard mutation endpoints forward through the local DynamoDB JSON protocol path instead of editing storage directly. Destructive `DeleteItem` and `DeleteTable` flows require confirmation text matching the selected table name, and disabled DynamoDB services do not expose active mutation controls.
297+
296298
### Google Cloud Pub/Sub-Compatible API
297299

298300
| Feature | Status | Notes |
@@ -349,7 +351,7 @@ Pub/Sub dashboard actions are available under `/dashboard/pubsub`:
349351
| Mail messages API | Yes | List, fetch detail/raw, delete. |
350352
| S3 dashboard API | Yes | Bucket/object listing, download links. |
351353
| GCS dashboard API | Yes | Bucket/object/upload-session inspection. |
352-
| DynamoDB dashboard API | Yes | Status, tables, table detail, indexes, TTL, streams, items. |
354+
| DynamoDB dashboard API | Yes | Status, tables, table detail, indexes, TTL, streams, items, guarded management operations, Query, and Scan. |
353355
| SQS dashboard API | Yes | Status, queues, messages, leases, DLQ, and purge. |
354356
| Pub/Sub dashboard API | Yes | Status, topics, subscriptions, publish, pull, ack, and message metadata. |
355357
| Redshift dashboard API | Yes | Status, clusters, catalog, table detail, query runner, and statement history. |

internal/dashboard/assets/react/assets/index-Jon0rN-z.css renamed to internal/dashboard/assets/react/assets/index-BbvjViDt.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/dashboard/assets/react/assets/index-BvaRNLFE.js

Lines changed: 0 additions & 40 deletions
This file was deleted.

internal/dashboard/assets/react/assets/index-CmaHHuQ1.js

Lines changed: 76 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/dashboard/assets/react/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>devcloud Dashboard</title>
7-
<script type="module" crossorigin src="/dashboard/assets/index-BvaRNLFE.js"></script>
8-
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-Jon0rN-z.css">
7+
<script type="module" crossorigin src="/dashboard/assets/index-CmaHHuQ1.js"></script>
8+
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-BbvjViDt.css">
99
</head>
1010
<body>
1111
<div id="root"></div>

internal/dashboard/server.go

Lines changed: 112 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dashboard
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"errors"
@@ -1251,25 +1252,24 @@ func (s *Server) handleDynamoDBStatus(w http.ResponseWriter, r *http.Request) {
12511252
}
12521253

12531254
func (s *Server) handleDynamoDBTables(w http.ResponseWriter, r *http.Request) {
1254-
if r.Method != http.MethodGet {
1255-
methodNotAllowed(w, "GET")
1256-
return
1257-
}
12581255
if s.dynamo == nil {
12591256
http.Error(w, "dynamodb service is disabled", http.StatusServiceUnavailable)
12601257
return
12611258
}
1262-
snapshot := s.dynamo.Snapshot()
1263-
writeJSON(w, map[string]any{
1264-
"tables": snapshot.Tables,
1265-
})
1259+
switch r.Method {
1260+
case http.MethodGet:
1261+
snapshot := s.dynamo.Snapshot()
1262+
writeJSON(w, map[string]any{
1263+
"tables": snapshot.Tables,
1264+
})
1265+
case http.MethodPost:
1266+
s.forwardDynamoDBDashboardOperation(w, r, "CreateTable", "")
1267+
default:
1268+
methodNotAllowed(w, "GET, POST")
1269+
}
12661270
}
12671271

12681272
func (s *Server) handleDynamoDBTable(w http.ResponseWriter, r *http.Request) {
1269-
if r.Method != http.MethodGet {
1270-
methodNotAllowed(w, "GET")
1271-
return
1272-
}
12731273
if s.dynamo == nil {
12741274
http.Error(w, "dynamodb service is disabled", http.StatusServiceUnavailable)
12751275
return
@@ -1285,6 +1285,39 @@ func (s *Server) handleDynamoDBTable(w http.ResponseWriter, r *http.Request) {
12851285
http.NotFound(w, r)
12861286
return
12871287
}
1288+
if hasSuffix {
1289+
switch suffix {
1290+
case "items":
1291+
if r.Method == http.MethodPost {
1292+
s.forwardDynamoDBDashboardOperation(w, r, "PutItem", tableName)
1293+
return
1294+
}
1295+
case "items/update":
1296+
s.forwardDynamoDBDashboardOperation(w, r, "UpdateItem", tableName)
1297+
return
1298+
case "items/delete":
1299+
s.forwardDynamoDBDashboardOperationWithConfirmation(w, r, "DeleteItem", tableName, tableName)
1300+
return
1301+
case "ttl":
1302+
if r.Method == http.MethodPost {
1303+
s.forwardDynamoDBDashboardOperation(w, r, "UpdateTimeToLive", tableName)
1304+
return
1305+
}
1306+
case "query":
1307+
s.forwardDynamoDBDashboardOperation(w, r, "Query", tableName)
1308+
return
1309+
case "scan":
1310+
s.forwardDynamoDBDashboardOperation(w, r, "Scan", tableName)
1311+
return
1312+
case "delete":
1313+
s.forwardDynamoDBDashboardOperationWithConfirmation(w, r, "DeleteTable", tableName, tableName)
1314+
return
1315+
}
1316+
}
1317+
if r.Method != http.MethodGet {
1318+
methodNotAllowed(w, "GET")
1319+
return
1320+
}
12881321
table, found := s.dynamo.TableSnapshot(tableName)
12891322
if !found {
12901323
http.NotFound(w, r)
@@ -1345,6 +1378,73 @@ func (s *Server) handleDynamoDBTable(w http.ResponseWriter, r *http.Request) {
13451378
})
13461379
}
13471380

1381+
type dashboardDynamoDBOperationRequest struct {
1382+
Input json.RawMessage `json:"input"`
1383+
Confirmation string `json:"confirmation"`
1384+
}
1385+
1386+
func (s *Server) forwardDynamoDBDashboardOperation(w http.ResponseWriter, r *http.Request, operation string, tableName string) {
1387+
s.forwardDynamoDBDashboardOperationWithConfirmation(w, r, operation, tableName, "")
1388+
}
1389+
1390+
func (s *Server) forwardDynamoDBDashboardOperationWithConfirmation(w http.ResponseWriter, r *http.Request, operation string, tableName string, requiredConfirmation string) {
1391+
if r.Method != http.MethodPost {
1392+
methodNotAllowed(w, "POST")
1393+
return
1394+
}
1395+
var request dashboardDynamoDBOperationRequest
1396+
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
1397+
http.Error(w, "invalid json request", http.StatusBadRequest)
1398+
return
1399+
}
1400+
if requiredConfirmation != "" && request.Confirmation != requiredConfirmation {
1401+
http.Error(w, "confirmation must match table name", http.StatusBadRequest)
1402+
return
1403+
}
1404+
input, err := normalizeDynamoDBDashboardInput(request.Input, tableName)
1405+
if err != nil {
1406+
http.Error(w, err.Error(), http.StatusBadRequest)
1407+
return
1408+
}
1409+
req := r.Clone(r.Context())
1410+
req.Method = http.MethodPost
1411+
req.URL = &url.URL{Path: "/"}
1412+
req.RequestURI = ""
1413+
req.Body = io.NopCloser(bytes.NewReader(input))
1414+
req.ContentLength = int64(len(input))
1415+
req.Header = make(http.Header)
1416+
req.Header.Set("Content-Type", "application/x-amz-json-1.0")
1417+
req.Header.Set("X-Amz-Target", "DynamoDB_20120810."+operation)
1418+
s.dynamo.ServeHTTP(w, req)
1419+
}
1420+
1421+
func normalizeDynamoDBDashboardInput(raw json.RawMessage, tableName string) ([]byte, error) {
1422+
if len(raw) == 0 {
1423+
return nil, errors.New("input is required")
1424+
}
1425+
var input map[string]any
1426+
if err := json.Unmarshal(raw, &input); err != nil {
1427+
return nil, errors.New("input must be valid JSON")
1428+
}
1429+
if input == nil {
1430+
return nil, errors.New("input must be a JSON object")
1431+
}
1432+
if tableName != "" {
1433+
if existing, ok := input["TableName"]; ok {
1434+
if existingName, ok := existing.(string); !ok || existingName != tableName {
1435+
return nil, errors.New("input TableName must match the selected table")
1436+
}
1437+
} else {
1438+
input["TableName"] = tableName
1439+
}
1440+
}
1441+
encoded, err := json.Marshal(input)
1442+
if err != nil {
1443+
return nil, errors.New("input could not be encoded")
1444+
}
1445+
return encoded, nil
1446+
}
1447+
13481448
func (s *Server) handleGCSUploadSessions(w http.ResponseWriter, r *http.Request) {
13491449
if r.Method != http.MethodGet {
13501450
methodNotAllowed(w, "GET")

0 commit comments

Comments
 (0)