|
|
@@ -79,6 +79,8 @@ export default function DatabasePage() {
|
|
|
const [showNewCollectionModal, setShowNewCollectionModal] = useState(false)
|
|
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
|
|
const [documentsToDelete, setDocumentsToDelete] = useState<string[]>([])
|
|
|
+ const [sortField, setSortField] = useState<'_updated_at' | '_created_at'>('_updated_at')
|
|
|
+ const [sortDescending, setSortDescending] = useState(true)
|
|
|
const [newCollectionName, setNewCollectionName] = useState('')
|
|
|
const [collectionSettings, setCollectionSettings] = useState<CollectionSettings>({})
|
|
|
const [showUnsavedWarning, setShowUnsavedWarning] = useState(false)
|
|
|
@@ -123,8 +125,10 @@ export default function DatabasePage() {
|
|
|
}, [collectionsData])
|
|
|
|
|
|
const { data: documentsData, isLoading: documentsLoading, error: documentsError } = useQuery({
|
|
|
- queryKey: ['database-documents', selectedCollection, currentPage],
|
|
|
- queryFn: () => databaseApi.getDocuments(selectedCollection!, currentPage, PAGE_SIZE),
|
|
|
+ queryKey: ['database-documents', selectedCollection, currentPage, sortField, sortDescending],
|
|
|
+ queryFn: () =>
|
|
|
+ databaseApi.getDocuments(selectedCollection!, currentPage, PAGE_SIZE, sortField,
|
|
|
+ sortDescending ? 'desc' : 'asc'),
|
|
|
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.
|
|
|
@@ -446,7 +450,24 @@ export default function DatabasePage() {
|
|
|
return { systemCollections: system, customCollections: custom }
|
|
|
}, [collections])
|
|
|
|
|
|
- // Filter documents by search term
|
|
|
+ // The database writes these in nanoseconds. Dividing blindly would be wrong
|
|
|
+ // for anything that ever wrote milliseconds, so the magnitude decides - the
|
|
|
+ // same rule the server uses.
|
|
|
+ const NANOSECOND_THRESHOLD = 100000000000000
|
|
|
+ const stampMs = (raw?: number) => {
|
|
|
+ if (!raw || raw <= 0) return 0
|
|
|
+ return raw > NANOSECOND_THRESHOLD ? Math.floor(raw / 1000000) : raw
|
|
|
+ }
|
|
|
+
|
|
|
+ // Newest first, by when it was last written.
|
|
|
+ //
|
|
|
+ // The list arrived in whatever order the database returned it, which for a
|
|
|
+ // collection of ten thousand executions means the useful rows are wherever
|
|
|
+ // they happen to be. Sorting is over the page the server sent, so it orders
|
|
|
+ // what is on screen rather than the whole collection.
|
|
|
+ // Order comes from the server, so the page is a page of the whole collection
|
|
|
+ // rather than the newest of whatever 25 rows arrived. The search below still
|
|
|
+ // narrows what is on screen - it is a filter over the page, and says so.
|
|
|
const filteredDocuments = documents.filter((doc) => {
|
|
|
if (!searchTerm) return true
|
|
|
const docStr = JSON.stringify(doc).toLowerCase()
|
|
|
@@ -634,10 +655,36 @@ export default function DatabasePage() {
|
|
|
type="text"
|
|
|
value={searchTerm}
|
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
|
- placeholder="Search documents..."
|
|
|
+ placeholder="Search this page..."
|
|
|
className="w-full pl-9 pr-3 py-1.5 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
|
/>
|
|
|
</div>
|
|
|
+ {/* Which stamp to order by, and which way. Newest-updated first by
|
|
|
+ default, because that is the row somebody has come to look at.
|
|
|
+ It orders the page the server sent, not the whole collection -
|
|
|
+ said here rather than left to be inferred from a page of 25
|
|
|
+ that does not start where the previous one ended. */}
|
|
|
+ <div className="mt-2 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
|
|
+ <span>Order by</span>
|
|
|
+ <select
|
|
|
+ value={sortField}
|
|
|
+ onChange={(e) => setSortField(e.target.value as '_updated_at' | '_created_at')}
|
|
|
+ className="px-2 py-1 border border-gray-300 dark:border-slate-600 rounded bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100"
|
|
|
+ >
|
|
|
+ <option value="_updated_at">updated</option>
|
|
|
+ <option value="_created_at">created</option>
|
|
|
+ </select>
|
|
|
+ <button
|
|
|
+ onClick={() => setSortDescending((v) => !v)}
|
|
|
+ className="px-2 py-1 border border-gray-300 dark:border-slate-600 rounded hover:bg-gray-50 dark:hover:bg-slate-700"
|
|
|
+ title={sortDescending ? 'Newest first' : 'Oldest first'}
|
|
|
+ >
|
|
|
+ {sortDescending ? 'newest first' : 'oldest first'}
|
|
|
+ </button>
|
|
|
+ <span className="ml-auto text-gray-400 dark:text-gray-500">
|
|
|
+ whole collection
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
{/* Selection info bar */}
|
|
|
{selectedDocuments.size > 0 && (
|
|
|
@@ -687,6 +734,26 @@ export default function DatabasePage() {
|
|
|
{doc.name || doc.username || doc._id}
|
|
|
</div>
|
|
|
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">{doc._id}</div>
|
|
|
+ {/* When it was written and by whom, on the row itself. The
|
|
|
+ panel that showed this only appeared once a document
|
|
|
+ was opened, so a list of ten thousand executions gave
|
|
|
+ no way to tell which was recent without opening them
|
|
|
+ one at a time. */}
|
|
|
+ <div className="mt-0.5 flex items-center gap-2 text-[11px] text-gray-400 dark:text-gray-500">
|
|
|
+ {stampMs(doc._updated_at) > 0 && (
|
|
|
+ <span title={`Updated ${formatTimestamp(stampMs(doc._updated_at))}`}>
|
|
|
+ {formatTimestamp(stampMs(doc._updated_at))}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ {stampMs(doc._created_at) > 0 &&
|
|
|
+ stampMs(doc._created_at) !== stampMs(doc._updated_at) && (
|
|
|
+ <span title={`Created ${formatTimestamp(stampMs(doc._created_at))}`}>
|
|
|
+ · created {formatTimestamp(stampMs(doc._created_at))}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ {doc._version != null && <span>· v{doc._version}</span>}
|
|
|
+ {doc._updated_by && <span className="truncate">· {doc._updated_by}</span>}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</button>
|
|
|
))
|