@cedros/data-react 0.1.0
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/CHANGELOG.md +24 -0
- package/README.md +177 -0
- package/dist/admin/api.d.ts +19 -0
- package/dist/admin/api.js +108 -0
- package/dist/admin/components.d.ts +36 -0
- package/dist/admin/components.js +22 -0
- package/dist/admin/history.d.ts +17 -0
- package/dist/admin/history.js +103 -0
- package/dist/admin/icons.d.ts +15 -0
- package/dist/admin/icons.js +18 -0
- package/dist/admin/index.d.ts +13 -0
- package/dist/admin/index.js +12 -0
- package/dist/admin/permissions.d.ts +4 -0
- package/dist/admin/permissions.js +45 -0
- package/dist/admin/plugin.d.ts +4 -0
- package/dist/admin/plugin.js +180 -0
- package/dist/admin/primitives/ConfirmDialog.d.ts +14 -0
- package/dist/admin/primitives/ConfirmDialog.js +7 -0
- package/dist/admin/primitives/DataTable.d.ts +14 -0
- package/dist/admin/primitives/DataTable.js +7 -0
- package/dist/admin/primitives/DiffViewer.d.ts +11 -0
- package/dist/admin/primitives/DiffViewer.js +8 -0
- package/dist/admin/primitives/FormFieldRow.d.ts +23 -0
- package/dist/admin/primitives/FormFieldRow.js +16 -0
- package/dist/admin/primitives/JsonCodeEditor.d.ts +10 -0
- package/dist/admin/primitives/JsonCodeEditor.js +42 -0
- package/dist/admin/primitives/Pagination.d.ts +8 -0
- package/dist/admin/primitives/Pagination.js +8 -0
- package/dist/admin/primitives/Toolbar.d.ts +23 -0
- package/dist/admin/primitives/Toolbar.js +10 -0
- package/dist/admin/primitives/alerts.d.ts +21 -0
- package/dist/admin/primitives/alerts.js +44 -0
- package/dist/admin/sectionIds.d.ts +20 -0
- package/dist/admin/sectionIds.js +33 -0
- package/dist/admin/sections/CollectionsSection.d.ts +2 -0
- package/dist/admin/sections/CollectionsSection.js +125 -0
- package/dist/admin/sections/ContractVerifySection.d.ts +11 -0
- package/dist/admin/sections/ContractVerifySection.js +98 -0
- package/dist/admin/sections/CustomDataSection.d.ts +2 -0
- package/dist/admin/sections/CustomDataSection.js +256 -0
- package/dist/admin/sections/DataOpsSection.d.ts +26 -0
- package/dist/admin/sections/DataOpsSection.js +245 -0
- package/dist/admin/sections/HistorySection.d.ts +2 -0
- package/dist/admin/sections/HistorySection.js +26 -0
- package/dist/admin/sections/MonetizationSection.d.ts +2 -0
- package/dist/admin/sections/MonetizationSection.js +140 -0
- package/dist/admin/sections/NavigationSection.d.ts +13 -0
- package/dist/admin/sections/NavigationSection.js +195 -0
- package/dist/admin/sections/PagesSection.d.ts +2 -0
- package/dist/admin/sections/PagesSection.js +157 -0
- package/dist/admin/sections/SchemaDesignerSection.d.ts +2 -0
- package/dist/admin/sections/SchemaDesignerSection.js +167 -0
- package/dist/admin/sections/SiteSettingsSection.d.ts +12 -0
- package/dist/admin/sections/SiteSettingsSection.js +122 -0
- package/dist/admin/sections/TippingSection.d.ts +2 -0
- package/dist/admin/sections/TippingSection.js +178 -0
- package/dist/admin/sections/media/MediaDetail.d.ts +12 -0
- package/dist/admin/sections/media/MediaDetail.js +74 -0
- package/dist/admin/sections/media/MediaGrid.d.ts +14 -0
- package/dist/admin/sections/media/MediaGrid.js +22 -0
- package/dist/admin/sections/media/MediaSection.d.ts +2 -0
- package/dist/admin/sections/media/MediaSection.js +97 -0
- package/dist/admin/sections/media/MediaUploader.d.ts +7 -0
- package/dist/admin/sections/media/MediaUploader.js +72 -0
- package/dist/admin/sections/media/types.d.ts +33 -0
- package/dist/admin/sections/media/types.js +1 -0
- package/dist/admin/styles.css +533 -0
- package/dist/admin/types.d.ts +85 -0
- package/dist/admin/types.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/react/CmsContent.d.ts +20 -0
- package/dist/react/CmsContent.js +31 -0
- package/dist/react/entries.d.ts +9 -0
- package/dist/react/entries.js +25 -0
- package/dist/react/fetch.d.ts +11 -0
- package/dist/react/fetch.js +32 -0
- package/dist/react/index.d.ts +10 -0
- package/dist/react/index.js +9 -0
- package/dist/react/metadata.d.ts +44 -0
- package/dist/react/metadata.js +142 -0
- package/dist/react/sanitize.d.ts +17 -0
- package/dist/react/sanitize.js +326 -0
- package/dist/react/server.d.ts +14 -0
- package/dist/react/server.js +13 -0
- package/dist/react/sitemap.d.ts +28 -0
- package/dist/react/sitemap.js +91 -0
- package/dist/react/slugs.d.ts +27 -0
- package/dist/react/slugs.js +52 -0
- package/dist/react/types.d.ts +85 -0
- package/dist/react/types.js +1 -0
- package/dist/react/visitor.d.ts +7 -0
- package/dist/react/visitor.js +18 -0
- package/dist/site-templates/BlogTemplates.d.ts +95 -0
- package/dist/site-templates/BlogTemplates.js +64 -0
- package/dist/site-templates/ContactPageTemplate.d.ts +14 -0
- package/dist/site-templates/ContactPageTemplate.js +5 -0
- package/dist/site-templates/DashboardOverviewTemplate.d.ts +29 -0
- package/dist/site-templates/DashboardOverviewTemplate.js +17 -0
- package/dist/site-templates/DashboardShell.d.ts +28 -0
- package/dist/site-templates/DashboardShell.js +10 -0
- package/dist/site-templates/DocsSidebar.d.ts +14 -0
- package/dist/site-templates/DocsSidebar.js +13 -0
- package/dist/site-templates/DocsTemplates.d.ts +60 -0
- package/dist/site-templates/DocsTemplates.js +47 -0
- package/dist/site-templates/HomePageTemplate.d.ts +15 -0
- package/dist/site-templates/HomePageTemplate.js +10 -0
- package/dist/site-templates/LegalPageTemplate.d.ts +12 -0
- package/dist/site-templates/LegalPageTemplate.js +6 -0
- package/dist/site-templates/MarkdownContent.d.ts +7 -0
- package/dist/site-templates/MarkdownContent.js +24 -0
- package/dist/site-templates/NotFoundTemplate.d.ts +9 -0
- package/dist/site-templates/NotFoundTemplate.js +5 -0
- package/dist/site-templates/SiteFooter.d.ts +13 -0
- package/dist/site-templates/SiteFooter.js +4 -0
- package/dist/site-templates/SiteLayout.d.ts +14 -0
- package/dist/site-templates/SiteLayout.js +6 -0
- package/dist/site-templates/TopNav.d.ts +10 -0
- package/dist/site-templates/TopNav.js +8 -0
- package/dist/site-templates/blogControls.d.ts +19 -0
- package/dist/site-templates/blogControls.js +37 -0
- package/dist/site-templates/codeBlock.d.ts +9 -0
- package/dist/site-templates/codeBlock.js +31 -0
- package/dist/site-templates/content-styles.css +410 -0
- package/dist/site-templates/contentIndex.d.ts +65 -0
- package/dist/site-templates/contentIndex.js +181 -0
- package/dist/site-templates/contentUi.d.ts +14 -0
- package/dist/site-templates/contentUi.js +24 -0
- package/dist/site-templates/docs-styles.css +259 -0
- package/dist/site-templates/docsNavigation.d.ts +18 -0
- package/dist/site-templates/docsNavigation.js +50 -0
- package/dist/site-templates/index.d.ts +28 -0
- package/dist/site-templates/index.js +25 -0
- package/dist/site-templates/monetization-styles.css +154 -0
- package/dist/site-templates/paywallControls.d.ts +22 -0
- package/dist/site-templates/paywallControls.js +9 -0
- package/dist/site-templates/routing.d.ts +12 -0
- package/dist/site-templates/routing.js +36 -0
- package/dist/site-templates/solanaAtaSetup.d.ts +11 -0
- package/dist/site-templates/solanaAtaSetup.js +38 -0
- package/dist/site-templates/solanaMicropayments.d.ts +65 -0
- package/dist/site-templates/solanaMicropayments.js +115 -0
- package/dist/site-templates/styles.css +332 -0
- package/dist/site-templates/tipControls.d.ts +24 -0
- package/dist/site-templates/tipControls.js +43 -0
- package/dist/site-templates/tocExtractor.d.ts +16 -0
- package/dist/site-templates/tocExtractor.js +58 -0
- package/dist/site-templates/tocScrollSpy.d.ts +16 -0
- package/dist/site-templates/tocScrollSpy.js +37 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +20 -0
- package/package.json +58 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { requestJson } from "../api.js";
|
|
4
|
+
import { AdminButton, Card, JsonEditor, SelectInput, StatusNotice, TextInput } from "../components.js";
|
|
5
|
+
const RESERVED_COLLECTIONS = new Set(["pages", "navigation", "site_settings"]);
|
|
6
|
+
export default function CollectionsSection({ pluginContext }) {
|
|
7
|
+
const canWriteCollections = pluginContext.hasPermission("data:collections:write");
|
|
8
|
+
const [collections, setCollections] = useState([]);
|
|
9
|
+
const [selectedCollectionName, setSelectedCollectionName] = useState("");
|
|
10
|
+
const [collectionName, setCollectionName] = useState("");
|
|
11
|
+
const [mode, setMode] = useState("jsonb");
|
|
12
|
+
const [tableName, setTableName] = useState("");
|
|
13
|
+
const [strictContractJson, setStrictContractJson] = useState("");
|
|
14
|
+
const [status, setStatus] = useState("");
|
|
15
|
+
const [tone, setTone] = useState("neutral");
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const reservedCollection = RESERVED_COLLECTIONS.has(collectionName.trim());
|
|
18
|
+
const load = useCallback(async () => {
|
|
19
|
+
setLoading(true);
|
|
20
|
+
setStatus("");
|
|
21
|
+
setTone("neutral");
|
|
22
|
+
try {
|
|
23
|
+
const data = await requestJson(pluginContext, "/admin/collections");
|
|
24
|
+
setCollections(data);
|
|
25
|
+
setSelectedCollectionName((current) => {
|
|
26
|
+
if (current && data.some((collection) => collection.collection_name === current)) {
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
return data[0]?.collection_name || "";
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
setStatus(error.message);
|
|
34
|
+
setTone("error");
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
}, [pluginContext]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
void load();
|
|
42
|
+
}, [load]);
|
|
43
|
+
const submit = useCallback(async () => {
|
|
44
|
+
if (!canWriteCollections) {
|
|
45
|
+
setStatus("You do not have permission to edit collections.");
|
|
46
|
+
setTone("error");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const name = collectionName.trim();
|
|
50
|
+
if (!name) {
|
|
51
|
+
setStatus("Collection name is required.");
|
|
52
|
+
setTone("error");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (RESERVED_COLLECTIONS.has(name)) {
|
|
56
|
+
setStatus(`Collection "${name}" is reserved and cannot be reconfigured.`);
|
|
57
|
+
setTone("error");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (mode === "typed" && !tableName.trim()) {
|
|
61
|
+
setStatus("Typed collections require a table name.");
|
|
62
|
+
setTone("error");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let strictContract;
|
|
66
|
+
if (strictContractJson.trim()) {
|
|
67
|
+
try {
|
|
68
|
+
strictContract = JSON.parse(strictContractJson);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
setStatus(`Invalid strict contract JSON: ${error.message}`);
|
|
72
|
+
setTone("error");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
setLoading(true);
|
|
77
|
+
setStatus("");
|
|
78
|
+
setTone("neutral");
|
|
79
|
+
try {
|
|
80
|
+
await requestJson(pluginContext, "/admin/collections", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: {
|
|
83
|
+
collection_name: name,
|
|
84
|
+
mode,
|
|
85
|
+
table_name: mode === "typed" ? tableName.trim() || undefined : undefined,
|
|
86
|
+
strict_contract: strictContract
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
setStatus(`Collection "${name}" registered.`);
|
|
90
|
+
setTone("success");
|
|
91
|
+
setSelectedCollectionName(name);
|
|
92
|
+
setCollectionName("");
|
|
93
|
+
setTableName("");
|
|
94
|
+
setStrictContractJson("");
|
|
95
|
+
await load();
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
setStatus(error.message);
|
|
99
|
+
setTone("error");
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
setLoading(false);
|
|
103
|
+
}
|
|
104
|
+
}, [
|
|
105
|
+
canWriteCollections,
|
|
106
|
+
collectionName,
|
|
107
|
+
load,
|
|
108
|
+
mode,
|
|
109
|
+
pluginContext,
|
|
110
|
+
strictContractJson,
|
|
111
|
+
tableName
|
|
112
|
+
]);
|
|
113
|
+
const selectCollection = useCallback((collection) => {
|
|
114
|
+
setSelectedCollectionName(collection.collection_name);
|
|
115
|
+
setCollectionName(collection.collection_name);
|
|
116
|
+
setMode(collection.mode);
|
|
117
|
+
setTableName(collection.table_name ?? "");
|
|
118
|
+
setStrictContractJson(collection.strict_contract ? JSON.stringify(collection.strict_contract, null, 2) : "");
|
|
119
|
+
}, []);
|
|
120
|
+
return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Collections" }), _jsx("p", { className: "cedros-data__subtitle", children: "Manage the collection schemas for this deployment." }), reservedCollection && (_jsx("p", { className: "cedros-data__subtitle", children: "Reserved collections stay JSONB-backed and are not editable here." })), !canWriteCollections && (_jsx("p", { className: "cedros-data__subtitle", children: "Read-only mode. Missing `data:collections:write` permission." }))] }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "secondary", onClick: () => void load(), disabled: loading, children: "Refresh" }), _jsx(AdminButton, { variant: "primary", onClick: () => void submit(), disabled: loading || !canWriteCollections || reservedCollection, title: !canWriteCollections
|
|
121
|
+
? "Requires data:collections:write"
|
|
122
|
+
: reservedCollection
|
|
123
|
+
? "Reserved collections are managed by bootstrap defaults"
|
|
124
|
+
: undefined, children: "Save Collection" })] })] }), _jsxs("div", { className: "cedros-data-grid cedros-data-grid--two", children: [_jsx(Card, { title: `Registered Collections (${collections.length})`, subtitle: "Current collection registrations and storage modes.", children: _jsxs("div", { className: "cedros-data-list", children: [collections.length === 0 && _jsx("div", { className: "cedros-data-empty", children: "No collections yet." }), collections.map((collection) => (_jsxs("button", { type: "button", className: `cedros-data-list-item ${collection.collection_name === selectedCollectionName ? "cedros-data-list-item--active" : ""}`, onClick: () => selectCollection(collection), children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: "0.5rem" }, children: [_jsx("strong", { children: collection.collection_name }), _jsx("span", { className: "cedros-data-pill", children: collection.mode })] }), _jsxs("div", { className: "cedros-data-key", children: ["table: ", collection.table_name || "entries(jsonb)", " \u2022 updated:", " ", new Date(collection.updated_at).toLocaleString()] })] }, collection.collection_name)))] }) }), _jsxs(Card, { title: "Register / Update Collection", subtitle: "Select an existing collection to edit or enter a new name.", children: [_jsx(TextInput, { label: "Collection Name", value: collectionName, onChange: (event) => setCollectionName(event.target.value), placeholder: "articles", disabled: !canWriteCollections }), _jsxs(SelectInput, { label: "Mode", value: mode, onChange: (event) => setMode(event.target.value), disabled: !canWriteCollections || reservedCollection, children: [_jsx("option", { value: "jsonb", children: "jsonb" }), _jsx("option", { value: "typed", children: "typed" })] }), mode === "typed" && (_jsx(TextInput, { label: "Typed Table Name", value: tableName, onChange: (event) => setTableName(event.target.value), placeholder: "articles", disabled: !canWriteCollections || reservedCollection })), _jsx(JsonEditor, { label: "Strict Contract (optional)", value: strictContractJson, onChange: (event) => setStrictContractJson(event.target.value), placeholder: '{"fields":[{"path":"title","required":true,"types":["string"]}]}', disabled: !canWriteCollections || reservedCollection })] })] }), status && _jsx(StatusNotice, { tone: tone, message: status })] }));
|
|
125
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PluginContext } from "../types.js";
|
|
2
|
+
import type { AdminSectionProps } from "../types.js";
|
|
3
|
+
interface CollectionRecord {
|
|
4
|
+
collection_name: string;
|
|
5
|
+
}
|
|
6
|
+
export default function ContractVerifySection({ pluginContext }: AdminSectionProps): React.JSX.Element;
|
|
7
|
+
export declare function loadContractVerifyCollections(pluginContext: PluginContext): Promise<{
|
|
8
|
+
collections: CollectionRecord[];
|
|
9
|
+
errorMessage: string | null;
|
|
10
|
+
}>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { requestJson } from "../api.js";
|
|
4
|
+
import { AdminButton, Card, JsonEditor, StatusNotice, TextInput } from "../components.js";
|
|
5
|
+
const SAMPLE_JSON_PLACEHOLDER = JSON.stringify([
|
|
6
|
+
{
|
|
7
|
+
title: "Welcome",
|
|
8
|
+
body: "Hello world"
|
|
9
|
+
}
|
|
10
|
+
], null, 2);
|
|
11
|
+
export default function ContractVerifySection({ pluginContext }) {
|
|
12
|
+
const [collections, setCollections] = useState([]);
|
|
13
|
+
const [collectionsLoadError, setCollectionsLoadError] = useState("");
|
|
14
|
+
const [collectionName, setCollectionName] = useState("");
|
|
15
|
+
const [samplesEditor, setSamplesEditor] = useState(SAMPLE_JSON_PLACEHOLDER);
|
|
16
|
+
const [report, setReport] = useState(null);
|
|
17
|
+
const [status, setStatus] = useState("");
|
|
18
|
+
const [tone, setTone] = useState("neutral");
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const loadCollections = useCallback(async () => {
|
|
21
|
+
const result = await loadContractVerifyCollections(pluginContext);
|
|
22
|
+
setCollections(result.collections);
|
|
23
|
+
setCollectionsLoadError(result.errorMessage ?? "");
|
|
24
|
+
setCollectionName((current) => {
|
|
25
|
+
if (current &&
|
|
26
|
+
(result.collections.some((collection) => collection.collection_name === current) ||
|
|
27
|
+
result.collections.length === 0)) {
|
|
28
|
+
return current;
|
|
29
|
+
}
|
|
30
|
+
return result.collections[0]?.collection_name || "";
|
|
31
|
+
});
|
|
32
|
+
}, [pluginContext]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
void loadCollections();
|
|
35
|
+
}, [loadCollections]);
|
|
36
|
+
const runVerify = useCallback(async () => {
|
|
37
|
+
const collection = collectionName.trim();
|
|
38
|
+
if (!collection) {
|
|
39
|
+
setStatus("Collection name is required.");
|
|
40
|
+
setTone("error");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let samples;
|
|
44
|
+
try {
|
|
45
|
+
samples = JSON.parse(samplesEditor);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
setStatus(`Invalid samples JSON: ${error.message}`);
|
|
49
|
+
setTone("error");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const normalizedSamples = Array.isArray(samples) ? samples : [samples];
|
|
53
|
+
if (normalizedSamples.length === 0) {
|
|
54
|
+
setStatus("Provide at least one sample object.");
|
|
55
|
+
setTone("error");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setLoading(true);
|
|
59
|
+
setStatus("");
|
|
60
|
+
setTone("neutral");
|
|
61
|
+
try {
|
|
62
|
+
const nextReport = await requestJson(pluginContext, "/contract/verify", {
|
|
63
|
+
method: "POST",
|
|
64
|
+
body: {
|
|
65
|
+
collection_name: collection,
|
|
66
|
+
samples: normalizedSamples
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
setReport(nextReport);
|
|
70
|
+
setStatus(nextReport.passes
|
|
71
|
+
? "Contract check passed (additive-only or identical)."
|
|
72
|
+
: "Breaking changes detected.");
|
|
73
|
+
setTone(nextReport.passes ? "success" : "error");
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
setStatus(error.message);
|
|
77
|
+
setTone("error");
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
setLoading(false);
|
|
81
|
+
}
|
|
82
|
+
}, [collectionName, pluginContext, samplesEditor]);
|
|
83
|
+
return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Contract Verify" }), _jsx("p", { className: "cedros-data__subtitle", children: "Compare incoming JSON shapes against stored collection contracts." })] }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "secondary", onClick: () => void loadCollections(), disabled: loading, children: "Refresh Collections" }), _jsx(AdminButton, { variant: "primary", onClick: () => void runVerify(), disabled: loading, children: "Run Verify" })] })] }), _jsxs("div", { className: "cedros-data-grid cedros-data-grid--two", children: [_jsxs(Card, { title: "Input", subtitle: "Compare sample payloads against the current collection contract.", children: [collectionsLoadError && _jsx(StatusNotice, { tone: "error", message: collectionsLoadError }), collections.length > 0 ? (_jsxs("label", { className: "cedros-data-field", children: [_jsx("span", { className: "cedros-data-label", children: "Collection Name" }), _jsx("select", { className: "cedros-data-select", value: collectionName, onChange: (event) => setCollectionName(event.target.value), children: collections.map((collection) => (_jsx("option", { value: collection.collection_name, children: collection.collection_name }, collection.collection_name))) })] })) : (_jsx(TextInput, { label: "Collection Name", value: collectionName, onChange: (event) => setCollectionName(event.target.value), placeholder: "articles" })), _jsx(JsonEditor, { label: "Sample JSON (array or object)", value: samplesEditor, onChange: (event) => setSamplesEditor(event.target.value) })] }), _jsx(Card, { title: "Result", subtitle: "Additive and breaking changes from verification.", children: report ? (_jsxs("div", { style: { display: "grid", gap: "0.8rem" }, children: [_jsx("div", { className: "cedros-data-pill", children: report.passes ? "PASS: additive-only" : "FAIL: breaking changes" }), _jsxs("div", { children: [_jsx("strong", { children: "Additive" }), report.additive_changes.length === 0 && _jsx("div", { className: "cedros-data-empty", children: "None." }), report.additive_changes.map((item) => (_jsxs("div", { className: "cedros-data-key", children: ["\u2022 ", item] }, item)))] }), _jsxs("div", { children: [_jsx("strong", { children: "Breaking" }), report.breaking_changes.length === 0 && _jsx("div", { className: "cedros-data-empty", children: "None." }), report.breaking_changes.map((item) => (_jsxs("div", { className: "cedros-data-key", style: { color: "var(--cd-error)" }, children: ["\u2022 ", item] }, item)))] }), _jsx(JsonEditor, { label: "Stored Contract", value: JSON.stringify(report.stored_contract ?? {}, null, 2), readOnly: true }), _jsx(JsonEditor, { label: "Incoming Contract", value: JSON.stringify(report.incoming_contract ?? {}, null, 2), readOnly: true })] })) : (_jsx("div", { className: "cedros-data-empty", children: "Run verification to see a report." })) })] }), status && _jsx(StatusNotice, { tone: tone, message: status })] }));
|
|
84
|
+
}
|
|
85
|
+
export async function loadContractVerifyCollections(pluginContext) {
|
|
86
|
+
try {
|
|
87
|
+
return {
|
|
88
|
+
collections: await requestJson(pluginContext, "/admin/collections"),
|
|
89
|
+
errorMessage: null
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return {
|
|
94
|
+
collections: [],
|
|
95
|
+
errorMessage: `Unable to load collections automatically: ${error.message}. Enter a collection name manually.`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { createLatestRequestController, requestJson } from "../api.js";
|
|
4
|
+
import { AdminButton, Card, JsonEditor, SelectInput, StatusNotice, TextInput } from "../components.js";
|
|
5
|
+
import { readPermissionForCollection, writePermissionForCollection } from "../permissions.js";
|
|
6
|
+
export default function CustomDataSection({ pluginContext, pageSize }) {
|
|
7
|
+
const canReadCollections = pluginContext.hasPermission("data:collections:read") ||
|
|
8
|
+
pluginContext.hasPermission("data:custom:read");
|
|
9
|
+
const canCreateCollections = pluginContext.hasPermission("data:collections:write");
|
|
10
|
+
const canWriteCustomEntries = pluginContext.hasPermission("data:custom:write");
|
|
11
|
+
const queryLimit = Math.max(1, pageSize);
|
|
12
|
+
const [collections, setCollections] = useState([]);
|
|
13
|
+
const [selectedCollection, setSelectedCollection] = useState("");
|
|
14
|
+
const [newCollectionName, setNewCollectionName] = useState("");
|
|
15
|
+
const [containsEditor, setContainsEditor] = useState("");
|
|
16
|
+
const [offset, setOffset] = useState(0);
|
|
17
|
+
const [entries, setEntries] = useState([]);
|
|
18
|
+
const [entryKey, setEntryKey] = useState("");
|
|
19
|
+
const [payloadEditor, setPayloadEditor] = useState("{}");
|
|
20
|
+
const [loading, setLoading] = useState(false);
|
|
21
|
+
const [status, setStatus] = useState("");
|
|
22
|
+
const [statusTone, setStatusTone] = useState("neutral");
|
|
23
|
+
const entriesRequestControllerRef = useRef(createLatestRequestController());
|
|
24
|
+
const setError = useCallback((error) => {
|
|
25
|
+
setStatus(error.message);
|
|
26
|
+
setStatusTone("error");
|
|
27
|
+
}, []);
|
|
28
|
+
const loadCollections = useCallback(async () => {
|
|
29
|
+
if (!canReadCollections) {
|
|
30
|
+
throw new Error("You do not have permission to view collections.");
|
|
31
|
+
}
|
|
32
|
+
const data = await requestJson(pluginContext, "/admin/collections");
|
|
33
|
+
setCollections(data);
|
|
34
|
+
setSelectedCollection((current) => {
|
|
35
|
+
if (current && data.some((collection) => collection.collection_name === current)) {
|
|
36
|
+
return current;
|
|
37
|
+
}
|
|
38
|
+
return data[0]?.collection_name || "";
|
|
39
|
+
});
|
|
40
|
+
}, [canReadCollections, pluginContext]);
|
|
41
|
+
const loadEntries = useCallback(async () => {
|
|
42
|
+
if (!selectedCollection) {
|
|
43
|
+
entriesRequestControllerRef.current.cancel();
|
|
44
|
+
setEntries([]);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const requiredReadPermission = readPermissionForCollection(selectedCollection);
|
|
48
|
+
if (!pluginContext.hasPermission(requiredReadPermission)) {
|
|
49
|
+
entriesRequestControllerRef.current.cancel();
|
|
50
|
+
setEntries([]);
|
|
51
|
+
setStatus(`You do not have permission to view entries in ${selectedCollection}.`);
|
|
52
|
+
setStatusTone("error");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let contains = null;
|
|
56
|
+
if (containsEditor.trim()) {
|
|
57
|
+
try {
|
|
58
|
+
contains = JSON.parse(containsEditor);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
entriesRequestControllerRef.current.cancel();
|
|
62
|
+
setStatus(`Invalid contains filter JSON: ${error.message}`);
|
|
63
|
+
setStatusTone("error");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const request = entriesRequestControllerRef.current.begin();
|
|
68
|
+
try {
|
|
69
|
+
const data = await requestJson(pluginContext, "/entries/query", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
body: {
|
|
72
|
+
collection_name: selectedCollection,
|
|
73
|
+
entry_keys: [],
|
|
74
|
+
contains,
|
|
75
|
+
limit: queryLimit,
|
|
76
|
+
offset
|
|
77
|
+
},
|
|
78
|
+
signal: request.signal
|
|
79
|
+
});
|
|
80
|
+
if (!request.isCurrent()) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
setEntries(data);
|
|
84
|
+
if (data[0]) {
|
|
85
|
+
setEntryKey((current) => current || data[0].entry_key);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (isAbortError(error)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}, [containsEditor, offset, pluginContext, queryLimit, selectedCollection]);
|
|
95
|
+
const loadAll = useCallback(async () => {
|
|
96
|
+
setLoading(true);
|
|
97
|
+
setStatus("");
|
|
98
|
+
setStatusTone("neutral");
|
|
99
|
+
try {
|
|
100
|
+
await loadCollections();
|
|
101
|
+
await loadEntries();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
setError(error);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
setLoading(false);
|
|
108
|
+
}
|
|
109
|
+
}, [loadCollections, loadEntries, setError]);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
void loadAll();
|
|
112
|
+
}, [loadAll]);
|
|
113
|
+
useEffect(() => () => {
|
|
114
|
+
entriesRequestControllerRef.current.cancel();
|
|
115
|
+
}, []);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const selectedEntry = entries.find((entry) => entry.entry_key === entryKey);
|
|
118
|
+
if (!selectedEntry) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
setPayloadEditor(JSON.stringify(selectedEntry.payload, null, 2));
|
|
122
|
+
}, [entries, entryKey]);
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!selectedCollection) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
void loadEntries().catch(setError);
|
|
128
|
+
}, [loadEntries, selectedCollection]);
|
|
129
|
+
const createCollection = useCallback(async () => {
|
|
130
|
+
if (!canCreateCollections) {
|
|
131
|
+
setStatus("You do not have permission to create collections.");
|
|
132
|
+
setStatusTone("error");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const trimmed = newCollectionName.trim();
|
|
136
|
+
if (!trimmed) {
|
|
137
|
+
setStatus("Collection name is required.");
|
|
138
|
+
setStatusTone("error");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
setLoading(true);
|
|
142
|
+
setStatus("");
|
|
143
|
+
setStatusTone("neutral");
|
|
144
|
+
try {
|
|
145
|
+
await requestJson(pluginContext, "/admin/collections", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
body: {
|
|
148
|
+
collection_name: trimmed,
|
|
149
|
+
mode: "jsonb"
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
setNewCollectionName("");
|
|
153
|
+
setSelectedCollection(trimmed);
|
|
154
|
+
setStatus(`Collection "${trimmed}" ready.`);
|
|
155
|
+
setStatusTone("success");
|
|
156
|
+
await loadCollections();
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
setError(error);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
setLoading(false);
|
|
163
|
+
}
|
|
164
|
+
}, [canCreateCollections, loadCollections, newCollectionName, pluginContext, setError]);
|
|
165
|
+
const saveEntry = useCallback(async () => {
|
|
166
|
+
const key = entryKey.trim();
|
|
167
|
+
if (!selectedCollection) {
|
|
168
|
+
setStatus("Select a collection first.");
|
|
169
|
+
setStatusTone("error");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const requiredWritePermission = writePermissionForCollection(selectedCollection);
|
|
173
|
+
if (!pluginContext.hasPermission(requiredWritePermission)) {
|
|
174
|
+
setStatus(`You do not have permission to edit ${selectedCollection}.`);
|
|
175
|
+
setStatusTone("error");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!key) {
|
|
179
|
+
setStatus("Entry key is required.");
|
|
180
|
+
setStatusTone("error");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
let payload;
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(payloadEditor);
|
|
186
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
187
|
+
throw new Error("payload must be a JSON object");
|
|
188
|
+
}
|
|
189
|
+
payload = parsed;
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
setStatus(`Invalid JSON: ${error.message}`);
|
|
193
|
+
setStatusTone("error");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
setLoading(true);
|
|
197
|
+
setStatus("");
|
|
198
|
+
setStatusTone("neutral");
|
|
199
|
+
try {
|
|
200
|
+
await requestJson(pluginContext, "/entries/upsert", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
body: {
|
|
203
|
+
collection_name: selectedCollection,
|
|
204
|
+
entry_key: key,
|
|
205
|
+
payload
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
setStatus(`Saved ${selectedCollection}/${key}.`);
|
|
209
|
+
setStatusTone("success");
|
|
210
|
+
await loadEntries();
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
setError(error);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
setLoading(false);
|
|
217
|
+
}
|
|
218
|
+
}, [
|
|
219
|
+
entryKey,
|
|
220
|
+
loadEntries,
|
|
221
|
+
payloadEditor,
|
|
222
|
+
pluginContext,
|
|
223
|
+
selectedCollection,
|
|
224
|
+
setError
|
|
225
|
+
]);
|
|
226
|
+
const selectedReadPermission = selectedCollection
|
|
227
|
+
? readPermissionForCollection(selectedCollection)
|
|
228
|
+
: null;
|
|
229
|
+
const selectedWritePermission = selectedCollection
|
|
230
|
+
? writePermissionForCollection(selectedCollection)
|
|
231
|
+
: null;
|
|
232
|
+
const canReadSelectedCollection = selectedReadPermission === null || pluginContext.hasPermission(selectedReadPermission);
|
|
233
|
+
const canWriteSelectedCollection = selectedWritePermission !== null && pluginContext.hasPermission(selectedWritePermission);
|
|
234
|
+
const collectionOptions = useMemo(() => collections.map((collection) => (_jsxs("option", { value: collection.collection_name, children: [collection.collection_name, " (", collection.mode, ")"] }, collection.collection_name))), [collections]);
|
|
235
|
+
return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Custom Data" }), _jsx("p", { className: "cedros-data__subtitle", children: "Manage collection entries for this deployment." }), (!canCreateCollections || !canWriteCustomEntries) && (_jsx("p", { className: "cedros-data__subtitle", children: "Read-only mode. Creating collections requires `data:collections:write`; editing custom entries requires `data:custom:write`." }))] }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "secondary", onClick: () => void loadEntries(), disabled: loading || !selectedCollection || !canReadSelectedCollection, title: !selectedCollection || canReadSelectedCollection || selectedReadPermission === null
|
|
236
|
+
? undefined
|
|
237
|
+
: `Requires ${selectedReadPermission}`, children: "Refresh Entries" }), _jsx(AdminButton, { variant: "primary", onClick: () => void saveEntry(), disabled: loading || !selectedCollection || !canWriteSelectedCollection, title: !selectedCollection || canWriteSelectedCollection || selectedWritePermission === null
|
|
238
|
+
? undefined
|
|
239
|
+
: `Requires ${selectedWritePermission}`, children: "Save Entry" })] })] }), _jsxs("div", { className: "cedros-data-grid cedros-data-grid--page", children: [_jsxs(Card, { title: "Collections", subtitle: "Create JSONB collections and browse existing entries.", children: [_jsx(SelectInput, { label: "Collection", value: selectedCollection, onChange: (event) => {
|
|
240
|
+
setSelectedCollection(event.target.value);
|
|
241
|
+
setEntryKey("");
|
|
242
|
+
setOffset(0);
|
|
243
|
+
}, disabled: loading || collections.length === 0, children: collectionOptions }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(TextInput, { label: "New Collection", value: newCollectionName, onChange: (event) => setNewCollectionName(event.target.value), placeholder: "blog_posts", disabled: !canCreateCollections }), _jsx(AdminButton, { variant: "secondary", onClick: () => void createCollection(), disabled: loading || !canCreateCollections, title: !canCreateCollections ? "Requires data:collections:write" : undefined, children: "Create" })] }), _jsx(JsonEditor, { label: "Contains Filter (optional JSON)", value: containsEditor, onChange: (event) => setContainsEditor(event.target.value), placeholder: '{"status":"published"}' }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "secondary", size: "sm", onClick: () => {
|
|
244
|
+
setOffset((current) => Math.max(0, current - queryLimit));
|
|
245
|
+
setEntryKey("");
|
|
246
|
+
}, disabled: loading || offset === 0, children: "Prev" }), _jsx(AdminButton, { variant: "secondary", size: "sm", onClick: () => {
|
|
247
|
+
setOffset((current) => current + queryLimit);
|
|
248
|
+
setEntryKey("");
|
|
249
|
+
}, disabled: loading || entries.length < queryLimit, children: "Next" }), _jsxs("span", { className: "cedros-data-key", children: ["offset: ", offset] })] }), _jsxs("div", { className: "cedros-data-list", children: [entries.length === 0 && _jsx("div", { className: "cedros-data-empty", children: "No entries in this collection." }), entries.map((entry) => (_jsx("button", { className: `cedros-data-list-item ${entry.entry_key === entryKey ? "cedros-data-list-item--active" : ""}`, onClick: () => {
|
|
250
|
+
setEntryKey(entry.entry_key);
|
|
251
|
+
setPayloadEditor(JSON.stringify(entry.payload, null, 2));
|
|
252
|
+
}, children: entry.entry_key }, entry.entry_key)))] })] }), _jsxs(Card, { title: selectedCollection ? `Entry Editor (${selectedCollection})` : "Entry Editor", subtitle: "Edit JSON payload for the selected entry key.", children: [_jsx(TextInput, { label: "Entry Key", value: entryKey, onChange: (event) => setEntryKey(event.target.value), placeholder: "homepage_hero", disabled: !selectedCollection || !canWriteSelectedCollection }), _jsx(JsonEditor, { label: "Entry Payload", value: payloadEditor, onChange: (event) => setPayloadEditor(event.target.value), disabled: !selectedCollection || !canWriteSelectedCollection })] })] }), status && _jsx(StatusNotice, { tone: statusTone, message: status })] }));
|
|
253
|
+
}
|
|
254
|
+
function isAbortError(error) {
|
|
255
|
+
return error instanceof Error && error.name === "AbortError";
|
|
256
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AdminSectionProps } from "../types.js";
|
|
2
|
+
interface SiteExport {
|
|
3
|
+
site?: unknown;
|
|
4
|
+
collections?: unknown[];
|
|
5
|
+
custom_schema?: unknown;
|
|
6
|
+
contracts?: unknown[];
|
|
7
|
+
entries?: unknown[];
|
|
8
|
+
}
|
|
9
|
+
export default function DataOpsSection({ pluginContext }: AdminSectionProps): React.JSX.Element;
|
|
10
|
+
interface ParsedSiteExport {
|
|
11
|
+
value: SiteExport | null;
|
|
12
|
+
summary: HistoryArtifactSummary | null;
|
|
13
|
+
error: string | null;
|
|
14
|
+
}
|
|
15
|
+
interface HistoryArtifactSummary {
|
|
16
|
+
displayName: string;
|
|
17
|
+
collections: number;
|
|
18
|
+
contracts: number;
|
|
19
|
+
entries: number;
|
|
20
|
+
hasCustomSchema: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function parseSiteExport(raw: string): ParsedSiteExport;
|
|
23
|
+
export declare function isImportPayloadPending(raw: string, deferredRaw: string): boolean;
|
|
24
|
+
export declare function resolveImportPayload(raw: string, deferredRaw: string, deferredParsed: ParsedSiteExport): ParsedSiteExport;
|
|
25
|
+
export declare function formatHistoryArtifact(operation: "export" | "import", summary: HistoryArtifactSummary, overwriteContracts?: boolean): string;
|
|
26
|
+
export {};
|