up
This commit is contained in:
@@ -93,6 +93,22 @@
|
||||
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.olapColumns') }}</span>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="userStore.role === 'admin'"
|
||||
to="/database-connections"
|
||||
class="flex items-center rounded-lg hover:bg-gray-100 transition-colors group"
|
||||
:class="[
|
||||
route.path === '/database-connections' ? 'bg-primary-50 text-primary-700' : 'text-gray-700',
|
||||
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3'
|
||||
]"
|
||||
:title="sidebarCollapsed ? t('dbConnections.pageName') : ''"
|
||||
>
|
||||
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||
</svg>
|
||||
<span v-if="!sidebarCollapsed" class="truncate">{{ t('dbConnections.pageName') }}</span>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="userStore.role === 'admin'"
|
||||
to="/settings"
|
||||
@@ -109,30 +125,6 @@
|
||||
</svg>
|
||||
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.settings') }}</span>
|
||||
</router-link>
|
||||
|
||||
<!-- PhpMyAdmin - только для администраторов -->
|
||||
<a
|
||||
v-if="userStore.role === 'admin'"
|
||||
href="/phpmyadmin"
|
||||
target="_self"
|
||||
class="flex items-center rounded-lg hover:bg-gray-100 transition-colors no-router-link"
|
||||
:class="[
|
||||
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3',
|
||||
route.path === '/phpmyadmin' ? 'bg-primary-50 text-primary-700' : 'text-gray-700'
|
||||
]"
|
||||
:title="sidebarCollapsed ? t('app.database') : ''"
|
||||
>
|
||||
<!-- Иконка базы данных -->
|
||||
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 7v10c0 2 1.5 3 3 3h10c1.5 0 3-1 3-3V7c0-2-1.5-3-3-3H7c-1.5 0-3 1-3 3z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 7c0 2 1.5 3 3 3h10c1.5 0 3-1 3-3" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 12c0 2 1.5 3 3 3h10c1.5 0 3-1 3-3" />
|
||||
</svg>
|
||||
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.database') }}</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- User Info (collapsed aware) -->
|
||||
@@ -193,6 +185,17 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</router-link>
|
||||
<a
|
||||
href="/phpmyadmin"
|
||||
v-if="userStore.role === 'admin'"
|
||||
target="_self"
|
||||
class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
:title="t('app.database')"
|
||||
>
|
||||
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||
</svg>
|
||||
</a>
|
||||
<button @click="logout" class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors" :title="t('app.logout')">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
|
||||
@@ -234,5 +234,31 @@
|
||||
"deleteError": "Error deleting field",
|
||||
"deleteField": "Delete Field",
|
||||
"deleteFieldConfirm": "Are you sure you want to delete this field? This action cannot be undone."
|
||||
},
|
||||
"dbConnections": {
|
||||
"pageName": "Databases",
|
||||
"add": "Add Connection",
|
||||
"edit": "Edit Connection",
|
||||
"delete": "Delete Connection",
|
||||
"deleteConfirmation": "Are you sure you want to delete this database connection? This action cannot be undone.",
|
||||
"type": "Type",
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"database": "Database",
|
||||
"user": "User",
|
||||
"test": "Test connection",
|
||||
"noConnections": "No database connections found. Click 'Add Connection' to create one.",
|
||||
"loadError": "Failed to load database connections.",
|
||||
"testSuccess": "Connection successful! Latency: {latency} ms",
|
||||
"testError": "Connection failed: {error}",
|
||||
"testNetworkError": "Network error while testing connection: {error}",
|
||||
"testUnknownError": "Unknown error",
|
||||
"passwordRequired": "Password is required for new connection.",
|
||||
"createSuccess": "Database connection created successfully.",
|
||||
"updateSuccess": "Database connection updated successfully.",
|
||||
"createError": "Failed to create database connection.",
|
||||
"updateError": "Failed to update database connection.",
|
||||
"deleteSuccess": "Database connection deleted successfully.",
|
||||
"deleteError": "Failed to delete database connection."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,5 +234,31 @@
|
||||
"deleteError": "Ошибка при удалении поля",
|
||||
"deleteField": "Удаление поля",
|
||||
"deleteFieldConfirm": "Вы уверены, что хотите удалить это поле? Это действие необратимо."
|
||||
},
|
||||
"dbConnections": {
|
||||
"pageName": "Базы данных",
|
||||
"add": "Добавить подключение",
|
||||
"edit": "Редактировать подключение",
|
||||
"delete": "Удалить подключение",
|
||||
"deleteConfirmation": "Вы уверены, что хотите удалить это подключение к базе данных? Действие необратимо.",
|
||||
"type": "Тип",
|
||||
"host": "Хост",
|
||||
"port": "Порт",
|
||||
"database": "База данных",
|
||||
"user": "Пользователь",
|
||||
"test": "Проверить подключение",
|
||||
"noConnections": "Подключения к базам данных не найдены. Нажмите «Добавить подключение», чтобы создать.",
|
||||
"loadError": "Не удалось загрузить список подключений.",
|
||||
"testSuccess": "Подключение успешно! Задержка: {latency} мс",
|
||||
"testError": "Ошибка подключения: {error}",
|
||||
"testNetworkError": "Сетевая ошибка при проверке подключения: {error}",
|
||||
"testUnknownError": "Неизвестная ошибка",
|
||||
"passwordRequired": "Пароль обязателен для нового подключения.",
|
||||
"createSuccess": "Подключение к БД успешно создано.",
|
||||
"updateSuccess": "Подключение к БД успешно обновлено.",
|
||||
"createError": "Не удалось создать подключение к БД.",
|
||||
"updateError": "Не удалось обновить подключение к БД.",
|
||||
"deleteSuccess": "Подключение к БД успешно удалено.",
|
||||
"deleteError": "Не удалось удалить подключение к БД."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Dashboard from '@/views/Dashboard.vue'
|
||||
import Users from '@/views/Users.vue'
|
||||
import Restaurants from '@/views/Restaurants.vue'
|
||||
import OlapColumnsView from '@/views/OlapColumnsView.vue'
|
||||
import DBConnections from '@/views/DBConnections.vue'
|
||||
import AdminSettings from '@/views/AdminSettings.vue'
|
||||
import Profile from '@/views/Profile.vue'
|
||||
import NotFound from '@/views/NotFound.vue'
|
||||
@@ -52,6 +53,11 @@ const routes = [
|
||||
component: OlapColumnsView,
|
||||
meta: { requiresAuth: true, requiresAdmin: true, title: 'OlapColumns' }
|
||||
},
|
||||
{
|
||||
path: '/database-connections',
|
||||
component: DBConnections,
|
||||
meta: { requiresAuth: true, requiresAdmin: true, title: 'Database Connections' }
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
component: AdminSettings,
|
||||
|
||||
384
frontend/src/views/DBConnections.vue
Normal file
384
frontend/src/views/DBConnections.vue
Normal file
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<AppLayout>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">{{ t('dbConnections.pageName') }}</h1>
|
||||
<button @click="openModal('create')" class="btn-primary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{{ t('dbConnections.add') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.id') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.name') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('dbConnections.type') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('dbConnections.host') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('dbConnections.port') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('dbConnections.database') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('dbConnections.user') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.created') }}</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
<tr v-for="conn in connections" :key="conn.id" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ conn.id }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ conn.name }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<span :class="getTypeBadgeClass(conn.type)" class="px-2 py-1 rounded-full text-xs font-medium">
|
||||
{{ getTypeLabel(conn.type) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ conn.host }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ conn.port }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ conn.database }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ conn.user }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(conn.created) }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end space-x-3">
|
||||
<button @click="testConnection(conn)" :disabled="conn.testing" class="text-green-600 hover:text-green-800 transition-colors disabled:opacity-50" :title="t('dbConnections.test')">
|
||||
<svg v-if="!conn.testing" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<svg v-else class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="openModal('edit', conn)" class="text-blue-600 hover:text-blue-800 transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="confirmDelete(conn.id)" class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<span v-if="conn.testResult" class="text-xs px-2 py-1 rounded-full bg-green-100 text-green-700 ml-1 whitespace-nowrap">
|
||||
{{ conn.testResult }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="connections.length === 0">
|
||||
<td colspan="9" class="px-6 py-12 text-center text-gray-500">{{ t('dbConnections.noConnections') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно создания/редактирования -->
|
||||
<Transition name="fade">
|
||||
<div v-if="modalOpen" class="fixed inset-0 z-50 overflow-y-auto" @click.self="closeModal">
|
||||
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="relative bg-white rounded-2xl shadow-xl max-w-md w-full transform transition-all">
|
||||
<div class="flex justify-between items-center p-6 border-b">
|
||||
<h2 class="text-xl font-bold text-gray-900">{{ modalTitle }}</h2>
|
||||
<button @click="closeModal" class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form @submit.prevent="submitConnection" class="p-6 space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('common.name') }} *</label>
|
||||
<input v-model="form.name" type="text" required class="input-field" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('dbConnections.type') }} *</label>
|
||||
<select v-model="form.type" required class="input-field">
|
||||
<option value="mysql">MySQL</option>
|
||||
<option value="postgres">PostgreSQL</option>
|
||||
<option value="clickhouse">ClickHouse</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('dbConnections.host') }} *</label>
|
||||
<input v-model="form.host" type="text" required class="input-field" placeholder="localhost or IP" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('dbConnections.port') }} *</label>
|
||||
<input v-model="form.port" type="number" required class="input-field" placeholder="3306, 5432, 8123..." />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('dbConnections.database') }} *</label>
|
||||
<input v-model="form.database" type="text" required class="input-field" placeholder="database name" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('dbConnections.user') }} *</label>
|
||||
<input v-model="form.user" type="text" required class="input-field" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('common.password') }}</label>
|
||||
<input
|
||||
v-model="form.password"
|
||||
:required="modalMode === 'create'"
|
||||
type="password"
|
||||
class="input-field"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<p v-if="modalMode === 'edit'" class="text-xs text-gray-500 mt-1">{{ t('common.leavePasswordBlank') }}</p>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 pt-2">
|
||||
<button type="button" @click="closeModal" class="btn-secondary">{{ t('app.cancel') }}</button>
|
||||
<button type="submit" class="btn-primary">{{ t('app.save') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Модальное окно подтверждения удаления -->
|
||||
<Transition name="fade">
|
||||
<div v-if="deleteConfirm.show" class="fixed inset-0 z-50 overflow-y-auto" @click.self="deleteConfirm.show = false">
|
||||
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="relative bg-white rounded-2xl shadow-xl max-w-md w-full">
|
||||
<div class="p-6 text-center">
|
||||
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
|
||||
<svg class="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">{{ t('dbConnections.delete') }}</h3>
|
||||
<p class="text-sm text-gray-500 mb-6">{{ t('dbConnections.deleteConfirmation') }}</p>
|
||||
<div class="flex justify-center space-x-3">
|
||||
<button @click="deleteConfirm.show = false" class="btn-secondary">{{ t('app.cancel') }}</button>
|
||||
<button @click="deleteConnection(deleteConfirm.id)" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors">{{ t('app.delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import AppLayout from '@/components/Layout/AppLayout.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useNotification } from '@/composables/useNotification';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
type Connection = {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'mysql' | 'postgres' | 'clickhouse';
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
user: string;
|
||||
created: string;
|
||||
testing?: boolean;
|
||||
testResult?: string | null;
|
||||
};
|
||||
|
||||
const connections = ref<Connection[]>([]);
|
||||
const modalOpen = ref(false);
|
||||
const modalMode = ref<'create' | 'edit'>('create');
|
||||
const form = ref({
|
||||
id: null as number | null,
|
||||
name: '',
|
||||
type: 'mysql' as 'mysql' | 'postgres' | 'clickhouse',
|
||||
host: '',
|
||||
port: 3306,
|
||||
database: '',
|
||||
user: '',
|
||||
password: ''
|
||||
});
|
||||
const modalTitle = ref('');
|
||||
const deleteConfirm = ref({ show: false, id: null as number | null });
|
||||
|
||||
// Загрузка списка подключений
|
||||
async function loadConnections() {
|
||||
try {
|
||||
const res = await fetch('/api/admin/database-connections');
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
connections.value = data.map((c: any) => ({
|
||||
...c,
|
||||
testing: false,
|
||||
testResult: null
|
||||
}));
|
||||
} catch (e) {
|
||||
showNotification('dbConnections.loadError', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr).toLocaleString();
|
||||
}
|
||||
|
||||
// Тестирование соединения
|
||||
async function testConnection(conn: Connection) {
|
||||
conn.testing = true;
|
||||
conn.testResult = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/database-connections/${conn.id}/test`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
conn.testResult = `${data.latency_ms} ms`;
|
||||
showNotification('dbConnections.testSuccess', 'success', { latency: data.latency_ms });
|
||||
} else {
|
||||
const errorText = data.error || t('dbConnections.testUnknownError');
|
||||
showNotification('dbConnections.testError', 'error', { error: errorText });
|
||||
}
|
||||
} catch (error: any) {
|
||||
showNotification('dbConnections.testNetworkError', 'error', { error: error.message });
|
||||
} finally {
|
||||
conn.testing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательные функции для отображения типа
|
||||
function getTypeLabel(type: string) {
|
||||
const labels: Record<string, string> = {
|
||||
mysql: 'MySQL',
|
||||
postgres: 'PostgreSQL',
|
||||
clickhouse: 'ClickHouse'
|
||||
};
|
||||
return labels[type] || type;
|
||||
}
|
||||
|
||||
function getTypeBadgeClass(type: string) {
|
||||
const classes: Record<string, string> = {
|
||||
mysql: 'bg-blue-100 text-blue-800',
|
||||
postgres: 'bg-indigo-100 text-indigo-800',
|
||||
clickhouse: 'bg-amber-100 text-amber-800'
|
||||
};
|
||||
return classes[type] || 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
|
||||
function openModal(mode: 'create' | 'edit', conn: Connection | null = null) {
|
||||
modalMode.value = mode;
|
||||
if (mode === 'create') {
|
||||
form.value = {
|
||||
id: null,
|
||||
name: '',
|
||||
type: 'mysql',
|
||||
host: '',
|
||||
port: 3306,
|
||||
database: '',
|
||||
user: '',
|
||||
password: ''
|
||||
};
|
||||
modalTitle.value = t('dbConnections.add');
|
||||
} else if (conn) {
|
||||
form.value = {
|
||||
id: conn.id,
|
||||
name: conn.name,
|
||||
type: conn.type,
|
||||
host: conn.host,
|
||||
port: conn.port,
|
||||
database: conn.database,
|
||||
user: conn.user,
|
||||
password: ''
|
||||
};
|
||||
modalTitle.value = t('dbConnections.edit');
|
||||
}
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen.value = false;
|
||||
}
|
||||
|
||||
async function submitConnection() {
|
||||
if (modalMode.value === 'create' && !form.value.password) {
|
||||
showNotification('dbConnections.passwordRequired', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload: any = {
|
||||
name: form.value.name,
|
||||
type: form.value.type,
|
||||
host: form.value.host,
|
||||
port: form.value.port,
|
||||
database: form.value.database,
|
||||
user: form.value.user,
|
||||
};
|
||||
if (form.value.password) {
|
||||
payload.password = form.value.password;
|
||||
}
|
||||
|
||||
if (modalMode.value === 'create') {
|
||||
const res = await fetch('/api/admin/database-connections', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
showNotification('dbConnections.createSuccess', 'success');
|
||||
} else {
|
||||
const res = await fetch(`/api/admin/database-connections/${form.value.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
showNotification('dbConnections.updateSuccess', 'success');
|
||||
}
|
||||
await loadConnections();
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
showNotification(modalMode.value === 'create' ? 'dbConnections.createError' : 'dbConnections.updateError', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(id: number) {
|
||||
deleteConfirm.value = { show: true, id };
|
||||
}
|
||||
|
||||
async function deleteConnection(id: number) {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/database-connections/${id}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error();
|
||||
showNotification('dbConnections.deleteSuccess', 'success');
|
||||
await loadConnections();
|
||||
} catch (e) {
|
||||
showNotification('dbConnections.deleteError', 'error');
|
||||
} finally {
|
||||
deleteConfirm.value.show = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadConnections);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user