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:
ARCHITECT
2026-01-17 23:28:37 +00:00
parent aa4fb69013
commit 463ae90364
3 changed files with 126 additions and 3 deletions

View File

@@ -174,3 +174,20 @@ export const fetchTableRelations = async (schemaId: string): Promise<TableRelati
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();
};

View File

@@ -582,3 +582,70 @@ svg {
max-height: calc(100vh - 150px);
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;
}

View File

@@ -1,6 +1,6 @@
import type { DBSchema, TableSchema, TableRelation } 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');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -42,7 +42,7 @@ export class TablesGraph {
this.container = container;
this.serverName = serverName;
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> {
@@ -254,7 +254,7 @@ export class TablesGraph {
const sidebar = this.container.querySelector('#graph-sidebar');
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 = `
<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}">
</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);
@@ -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 {