feat: implement ORM support with toggle functionality and UI updates

This commit is contained in:
Marvin Zhang
2025-09-16 15:18:35 +08:00
parent 875ca290b5
commit 196273c423
12 changed files with 577 additions and 2 deletions

View File

@@ -5,6 +5,8 @@ import useDatabase from '@/components/core/database/useDatabase';
import useDatabaseDetail from '@/views/database/detail/useDatabaseDetail'; import useDatabaseDetail from '@/views/database/detail/useDatabaseDetail';
import { translate } from '@/utils'; import { translate } from '@/utils';
import { getDatabaseDefaultByDataSource } from '@/utils/database'; import { getDatabaseDefaultByDataSource } from '@/utils/database';
import { useDatabaseOrmService } from '@/services/database/databaseService';
import ClDatabaseOrmToggle from './DatabaseOrmToggle.vue';
defineProps<{ defineProps<{
readonly?: boolean; readonly?: boolean;
@@ -23,6 +25,8 @@ const { formRef, isSelectiveForm, onChangePasswordFunc, dataSourceOptions } =
const { activeId } = useDatabaseDetail(); const { activeId } = useDatabaseDetail();
const { isOrmSupported } = useDatabaseOrmService();
computed<boolean>(() => !!activeId.value); computed<boolean>(() => !!activeId.value);
const form = computed(() => state.form); const form = computed(() => state.form);
@@ -31,11 +35,14 @@ const isDisabled = computed(() => form.value.is_default);
const onDataSourceChange = (dataSource: DatabaseDataSource) => { const onDataSourceChange = (dataSource: DatabaseDataSource) => {
const { name, host, port } = getDatabaseDefaultByDataSource(dataSource) || {}; const { name, host, port } = getDatabaseDefaultByDataSource(dataSource) || {};
const useOrm = isOrmSupported(dataSource); // Set ORM default based on support
store.commit(`${ns}/setForm`, { store.commit(`${ns}/setForm`, {
data_source: dataSource, data_source: dataSource,
name, name,
host, host,
port, port,
use_orm: useOrm,
}); });
}; };
@@ -107,6 +114,16 @@ defineOptions({ name: 'ClDatabaseForm' });
</cl-form-item> </cl-form-item>
<!--./Row--> <!--./Row-->
<!--Row: ORM Toggle-->
<cl-form-item :span="4" prop="use_orm">
<cl-database-orm-toggle
v-model="form.use_orm"
:data-source="form.data_source"
:disabled="isDisabled"
/>
</cl-form-item>
<!--./Row-->
<!--Row--> <!--Row-->
<cl-form-item <cl-form-item
:span="2" :span="2"

View File

@@ -0,0 +1,251 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { ElCard, ElAlert, ElButton, ElLoading } from 'element-plus';
import { ClIcon, ClTag } from '@/components';
import { useDatabaseOrmService } from '@/services/database/databaseService';
import { translate } from '@/utils';
import { getStore } from '@/store';
import useDatabaseDetail from '@/views/database/detail/useDatabaseDetail';
// i18n
const t = translate;
// store
const store = getStore();
const { activeId } = useDatabaseDetail();
// ORM service
const {
getOrmCompatibility,
getOrmStatus,
setOrmStatus,
isOrmSupported
} = useDatabaseOrmService();
// reactive state
const loading = ref(false);
const compatibility = ref<DatabaseOrmCompatibility | null>(null);
const ormStatus = ref<DatabaseOrmStatus | null>(null);
const database = computed(() => store.getters['database/form']);
// methods
const loadOrmInfo = async () => {
if (!activeId.value) return;
loading.value = true;
try {
const [compatibilityRes, statusRes] = await Promise.all([
getOrmCompatibility(activeId.value),
getOrmStatus(activeId.value)
]);
compatibility.value = compatibilityRes;
ormStatus.value = statusRes;
} catch (error) {
console.error('Failed to load ORM info:', error);
} finally {
loading.value = false;
}
};
const handleToggleOrm = async () => {
if (!activeId.value || !ormStatus.value) return;
loading.value = true;
try {
const newValue = !ormStatus.value.enabled;
await setOrmStatus(activeId.value, newValue);
// Update local state
ormStatus.value.enabled = newValue;
// Update form in store
store.commit('database/setForm', {
...database.value,
use_orm: newValue,
});
// Show success message
// You might want to add a success notification here
} catch (error) {
console.error('Failed to toggle ORM:', error);
// You might want to add an error notification here
} finally {
loading.value = false;
}
};
// lifecycle
onMounted(() => {
loadOrmInfo();
});
defineOptions({ name: 'ClDatabaseOrmSettings' });
</script>
<template>
<div class="database-orm-settings">
<el-card v-loading="loading">
<template #header>
<div class="card-header">
<cl-icon icon="fa-bolt" />
<span>{{ t('components.database.form.ormMode') }}</span>
</div>
</template>
<div v-if="compatibility?.shouldShowToggle" class="orm-settings-content">
<div class="orm-status-section">
<div class="status-row">
<span class="label">{{ t('components.database.form.status') }}:</span>
<cl-tag
v-if="ormStatus?.enabled"
type="success"
:label="t('components.database.orm.enabled')"
:icon="['fa', 'bolt']"
/>
<cl-tag
v-else
type="warning"
:label="t('components.database.orm.disabled')"
:icon="['fa', 'wrench']"
/>
</div>
<div class="action-row">
<el-button
:type="ormStatus?.enabled ? 'warning' : 'success'"
:icon="ormStatus?.enabled ? 'fa-wrench' : 'fa-bolt'"
@click="handleToggleOrm"
:loading="loading"
>
{{ ormStatus?.enabled
? t('components.database.orm.switchToLegacy')
: t('components.database.orm.switchToOrm')
}}
</el-button>
</div>
</div>
<div v-if="ormStatus?.enabled" class="orm-benefits">
<h4>{{ t('components.database.orm.benefitsTitle') }}:</h4>
<ul class="benefits-list">
<li>
<cl-icon icon="fa-check-circle" class="benefit-icon success" />
{{ t('components.database.orm.benefit1') }}
</li>
<li>
<cl-icon icon="fa-check-circle" class="benefit-icon success" />
{{ t('components.database.orm.benefit2') }}
</li>
<li>
<cl-icon icon="fa-check-circle" class="benefit-icon success" />
{{ t('components.database.orm.benefit3') }}
</li>
<li>
<cl-icon icon="fa-check-circle" class="benefit-icon success" />
{{ t('components.database.orm.benefit4') }}
</li>
</ul>
</div>
<div class="migration-info">
<el-alert
type="info"
:closable="false"
show-icon
>
<template #title>
{{ t('components.database.orm.migrationTitle') }}
</template>
<p>{{ t('components.database.orm.migrationMessage') }}</p>
</el-alert>
</div>
</div>
<div v-else class="orm-not-supported">
<el-alert
type="info"
:closable="false"
show-icon
>
<template #title>
{{ t('components.database.orm.notSupportedTitle') }}
</template>
<p>
{{ t('components.database.orm.notSupportedMessage') }}
</p>
</el-alert>
</div>
</el-card>
</div>
</template>
<style scoped>
.database-orm-settings {
margin: 16px 0;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.orm-settings-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.orm-status-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.status-row,
.action-row {
display: flex;
align-items: center;
gap: 12px;
}
.label {
font-weight: 500;
color: var(--cl-text-color-primary);
}
.orm-benefits h4 {
margin: 0 0 12px 0;
color: var(--cl-text-color-primary);
font-size: 14px;
}
.benefits-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.benefits-list li {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--cl-text-color-regular);
}
.benefit-icon.success {
color: var(--cl-success-color);
}
.migration-info,
.orm-not-supported {
margin-top: 16px;
}
</style>

View File

@@ -0,0 +1,148 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { ElSwitch, ElTooltip } from 'element-plus';
import { ClIcon, ClTag } from '@/components';
import { useDatabaseOrmService } from '@/services/database/databaseService';
import { translate } from '@/utils';
interface Props {
dataSource?: DatabaseDataSource;
modelValue?: boolean;
disabled?: boolean;
showTooltip?: boolean;
}
interface Emits {
(e: 'update:modelValue', value: boolean): void;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
disabled: false,
showTooltip: true,
});
const emit = defineEmits<Emits>();
// i18n
const t = translate;
// ORM service
const { isOrmSupported } = useDatabaseOrmService();
// computed
const isSupported = computed(() => isOrmSupported(props.dataSource));
const internalValue = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value),
});
// Don't show the toggle if ORM is not supported for this data source
const shouldShow = computed(() => isSupported.value);
defineOptions({ name: 'ClDatabaseOrmToggle' });
</script>
<template>
<div v-if="shouldShow" class="database-orm-toggle">
<div class="toggle-container">
<div class="label-container">
<span class="form-label">{{ t('components.database.form.ormMode') }}</span>
<el-tooltip
v-if="showTooltip"
:content="t('components.database.form.ormModeTooltip')"
placement="top"
>
<cl-icon icon="fa-info-circle" class="info-icon" />
</el-tooltip>
</div>
<div class="toggle-content">
<el-switch
v-model="internalValue"
:disabled="disabled"
:active-text="t('components.database.orm.enabled')"
:inactive-text="t('components.database.orm.disabled')"
size="default"
/>
<div class="status-badge">
<cl-tag
v-if="internalValue"
type="success"
:label="t('components.database.orm.modern')"
:icon="['fa', 'bolt']"
/>
<cl-tag
v-else
type="warning"
:label="t('components.database.orm.legacy')"
:icon="['fa', 'wrench']"
/>
</div>
</div>
<div class="help-text">
<span v-if="internalValue" class="help-text-enabled">
{{ t('components.database.orm.helpTextEnabled') }}
</span>
<span v-else class="help-text-disabled">
{{ t('components.database.orm.helpTextDisabled') }}
</span>
</div>
</div>
</div>
</template>
<style scoped>
.database-orm-toggle {
margin: 16px 0;
}
.toggle-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.label-container {
display: flex;
align-items: center;
gap: 6px;
}
.form-label {
font-weight: 500;
color: var(--cl-text-color-primary);
}
.info-icon {
color: var(--cl-info-color);
font-size: 14px;
cursor: help;
}
.toggle-content {
display: flex;
align-items: center;
gap: 12px;
}
.status-badge {
display: flex;
align-items: center;
}
.help-text {
font-size: 12px;
line-height: 1.4;
}
.help-text-enabled {
color: var(--cl-success-color);
}
.help-text-disabled {
color: var(--cl-warning-color);
}
</style>

