feat(schema-diff): add table filters
This commit is contained in:
1 parent
37f7c48c3b
commit
21ec91a3d5
13 files changed
+360
-47
No files matched your search
@@ -15,10 +15,11 @@ import SchemaDiffDeployStep from "@/components/diff/SchemaDiffDeployStep.vue";
|
||||
import SchemaDiffOptionsPanel from "@/components/diff/SchemaDiffOptionsPanel.vue";
|
||||
|
||||
import { getSchemaDiffOptionsForDbType } from "@/lib/schemaDiffOptions";
|
||||
import { getDefaultOptionsForDbType } from "@/types/schemaDiff";
|
||||
import { normalizeSchemaDiffCompareOptions } from "@/types/schemaDiff";
|
||||
import type { SchemaDiffCompareOptions, SchemaDiffConfig } from "@/types/schemaDiff";
|
||||
import type { ObjectSourceKind } from "@/types/database";
|
||||
import { buildDeploySqlForObjects, convertToSchemaDiffObjects, groupDiffObjects, type OperationGroup, type SchemaDiffObject, type DiffOperationType, type DiffObjectKind, type SchemaDiffPreparation } from "@/lib/schemaDiff";
|
||||
import { compileSchemaDiffTableFilter, filterSchemaDiffTables } from "@/lib/schemaDiffTableFilter";
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
import "splitpanes/dist/splitpanes.css";
|
||||
|
||||
@@ -180,6 +181,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
// Config management
|
||||
const { configs, activeConfigId, activeConfig, recentConfigs, ensureDefaultConfig, updateActiveConfigConnection, updateActiveConfigOptions, saveToHistory, deleteFromHistory } = useSchemaDiffConfig();
|
||||
const schemaDiffPanelOptions = computed(() => normalizeSchemaDiffCompareOptions(activeConfig.value?.options, getDbType()));
|
||||
|
||||
const selectedObject = computed(() => {
|
||||
if (!selectedObjectId.value) return null;
|
||||
@@ -259,7 +261,7 @@ function handleSwap() {
|
||||
|
||||
function handleOptionsUpdate(options: SchemaDiffCompareOptions) {
|
||||
if (activeConfig.value) {
|
||||
updateActiveConfigOptions(options);
|
||||
updateActiveConfigOptions(normalizeSchemaDiffCompareOptions(options, getDbType()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,14 +284,20 @@ async function handleCompare() {
|
||||
step.value = "compare";
|
||||
|
||||
try {
|
||||
const targetConfig = store.getConfig(targetConnectionId.value);
|
||||
const dbType = targetConfig?.db_type || "mysql";
|
||||
const opts = normalizeSchemaDiffCompareOptions(activeConfig.value?.options, dbType);
|
||||
const tableFilter = compileSchemaDiffTableFilter(opts);
|
||||
|
||||
await store.ensureConnected(sourceConnectionId.value);
|
||||
await store.ensureConnected(targetConnectionId.value);
|
||||
|
||||
const [srcTables, tgtTables] = await Promise.all([api.listTables(sourceConnectionId.value, sourceDatabase.value, sourceSchema.value), api.listTables(targetConnectionId.value, targetDatabase.value, targetSchema.value)]);
|
||||
const { sourceTables, targetTables } = filterSchemaDiffTables(srcTables, tgtTables, tableFilter);
|
||||
|
||||
// Load schema details in parallel
|
||||
const sourceDetails = await Promise.all(
|
||||
srcTables.map(async (table) => {
|
||||
sourceTables.map(async (table) => {
|
||||
const objectType = isViewOrMaterializedView(table.table_type);
|
||||
const [columns, indexes, foreignKeys, triggers, ddl] = await Promise.all([
|
||||
api.getColumns(sourceConnectionId.value, sourceDatabase.value, sourceSchema.value, table.name),
|
||||
@@ -303,7 +311,7 @@ async function handleCompare() {
|
||||
);
|
||||
|
||||
const targetDetails = await Promise.all(
|
||||
tgtTables.map(async (table) => {
|
||||
targetTables.map(async (table) => {
|
||||
const objectType = isViewOrMaterializedView(table.table_type);
|
||||
const [columns, indexes, foreignKeys, triggers, ddl] = await Promise.all([
|
||||
api.getColumns(targetConnectionId.value, targetDatabase.value, targetSchema.value, table.name),
|
||||
@@ -316,10 +324,7 @@ async function handleCompare() {
|
||||
}),
|
||||
);
|
||||
|
||||
const targetConfig = store.getConfig(targetConnectionId.value);
|
||||
const dbType = targetConfig?.db_type || "mysql";
|
||||
const isPostgresLike = dbType === "postgres" || dbType === "opengauss";
|
||||
const opts = activeConfig.value?.options;
|
||||
|
||||
// Fetch new object types for PostgreSQL-like databases
|
||||
const promises: Promise<any>[] = [];
|
||||
@@ -352,8 +357,8 @@ async function handleCompare() {
|
||||
const tgtOwners = opts?.owners && isPostgresLike ? results[idx++] : [];
|
||||
|
||||
const result = await api.prepareSchemaDiff({
|
||||
sourceTables: srcTables,
|
||||
targetTables: tgtTables,
|
||||
sourceTables,
|
||||
targetTables,
|
||||
sourceDetails,
|
||||
targetDetails,
|
||||
sourceFunctions: srcFunctions,
|
||||
@@ -368,6 +373,7 @@ async function handleCompare() {
|
||||
targetSchema: targetSchema.value,
|
||||
ignoreComments: ignoreComments.value,
|
||||
cascadeDelete: opts?.cascadeDelete ?? false,
|
||||
compareColumnOrder: opts.compareColumnOrder,
|
||||
});
|
||||
|
||||
// Convert to unified objects
|
||||
@@ -822,12 +828,12 @@ const targetConnectionInfo = computed(() => {
|
||||
|
||||
<!-- Options Panel Overlay -->
|
||||
<div v-if="showOptionsPanel" class="absolute inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center" @click.self="showOptionsPanel = false">
|
||||
<div class="bg-card border rounded-lg shadow-lg w-[500px] max-h-[80vh] overflow-auto p-4">
|
||||
<div class="bg-card border rounded-lg shadow-lg w-[760px] max-w-[calc(100vw-2rem)] max-h-[80vh] overflow-auto p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-medium">{{ t("schemaDiff.optionsTitle") }}</h3>
|
||||
<Button variant="ghost" size="sm" @click="showOptionsPanel = false">✕</Button>
|
||||
</div>
|
||||
<SchemaDiffOptionsPanel :options="activeConfig?.options ?? getDefaultOptionsForDbType(getDbType())" :option-tree="optionTree" @update:options="handleOptionsUpdate" @close="showOptionsPanel = false" />
|
||||
<SchemaDiffOptionsPanel :options="schemaDiffPanelOptions" :option-tree="optionTree" @update:options="handleOptionsUpdate" @close="showOptionsPanel = false" />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
import { ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { SchemaDiffCompareOptions, SchemaDiffOptionItem } from "@/types/schemaDiff";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { HelpTooltip } from "@/components/ui/tooltip";
|
||||
import type { BooleanSchemaDiffCompareOptionKey, SchemaDiffCompareOptions, SchemaDiffOptionItem } from "@/types/schemaDiff";
|
||||
import { normalizeSchemaDiffCompareOptions } from "@/types/schemaDiff";
|
||||
|
||||
const props = defineProps<{
|
||||
options: SchemaDiffCompareOptions;
|
||||
@@ -17,22 +20,22 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n();
|
||||
|
||||
// Local copy of options
|
||||
const localOptions = ref<SchemaDiffCompareOptions>({ ...props.options });
|
||||
const localOptions = ref<SchemaDiffCompareOptions>(normalizeSchemaDiffCompareOptions(props.options));
|
||||
|
||||
// Watch for external changes
|
||||
watch(
|
||||
() => props.options,
|
||||
(newOptions) => {
|
||||
localOptions.value = { ...newOptions };
|
||||
localOptions.value = normalizeSchemaDiffCompareOptions(newOptions);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
function isChecked(id: keyof SchemaDiffCompareOptions): boolean {
|
||||
function isChecked(id: BooleanSchemaDiffCompareOptionKey): boolean {
|
||||
return !!localOptions.value[id];
|
||||
}
|
||||
|
||||
function setOption(id: keyof SchemaDiffCompareOptions, checked: boolean) {
|
||||
function setOption(id: BooleanSchemaDiffCompareOptionKey, checked: boolean) {
|
||||
localOptions.value = { ...localOptions.value, [id]: checked };
|
||||
}
|
||||
|
||||
@@ -84,32 +87,67 @@ function getItemClasses(state: "checked" | "unchecked" | "indeterminate"): strin
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-1 overflow-auto space-y-1">
|
||||
<template v-for="item in optionTree" :key="item.id">
|
||||
<div class="grid flex-1 grid-cols-[minmax(0,1fr)_minmax(300px,0.95fr)] gap-4 overflow-auto pr-1">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="px-2 pb-1 text-xs font-medium text-muted-foreground">{{ t("schemaDiff.optionsTitle") }}</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2 py-1 px-2 rounded hover:bg-muted/50 cursor-pointer" @click="toggleItem(item)">
|
||||
<div :class="getItemClasses(getChildState(item))">
|
||||
<svg v-if="getChildState(item) === 'checked'" class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
<svg v-else-if="getChildState(item) === 'indeterminate'" class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm select-none">{{ t(item.labelKey) }}</span>
|
||||
</div>
|
||||
<div v-if="item.children" class="ml-6 space-y-1">
|
||||
<div v-for="child in item.children" :key="child.id" class="flex items-center gap-2 py-1 px-2 rounded hover:bg-muted/50 cursor-pointer" @click="toggleItem(child)">
|
||||
<div :class="getItemClasses(getChildState(child))">
|
||||
<svg v-if="getChildState(child) === 'checked'" class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
<template v-for="item in optionTree" :key="item.id">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2 py-1 px-2 rounded hover:bg-muted/50 cursor-pointer" @click="toggleItem(item)">
|
||||
<div :class="getItemClasses(getChildState(item))">
|
||||
<svg v-if="getChildState(item) === 'checked'" class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
<svg v-else-if="getChildState(item) === 'indeterminate'" class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm select-none">{{ t(item.labelKey) }}</span>
|
||||
</div>
|
||||
<div v-if="item.children" class="ml-6 space-y-1">
|
||||
<div v-for="child in item.children" :key="child.id" class="flex items-center gap-2 py-1 px-2 rounded hover:bg-muted/50 cursor-pointer" @click="toggleItem(child)">
|
||||
<div :class="getItemClasses(getChildState(child))">
|
||||
<svg v-if="getChildState(child) === 'checked'" class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm select-none">{{ t(child.labelKey) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm select-none">{{ t(child.labelKey) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="grid min-w-0 grid-cols-[112px_minmax(0,1fr)] content-start gap-x-3 gap-y-2 border-l pl-4">
|
||||
<div class="pt-1 text-xs font-medium text-muted-foreground">{{ t("schemaDiff.tableFilter") }}</div>
|
||||
<div class="flex min-w-0 items-center justify-between gap-2">
|
||||
<span class="truncate text-xs text-muted-foreground">{{ t("schemaDiff.tableFilterHintShort") }}</span>
|
||||
<HelpTooltip :label="t('schemaDiff.tableFilterHelp')" side="left">
|
||||
<div class="space-y-1">
|
||||
<div>{{ t("schemaDiff.tableFilterHelpRule") }}</div>
|
||||
<div>{{ t("schemaDiff.tableFilterHelpBlank") }}</div>
|
||||
<div>{{ t("schemaDiff.tableFilterHelpExample") }}</div>
|
||||
</div>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<label class="pt-2 text-xs font-medium text-muted-foreground" for="schema-diff-table-include">{{ t("schemaDiff.tableIncludePattern") }}</label>
|
||||
<input id="schema-diff-table-include" v-model="localOptions.tableIncludePattern" class="h-8 w-full rounded-md border border-input bg-background px-2 text-xs outline-none focus:ring-1 focus:ring-ring" :placeholder="t('schemaDiff.tableIncludePatternPlaceholder')" />
|
||||
<label class="pt-2 text-xs font-medium text-muted-foreground" for="schema-diff-table-exclude">{{ t("schemaDiff.tableExcludePattern") }}</label>
|
||||
<input id="schema-diff-table-exclude" v-model="localOptions.tableExcludePattern" class="h-8 w-full rounded-md border border-input bg-background px-2 text-xs outline-none focus:ring-1 focus:ring-ring" :placeholder="t('schemaDiff.tableExcludePatternPlaceholder')" />
|
||||
<label class="pt-2 text-xs font-medium text-muted-foreground" for="schema-diff-table-priority">{{ t("schemaDiff.tableFilterPriority") }}</label>
|
||||
<div>
|
||||
<Select v-model="localOptions.tableFilterPriority">
|
||||
<SelectTrigger id="schema-diff-table-priority" class="h-8 w-full text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="exclude">{{ t("schemaDiff.tableFilterPriorityExclude") }}</SelectItem>
|
||||
<SelectItem value="include">{{ t("schemaDiff.tableFilterPriorityInclude") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-2 pt-4 border-t mt-2">
|
||||
|
||||
Reference in new issue
Block a user