Skip to content

Commit fc3d144

Browse files
committed
feat: enhance QueryDisplay and DataExplorer components with filter state and handover functionality
1 parent 706bb77 commit fc3d144

5 files changed

Lines changed: 278 additions & 12 deletions

File tree

frontend/components/QueryDisplay.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ interface QueryDisplayProps {
1010
historyCount: number;
1111
historyIndex: number;
1212
onNavigateHistory: (direction: 'prev' | 'next') => void;
13+
isTransferable?: boolean;
14+
onOpenInExplorer?: () => void;
1315
}
1416

1517
const WRITE_OPERATION_REGEX = /\.(insert_one|insert_many|update_one|update_many|replace_one|delete_one|delete_many|bulk_write|drop|drop_index|drop_indexes|create_index|create_indexes|rename_collection)\s*\(/i;
@@ -30,6 +32,7 @@ const detectQueryType = (code: string): string => {
3032
const QueryDisplay: React.FC<QueryDisplayProps> = ({
3133
code, onCodeChange, onRunQuery, onSaveQuery,
3234
isExecuting, historyCount, historyIndex, onNavigateHistory,
35+
isTransferable, onOpenInExplorer,
3336
}) => {
3437
const [copied, setCopied] = useState(false);
3538
const [allowWrite, setAllowWrite] = useState(false);
@@ -92,6 +95,34 @@ const QueryDisplay: React.FC<QueryDisplayProps> = ({
9295
write op
9396
</span>
9497
)}
98+
{onOpenInExplorer && (
99+
<button
100+
onClick={isTransferable ? onOpenInExplorer : undefined}
101+
disabled={!isTransferable}
102+
title={
103+
isTransferable
104+
? 'Open in Data Explorer'
105+
: 'This query can\'t be opened in Explorer (only find() queries are supported). Your result will be saved if you switch tabs.'
106+
}
107+
style={{
108+
display: 'inline-flex', alignItems: 'center', gap: 4,
109+
background: isTransferable ? 'var(--accent-soft)' : 'var(--soft)',
110+
border: `1px solid ${isTransferable ? 'color-mix(in oklch, var(--accent) 30%, transparent)' : 'var(--border)'}`,
111+
color: isTransferable ? 'var(--accent)' : 'var(--muted)',
112+
borderRadius: 4, cursor: isTransferable ? 'pointer' : 'default',
113+
fontSize: 11, padding: '2px 8px', fontFamily: 'var(--font-body)',
114+
opacity: isTransferable ? 1 : 0.55,
115+
}}
116+
>
117+
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8">
118+
<rect x="1" y="1" width="6" height="6" rx="1" />
119+
<rect x="9" y="1" width="6" height="6" rx="1" />
120+
<rect x="1" y="9" width="6" height="6" rx="1" />
121+
<path d="M11.5 11.5h3m-1.5-1.5v3" strokeLinecap="round" />
122+
</svg>
123+
Open in Explorer
124+
</button>
125+
)}
95126
</div>
96127
</div>
97128

frontend/pages/DataExplorerPage.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
2+
import { FilterState } from '../utils/queryHandover';
23
import { useNavigate } from 'react-router-dom';
34
import { SelectedResource, DbInfo, BreadcrumbItem, CosmosDBAccount, CollectionInfo } from '../types';
45
import { getDocuments, getCollectionInfo, findDocumentById, getDatabasesForAccount, clearDocumentsCache, getSingleDocument, getDocumentsQueryCode } from '../services/dbService';
@@ -75,6 +76,7 @@ interface DataExplorerPageProps {
7576
availableDbs: DbInfo[];
7677
availableAccounts: CosmosDBAccount[];
7778
initialDocumentId?: string;
79+
initialFilters?: FilterState[];
7880
onNavigateBack: () => void;
7981
embedded?: boolean;
8082
sidebarSelectedCollection?: string;
@@ -158,6 +160,7 @@ const DataExplorerPage: React.FC<DataExplorerPageProps> = ({
158160
availableDbs,
159161
availableAccounts,
160162
initialDocumentId,
163+
initialFilters,
161164
onNavigateBack,
162165
embedded = false,
163166
sidebarSelectedCollection,
@@ -400,6 +403,15 @@ const DataExplorerPage: React.FC<DataExplorerPageProps> = ({
400403
// Do not reset pinned documents here, as they should persist across DB/collection changes.
401404
}, []);
402405

406+
// Apply handover filters once after the first collection selection
407+
const appliedInitialFilters = useRef(false);
408+
useEffect(() => {
409+
if (!initialFilters?.length || appliedInitialFilters.current || !selectedCollection) return;
410+
appliedInitialFilters.current = true;
411+
setFilters(initialFilters);
412+
setDebouncedFilters(initialFilters);
413+
}, [selectedCollection, initialFilters]);
414+
403415
// Debounce filters
404416
useEffect(() => {
405417
const handler = setTimeout(() => {

frontend/pages/DataExplorerPageWrapper.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import DataExplorerPage from './DataExplorerPage';
44
import AppLayout from '../components/AppLayout';
55
import { SelectedResource, DbInfo, CosmosDBAccount, CollectionSummary } from '../types';
66
import { getAzureCosmosAccounts, getDatabasesForAccount } from '../services/dbService';
7+
import { FilterState } from '../utils/queryHandover';
78

89
interface LocationState {
910
dbInfo?: DbInfo;
1011
accountName?: string;
1112
availableDbs?: DbInfo[];
1213
availableAccounts?: CosmosDBAccount[];
1314
initialCollection?: string;
15+
initialFilters?: FilterState[];
1416
}
1517

1618
const DataExplorerPageWrapper: React.FC = () => {
@@ -43,6 +45,7 @@ const DataExplorerPageWrapper: React.FC = () => {
4345
availableDbs: DbInfo[];
4446
availableAccounts: CosmosDBAccount[];
4547
initialDocumentId?: string;
48+
initialFilters?: FilterState[];
4649
} | null>(null);
4750

4851
// Reset active collection when DB/account changes so the new page starts fresh
@@ -108,6 +111,7 @@ const DataExplorerPageWrapper: React.FC = () => {
108111
availableDbs: sessionConn.availableDbs,
109112
availableAccounts: sessionConn.availableAccounts,
110113
initialDocumentId: decodedDocumentId,
114+
initialFilters: state?.initialFilters,
111115
});
112116
return;
113117
}
@@ -129,7 +133,8 @@ const DataExplorerPageWrapper: React.FC = () => {
129133
accountName: state.accountName,
130134
availableDbs: state.availableDbs,
131135
availableAccounts: state.availableAccounts,
132-
initialDocumentId: decodedDocumentId
136+
initialDocumentId: decodedDocumentId,
137+
initialFilters: state.initialFilters,
133138
});
134139
} else {
135140
// Otherwise, fetch the data we need
@@ -283,6 +288,7 @@ const DataExplorerPageWrapper: React.FC = () => {
283288
availableDbs={pageData.availableDbs}
284289
availableAccounts={pageData.availableAccounts}
285290
initialDocumentId={pageData.initialDocumentId}
291+
initialFilters={pageData.initialFilters}
286292
onNavigateBack={onNavigateBack}
287293
onCollectionChange={setActiveCollection}
288294
sidebarSelectedCollection={activeCollection}

frontend/pages/QueryGeneratorPage.tsx

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
22
import { createPortal } from 'react-dom';
3-
import { useSearchParams } from 'react-router-dom';
3+
import { useSearchParams, useNavigate } from 'react-router-dom';
4+
import { parseQueryForHandover } from '../utils/queryHandover';
45
import { generateMongoQuery, debugMongoQuery, analyzeQueryResult, inferSchemaRelationships, evaluateWriteResult } from '../services/geminiService';
56
import { getAzureCosmosAccounts, getDatabasesForAccount, runMongoQuery, getCollectionInfo, clearSystemCache } from '../services/dbService';
67
import { getSavedQueries, saveQuery, updateSavedQuery, deleteSavedQuery } from '../services/userDataService';
@@ -408,7 +409,16 @@ export interface QueryGeneratorPageProps {
408409

409410
// --- Main Page Component ---
410411
const QueryGeneratorPage: React.FC<QueryGeneratorPageProps> = ({ name, email, onLogout, onNavigateToExplorer, onConnectionChange, embedded = false, preselectedAccountId }) => {
411-
const [userInput, setUserInput] = useState<string>('');
412+
const navigate = useNavigate();
413+
414+
const [userInput, setUserInput] = useState<string>(() => {
415+
try {
416+
const ws = JSON.parse(sessionStorage.getItem('qp_workspace') ?? 'null');
417+
const conn = JSON.parse(sessionStorage.getItem('qp_connection') ?? 'null');
418+
if (ws && conn && ws.accountId === conn.accountId && ws.databaseName === conn.databaseName) return ws.userInput ?? '';
419+
} catch { /* ignore */ }
420+
return '';
421+
});
412422
const [isLoading, setIsLoading] = useState<boolean>(false);
413423
const [error, setError] = useState<string | null>(null);
414424

@@ -434,16 +444,44 @@ const QueryGeneratorPage: React.FC<QueryGeneratorPageProps> = ({ name, email, on
434444
const [quickExploringAccountId, setQuickExploringAccountId] = useState<string | null>(null);
435445

436446
// State for AI query generation
437-
const [_queryResult, setQueryResult] = useState<QueryResultData | null>(null);
438-
const [querySourceCollection, setQuerySourceCollection] = useState<string | null>(null);
439-
const [editableCode, setEditableCode] = useState<string>('');
447+
const [_queryResult, setQueryResult] = useState<QueryResultData | null>(() => {
448+
try {
449+
const ws = JSON.parse(sessionStorage.getItem('qp_workspace') ?? 'null');
450+
const conn = JSON.parse(sessionStorage.getItem('qp_connection') ?? 'null');
451+
if (ws && conn && ws.accountId === conn.accountId && ws.databaseName === conn.databaseName) return ws.queryResult ?? null;
452+
} catch { /* ignore */ }
453+
return null;
454+
});
455+
const [querySourceCollection, setQuerySourceCollection] = useState<string | null>(() => {
456+
try {
457+
const ws = JSON.parse(sessionStorage.getItem('qp_workspace') ?? 'null');
458+
const conn = JSON.parse(sessionStorage.getItem('qp_connection') ?? 'null');
459+
if (ws && conn && ws.accountId === conn.accountId && ws.databaseName === conn.databaseName) return ws.querySourceCollection ?? null;
460+
} catch { /* ignore */ }
461+
return null;
462+
});
463+
const [editableCode, setEditableCode] = useState<string>(() => {
464+
try {
465+
const ws = JSON.parse(sessionStorage.getItem('qp_workspace') ?? 'null');
466+
const conn = JSON.parse(sessionStorage.getItem('qp_connection') ?? 'null');
467+
if (ws && conn && ws.accountId === conn.accountId && ws.databaseName === conn.databaseName) return ws.editableCode ?? '';
468+
} catch { /* ignore */ }
469+
return '';
470+
});
440471
const [codeHistory, setCodeHistory] = useState<string[]>([]);
441472
const [historyIndex, setHistoryIndex] = useState<number>(-1);
442473
const [lastSuccessfulPrompt, setLastSuccessfulPrompt] = useState<string>('');
443474

444475
// State for query execution
445476
const [isExecuting, setIsExecuting] = useState<boolean>(false);
446-
const [executionResult, setExecutionResult] = useState<any | null>(null);
477+
const [executionResult, setExecutionResult] = useState<any | null>(() => {
478+
try {
479+
const ws = JSON.parse(sessionStorage.getItem('qp_workspace') ?? 'null');
480+
const conn = JSON.parse(sessionStorage.getItem('qp_connection') ?? 'null');
481+
if (ws && conn && ws.accountId === conn.accountId && ws.databaseName === conn.databaseName) return ws.executionResult ?? null;
482+
} catch { /* ignore */ }
483+
return null;
484+
});
447485
const [executionError, setExecutionError] = useState<string | null>(null);
448486

449487
// State for intermediate context (multi-step queries)
@@ -526,6 +564,50 @@ const QueryGeneratorPage: React.FC<QueryGeneratorPageProps> = ({ name, email, on
526564
return azureAccounts.find(acc => acc.id === connectedResource.accountId)?.name ?? 'Unknown Account';
527565
}, [connectedResource, azureAccounts]);
528566

567+
// Query handover — detect transferable find() queries for "Open in Explorer"
568+
const handover = useMemo(
569+
() => editableCode && !_queryResult?.is_write_action ? parseQueryForHandover(editableCode) : null,
570+
[editableCode, _queryResult]
571+
);
572+
573+
const handleOpenInExplorer = useCallback(() => {
574+
if (!handover || !connectedResource || !connectedDbInfo) return;
575+
navigate(
576+
`/data-explorer/${encodeURIComponent(connectedResource.accountId)}/${encodeURIComponent(connectedResource.databaseName)}`,
577+
{
578+
state: {
579+
dbInfo: connectedDbInfo,
580+
accountName: connectedAccountName,
581+
availableDbs: accountDatabases,
582+
availableAccounts: azureAccounts,
583+
initialCollection: handover.collection,
584+
initialFilters: handover.filters,
585+
},
586+
}
587+
);
588+
}, [handover, connectedResource, connectedDbInfo, connectedAccountName, accountDatabases, azureAccounts, navigate]);
589+
590+
// Persist workspace state so it survives sidebar tab switches
591+
useEffect(() => {
592+
if (!connectedResource) return;
593+
try {
594+
const payload: Record<string, unknown> = {
595+
accountId: connectedResource.accountId,
596+
databaseName: connectedResource.databaseName,
597+
userInput,
598+
editableCode,
599+
querySourceCollection,
600+
queryResult: _queryResult,
601+
};
602+
// Only persist executionResult if it's under 1 MB to avoid quota errors
603+
if (executionResult !== null) {
604+
const resultStr = JSON.stringify(executionResult);
605+
if (resultStr.length < 1_000_000) payload.executionResult = executionResult;
606+
}
607+
sessionStorage.setItem('qp_workspace', JSON.stringify(payload));
608+
} catch { /* ignore */ }
609+
}, [userInput, editableCode, querySourceCollection, connectedResource, _queryResult, executionResult]);
610+
529611
const [isWaitingForAuth, setIsWaitingForAuth] = useState<boolean>(false);
530612

531613
const fetchAccounts = useCallback(async () => {
@@ -636,6 +718,7 @@ const QueryGeneratorPage: React.FC<QueryGeneratorPageProps> = ({ name, email, on
636718
setCurrentQueryContextSource(null);
637719
setRelationships(null); // Clear relationships
638720
setRelationshipError(null); // Clear relationship error
721+
sessionStorage.removeItem('qp_workspace');
639722
}, []);
640723

641724
const handleDisconnect = useCallback(() => {
@@ -695,7 +778,7 @@ const QueryGeneratorPage: React.FC<QueryGeneratorPageProps> = ({ name, email, on
695778
}
696779
}, [selectedAccountId, connectedResource, azureAccounts, handleDisconnect]);
697780

698-
const handleConnectDatabase = useCallback(async (dbInfo: DbInfo) => {
781+
const handleConnectDatabase = useCallback(async (dbInfo: DbInfo, preserveWorkspace = false) => {
699782
const account = azureAccounts.find(acc => acc.id === selectedAccountId);
700783
if (!account) return;
701784

@@ -708,7 +791,7 @@ const QueryGeneratorPage: React.FC<QueryGeneratorPageProps> = ({ name, email, on
708791
setConnectedDbInfo(dbInfo);
709792
sessionStorage.setItem('qp_connection', JSON.stringify({ accountId: account.id, databaseName: dbInfo.name, accountName: account.name, collections: dbInfo.collections, availableAccounts: azureAccounts, availableDbs: accountDatabases }));
710793
onConnectionChange?.(account.id, dbInfo.name, account.name, dbInfo.collections, azureAccounts, accountDatabases);
711-
clearQueryState();
794+
if (!preserveWorkspace) clearQueryState();
712795
setIsConnectingToDb(null);
713796
}, [selectedAccountId, azureAccounts, clearQueryState, onConnectionChange]);
714797

@@ -741,9 +824,9 @@ const QueryGeneratorPage: React.FC<QueryGeneratorPageProps> = ({ name, email, on
741824
) {
742825
if (targetDatabaseName) {
743826
const db = accountDatabases.find(d => d.name === targetDatabaseName) ?? accountDatabases[0];
744-
handleConnectDatabase(db);
827+
handleConnectDatabase(db, true);
745828
} else if (accountDatabases.length === 1) {
746-
handleConnectDatabase(accountDatabases[0]);
829+
handleConnectDatabase(accountDatabases[0], true);
747830
}
748831
}
749832
}, [preselectedAccountId, initialConnection, selectedAccountId, accountDatabases, connectedResource, isLoadingDatabases, isConnectingToDb, handleConnectDatabase]);
@@ -2389,7 +2472,7 @@ const QueryGeneratorPage: React.FC<QueryGeneratorPageProps> = ({ name, email, on
23892472
{(!isLoading && !error && !isDemoModeForResultsStep && !isDemoModeForDebugStep && !isDemoModeForContextActiveStep && !isDemoModeForRunStep && !isDemoModeForSaveStep) && (
23902473
editableCode ? (
23912474
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
2392-
<QueryDisplay code={editableCode} onCodeChange={setEditableCode} onRunQuery={handleRunQuery} onSaveQuery={handleOpenSaveDialog} isExecuting={isExecuting} historyCount={codeHistory.length} historyIndex={historyIndex} onNavigateHistory={handleNavigateHistory} />
2475+
<QueryDisplay code={editableCode} onCodeChange={setEditableCode} onRunQuery={handleRunQuery} onSaveQuery={handleOpenSaveDialog} isExecuting={isExecuting} historyCount={codeHistory.length} historyIndex={historyIndex} onNavigateHistory={handleNavigateHistory} isTransferable={!!handover} onOpenInExplorer={handleOpenInExplorer} />
23932476
<QueryResult isExecuting={isExecuting} executionError={executionError} executionResult={executionResult} onDebug={handleDebugQuery} isDebugging={isDebugging} debuggingResult={debuggingResult} debugError={debugError} sourceCollection={querySourceCollection} onSetIntermediateContext={handleSetIntermediateContext} intermediateContext={intermediateContext} onAnalyze={handleAnalyzeQuery} isAnalyzing={isAnalyzing} analysisResult={analysisResult} analysisError={analysisError} onEvaluateWrite={handleEvaluateWrite} isEvaluatingWrite={isEvaluatingWrite} writeEvaluationResult={writeEvaluationResult} writeEvaluationError={writeEvaluationError} />
23942477
</div>
23952478
) : (

0 commit comments

Comments
 (0)