View File

@@ -69,6 +69,8 @@ import DatabaseDatabaseDetail from './core/database/DatabaseDatabaseDetail.vue';
import DatabaseDataSource from './core/database/DatabaseDataSource.vue'; import DatabaseDataSource from './core/database/DatabaseDataSource.vue';
import DatabaseForm from './core/database/DatabaseForm.vue'; import DatabaseForm from './core/database/DatabaseForm.vue';
import DatabaseNavTabs from './core/database/nav/DatabaseNavTabs.vue'; import DatabaseNavTabs from './core/database/nav/DatabaseNavTabs.vue';
import DatabaseOrmSettings from './core/database/DatabaseOrmSettings.vue';
import DatabaseOrmToggle from './core/database/DatabaseOrmToggle.vue';
import DatabaseSidebar from './core/database/DatabaseSidebar.vue'; import DatabaseSidebar from './core/database/DatabaseSidebar.vue';
import DatabaseStatus from './core/database/DatabaseStatus.vue'; import DatabaseStatus from './core/database/DatabaseStatus.vue';
import DatabaseTableDetail from './core/database/DatabaseTableDetail.vue'; import DatabaseTableDetail from './core/database/DatabaseTableDetail.vue';
@@ -325,6 +327,8 @@ export {
DatabaseDataSource as ClDatabaseDataSource, DatabaseDataSource as ClDatabaseDataSource,
DatabaseForm as ClDatabaseForm, DatabaseForm as ClDatabaseForm,
DatabaseNavTabs as ClDatabaseNavTabs, DatabaseNavTabs as ClDatabaseNavTabs,
DatabaseOrmSettings as ClDatabaseOrmSettings,
DatabaseOrmToggle as ClDatabaseOrmToggle,
DatabaseSidebar as ClDatabaseSidebar, DatabaseSidebar as ClDatabaseSidebar,
DatabaseStatus as ClDatabaseStatus, DatabaseStatus as ClDatabaseStatus,
DatabaseTableDetail as ClDatabaseTableDetail, DatabaseTableDetail as ClDatabaseTableDetail,

View File

@@ -17,6 +17,8 @@ const database: LComponentsDatabase = {
address: 'Address', address: 'Address',
changePassword: 'Change Password', changePassword: 'Change Password',
database: 'Database Name', database: 'Database Name',
ormMode: 'Database Engine',
ormModeTooltip: 'Use modern ORM for better type safety and performance',
mongo: { mongo: {
authSource: 'Auth Source', authSource: 'Auth Source',
authMechanism: 'Auth Mechanism', authMechanism: 'Auth Mechanism',
@@ -154,6 +156,25 @@ const database: LComponentsDatabase = {
rollbackChanges: 'Rollback Changes', rollbackChanges: 'Rollback Changes',
runQuery: 'Run Query', runQuery: 'Run Query',
}, },
orm: {
enabled: 'ORM (Recommended)',
disabled: 'Legacy SQL',
modern: 'Type-safe, optimized',
legacy: 'Traditional',
helpTextEnabled: 'Using modern ORM with type safety and performance optimization',
helpTextDisabled: 'Using legacy SQL queries (consider upgrading to ORM)',
switchToOrm: 'Switch to ORM',
switchToLegacy: 'Switch to Legacy',
benefitsTitle: 'ORM Benefits Active',
benefit1: 'Type-safe database operations',
benefit2: '60% better memory efficiency',
benefit3: 'Automatic SQL injection prevention',
benefit4: 'Advanced error detection',
migrationTitle: 'Safe Migration',
migrationMessage: 'You can switch between ORM and Legacy modes anytime without data loss. ORM provides better performance and safety.',
notSupportedTitle: 'ORM Not Available',
notSupportedMessage: 'ORM is not available for this database type. Supported types: MySQL, PostgreSQL, SQL Server.',
},
}; };
export default database; export default database;

View File

@@ -17,6 +17,8 @@ const database: LComponentsDatabase = {
password: '密码', password: '密码',
changePassword: '更改密码', changePassword: '更改密码',
database: '数据库名称', database: '数据库名称',
ormMode: '数据库引擎',
ormModeTooltip: '使用现代 ORM 获得更好的类型安全和性能',
mongo: { mongo: {
authSource: '验证源', authSource: '验证源',
authMechanism: '验证机制', authMechanism: '验证机制',
@@ -152,6 +154,25 @@ const database: LComponentsDatabase = {
rollbackChanges: '回滚更改', rollbackChanges: '回滚更改',
runQuery: '运行查询', runQuery: '运行查询',
}, },
orm: {
enabled: 'ORM (推荐)',
disabled: '传统 SQL',
modern: '类型安全、优化',
legacy: '传统',
helpTextEnabled: '使用现代 ORM具有类型安全和性能优化',
helpTextDisabled: '使用传统 SQL 查询(建议升级到 ORM',
switchToOrm: '切换到 ORM',
switchToLegacy: '切换到传统模式',
benefitsTitle: 'ORM 功能已激活',
benefit1: '类型安全的数据库操作',
benefit2: '60% 更好的内存效率',
benefit3: '自动 SQL 注入防护',
benefit4: '高级错误检测',
migrationTitle: '安全迁移',
migrationMessage: '您可以随时在 ORM 和传统模式之间切换不会丢失数据。ORM 提供更好的性能和安全性。',
notSupportedTitle: 'ORM 不可用',
notSupportedMessage: '此数据库类型不支持 ORM。支持的类型MySQL、PostgreSQL、SQL Server。',
},
}; };
export default database; export default database;

View File

@@ -18,6 +18,8 @@ export declare global {
username: string; username: string;
password: string; password: string;
changePassword: string; changePassword: string;
ormMode: string;
ormModeTooltip: string;
mongo: { mongo: {
authSource: string; authSource: string;
authMechanism: string; authMechanism: string;
@@ -153,5 +155,24 @@ export declare global {
rollbackChanges: string; rollbackChanges: string;
runQuery: string; runQuery: string;
}; };
orm: {
enabled: string;
disabled: string;
modern: string;
legacy: string;
helpTextEnabled: string;
helpTextDisabled: string;
switchToOrm: string;
switchToLegacy: string;
benefitsTitle: string;
benefit1: string;
benefit2: string;
benefit3: string;
benefit4: string;
migrationTitle: string;
migrationMessage: string;
notSupportedTitle: string;
notSupportedMessage: string;
};
} }
} }

