modified: backend/src/services/DatabasePoolManager.ts
modified: backend/src/services/ScriptExecutor.ts modified: backend/src/types/index.ts modified: frontend/src/components/CodeEditor.tsx modified: frontend/src/components/EndpointModal.tsx deleted: frontend/src/pages/Databases.tsx modified: frontend/src/pages/Settings.tsx modified: frontend/src/types/index.ts
This commit is contained in:
@@ -80,6 +80,38 @@ class DatabasePoolManager {
|
|||||||
return this.pools.get(databaseId);
|
return this.pools.get(databaseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDatabaseConfig(databaseId: string): Promise<DatabaseConfig | null> {
|
||||||
|
try {
|
||||||
|
const result = await mainPool.query(
|
||||||
|
'SELECT * FROM databases WHERE id = $1',
|
||||||
|
[databaseId]
|
||||||
|
);
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const row = result.rows[0];
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
type: row.type,
|
||||||
|
host: row.host,
|
||||||
|
port: row.port,
|
||||||
|
database_name: row.database_name,
|
||||||
|
username: row.username,
|
||||||
|
password: row.password,
|
||||||
|
ssl: row.ssl,
|
||||||
|
is_active: row.is_active,
|
||||||
|
aql_base_url: row.aql_base_url,
|
||||||
|
aql_auth_type: row.aql_auth_type,
|
||||||
|
aql_auth_value: row.aql_auth_value,
|
||||||
|
aql_headers: row.aql_headers,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching database config:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getAllDatabaseConfigs(): Promise<DatabaseConfig[]> {
|
async getAllDatabaseConfigs(): Promise<DatabaseConfig[]> {
|
||||||
try {
|
try {
|
||||||
const result = await mainPool.query(
|
const result = await mainPool.query(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { sqlExecutor } from './SqlExecutor';
|
import { sqlExecutor } from './SqlExecutor';
|
||||||
|
import { aqlExecutor } from './AqlExecutor';
|
||||||
import { ScriptQuery, EndpointParameter } from '../types';
|
import { ScriptQuery, EndpointParameter } from '../types';
|
||||||
|
import { databasePoolManager } from './DatabasePoolManager';
|
||||||
|
|
||||||
interface ScriptContext {
|
interface ScriptContext {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
@@ -23,32 +25,60 @@ export class ScriptExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allParams = { ...context.requestParams, ...additionalParams };
|
const allParams = { ...context.requestParams, ...additionalParams };
|
||||||
|
|
||||||
let processedQuery = query.sql;
|
|
||||||
const paramValues: any[] = [];
|
|
||||||
const paramMatches = query.sql.match(/\$\w+/g) || [];
|
|
||||||
const uniqueParams = [...new Set(paramMatches.map(p => p.substring(1)))];
|
|
||||||
|
|
||||||
uniqueParams.forEach((paramName, index) => {
|
|
||||||
const regex = new RegExp(`\\$${paramName}\\b`, 'g');
|
|
||||||
processedQuery = processedQuery.replace(regex, `$${index + 1}`);
|
|
||||||
const value = allParams[paramName];
|
|
||||||
paramValues.push(value !== undefined ? value : null);
|
|
||||||
});
|
|
||||||
|
|
||||||
const dbId = (query as any).database_id || context.databaseId;
|
const dbId = (query as any).database_id || context.databaseId;
|
||||||
|
|
||||||
if (!dbId) {
|
if (!dbId) {
|
||||||
throw new Error(`Database ID not found for query '${queryName}'. Query database_id: ${(query as any).database_id}, Context databaseId: ${context.databaseId}. Please specify database_id in the Script Queries configuration for query '${queryName}'.`);
|
throw new Error(`Database ID not found for query '${queryName}'. Query database_id: ${(query as any).database_id}, Context databaseId: ${context.databaseId}. Please specify database_id in the Script Queries configuration for query '${queryName}'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await sqlExecutor.executeQuery(dbId, processedQuery, paramValues);
|
// Получаем конфигурацию базы данных для определения типа
|
||||||
|
const dbConfig = await databasePoolManager.getDatabaseConfig(dbId);
|
||||||
|
if (!dbConfig) {
|
||||||
|
throw new Error(`Database configuration not found for ID: ${dbId}`);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
// Проверяем тип базы данных и выполняем соответствующий запрос
|
||||||
data: result.rows,
|
if (dbConfig.type === 'aql') {
|
||||||
rowCount: result.rowCount,
|
// AQL запрос
|
||||||
executionTime: result.executionTime,
|
const result = await aqlExecutor.executeAqlQuery(dbId, {
|
||||||
};
|
method: query.aql_method || 'GET',
|
||||||
|
endpoint: query.aql_endpoint || '',
|
||||||
|
body: query.aql_body || '',
|
||||||
|
queryParams: query.aql_query_params || {},
|
||||||
|
parameters: allParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: result.rows,
|
||||||
|
rowCount: result.rowCount,
|
||||||
|
executionTime: result.executionTime,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// SQL запрос
|
||||||
|
if (!query.sql) {
|
||||||
|
throw new Error(`SQL query is required for database '${dbConfig.name}' (type: ${dbConfig.type})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let processedQuery = query.sql;
|
||||||
|
const paramValues: any[] = [];
|
||||||
|
const paramMatches = query.sql.match(/\$\w+/g) || [];
|
||||||
|
const uniqueParams = [...new Set(paramMatches.map(p => p.substring(1)))];
|
||||||
|
|
||||||
|
uniqueParams.forEach((paramName, index) => {
|
||||||
|
const regex = new RegExp(`\\$${paramName}\\b`, 'g');
|
||||||
|
processedQuery = processedQuery.replace(regex, `$${index + 1}`);
|
||||||
|
const value = allParams[paramName];
|
||||||
|
paramValues.push(value !== undefined ? value : null);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sqlExecutor.executeQuery(dbId, processedQuery, paramValues);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: result.rows,
|
||||||
|
rowCount: result.rowCount,
|
||||||
|
executionTime: result.executionTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Создаем асинхронную функцию из кода пользователя
|
// Создаем асинхронную функцию из кода пользователя
|
||||||
@@ -149,21 +179,6 @@ print(json.dumps(result))
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allParams = { ...context.requestParams, ...request.additional_params };
|
const allParams = { ...context.requestParams, ...request.additional_params };
|
||||||
|
|
||||||
// Преобразуем параметры
|
|
||||||
let processedQuery = query.sql;
|
|
||||||
const paramValues: any[] = [];
|
|
||||||
const paramMatches = query.sql.match(/\$\w+/g) || [];
|
|
||||||
const uniqueParams = [...new Set(paramMatches.map(p => p.substring(1)))];
|
|
||||||
|
|
||||||
uniqueParams.forEach((paramName, index) => {
|
|
||||||
const regex = new RegExp(`\\$${paramName}\\b`, 'g');
|
|
||||||
processedQuery = processedQuery.replace(regex, `$${index + 1}`);
|
|
||||||
const value = allParams[paramName];
|
|
||||||
paramValues.push(value !== undefined ? value : null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Используем database_id из запроса, если указан, иначе из контекста
|
|
||||||
const dbId = (query as any).database_id || context.databaseId;
|
const dbId = (query as any).database_id || context.databaseId;
|
||||||
|
|
||||||
if (!dbId) {
|
if (!dbId) {
|
||||||
@@ -173,17 +188,65 @@ print(json.dumps(result))
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await sqlExecutor.executeQuery(
|
// Получаем конфигурацию базы данных для определения типа
|
||||||
dbId,
|
const dbConfig = await databasePoolManager.getDatabaseConfig(dbId);
|
||||||
processedQuery,
|
if (!dbConfig) {
|
||||||
paramValues
|
python.stdin.write(JSON.stringify({
|
||||||
);
|
error: `Database configuration not found for ID: ${dbId}`
|
||||||
|
}) + '\n');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
python.stdin.write(JSON.stringify({
|
// Проверяем тип базы данных и выполняем соответствующий запрос
|
||||||
data: result.rows,
|
if (dbConfig.type === 'aql') {
|
||||||
rowCount: result.rowCount,
|
// AQL запрос
|
||||||
executionTime: result.executionTime,
|
const result = await aqlExecutor.executeAqlQuery(dbId, {
|
||||||
}) + '\n');
|
method: query.aql_method || 'GET',
|
||||||
|
endpoint: query.aql_endpoint || '',
|
||||||
|
body: query.aql_body || '',
|
||||||
|
queryParams: query.aql_query_params || {},
|
||||||
|
parameters: allParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
python.stdin.write(JSON.stringify({
|
||||||
|
data: result.rows,
|
||||||
|
rowCount: result.rowCount,
|
||||||
|
executionTime: result.executionTime,
|
||||||
|
}) + '\n');
|
||||||
|
} else {
|
||||||
|
// SQL запрос
|
||||||
|
if (!query.sql) {
|
||||||
|
python.stdin.write(JSON.stringify({
|
||||||
|
error: `SQL query is required for database '${dbConfig.name}' (type: ${dbConfig.type})`
|
||||||
|
}) + '\n');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Преобразуем параметры
|
||||||
|
let processedQuery = query.sql;
|
||||||
|
const paramValues: any[] = [];
|
||||||
|
const paramMatches = query.sql.match(/\$\w+/g) || [];
|
||||||
|
const uniqueParams = [...new Set(paramMatches.map(p => p.substring(1)))];
|
||||||
|
|
||||||
|
uniqueParams.forEach((paramName, index) => {
|
||||||
|
const regex = new RegExp(`\\$${paramName}\\b`, 'g');
|
||||||
|
processedQuery = processedQuery.replace(regex, `$${index + 1}`);
|
||||||
|
const value = allParams[paramName];
|
||||||
|
paramValues.push(value !== undefined ? value : null);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sqlExecutor.executeQuery(
|
||||||
|
dbId,
|
||||||
|
processedQuery,
|
||||||
|
paramValues
|
||||||
|
);
|
||||||
|
|
||||||
|
python.stdin.write(JSON.stringify({
|
||||||
|
data: result.rows,
|
||||||
|
rowCount: result.rowCount,
|
||||||
|
executionTime: result.executionTime,
|
||||||
|
}) + '\n');
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
python.stdin.write(JSON.stringify({ error: error.message }) + '\n');
|
python.stdin.write(JSON.stringify({ error: error.message }) + '\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,8 +86,13 @@ export interface EndpointParameter {
|
|||||||
|
|
||||||
export interface ScriptQuery {
|
export interface ScriptQuery {
|
||||||
name: string;
|
name: string;
|
||||||
sql: string;
|
sql?: string;
|
||||||
database_id?: string;
|
database_id?: string;
|
||||||
|
// AQL fields for script queries
|
||||||
|
aql_method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||||
|
aql_endpoint?: string;
|
||||||
|
aql_body?: string;
|
||||||
|
aql_query_params?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryResult {
|
export interface QueryResult {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Editor from '@monaco-editor/react';
|
|||||||
interface CodeEditorProps {
|
interface CodeEditorProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
language: 'javascript' | 'python';
|
language: 'javascript' | 'python' | 'json';
|
||||||
height?: string;
|
height?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -377,38 +377,43 @@ export default function EndpointModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">AQL Body (JSON)</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">AQL Body (JSON)</label>
|
||||||
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
||||||
<div>Используйте <code className="bg-blue-100 px-1 rounded">$параметр</code> в JSON для подстановки</div>
|
<div>Используйте <code className="bg-blue-100 px-1 rounded">$параметр</code> в JSON для подстановки</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<CodeEditor
|
||||||
value={formData.aql_body}
|
value={formData.aql_body}
|
||||||
onChange={(e) => setFormData({ ...formData, aql_body: e.target.value })}
|
onChange={(value) => setFormData({ ...formData, aql_body: value })}
|
||||||
className="input w-full font-mono text-sm"
|
language="json"
|
||||||
rows={6}
|
height="150px"
|
||||||
placeholder='{"aql": "select c from COMPOSITION c where c/uid/value= '$compositionId'"}'
|
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Пример: {`{"aql": "select c from COMPOSITION c where c/uid/value='$compositionId'"}`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">AQL Query Parameters (JSON)</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">AQL Query Parameters (JSON)</label>
|
||||||
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
||||||
<div>Формат: <code className="bg-blue-100 px-1 rounded">{`{"key": "value", "CompositionLink": "$linkValue"}`}</code></div>
|
<div>Формат: <code className="bg-blue-100 px-1 rounded">{`{"key": "value", "CompositionLink": "$linkValue"}`}</code></div>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<CodeEditor
|
||||||
value={typeof formData.aql_query_params === 'string' ? formData.aql_query_params : JSON.stringify(formData.aql_query_params, null, 2)}
|
value={typeof formData.aql_query_params === 'string' ? formData.aql_query_params : JSON.stringify(formData.aql_query_params, null, 2)}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(e.target.value);
|
const parsed = JSON.parse(value);
|
||||||
setFormData({ ...formData, aql_query_params: parsed });
|
setFormData({ ...formData, aql_query_params: parsed });
|
||||||
} catch {
|
} catch {
|
||||||
// Игнорируем невалидный JSON - не обновляем состояние
|
// Сохраняем невалидный JSON как строку для последующего редактирования
|
||||||
|
setFormData({ ...formData, aql_query_params: value as any });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="input w-full font-mono text-sm"
|
language="json"
|
||||||
rows={4}
|
height="120px"
|
||||||
placeholder='{"CompositionLink": "ehr:compositions/$compositionId"}'
|
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Пример: {`{"CompositionLink": "ehr:compositions/$compositionId"}`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : formData.execution_type === 'sql' ? (
|
) : formData.execution_type === 'sql' ? (
|
||||||
@@ -450,7 +455,15 @@ export default function EndpointModal({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newQueries = [...formData.script_queries, { name: '', sql: '', database_id: '' }];
|
const newQueries = [...formData.script_queries, {
|
||||||
|
name: '',
|
||||||
|
sql: '',
|
||||||
|
database_id: '',
|
||||||
|
aql_method: 'GET' as 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||||
|
aql_endpoint: '',
|
||||||
|
aql_body: '',
|
||||||
|
aql_query_params: {}
|
||||||
|
}];
|
||||||
setFormData({ ...formData, script_queries: newQueries });
|
setFormData({ ...formData, script_queries: newQueries });
|
||||||
setEditingQueryIndex(newQueries.length - 1);
|
setEditingQueryIndex(newQueries.length - 1);
|
||||||
}}
|
}}
|
||||||
@@ -469,9 +482,14 @@ export default function EndpointModal({
|
|||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<code className="text-sm font-semibold text-gray-900">{query.name || 'Безымянный запрос'}</code>
|
<code className="text-sm font-semibold text-gray-900">{query.name || 'Безымянный запрос'}</code>
|
||||||
{query.database_id && (
|
{query.database_id && (
|
||||||
<span className="text-xs text-gray-500">
|
<>
|
||||||
→ {databases.find(db => db.id === query.database_id)?.name || 'БД не выбрана'}
|
<span className="text-xs text-gray-500">
|
||||||
</span>
|
→ {databases.find(db => db.id === query.database_id)?.name || 'БД не выбрана'}
|
||||||
|
</span>
|
||||||
|
{databases.find(db => db.id === query.database_id)?.type === 'aql' && (
|
||||||
|
<span className="text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded">AQL</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{query.sql && (
|
{query.sql && (
|
||||||
@@ -479,6 +497,11 @@ export default function EndpointModal({
|
|||||||
{query.sql.substring(0, 100)}{query.sql.length > 100 ? '...' : ''}
|
{query.sql.substring(0, 100)}{query.sql.length > 100 ? '...' : ''}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{query.aql_endpoint && (
|
||||||
|
<div className="text-xs text-gray-600 font-mono bg-purple-50 p-2 rounded mt-1">
|
||||||
|
<span className="text-purple-700 font-semibold">{query.aql_method}</span> {query.aql_endpoint}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 ml-4">
|
<div className="flex gap-2 ml-4">
|
||||||
<button
|
<button
|
||||||
@@ -666,22 +689,112 @@ export default function EndpointModal({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">SQL Запрос</label>
|
{/* Определяем тип выбранной базы данных */}
|
||||||
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
{(() => {
|
||||||
<div><strong>Совет:</strong> Используйте <code className="bg-blue-100 px-1 rounded">$имяПараметра</code> для параметров из запроса или дополнительных параметров из скрипта.</div>
|
const selectedDb = databases.find(db => db.id === formData.script_queries[editingQueryIndex]?.database_id);
|
||||||
</div>
|
const isAql = selectedDb?.type === 'aql';
|
||||||
<SqlEditor
|
|
||||||
value={formData.script_queries[editingQueryIndex]?.sql || ''}
|
return isAql ? (
|
||||||
onChange={(value) => {
|
<>
|
||||||
const newQueries = [...formData.script_queries];
|
{/* AQL Fields */}
|
||||||
newQueries[editingQueryIndex].sql = value;
|
<div>
|
||||||
setFormData({ ...formData, script_queries: newQueries });
|
<label className="block text-sm font-medium text-gray-700 mb-1">AQL HTTP Метод</label>
|
||||||
}}
|
<select
|
||||||
databaseId={formData.script_queries[editingQueryIndex]?.database_id || ''}
|
value={formData.script_queries[editingQueryIndex]?.aql_method || 'GET'}
|
||||||
height="400px"
|
onChange={(e) => {
|
||||||
/>
|
const newQueries = [...formData.script_queries];
|
||||||
</div>
|
newQueries[editingQueryIndex].aql_method = e.target.value as any;
|
||||||
|
setFormData({ ...formData, script_queries: newQueries });
|
||||||
|
}}
|
||||||
|
className="input w-full"
|
||||||
|
>
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">AQL Endpoint URL</label>
|
||||||
|
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
||||||
|
<div>Используйте <code className="bg-blue-100 px-1 rounded">$параметр</code> для подстановки</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.script_queries[editingQueryIndex]?.aql_endpoint || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newQueries = [...formData.script_queries];
|
||||||
|
newQueries[editingQueryIndex].aql_endpoint = e.target.value;
|
||||||
|
setFormData({ ...formData, script_queries: newQueries });
|
||||||
|
}}
|
||||||
|
className="input w-full"
|
||||||
|
placeholder="/query"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">AQL Body (JSON)</label>
|
||||||
|
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
||||||
|
<div>Используйте <code className="bg-blue-100 px-1 rounded">$параметр</code> в JSON для подстановки</div>
|
||||||
|
</div>
|
||||||
|
<CodeEditor
|
||||||
|
value={formData.script_queries[editingQueryIndex]?.aql_body || ''}
|
||||||
|
onChange={(value) => {
|
||||||
|
const newQueries = [...formData.script_queries];
|
||||||
|
newQueries[editingQueryIndex].aql_body = value;
|
||||||
|
setFormData({ ...formData, script_queries: newQueries });
|
||||||
|
}}
|
||||||
|
language="json"
|
||||||
|
height="200px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">AQL Query Parameters (JSON)</label>
|
||||||
|
<CodeEditor
|
||||||
|
value={typeof formData.script_queries[editingQueryIndex]?.aql_query_params === 'string'
|
||||||
|
? formData.script_queries[editingQueryIndex]?.aql_query_params
|
||||||
|
: JSON.stringify(formData.script_queries[editingQueryIndex]?.aql_query_params || {}, null, 2)}
|
||||||
|
onChange={(value) => {
|
||||||
|
const newQueries = [...formData.script_queries];
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
newQueries[editingQueryIndex].aql_query_params = parsed;
|
||||||
|
} catch {
|
||||||
|
newQueries[editingQueryIndex].aql_query_params = value as any;
|
||||||
|
}
|
||||||
|
setFormData({ ...formData, script_queries: newQueries });
|
||||||
|
}}
|
||||||
|
language="json"
|
||||||
|
height="150px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* SQL Field */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">SQL Запрос</label>
|
||||||
|
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
||||||
|
<div><strong>Совет:</strong> Используйте <code className="bg-blue-100 px-1 rounded">$имяПараметра</code> для параметров из запроса или дополнительных параметров из скрипта.</div>
|
||||||
|
</div>
|
||||||
|
<SqlEditor
|
||||||
|
value={formData.script_queries[editingQueryIndex]?.sql || ''}
|
||||||
|
onChange={(value) => {
|
||||||
|
const newQueries = [...formData.script_queries];
|
||||||
|
newQueries[editingQueryIndex].sql = value;
|
||||||
|
setFormData({ ...formData, script_queries: newQueries });
|
||||||
|
}}
|
||||||
|
databaseId={formData.script_queries[editingQueryIndex]?.database_id || ''}
|
||||||
|
height="400px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 border-t border-gray-200 flex gap-3">
|
<div className="p-6 border-t border-gray-200 flex gap-3">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,473 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { dbManagementApi } from '@/services/api';
|
|
||||||
import { useAuthStore } from '@/stores/authStore';
|
|
||||||
import { Database, Plus, Edit2, Trash2 } from 'lucide-react';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import Dialog from '@/components/Dialog';
|
|
||||||
|
|
||||||
export default function Databases() {
|
|
||||||
const { user } = useAuthStore();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
|
||||||
const [editingDatabase, setEditingDatabase] = useState<any | null>(null);
|
|
||||||
const isAdmin = user?.role === 'admin';
|
|
||||||
const [dialog, setDialog] = useState<{
|
|
||||||
isOpen: boolean;
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
type: 'alert' | 'confirm';
|
|
||||||
onConfirm?: () => void;
|
|
||||||
}>({
|
|
||||||
isOpen: false,
|
|
||||||
title: '',
|
|
||||||
message: '',
|
|
||||||
type: 'alert',
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: databases, isLoading } = useQuery({
|
|
||||||
queryKey: ['databases'],
|
|
||||||
queryFn: () => dbManagementApi.getAll().then(res => res.data),
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
|
||||||
mutationFn: (id: string) => dbManagementApi.delete(id),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['databases'] });
|
|
||||||
toast.success('База данных удалена');
|
|
||||||
},
|
|
||||||
onError: () => toast.error('Не удалось удалить базу данных'),
|
|
||||||
});
|
|
||||||
|
|
||||||
const testConnection = async (databaseId: string) => {
|
|
||||||
try {
|
|
||||||
const { data } = await dbManagementApi.test(databaseId);
|
|
||||||
if (data.success) {
|
|
||||||
toast.success('Подключение успешно!');
|
|
||||||
} else {
|
|
||||||
toast.error('Ошибка подключения');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Ошибка тестирования подключения');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (database: any) => {
|
|
||||||
setEditingDatabase(database);
|
|
||||||
setShowModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreate = () => {
|
|
||||||
setEditingDatabase(null);
|
|
||||||
setShowModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
|
||||||
setDialog({
|
|
||||||
isOpen: true,
|
|
||||||
title: 'Подтверждение',
|
|
||||||
message: 'Вы уверены, что хотите удалить эту базу данных?',
|
|
||||||
type: 'confirm',
|
|
||||||
onConfirm: () => {
|
|
||||||
deleteMutation.mutate(id);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Базы данных</h1>
|
|
||||||
<p className="text-gray-600">Управление подключениями к базам данных</p>
|
|
||||||
</div>
|
|
||||||
{isAdmin && (
|
|
||||||
<button onClick={handleCreate} className="btn btn-primary flex items-center gap-2">
|
|
||||||
<Plus size={20} />
|
|
||||||
Добавить базу данных
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
{databases?.map((db) => (
|
|
||||||
<div key={db.id} className="card p-6">
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="bg-primary-100 p-3 rounded-lg">
|
|
||||||
<Database size={24} className="text-primary-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-semibold text-gray-900">{db.name}</h3>
|
|
||||||
<p className="text-sm text-gray-500">{db.type}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isAdmin && (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => handleEdit(db)}
|
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
title="Редактировать"
|
|
||||||
>
|
|
||||||
<Edit2 size={18} className="text-gray-600" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDelete(db.id)}
|
|
||||||
className="p-2 hover:bg-red-50 rounded-lg transition-colors"
|
|
||||||
title="Удалить"
|
|
||||||
>
|
|
||||||
<Trash2 size={18} className="text-red-600" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 text-sm mb-4">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600">Хост:</span>
|
|
||||||
<span className="font-medium">{db.host}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600">Порт:</span>
|
|
||||||
<span className="font-medium">{db.port}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600">База данных:</span>
|
|
||||||
<span className="font-medium">{db.database_name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600">Пользователь:</span>
|
|
||||||
<span className="font-medium">{db.username}</span>
|
|
||||||
</div>
|
|
||||||
{db.is_active !== undefined && (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600">Статус:</span>
|
|
||||||
<span className={`font-medium ${db.is_active ? 'text-green-600' : 'text-red-600'}`}>
|
|
||||||
{db.is_active ? 'Активна' : 'Неактивна'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => testConnection(db.id)}
|
|
||||||
className="btn btn-primary w-full"
|
|
||||||
>
|
|
||||||
Тест подключения
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{databases?.length === 0 && (
|
|
||||||
<div className="col-span-2 text-center py-12">
|
|
||||||
<p className="text-gray-500">
|
|
||||||
{isAdmin
|
|
||||||
? 'Базы данных не настроены. Добавьте первую базу данных.'
|
|
||||||
: 'Базы данных не настроены.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isAdmin && !isLoading && databases && databases.length > 0 && (
|
|
||||||
<div className="card p-6 mt-6">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">Информация</h2>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Базы данных управляются через интерфейс администратора.
|
|
||||||
Вы можете добавлять, редактировать и удалять подключения к базам данных.
|
|
||||||
Каждый эндпоинт использует одно из этих подключений для выполнения SQL запросов.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showModal && (
|
|
||||||
<DatabaseModal
|
|
||||||
database={editingDatabase}
|
|
||||||
onClose={() => setShowModal(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
isOpen={dialog.isOpen}
|
|
||||||
onClose={() => setDialog({ ...dialog, isOpen: false })}
|
|
||||||
title={dialog.title}
|
|
||||||
message={dialog.message}
|
|
||||||
type={dialog.type}
|
|
||||||
onConfirm={dialog.onConfirm}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DatabaseModal({
|
|
||||||
database,
|
|
||||||
onClose,
|
|
||||||
}: {
|
|
||||||
database: any | null;
|
|
||||||
onClose: () => void;
|
|
||||||
}) {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: database?.name || '',
|
|
||||||
type: database?.type || 'postgresql',
|
|
||||||
host: database?.host || 'localhost',
|
|
||||||
port: database?.port || 5432,
|
|
||||||
database_name: database?.database_name || '',
|
|
||||||
username: database?.username || '',
|
|
||||||
password: database?.password || '',
|
|
||||||
ssl: database?.ssl || false,
|
|
||||||
is_active: database?.is_active !== undefined ? database.is_active : true,
|
|
||||||
// AQL-specific fields
|
|
||||||
aql_base_url: database?.aql_base_url || '',
|
|
||||||
aql_auth_type: database?.aql_auth_type || 'basic',
|
|
||||||
aql_auth_value: database?.aql_auth_value || '',
|
|
||||||
aql_headers: database?.aql_headers || {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const saveMutation = useMutation({
|
|
||||||
mutationFn: (data: any) =>
|
|
||||||
database ? dbManagementApi.update(database.id, data) : dbManagementApi.create(data),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['databases'] });
|
|
||||||
toast.success(database ? 'База данных обновлена' : 'База данных создана');
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
toast.error(error.response?.data?.error || 'Не удалось сохранить базу данных');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
saveMutation.mutate(formData);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
|
||||||
<div className="p-6 border-b border-gray-200">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900">
|
|
||||||
{database ? 'Редактировать базу данных' : 'Добавить базу данных'}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Название</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
||||||
className="input w-full"
|
|
||||||
placeholder="Основная база PostgreSQL"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Тип</label>
|
|
||||||
<select
|
|
||||||
value={formData.type}
|
|
||||||
onChange={(e) => {
|
|
||||||
console.log('🔄 Database type changed to:', e.target.value);
|
|
||||||
setFormData({ ...formData, type: e.target.value });
|
|
||||||
}}
|
|
||||||
className="input w-full"
|
|
||||||
>
|
|
||||||
<option value="postgresql">PostgreSQL</option>
|
|
||||||
<option value="aql">AQL (HTTP API)</option>
|
|
||||||
</select>
|
|
||||||
{/* DEBUG: AQL option should be visible above */}
|
|
||||||
<div className="text-xs text-gray-500 mt-1">Available types: PostgreSQL, AQL (HTTP API)</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{formData.type === 'aql' ? (
|
|
||||||
<>
|
|
||||||
{/* AQL Fields */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">AQL Base URL *</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.aql_base_url}
|
|
||||||
onChange={(e) => setFormData({ ...formData, aql_base_url: e.target.value })}
|
|
||||||
className="input w-full"
|
|
||||||
placeholder="http://api.ehrdb.ncms-i.ru/api/rest/v1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Тип аутентификации</label>
|
|
||||||
<select
|
|
||||||
value={formData.aql_auth_type}
|
|
||||||
onChange={(e) => setFormData({ ...formData, aql_auth_type: e.target.value })}
|
|
||||||
className="input w-full"
|
|
||||||
>
|
|
||||||
<option value="basic">Basic Auth</option>
|
|
||||||
<option value="bearer">Bearer Token</option>
|
|
||||||
<option value="custom">Custom Header</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
{formData.aql_auth_type === 'basic' && 'Basic Auth Value (Base64)'}
|
|
||||||
{formData.aql_auth_type === 'bearer' && 'Bearer Token'}
|
|
||||||
{formData.aql_auth_type === 'custom' && 'Custom Authorization Value'}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
required={!database}
|
|
||||||
value={formData.aql_auth_value}
|
|
||||||
onChange={(e) => setFormData({ ...formData, aql_auth_value: e.target.value })}
|
|
||||||
className="input w-full"
|
|
||||||
placeholder={database ? '••••••••' : 'Введите значение'}
|
|
||||||
/>
|
|
||||||
{database && (
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Оставьте пустым, чтобы не менять</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Дополнительные заголовки (JSON)
|
|
||||||
<span className="text-xs text-gray-500 ml-2">необязательно</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={typeof formData.aql_headers === 'string' ? formData.aql_headers : JSON.stringify(formData.aql_headers, null, 2)}
|
|
||||||
onChange={(e) => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(e.target.value);
|
|
||||||
setFormData({ ...formData, aql_headers: parsed });
|
|
||||||
} catch {
|
|
||||||
setFormData({ ...formData, aql_headers: e.target.value });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="input w-full font-mono text-sm"
|
|
||||||
rows={4}
|
|
||||||
placeholder='{"x-dbrole": "KIS.EMIAS.XАПИД", "Hack-Time": "true"}'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<label className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.is_active}
|
|
||||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700">Активна</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* PostgreSQL Fields */}
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Хост</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.host}
|
|
||||||
onChange={(e) => setFormData({ ...formData, host: e.target.value })}
|
|
||||||
className="input w-full"
|
|
||||||
placeholder="localhost"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Порт</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
required
|
|
||||||
value={formData.port}
|
|
||||||
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })}
|
|
||||||
className="input w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Имя базы данных</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.database_name}
|
|
||||||
onChange={(e) => setFormData({ ...formData, database_name: e.target.value })}
|
|
||||||
className="input w-full"
|
|
||||||
placeholder="my_database"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Пользователь</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={formData.username}
|
|
||||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
|
||||||
className="input w-full"
|
|
||||||
placeholder="postgres"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Пароль</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
required={!database}
|
|
||||||
value={formData.password}
|
|
||||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
||||||
className="input w-full"
|
|
||||||
placeholder={database ? '••••••••' : 'Введите пароль'}
|
|
||||||
/>
|
|
||||||
{database && (
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Оставьте пустым, чтобы не менять пароль</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<label className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.ssl}
|
|
||||||
onChange={(e) => setFormData({ ...formData, ssl: e.target.checked })}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700">Использовать SSL</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.is_active}
|
|
||||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700">Активна</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-3 pt-4 border-t border-gray-200">
|
|
||||||
<button type="button" onClick={onClose} className="btn btn-secondary">
|
|
||||||
Отмена
|
|
||||||
</button>
|
|
||||||
<button type="submit" disabled={saveMutation.isPending} className="btn btn-primary">
|
|
||||||
{saveMutation.isPending ? 'Сохранение...' : 'Сохранить'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/authStore';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { User, Lock, UserCircle, Database, Plus, Edit2, Trash2, Eye, EyeOff, Users } from 'lucide-react';
|
import { User, Lock, UserCircle, Database, Plus, Edit2, Trash2, Eye, EyeOff, Users } from 'lucide-react';
|
||||||
import Dialog from '@/components/Dialog';
|
import Dialog from '@/components/Dialog';
|
||||||
|
import CodeEditor from '@/components/CodeEditor';
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
@@ -557,24 +558,27 @@ function DatabaseModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Дополнительные заголовки (JSON)
|
Дополнительные заголовки (JSON)
|
||||||
<span className="text-xs text-gray-500 ml-2">необязательно</span>
|
<span className="text-xs text-gray-500 ml-2">необязательно</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<CodeEditor
|
||||||
value={typeof formData.aql_headers === 'string' ? formData.aql_headers : JSON.stringify(formData.aql_headers, null, 2)}
|
value={typeof formData.aql_headers === 'string' ? formData.aql_headers : JSON.stringify(formData.aql_headers, null, 2)}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(e.target.value);
|
const parsed = JSON.parse(value);
|
||||||
setFormData({ ...formData, aql_headers: parsed });
|
setFormData({ ...formData, aql_headers: parsed });
|
||||||
} catch {
|
} catch {
|
||||||
setFormData({ ...formData, aql_headers: e.target.value });
|
// Сохраняем невалидный JSON как строку для последующего редактирования
|
||||||
|
setFormData({ ...formData, aql_headers: value as any });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="input w-full font-mono text-sm"
|
language="json"
|
||||||
rows={4}
|
height="150px"
|
||||||
placeholder='{"x-dbrole": "KIS.EMIAS.XАПИД", "Hack-Time": "true"}'
|
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Пример: {`{"x-dbrole": "KIS.EMIAS.XАПИД", "Hack-Time": "true"}`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
|||||||
@@ -46,8 +46,13 @@ export interface EndpointParameter {
|
|||||||
|
|
||||||
export interface ScriptQuery {
|
export interface ScriptQuery {
|
||||||
name: string;
|
name: string;
|
||||||
sql: string;
|
sql?: string;
|
||||||
database_id?: string;
|
database_id?: string;
|
||||||
|
// AQL fields for script queries
|
||||||
|
aql_method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||||
|
aql_endpoint?: string;
|
||||||
|
aql_body?: string;
|
||||||
|
aql_query_params?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Endpoint {
|
export interface Endpoint {
|
||||||
|
|||||||
Reference in New Issue
Block a user