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 { translate } from '@/utils';
import { getDatabaseDefaultByDataSource } from '@/utils/database';
import { useDatabaseOrmService } from '@/services/database/databaseService';
import ClDatabaseOrmToggle from './DatabaseOrmToggle.vue';
defineProps<{
readonly?: boolean;
@@ -23,6 +25,8 @@ const { formRef, isSelectiveForm, onChangePasswordFunc, dataSourceOptions } =
const { activeId } = useDatabaseDetail();
const { isOrmSupported } = useDatabaseOrmService();
computed<boolean>(() => !!activeId.value);
const form = computed(() => state.form);
@@ -31,11 +35,14 @@ const isDisabled = computed(() => form.value.is_default);
const onDataSourceChange = (dataSource: DatabaseDataSource) => {
const { name, host, port } = getDatabaseDefaultByDataSource(dataSource) || {};
const useOrm = isOrmSupported(dataSource); // Set ORM default based on support
store.commit(`${ns}/setForm`, {
data_source: dataSource,
name,
host,
port,
use_orm: useOrm,
});
};
@@ -107,6 +114,16 @@ defineOptions({ name: 'ClDatabaseForm' });
</cl-form-item>
<!--./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-->
<cl-form-item
: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 DatabaseForm from './core/database/DatabaseForm.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 DatabaseStatus from './core/database/DatabaseStatus.vue';
import DatabaseTableDetail from './core/database/DatabaseTableDetail.vue';
@@ -325,6 +327,8 @@ export {
DatabaseDataSource as ClDatabaseDataSource,
DatabaseForm as ClDatabaseForm,
DatabaseNavTabs as ClDatabaseNavTabs,
DatabaseOrmSettings as ClDatabaseOrmSettings,
DatabaseOrmToggle as ClDatabaseOrmToggle,
DatabaseSidebar as ClDatabaseSidebar,
DatabaseStatus as ClDatabaseStatus,
DatabaseTableDetail as ClDatabaseTableDetail,

View File

@@ -17,6 +17,8 @@ const database: LComponentsDatabase = {
address: 'Address',
changePassword: 'Change Password',
database: 'Database Name',
ormMode: 'Database Engine',
ormModeTooltip: 'Use modern ORM for better type safety and performance',
mongo: {
authSource: 'Auth Source',
authMechanism: 'Auth Mechanism',
@@ -154,6 +156,25 @@ const database: LComponentsDatabase = {
rollbackChanges: 'Rollback Changes',
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;

View File

@@ -17,6 +17,8 @@ const database: LComponentsDatabase = {
password: '密码',
changePassword: '更改密码',
database: '数据库名称',
ormMode: '数据库引擎',
ormModeTooltip: '使用现代 ORM 获得更好的类型安全和性能',
mongo: {
authSource: '验证源',
authMechanism: '验证机制',
@@ -152,6 +154,25 @@ const database: LComponentsDatabase = {
rollbackChanges: '回滚更改',
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;

View File

@@ -18,6 +18,8 @@ export declare global {
username: string;
password: string;
changePassword: string;
ormMode: string;
ormModeTooltip: string;
mongo: {
authSource: string;
authMechanism: string;
@@ -153,5 +155,24 @@ export declare global {
rollbackChanges: 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;
database?: string;
is_default?: boolean;
use_orm?: boolean;
}
type DatabaseDataSource =
@@ -151,4 +152,16 @@ export declare global {
replication_lag?: 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 { getDefaultService } from '@/utils';
import useRequest from '@/services/request';
const useDataSourceService = (
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;

View File

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

View File

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

View File

@@ -6,8 +6,9 @@ import {
ClDatabaseStatus,
useDatabase,
ClIcon,
ClTag,
} from '@/components';
import useDataSourceService from '@/services/database/databaseService';
import useDataSourceService, { useDatabaseOrmService } from '@/services/database/databaseService';
import {
DATABASE_STATUS_OFFLINE,
DATABASE_STATUS_ONLINE,
@@ -47,6 +48,7 @@ const useDatabaseList = () => {
const { commit } = store;
const { dataSourceOptions } = useDatabase(store);
const { isOrmSupported } = useDatabaseOrmService();
// services
const { getList, deleteById } = useDataSourceService(store);
@@ -164,6 +166,38 @@ const useDatabaseList = () => {
<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
label: t('components.database.form.status'),
@@ -253,7 +287,7 @@ const useDatabaseList = () => {
tableColumns,
} as UseListOptions<Database>;
setupListComponent(ns, store, [], true);
setupListComponent(ns, store);
const selectableFunction: TableSelectableFunction<Database> = (
row: Database