Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/models/data_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

class DataDocumentsFilter(BaseModel):
key: str
value: Any # Can be str, int, etc. depending on the field type
value: Any = "" # Can be str, int, etc. depending on the field type
operator: Optional[str] = "equals" # 'equals', 'exists', 'not_exists'


class DataDocumentsRequest(BaseModel):
Expand All @@ -14,6 +15,7 @@ class DataDocumentsRequest(BaseModel):
page: int = 1
limit: int = 20
filter: Optional[DataDocumentsFilter] = None
filters: Optional[List[DataDocumentsFilter]] = None


class DataDocumentsResponse(BaseModel):
Expand Down
1 change: 1 addition & 0 deletions backend/routes/data_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def get_documents(
page=body.page,
limit=body.limit,
filter=body.filter.model_dump() if body.filter else None,
filters=[f.model_dump() for f in body.filters] if body.filters else None,
)


Expand Down
73 changes: 59 additions & 14 deletions backend/services/data_documents_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,40 +89,85 @@ def fetch_documents(
page: int,
limit: int,
filter: dict = None,
filters: list = None,
) -> DataDocumentsResponse:
client = MongoClient(connection_string)
db = client[database_name]
collection = db[collection_name]

query = {}
if filter and ("key" in filter) and ("value" in filter):
key = filter["key"]
value = filter["value"]
and_clauses = []

def get_query_for_filter(f: dict):
key = f.get("key")
value = f.get("value")
operator = f.get("operator", "equals")

if not key:
return {}

if operator == "exists":
return {key: {"$exists": True}}
if operator == "not_exists":
return {key: {"$exists": False}}

if value is None:
return {}

if key == "all":
sample_doc = collection.find_one()
if sample_doc:
or_clauses = []
for k, v in sample_doc.items():
if isinstance(v, str):
or_clauses.append(
{k: {"$regex": re.escape(value), "$options": "i"}}
{k: {"$regex": re.escape(str(value)), "$options": "i"}}
)
if or_clauses:
query = {"$or": or_clauses}
return {"$or": or_clauses}
return {}
else:
# If key is '_id', treat value as ObjectId
if key == "_id":
try:
query = {"_id": ObjectId(value)}
query_val = ObjectId(value)
except Exception:
# fallback to string match if not a valid ObjectId
query = {key: value}
query_val = value
else:
# Support dot notation for nested fields
if isinstance(value, str):
query = {key: {"$regex": re.escape(value), "$options": "i"}}
else:
query = {key: value}
query_val = value

if operator == "not_equals":
return {key: {"$ne": query_val}}
if operator == "greater_than":
return {key: {"$gt": query_val}}
if operator == "less_than":
return {key: {"$lt": query_val}}
if operator == "contains":
return {key: {"$regex": re.escape(str(value)), "$options": "i"}}

# default equals
if isinstance(query_val, str) and key != "_id":
return {key: {"$regex": re.escape(query_val), "$options": "i"}}
else:
return {key: query_val}

if filter and ("key" in filter) and ("value" in filter):
q = get_query_for_filter(filter)
if q:
and_clauses.append(q)

if filters:
for f in filters:
if ("key" in f) and ("value" in f):
q = get_query_for_filter(f)
if q:
and_clauses.append(q)

if and_clauses:
if len(and_clauses) == 1:
query = and_clauses[0]
else:
query = {"$and": and_clauses}

