From 13e7a45f868738abb461f7a0395db29d5e0ac92f Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 10 Jun 2025 13:19:37 +0300 Subject: [PATCH 1/4] foreign key preview, fix styles --- .../components/dashboard/db-table/db-table.component.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table/db-table.component.css b/frontend/src/app/components/dashboard/db-table/db-table.component.css index 5bcc5b3b8..8f7d1c70b 100644 --- a/frontend/src/app/components/dashboard/db-table/db-table.component.css +++ b/frontend/src/app/components/dashboard/db-table/db-table.component.css @@ -535,14 +535,17 @@ tr.mat-row:hover { .foreign-key-button { background: transparent; - border: none; + border: 1px solid transparent; + border-radius: 12px; color: var(--color-accentedPalette-500); + cursor: pointer; font-size: inherit; padding: 2px 8px; overflow: hidden; text-overflow: ellipsis; text-align: left; - width: 100%; + max-width: 100%; + transition: background 100ms, border-color 100ms; } .foreign-key-button_selected { From 3b539882cbe430c5f2dd02600103eab89efcb1cc Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 10 Jun 2025 14:37:03 +0300 Subject: [PATCH 2/4] table view: foreign key hover --- .../app/components/dashboard/db-table/db-table.component.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/app/components/dashboard/db-table/db-table.component.css b/frontend/src/app/components/dashboard/db-table/db-table.component.css index 8f7d1c70b..9404e6aad 100644 --- a/frontend/src/app/components/dashboard/db-table/db-table.component.css +++ b/frontend/src/app/components/dashboard/db-table/db-table.component.css @@ -548,6 +548,11 @@ tr.mat-row:hover { transition: background 100ms, border-color 100ms; } +.foreign-key-button:hover{ + background-color: var(--color-accentedPalette-100); + border: 1px solid var(--color-accentedPalette-300); +} + .foreign-key-button_selected { background-color: var(--color-accentedPalette-100); border-radius: 12px; From 12d8d3cc0ac265834c1d0bd356cbd257dacd8476 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 10 Jun 2025 21:20:32 +0300 Subject: [PATCH 3/4] table view: related records on record view; fix default table. --- .../dashboard/dashboard.component.ts | 4 +- .../db-table-row-view.component.css | 71 ++++++++++ .../db-table-row-view.component.html | 54 ++++++++ .../db-table-row-view.component.ts | 127 +++++++++++++++++- .../dashboard/db-table/db-table.component.ts | 21 ++- .../dashboard/db-tables-data-source.ts | 30 ++++- frontend/src/app/models/table.ts | 5 + .../src/app/services/connections.service.ts | 3 +- 8 files changed, 293 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/components/dashboard/dashboard.component.ts b/frontend/src/app/components/dashboard/dashboard.component.ts index cdbfc0483..e2240f3a7 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.ts +++ b/frontend/src/app/components/dashboard/dashboard.component.ts @@ -129,15 +129,13 @@ export class DashboardComponent implements OnInit, OnDestroy { } get defaultTableToOpen () { - console.log('dashboard component get defaultTableToOpen'); - console.log(this._connections.defaultTableToOpen); return this._connections.defaultTableToOpen; } ngOnInit() { this.connectionID = this._connections.currentConnectionID; // this.isTestConnection = this._connections.currentConnection.isTestConnection; - this.dataSource = new TablesDataSource(this._tables, this._connections, this._uiSettings); + this.dataSource = new TablesDataSource(this._tables, this._connections, this._uiSettings, this._tableRow); this._tableState.cast.subscribe(row => { this.selectedRow = row; diff --git a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.css b/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.css index baa52cac4..9566b057f 100644 --- a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.css +++ b/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.css @@ -82,4 +82,75 @@ width: 100%; margin-top: 8px; margin-bottom: 16px; +} + +.related-records__title { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 8px 0 16px; +} + +.related-records__title h3 { + margin: 0 !important; +} + +.related-records__toggle_open { + transform: rotate(180deg); + transition: transform 200ms ease; +} + +.related-records__accordion { + display: block; + margin-left: 8px; + margin-bottom: 16px; + width: calc(100% - 16px); +} + +.related-records__header { + padding: 0 8px; +} + +.related-records__table-name { + flex: 1 0 auto; +} + +.related-records__actions { + flex-grow: 0; + justify-content: flex-end; +} + +.related-record { + --mdc-list-list-item-two-line-container-height: 60px; + + padding-left: 8px; + padding-right: 8px; +} + +.related-record ::ng-deep .mdc-list-item__primary-text::before { + height: 24px; +} + +.related-records__panel ::ng-deep .mat-expansion-panel-body { + padding: 0 8px; +} + +.loading-related-records { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 8px 0 16px; +} + +.loading-related-records__title { + mix-blend-mode: normal !important; + height: 28px; + width: 120px; +} + +.loading-related-records__button { + mix-blend-mode: normal !important; + border-radius: 50%; + height: 36px; + width: 36px; } \ No newline at end of file diff --git a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.html b/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.html index 4c52d5d59..47c3ee531 100644 --- a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.html @@ -24,6 +24,60 @@

Preview


+ +
+ + + + + {{referencedTable.displayTableName}} + + + Absent + + settings + + + open_in_new + + + + + + + {{row[referencedRecords[referencedTable.table_name].identityColumn]}} + + + {{field_name}}: + {{ row[field_name]}} + + + + + + +
+ +
+ +
+
{{column.normalizedTitle}} diff --git a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.ts b/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.ts index 4305f9251..c9fd3fc81 100644 --- a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.ts @@ -1,16 +1,23 @@ import { ActivatedRoute, RouterModule } from '@angular/router'; -import { Component, Input, OnInit } from '@angular/core'; -import { TableRow, Widget } from 'src/app/models/table'; +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { TableField, TableRow, Widget } from 'src/app/models/table'; +import { normalizeFieldName, normalizeTableName } from 'src/app/lib/normalize'; import { ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import JsonURL from "@jsonurl/jsonurl"; import { MatButtonModule } from '@angular/material/button'; +import { MatExpansionModule } from '@angular/material/expansion'; import { MatIconModule } from '@angular/material/icon'; +import { MatListModule } from '@angular/material/list'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { NotificationsService } from 'src/app/services/notifications.service'; import { PlaceholderRecordViewComponent } from '../../skeletons/placeholder-record-view/placeholder-record-view.component'; import { TableStateService } from 'src/app/services/table-state.service'; -import { normalizeFieldName } from 'src/app/lib/normalize'; +import { TablesService } from 'src/app/services/tables.service'; +import { formatFieldValue } from 'src/app/lib/format-field-value'; @Component({ selector: 'app-db-table-row-view', @@ -21,41 +28,151 @@ import { normalizeFieldName } from 'src/app/lib/normalize'; MatButtonModule, ClipboardModule, MatTooltipModule, + MatExpansionModule, + MatProgressSpinnerModule, + MatListModule, RouterModule, CommonModule, PlaceholderRecordViewComponent ] }) -export class DbTableRowViewComponent implements OnInit { +export class DbTableRowViewComponent implements OnInit, OnDestroy { @Input() activeFilters: object; + public selectedRowCast: any; public selectedRow: TableRow; public columns: { title: string; normalizedTitle: string; }[] = []; + public referencedTables: { table_name: string; displayTableName: string; columns: string[] }[] = []; + public referencedTablesURLParams: any; + public referencedRecords: {} = {}; + public referencedRecordsShown: boolean = false; constructor( + private _tables: TablesService, private _tableState: TableStateService, private _notifications: NotificationsService, private route: ActivatedRoute, ) { } ngOnInit(): void { - this._tableState.cast.subscribe((row) => { + // this.connectionID = this._connections.connectionID; + + this.selectedRowCast = this._tableState.cast.subscribe((row) => { + row && console.log('Row selected:', row.primaryKeys); this.selectedRow = row; if (row && row.columnsOrder) { const columnsOrder = this.selectedRow.columnsOrder.length ? this.selectedRow.columnsOrder : Object.keys(this.selectedRow.record); + this.columns = columnsOrder.map(column => { return { title: column, normalizedTitle: normalizeFieldName(column) } }) + + if (row.relatedRecords?.referenced_by.length) { + this.referencedRecords = {}; + + this.referencedTables = row.relatedRecords.referenced_by + .map((table: any) => { return {...table, displayTableName: table.display_name || normalizeTableName(table.table_name)}}); + + this.referencedTablesURLParams = row.relatedRecords.referenced_by + .map((table: any) => { + const params = {[table.column_name]: { + eq: row.record[row.relatedRecords.referenced_on_column_name] + }}; + return { + filters: JsonURL.stringify(params), + page_index: 0 + }}); + + row.relatedRecords.referenced_by.forEach((table: any) => { + const filters = {[table.column_name]: { + eq: row.record[row.relatedRecords.referenced_on_column_name] + }}; + + this._tables.fetchTable({ + connectionID: row.connectionID, + tableName: table.table_name, + requstedPage: 1, + chunkSize: 30, + filters + }).subscribe((res) => { + let identityColumn = res.identity_column; + let fieldsOrder = []; + + const foreignKeyMap = {}; + for (const fk of res.foreignKeys) { + foreignKeyMap[fk.column_name] = fk.referenced_column_name; + } + + // Format each row + const formattedRows = res.rows.map(row => { + const formattedRow = {}; + + for (const key in row) { + if (foreignKeyMap[key] && typeof row[key] === 'object' && row[key] !== null) { + const preferredKey = Object.keys(row[key]).find(k => k !== foreignKeyMap[key]); + formattedRow[key] = preferredKey ? row[key][preferredKey] : row[key][foreignKeyMap[key]]; + } else { + formattedRow[key] = formatFieldValue(row[key], res.structure.find((field: TableField) => field.column_name === key)?.data_type || 'text'); + } + } + return formattedRow; + }) + + if (res.identity_column && res.list_fields.length) { + identityColumn = res.identity_column; + fieldsOrder = res.list_fields.filter((field: string) => field !== res.identity_column).slice(0, 3); + } + + if (res.identity_column && !res.list_fields.length) { + identityColumn = res.identity_column; + fieldsOrder = res.structure.filter((field: TableField) => field.column_name !== res.identity_column).map((field: TableField) => field.column_name).slice(0, 3); + } + + if (!res.identity_column && res.list_fields.length) { + identityColumn = res.list_fields[0]; + fieldsOrder = res.list_fields.slice(1, 4); + } + + if (!res.identity_column && !res.list_fields.length) { + identityColumn = res.structure[0].column_name; + console.log(identityColumn); + fieldsOrder = res.structure.slice(1, 4).map((field: TableField) => field.column_name); + } + + const tableRecords = { + rows: formattedRows, + links: res.rows.map(row => { + let params = {}; + Object.keys(res.primaryColumns).forEach((key) => { + params[res.primaryColumns[key].column_name] = row[res.primaryColumns[key].column_name]; + }); + return params; + }), + identityColumn, + fieldsOrder + } + this.referencedRecords[table.table_name] = tableRecords; + }); + }); + } } }); } + ngOnDestroy() { + this.selectedRowCast.unsubscribe(); + } + + toggleReferencedRecords() { + this.referencedRecordsShown = !this.referencedRecordsShown; + } + isForeignKey(columnName: string) { return this.selectedRow.foreignKeysList.includes(columnName); } diff --git a/frontend/src/app/components/dashboard/db-table/db-table.component.ts b/frontend/src/app/components/dashboard/db-table/db-table.component.ts index 8e368b528..9fd190618 100644 --- a/frontend/src/app/components/dashboard/db-table/db-table.component.ts +++ b/frontend/src/app/components/dashboard/db-table/db-table.component.ts @@ -115,7 +115,8 @@ export class DbTableComponent implements OnInit { lte: "<=" } public selectedRow: TableRow = null; - public selectedRowType: 'record' | 'foreignKey'; + public selectedRowType: 'record' | 'foreignKey' = 'record'; + public tableRelatedRecords: any = null; @Input() set table(value){ if (value) this.tableData = value; @@ -142,9 +143,8 @@ export class DbTableComponent implements OnInit { merge(this.sort.sortChange, this.paginator.page) .pipe( tap(() => { - const filters = JsonURL.stringify( this.activeFilters ); - + const filters = JsonURL.stringify( this.activeFilters ); this.router.navigate([`/dashboard/${this.connectionID}/${this.name}`], { queryParams: { filters, @@ -154,7 +154,7 @@ export class DbTableComponent implements OnInit { page_size: this.paginator.pageSize } }); - this.loadRowsPage() + this.loadRowsPage(); }) ) .subscribe(); @@ -183,6 +183,10 @@ export class DbTableComponent implements OnInit { } loadRowsPage() { + console.log('loadRowsPage'); + console.log('loadRowsPage, tableRelatedRecords before', this.tableRelatedRecords); + this.tableRelatedRecords = null; + console.log('loadRowsPage, tableRelatedRecords after', this.tableRelatedRecords); this.tableData.fetchRows({ connectionID: this.connectionID, tableName: this.name, @@ -390,6 +394,7 @@ export class DbTableComponent implements OnInit { handleViewRow(row: TableRow) { this.selectedRowType = 'record'; this._tableState.selectRow({ + connectionID: this.connectionID, tableName: this.name, record: row, columnsOrder: this.tableData.dataColumns, @@ -398,6 +403,7 @@ export class DbTableComponent implements OnInit { foreignKeysList: this.tableData.foreignKeysList, widgets: this.tableData.widgets, widgetsList: this.tableData.widgetsList, + relatedRecords: this.tableData.relatedRecords || null, link: `/dashboard/${this.connectionID}/${this.name}/entry` }); } @@ -407,6 +413,7 @@ export class DbTableComponent implements OnInit { this.selectedRowType = 'foreignKey'; this._tableState.selectRow({ + connectionID: null, tableName: null, record: null, columnsOrder: null, @@ -415,12 +422,14 @@ export class DbTableComponent implements OnInit { foreignKeysList: null, widgets: null, widgetsList: null, + relatedRecords: null, link: null }) this._tableRow.fetchTableRow(this.connectionID, foreignKeys.referenced_table_name, {[foreignKeys.referenced_column_name]: row[foreignKeys.referenced_column_name]}) .subscribe(res => { this._tableState.selectRow({ + connectionID: this.connectionID, tableName: foreignKeys.referenced_table_name, record: res.row, columnsOrder: res.list_fields, @@ -447,6 +456,7 @@ export class DbTableComponent implements OnInit { }) ), widgetsList: res.table_widgets.map(widget => widget.field_name), + relatedRecords: res.referenced_table_names_and_columns[0], link: `/dashboard/${this.connectionID}/${foreignKeys.referenced_table_name}/entry` }); }) @@ -457,7 +467,8 @@ export class DbTableComponent implements OnInit { } isRowSelected(primaryKeys) { - if (this.selectedRowType === 'record' && this.selectedRow && this.selectedRow.primaryKeys !== null) return this.selectedRow && Object.keys(this.selectedRow.primaryKeys).length && JSON.stringify(this.selectedRow.primaryKeys) === JSON.stringify(primaryKeys); + // console.log('isRowSelected', this.selectedRowType, this.selectedRow, primaryKeys); + if (this.selectedRowType === 'record' && this.selectedRow && this.selectedRow.primaryKeys !== null) return Object.keys(this.selectedRow.primaryKeys).length && JSON.stringify(this.selectedRow.primaryKeys) === JSON.stringify(primaryKeys); return false; } diff --git a/frontend/src/app/components/dashboard/db-tables-data-source.ts b/frontend/src/app/components/dashboard/db-tables-data-source.ts index 98067b81d..9ed475d1d 100644 --- a/frontend/src/app/components/dashboard/db-tables-data-source.ts +++ b/frontend/src/app/components/dashboard/db-tables-data-source.ts @@ -11,6 +11,7 @@ import { ConnectionsService } from 'src/app/services/connections.service'; import { DataSource } from '@angular/cdk/table'; import { MatPaginator } from '@angular/material/paginator'; import { NotificationsService } from 'src/app/services/notifications.service'; +import { TableRowService } from 'src/app/services/table-row.service'; // import { MatSort } from '@angular/material/sort'; import { TablesService } from 'src/app/services/tables.service'; import { UiSettingsService } from 'src/app/services/ui-settings.service'; @@ -62,6 +63,10 @@ export class TablesDataSource implements DataSource { public widgets: Widget[]; public widgetsCount: number = 0; public selectWidgetsOptions: object; + public relatedRecords = { + referenced_on_column_name: '', + referenced_by: [] + }; public permissions; public isExportAllowed: boolean; public isImportAllowed: boolean; @@ -79,7 +84,8 @@ export class TablesDataSource implements DataSource { constructor( private _tables: TablesService, private _connections: ConnectionsService, - private _uiSettings: UiSettingsService + private _uiSettings: UiSettingsService, + private _tableRow: TableRowService, ) {} connect(collectionViewer: CollectionViewer): Observable { @@ -120,12 +126,6 @@ export class TablesDataSource implements DataSource { this.alert_settingsInfo = null; this.alert_widgetsWarning = null; - console.log('requstedPage'); - console.log(requstedPage); - - console.log('pageSize'); - console.log(pageSize); - const fetchedTable = this._tables.fetchTable({ connectionID, tableName, @@ -146,6 +146,22 @@ export class TablesDataSource implements DataSource { finalize(() => this.loadingSubject.next(false)) ) .subscribe((res: any) => { + if (res.rows && res.rows.length) { + const firstRow = res.rows[0]; + this._tableRow.fetchTableRow( + connectionID, + tableName, + res.primaryColumns.reduce((keys, column) => { + if (this.foreignKeysList.includes(column.column_name)) { + const referencedColumnNameOfForeignKey = this.foreignKeys[column.column_name].referenced_column_name; + keys[column.column_name] = firstRow[column.column_name][referencedColumnNameOfForeignKey]; + } else { + keys[column.column_name] = firstRow[column.column_name]; + } + return keys; + }, {}) + ).subscribe((res) => this.relatedRecords = res.referenced_table_names_and_columns[0]); + } this.structure = [...res.structure]; const columns = res.structure .reduce((items, item) => { diff --git a/frontend/src/app/models/table.ts b/frontend/src/app/models/table.ts index 1280a96c8..8abf1eef1 100644 --- a/frontend/src/app/models/table.ts +++ b/frontend/src/app/models/table.ts @@ -39,6 +39,7 @@ export interface TableSettings { } export interface TableRow { + connectionID: string, tableName: string, record: object, columnsOrder: string[], @@ -47,6 +48,10 @@ export interface TableRow { foreignKeysList: string[], widgets: Widget[], widgetsList: string[], + relatedRecords: { + referenced_on_column_name: string, + referenced_by: [] + }[], link?: string } diff --git a/frontend/src/app/services/connections.service.ts b/frontend/src/app/services/connections.service.ts index fad670cd0..99eac8028 100644 --- a/frontend/src/app/services/connections.service.ts +++ b/frontend/src/app/services/connections.service.ts @@ -104,8 +104,6 @@ export class ConnectionsService { } get defaultTableToOpen() { - console.log('connections service get defaultTableToOpen'); - console.log(this.defaultDisplayTable); return this.defaultDisplayTable; } @@ -170,6 +168,7 @@ export class ConnectionsService { this.connection = {...this.connectionInitialState}; this.connectionLogo = null; this.companyName = null; + this.defaultDisplayTable = null; this.isCustomAccentedColor = false; this._themeService.updateColors({ palettes: { primaryPalette: '#212121', accentedPalette: '#C177FC' }}); } From c224915bb860c3cdb9557275862b58439fda688a Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 10 Jun 2025 21:25:42 +0300 Subject: [PATCH 4/4] fix unit tests --- .../db-table-row-view/db-table-row-view.component.spec.ts | 6 +++++- .../dashboard/db-table/db-table.component.spec.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.spec.ts b/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.spec.ts index 61953148a..5cb37d748 100644 --- a/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-row-view/db-table-row-view.component.spec.ts @@ -3,6 +3,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DbTableRowViewComponent } from './db-table-row-view.component'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { ActivatedRoute } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { Angulartics2Module } from 'angulartics2'; describe('DbTableRowViewComponent', () => { let component: DbTableRowViewComponent; @@ -12,9 +14,11 @@ describe('DbTableRowViewComponent', () => { TestBed.configureTestingModule({ imports: [ MatSnackBarModule, - DbTableRowViewComponent + DbTableRowViewComponent, + Angulartics2Module.forRoot() ], providers: [ + provideHttpClient(), { provide: ActivatedRoute, useValue: {} } ] }); diff --git a/frontend/src/app/components/dashboard/db-table/db-table.component.spec.ts b/frontend/src/app/components/dashboard/db-table/db-table.component.spec.ts index 1c047507d..6366f8b31 100644 --- a/frontend/src/app/components/dashboard/db-table/db-table.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table/db-table.component.spec.ts @@ -90,7 +90,7 @@ describe('DbTableComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(DbTableComponent); component = fixture.componentInstance; - component.table = new TablesDataSource({} as any, {} as any, {} as any); + component.table = new TablesDataSource({} as any, {} as any, {} as any, {} as any); component.selection = new SelectionModel(true, []); component.filterComparators = mockFilterComparators; fixture.autoDetectChanges();