mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-28 16:09:17 +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,
|
||||
|
||||
Reference in New Issue
Block a user