total_documents = collection.count_documents(query)
total_pages = max(1, (total_documents + limit - 1) // limit)
skip = (page - 1) * limit
Expand Down
73 changes: 73 additions & 0 deletions frontend/components/DiffOverwriteDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import ReactDiffViewer from 'react-diff-viewer-continued';
import { useTheme } from '../contexts/ThemeContext';
import { WarningIcon } from './icons/material-icons-imports';

interface DiffOverwriteDialogProps {
open: boolean;
oldValue: string; // The user's current edited value
newValue: string; // The newly fetched data from server
onClose: () => void;
onOverwrite: () => void;
}

const DiffOverwriteDialog: React.FC<DiffOverwriteDialogProps> = ({ open, oldValue, newValue, onClose, onOverwrite }) => {
const { theme } = useTheme();

if (!open) return null;

return (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/40 dark:bg-black/70 backdrop-blur-sm animate-fade-in-fast">
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-2xl max-w-5xl w-[90vw] flex flex-col overflow-hidden border border-slate-200 dark:border-slate-700 max-h-[90vh]">
<header className="px-6 py-4 border-b border-slate-200 dark:border-slate-700 bg-amber-50 dark:bg-amber-900/20 flex flex-col gap-1 flex-shrink-0">
<h2 className="text-xl font-bold text-amber-600 dark:text-amber-500 flex items-center gap-2">
<WarningIcon className="w-6 h-6" /> Refresh Will Overwrite Your Edits
</h2>
<p className="text-sm text-slate-700 dark:text-slate-300 ml-8">
The document on the server is different from what you are currently editing.
If you refresh, your current edits will be overwritten with the version from the server.
</p>
</header>

<div className="flex-1 overflow-auto p-4 bg-slate-50 dark:bg-slate-900">
<ReactDiffViewer
oldValue={oldValue}
newValue={newValue}
splitView={true}
useDarkTheme={theme === 'dark'}
leftTitle="Your Edits"
rightTitle="Server Document"
extraLinesSurroundingDiff={3}
styles={{
variables: {
light: { diffViewerBackground: '#fff', addedBackground: '#e6ffed', removedBackground: '#ffeef0' },
dark: { diffViewerBackground: '#1e293b', addedBackground: '#044B53', removedBackground: '#632F34', wordAddedBackground: '#055d67', wordRemovedBackground: '#7d3840' }
},
line: {
fontSize: '13px',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
}
}}
/>
</div>

<footer className="px-6 py-4 border-t border-slate-200 dark:border-slate-700 flex justify-end gap-3 flex-shrink-0 bg-white dark:bg-slate-800">
<button
onClick={onClose}
className="px-5 py-2.5 rounded-md border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors font-medium"
>
Cancel Refresh
</button>
<button
onClick={onOverwrite}
className="px-5 py-2.5 rounded-md border border-amber-500 bg-amber-500 text-white font-semibold hover:bg-amber-600 transition-colors flex items-center gap-2"
>
<WarningIcon className="w-5 h-5 text-amber-100" /> Overwrite My Edits
</button>
</footer>
</div>
</div>
);
};

export default DiffOverwriteDialog;
26 changes: 16 additions & 10 deletions frontend/components/DocumentDetailView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import { useState, forwardRef, useImperativeHandle } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import { Button, CircularProgress } from '@mui/material';
import MonacoEditor from '@monaco-editor/react';
Expand All @@ -16,25 +16,31 @@ interface DocumentEditViewProps {
onSave?: () => void | Promise<void>;
}

const DocumentEditView: React.FC<DocumentEditViewProps> = ({ accountId, databaseName, document, collection, docId, loading, onCancel, onSave }) => {
export interface DocumentEditViewRef {
getCurrentValue: () => string;
setCurrentValue: (val: string) => void;
}

const DocumentEditView = forwardRef<DocumentEditViewRef, DocumentEditViewProps>(({ accountId, databaseName, document, collection, docId, loading, onCancel, onSave }, ref) => {
const [jsonValue, setJsonValue] = useState(JSON.stringify(document, null, 2));
const [feedback, setFeedback] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
const { theme } = useTheme();
const [currentDoc, setCurrentDoc] = useState(document);
const [isSaving, setIsSaving] = useState(false);

useImperativeHandle(ref, () => ({
getCurrentValue: () => jsonValue,
setCurrentValue: (val: string) => setJsonValue(val)
}));

const handleSave = async () => {
setIsSaving(true);
try {
const parsed = JSON.parse(jsonValue);
if (!collection || !docId) throw new Error('Missing collection or document ID');
if (!accountId || !databaseName || !collection || !docId) throw new Error('Missing DB info');
await updateDocument(accountId, databaseName, collection, docId, parsed);
// Fetch the latest document after update
if (accountId && databaseName && collection && docId) {
const refreshed = await getSingleDocument(accountId, databaseName, collection, docId);
setCurrentDoc(refreshed);
setJsonValue(JSON.stringify(refreshed, null, 2));
}
const refreshed = await getSingleDocument(accountId, databaseName, collection, docId);
setJsonValue(JSON.stringify(refreshed, null, 2));
setFeedback({ type: 'success', message: 'Document saved and refreshed.' });
if (onSave) await onSave();
if (onCancel) onCancel();
Expand Down Expand Up @@ -92,6 +98,6 @@ const DocumentEditView: React.FC<DocumentEditViewProps> = ({ accountId, database
)}
</div>
);
};
});

export default DocumentEditView;
27 changes: 19 additions & 8 deletions frontend/components/DocumentHistoryDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const DocumentHistoryDialog: React.FC<DocumentHistoryDialogProps> = ({
const fetchDocumentHistory = async () => {
setLoading(true);
setError(null);

try {
const response = await getDocumentHistory(resource, collectionName, documentId);
setHistoryData(response);
Expand Down Expand Up @@ -130,11 +130,15 @@ const DocumentHistoryDialog: React.FC<DocumentHistoryDialogProps> = ({

const renderDiffData = (entry: DocumentHistoryEntry) => {
if (entry.operation === 'insert') {
const insertData = { ...(entry.diff_data as Record<string, any>) };
delete insertData['datetime_creation'];
delete insertData['datetime_last_modified'];

return (
<div className="mt-2 p-3 bg-green-100 dark:bg-green-900/30 rounded border">
<p className="text-sm font-medium text-green-800 dark:text-green-200 mb-2">Document created with initial data:</p>
<div className="text-xs font-mono bg-white dark:bg-gray-800 p-2 rounded border max-h-48 overflow-y-auto">
<pre className="whitespace-pre-wrap break-words overflow-wrap-anywhere">{JSON.stringify(entry.diff_data, null, 2)}</pre>
<pre className="whitespace-pre-wrap break-words overflow-wrap-anywhere">{JSON.stringify(insertData, null, 2)}</pre>
</div>
</div>
);
Expand All @@ -149,8 +153,12 @@ const DocumentHistoryDialog: React.FC<DocumentHistoryDialogProps> = ({
}

// Update operation
const changes = Object.entries(entry.diff_data as Record<string, any>);

const diffData = { ...(entry.diff_data as Record<string, any>) };
delete diffData['datetime_creation'];
delete diffData['datetime_last_modified'];

const changes = Object.entries(diffData);

return (
<div className="mt-2 space-y-2">
{changes.map(([field, change], index) => (
Expand Down Expand Up @@ -238,7 +246,10 @@ const DocumentHistoryDialog: React.FC<DocumentHistoryDialogProps> = ({
{historyData.history_entries.map((entry: DocumentHistoryEntry) => {
const timestamp = formatTimestamp(entry.timestamp_utc);
const isExpanded = expandedEntries.has(entry.id);
const hasChanges = entry.operation === 'update' && Object.keys(entry.diff_data).length > 0;
const diffDataKeys = Object.keys(entry.diff_data || {}).filter(k =>
!['datetime_creation', 'datetime_last_modified'].includes(k)
);
const hasChanges = entry.operation === 'update' && diffDataKeys.length > 0;

return (
<div
Expand All @@ -250,7 +261,7 @@ const DocumentHistoryDialog: React.FC<DocumentHistoryDialogProps> = ({
<div className="flex-shrink-0 mt-0.5">
{getOperationIcon(entry.operation)}
</div>

<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-gray-900 dark:text-gray-100 capitalize">
Expand All @@ -262,7 +273,7 @@ const DocumentHistoryDialog: React.FC<DocumentHistoryDialogProps> = ({
<span>{entry.user_email}</span>
</div>
</div>

<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 mb-2">
<AccessTimeIcon className="w-3 h-3" />
<span>{timestamp.relative}</span>
Expand All @@ -276,7 +287,7 @@ const DocumentHistoryDialog: React.FC<DocumentHistoryDialogProps> = ({
className="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 transition-colors"
>
{isExpanded ? <ChevronDownIcon className="w-3 h-3" /> : <ChevronRightIcon className="w-3 h-3" />}
{Object.keys(entry.diff_data).length} field{Object.keys(entry.diff_data).length !== 1 ? 's' : ''} changed
{diffDataKeys.length} field{diffDataKeys.length !== 1 ? 's' : ''} changed
</button>
)}

Expand Down
1 change: 1 addition & 0 deletions frontend/components/icons/material-icons-imports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,4 @@ export { default as WholeWordIcon } from '@mui/icons-material/ShortText';
export { default as RegexIcon } from '@mui/icons-material/Code';
export { default as DeleteIcon } from '@mui/icons-material/Delete';
export { default as ImageIcon } from '@mui/icons-material/Image';
export { default as WarningIcon } from '@mui/icons-material/WarningAmber';
Loading
Loading