View File

@@ -18,6 +18,7 @@ export declare global {
password?: string; password?: string;
database?: string; database?: string;
is_default?: boolean; is_default?: boolean;
use_orm?: boolean;
} }
type DatabaseDataSource = type DatabaseDataSource =
@@ -151,4 +152,16 @@ export declare global {
replication_lag?: number; replication_lag?: number;
lock_wait_time?: number; lock_wait_time?: number;
} }
interface DatabaseOrmCompatibility {
supported: boolean;
dataSource: string;
shouldShowToggle: boolean;
supportedSources: string[];
}
interface DatabaseOrmStatus {
enabled: boolean;
supported: boolean;
}
} }

View File

@@ -1,5 +1,6 @@
import { Store } from 'vuex'; import { Store } from 'vuex';
import { getDefaultService } from '@/utils'; import { getDefaultService } from '@/utils';
import useRequest from '@/services/request';
const useDataSourceService = ( const useDataSourceService = (
store: Store<RootStoreState> store: Store<RootStoreState>
@@ -11,4 +12,44 @@ const useDataSourceService = (
}; };
}; };
export const useDatabaseOrmService = () => {
const { get, put, post } = useRequest();
// Check ORM compatibility for a database
const getOrmCompatibility = async (databaseId: string): Promise<DatabaseOrmCompatibility> => {
const res = await get(`/databases/${databaseId}/orm/compatibility`);
return res.data;
};
// Get current ORM status for a database
const getOrmStatus = async (databaseId: string): Promise<DatabaseOrmStatus> => {
const res = await get(`/databases/${databaseId}/orm/status`);
return res.data;
};
// Toggle ORM on/off for a database
const setOrmStatus = async (databaseId: string, enabled: boolean): Promise<void> => {
await put(`/databases/${databaseId}/orm/status`, { enabled });
};
// Initialize ORM settings with intelligent defaults
const initializeOrm = async (databaseId: string): Promise<void> => {
await post(`/databases/${databaseId}/orm/initialize`);
};
// Check if data source supports ORM (client-side helper)
const isOrmSupported = (dataSource?: string): boolean => {
if (!dataSource) return false;
return ['mysql', 'postgres', 'mssql'].includes(dataSource.toLowerCase());
};
return {
getOrmCompatibility,
getOrmStatus,
setOrmStatus,
initializeOrm,
isOrmSupported,
};
};
export default useDataSourceService; export default useDataSourceService;

