mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-22 02:06:26 +00:00
fix(mongo): align collection loading state
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { Loader2, Square } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
labelKey?: string;
|
||||
elapsedSeconds?: number | string;
|
||||
showCancel?: boolean;
|
||||
cancelDisabled?: boolean;
|
||||
cancelling?: boolean;
|
||||
cancelLabelKey?: string;
|
||||
}>(),
|
||||
{
|
||||
labelKey: "common.loading",
|
||||
elapsedSeconds: undefined,
|
||||
showCancel: false,
|
||||
cancelDisabled: false,
|
||||
cancelling: false,
|
||||
cancelLabelKey: "toolbar.stopQuery",
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center gap-3 text-muted-foreground text-sm">
|
||||
<div class="flex items-center">
|
||||
<Loader2 class="h-5 w-5 animate-spin mr-2" />
|
||||
{{ t(labelKey) }}
|
||||
<span v-if="elapsedSeconds !== undefined" class="ml-1 tabular-nums text-muted-foreground/80">· {{ elapsedSeconds }}s</span>
|
||||
</div>
|
||||
<Button v-if="showCancel" variant="destructive" size="sm" class="h-7 gap-1.5" :disabled="cancelDisabled" @click="emit('cancel')">
|
||||
<Loader2 v-if="cancelling" class="h-3.5 w-3.5 animate-spin" />
|
||||
<Square v-else class="h-3.5 w-3.5 fill-current" />
|
||||
{{ t(cancelLabelKey) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -11,6 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import ErrorBanner from "@/components/ui/ErrorBanner.vue";
|
||||
import DataGrid from "@/components/grid/DataGrid.vue";
|
||||
import QueryLoadingState from "@/components/common/QueryLoadingState.vue";
|
||||
import * as api from "@/lib/api";
|
||||
import { clampSearchSplitWidth } from "@/lib/dataGridSearchSplit";
|
||||
import { buildDocumentFilterCondition, combineDocumentFilterConditions, currentDocumentFilterJson, defaultDocumentFilterRule, documentFilterModeNeedsValue, documentFilterModeOptions, documentStoreProviderFor, type DocumentFilterMode, type DocumentFilterRule } from "@/lib/documentStoreProvider";
|
||||
@@ -40,6 +41,9 @@ const documents = ref<JsonRecord[]>([]);
|
||||
const lastGridColumns = ref<string[]>([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const documentLoadExecutionId = ref("");
|
||||
const documentLoadCancelling = ref(false);
|
||||
const documentLoadingElapsedSeconds = ref("0.0");
|
||||
const page = ref(0);
|
||||
const pageSize = ref(normalizeResultPageSize(settingsStore.editorSettings.pageSize));
|
||||
const selectedIdx = ref<number | null>(null);
|
||||
@@ -145,6 +149,8 @@ const gridResult = computed<QueryResult>(() => {
|
||||
});
|
||||
const documentFilterFieldOptions = computed(() => gridResult.value.columns);
|
||||
const documentStructuredFilterCount = computed(() => (appliedDocumentFilter.value ? 1 : 0));
|
||||
const documentLoadingLabelKey = computed(() => (documentLoadCancelling.value ? "common.stopping" : "common.loading"));
|
||||
let documentLoadingTimer: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
function createDocumentFilterRule(): DocumentFilterRule {
|
||||
return defaultDocumentFilterRule(uuid(), documentFilterFieldOptions.value[0] ?? "");
|
||||
@@ -362,15 +368,35 @@ const customSaveHandler = computed<CustomSaveHandler>(() => ({
|
||||
preview: previewDocumentChanges,
|
||||
}));
|
||||
|
||||
function stopDocumentLoadingTimer() {
|
||||
if (documentLoadingTimer) clearInterval(documentLoadingTimer);
|
||||
documentLoadingTimer = undefined;
|
||||
}
|
||||
|
||||
function startDocumentLoadingTimer() {
|
||||
stopDocumentLoadingTimer();
|
||||
const startedAt = Date.now();
|
||||
documentLoadingElapsedSeconds.value = "0.0";
|
||||
documentLoadingTimer = setInterval(() => {
|
||||
documentLoadingElapsedSeconds.value = ((Date.now() - startedAt) / 1000).toFixed(1);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (documentLoadExecutionId.value) void api.cancelQuery(documentLoadExecutionId.value);
|
||||
const executionId = uuid();
|
||||
loading.value = true;
|
||||
documentLoadExecutionId.value = executionId;
|
||||
documentLoadCancelling.value = false;
|
||||
startDocumentLoadingTimer();
|
||||
error.value = "";
|
||||
const previousSelectedIdx = selectedIdx.value;
|
||||
const previousSelectedId = previousSelectedIdx === null ? null : documentIdentity(documents.value[previousSelectedIdx]);
|
||||
try {
|
||||
const filter = currentDocumentFilter();
|
||||
const sort = sortInput.value.trim() || undefined;
|
||||
const result = await api.documentFindDocuments(props.connectionId, props.database, props.collection, page.value * pageSize.value, pageSize.value, filter, sort);
|
||||
const result = await api.documentFindDocuments(props.connectionId, props.database, props.collection, page.value * pageSize.value, pageSize.value, filter, sort, executionId);
|
||||
if (documentLoadExecutionId.value !== executionId) return;
|
||||
const nextDocuments = result.documents.map(asRecord);
|
||||
documents.value = nextDocuments;
|
||||
if (nextDocuments.length > 0) {
|
||||
@@ -386,9 +412,30 @@ async function load() {
|
||||
total.value = result.total;
|
||||
syncSelectedDocumentAfterLoad(previousSelectedIdx, previousSelectedId);
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : String(e);
|
||||
if (documentLoadExecutionId.value === executionId) error.value = e instanceof Error ? e.message : String(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (documentLoadExecutionId.value === executionId) {
|
||||
loading.value = false;
|
||||
documentLoadExecutionId.value = "";
|
||||
documentLoadCancelling.value = false;
|
||||
stopDocumentLoadingTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelDocumentLoad() {
|
||||
const executionId = documentLoadExecutionId.value;
|
||||
if (!executionId || documentLoadCancelling.value) return;
|
||||
documentLoadCancelling.value = true;
|
||||
try {
|
||||
await api.cancelQuery(executionId);
|
||||
} finally {
|
||||
if (documentLoadExecutionId.value === executionId) {
|
||||
loading.value = false;
|
||||
documentLoadExecutionId.value = "";
|
||||
documentLoadCancelling.value = false;
|
||||
stopDocumentLoadingTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,6 +722,8 @@ function highlightedJson(json: string): string {
|
||||
|
||||
onMounted(load);
|
||||
onBeforeUnmount(() => {
|
||||
if (documentLoadExecutionId.value) void api.cancelQuery(documentLoadExecutionId.value);
|
||||
stopDocumentLoadingTimer();
|
||||
endTableSearchSplitResize();
|
||||
});
|
||||
|
||||
@@ -817,8 +866,18 @@ function resetTableSearchSplitWidth() {
|
||||
</div>
|
||||
|
||||
<!-- Table view -->
|
||||
<QueryLoadingState
|
||||
v-if="viewMode === 'table' && loading && gridResult.columns.length === 0"
|
||||
class="flex-1 min-h-0"
|
||||
:label-key="documentLoadingLabelKey"
|
||||
:elapsed-seconds="documentLoadingElapsedSeconds"
|
||||
show-cancel
|
||||
:cancel-disabled="!documentLoadExecutionId || documentLoadCancelling"
|
||||
:cancelling="documentLoadCancelling"
|
||||
@cancel="cancelDocumentLoad"
|
||||
/>
|
||||
<DataGrid
|
||||
v-if="viewMode === 'table'"
|
||||
v-else-if="viewMode === 'table'"
|
||||
ref="dataGridRef"
|
||||
class="flex-1 min-h-0"
|
||||
:result="gridResult"
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
TableProperties,
|
||||
} from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import QueryLoadingState from "@/components/common/QueryLoadingState.vue";
|
||||
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -7256,7 +7257,8 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</div>
|
||||
</div>
|
||||
</CustomContextMenu>
|
||||
<div v-if="!hasData" class="flex-1 flex items-center justify-center text-muted-foreground text-sm">
|
||||
<QueryLoadingState v-if="!hasData && loading" class="flex-1 min-h-0" />
|
||||
<div v-else-if="!hasData" class="flex-1 flex items-center justify-center text-muted-foreground text-sm">
|
||||
{{ t("grid.querySuccess") }}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
import { computed, ref, defineAsyncComponent, watch, nextTick, onMounted, onUnmounted } from "vue";
|
||||
import type { CSSProperties } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Check, Columns3, Loader2, Search, Square, Bot, GitBranch, BarChart3, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Wrench, ListChecks, Download, X } from "@lucide/vue";
|
||||
import { Check, Columns3, Loader2, Search, Bot, GitBranch, BarChart3, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Wrench, ListChecks, Download, X } from "@lucide/vue";
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
import "splitpanes/dist/splitpanes.css";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import LightTooltip from "@/components/ui/LightTooltip.vue";
|
||||
import QueryLoadingState from "@/components/common/QueryLoadingState.vue";
|
||||
import QueryEditor from "@/components/editor/QueryEditor.vue";
|
||||
import ColumnInfoPanel from "@/components/editor/ColumnInfoPanel.vue";
|
||||
import type { ColumnInfo } from "@/components/editor/ColumnInfoPanel.vue";
|
||||
@@ -700,18 +701,16 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
|
||||
</Button>
|
||||
</template>
|
||||
</DataGrid>
|
||||
<div v-else-if="!activeTab.result && activeTab.isExecuting" class="flex-1 min-h-0 flex flex-col items-center justify-center gap-3 text-muted-foreground text-sm">
|
||||
<div class="flex items-center">
|
||||
<Loader2 class="h-5 w-5 animate-spin mr-2" />
|
||||
{{ t(queryExecutionLabelKey(activeTab)) }}
|
||||
<span class="ml-1 tabular-nums text-muted-foreground/80">· {{ queryRunningElapsedSeconds }}s</span>
|
||||
</div>
|
||||
<Button variant="destructive" size="sm" class="h-7 gap-1.5" :disabled="!canCancelQueryExecution(activeTab)" @click="emit('cancel')">
|
||||
<Loader2 v-if="activeTab.isCancelling" class="h-3.5 w-3.5 animate-spin" />
|
||||
<Square v-else class="h-3.5 w-3.5 fill-current" />
|
||||
{{ t("toolbar.stopQuery") }}
|
||||
</Button>
|
||||
</div>
|
||||
<QueryLoadingState
|
||||
v-else-if="!activeTab.result && activeTab.isExecuting"
|
||||
class="flex-1 min-h-0"
|
||||
:label-key="queryExecutionLabelKey(activeTab)"
|
||||
:elapsed-seconds="queryRunningElapsedSeconds"
|
||||
show-cancel
|
||||
:cancel-disabled="!canCancelQueryExecution(activeTab)"
|
||||
:cancelling="activeTab.isCancelling"
|
||||
@cancel="emit('cancel')"
|
||||
/>
|
||||
<div v-else-if="!activeTab.result" class="flex-1 min-h-0 flex flex-col items-center justify-center gap-1 text-muted-foreground text-sm">
|
||||
<div>{{ t("editor.pressToExecute", { mod: shortcutModifier }) }}</div>
|
||||
<div>{{ t("editor.pressToSaveSql", { mod: shortcutModifier }) }}</div>
|
||||
@@ -847,18 +846,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
|
||||
@paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string) => emit('paginate', offset, limit, whereInput, orderBy)"
|
||||
@sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string) => emit('sort', column, columnIndex, direction, whereInput)"
|
||||
/>
|
||||
<div v-else-if="activeTab.isExecuting" class="h-full flex flex-col items-center justify-center gap-3 text-muted-foreground text-sm">
|
||||
<div class="flex items-center">
|
||||
<Loader2 class="h-5 w-5 animate-spin mr-2" />
|
||||
{{ t(queryExecutionLabelKey(activeTab)) }}
|
||||
<span class="ml-1 tabular-nums text-muted-foreground/80">· {{ queryRunningElapsedSeconds }}s</span>
|
||||
</div>
|
||||
<Button variant="destructive" size="sm" class="h-7 gap-1.5" :disabled="!canCancelQueryExecution(activeTab)" @click="emit('cancel')">
|
||||
<Loader2 v-if="activeTab.isCancelling" class="h-3.5 w-3.5 animate-spin" />
|
||||
<Square v-else class="h-3.5 w-3.5 fill-current" />
|
||||
{{ t("toolbar.stopQuery") }}
|
||||
</Button>
|
||||
</div>
|
||||
<QueryLoadingState v-else-if="activeTab.isExecuting" class="h-full" :label-key="queryExecutionLabelKey(activeTab)" :elapsed-seconds="queryRunningElapsedSeconds" show-cancel :cancel-disabled="!canCancelQueryExecution(activeTab)" :cancelling="activeTab.isCancelling" @cancel="emit('cancel')" />
|
||||
<div v-else class="h-full flex flex-col items-center justify-center gap-3 text-muted-foreground text-sm">
|
||||
<Inbox class="h-8 w-8 opacity-60" />
|
||||
<div>{{ t("grid.dataUnavailable") }}</div>
|
||||
|
||||
@@ -545,7 +545,8 @@ export async function executeInTransaction(connectionId: string, database: strin
|
||||
}
|
||||
|
||||
export async function cancelQuery(executionId: string): Promise<boolean> {
|
||||
return post("/api/query/cancel", { executionId });
|
||||
const result = await post<boolean | { cancelled?: boolean }>("/api/query/cancel", { executionId });
|
||||
return typeof result === "boolean" ? result : result.cancelled === true;
|
||||
}
|
||||
|
||||
export async function analyzeSqlReferences(sql: string, dialect?: string): Promise<SqlReferenceAnalysis> {
|
||||
@@ -1389,16 +1390,16 @@ export async function elasticsearchListIndices(connectionId: string): Promise<st
|
||||
return mongoListCollections(connectionId, "default");
|
||||
}
|
||||
|
||||
export async function mongoFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string): Promise<MongoDocumentResult> {
|
||||
return post("/api/mongo/find-documents", { connectionId, database, collection, skip, limit, filter, sort });
|
||||
export async function mongoFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string, executionId?: string): Promise<MongoDocumentResult> {
|
||||
return post("/api/mongo/find-documents", { connectionId, database, collection, skip, limit, filter, sort, executionId });
|
||||
}
|
||||
|
||||
export async function documentFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string): Promise<MongoDocumentResult> {
|
||||
return post("/api/document-store/find-documents", { connectionId, database, collection, skip, limit, filter, sort });
|
||||
export async function documentFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string, executionId?: string): Promise<MongoDocumentResult> {
|
||||
return post("/api/document-store/find-documents", { connectionId, database, collection, skip, limit, filter, sort, executionId });
|
||||
}
|
||||
|
||||
export async function mongoAggregateDocuments(connectionId: string, database: string, collection: string, pipelineJson: string, maxRows?: number): Promise<MongoDocumentResult> {
|
||||
return post("/api/mongo/aggregate-documents", { connectionId, database, collection, pipelineJson, maxRows });
|
||||
export async function mongoAggregateDocuments(connectionId: string, database: string, collection: string, pipelineJson: string, maxRows?: number, executionId?: string): Promise<MongoDocumentResult> {
|
||||
return post("/api/mongo/aggregate-documents", { connectionId, database, collection, pipelineJson, maxRows, executionId });
|
||||
}
|
||||
|
||||
export async function mongoInsertDocument(connectionId: string, database: string, collection: string, docJson: string): Promise<string> {
|
||||
|
||||
@@ -1268,16 +1268,16 @@ export async function elasticsearchListIndices(connectionId: string): Promise<st
|
||||
return mongoListCollections(connectionId, "default");
|
||||
}
|
||||
|
||||
export async function mongoFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string): Promise<MongoDocumentResult> {
|
||||
return invoke("mongo_find_documents", { connectionId, database, collection, skip, limit, filter, sort });
|
||||
export async function mongoFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string, executionId?: string): Promise<MongoDocumentResult> {
|
||||
return invoke("mongo_find_documents", { connectionId, database, collection, skip, limit, filter, sort, executionId });
|
||||
}
|
||||
|
||||
export async function documentFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string): Promise<MongoDocumentResult> {
|
||||
return invoke("document_find_documents", { connectionId, database, collection, skip, limit, filter, sort });
|
||||
export async function documentFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string, executionId?: string): Promise<MongoDocumentResult> {
|
||||
return invoke("document_find_documents", { connectionId, database, collection, skip, limit, filter, sort, executionId });
|
||||
}
|
||||
|
||||
export async function mongoAggregateDocuments(connectionId: string, database: string, collection: string, pipelineJson: string, maxRows?: number): Promise<MongoDocumentResult> {
|
||||
return invoke("mongo_aggregate_documents", { connectionId, database, collection, pipelineJson, maxRows });
|
||||
export async function mongoAggregateDocuments(connectionId: string, database: string, collection: string, pipelineJson: string, maxRows?: number, executionId?: string): Promise<MongoDocumentResult> {
|
||||
return invoke("mongo_aggregate_documents", { connectionId, database, collection, pipelineJson, maxRows, executionId });
|
||||
}
|
||||
|
||||
export async function mongoInsertDocument(connectionId: string, database: string, collection: string, docJson: string): Promise<string> {
|
||||
|
||||
@@ -1225,7 +1225,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (mongoFind) {
|
||||
await connStore.ensureConnected(tab.connectionId);
|
||||
console.info("[DBX][executeTabSql:mongo-find:start]", { traceId, collection: mongoFind.collection });
|
||||
const result = await api.mongoFindDocuments(tab.connectionId, tab.database, mongoFind.collection, mongoFind.skip, mongoFind.limit, mongoFind.filter, mongoFind.sort);
|
||||
const result = await api.mongoFindDocuments(tab.connectionId, tab.database, mongoFind.collection, mongoFind.skip, mongoFind.limit, mongoFind.filter, mongoFind.sort, executionId);
|
||||
console.info("[DBX][executeTabSql:mongo-find:done]", {
|
||||
traceId,
|
||||
rowCount: result.documents.length,
|
||||
@@ -1252,7 +1252,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (mongoCount) {
|
||||
await connStore.ensureConnected(tab.connectionId);
|
||||
console.info("[DBX][executeTabSql:mongo-count:start]", { traceId, collection: mongoCount.collection });
|
||||
const result = await api.mongoFindDocuments(tab.connectionId, tab.database, mongoCount.collection, 0, 1, mongoCount.filter);
|
||||
const result = await api.mongoFindDocuments(tab.connectionId, tab.database, mongoCount.collection, 0, 1, mongoCount.filter, undefined, executionId);
|
||||
console.info("[DBX][executeTabSql:mongo-count:done]", {
|
||||
traceId,
|
||||
total: result.total,
|
||||
@@ -1283,7 +1283,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
await connStore.ensureConnected(tab.connectionId);
|
||||
console.info("[DBX][executeTabSql:mongo-aggregate:start]", { traceId, collection: mongoAggregate.collection });
|
||||
const result = await api.mongoAggregateDocuments(tab.connectionId, tab.database, mongoAggregate.collection, mongoAggregate.pipeline, pageLimit);
|
||||
const result = await api.mongoAggregateDocuments(tab.connectionId, tab.database, mongoAggregate.collection, mongoAggregate.pipeline, pageLimit, executionId);
|
||||
console.info("[DBX][executeTabSql:mongo-aggregate:done]", {
|
||||
traceId,
|
||||
rowCount: result.documents.length,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::State;
|
||||
@@ -7,6 +8,26 @@ use serde::Deserialize;
|
||||
use crate::error::AppError;
|
||||
use crate::state::WebState;
|
||||
|
||||
async fn run_cancellable<T, F>(state: &Arc<WebState>, execution_id: Option<String>, future: F) -> Result<T, AppError>
|
||||
where
|
||||
F: Future<Output = Result<T, String>>,
|
||||
{
|
||||
let registered = execution_id
|
||||
.as_ref()
|
||||
.filter(|id| !id.trim().is_empty())
|
||||
.map(|id| state.app.running_queries.register(id.clone()));
|
||||
if let Some(query) = registered.as_ref() {
|
||||
let token = query.token();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = token.cancelled() => Err(AppError(dbx_core::query::canceled_error())),
|
||||
result = future => result.map_err(AppError),
|
||||
}
|
||||
} else {
|
||||
future.await.map_err(AppError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a connection is read-only and return an error if so.
|
||||
async fn ensure_writable(
|
||||
app: &dbx_core::connection::AppState,
|
||||
@@ -45,6 +66,7 @@ pub struct MongoFindRequest {
|
||||
pub limit: Option<i64>,
|
||||
pub filter: Option<String>,
|
||||
pub sort: Option<String>,
|
||||
pub execution_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -55,6 +77,7 @@ pub struct MongoAggregateRequest {
|
||||
pub collection: String,
|
||||
pub pipeline_json: String,
|
||||
pub max_rows: Option<usize>,
|
||||
pub execution_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -138,18 +161,21 @@ pub async fn find_documents(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<MongoFindRequest>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let result = dbx_core::mongo_ops::mongo_find_documents_core(
|
||||
&state.app,
|
||||
&req.connection_id,
|
||||
&req.database,
|
||||
&req.collection,
|
||||
req.skip.unwrap_or(0),
|
||||
req.limit.unwrap_or(50),
|
||||
req.filter.as_deref(),
|
||||
req.sort.as_deref(),
|
||||
let result = run_cancellable(
|
||||
&state,
|
||||
req.execution_id.clone(),
|
||||
dbx_core::mongo_ops::mongo_find_documents_core(
|
||||
&state.app,
|
||||
&req.connection_id,
|
||||
&req.database,
|
||||
&req.collection,
|
||||
req.skip.unwrap_or(0),
|
||||
req.limit.unwrap_or(50),
|
||||
req.filter.as_deref(),
|
||||
req.sort.as_deref(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map_err(AppError)?;
|
||||
.await?;
|
||||
Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?))
|
||||
}
|
||||
|
||||
@@ -157,18 +183,21 @@ pub async fn document_find_documents(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<MongoFindRequest>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let result = dbx_core::mongo_ops::document_find_documents_core(
|
||||
&state.app,
|
||||
&req.connection_id,
|
||||
&req.database,
|
||||
&req.collection,
|
||||
req.skip.unwrap_or(0),
|
||||
req.limit.unwrap_or(50),
|
||||
req.filter.as_deref(),
|
||||
req.sort.as_deref(),
|
||||
let result = run_cancellable(
|
||||
&state,
|
||||
req.execution_id.clone(),
|
||||
dbx_core::mongo_ops::document_find_documents_core(
|
||||
&state.app,
|
||||
&req.connection_id,
|
||||
&req.database,
|
||||
&req.collection,
|
||||
req.skip.unwrap_or(0),
|
||||
req.limit.unwrap_or(50),
|
||||
req.filter.as_deref(),
|
||||
req.sort.as_deref(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map_err(AppError)?;
|
||||
.await?;
|
||||
Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?))
|
||||
}
|
||||
|
||||
@@ -176,16 +205,19 @@ pub async fn aggregate_documents(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<MongoAggregateRequest>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let result = dbx_core::mongo_ops::mongo_aggregate_documents_core(
|
||||
&state.app,
|
||||
&req.connection_id,
|
||||
&req.database,
|
||||
&req.collection,
|
||||
&req.pipeline_json,
|
||||
req.max_rows,
|
||||
let result = run_cancellable(
|
||||
&state,
|
||||
req.execution_id.clone(),
|
||||
dbx_core::mongo_ops::mongo_aggregate_documents_core(
|
||||
&state.app,
|
||||
&req.connection_id,
|
||||
&req.database,
|
||||
&req.collection,
|
||||
&req.pipeline_json,
|
||||
req.max_rows,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map_err(AppError)?;
|
||||
.await?;
|
||||
Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
use crate::commands::connection::{ensure_connection_writable, AppState};
|
||||
use dbx_core::db::mongo_driver::MongoDocumentResult;
|
||||
|
||||
async fn run_cancellable<T, F>(state: &Arc<AppState>, execution_id: Option<String>, future: F) -> Result<T, String>
|
||||
where
|
||||
F: Future<Output = Result<T, String>>,
|
||||
{
|
||||
let registered_query =
|
||||
execution_id.as_ref().filter(|id| !id.trim().is_empty()).map(|id| state.running_queries.register(id.clone()));
|
||||
if let Some(query) = registered_query.as_ref() {
|
||||
let token = query.token();
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = token.cancelled() => Err(dbx_core::query::canceled_error()),
|
||||
result = future => result,
|
||||
}
|
||||
} else {
|
||||
future.await
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mongo_list_databases(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
@@ -32,16 +51,22 @@ pub async fn mongo_find_documents(
|
||||
limit: i64,
|
||||
filter: Option<String>,
|
||||
sort: Option<String>,
|
||||
execution_id: Option<String>,
|
||||
) -> Result<MongoDocumentResult, String> {
|
||||
dbx_core::mongo_ops::mongo_find_documents_core(
|
||||
&state,
|
||||
&connection_id,
|
||||
&database,
|
||||
&collection,
|
||||
skip,
|
||||
limit,
|
||||
filter.as_deref(),
|
||||
sort.as_deref(),
|
||||
let app = state.inner().clone();
|
||||
run_cancellable(
|
||||
&app,
|
||||
execution_id,
|
||||
dbx_core::mongo_ops::mongo_find_documents_core(
|
||||
&app,
|
||||
&connection_id,
|
||||
&database,
|
||||
&collection,
|
||||
skip,
|
||||
limit,
|
||||
filter.as_deref(),
|
||||
sort.as_deref(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -57,16 +82,22 @@ pub async fn document_find_documents(
|
||||
limit: i64,
|
||||
filter: Option<String>,
|
||||
sort: Option<String>,
|
||||
execution_id: Option<String>,
|
||||
) -> Result<MongoDocumentResult, String> {
|
||||
dbx_core::mongo_ops::document_find_documents_core(
|
||||
&state,
|
||||
&connection_id,
|
||||
&database,
|
||||
&collection,
|
||||
skip,
|
||||
limit,
|
||||
filter.as_deref(),
|
||||
sort.as_deref(),
|
||||
let app = state.inner().clone();
|
||||
run_cancellable(
|
||||
&app,
|
||||
execution_id,
|
||||
dbx_core::mongo_ops::document_find_documents_core(
|
||||
&app,
|
||||
&connection_id,
|
||||
&database,
|
||||
&collection,
|
||||
skip,
|
||||
limit,
|
||||
filter.as_deref(),
|
||||
sort.as_deref(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -79,14 +110,20 @@ pub async fn mongo_aggregate_documents(
|
||||
collection: String,
|
||||
pipeline_json: String,
|
||||
max_rows: Option<usize>,
|
||||
execution_id: Option<String>,
|
||||
) -> Result<MongoDocumentResult, String> {
|
||||
dbx_core::mongo_ops::mongo_aggregate_documents_core(
|
||||
&state,
|
||||
&connection_id,
|
||||
&database,
|
||||
&collection,
|
||||
&pipeline_json,
|
||||
max_rows,
|
||||
let app = state.inner().clone();
|
||||
run_cancellable(
|
||||
&app,
|
||||
execution_id,
|
||||
dbx_core::mongo_ops::mongo_aggregate_documents_core(
|
||||
&app,
|
||||
&connection_id,
|
||||
&database,
|
||||
&collection,
|
||||
&pipeline_json,
|
||||
max_rows,
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user