Repository navigation
Commit 1c18596
feat(pod-file-browser): inline preview for size-bounded text files (#1711)
## Description
Adds a read-only inline viewer to the Pod filesystem browser (issue
#1581). A **Preview** icon on each regular-file row opens the file in
Monaco (read-only, language auto-detected). Downloads remain unchanged.
**Assigned by @nadaverell** after the [assumptions comment on
#1581](#1581 (comment));
the guidance in the [assign
comment](#1581 (comment))
to \"probably want curated error messages rather than dumping raw
error\" is why every failure state on this PR is a named error code, not
a leaked stderr line.
### Backend (`internal/server/copy.go`)
- New `GET /api/pods/{namespace}/{name}/file` endpoint, registered in
the 60s-timeout group (preview is capped at 1 MiB — never legitimately >
60s over a real cluster link).
- Reuses `openPodFileForPreview` — a preview twin of
`openPodFileForRequest` — so the tar/cat plumbing, drain guard, and
error classification are the battle-tested ones already in use for
downloads. The only structural change to the file: everything
preview-related is appended at the bottom.
- **Size gate fires before any body is buffered**: a file whose tar
header declares > 1 MiB is rejected with a 413 `file_too_large` code and
the real size on the wire, so the UI can say \"12.3 MiB, too large\"
instead of showing a truncated preview.
- **Layered content sniff** via new pure helper `classifyPreviewBytes`:
1. Empty → `empty_file` (success shape with an explicit empty state on
the UI).
2. `http.DetectContentType` accepts `text/*`, `application/json`,
`application/xml`, `application/x-yaml`.
3. Otherwise, a NUL-byte scan of the first 8 KiB catches
YAML/nginx-conf-shaped files that `DetectContentType` calls
`application/octet-stream`.
4. `utf8.Valid` — non-UTF-8 falls to `binary_file` with a distinct
message.
- **Structured error envelope**: every failure returns `{error, code,
size?, mimeType?}` — never a raw stderr string. Codes: `file_too_large`,
`binary_file`, `empty_file`, `not_a_regular_file`, `not_found`,
`permission_denied`, `no_shell`, `container_missing_tools`,
`read_failed`. Backed by a single `writePodFilePreviewError` helper so
the wire shape has one owner.
- **RBAC/capability**: same shape as the download handler —
`requireConnected` + apiserver-enforced RBAC on pod-exec via
`getClientForRequest`. No new authorization path, per issue.
### Frontend (`web/src/components/resources/PodFilePreviewModal.tsx`)
New portal-based modal, nested above the existing filesystem browser.
ESC on the preview returns to the filesystem list (nested portal,
stopPropagation).
- **Monaco** in read-only mode. Language auto-detected from filename
(`.yaml`/`.yml`/`.json`/`.xml`/`.md`/`.sh`/`.conf`/`.ini`/…); unknown
extensions fall to `plaintext`. Dark/light theme tracks the document
class the same way `YamlEditor` does.
- **Curated states per code** — the primary description never leaks the
raw server message:
| Code | Actions | Notes |
|----------------------------|--------------------|-----------------------------------------------------------------|
| `file_too_large` | Download | Names the real size (\"This file is 12.3
MiB…\") |
| `binary_file` | Download | Names the detected mime type |
| `empty_file` | — | Distinct \"is empty\" state — not an error |
| `not_a_regular_file` | — | \"directory, device, or pipe\" |
| `not_found` | Retry | \"may have been rotated or deleted since the
listing\" |
| `permission_denied` | — | Suggests a different container |
| `no_shell` | — | \"likely distroless or scratch-based\" |
| `container_missing_tools` | Download | Falls back to the raw-cat
download path |
| `network_error` | Retry + Details | Raw message hidden behind a
\"Technical details\" disclosure |
| `read_failed` (default) | Download + Retry | Raw message hidden behind
Technical details |
- **Wired into `PodFilesystemModal`** as a new `Eye` icon on each
regular-file row — symlinks stay download-only for parity with existing
behavior.
## Tests
- `internal/server/preview_test.go`:
- 6 pure-classifier tests (`classifyPreviewBytes`) covering text, JSON,
YAML-via-NUL fallback, NUL binary, non-UTF-8, empty.
- 7 handler-level tests through the real router and the existing
`newExecutor` fake seam: happy path, oversized (asserts size on the
wire), binary, empty (success shape), permission denied, missing file,
bad path.
- `web/src/components/resources/PodFilePreviewModal.test.tsx`:
- Locks the action affordances per code (Download vs Retry).
- Iterates every code and asserts the raw stderr never appears in the
primary title/description.
- `detectLanguage` unit table.
```
$ CGO_ENABLED=0 go test ./internal/server/...
ok github.com/skyhook-io/radar/internal/server 14.1s
$ cd web && npx vitest run src/components/resources/PodFilePreviewModal.test.tsx
Test Files 1 passed (1) Tests 26 passed (26)
$ make tsc
green
```
Full `web` vitest sweep: 84 files / 932 tests pass. 3 files failed (all
`e2e/*.spec.ts` Playwright specs that need a browser + one flaky
`authRedirect` timing test) — verified against upstream HEAD; unrelated
to this change.
## Type of change
- [x] New feature (non-breaking change that adds functionality)
## How has this been tested?
- [x] Added/updated unit tests (backend and frontend)
- [x] Tested locally with kind — user smoke on a real cluster covering:
preview of a YAML config (renders inline), preview of a binary
(`/usr/bin/…` → binary_file curated state), preview of an empty file,
permission-denied path, and download from within each error state.
## Checklist
- [x] My code follows the project's coding standards
- [x] I have performed a self-review of my code
- [x] I have added comments where necessary
- [x] My changes generate no new warnings
## Related issues
Fixes #1581
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> New pod-exec read path buffers up to 1 MiB per preview request, but
authorization matches existing downloads and failures are bounded by
explicit size and content gates.
>
> **Overview**
> Adds a **read-only inline file preview** to the pod filesystem
browser: clicking a regular file (under **1 MiB**) replaces the listing
with a Monaco viewer, with **back navigation**, **ESC** stepping out of
preview first, and a shared **download** path from the preview header
and tree rows.
>
> The backend exposes **`GET /api/pods/{namespace}/{name}/file`**
(alongside the existing file list routes), reusing the same tar/cat
pod-exec plumbing as downloads but returning JSON. Preview **rejects
files over 1 MiB before reading the body**, classifies content as text
vs binary (sniff + UTF-8), and returns **stable error codes**
(`file_too_large`, `binary_file`, `permission_denied`, etc.) so the UI
can show curated fallows instead of raw stderr.
>
> The frontend **`PodFilePreview`** maps those codes to operator-facing
copy with Download/Retry affordances; oversized files are **blocked in
the listing** with a tooltip. **`ensureMonacoRuntime`** and extra Monaco
basic-language tokenizers support syntax highlighting by filename (JSON
highlighted via YAML tokenizer).
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
84bca19. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Nadav Erell <nadaverell@gmail.com>1 parent 3bf7b43 commit 1c18596
9 files changed
Lines changed: 1355 additions & 67 deletions
File tree
- internal/server
- packages/k8s-ui/src
- components/ui
- web/src/components/resources
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| 22 | + | |
21 | 23 | | |
22 | 24 | | |
23 | 25 | | |
| |||
1000 | 1002 | | |
1001 | 1003 | | |
1002 | 1004 | | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
| 1014 | + | |
| 1015 | + | |
| 1016 | + | |
| 1017 | + | |
| 1018 | + | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + | |
| 1023 | + | |
| 1024 | + | |
| 1025 | + | |
| 1026 | + | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
| 1030 | + | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
| 1037 | + | |
| 1038 | + | |
| 1039 | + | |
| 1040 | + | |
| 1041 | + | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
| 1052 | + | |
| 1053 | + | |
| 1054 | + | |
| 1055 | + | |
| 1056 | + | |
| 1057 | + | |
| 1058 | + | |
| 1059 | + | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
| 1073 | + | |
| 1074 | + | |
| 1075 | + | |
| 1076 | + | |
| 1077 | + | |
| 1078 | + | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
| 1101 | + | |
| 1102 | + | |
| 1103 | + | |
| 1104 | + | |
| 1105 | + | |
| 1106 | + | |
| 1107 | + | |
| 1108 | + | |
| 1109 | + | |
| 1110 | + | |
| 1111 | + | |
| 1112 | + | |
| 1113 | + | |
| 1114 | + | |
| 1115 | + | |
| 1116 | + | |
| 1117 | + | |
| 1118 | + | |
| 1119 | + | |
| 1120 | + | |
| 1121 | + | |
| 1122 | + | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
| 1126 | + | |
| 1127 | + | |
| 1128 | + | |
| 1129 | + | |
| 1130 | + | |
| 1131 | + | |
| 1132 | + | |
| 1133 | + | |
| 1134 | + | |
| 1135 | + | |
| 1136 | + | |
| 1137 | + | |
| 1138 | + | |
| 1139 | + | |
| 1140 | + | |
| 1141 | + | |
| 1142 | + | |
| 1143 | + | |
| 1144 | + | |
| 1145 | + | |
| 1146 | + | |
| 1147 | + | |
| 1148 | + | |
| 1149 | + | |
| 1150 | + | |
| 1151 | + | |
| 1152 | + | |
| 1153 | + | |
| 1154 | + | |
| 1155 | + | |
| 1156 | + | |
| 1157 | + | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
| 1162 | + | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
| 1175 | + | |
| 1176 | + | |
| 1177 | + | |
| 1178 | + | |
| 1179 | + | |
| 1180 | + | |
| 1181 | + | |
| 1182 | + | |
| 1183 | + | |
| 1184 | + | |
| 1185 | + | |
| 1186 | + | |
| 1187 | + | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
| 1191 | + | |
| 1192 | + | |
| 1193 | + | |
| 1194 | + | |
| 1195 | + | |
| 1196 | + | |
| 1197 | + | |
| 1198 | + | |
| 1199 | + | |
| 1200 | + | |
| 1201 | + | |
| 1202 | + | |
| 1203 | + | |
| 1204 | + | |
| 1205 | + | |
| 1206 | + | |
| 1207 | + | |
| 1208 | + | |
| 1209 | + | |
| 1210 | + | |
| 1211 | + | |
| 1212 | + | |
| 1213 | + | |
| 1214 | + | |
| 1215 | + | |
| 1216 | + | |
| 1217 | + | |
| 1218 | + | |
| 1219 | + | |
| 1220 | + | |
| 1221 | + | |
| 1222 | + | |
| 1223 | + | |
| 1224 | + | |
| 1225 | + | |
| 1226 | + | |
| 1227 | + | |
| 1228 | + | |
| 1229 | + | |
| 1230 | + | |
| 1231 | + | |
| 1232 | + | |
| 1233 | + | |
| 1234 | + | |
| 1235 | + | |
| 1236 | + | |
| 1237 | + | |
| 1238 | + | |
| 1239 | + | |
| 1240 | + | |
| 1241 | + | |
| 1242 | + | |
| 1243 | + | |
| 1244 | + | |
| 1245 | + | |
| 1246 | + | |
| 1247 | + | |
| 1248 | + | |
| 1249 | + | |
| 1250 | + | |
| 1251 | + | |
| 1252 | + | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
| 1275 | + | |
| 1276 | + | |
| 1277 | + | |
| 1278 | + | |
| 1279 | + | |
| 1280 | + | |
| 1281 | + | |
| 1282 | + | |
| 1283 | + | |
| 1284 | + | |
| 1285 | + | |
| 1286 | + | |
| 1287 | + | |
| 1288 | + | |
| 1289 | + | |
| 1290 | + | |
| 1291 | + | |
| 1292 | + | |
| 1293 | + | |
| 1294 | + | |
| 1295 | + | |
| 1296 | + | |
0 commit comments