Add schema sync button to sidebar
- Add syncSchemas() API function calling /rpc/sync_all_schemas - Add sync button in TablesGraph sidebar - Show sync results per schema (ok, pending, error) - Auto-reload current schema after successful sync - Add CSS styles for sync button with spinning animation - Add 'storage' to visible categories Backend: PostgreSQL function infra.sync_all_schemas() syncs tables and columns from gitea and architect databases via dblink. HST and DECK remote sync pending configuration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -174,3 +174,20 @@ export const fetchTableRelations = async (schemaId: string): Promise<TableRelati
|
|||||||
type: r.relation_type as 'fk' | 'ref' | 'logical'
|
type: r.relation_type as 'fk' | 'ref' | 'logical'
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sync schemas from source databases
|
||||||
|
export interface SyncResult {
|
||||||
|
schema_id: string;
|
||||||
|
tables_synced: number;
|
||||||
|
columns_synced: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const syncSchemas = async (): Promise<SyncResult[]> => {
|
||||||
|
const res = await fetch(`${API_BASE}/rpc/sync_all_schemas`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Sync error: ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|||||||
@@ -582,3 +582,70 @@ svg {
|
|||||||
max-height: calc(100vh - 150px);
|
max-height: calc(100vh - 150px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sync button styles */
|
||||||
|
.btn-sync {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: background 0.2s, opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sync:hover:not(:disabled) {
|
||||||
|
background: var(--primary-dark, #5a6be8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sync:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon.spinning {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-status {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-result {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-result.success {
|
||||||
|
background: rgba(76, 175, 80, 0.15);
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-result.pending {
|
||||||
|
background: rgba(255, 152, 0, 0.15);
|
||||||
|
color: #FF9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-result.error {
|
||||||
|
background: rgba(244, 67, 54, 0.15);
|
||||||
|
color: #F44336;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DBSchema, TableSchema, TableRelation } from '../api/schemas.ts';
|
import type { DBSchema, TableSchema, TableRelation } from '../api/schemas.ts';
|
||||||
import { CATEGORY_COLORS, DB_SCHEMAS } from '../api/schemas.ts';
|
import { CATEGORY_COLORS, DB_SCHEMAS } from '../api/schemas.ts';
|
||||||
import { fetchColumns, fetchTables, fetchTableRelations, type Column } from '../api/infrastructure.ts';
|
import { fetchColumns, fetchTables, fetchTableRelations, syncSchemas, type Column, type SyncResult } from '../api/infrastructure.ts';
|
||||||
|
|
||||||
type D3Module = typeof import('d3');
|
type D3Module = typeof import('d3');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -42,7 +42,7 @@ export class TablesGraph {
|
|||||||
this.container = container;
|
this.container = container;
|
||||||
this.serverName = serverName;
|
this.serverName = serverName;
|
||||||
this.schema = schema;
|
this.schema = schema;
|
||||||
this.showCategories = new Set(['core', 'api', 'graph', 'tree', 'library', 'data', 'system', 'directus', 'comm', 'repo', 'user', 'action', 'auth', 'agent', 'context', 'creds']);
|
this.showCategories = new Set(['core', 'api', 'graph', 'tree', 'library', 'data', 'system', 'directus', 'comm', 'repo', 'user', 'action', 'auth', 'agent', 'context', 'creds', 'storage']);
|
||||||
}
|
}
|
||||||
|
|
||||||
async mount(): Promise<void> {
|
async mount(): Promise<void> {
|
||||||
@@ -254,7 +254,7 @@ export class TablesGraph {
|
|||||||
const sidebar = this.container.querySelector('#graph-sidebar');
|
const sidebar = this.container.querySelector('#graph-sidebar');
|
||||||
if (!sidebar) return;
|
if (!sidebar) return;
|
||||||
|
|
||||||
const categories: TableSchema['category'][] = ['core', 'api', 'graph', 'tree', 'library', 'data', 'directus', 'system', 'comm', 'repo', 'user', 'action', 'auth', 'agent', 'context', 'creds'];
|
const categories: TableSchema['category'][] = ['core', 'api', 'graph', 'tree', 'library', 'data', 'directus', 'system', 'comm', 'repo', 'user', 'action', 'auth', 'agent', 'context', 'creds', 'storage'];
|
||||||
|
|
||||||
sidebar.innerHTML = `
|
sidebar.innerHTML = `
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
@@ -323,6 +323,14 @@ export class TablesGraph {
|
|||||||
<input type="range" id="graph-link-dist" min="60" max="200" value="${this.linkDist}">
|
<input type="range" id="graph-link-dist" min="60" max="200" value="${this.linkDist}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="graph-section">
|
||||||
|
<div class="graph-section-title">Sincronizacion</div>
|
||||||
|
<button class="btn btn-sync" id="btn-sync">
|
||||||
|
<span class="sync-icon">↻</span> Sincronizar Schemas
|
||||||
|
</button>
|
||||||
|
<div id="sync-status" class="sync-status"></div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.bindSidebarEvents(sidebar as HTMLElement);
|
this.bindSidebarEvents(sidebar as HTMLElement);
|
||||||
@@ -379,6 +387,37 @@ export class TablesGraph {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync button
|
||||||
|
const syncBtn = sidebar.querySelector<HTMLButtonElement>('#btn-sync');
|
||||||
|
const syncStatus = sidebar.querySelector('#sync-status');
|
||||||
|
if (syncBtn && syncStatus) {
|
||||||
|
syncBtn.onclick = async () => {
|
||||||
|
syncBtn.disabled = true;
|
||||||
|
syncBtn.innerHTML = '<span class="sync-icon spinning">↻</span> Sincronizando...';
|
||||||
|
syncStatus.innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await syncSchemas();
|
||||||
|
syncStatus.innerHTML = results.map(r =>
|
||||||
|
`<div class="sync-result ${r.status === 'ok' ? 'success' : r.status.includes('pending') ? 'pending' : 'error'}">
|
||||||
|
<strong>${r.schema_id}</strong>: ${r.status === 'ok' ? `${r.columns_synced} cols` : r.status}
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
// Reload current schema if it was synced
|
||||||
|
const currentSynced = results.find(r => r.schema_id === this.serverName && r.status === 'ok');
|
||||||
|
if (currentSynced) {
|
||||||
|
setTimeout(() => this.mount(), 1000);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
syncStatus.innerHTML = `<div class="sync-result error">Error: ${e}</div>`;
|
||||||
|
} finally {
|
||||||
|
syncBtn.disabled = false;
|
||||||
|
syncBtn.innerHTML = '<span class="sync-icon">↻</span> Sincronizar Schemas';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderLegend(): void {
|
private renderLegend(): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user