@mano8/astro-media-m8 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +203 -0
- package/dist/src/integration.d.ts +57 -0
- package/dist/src/integration.d.ts.map +1 -0
- package/dist/src/integration.js +87 -0
- package/dist/src/integration.js.map +1 -0
- package/dist/src/lib/csp.d.ts +27 -0
- package/dist/src/lib/csp.d.ts.map +1 -0
- package/dist/src/lib/csp.js +54 -0
- package/dist/src/lib/csp.js.map +1 -0
- package/dist/src/middleware.d.ts +4 -0
- package/dist/src/middleware.d.ts.map +1 -0
- package/dist/src/middleware.js +24 -0
- package/dist/src/middleware.js.map +1 -0
- package/dist/src/runtime/api/admin.d.ts +22 -0
- package/dist/src/runtime/api/admin.d.ts.map +1 -0
- package/dist/src/runtime/api/admin.js +115 -0
- package/dist/src/runtime/api/admin.js.map +1 -0
- package/dist/src/runtime/api/categories.d.ts +12 -0
- package/dist/src/runtime/api/categories.d.ts.map +1 -0
- package/dist/src/runtime/api/categories.js +56 -0
- package/dist/src/runtime/api/categories.js.map +1 -0
- package/dist/src/runtime/api/dashboard.d.ts +5 -0
- package/dist/src/runtime/api/dashboard.d.ts.map +1 -0
- package/dist/src/runtime/api/dashboard.js +22 -0
- package/dist/src/runtime/api/dashboard.js.map +1 -0
- package/dist/src/runtime/api/index.d.ts +72 -0
- package/dist/src/runtime/api/index.d.ts.map +1 -0
- package/dist/src/runtime/api/index.js +74 -0
- package/dist/src/runtime/api/index.js.map +1 -0
- package/dist/src/runtime/api/internal.server.d.ts +19 -0
- package/dist/src/runtime/api/internal.server.d.ts.map +1 -0
- package/dist/src/runtime/api/internal.server.js +64 -0
- package/dist/src/runtime/api/internal.server.js.map +1 -0
- package/dist/src/runtime/api/objects.d.ts +7 -0
- package/dist/src/runtime/api/objects.d.ts.map +1 -0
- package/dist/src/runtime/api/objects.js +71 -0
- package/dist/src/runtime/api/objects.js.map +1 -0
- package/dist/src/runtime/api/presets.d.ts +6 -0
- package/dist/src/runtime/api/presets.d.ts.map +1 -0
- package/dist/src/runtime/api/presets.js +31 -0
- package/dist/src/runtime/api/presets.js.map +1 -0
- package/dist/src/runtime/api/shares.d.ts +10 -0
- package/dist/src/runtime/api/shares.d.ts.map +1 -0
- package/dist/src/runtime/api/shares.js +38 -0
- package/dist/src/runtime/api/shares.js.map +1 -0
- package/dist/src/runtime/api/uploads.d.ts +5 -0
- package/dist/src/runtime/api/uploads.d.ts.map +1 -0
- package/dist/src/runtime/api/uploads.js +28 -0
- package/dist/src/runtime/api/uploads.js.map +1 -0
- package/dist/src/runtime/api/variants.d.ts +18 -0
- package/dist/src/runtime/api/variants.d.ts.map +1 -0
- package/dist/src/runtime/api/variants.js +61 -0
- package/dist/src/runtime/api/variants.js.map +1 -0
- package/dist/src/runtime/authAdapter.d.ts +48 -0
- package/dist/src/runtime/authAdapter.d.ts.map +1 -0
- package/dist/src/runtime/authAdapter.js +62 -0
- package/dist/src/runtime/authAdapter.js.map +1 -0
- package/dist/src/runtime/client.d.ts +24 -0
- package/dist/src/runtime/client.d.ts.map +1 -0
- package/dist/src/runtime/client.js +87 -0
- package/dist/src/runtime/client.js.map +1 -0
- package/dist/src/runtime/compatibility.d.ts +32 -0
- package/dist/src/runtime/compatibility.d.ts.map +1 -0
- package/dist/src/runtime/compatibility.js +101 -0
- package/dist/src/runtime/compatibility.js.map +1 -0
- package/dist/src/runtime/config.d.ts +27 -0
- package/dist/src/runtime/config.d.ts.map +1 -0
- package/dist/src/runtime/config.js +32 -0
- package/dist/src/runtime/config.js.map +1 -0
- package/dist/src/runtime/errors.d.ts +23 -0
- package/dist/src/runtime/errors.d.ts.map +1 -0
- package/dist/src/runtime/errors.js +50 -0
- package/dist/src/runtime/errors.js.map +1 -0
- package/dist/src/runtime/hooks/index.d.ts +8 -0
- package/dist/src/runtime/hooks/index.d.ts.map +1 -0
- package/dist/src/runtime/hooks/index.js +8 -0
- package/dist/src/runtime/hooks/index.js.map +1 -0
- package/dist/src/runtime/hooks/useDownloadUrl.d.ts +12 -0
- package/dist/src/runtime/hooks/useDownloadUrl.d.ts.map +1 -0
- package/dist/src/runtime/hooks/useDownloadUrl.js +40 -0
- package/dist/src/runtime/hooks/useDownloadUrl.js.map +1 -0
- package/dist/src/runtime/hooks/useMediaAdmin.d.ts +30 -0
- package/dist/src/runtime/hooks/useMediaAdmin.d.ts.map +1 -0
- package/dist/src/runtime/hooks/useMediaAdmin.js +145 -0
- package/dist/src/runtime/hooks/useMediaAdmin.js.map +1 -0
- package/dist/src/runtime/hooks/useMediaObject.d.ts +17 -0
- package/dist/src/runtime/hooks/useMediaObject.d.ts.map +1 -0
- package/dist/src/runtime/hooks/useMediaObject.js +62 -0
- package/dist/src/runtime/hooks/useMediaObject.js.map +1 -0
- package/dist/src/runtime/hooks/useMediaObjects.d.ts +12 -0
- package/dist/src/runtime/hooks/useMediaObjects.d.ts.map +1 -0
- package/dist/src/runtime/hooks/useMediaObjects.js +76 -0
- package/dist/src/runtime/hooks/useMediaObjects.js.map +1 -0
- package/dist/src/runtime/hooks/useMediaPresets.d.ts +24 -0
- package/dist/src/runtime/hooks/useMediaPresets.d.ts.map +1 -0
- package/dist/src/runtime/hooks/useMediaPresets.js +55 -0
- package/dist/src/runtime/hooks/useMediaPresets.js.map +1 -0
- package/dist/src/runtime/hooks/useMediaUpload.d.ts +11 -0
- package/dist/src/runtime/hooks/useMediaUpload.d.ts.map +1 -0
- package/dist/src/runtime/hooks/useMediaUpload.js +34 -0
- package/dist/src/runtime/hooks/useMediaUpload.js.map +1 -0
- package/dist/src/runtime/hooks/useMediaVariants.d.ts +18 -0
- package/dist/src/runtime/hooks/useMediaVariants.d.ts.map +1 -0
- package/dist/src/runtime/hooks/useMediaVariants.js +78 -0
- package/dist/src/runtime/hooks/useMediaVariants.js.map +1 -0
- package/dist/src/runtime/listParams.d.ts +27 -0
- package/dist/src/runtime/listParams.d.ts.map +1 -0
- package/dist/src/runtime/listParams.js +66 -0
- package/dist/src/runtime/listParams.js.map +1 -0
- package/dist/src/runtime/queryKeys.d.ts +18 -0
- package/dist/src/runtime/queryKeys.d.ts.map +1 -0
- package/dist/src/runtime/queryKeys.js +23 -0
- package/dist/src/runtime/queryKeys.js.map +1 -0
- package/dist/src/runtime/react/AdminMediaPanel.d.ts +2 -0
- package/dist/src/runtime/react/AdminMediaPanel.d.ts.map +1 -0
- package/dist/src/runtime/react/AdminMediaPanel.js +14 -0
- package/dist/src/runtime/react/AdminMediaPanel.js.map +1 -0
- package/dist/src/runtime/react/MediaLibrary.d.ts +6 -0
- package/dist/src/runtime/react/MediaLibrary.d.ts.map +1 -0
- package/dist/src/runtime/react/MediaLibrary.js +138 -0
- package/dist/src/runtime/react/MediaLibrary.js.map +1 -0
- package/dist/src/runtime/react/MediaProvider.d.ts +18 -0
- package/dist/src/runtime/react/MediaProvider.d.ts.map +1 -0
- package/dist/src/runtime/react/MediaProvider.js +79 -0
- package/dist/src/runtime/react/MediaProvider.js.map +1 -0
- package/dist/src/runtime/react/MediaQueryProvider.d.ts +8 -0
- package/dist/src/runtime/react/MediaQueryProvider.d.ts.map +1 -0
- package/dist/src/runtime/react/MediaQueryProvider.js +11 -0
- package/dist/src/runtime/react/MediaQueryProvider.js.map +1 -0
- package/dist/src/runtime/react/MediaUploadDropzone.d.ts +8 -0
- package/dist/src/runtime/react/MediaUploadDropzone.d.ts.map +1 -0
- package/dist/src/runtime/react/MediaUploadDropzone.js +34 -0
- package/dist/src/runtime/react/MediaUploadDropzone.js.map +1 -0
- package/dist/src/runtime/react/ObjectDetail.d.ts +5 -0
- package/dist/src/runtime/react/ObjectDetail.d.ts.map +1 -0
- package/dist/src/runtime/react/ObjectDetail.js +34 -0
- package/dist/src/runtime/react/ObjectDetail.js.map +1 -0
- package/dist/src/runtime/react/PresetEditor.d.ts +3 -0
- package/dist/src/runtime/react/PresetEditor.d.ts.map +1 -0
- package/dist/src/runtime/react/PresetEditor.js +36 -0
- package/dist/src/runtime/react/PresetEditor.js.map +1 -0
- package/dist/src/runtime/react/RequireSuperuser.d.ts +7 -0
- package/dist/src/runtime/react/RequireSuperuser.d.ts.map +1 -0
- package/dist/src/runtime/react/RequireSuperuser.js +10 -0
- package/dist/src/runtime/react/RequireSuperuser.js.map +1 -0
- package/dist/src/runtime/react/VariantPicker.d.ts +5 -0
- package/dist/src/runtime/react/VariantPicker.d.ts.map +1 -0
- package/dist/src/runtime/react/VariantPicker.js +28 -0
- package/dist/src/runtime/react/VariantPicker.js.map +1 -0
- package/dist/src/runtime/react/default-ui/index.d.ts +25 -0
- package/dist/src/runtime/react/default-ui/index.d.ts.map +1 -0
- package/dist/src/runtime/react/default-ui/index.js +33 -0
- package/dist/src/runtime/react/default-ui/index.js.map +1 -0
- package/dist/src/runtime/react/index.d.ts +10 -0
- package/dist/src/runtime/react/index.d.ts.map +1 -0
- package/dist/src/runtime/react/index.js +10 -0
- package/dist/src/runtime/react/index.js.map +1 -0
- package/dist/src/runtime/routes.d.ts +19 -0
- package/dist/src/runtime/routes.d.ts.map +1 -0
- package/dist/src/runtime/routes.js +35 -0
- package/dist/src/runtime/routes.js.map +1 -0
- package/dist/src/runtime/schemas.d.ts +812 -0
- package/dist/src/runtime/schemas.d.ts.map +1 -0
- package/dist/src/runtime/schemas.js +427 -0
- package/dist/src/runtime/schemas.js.map +1 -0
- package/dist/src/runtime/upload/index.d.ts +2 -0
- package/dist/src/runtime/upload/index.d.ts.map +1 -0
- package/dist/src/runtime/upload/index.js +2 -0
- package/dist/src/runtime/upload/index.js.map +1 -0
- package/dist/src/runtime/upload/uploadController.d.ts +71 -0
- package/dist/src/runtime/upload/uploadController.d.ts.map +1 -0
- package/dist/src/runtime/upload/uploadController.js +217 -0
- package/dist/src/runtime/upload/uploadController.js.map +1 -0
- package/package.json +97 -0
- package/registry/r/admin-media-dashboard.json +23 -0
- package/registry/r/data-table.json +23 -0
- package/registry/r/media-dashboard-overview.json +26 -0
- package/registry/r/media-maintenance-panel.json +23 -0
- package/registry/r/media-storage-chart.json +21 -0
- package/registry/r/registry.json +37 -0
- package/registry.json +92 -0
- package/src/routes/admin/media.astro +12 -0
- package/src/routes/library.astro +12 -0
- package/src/routes/object/[id].astro +14 -0
- package/src/routes/presets.astro +12 -0
- package/src/routes/upload.astro +12 -0
- package/src/scaffold/styles/media.css +208 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { abortUpload, completeUpload, initiateUpload } from "../api/uploads.js";
|
|
2
|
+
import { getObject } from "../api/objects.js";
|
|
3
|
+
import { getMediaConfig } from "../config.js";
|
|
4
|
+
import { ApiError } from "../errors.js";
|
|
5
|
+
export class UploadError extends Error {
|
|
6
|
+
kind;
|
|
7
|
+
cause;
|
|
8
|
+
constructor(kind, message, cause) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "UploadError";
|
|
11
|
+
this.kind = kind;
|
|
12
|
+
this.cause = cause;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const PENDING_SCAN_STATES = new Set(["pending"]);
|
|
16
|
+
const PENDING_OBJECT_STATES = new Set(["pending_upload", "uploaded", "processing"]);
|
|
17
|
+
/**
|
|
18
|
+
* Compute a lowercase hex SHA-256 of a blob using Web Crypto. Returns `null`
|
|
19
|
+
* when the platform has no `crypto.subtle` (e.g. non-secure context).
|
|
20
|
+
*/
|
|
21
|
+
export async function sha256Hex(blob) {
|
|
22
|
+
const subtle = globalThis.crypto?.subtle;
|
|
23
|
+
if (!subtle)
|
|
24
|
+
return null;
|
|
25
|
+
const digest = await subtle.digest("SHA-256", await blob.arrayBuffer());
|
|
26
|
+
return Array.from(new Uint8Array(digest))
|
|
27
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
28
|
+
.join("");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Upload a blob to MinIO/S3 via a presigned POST policy. The bearer token is
|
|
32
|
+
* never attached here — storage authenticates the signed `upload_fields` only.
|
|
33
|
+
* Uses `XMLHttpRequest` for progress events, falling back to `fetch` when XHR
|
|
34
|
+
* is unavailable (e.g. SSR).
|
|
35
|
+
*/
|
|
36
|
+
export function putToStorage(presigned, file, options = {}) {
|
|
37
|
+
const form = new FormData();
|
|
38
|
+
for (const [key, value] of Object.entries(presigned.upload_fields)) {
|
|
39
|
+
form.append(key, value);
|
|
40
|
+
}
|
|
41
|
+
form.append("file", file);
|
|
42
|
+
const Xhr = globalThis.XMLHttpRequest;
|
|
43
|
+
if (!Xhr) {
|
|
44
|
+
return fetch(presigned.upload_url, { method: "POST", body: form }).then((response) => {
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new UploadError("storage", `Storage upload failed with ${response.status}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const xhr = new Xhr();
|
|
52
|
+
xhr.open("POST", presigned.upload_url);
|
|
53
|
+
xhr.upload.onprogress = (event) => {
|
|
54
|
+
if (event.lengthComputable)
|
|
55
|
+
options.onProgress?.(event.loaded, event.total);
|
|
56
|
+
};
|
|
57
|
+
xhr.onload = () => {
|
|
58
|
+
if (xhr.status >= 200 && xhr.status < 300)
|
|
59
|
+
resolve();
|
|
60
|
+
else
|
|
61
|
+
reject(new UploadError("storage", `Storage upload failed with ${xhr.status}`));
|
|
62
|
+
};
|
|
63
|
+
xhr.onerror = () => reject(new UploadError("storage", "Storage upload network error"));
|
|
64
|
+
xhr.onabort = () => reject(new UploadError("abort", "Upload aborted"));
|
|
65
|
+
if (options.signal) {
|
|
66
|
+
if (options.signal.aborted) {
|
|
67
|
+
xhr.abort();
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
options.signal.addEventListener("abort", () => xhr.abort(), { once: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
xhr.send(form);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export class MediaUploadController {
|
|
77
|
+
input;
|
|
78
|
+
listeners = new Set();
|
|
79
|
+
controller = new AbortController();
|
|
80
|
+
loaded = 0;
|
|
81
|
+
total;
|
|
82
|
+
currentState = "idle";
|
|
83
|
+
object;
|
|
84
|
+
sessionId;
|
|
85
|
+
constructor(input) {
|
|
86
|
+
this.input = input;
|
|
87
|
+
this.total = input.file.size;
|
|
88
|
+
}
|
|
89
|
+
get state() {
|
|
90
|
+
return this.currentState;
|
|
91
|
+
}
|
|
92
|
+
on(_event, listener) {
|
|
93
|
+
this.listeners.add(listener);
|
|
94
|
+
return () => this.listeners.delete(listener);
|
|
95
|
+
}
|
|
96
|
+
abort() {
|
|
97
|
+
this.controller.abort();
|
|
98
|
+
}
|
|
99
|
+
emit(state) {
|
|
100
|
+
this.currentState = state;
|
|
101
|
+
const progress = {
|
|
102
|
+
state,
|
|
103
|
+
loaded: this.loaded,
|
|
104
|
+
total: this.total,
|
|
105
|
+
fraction: this.total > 0 ? Math.min(1, this.loaded / this.total) : null,
|
|
106
|
+
object: this.object
|
|
107
|
+
};
|
|
108
|
+
for (const listener of this.listeners)
|
|
109
|
+
listener(progress);
|
|
110
|
+
}
|
|
111
|
+
async start() {
|
|
112
|
+
const { input } = this;
|
|
113
|
+
const filename = input.filename ?? input.file.name;
|
|
114
|
+
const mimeType = input.mimeType ?? input.file.type;
|
|
115
|
+
if (!filename)
|
|
116
|
+
throw new UploadError("api", "A filename is required to upload");
|
|
117
|
+
if (!mimeType)
|
|
118
|
+
throw new UploadError("api", "A MIME type is required to upload");
|
|
119
|
+
let presigned;
|
|
120
|
+
try {
|
|
121
|
+
this.emit("initiating");
|
|
122
|
+
presigned = await initiateUpload({
|
|
123
|
+
category: input.category,
|
|
124
|
+
visibility: input.visibility,
|
|
125
|
+
original_filename: filename,
|
|
126
|
+
mime_type: mimeType,
|
|
127
|
+
expected_size_bytes: input.file.size
|
|
128
|
+
});
|
|
129
|
+
this.sessionId = presigned.session_id;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
this.emit("failed");
|
|
133
|
+
throw new UploadError("api", "Failed to initiate upload", error);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
this.emit("uploading");
|
|
137
|
+
await putToStorage(presigned, input.file, {
|
|
138
|
+
signal: this.controller.signal,
|
|
139
|
+
onProgress: (loaded, total) => {
|
|
140
|
+
this.loaded = loaded;
|
|
141
|
+
this.total = total;
|
|
142
|
+
this.emit("uploading");
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
await this.safeAbort();
|
|
148
|
+
if (error instanceof UploadError && error.kind === "abort") {
|
|
149
|
+
this.emit("aborted");
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
this.emit("failed");
|
|
153
|
+
throw error instanceof UploadError
|
|
154
|
+
? error
|
|
155
|
+
: new UploadError("storage", "Storage upload failed", error);
|
|
156
|
+
}
|
|
157
|
+
let sha256 = null;
|
|
158
|
+
if (input.checksum === "sha256") {
|
|
159
|
+
sha256 = await sha256Hex(input.file);
|
|
160
|
+
}
|
|
161
|
+
let object;
|
|
162
|
+
try {
|
|
163
|
+
this.emit("completing");
|
|
164
|
+
const response = await completeUpload(presigned.session_id, sha256 ? { sha256 } : {});
|
|
165
|
+
object = response.media_object;
|
|
166
|
+
this.object = object;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
this.emit("failed");
|
|
170
|
+
throw new UploadError("api", "Failed to complete upload", error);
|
|
171
|
+
}
|
|
172
|
+
if (input.waitForScan) {
|
|
173
|
+
this.emit("scanning");
|
|
174
|
+
object = await this.pollUntilScanned(object.id);
|
|
175
|
+
this.object = object;
|
|
176
|
+
}
|
|
177
|
+
this.emit("ready");
|
|
178
|
+
return object;
|
|
179
|
+
}
|
|
180
|
+
async pollUntilScanned(objectId) {
|
|
181
|
+
const polling = getMediaConfig().polling;
|
|
182
|
+
const intervalMs = this.input.scanIntervalMs ?? polling.uploadScanMs;
|
|
183
|
+
const timeoutMs = this.input.scanTimeoutMs ?? polling.timeoutMs;
|
|
184
|
+
const deadline = Date.now() + timeoutMs;
|
|
185
|
+
for (;;) {
|
|
186
|
+
if (this.controller.signal.aborted)
|
|
187
|
+
throw new UploadError("abort", "Upload aborted");
|
|
188
|
+
const object = await getObject(objectId);
|
|
189
|
+
if (object.status === "rejected" || object.scan_status === "infected" || object.scan_status === "quarantined") {
|
|
190
|
+
throw new UploadError("scan", `Object rejected by scan (${object.scan_status})`);
|
|
191
|
+
}
|
|
192
|
+
const stillPending = PENDING_OBJECT_STATES.has(object.status) || PENDING_SCAN_STATES.has(object.scan_status);
|
|
193
|
+
if (!stillPending)
|
|
194
|
+
return object;
|
|
195
|
+
if (Date.now() + intervalMs >= deadline) {
|
|
196
|
+
throw new UploadError("scan", "Timed out waiting for scan to finish");
|
|
197
|
+
}
|
|
198
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async safeAbort() {
|
|
202
|
+
// Only ever called after `initiate` has set the session id.
|
|
203
|
+
try {
|
|
204
|
+
await abortUpload(this.sessionId);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// Best-effort cleanup; surface API aborts only via logs, not by masking
|
|
208
|
+
// the original failure.
|
|
209
|
+
if (!(error instanceof ApiError))
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export function createMediaUploadController(input) {
|
|
215
|
+
return new MediaUploadController(input);
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=uploadController.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uploadController.js","sourceRoot":"","sources":["../../../../src/runtime/upload/uploadController.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mano8/astro-media-m8",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Astro integration and headless client for media-service-m8.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mano8/astro-media-m8.git"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"astro-integration",
|
|
16
|
+
"astro",
|
|
17
|
+
"media",
|
|
18
|
+
"uploads",
|
|
19
|
+
"media-service-m8"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src/routes",
|
|
24
|
+
"src/scaffold/styles",
|
|
25
|
+
"registry.json",
|
|
26
|
+
"registry/r",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./dist/src/integration.js",
|
|
32
|
+
"./api": "./dist/src/runtime/api/index.js",
|
|
33
|
+
"./client": "./dist/src/runtime/client.js",
|
|
34
|
+
"./schemas": "./dist/src/runtime/schemas.js",
|
|
35
|
+
"./auth-adapter": "./dist/src/runtime/authAdapter.js",
|
|
36
|
+
"./upload": "./dist/src/runtime/upload/index.js",
|
|
37
|
+
"./hooks": "./dist/src/runtime/hooks/index.js",
|
|
38
|
+
"./react": "./dist/src/runtime/react/index.js",
|
|
39
|
+
"./default-ui": "./dist/src/runtime/react/default-ui/index.js",
|
|
40
|
+
"./list-params": "./dist/src/runtime/listParams.js",
|
|
41
|
+
"./routes": "./dist/src/runtime/routes.js",
|
|
42
|
+
"./routes/upload.astro": "./src/routes/upload.astro",
|
|
43
|
+
"./routes/library.astro": "./src/routes/library.astro",
|
|
44
|
+
"./routes/object/[id].astro": "./src/routes/object/[id].astro",
|
|
45
|
+
"./routes/presets.astro": "./src/routes/presets.astro",
|
|
46
|
+
"./routes/admin/media.astro": "./src/routes/admin/media.astro",
|
|
47
|
+
"./middleware": "./dist/src/middleware.js",
|
|
48
|
+
"./internal-server": "./dist/src/runtime/api/internal.server.js",
|
|
49
|
+
"./compatibility": "./dist/src/runtime/compatibility.js"
|
|
50
|
+
},
|
|
51
|
+
"types": "./dist/src/integration.d.ts",
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc -p tsconfig.json && npm run build:registry",
|
|
54
|
+
"build:registry": "node scripts/build-registry.mjs",
|
|
55
|
+
"test": "vitest run --coverage",
|
|
56
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
57
|
+
"test:unit": "vitest run"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@mano8/astro-auth-m8": "^0.1.0-alpha.1",
|
|
61
|
+
"@tanstack/react-query": "^5.101.1",
|
|
62
|
+
"astro": "^5.0.0 || ^6.0.0",
|
|
63
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
64
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
65
|
+
"zod": "^4.0.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"react": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"react-dom": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@mano8/astro-auth-m8": "^0.1.0-alpha.1",
|
|
77
|
+
"@tanstack/react-query": "^5.101.1",
|
|
78
|
+
"@types/node": "^22.0.0",
|
|
79
|
+
"@types/react": "^19.0.0",
|
|
80
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
81
|
+
"astro": "^6.0.0",
|
|
82
|
+
"jsdom": "^29.1.1",
|
|
83
|
+
"react": "^19.0.0",
|
|
84
|
+
"react-dom": "^19.0.0",
|
|
85
|
+
"typescript": "^5.8.0",
|
|
86
|
+
"vitest": "^4.1.9",
|
|
87
|
+
"zod": "^4.0.0"
|
|
88
|
+
},
|
|
89
|
+
"overrides": {
|
|
90
|
+
"esbuild": "^0.28.1"
|
|
91
|
+
},
|
|
92
|
+
"mediaServiceM8": {
|
|
93
|
+
"contract": "media-service-m8@0.0",
|
|
94
|
+
"testedServiceVersion": "0.0.10",
|
|
95
|
+
"serviceVersionRange": ">=0.0.10 <0.1.0"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "admin-media-dashboard",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"title": "Admin Media Dashboard",
|
|
6
|
+
"description": "Media admin view whose first/landing tab is the storage dashboard and whose destructive ops live in a guarded Maintenance tab; wraps the package's MediaProvider + RequireSuperuser. Requires @fa-m8/astro-media-m8 (and a fa-auth-m8 token adapter) installed.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"tabs",
|
|
12
|
+
"./node_modules/@fa-m8/astro-media-m8/registry/r/media-dashboard-overview.json",
|
|
13
|
+
"./node_modules/@fa-m8/astro-media-m8/registry/r/media-maintenance-panel.json"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "registry/blocks/admin/admin-media-dashboard.tsx",
|
|
18
|
+
"content": "\"use client\";\r\n\r\n// Media admin view whose FIRST/landing tab is the storage dashboard; the\r\n// destructive operations are demoted to a guarded \"Maintenance\" tab. Headless\r\n// state comes from the package (@fa-m8/astro-media-m8/react + /hooks); this file\r\n// is only the shadcn skin, copied into the consumer via the @fa-m8-media\r\n// registry. Because media-service only accepts fa-auth-m8 tokens, the auth\r\n// package must also be present (pass its adapter via `adapter`). Edit per app.\r\nimport * as React from \"react\";\r\nimport { LayoutDashboard, Wrench } from \"lucide-react\";\r\nimport {\r\n MediaProvider,\r\n RequireSuperuser,\r\n type MediaContextValue,\r\n} from \"@fa-m8/astro-media-m8/react\";\r\n\r\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\r\nimport {\r\n MediaDashboardOverview,\r\n type MediaDashboardOverviewLabels,\r\n} from \"@/components/fa-media/media-dashboard-overview\";\r\nimport {\r\n MediaMaintenancePanel,\r\n type MediaMaintenanceLabels,\r\n} from \"@/components/fa-media/media-maintenance-panel\";\r\n\r\nexport interface AdminMediaDashboardLabels {\r\n dashboardTab: string;\r\n maintenanceTab: string;\r\n forbidden: string;\r\n overview: Partial<MediaDashboardOverviewLabels>;\r\n maintenance: Partial<MediaMaintenanceLabels>;\r\n}\r\n\r\nconst DEFAULT_LABELS: AdminMediaDashboardLabels = {\r\n dashboardTab: \"Dashboard\",\r\n maintenanceTab: \"Maintenance\",\r\n forbidden: \"You need administrator access to view this page.\",\r\n overview: {},\r\n maintenance: {},\r\n};\r\n\r\nexport interface AdminMediaDashboardProps {\r\n /** Media runtime config (apiBase/v1Base/legacyBase/adminRole). */\r\n config?: React.ComponentProps<typeof MediaProvider>[\"config\"];\r\n /** fa-auth-backed media auth adapter (media only accepts fa-auth-m8 tokens). */\r\n adapter?: MediaContextValue[\"adapter\"];\r\n labels?: Partial<AdminMediaDashboardLabels>;\r\n}\r\n\r\nfunction AdminMediaShell({ labels }: { labels: AdminMediaDashboardLabels }) {\r\n return (\r\n <RequireSuperuser\r\n fallback={\r\n <div className=\"not-content mx-auto w-full max-w-md py-10 text-center text-sm text-muted-foreground\">\r\n {labels.forbidden}\r\n </div>\r\n }\r\n >\r\n <Tabs defaultValue=\"dashboard\" className=\"not-content mx-auto w-full max-w-6xl space-y-4\">\r\n <TabsList className=\"flex h-auto flex-wrap justify-start gap-1\">\r\n <TabsTrigger value=\"dashboard\" className=\"gap-2\">\r\n <LayoutDashboard className=\"size-4\" />\r\n {labels.dashboardTab}\r\n </TabsTrigger>\r\n <TabsTrigger value=\"maintenance\" className=\"gap-2\">\r\n <Wrench className=\"size-4\" />\r\n {labels.maintenanceTab}\r\n </TabsTrigger>\r\n </TabsList>\r\n\r\n <TabsContent value=\"dashboard\">\r\n <MediaDashboardOverview labels={labels.overview} />\r\n </TabsContent>\r\n <TabsContent value=\"maintenance\">\r\n <MediaMaintenancePanel labels={labels.maintenance} />\r\n </TabsContent>\r\n </Tabs>\r\n </RequireSuperuser>\r\n );\r\n}\r\n\r\nexport default function AdminMediaDashboard({\r\n config,\r\n adapter,\r\n labels,\r\n}: AdminMediaDashboardProps) {\r\n const resolved: AdminMediaDashboardLabels = {\r\n ...DEFAULT_LABELS,\r\n ...labels,\r\n overview: { ...DEFAULT_LABELS.overview, ...labels?.overview },\r\n maintenance: { ...DEFAULT_LABELS.maintenance, ...labels?.maintenance },\r\n };\r\n return (\r\n <MediaProvider config={config} adapter={adapter}>\r\n <AdminMediaShell labels={resolved} />\r\n </MediaProvider>\r\n );\r\n}\r\n",
|
|
19
|
+
"type": "registry:component",
|
|
20
|
+
"target": "components/fa-media/admin-media-dashboard.tsx"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "data-table",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"title": "Data Table",
|
|
6
|
+
"description": "Generic pure-shadcn data table (sorting, filtering, pagination) built on @tanstack/react-table.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tanstack/react-table"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"table",
|
|
12
|
+
"button",
|
|
13
|
+
"input"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "registry/blocks/data-table/data-table.tsx",
|
|
18
|
+
"content": "\"use client\";\n\n// Generic, pure-shadcn data table (sorting + filtering + pagination) built on\n// @tanstack/react-table and the shadcn `table` primitive. Copied into the\n// consumer via the @fa-m8-media registry; edit freely per app.\nimport * as React from \"react\";\nimport {\n type ColumnDef,\n type ColumnFiltersState,\n type SortingState,\n flexRender,\n getCoreRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n useReactTable,\n} from \"@tanstack/react-table\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\n\nexport interface DataTableProps<TData, TValue> {\n columns: ColumnDef<TData, TValue>[];\n data: TData[];\n /** Column id to wire the search box to; omit to hide the filter input. */\n filterColumn?: string;\n filterPlaceholder?: string;\n /** Page size for client-side pagination. Defaults to 10. */\n pageSize?: number;\n /** Rendered when there are no rows. */\n emptyMessage?: string;\n}\n\nexport function DataTable<TData, TValue>({\n columns,\n data,\n filterColumn,\n filterPlaceholder = \"Filter…\",\n pageSize = 10,\n emptyMessage = \"No results.\",\n}: DataTableProps<TData, TValue>) {\n const [sorting, setSorting] = React.useState<SortingState>([]);\n const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);\n\n const table = useReactTable({\n data,\n columns,\n getCoreRowModel: getCoreRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n onSortingChange: setSorting,\n onColumnFiltersChange: setColumnFilters,\n initialState: { pagination: { pageSize } },\n state: { sorting, columnFilters },\n });\n\n const filterCol = filterColumn ? table.getColumn(filterColumn) : undefined;\n\n return (\n <div className=\"space-y-3\">\n {filterCol ? (\n <Input\n value={(filterCol.getFilterValue() as string) ?? \"\"}\n onChange={(event) => filterCol.setFilterValue(event.target.value)}\n placeholder={filterPlaceholder}\n className=\"max-w-sm\"\n aria-label={filterPlaceholder}\n />\n ) : null}\n\n <div className=\"rounded-md border\">\n <Table>\n <TableHeader>\n {table.getHeaderGroups().map((headerGroup) => (\n <TableRow key={headerGroup.id}>\n {headerGroup.headers.map((header) => (\n <TableHead key={header.id}>\n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n </TableHead>\n ))}\n </TableRow>\n ))}\n </TableHeader>\n <TableBody>\n {table.getRowModel().rows.length ? (\n table.getRowModel().rows.map((row) => (\n <TableRow\n key={row.id}\n data-state={row.getIsSelected() && \"selected\"}\n >\n {row.getVisibleCells().map((cell) => (\n <TableCell key={cell.id}>\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n </TableCell>\n ))}\n </TableRow>\n ))\n ) : (\n <TableRow>\n <TableCell colSpan={columns.length} className=\"h-24 text-center\">\n {emptyMessage}\n </TableCell>\n </TableRow>\n )}\n </TableBody>\n </Table>\n </div>\n\n {table.getPageCount() > 1 ? (\n <div className=\"flex items-center justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n >\n Previous\n </Button>\n <span className=\"text-sm text-muted-foreground\">\n Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n {table.getPageCount()}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => table.nextPage()}\n disabled={!table.getCanNextPage()}\n >\n Next\n </Button>\n </div>\n ) : null}\n </div>\n );\n}\n",
|
|
19
|
+
"type": "registry:component",
|
|
20
|
+
"target": "components/fa-media/data-table.tsx"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "media-dashboard-overview",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"title": "Media Admin Dashboard Overview",
|
|
6
|
+
"description": "Media storage dashboard (stat cards + chart + subscriptions table) driven by the package's useMediaAdmin hook. Requires @fa-m8/astro-media-m8 installed.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react",
|
|
9
|
+
"@tanstack/react-table"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"card",
|
|
13
|
+
"button",
|
|
14
|
+
"skeleton",
|
|
15
|
+
"./node_modules/@fa-m8/astro-media-m8/registry/r/media-storage-chart.json",
|
|
16
|
+
"./node_modules/@fa-m8/astro-media-m8/registry/r/data-table.json"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
{
|
|
20
|
+
"path": "registry/blocks/admin/media-dashboard-overview.tsx",
|
|
21
|
+
"content": "\"use client\";\r\n\r\n// Media admin landing view: a storage dashboard built from the package's\r\n// headless `useMediaAdmin` hook. Logic stays a live dependency\r\n// (@fa-m8/astro-media-m8/hooks); this file is only the shadcn skin and is copied\r\n// into the consumer via the @fa-m8-media registry — edit freely per app.\r\nimport * as React from \"react\";\r\nimport {\r\n Boxes,\r\n HardDrive,\r\n Trash2,\r\n Clock,\r\n Database,\r\n CloudOff,\r\n} from \"lucide-react\";\r\nimport { useMediaAdmin } from \"@fa-m8/astro-media-m8/hooks\";\r\nimport type { SubscriptionPublic } from \"@fa-m8/astro-media-m8/schemas\";\r\nimport type { ColumnDef } from \"@tanstack/react-table\";\r\n\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Skeleton } from \"@/components/ui/skeleton\";\r\nimport {\r\n MediaStorageChart,\r\n humanizeBytes,\r\n} from \"@/components/fa-media/media-storage-chart\";\r\nimport { DataTable } from \"@/components/fa-media/data-table\";\r\n\r\nexport interface MediaDashboardOverviewLabels {\r\n title: string;\r\n subtitle: string;\r\n totalObjects: string;\r\n totalBytes: string;\r\n deletedObjects: string;\r\n staleSessions: string;\r\n dbOrphans: string;\r\n storageOrphans: string;\r\n storageByCategory: string;\r\n storageEmpty: string;\r\n subscriptionsTitle: string;\r\n subUrl: string;\r\n subEvents: string;\r\n subStatus: string;\r\n subActive: string;\r\n subInactive: string;\r\n subActions: string;\r\n subDelete: string;\r\n subEmpty: string;\r\n error: string;\r\n}\r\n\r\nconst DEFAULT_LABELS: MediaDashboardOverviewLabels = {\r\n title: \"Overview\",\r\n subtitle: \"Storage and processing at a glance.\",\r\n totalObjects: \"Objects\",\r\n totalBytes: \"Storage used\",\r\n deletedObjects: \"Deleted\",\r\n staleSessions: \"Stale uploads\",\r\n dbOrphans: \"DB orphans\",\r\n storageOrphans: \"Storage orphans\",\r\n storageByCategory: \"Storage by category\",\r\n storageEmpty: \"No stored objects yet.\",\r\n subscriptionsTitle: \"Webhook subscriptions\",\r\n subUrl: \"Endpoint\",\r\n subEvents: \"Events\",\r\n subStatus: \"Status\",\r\n subActive: \"Active\",\r\n subInactive: \"Inactive\",\r\n subActions: \"Actions\",\r\n subDelete: \"Delete\",\r\n subEmpty: \"No webhook subscriptions.\",\r\n error: \"Could not load admin data.\",\r\n};\r\n\r\nexport interface MediaDashboardOverviewProps {\r\n labels?: Partial<MediaDashboardOverviewLabels>;\r\n}\r\n\r\nfunction StatCard({\r\n label,\r\n value,\r\n icon: Icon,\r\n}: {\r\n label: string;\r\n value: React.ReactNode;\r\n icon: React.ComponentType<{ className?: string }>;\r\n}) {\r\n return (\r\n <Card className=\"h-full\">\r\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\r\n <CardTitle className=\"text-sm font-medium text-muted-foreground\">\r\n {label}\r\n </CardTitle>\r\n <Icon className=\"size-4 text-muted-foreground\" />\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"text-2xl font-semibold tabular-nums\">{value}</div>\r\n </CardContent>\r\n </Card>\r\n );\r\n}\r\n\r\nexport function MediaDashboardOverview({\r\n labels,\r\n}: MediaDashboardOverviewProps) {\r\n const t = { ...DEFAULT_LABELS, ...labels };\r\n const {\r\n stats,\r\n stale,\r\n orphans,\r\n subscriptions,\r\n error,\r\n loadStats,\r\n loadStale,\r\n loadOrphans,\r\n loadSubscriptions,\r\n removeSubscription,\r\n } = useMediaAdmin();\r\n const [ready, setReady] = React.useState(false);\r\n\r\n React.useEffect(() => {\r\n let cancelled = false;\r\n void Promise.allSettled([\r\n loadStats(),\r\n loadStale(),\r\n loadOrphans(),\r\n loadSubscriptions(),\r\n ]).finally(() => {\r\n if (!cancelled) setReady(true);\r\n });\r\n return () => {\r\n cancelled = true;\r\n };\r\n // The hook callbacks are stable for a given superuser identity; run once.\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, []);\r\n\r\n const storageData = (stats?.by_category ?? []).map((row) => ({\r\n category: row.category,\r\n bytes: row.total_bytes,\r\n count: row.count,\r\n }));\r\n\r\n const columns = React.useMemo<ColumnDef<SubscriptionPublic>[]>(\r\n () => [\r\n { accessorKey: \"url\", header: t.subUrl },\r\n {\r\n accessorKey: \"event_types\",\r\n header: t.subEvents,\r\n cell: ({ row }) => row.original.event_types.join(\", \") || \"*\",\r\n },\r\n {\r\n accessorKey: \"active\",\r\n header: t.subStatus,\r\n cell: ({ row }) => (row.original.active ? t.subActive : t.subInactive),\r\n },\r\n {\r\n id: \"actions\",\r\n header: t.subActions,\r\n cell: ({ row }) => (\r\n <Button\r\n variant=\"ghost\"\r\n size=\"sm\"\r\n className=\"text-destructive hover:text-destructive\"\r\n onClick={() => void removeSubscription(row.original.id)}\r\n >\r\n {t.subDelete}\r\n </Button>\r\n ),\r\n },\r\n ],\r\n [t, removeSubscription],\r\n );\r\n\r\n if (!ready) {\r\n return (\r\n <div className=\"not-content space-y-4\" aria-busy=\"true\">\r\n <div className=\"grid auto-rows-fr gap-4 sm:grid-cols-2 lg:grid-cols-3\">\r\n {[0, 1, 2, 3, 4, 5].map((i) => (\r\n <Skeleton key={i} className=\"h-28 w-full\" />\r\n ))}\r\n </div>\r\n <Skeleton className=\"h-64 w-full\" />\r\n </div>\r\n );\r\n }\r\n\r\n if (error && !stats) {\r\n return (\r\n <Card role=\"alert\" className=\"border-destructive/50\">\r\n <CardContent className=\"py-6 text-sm text-destructive\">\r\n {t.error}\r\n </CardContent>\r\n </Card>\r\n );\r\n }\r\n\r\n return (\r\n <div className=\"not-content space-y-6\">\r\n <div className=\"space-y-1\">\r\n <h2 className=\"text-xl font-semibold tracking-tight\">{t.title}</h2>\r\n <p className=\"text-sm text-muted-foreground\">{t.subtitle}</p>\r\n </div>\r\n\r\n <div className=\"grid auto-rows-fr gap-4 sm:grid-cols-2 lg:grid-cols-3\">\r\n <StatCard\r\n label={t.totalObjects}\r\n value={(stats?.total_objects ?? 0).toLocaleString()}\r\n icon={Boxes}\r\n />\r\n <StatCard\r\n label={t.totalBytes}\r\n value={humanizeBytes(stats?.total_bytes ?? 0)}\r\n icon={HardDrive}\r\n />\r\n <StatCard\r\n label={t.deletedObjects}\r\n value={(stats?.deleted_objects ?? 0).toLocaleString()}\r\n icon={Trash2}\r\n />\r\n <StatCard\r\n label={t.staleSessions}\r\n value={(stale?.count ?? 0).toLocaleString()}\r\n icon={Clock}\r\n />\r\n <StatCard\r\n label={t.dbOrphans}\r\n value={(orphans?.db_orphan_count ?? 0).toLocaleString()}\r\n icon={Database}\r\n />\r\n <StatCard\r\n label={t.storageOrphans}\r\n value={(orphans?.storage_orphan_count ?? 0).toLocaleString()}\r\n icon={CloudOff}\r\n />\r\n </div>\r\n\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"text-base\">{t.storageByCategory}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n {storageData.length ? (\r\n <MediaStorageChart\r\n data={storageData}\r\n bytesLabel={t.totalBytes}\r\n className=\"aspect-auto h-64 w-full\"\r\n />\r\n ) : (\r\n <p className=\"py-8 text-center text-sm text-muted-foreground\">\r\n {t.storageEmpty}\r\n </p>\r\n )}\r\n </CardContent>\r\n </Card>\r\n\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"text-base\">{t.subscriptionsTitle}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <DataTable\r\n columns={columns}\r\n data={subscriptions?.items ?? []}\r\n filterColumn=\"url\"\r\n filterPlaceholder={t.subUrl}\r\n emptyMessage={t.subEmpty}\r\n />\r\n </CardContent>\r\n </Card>\r\n </div>\r\n );\r\n}\r\n",
|
|
22
|
+
"type": "registry:component",
|
|
23
|
+
"target": "components/fa-media/media-dashboard-overview.tsx"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "media-maintenance-panel",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"title": "Media Admin Maintenance Panel",
|
|
6
|
+
"description": "Media admin danger zone (purge-stale / repair-orphans / purge-expired), each behind a shadcn alert-dialog confirmation, driven by useMediaAdmin. Requires @fa-m8/astro-media-m8 installed.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"card",
|
|
12
|
+
"button",
|
|
13
|
+
"alert-dialog"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "registry/blocks/admin/media-maintenance-panel.tsx",
|
|
18
|
+
"content": "\"use client\";\r\n\r\n// Media admin \"Maintenance / Danger zone\": the destructive storage operations,\r\n// each behind a shadcn `alert-dialog` confirmation. Logic stays a live\r\n// dependency (@fa-m8/astro-media-m8/hooks); this file is only the shadcn skin,\r\n// copied into the consumer via the @fa-m8-media registry — edit freely per app.\r\nimport * as React from \"react\";\r\nimport { AlertTriangle } from \"lucide-react\";\r\nimport { useMediaAdmin } from \"@fa-m8/astro-media-m8/hooks\";\r\n\r\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n AlertDialog,\r\n AlertDialogAction,\r\n AlertDialogCancel,\r\n AlertDialogContent,\r\n AlertDialogDescription,\r\n AlertDialogFooter,\r\n AlertDialogHeader,\r\n AlertDialogTitle,\r\n AlertDialogTrigger,\r\n} from \"@/components/ui/alert-dialog\";\r\n\r\nexport interface MediaMaintenanceLabels {\r\n title: string;\r\n subtitle: string;\r\n confirmTitle: string;\r\n cancel: string;\r\n confirm: string;\r\n running: string;\r\n done: string;\r\n error: string;\r\n purgeStaleTitle: string;\r\n purgeStaleDescription: string;\r\n repairTitle: string;\r\n repairDescription: string;\r\n purgeExpiredTitle: string;\r\n purgeExpiredDescription: string;\r\n}\r\n\r\nconst DEFAULT_LABELS: MediaMaintenanceLabels = {\r\n title: \"Maintenance\",\r\n subtitle: \"Destructive storage operations. Each action is irreversible.\",\r\n confirmTitle: \"Are you sure?\",\r\n cancel: \"Cancel\",\r\n confirm: \"Run\",\r\n running: \"Running…\",\r\n done: \"Done.\",\r\n error: \"Operation failed.\",\r\n purgeStaleTitle: \"Purge stale uploads\",\r\n purgeStaleDescription:\r\n \"Delete upload sessions that expired before completing. Frees reserved storage keys.\",\r\n repairTitle: \"Repair orphans\",\r\n repairDescription:\r\n \"Reconcile DB and storage, deleting storage-orphan bytes that have no matching object record.\",\r\n purgeExpiredTitle: \"Purge expired (retention)\",\r\n purgeExpiredDescription:\r\n \"Hard-delete soft-deleted objects past their retention window. Cannot be undone.\",\r\n};\r\n\r\nexport interface MediaMaintenancePanelProps {\r\n labels?: Partial<MediaMaintenanceLabels>;\r\n}\r\n\r\ntype OpState = { status: \"idle\" | \"running\" | \"done\" | \"error\"; message?: string };\r\n\r\nfunction DangerAction({\r\n title,\r\n description,\r\n labels,\r\n run,\r\n}: {\r\n title: string;\r\n description: string;\r\n labels: MediaMaintenanceLabels;\r\n run: () => Promise<unknown>;\r\n}) {\r\n const [state, setState] = React.useState<OpState>({ status: \"idle\" });\r\n\r\n const onConfirm = async () => {\r\n setState({ status: \"running\" });\r\n try {\r\n await run();\r\n setState({ status: \"done\", message: labels.done });\r\n } catch (err) {\r\n setState({\r\n status: \"error\",\r\n message: err instanceof Error ? err.message : labels.error,\r\n });\r\n }\r\n };\r\n\r\n return (\r\n <div className=\"flex flex-col gap-3 rounded-md border p-4 sm:flex-row sm:items-center sm:justify-between\">\r\n <div className=\"min-w-0 space-y-1\">\r\n <p className=\"text-sm font-medium\">{title}</p>\r\n <p className=\"text-sm text-muted-foreground\">{description}</p>\r\n {state.status === \"done\" ? (\r\n <p className=\"text-sm text-emerald-600\">{state.message}</p>\r\n ) : null}\r\n {state.status === \"error\" ? (\r\n <p className=\"text-sm text-destructive\" role=\"alert\">\r\n {state.message}\r\n </p>\r\n ) : null}\r\n </div>\r\n <AlertDialog>\r\n <AlertDialogTrigger asChild>\r\n <Button variant=\"destructive\" disabled={state.status === \"running\"}>\r\n {state.status === \"running\" ? labels.running : labels.confirm}\r\n </Button>\r\n </AlertDialogTrigger>\r\n <AlertDialogContent>\r\n <AlertDialogHeader>\r\n <AlertDialogTitle>{labels.confirmTitle}</AlertDialogTitle>\r\n <AlertDialogDescription>{description}</AlertDialogDescription>\r\n </AlertDialogHeader>\r\n <AlertDialogFooter>\r\n <AlertDialogCancel>{labels.cancel}</AlertDialogCancel>\r\n <AlertDialogAction onClick={() => void onConfirm()}>\r\n {labels.confirm}\r\n </AlertDialogAction>\r\n </AlertDialogFooter>\r\n </AlertDialogContent>\r\n </AlertDialog>\r\n </div>\r\n );\r\n}\r\n\r\nexport function MediaMaintenancePanel({ labels }: MediaMaintenancePanelProps) {\r\n const t = { ...DEFAULT_LABELS, ...labels };\r\n const { purgeStale, repair, purgeExpiredObjects } = useMediaAdmin();\r\n\r\n return (\r\n <Card className=\"not-content border-destructive/40\">\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2 text-base text-destructive\">\r\n <AlertTriangle className=\"size-4\" />\r\n {t.title}\r\n </CardTitle>\r\n <CardDescription>{t.subtitle}</CardDescription>\r\n </CardHeader>\r\n <CardContent className=\"space-y-3\">\r\n <DangerAction\r\n title={t.purgeStaleTitle}\r\n description={t.purgeStaleDescription}\r\n labels={t}\r\n run={() => purgeStale()}\r\n />\r\n <DangerAction\r\n title={t.repairTitle}\r\n description={t.repairDescription}\r\n labels={t}\r\n run={() => repair(true)}\r\n />\r\n <DangerAction\r\n title={t.purgeExpiredTitle}\r\n description={t.purgeExpiredDescription}\r\n labels={t}\r\n run={() => purgeExpiredObjects()}\r\n />\r\n </CardContent>\r\n </Card>\r\n );\r\n}\r\n",
|
|
19
|
+
"type": "registry:component",
|
|
20
|
+
"target": "components/fa-media/media-maintenance-panel.tsx"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "media-storage-chart",
|
|
4
|
+
"type": "registry:component",
|
|
5
|
+
"title": "Media Storage Chart",
|
|
6
|
+
"description": "shadcn chart preset (recharts) for media storage bytes per category, with a base-1024 byte humanizer.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"recharts"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"chart"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "registry/blocks/chart/media-storage-chart.tsx",
|
|
16
|
+
"content": "\"use client\";\n\n// Pure-shadcn bar chart preset (recharts-backed) for the media admin dashboard:\n// storage bytes per category. Copied into the consumer via the @fa-m8-media\n// registry; edit freely per app.\nimport { Bar, BarChart, CartesianGrid, XAxis, YAxis } from \"recharts\";\n\nimport {\n type ChartConfig,\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n} from \"@/components/ui/chart\";\n\nexport interface StorageDatum {\n category: string;\n bytes: number;\n count: number;\n}\n\nexport interface MediaStorageChartProps {\n data: StorageDatum[];\n bytesLabel?: string;\n className?: string;\n}\n\n/** Compact, base-1024 byte humanizer shared by the axis + tooltip. */\nexport function humanizeBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes <= 0) return \"0 B\";\n const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"];\n const exponent = Math.min(\n units.length - 1,\n Math.floor(Math.log(bytes) / Math.log(1024)),\n );\n const value = bytes / 1024 ** exponent;\n return `${value.toFixed(value >= 10 || exponent === 0 ? 0 : 1)} ${units[exponent]}`;\n}\n\nexport function MediaStorageChart({\n data,\n bytesLabel = \"Storage\",\n className,\n}: MediaStorageChartProps) {\n const chartConfig = {\n bytes: { label: bytesLabel, color: \"var(--chart-1)\" },\n } satisfies ChartConfig;\n\n return (\n <ChartContainer config={chartConfig} className={className}>\n <BarChart accessibilityLayer data={data}>\n <CartesianGrid vertical={false} />\n <XAxis\n dataKey=\"category\"\n tickLine={false}\n tickMargin={10}\n axisLine={false}\n />\n <YAxis\n tickLine={false}\n axisLine={false}\n width={56}\n tickFormatter={(value: number) => humanizeBytes(value)}\n />\n <ChartTooltip\n content={\n <ChartTooltipContent\n formatter={(value) => humanizeBytes(Number(value))}\n />\n }\n />\n <Bar dataKey=\"bytes\" fill=\"var(--color-bytes)\" radius={4} />\n </BarChart>\n </ChartContainer>\n );\n}\n",
|
|
17
|
+
"type": "registry:component",
|
|
18
|
+
"target": "components/fa-media/media-storage-chart.tsx"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
|
3
|
+
"name": "fa-m8-media",
|
|
4
|
+
"homepage": "https://github.com/mano8/astro-media-m8",
|
|
5
|
+
"items": [
|
|
6
|
+
{
|
|
7
|
+
"name": "data-table",
|
|
8
|
+
"type": "registry:component",
|
|
9
|
+
"title": "Data Table",
|
|
10
|
+
"description": "Generic pure-shadcn data table (sorting, filtering, pagination) built on @tanstack/react-table."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "media-storage-chart",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"title": "Media Storage Chart",
|
|
16
|
+
"description": "shadcn chart preset (recharts) for media storage bytes per category, with a base-1024 byte humanizer."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "media-dashboard-overview",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"title": "Media Admin Dashboard Overview",
|
|
22
|
+
"description": "Media storage dashboard (stat cards + chart + subscriptions table) driven by the package's useMediaAdmin hook. Requires @fa-m8/astro-media-m8 installed."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "media-maintenance-panel",
|
|
26
|
+
"type": "registry:component",
|
|
27
|
+
"title": "Media Admin Maintenance Panel",
|
|
28
|
+
"description": "Media admin danger zone (purge-stale / repair-orphans / purge-expired), each behind a shadcn alert-dialog confirmation, driven by useMediaAdmin. Requires @fa-m8/astro-media-m8 installed."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "admin-media-dashboard",
|
|
32
|
+
"type": "registry:component",
|
|
33
|
+
"title": "Admin Media Dashboard",
|
|
34
|
+
"description": "Media admin view whose first/landing tab is the storage dashboard and whose destructive ops live in a guarded Maintenance tab; wraps the package's MediaProvider + RequireSuperuser. Requires @fa-m8/astro-media-m8 (and a fa-auth-m8 token adapter) installed."
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
package/registry.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
|
3
|
+
"name": "fa-m8-media",
|
|
4
|
+
"homepage": "https://github.com/mano8/astro-media-m8",
|
|
5
|
+
"items": [
|
|
6
|
+
{
|
|
7
|
+
"name": "data-table",
|
|
8
|
+
"type": "registry:component",
|
|
9
|
+
"title": "Data Table",
|
|
10
|
+
"description": "Generic pure-shadcn data table (sorting, filtering, pagination) built on @tanstack/react-table.",
|
|
11
|
+
"dependencies": ["@tanstack/react-table"],
|
|
12
|
+
"registryDependencies": ["table", "button", "input"],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "registry/blocks/data-table/data-table.tsx",
|
|
16
|
+
"type": "registry:component",
|
|
17
|
+
"target": "components/fa-media/data-table.tsx"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "media-storage-chart",
|
|
23
|
+
"type": "registry:component",
|
|
24
|
+
"title": "Media Storage Chart",
|
|
25
|
+
"description": "shadcn chart preset (recharts) for media storage bytes per category, with a base-1024 byte humanizer.",
|
|
26
|
+
"dependencies": ["recharts"],
|
|
27
|
+
"registryDependencies": ["chart"],
|
|
28
|
+
"files": [
|
|
29
|
+
{
|
|
30
|
+
"path": "registry/blocks/chart/media-storage-chart.tsx",
|
|
31
|
+
"type": "registry:component",
|
|
32
|
+
"target": "components/fa-media/media-storage-chart.tsx"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "media-dashboard-overview",
|
|
38
|
+
"type": "registry:component",
|
|
39
|
+
"title": "Media Admin Dashboard Overview",
|
|
40
|
+
"description": "Media storage dashboard (stat cards + chart + subscriptions table) driven by the package's useMediaAdmin hook. Requires @fa-m8/astro-media-m8 installed.",
|
|
41
|
+
"dependencies": ["lucide-react", "@tanstack/react-table"],
|
|
42
|
+
"registryDependencies": [
|
|
43
|
+
"card",
|
|
44
|
+
"button",
|
|
45
|
+
"skeleton",
|
|
46
|
+
"./node_modules/@fa-m8/astro-media-m8/registry/r/media-storage-chart.json",
|
|
47
|
+
"./node_modules/@fa-m8/astro-media-m8/registry/r/data-table.json"
|
|
48
|
+
],
|
|
49
|
+
"files": [
|
|
50
|
+
{
|
|
51
|
+
"path": "registry/blocks/admin/media-dashboard-overview.tsx",
|
|
52
|
+
"type": "registry:component",
|
|
53
|
+
"target": "components/fa-media/media-dashboard-overview.tsx"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "media-maintenance-panel",
|
|
59
|
+
"type": "registry:component",
|
|
60
|
+
"title": "Media Admin Maintenance Panel",
|
|
61
|
+
"description": "Media admin danger zone (purge-stale / repair-orphans / purge-expired), each behind a shadcn alert-dialog confirmation, driven by useMediaAdmin. Requires @fa-m8/astro-media-m8 installed.",
|
|
62
|
+
"dependencies": ["lucide-react"],
|
|
63
|
+
"registryDependencies": ["card", "button", "alert-dialog"],
|
|
64
|
+
"files": [
|
|
65
|
+
{
|
|
66
|
+
"path": "registry/blocks/admin/media-maintenance-panel.tsx",
|
|
67
|
+
"type": "registry:component",
|
|
68
|
+
"target": "components/fa-media/media-maintenance-panel.tsx"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "admin-media-dashboard",
|
|
74
|
+
"type": "registry:component",
|
|
75
|
+
"title": "Admin Media Dashboard",
|
|
76
|
+
"description": "Media admin view whose first/landing tab is the storage dashboard and whose destructive ops live in a guarded Maintenance tab; wraps the package's MediaProvider + RequireSuperuser. Requires @fa-m8/astro-media-m8 (and a fa-auth-m8 token adapter) installed.",
|
|
77
|
+
"dependencies": ["lucide-react"],
|
|
78
|
+
"registryDependencies": [
|
|
79
|
+
"tabs",
|
|
80
|
+
"./node_modules/@fa-m8/astro-media-m8/registry/r/media-dashboard-overview.json",
|
|
81
|
+
"./node_modules/@fa-m8/astro-media-m8/registry/r/media-maintenance-panel.json"
|
|
82
|
+
],
|
|
83
|
+
"files": [
|
|
84
|
+
{
|
|
85
|
+
"path": "registry/blocks/admin/admin-media-dashboard.tsx",
|
|
86
|
+
"type": "registry:component",
|
|
87
|
+
"target": "components/fa-media/admin-media-dashboard.tsx"
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|