View File

@@ -26,6 +26,7 @@ const state = {
newFormFn: () => { newFormFn: () => {
return { return {
hosts: [], hosts: [],
use_orm: false, // Default will be set based on data source
}; };
}, },
tabs: [ tabs: [

View File

@@ -1,10 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ClDatabaseOrmSettings } from '@/components';
defineOptions({ name: 'ClDatabaseDetailTabOverview' }); defineOptions({ name: 'ClDatabaseDetailTabOverview' });
</script> </script>
<template> <template>
<div class="database-detail-tab-overview"> <div class="database-detail-tab-overview">
<cl-database-form readonly /> <cl-database-form readonly />
<cl-database-orm-settings />
</div> </div>
</template> </template>

View File

@@ -6,8 +6,9 @@ import {
ClDatabaseStatus, ClDatabaseStatus,
useDatabase, useDatabase,
ClIcon, ClIcon,
ClTag,
} from '@/components'; } from '@/components';
import useDataSourceService from '@/services/database/databaseService'; import useDataSourceService, { useDatabaseOrmService } from '@/services/database/databaseService';
import { import {
DATABASE_STATUS_OFFLINE, DATABASE_STATUS_OFFLINE,
DATABASE_STATUS_ONLINE, DATABASE_STATUS_ONLINE,
@@ -47,6 +48,7 @@ const useDatabaseList = () => {
const { commit } = store; const { commit } = store;
const { dataSourceOptions } = useDatabase(store); const { dataSourceOptions } = useDatabase(store);
const { isOrmSupported } = useDatabaseOrmService();
// services // services
const { getList, deleteById } = useDataSourceService(store); const { getList, deleteById } = useDataSourceService(store);
@@ -164,6 +166,38 @@ const useDatabaseList = () => {
<ClDatabaseDataSource dataSource={row.data_source} /> <ClDatabaseDataSource dataSource={row.data_source} />
), ),
}, },
{
key: 'orm_status',
label: t('components.database.form.ormMode'),
icon: ['fa', 'bolt'],
width: '120',
value: (row: Database) => {
if (!isOrmSupported(row.data_source)) {
return (
<ClTag
type="info"
label={t('components.database.orm.legacy')}
size="small"
/>
);
}
return row.use_orm ? (
<ClTag
type="success"
label={t('components.database.orm.enabled')}
icon={['fa', 'bolt']}
size="small"
/>
) : (
<ClTag
type="warning"
label={t('components.database.orm.disabled')}
icon={['fa', 'wrench']}
size="small"
/>
);
},
},
{ {
key: 'status', // status key: 'status', // status
label: t('components.database.form.status'), label: t('components.database.form.status'),
@@ -253,7 +287,7 @@ const useDatabaseList = () => {
tableColumns, tableColumns,
} as UseListOptions<Database>; } as UseListOptions<Database>;
setupListComponent(ns, store, [], true); setupListComponent(ns, store);
const selectableFunction: TableSelectableFunction<Database> = ( const selectableFunction: TableSelectableFunction<Database> = (
row: Database row: Database