|
|
@@ -40,19 +40,14 @@ interface Document {
|
|
|
[key: string]: any
|
|
|
}
|
|
|
|
|
|
-// System collections - predefined by the system (cannot be created by users)
|
|
|
-const SYSTEM_COLLECTIONS = [
|
|
|
- 'users',
|
|
|
- 'sessions',
|
|
|
- 'api_keys',
|
|
|
- 'credentials',
|
|
|
- 'collection_permissions',
|
|
|
- 'workflows',
|
|
|
- 'workflow_groups',
|
|
|
- 'executions',
|
|
|
- 'runners',
|
|
|
- 'nodes',
|
|
|
-]
|
|
|
+// Which collections belong to SmartBotic is the server's answer, not ours: it
|
|
|
+// arrives as `protection` on every collection in the list. This page used to
|
|
|
+// keep its own copy of the list, which is how "projects" came to be shown as an
|
|
|
+// ordinary collection long after the server had started protecting it - the
|
|
|
+// third such copy, after one in the runner and one in the webserver.
|
|
|
+//
|
|
|
+// A workflow's own storage is protected too, and is named for its workflow, so
|
|
|
+// no fixed list could have covered it in any case.
|
|
|
|
|
|
const PAGE_SIZE = 25
|
|
|
|
|
|
@@ -109,12 +104,40 @@ export default function DatabasePage() {
|
|
|
queryFn: () => databaseApi.listCollections(),
|
|
|
})
|
|
|
|
|
|
- const { data: documentsData, isLoading: documentsLoading } = useQuery({
|
|
|
+ // The server's verdict per collection, from the same list call. Anything it
|
|
|
+ // has not mentioned - a collection created a moment ago - is ordinary until
|
|
|
+ // it says otherwise. Declared here, above every use, because the create form
|
|
|
+ // consults it while the page is still rendering.
|
|
|
+ const protectionOf = useMemo(() => {
|
|
|
+ const byName: Record<string, 'none' | 'structural' | 'secret'> = {}
|
|
|
+ for (const detail of collectionsData?.details || []) {
|
|
|
+ byName[detail.name] = detail.protection || 'none'
|
|
|
+ }
|
|
|
+ // The server answers with the qualified "<project>:<collection>" name in
|
|
|
+ // some places and the bare one in others. Both are the same collection.
|
|
|
+ return (name: string) => {
|
|
|
+ const bare = name.includes(':') ? name.slice(name.lastIndexOf(':') + 1) : name
|
|
|
+ return byName[name] || byName[bare] ||
|
|
|
+ Object.entries(byName).find(([k]) => k.endsWith(':' + bare))?.[1] || 'none'
|
|
|
+ }
|
|
|
+ }, [collectionsData])
|
|
|
+
|
|
|
+ const { data: documentsData, isLoading: documentsLoading, error: documentsError } = useQuery({
|
|
|
queryKey: ['database-documents', selectedCollection, currentPage],
|
|
|
queryFn: () => databaseApi.getDocuments(selectedCollection!, currentPage, PAGE_SIZE),
|
|
|
enabled: !!selectedCollection,
|
|
|
+ // A collection the server will not open is not going to open on the third
|
|
|
+ // try either, and retrying a refusal only delays saying so.
|
|
|
+ retry: false,
|
|
|
})
|
|
|
|
|
|
+ // The server explains itself when it refuses; that explanation is what the
|
|
|
+ // reader needs, not a generic failure.
|
|
|
+ const documentsErrorMessage =
|
|
|
+ (documentsError as any)?.response?.data?.error ||
|
|
|
+ (documentsError as any)?.message ||
|
|
|
+ 'This collection could not be read.'
|
|
|
+
|
|
|
const createMutation = useMutation({
|
|
|
mutationFn: ({ collection, data }: { collection: string; data: Record<string, any> }) =>
|
|
|
databaseApi.createDocument(collection, data),
|
|
|
@@ -262,7 +285,7 @@ export default function DatabasePage() {
|
|
|
|
|
|
const createCollection = () => {
|
|
|
if (!newCollectionName.trim()) return
|
|
|
- if (SYSTEM_COLLECTIONS.includes(newCollectionName.toLowerCase())) return
|
|
|
+ if (protectionOf(newCollectionName.toLowerCase()) !== 'none') return
|
|
|
createCollectionMutation.mutate({
|
|
|
name: newCollectionName.trim(),
|
|
|
settings: collectionSettings,
|
|
|
@@ -409,7 +432,7 @@ export default function DatabasePage() {
|
|
|
const custom: string[] = []
|
|
|
|
|
|
collections.forEach(c => {
|
|
|
- if (SYSTEM_COLLECTIONS.includes(c)) {
|
|
|
+ if (protectionOf(c) !== 'none') {
|
|
|
system.push(c)
|
|
|
} else {
|
|
|
custom.push(c)
|
|
|
@@ -431,7 +454,7 @@ export default function DatabasePage() {
|
|
|
})
|
|
|
|
|
|
// Check if selected collection is a system collection
|
|
|
- const isSystemCollection = selectedCollection ? SYSTEM_COLLECTIONS.includes(selectedCollection) : false
|
|
|
+ const isSystemCollection = selectedCollection ? protectionOf(selectedCollection) !== 'none' : false
|
|
|
|
|
|
// Format timestamp for display (with safeguards for invalid values)
|
|
|
const formatTimestamp = (ts?: number) => {
|
|
|
@@ -668,6 +691,16 @@ export default function DatabasePage() {
|
|
|
</button>
|
|
|
))
|
|
|
) : (
|
|
|
+ documentsError ? (
|
|
|
+ // A refusal is not an empty collection. "sessions" holds 8,000
|
|
|
+ // live login sessions and the server answers 403 with a reason;
|
|
|
+ // rendering that as "No documents" told the exact opposite of
|
|
|
+ // what had happened.
|
|
|
+ <div className="px-3 py-4 text-sm text-center">
|
|
|
+ <Shield className="w-5 h-5 mx-auto mb-2 text-amber-500" />
|
|
|
+ <p className="text-gray-600 dark:text-gray-300">{documentsErrorMessage}</p>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
|
|
|
{searchTerm
|
|
|
? 'No matching documents'
|
|
|
@@ -675,6 +708,7 @@ export default function DatabasePage() {
|
|
|
? 'Created, but empty. The database does not list or return a collection until something writes to it, so this will not appear after a reload until it holds a document.'
|
|
|
: 'No documents'}
|
|
|
</p>
|
|
|
+ )
|
|
|
)}
|
|
|
</div>
|
|
|
{/* Pagination */}
|
|
|
@@ -1096,7 +1130,7 @@ export default function DatabasePage() {
|
|
|
Only letters, numbers, underscores, and hyphens allowed
|
|
|
</p>
|
|
|
</div>
|
|
|
- {SYSTEM_COLLECTIONS.includes(newCollectionName.toLowerCase()) && (
|
|
|
+ {protectionOf(newCollectionName.toLowerCase()) !== 'none' && (
|
|
|
<div className="p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-700 dark:text-red-400">
|
|
|
This name is reserved for system collections
|
|
|
</div>
|
|
|
@@ -1225,7 +1259,7 @@ export default function DatabasePage() {
|
|
|
</button>
|
|
|
<button
|
|
|
onClick={createCollection}
|
|
|
- disabled={!newCollectionName.trim() || SYSTEM_COLLECTIONS.includes(newCollectionName.toLowerCase()) || createCollectionMutation.isPending}
|
|
|
+ disabled={!newCollectionName.trim() || protectionOf(newCollectionName.toLowerCase()) !== 'none' || createCollectionMutation.isPending}
|
|
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
|
|
|
>
|
|
|
{createCollectionMutation.isPending ? 'Creating...' : 'Create Collection'}
|