Skip to content

Commit 3a1c149

Browse files
emily-shenNuroDev
andauthored
feat(local-explorer-ui): Add R2 support (#12888)
Co-authored-by: Ben Dixon <ben@nuro.dev> Co-authored-by: Ben <4991309+NuroDev@users.noreply.github.com>
1 parent 4f7fd79 commit 3a1c149

File tree

34 files changed

+4675
-104
lines changed

34 files changed

+4675
-104
lines changed

.changeset/sour-sides-enjoy.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@cloudflare/local-explorer-ui": minor
3+
"miniflare": minor
4+
---
5+
6+
Add R2 support to the local explorer.
7+
8+
The local explorer now supports the following:
9+
10+
- Viewing, modifying & deleting objects
11+
- Uploading files
12+
- Creating directories / prefixes
13+
14+
Note: The local explorer is an experimental WIP feature that is now enabled by default. This can still be opt-ed out of by using `X_LOCAL_EXPLORER=false` to disable it.

fixtures/worker-with-resources/src/index.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ export default {
2626
return new Response(`Seeded ${SEED_DATA.length} KV entries`);
2727
}
2828

29+
// R2 routes
30+
case "/r2/seed": {
31+
await Promise.all(
32+
R2_SEED_DATA.map(({ key, content, contentType, customMetadata }) =>
33+
env.BUCKET.put(key, content, {
34+
httpMetadata: { contentType },
35+
customMetadata,
36+
})
37+
)
38+
);
39+
return new Response(`Seeded ${R2_SEED_DATA.length} R2 objects`);
40+
}
41+
2942
// D1 database route
3043
case "/d1": {
3144
await env.DB.exec(`
@@ -202,3 +215,134 @@ const SEED_DATA: [string, string][] = [
202215
["number-float", "3.14159"],
203216
["number-negative", "-273.15"],
204217
];
218+
219+
interface R2SeedItem {
220+
key: string;
221+
content: string | ArrayBuffer;
222+
contentType: string;
223+
customMetadata?: Record<string, string>;
224+
}
225+
226+
const R2_SEED_DATA: R2SeedItem[] = [
227+
// Root level files
228+
{
229+
key: "readme.txt",
230+
content: "Welcome to the R2 bucket! This is a sample readme file.",
231+
contentType: "text/plain",
232+
},
233+
{
234+
key: "config.json",
235+
content: JSON.stringify(
236+
{ version: "1.0.0", environment: "development" },
237+
null,
238+
2
239+
),
240+
contentType: "application/json",
241+
customMetadata: { author: "admin", created: "2025-01-15" },
242+
},
243+
244+
// Images folder
245+
{
246+
key: "images/logo.svg",
247+
content:
248+
'<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><circle cx="50" cy="50" r="40" fill="orange"/></svg>',
249+
contentType: "image/svg+xml",
250+
},
251+
{
252+
key: "images/banner.svg",
253+
content:
254+
'<svg xmlns="http://www.w3.org/2000/svg" width="300" height="100"><rect width="300" height="100" fill="blue"/></svg>',
255+
contentType: "image/svg+xml",
256+
},
257+
{
258+
key: "images/icons/home.svg",
259+
content:
260+
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3L2 12h3v9h6v-6h2v6h6v-9h3L12 3z"/></svg>',
261+
contentType: "image/svg+xml",
262+
},
263+
{
264+
key: "images/icons/settings.svg",
265+
content:
266+
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><circle cx="12" cy="12" r="3"/></svg>',
267+
contentType: "image/svg+xml",
268+
},
269+
270+
// Documents folder
271+
{
272+
key: "documents/report.txt",
273+
content:
274+
"Annual Report 2024\n\nThis is a sample annual report with important business metrics.",
275+
contentType: "text/plain",
276+
customMetadata: { department: "finance", year: "2024" },
277+
},
278+
{
279+
key: "documents/notes.md",
280+
content:
281+
"# Meeting Notes\n\n## Action Items\n- Review budget\n- Update roadmap\n- Schedule follow-up",
282+
contentType: "text/markdown",
283+
},
284+
{
285+
key: "documents/data.csv",
286+
content: "id,name,value\n1,Alpha,100\n2,Beta,200\n3,Gamma,300",
287+
contentType: "text/csv",
288+
},
289+
290+
// Data folder with nested structure
291+
{
292+
key: "data/users.json",
293+
content: JSON.stringify(
294+
[
295+
{ id: 1, name: "Alice", role: "admin" },
296+
{ id: 2, name: "Bob", role: "user" },
297+
],
298+
null,
299+
2
300+
),
301+
contentType: "application/json",
302+
},
303+
{
304+
key: "data/backup/2024/january.json",
305+
content: JSON.stringify({ month: "January", records: 150 }),
306+
contentType: "application/json",
307+
customMetadata: { backup: "true", period: "2024-01" },
308+
},
309+
{
310+
key: "data/backup/2024/february.json",
311+
content: JSON.stringify({ month: "February", records: 175 }),
312+
contentType: "application/json",
313+
customMetadata: { backup: "true", period: "2024-02" },
314+
},
315+
316+
// Logs folder
317+
{
318+
key: "logs/access.log",
319+
content:
320+
"2025-01-15 10:00:00 GET /api/users 200\n2025-01-15 10:01:00 POST /api/login 200\n2025-01-15 10:02:00 GET /api/products 200",
321+
contentType: "text/plain",
322+
},
323+
{
324+
key: "logs/error.log",
325+
content:
326+
"2025-01-15 09:30:00 ERROR Connection timeout\n2025-01-15 09:45:00 ERROR Invalid token",
327+
contentType: "text/plain",
328+
},
329+
330+
// Assets with various content types
331+
{
332+
key: "assets/styles.css",
333+
content:
334+
"body { font-family: sans-serif; }\n.container { max-width: 1200px; margin: 0 auto; }",
335+
contentType: "text/css",
336+
},
337+
{
338+
key: "assets/script.js",
339+
content: 'console.log("Hello from R2!");\nfunction init() { return true; }',
340+
contentType: "application/javascript",
341+
},
342+
{
343+
key: "assets/template.html",
344+
content:
345+
"<!DOCTYPE html>\n<html><head><title>Template</title></head><body><h1>Hello</h1></body></html>",
346+
contentType: "text/html",
347+
},
348+
];

fixtures/worker-with-resources/worker-b/wrangler.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
],
2525
},
26+
"r2_buckets": [{ "bucket_name": "bucket-b", "binding": "BUCKET_B" }],
2627
"migrations": [
2728
{
2829
"new_sqlite_classes": ["WorkerBDurableObject"],

fixtures/worker-with-resources/worker-configuration.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable */
2-
// Generated by Wrangler by running `wrangler types --no-include-runtime` (hash: 1af53220e7fe93ac70789822977c3be3)
2+
// Generated by Wrangler by running `wrangler types --no-include-runtime` (hash: 9b4b1032686eb7f16ed05d3e59c53f88)
33
declare namespace Cloudflare {
44
interface GlobalProps {
55
mainModule: typeof import("./src/index");
@@ -8,6 +8,7 @@ declare namespace Cloudflare {
88
interface Env {
99
KV: KVNamespace;
1010
KV_WITH_ID: KVNamespace;
11+
BUCKET: R2Bucket;
1112
DB: D1Database;
1213
BACKUP_DB: D1Database;
1314
DO: DurableObjectNamespace<import("./src/index").MyDurableObject>;

fixtures/worker-with-resources/wrangler.jsonc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
"name": "worker-w-resources",
44
"main": "src/index.ts",
55
"compatibility_date": "2023-05-04",
6+
"r2_buckets": [
7+
{
8+
"bucket_name": "my-bucket",
9+
"binding": "BUCKET",
10+
},
11+
],
612
"kv_namespaces": [
713
{
814
"binding": "KV",

packages/local-explorer-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@tailwindcss/vite": "^4.0.15",
3737
"@tanstack/react-router": "^1.158.0",
3838
"immer": "^11.1.4",
39+
"pretty-bytes": "^7.1.0",
3940
"react": "^19.2.0",
4041
"react-dom": "^19.2.0",
4142
"tailwindcss": "^4.0.15"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, test } from "vitest";
2+
import { formatDate } from "../../utils/format";
3+
4+
describe("formatDate", () => {
5+
test("`undefined` returns '-'", ({ expect }) => {
6+
expect(formatDate(undefined)).toBe("-");
7+
});
8+
9+
test("empty string returns '-'", ({ expect }) => {
10+
expect(formatDate("")).toBe("-");
11+
});
12+
13+
test("valid ISO date string", ({ expect }) => {
14+
expect(formatDate("2025-05-13T01:11:37.000Z")).toBe(
15+
"13 May 2025, 01:11:37 UTC"
16+
);
17+
});
18+
19+
test("different month (January)", ({ expect }) => {
20+
expect(formatDate("2024-01-05T12:30:45.000Z")).toBe(
21+
"5 Jan 2024, 12:30:45 UTC"
22+
);
23+
});
24+
25+
test("single-digit day (no leading zero)", ({ expect }) => {
26+
expect(formatDate("2025-03-01T00:00:00.000Z")).toBe(
27+
"1 Mar 2025, 00:00:00 UTC"
28+
);
29+
});
30+
31+
test("end of year date", ({ expect }) => {
32+
expect(formatDate("2025-12-31T23:59:59.000Z")).toBe(
33+
"31 Dec 2025, 23:59:59 UTC"
34+
);
35+
});
36+
37+
test("invalid date string returns '-'", ({ expect }) => {
38+
expect(formatDate("not-a-date")).toBe("-");
39+
});
40+
41+
test("malformed date string returns '-'", ({ expect }) => {
42+
expect(formatDate("2025-13-45")).toBe("-");
43+
});
44+
45+
test("random string returns '-'", ({ expect }) => {
46+
expect(formatDate("hello world")).toBe("-");
47+
});
48+
});
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)