feat(process-list): batch cancel MySQL queries

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-10 11:23:18 +08:00
1 parent 1d9cfe45f9
commit b2d2507aec
12 files changed
+314 -26

No files matched your search

@@ -44,6 +44,14 @@ let timer: ReturnType<typeof setInterval> | undefined;
const cancelTarget = ref<ProcessRow | null>(null);
const canceling = ref(false);
const batchSupported = computed(() => driver.value?.supportsBatchCancel === true);
const selectedIds = ref(new Set<number>());
const batchTargets = ref<ProcessRow[] | null>(null);
const batchResult = ref<{ succeeded: number; failures: { id: number; message: string }[] } | null>(null);
const refreshing = ref(false);
const actionsLocked = computed(() => canceling.value || cancelTarget.value !== null || batchTargets.value !== null);
let connectionGeneration = 0;
let disposed = false;
const fallbackListSql = ref<string | null>(null);
// Full-text preview for long cells (SQL statement / info), opened by clicking them.
@@ -80,6 +88,84 @@ const filteredRows = computed(() => {
});
});
const selectableRows = computed(() => (ownSessionId.value === null ? [] : filteredRows.value.filter((row) => !isOwnSession(row) && Number.isInteger(row.id) && row.id > 0)));
const allSelected = computed(() => selectableRows.value.length > 0 && selectableRows.value.every((row) => selectedIds.value.has(row.id)));
const partiallySelected = computed(() => selectedIds.value.size > 0 && !allSelected.value);
watch(
selectableRows,
(visible) => {
const available = new Set(visible.map((row) => row.id));
selectedIds.value = new Set([...selectedIds.value].filter((id) => available.has(id)));
},
{ flush: "sync" },
);
function toggleSelection(id: number, checked: boolean) {
if (actionsLocked.value || refreshing.value) return;
const next = new Set(selectedIds.value);
if (checked && selectableRows.value.some((row) => row.id === id)) next.add(id);
else next.delete(id);
selectedIds.value = next;
}
function toggleAll(checked: boolean) {
if (actionsLocked.value || refreshing.value) return;
selectedIds.value = new Set(checked ? selectableRows.value.map((row) => row.id) : []);
}
function requestBatchCancel() {
if (!batchSupported.value || actionsLocked.value || refreshing.value || ownSessionId.value === null) return;
const targets = selectableRows.value.filter((row) => selectedIds.value.has(row.id));
if (targets.length) batchTargets.value = targets.map((row) => ({ ...row }));
}
async function confirmBatchCancel() {
const targets = batchTargets.value;
const activeDriver = driver.value;
if (!targets?.length || !activeDriver?.supportsBatchCancel || ownSessionId.value === null || canceling.value) return;
const connection = { ...props.connection };
const generation = connectionGeneration;
const isCurrent = () => !disposed && generation === connectionGeneration;
canceling.value = true;
let executed = false;
try {
const statements = targets.map((row) => ({ id: row.id, sql: activeDriver.buildCancelQuerySql(row.id) }));
const result = await executeWithProductionSqlGuard({
connection,
database: "",
sql: statements.map((statement) => statement.sql).join(";\n"),
source: t("production.sourceAdmin"),
execute: async () => {
const summary = { succeeded: 0, failures: [] as { id: number; message: string }[] };
for (const statement of statements) {
// A closed tab or changed connection must not dispatch remaining work.
if (!isCurrent()) break;
executed = true;
try {
const results = await api.executeMulti(connection.id, "", statement.sql, undefined, undefined, { maxRows: 1 });
const error = processListExecutionError(results) || activeDriver.cancelQueryResultError?.(results);
if (error) throw new Error(error);
summary.succeeded++;
} catch (error: unknown) {
summary.failures.push({ id: statement.id, message: error instanceof Error ? error.message : String(error) });
}
}
return summary;
},
});
if (result === undefined || !isCurrent()) return;
batchResult.value = result;
batchTargets.value = null;
selectedIds.value = new Set();
} catch (error: unknown) {
if (isCurrent()) toast(t("processList.killFailed", { message: error instanceof Error ? error.message : String(error) }), 5000);
} finally {
canceling.value = false;
if (!disposed && (executed || !isCurrent())) void load({ silent: true });
}
}
function toggleSort(key: string) {
if (sortKey.value === key) {
sortDir.value = sortDir.value === "asc" ? "desc" : "asc";
@@ -91,45 +177,54 @@ function toggleSort(key: string) {
async function load(options: { silent?: boolean } = {}) {
const activeDriver = driver.value;
if (!activeDriver) return;
if (!activeDriver || disposed || actionsLocked.value) return;
if (!loadCoordinator.tryStart()) return;
const connectionId = props.connection.id;
const generation = connectionGeneration;
const isCurrent = () => !disposed && generation === connectionGeneration;
refreshing.value = true;
if (!options.silent) loading.value = true;
loadError.value = "";
try {
await connectionStore.ensureConnected(props.connection.id);
await connectionStore.ensureConnected(connectionId);
if (!isCurrent()) return;
if (ownSessionId.value === null) {
try {
let idResult;
try {
idResult = await api.executeQuery(props.connection.id, "", activeDriver.ownSessionSql, undefined, undefined, { maxRows: 1 });
idResult = await api.executeQuery(connectionId, "", activeDriver.ownSessionSql, undefined, undefined, { maxRows: 1 });
} catch (error) {
if (!activeDriver.fallbackOwnSessionSql || !activeDriver.shouldUseFallbackOwnSessionSql?.(error)) throw error;
idResult = await api.executeQuery(props.connection.id, "", activeDriver.fallbackOwnSessionSql, undefined, undefined, { maxRows: 1 });
idResult = await api.executeQuery(connectionId, "", activeDriver.fallbackOwnSessionSql, undefined, undefined, { maxRows: 1 });
}
const raw = idResult?.rows?.[0]?.[0];
const parsed = Number(raw);
if (Number.isFinite(parsed)) ownSessionId.value = parsed;
if (isCurrent() && Number.isInteger(parsed) && parsed > 0) ownSessionId.value = parsed;
} catch {
// Non-fatal: without our own id we simply cannot dim the self row.
}
}
if (!isCurrent()) return;
const listSql = fallbackListSql.value ?? activeDriver.listSql;
let result;
try {
result = await api.executeQuery(props.connection.id, "", listSql, undefined, undefined, { maxRows: activeDriver.maxRows });
result = await api.executeQuery(connectionId, "", listSql, undefined, undefined, { maxRows: activeDriver.maxRows });
} catch (error) {
if (fallbackListSql.value || !activeDriver.fallbackListSql || !activeDriver.shouldUseFallbackListSql?.(error)) throw error;
result = await api.executeQuery(props.connection.id, "", activeDriver.fallbackListSql, undefined, undefined, { maxRows: activeDriver.maxRows });
result = await api.executeQuery(connectionId, "", activeDriver.fallbackListSql, undefined, undefined, { maxRows: activeDriver.maxRows });
// Cache the compatible query so old servers do not fail once per refresh.
fallbackListSql.value = activeDriver.fallbackListSql;
if (isCurrent()) fallbackListSql.value = activeDriver.fallbackListSql;
}
if (!isCurrent()) return;
rows.value = activeDriver.mapRows(result);
truncated.value = result.truncated === true;
} catch (error: any) {
loadError.value = error?.message || String(error);
if (isCurrent()) loadError.value = error?.message || String(error);
} finally {
loading.value = false;
refreshing.value = false;
loadCoordinator.finish();
if (!disposed && !isCurrent()) void load();
}
}
@@ -138,26 +233,30 @@ function isOwnSession(row: ProcessRow): boolean {
}
function requestCancel(row: ProcessRow) {
if (isOwnSession(row)) return;
if (isOwnSession(row) || actionsLocked.value || refreshing.value) return;
cancelTarget.value = row;
}
async function confirmCancel() {
const target = cancelTarget.value;
const activeDriver = driver.value;
if (!target || !activeDriver) return;
if (!target || !activeDriver || canceling.value) return;
const connection = { ...props.connection };
const generation = connectionGeneration;
const isCurrent = () => !disposed && generation === connectionGeneration;
canceling.value = true;
try {
const cancelSql = activeDriver.buildCancelQuerySql(target.id);
let usedFallbackCancelSql = false;
const executeCancelSql = async (sql: string) => {
const results = await api.executeMulti(props.connection.id, "", sql, undefined, undefined, { maxRows: 1 });
if (!isCurrent()) return undefined;
const results = await api.executeMulti(connection.id, "", sql, undefined, undefined, { maxRows: 1 });
const executionError = processListExecutionError(results);
if (executionError) throw new Error(executionError);
return results;
};
const result = await executeWithProductionSqlGuard({
connection: props.connection,
connection,
database: "",
sql: cancelSql,
source: t("production.sourceAdmin"),
@@ -171,16 +270,16 @@ async function confirmCancel() {
}
},
});
if (result === undefined) return;
if (result === undefined || !isCurrent()) return;
const cancelResultError = usedFallbackCancelSql ? activeDriver.fallbackCancelQueryResultError?.(result) : activeDriver.cancelQueryResultError?.(result);
if (cancelResultError) throw new Error(cancelResultError);
toast(t("processList.killSuccess", { id: target.id }), 2500);
cancelTarget.value = null;
await load({ silent: true });
} catch (error: any) {
toast(t("processList.killFailed", { message: error?.message || String(error) }), 5000);
if (isCurrent()) toast(t("processList.killFailed", { message: error?.message || String(error) }), 5000);
} finally {
canceling.value = false;
if (cancelTarget.value === null) void load({ silent: true });
}
}
@@ -216,6 +315,12 @@ watch(intervalSeconds, () => {
watch(
() => props.connection.id,
() => {
connectionGeneration++;
selectedIds.value = new Set();
batchTargets.value = null;
batchResult.value = null;
cancelTarget.value = null;
fallbackListSql.value = null;
rows.value = [];
truncated.value = false;
ownSessionId.value = null;
@@ -227,22 +332,39 @@ watch(
);
onMounted(() => void load());
onBeforeUnmount(stopTimer);
onBeforeUnmount(() => {
disposed = true;
connectionGeneration++;
stopTimer();
});
</script>
<template>
<div class="flex h-full min-h-0 flex-col bg-background">
<div class="flex h-11 shrink-0 items-center gap-2 border-b bg-muted/20 px-3">
<div class="flex min-h-11 shrink-0 flex-wrap items-center gap-2 border-b bg-muted/20 px-3 py-1">
<div class="flex min-w-0 items-center gap-2">
<Activity class="h-4 w-4 text-primary" />
<div class="truncate text-sm font-semibold">{{ t("processList.title") }}</div>
<Badge variant="outline" class="h-5 rounded-md px-1.5 text-[11px]">{{ connection.name }}</Badge>
<Badge variant="secondary" class="h-5 rounded-md px-1.5 text-[11px]">{{ t("processList.sessionCount", { count: processListSessionCount(truncated ? rows.length : filteredRows.length, truncated) }) }}</Badge>
</div>
<div class="ml-auto flex items-center gap-2">
<div class="ml-auto flex flex-wrap items-center gap-2">
<Button
v-if="batchSupported"
variant="destructive"
size="sm"
class="h-7 gap-1.5 px-2 text-xs"
:disabled="actionsLocked || refreshing || selectedIds.size === 0 || ownSessionId === null"
:title="ownSessionId === null ? t('processList.batchNeedsSession') : undefined"
@click="requestBatchCancel"
>
<Loader2 v-if="canceling && batchTargets" class="h-3.5 w-3.5 animate-spin" />
<Ban v-else class="h-3.5 w-3.5" />
{{ t("processList.batchCancel", { count: selectedIds.size }) }}
</Button>
<div class="flex h-7 items-center gap-1.5 rounded-md border bg-background px-2">
<Search class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<input v-model="search" class="h-full w-40 min-w-0 bg-transparent text-xs outline-none placeholder:text-muted-foreground" :placeholder="t('processList.filter')" />
<input v-model="search" :disabled="actionsLocked" class="h-full w-40 min-w-0 bg-transparent text-xs outline-none placeholder:text-muted-foreground" :placeholder="t('processList.filter')" />
</div>
<label class="flex items-center gap-1.5 text-xs text-muted-foreground">
<input v-model="autoRefresh" type="checkbox" class="h-3.5 w-3.5 accent-primary" />
@@ -252,7 +374,7 @@ onBeforeUnmount(stopTimer);
<Input v-model.number="intervalSeconds" type="number" min="1" max="3600" class="h-6 w-14 border-0 px-1 text-xs shadow-none focus-visible:ring-0" @change="onIntervalInput" />
<span class="pr-1 text-[11px] text-muted-foreground">{{ t("processList.seconds") }}</span>
</div>
<Button variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" :disabled="loading" @click="load()">
<Button variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" :disabled="refreshing || actionsLocked" @click="load()">
<Loader2 v-if="loading" class="h-3.5 w-3.5 animate-spin" />
<RefreshCcw v-else class="h-3.5 w-3.5" />
{{ t("grid.refresh") }}
@@ -260,12 +382,26 @@ onBeforeUnmount(stopTimer);
</div>
</div>
<div v-if="batchSupported && ownSessionId === null && !refreshing" class="border-b px-3 py-2 text-xs text-muted-foreground">{{ t("processList.batchNeedsSession") }}</div>
<div v-if="loadError" class="border-b bg-destructive/10 px-3 py-2 text-xs text-destructive">{{ loadError }}</div>
<div class="min-h-0 flex-1 overflow-auto">
<table class="w-full border-collapse text-xs">
<thead class="sticky top-0 z-10 bg-muted/40 backdrop-blur">
<tr>
<th v-if="batchSupported" class="w-8 border-b px-3 py-2">
<input
type="checkbox"
class="h-3.5 w-3.5 accent-primary"
:checked="allSelected"
:indeterminate="partiallySelected"
:disabled="actionsLocked || refreshing || selectableRows.length === 0"
:aria-label="t('processList.selectAll')"
:title="t('processList.selectAll')"
@change="toggleAll(($event.target as HTMLInputElement).checked)"
/>
</th>
<th v-for="column in columns" :key="column.key" class="cursor-pointer select-none whitespace-nowrap border-b px-3 py-2 text-left font-medium hover:bg-accent" @click="toggleSort(column.key)">
<span class="inline-flex items-center gap-1">
{{ t(column.labelKey) }}
@@ -278,6 +414,16 @@ onBeforeUnmount(stopTimer);
</thead>
<tbody>
<tr v-for="row in filteredRows" :key="row.id" class="border-b hover:bg-accent/40" :class="{ 'bg-primary/5': isOwnSession(row) }">
<td v-if="batchSupported" class="px-3 py-1.5">
<input
type="checkbox"
class="h-3.5 w-3.5 accent-primary"
:checked="selectedIds.has(row.id)"
:disabled="actionsLocked || refreshing || ownSessionId === null || isOwnSession(row) || !Number.isInteger(row.id) || row.id <= 0"
:aria-label="t('processList.selectSession', { id: row.id })"
@change="toggleSelection(row.id, ($event.target as HTMLInputElement).checked)"
/>
</td>
<td
v-for="column in columns"
:key="column.key"
@@ -297,7 +443,7 @@ onBeforeUnmount(stopTimer);
variant="ghost"
size="sm"
class="h-6 gap-1 px-1.5 text-[11px] text-destructive hover:bg-destructive/10 hover:text-destructive disabled:opacity-40"
:disabled="isOwnSession(row)"
:disabled="isOwnSession(row) || actionsLocked || refreshing"
:title="isOwnSession(row) ? t('processList.cannotKillSelf') : t('processList.kill')"
@click="requestCancel(row)"
>
@@ -307,7 +453,7 @@ onBeforeUnmount(stopTimer);
</td>
</tr>
<tr v-if="!loading && filteredRows.length === 0">
<td :colspan="columns.length + 1" class="px-3 py-10 text-center text-muted-foreground">
<td :colspan="columns.length + 1 + (batchSupported ? 1 : 0)" class="px-3 py-10 text-center text-muted-foreground">
{{ search ? t("grid.noSearchResults") : t("processList.empty") }}
</td>
</tr>
@@ -319,11 +465,11 @@ onBeforeUnmount(stopTimer);
:open="cancelTarget !== null"
@update:open="
(open) => {
if (!open) cancelTarget = null;
if (!open && !canceling) cancelTarget = null;
}
"
>
<DialogContent class="max-w-sm">
<DialogContent class="max-w-sm" :show-close-button="!canceling">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<AlertTriangle class="h-4 w-4 text-destructive" />
@@ -334,7 +480,7 @@ onBeforeUnmount(stopTimer);
{{ t("processList.killConfirm", { id: cancelTarget.id, user: cancelTarget.user }) }}
</p>
<DialogFooter>
<Button variant="outline" @click="cancelTarget = null">{{ t("dangerDialog.cancel") }}</Button>
<Button variant="outline" :disabled="canceling" @click="cancelTarget = null">{{ t("dangerDialog.cancel") }}</Button>
<Button variant="destructive" :disabled="canceling" @click="confirmCancel">
<Loader2 v-if="canceling" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
{{ t("processList.kill") }}
@@ -343,6 +489,65 @@ onBeforeUnmount(stopTimer);
</DialogContent>
</Dialog>
<Dialog
:open="batchTargets !== null"
@update:open="
(open) => {
if (!open && !canceling) batchTargets = null;
}
"
>
<DialogContent
class="max-w-md"
:show-close-button="!canceling"
@interact-outside="
(event) => {
if (canceling) event.preventDefault();
}
"
@escape-key-down="
(event) => {
if (canceling) event.preventDefault();
}
"
>
<DialogHeader>
<DialogTitle>{{ t("processList.batchTitle") }}</DialogTitle>
</DialogHeader>
<p class="text-sm text-muted-foreground">{{ t("processList.batchConfirm", { count: batchTargets?.length ?? 0 }) }}</p>
<p class="max-h-40 overflow-auto break-words font-mono text-xs">{{ batchTargets?.map((row) => row.id).join(", ") }}</p>
<DialogFooter>
<Button variant="outline" :disabled="canceling" @click="batchTargets = null">{{ t("dangerDialog.cancel") }}</Button>
<Button variant="destructive" :disabled="canceling" @click="confirmBatchCancel">
<Loader2 v-if="canceling" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
{{ t(canceling ? "processList.batchRunning" : "processList.kill") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog
:open="batchResult !== null"
@update:open="
(open) => {
if (!open) batchResult = null;
}
"
>
<DialogContent class="max-w-lg">
<DialogHeader>
<DialogTitle>{{ t("processList.batchTitle") }}</DialogTitle>
</DialogHeader>
<p v-if="batchResult" class="text-sm">{{ t("processList.batchSummary", { succeeded: batchResult.succeeded, failed: batchResult.failures.length }) }}</p>
<ul v-if="batchResult?.failures.length" class="max-h-60 space-y-2 overflow-auto text-xs text-destructive">
<li v-for="failure in batchResult.failures" :key="failure.id" class="break-words">{{ failure.id }}: {{ failure.message }}</li>
</ul>
<DialogFooter>
<Button variant="secondary" @click="batchResult = null">{{ t("common.close") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog
:open="previewText !== null"
@update:open="
+8
View File
@@ -3620,6 +3620,14 @@ export default withEnglishFallback({
noInstalled: "Heç bir genişləndirmə quraşdırılmayıb.",
},
processList: {
batchCancel: "Seçilmiş sorğuları ləğv et ({count})",
batchNeedsSession: "Cari sessiya məlum deyil. Yeniləyin və yenidən cəhd edin.",
selectAll: "Cari filtrdə yüklənmiş uyğun sessiyaları seç",
selectSession: "{id} sessiyasını seç",
batchTitle: "Seçilmiş sorğuları ləğv et",
batchConfirm: "Bu {count} sessiyada işləyən sorğular ləğv edilsin? Sessiyalar bağlı qalacaq.",
batchRunning: "Sorğular ləğv edilir…",
batchSummary: "Ləğv sorğuları: {succeeded} uğurlu, {failed} uğursuz.",
title: "Proses siyahısı",
sessionCount: "{count} sessiya",
filter: "Sessiyaları süzgəcdən keçir",
+8
View File
@@ -3619,6 +3619,14 @@ export default {
noInstalled: "No extensions installed.",
},
processList: {
batchCancel: "Cancel selected queries ({count})",
batchNeedsSession: "Current session is unknown. Refresh and try again.",
selectAll: "Select eligible loaded sessions in the current filter",
selectSession: "Select session {id}",
batchTitle: "Cancel selected queries",
batchConfirm: "Cancel running queries in these {count} sessions? The sessions will remain connected.",
batchRunning: "Canceling queries…",
batchSummary: "Cancellation requests: {succeeded} succeeded, {failed} failed.",
title: "Process List",
sessionCount: "{count} sessions",
filter: "Filter sessions",
+8
View File
@@ -8782,6 +8782,14 @@ export default withEnglishFallback({
sourceDataEditor: "Editor de datos",
},
processList: {
batchCancel: "Cancelar consultas seleccionadas ({count})",
batchNeedsSession: "Se desconoce la sesión actual. Actualiza e inténtalo de nuevo.",
selectAll: "Seleccionar sesiones disponibles cargadas en el filtro actual",
selectSession: "Seleccionar sesión {id}",
batchTitle: "Cancelar consultas seleccionadas",
batchConfirm: "¿Cancelar las consultas en ejecución de estas {count} sesiones? Las sesiones permanecerán conectadas.",
batchRunning: "Cancelando consultas…",
batchSummary: "Solicitudes de cancelación: {succeeded} correctas, {failed} fallidas.",
title: "Lista de procesos",
sessionCount: "{count} sesiones",
filter: "Filtrar sesiones",
+8
View File
@@ -8783,6 +8783,14 @@ export default withEnglishFallback({
sourceDataEditor: "Editor dati",
},
processList: {
batchCancel: "Annulla query selezionate ({count})",
batchNeedsSession: "Sessione corrente sconosciuta. Aggiorna e riprova.",
selectAll: "Seleziona le sessioni disponibili caricate nel filtro corrente",
selectSession: "Seleziona sessione {id}",
batchTitle: "Annulla query selezionate",
batchConfirm: "Annullare le query in esecuzione in queste {count} sessioni? Le sessioni rimarranno connesse.",
batchRunning: "Annullamento query…",
batchSummary: "Richieste di annullamento: {succeeded} riuscite, {failed} non riuscite.",
title: "Elenco processi",
sessionCount: "{count} sessioni",
filter: "Filtra sessioni",
+8
View File
@@ -8834,6 +8834,14 @@ export default withEnglishFallback({
sourceDataEditor: "データエディター",
},
processList: {
batchCancel: "選択したクエリをキャンセル({count})",
batchNeedsSession: "現在のセッションを識別できません。更新して再試行してください。",
selectAll: "現在の絞り込み結果で読み込み済みの選択可能なセッションをすべて選択",
selectSession: "セッション {id} を選択",
batchTitle: "選択したクエリをキャンセル",
batchConfirm: "これら {count} 個のセッションで実行中のクエリをキャンセルしますか?セッションの接続は維持されます。",
batchRunning: "クエリをキャンセル中…",
batchSummary: "キャンセル要求:成功 {succeeded} 件、失敗 {failed} 件。",
title: "プロセス一覧",
sessionCount: "{count} セッション",
filter: "セッションをフィルター",
+8
View File
@@ -3385,6 +3385,14 @@ export default withEnglishFallback({
actionSuccess: '"{name}"에 대한 {action}이(가) 완료되었습니다.',
},
processList: {
batchCancel: "선택한 쿼리 취소 ({count})",
batchNeedsSession: "현재 세션을 확인할 수 없습니다. 새로 고침 후 다시 시도하세요.",
selectAll: "현재 필터에서 로드된 선택 가능한 세션 모두 선택",
selectSession: "세션 {id} 선택",
batchTitle: "선택한 쿼리 취소",
batchConfirm: "이 {count}개 세션에서 실행 중인 쿼리를 취소하시겠습니까? 세션 연결은 유지됩니다.",
batchRunning: "쿼리 취소 중…",
batchSummary: "취소 요청: 성공 {succeeded}개, 실패 {failed}개.",
title: "프로세스 목록",
sessionCount: "{count}개 세션",
filter: "세션 필터",
+8
View File
@@ -8784,6 +8784,14 @@ export default withEnglishFallback({
sourceDataEditor: "Editor de dados",
},
processList: {
batchCancel: "Cancelar consultas selecionadas ({count})",
batchNeedsSession: "Sessão atual desconhecida. Atualize e tente novamente.",
selectAll: "Selecionar sessões disponíveis carregadas no filtro atual",
selectSession: "Selecionar sessão {id}",
batchTitle: "Cancelar consultas selecionadas",
batchConfirm: "Cancelar as consultas em execução nestas {count} sessões? As sessões permanecerão conectadas.",
batchRunning: "Cancelando consultas…",
batchSummary: "Solicitações de cancelamento: {succeeded} bem-sucedidas, {failed} com falha.",
title: "Lista de Processos",
sessionCount: "{count} sessões",
filter: "Filtrar sessões",
+8
View File
@@ -3592,6 +3592,14 @@ export default withEnglishFallback({
noInstalled: "Kurulu uzantı yok.",
},
processList: {
batchCancel: "Seçili sorguları iptal et ({count})",
batchNeedsSession: "Geçerli oturum bilinmiyor. Yenileyip tekrar deneyin.",
selectAll: "Geçerli filtrede yüklenen uygun oturumları seç",
selectSession: "{id} oturumunu seç",
batchTitle: "Seçili sorguları iptal et",
batchConfirm: "Bu {count} oturumdaki çalışan sorgular iptal edilsin mi? Oturumlar bağlı kalacaktır.",
batchRunning: "Sorgular iptal ediliyor…",
batchSummary: "İptal istekleri: {succeeded} başarılı, {failed} başarısız.",
title: "İşlem Listesi",
sessionCount: "{count} oturum",
filter: "Oturumları filtrele",
+8
View File
@@ -3540,6 +3540,14 @@ export default withEnglishFallback({
noInstalled: "暂无已安装的扩展。",
},
processList: {
batchCancel: "批量取消查询({count})",
batchNeedsSession: "尚未识别当前会话,请刷新后重试",
selectAll: "选择当前筛选结果中已加载的可选会话",
selectSession: "选择会话 {id}",
batchTitle: "批量取消查询",
batchConfirm: "确定取消这 {count} 个会话中正在运行的查询?会话将保持连接。",
batchRunning: "正在取消查询…",
batchSummary: "取消请求执行成功 {succeeded} 个,失败 {failed} 个。",
title: "进程列表",
sessionCount: "{count} 个会话",
filter: "筛选会话",
+8
View File
@@ -8772,6 +8772,14 @@ export default withEnglishFallback({
sourceDataEditor: "資料編輯器",
},
processList: {
batchCancel: "批次取消查詢({count})",
batchNeedsSession: "尚未識別目前工作階段,請重新整理後重試",
selectAll: "選取目前篩選結果中已載入的可選工作階段",
selectSession: "選取工作階段 {id}",
batchTitle: "批次取消查詢",
batchConfirm: "確定取消這 {count} 個工作階段中正在執行的查詢?工作階段將保持連線。",
batchRunning: "正在取消查詢…",
batchSummary: "取消請求執行成功 {succeeded} 個,失敗 {failed} 個。",
title: "處理程序清單",
sessionCount: "{count} 個會話",
filter: "篩選會話",
@@ -47,6 +47,8 @@ export interface ProcessColumn {
}
export interface ProcessListDriver {
/** Whether the panel offers multi-selection and batch query cancellation. */
supportsBatchCancel?: boolean;
/** SQL that lists current sessions, one row each. */
listSql: string;
/** Compatibility query used when the primary list SQL references newer columns. */
@@ -103,6 +105,7 @@ const POSTGRES_COLUMNS: ProcessColumn[] = [
];
const MYSQL_DRIVER: ProcessListDriver = {
supportsBatchCancel: true,
listSql: MYSQL_PROCESS_LIST_SQL,
ownSessionSql: "SELECT CONNECTION_ID()",
columns: MYSQL_COLUMNS,