From 9786e0e0b65e22b1328c3f97225ce5a35cc3da92 Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Tue, 2 Jun 2026 23:43:56 +0300 Subject: [PATCH 1/8] feat(mobile): permissions dialog as bottom sheet + policy/empty-state polish - Permissions (Cedar policy) dialog opens as a mobile bottom sheet via panelClass - Empty state in the policy list gets an inline "Add policy" CTA (accent color), and the dialog actions now keep only one "Add policy" once there's at least one policy - Cancel pinned to the left of the dialog footer; Save stays on the right - User add dialog: swap order so the primary "Add" sits before "Cancel" - Users page on mobile: wrapper full-width with 16px padding, header flex-wraps the New group button if needed, smaller h1 - Connection settings on mobile: keep Primary/Accented color inputs on one row by forcing min-width: 0 through the Material form-field internals Co-Authored-By: Claude Opus 4.7 (1M context) --- .../connection-settings.component.css | 30 +++++++++++++++++++ .../cedar-policy-editor-dialog.component.html | 6 ++-- .../cedar-policy-list.component.css | 10 ++++++- .../cedar-policy-list.component.html | 7 ++++- .../user-add-dialog.component.html | 2 +- .../app/components/users/users.component.css | 22 ++++++++++++++ .../app/components/users/users.component.ts | 2 ++ 7 files changed, 73 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/components/connection-settings/connection-settings.component.css b/frontend/src/app/components/connection-settings/connection-settings.component.css index da3d1c426..a34b0039d 100644 --- a/frontend/src/app/components/connection-settings/connection-settings.component.css +++ b/frontend/src/app/components/connection-settings/connection-settings.component.css @@ -178,3 +178,33 @@ .audit-toggle { margin-top: 4px; } + +@media (width <= 600px) { + .color-theme { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-gap: 8px; + align-items: center; + } + + .color-item { + min-width: 0; + } + + .color-input { + width: 100%; + min-width: 0; + } + + .color-input ::ng-deep .mat-mdc-text-field-wrapper, + .color-input ::ng-deep .mat-mdc-form-field-flex, + .color-input ::ng-deep .mat-mdc-form-field-infix { + min-width: 0 !important; + width: auto !important; + } + + .color-input ::ng-deep input.mat-mdc-input-element { + padding-left: 24px; + min-width: 0; + } +} diff --git a/frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.html b/frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.html index d82d46ca1..8bdbe20c6 100644 --- a/frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.html +++ b/frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.html @@ -48,14 +48,14 @@

Policy — {{ data.groupTitle }}

} - @if (editorMode() === 'form' && !formParseError() && !loading() && !policyList?.showAddForm) { + + + @if (editorMode() === 'form' && !formParseError() && !loading() && !policyList?.showAddForm && policyList?.policies()?.length) { } - - } diff --git a/frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html b/frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html index 5a468467c..7501c2261 100644 --- a/frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html +++ b/frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html @@ -26,7 +26,6 @@

Add user to {{ data.group.title }} group - @if (data.availableMembers.length) { diff --git a/frontend/src/app/components/users/users.component.css b/frontend/src/app/components/users/users.component.css index cdd58c670..14d9166d3 100644 --- a/frontend/src/app/components/users/users.component.css +++ b/frontend/src/app/components/users/users.component.css @@ -131,3 +131,25 @@ header { .no-access { margin-top: 32px !important; } + +@media (width <= 600px) { + .wrapper { + width: 100%; + padding: 0 16px; + box-sizing: border-box; + } + + header { + flex-wrap: wrap; + gap: 8px; + margin: 16px 0 12px; + } + + .add-group-button { + flex: 0 0 auto; + } + + .mat-h1 { + font-size: 22px; + } +} diff --git a/frontend/src/app/components/users/users.component.ts b/frontend/src/app/components/users/users.component.ts index 1dcd05419..ceda8e065 100644 --- a/frontend/src/app/components/users/users.component.ts +++ b/frontend/src/app/components/users/users.component.ts @@ -157,6 +157,7 @@ export class UsersComponent implements OnInit { if (createdGroup) { this._dialog.open(CedarPolicyEditorDialogComponent, { width: '40em', + panelClass: 'mobile-bottom-sheet-dialog', data: { groupId: createdGroup.id, groupTitle: createdGroup.title, cedarPolicy: null }, }); } @@ -194,6 +195,7 @@ export class UsersComponent implements OnInit { openCedarPolicyDialog(group: UserGroup) { this._dialog.open(CedarPolicyEditorDialogComponent, { width: '40em', + panelClass: 'mobile-bottom-sheet-dialog', data: { groupId: group.id, groupTitle: group.title, cedarPolicy: group.cedarPolicy }, }); } From 82e46857557c388c27cde41e2845514bee35e227 Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Tue, 2 Jun 2026 23:04:09 +0300 Subject: [PATCH 2/8] feat(mobile): audit feed view + sidebar/AI panel polish - Audit mobile feed: replace mat-table with a card list (sticky date headers grouping entries per day, avatar with initials, top row user/time, middle row action+table, bottom row status+Details, last card fades for an infinite-scroll hint, tap anywhere on the card opens the details dialog) - Audit header on mobile: arrow-back next to the "Audit" title, Tables/Users selects side-by-side without breaking layout (force min-width: 0 chain, no horizontal overflow), select arrow pinned to the right edge of the field - Audit paginator: match the table paginator (compact "Per page:" select on the left, range + nav buttons on the right, single row, capitalized label) - AI panel: raise z-index of the expanded content back to 100 so close/send buttons aren't covered (they were being intercepted by other UI when the panel is force-expanded) - Sidebar drawer: close automatically on any nav-list click and on Upgrade click Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/app/app.component.html | 7 +- .../app/components/audit/audit.component.css | 459 ++++++++++++++++++ .../app/components/audit/audit.component.html | 51 +- .../app/components/audit/audit.component.ts | 49 +- .../db-table-ai-panel.component.css | 2 +- .../db-table-view/db-table-view.component.ts | 8 +- 6 files changed, 566 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index beec4e212..9cb503268 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -5,8 +5,8 @@ class="main-menu-sidenav" > Rocketadmin - - + @@ -63,7 +63,8 @@ + routerLinkActive="nav-bar__button_active" + (click)="drawer.close()"> Upgrade diff --git a/frontend/src/app/components/audit/audit.component.css b/frontend/src/app/components/audit/audit.component.css index bf7327dfa..d5a7162dd 100644 --- a/frontend/src/app/components/audit/audit.component.css +++ b/frontend/src/app/components/audit/audit.component.css @@ -20,6 +20,21 @@ header { margin: 0; } +.title-block { + display: flex; + align-items: center; + gap: 8px; +} + +.title-block h1 { + margin: 0; + line-height: 1; +} + +.back-button { + display: none !important; +} + .filters { display: flex; /* grid-template-columns: 1fr 1fr; */ @@ -215,3 +230,447 @@ td.mat-cell { color: rgba(255, 255, 255, 0.6); } } + +@media (width <= 600px) { + .wrapper { + width: 100%; + max-width: 100%; + min-width: 0; + padding: 0 16px; + box-sizing: border-box; + overflow-x: clip; + overflow-y: visible; + } + + header { + flex-direction: column; + align-items: stretch; + gap: 12px; + margin: 16px 0 12px; + } + + .back-button { + display: inline-flex !important; + align-items: center; + justify-content: center; + margin-left: -12px; + } + + .title-block { + gap: 0; + align-items: center; + } + + .title-block h1 { + font-size: 22px; + font-weight: 600; + line-height: 1; + } + + .filters { + flex-direction: row; + gap: 8px; + width: 100%; + max-width: 100%; + min-width: 0; + } + + .filters mat-form-field { + flex: 1 1 0; + min-width: 0; + max-width: 100%; + width: auto; + } + + .filters mat-form-field ::ng-deep .mat-mdc-text-field-wrapper, + .filters mat-form-field ::ng-deep .mat-mdc-form-field-flex { + min-width: 0 !important; + width: 100% !important; + } + + .filters mat-form-field ::ng-deep .mat-mdc-form-field-infix { + min-width: 0 !important; + flex: 1 1 auto !important; + width: auto !important; + } + + .filters mat-form-field ::ng-deep .mat-mdc-select { + width: 100%; + } + + .filters mat-form-field ::ng-deep .mat-mdc-select-arrow-wrapper { + padding-left: 4px; + } + + .audit-banner { + margin: 0 -16px 12px; + width: calc(100% + 32px); + } + + .table-wrapper { + width: 100%; + max-width: 100%; + min-width: 0; + background: transparent !important; + box-shadow: none !important; + box-sizing: border-box; + } + + .table-wrapper table.mat-mdc-table, + .table-wrapper table.mat-mdc-table thead, + .table-wrapper table.mat-mdc-table tbody, + .table-wrapper table.mat-mdc-table tr { + display: block; + width: 100%; + } + + .table-wrapper table.mat-mdc-table thead, + .table-wrapper table.mat-mdc-table tr.mat-mdc-header-row, + .table-wrapper table.mat-mdc-table th.mat-mdc-header-cell { + display: none !important; + } + + .table-wrapper table.mat-mdc-table tbody { + display: flex; + flex-direction: column; + gap: 10px; + } + + .table-wrapper tr.mat-mdc-row { + background: var(--mat-table-background-color, #fff); + border-radius: 12px; + padding: 10px 14px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04); + display: grid !important; + grid-template-columns: auto 1fr; + grid-column-gap: 12px; + grid-row-gap: 4px; + height: auto !important; + width: calc(100vw - 32px) !important; + min-width: 0 !important; + max-width: calc(100vw - 32px) !important; + box-sizing: border-box; + margin: 0 !important; + } + + .table-wrapper table.mat-mdc-table { + min-width: 0 !important; + max-width: 100% !important; + width: 100% !important; + background: transparent !important; + } + + .table-wrapper table.mat-mdc-table tbody { + width: 100% !important; + } + + .table-wrapper td.mat-mdc-cell { + display: block; + border: 0 !important; + padding: 0 !important; + font-size: 13px; + } + + .table-wrapper td.mat-mdc-cell .table-cell-content { + padding: 0; + } + + /* columns order: User(1) Table(2) Action(3) Status(4) Date(5) Changes(6) + Card layout: + row 1: User (name + email) full width + row 2: Action + Table (inline) + row 3: Status + row 4: Date + Details */ + .table-wrapper td.mat-mdc-cell:nth-of-type(1) { + grid-column: 1 / -1; + grid-row: 1; + } + + .table-wrapper td.mat-mdc-cell:nth-of-type(3) { + grid-column: 1 / 2; + grid-row: 2; + font-weight: 500; + } + + .table-wrapper td.mat-mdc-cell:nth-of-type(2) { + grid-column: 2 / 3; + grid-row: 2; + font-weight: 600; + font-size: 14px; + justify-self: start; + } + + .table-wrapper td.mat-mdc-cell:nth-of-type(4) { + grid-column: 1 / -1; + grid-row: 3; + } + + .table-wrapper td.mat-mdc-cell:nth-of-type(5) { + grid-column: 1 / 2; + grid-row: 4; + opacity: 0.75; + font-size: 12px; + } + + .table-wrapper td.mat-mdc-cell:nth-of-type(6) { + grid-column: 2 / 3; + grid-row: 4; + justify-self: end; + } + + .status-badge { + padding: 2px 8px; + font-size: 12px; + } + + .mat-h1 { + font-size: 22px; + } + + .table-wrapper mat-paginator ::ng-deep .mat-mdc-paginator-container { + flex-wrap: nowrap !important; + justify-content: space-between !important; + padding: 0 8px !important; + min-height: 48px; + } + + .table-wrapper mat-paginator ::ng-deep .mat-mdc-paginator-page-size { + margin: 0 !important; + margin-right: auto !important; + } + + .table-wrapper mat-paginator ::ng-deep .mat-mdc-paginator-range-label { + margin: 0 4px !important; + } + + .table-wrapper mat-paginator { + margin-top: 8px; + background: transparent !important; + } +} + +@media (prefers-color-scheme: dark) and (width <= 600px) { + .table-wrapper tr.mat-mdc-row { + background: var(--surface-dark-color); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2); + } +} + +@media (width <= 600px) { + .audit-feed { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + max-width: 100%; + min-width: 0; + padding-bottom: 16px; + } + + .audit-feed.hidden { + display: none; + } + + .audit-feed__date-header { + position: sticky; + top: 44px; + z-index: 2; + background: var(--mat-sidenav-content-background-color, #fff); + padding: 8px 0 4px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: rgba(0, 0, 0, 0.54); + } + + .audit-card { + display: flex; + align-items: flex-start; + gap: 12px; + background: var(--mat-table-background-color, #fff); + border-radius: 12px; + padding: 10px 14px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04); + cursor: pointer; + transition: opacity 200ms ease; + } + + .audit-card_fade { + opacity: 0.6; + } + + .audit-card__avatar { + flex: 0 0 36px; + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--color-accentedPalette-100); + color: var(--color-accentedPalette-700); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 600; + } + + .audit-card__body { + flex: 1 1 auto; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; + } + + .audit-card__row { + display: flex; + align-items: center; + gap: 6px; + } + + .audit-card__row_top { + justify-content: space-between; + } + + .audit-card__user { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 600; + font-size: 14px; + } + + .audit-card__user-email { + font-weight: 400; + color: rgba(0, 0, 0, 0.72); + } + + .audit-card__time { + flex: 0 0 auto; + font-size: 12px; + color: rgba(0, 0, 0, 0.54); + } + + .audit-card__row_middle { + font-size: 13px; + } + + .audit-card__action-icon { + font-size: 16px; + width: 16px; + height: 16px; + } + + .audit-card__action { + font-weight: 500; + } + + .audit-card__sep { + color: rgba(0, 0, 0, 0.3); + } + + .audit-card__table { + font-family: "IBM Plex Mono", monospace; + font-size: 12px; + color: rgba(0, 0, 0, 0.72); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .audit-card__row_bottom { + justify-content: space-between; + margin-top: 2px; + } + + .audit-card__details-link { + color: var(--color-accentedPalette-500); + font-size: 13px; + font-weight: 500; + } + + .audit-feed__paginator { + margin-top: 12px; + background: transparent !important; + } + + .audit-feed__paginator ::ng-deep .mat-mdc-paginator-outer-container, + .audit-feed__paginator ::ng-deep .mat-mdc-paginator-container { + justify-content: space-between !important; + flex-wrap: nowrap !important; + min-height: 48px; + padding: 0 !important; + } + + .audit-feed__paginator ::ng-deep .mat-mdc-paginator-page-size { + margin: 0 !important; + padding: 0 !important; + margin-right: auto !important; + flex: 0 0 auto; + } + + .audit-feed__paginator ::ng-deep .mat-mdc-paginator-page-size-label { + margin: 0 4px 0 0 !important; + } + + .audit-feed__paginator ::ng-deep .mat-mdc-paginator-page-size-select { + margin: 0 8px 0 0 !important; + width: auto !important; + } + + .audit-feed__paginator ::ng-deep .mat-mdc-paginator-page-size-select .mat-mdc-form-field-infix { + width: auto !important; + min-width: 36px !important; + padding-right: 4px !important; + } + + .audit-feed__paginator ::ng-deep .mat-mdc-paginator-page-size-select .mat-mdc-select { + width: auto !important; + } + + .audit-feed__paginator ::ng-deep .mat-mdc-paginator-page-size-select .mat-mdc-select-arrow-wrapper { + padding-left: 6px !important; + } + + .audit-feed__paginator ::ng-deep .mat-mdc-paginator-page-size-select .mat-mdc-text-field-wrapper { + padding: 0 10px !important; + } + + .audit-feed__paginator ::ng-deep .mat-mdc-paginator-range-actions { + flex: 0 0 auto; + margin: 0 !important; + } + + .audit-feed__paginator ::ng-deep .mat-mdc-paginator-range-label { + margin: 0 4px !important; + } +} + +@media (prefers-color-scheme: dark) and (width <= 600px) { + .audit-feed__date-header { + background: var(--surface-dark-color); + color: rgba(255, 255, 255, 0.6); + } + + .audit-card { + background: var(--surface-dark-color); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2); + } + + .audit-card__user-email { + color: rgba(255, 255, 255, 0.7); + } + + .audit-card__time { + color: rgba(255, 255, 255, 0.54); + } + + .audit-card__sep { + color: rgba(255, 255, 255, 0.3); + } + + .audit-card__table { + color: rgba(255, 255, 255, 0.72); + } +} diff --git a/frontend/src/app/components/audit/audit.component.html b/frontend/src/app/components/audit/audit.component.html index 7e6c2ca45..fc1926423 100644 --- a/frontend/src/app/components/audit/audit.component.html +++ b/frontend/src/app/components/audit/audit.component.html @@ -1,6 +1,11 @@
-

Audit

+
+ + arrow_back + +

Audit

+
Tables @@ -67,7 +72,49 @@

Rocketadmin can not find any tables

-
+ +
{{ group.date }}
+
+
{{ getInitials(entry.UserEmail) }}
+
+
+ + {{ getUserName(entry.UserEmail) }} + {{ entry.UserEmail }} + + {{ entry.TimeOnly }} +
+
+ {{ entry.ActionIcon }} + {{ entry.Action || '—' }} + · + {{ entry.Table }} +
+
+ + {{ entry.Status === 'successfully' ? 'Success' : (entry.Status || '—') }} + + Details › +
+
+
+
+ + +
+ +
diff --git a/frontend/src/app/components/audit/audit.component.ts b/frontend/src/app/components/audit/audit.component.ts index 2969e8f5a..51b8d5850 100644 --- a/frontend/src/app/components/audit/audit.component.ts +++ b/frontend/src/app/components/audit/audit.component.ts @@ -5,7 +5,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; -import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; +import { MatPaginator, MatPaginatorIntl, MatPaginatorModule } from '@angular/material/paginator'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { Title } from '@angular/platform-browser'; @@ -69,6 +69,12 @@ export class AuditComponent implements OnInit { public dataSource: AuditDataSource = null; + public mobileGroupedLogs: { date: string; entries: any[] }[] = []; + + get isMobileView(): boolean { + return typeof window !== 'undefined' && window.innerWidth <= 600; + } + @ViewChild(MatPaginator) paginator: MatPaginator; constructor( @@ -78,7 +84,11 @@ export class AuditComponent implements OnInit { private _companyService: CompanyService, public dialog: MatDialog, private title: Title, - ) {} + private paginatorIntl: MatPaginatorIntl, + ) { + this.paginatorIntl.itemsPerPageLabel = 'Per page:'; + this.paginatorIntl.changes.next(); + } ngAfterViewInit() { this.dataSource.paginator = this.paginator; @@ -100,6 +110,9 @@ export class AuditComponent implements OnInit { this.columns = ['User', 'Table', 'Action', 'Status', 'Date', 'Changes']; this.dataColumns = ['User', 'Table', 'Action', 'Status', 'Date']; this.dataSource = new AuditDataSource(this._connections); + this.dataSource.connect(null as any).subscribe((rows) => { + this.mobileGroupedLogs = this.groupByDate(rows); + }); this.loadLogsPage(); this._tables.fetchTables(this.connectionID).subscribe( @@ -149,4 +162,36 @@ export class AuditComponent implements OnInit { const user = this.usersList.find((u) => u.email === email); return user?.name || null; } + + getInitials(email: string): string { + if (!email) return '?'; + const name = this.getUserName(email); + if (name) { + const parts = name.trim().split(/\s+/); + return parts + .slice(0, 2) + .map((p) => p[0]?.toUpperCase() ?? '') + .join(''); + } + const local = email.split('@')[0]; + const parts = local.split(/[._-]/); + return parts + .slice(0, 2) + .map((p) => p[0]?.toUpperCase() ?? '') + .join(''); + } + + trackByDate(_index: number, group: { date: string }): string { + return group.date; + } + + private groupByDate(rows: any[]): { date: string; entries: any[] }[] { + const groups = new Map(); + for (const row of rows) { + const date = row.DateOnly; + if (!groups.has(date)) groups.set(date, []); + groups.get(date)!.push(row); + } + return Array.from(groups, ([date, entries]) => ({ date, entries })); + } } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css index 507a5c926..e358e68aa 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css @@ -75,7 +75,7 @@ width: calc(100vw - 65px); padding-left: 175px; border-left: none; - z-index: 3; + z-index: 100; transition: left 400ms cubic-bezier(0.4, 0, 0.2, 1), width 400ms cubic-bezier(0.4, 0, 0.2, 1), diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index 359a875e2..8f517b709 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -28,7 +28,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; -import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; +import { MatPaginator, MatPaginatorIntl, MatPaginatorModule } from '@angular/material/paginator'; import { MatSelectModule } from '@angular/material/select'; import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; @@ -193,7 +193,11 @@ export class DbTableViewComponent implements OnInit, OnChanges { public router: Router, public dialog: MatDialog, private cdr: ChangeDetectorRef, - ) {} + private paginatorIntl: MatPaginatorIntl, + ) { + this.paginatorIntl.itemsPerPageLabel = 'Per page:'; + this.paginatorIntl.changes.next(); + } ngAfterViewInit() { this.tableData.paginator = this.paginator; From 9b923bf53697eea861f19a5d87b3df8cc306308c Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Tue, 2 Jun 2026 22:14:16 +0300 Subject: [PATCH 3/8] feat(mobile): widgets page, AI panel, sidebar polish + misc fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Widgets page mobile layout: stacked widget cards (display:contents → block), header with arrow-back + "Widgets" / table-name stack, hide breadcrumbs and per-card delete button, drop the bottom "Back" button (arrow-back covers it), Save aligns right - Widgets empty state mobile: hide right-side widget cards grid, hide divider and the tune icon, symmetric padding, Docs link + "Create first widget" button right-aligned together, breadcrumbs hidden, extra bottom buffer so Create doesn't collide with the version footer - AI panel: full-size only on mobile (removed expand/collapse toggle and the +175px expanded padding), trim "AI insights for
" to plain "AI insights" so the title doesn't push out, FAB Add row hides while the chat is open, and on mobile the chat panel state is reset on each table view init (so navigating away doesn't reopen the chat on return) - Sidebar drawer mobile: sits below the 44px nav-bar (with backdrop offset), hide the dark mat-toolbar, "Connections" hidden when already inside a connection, "Hosted databases" added between Company and Secrets, account-section items (.account-section-item) get smaller text + icons, drop the connection-nav top border, Upgrade button nudged up - Connections list dropdown: divider + "Hosted databases" link at the bottom - Logo: short variant only on mobile when inside a connection (connections-list, account, hosted-databases pages keep the full wordmark) - Row edit form: drop the leftover .widget grid column reservation so inputs fill width - Mobile loading skeleton: dedicated card-shape placeholder for ≤600px, no left label column, no elevation around the wrapper - Row preview dividers: inset 20px each side via ::after so they don't reach the popup edges - Filter dialog when tapping an active filter chip: opens as the mobile bottom sheet (panelClass: 'mobile-bottom-sheet-dialog') Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/app/app.component.css | 32 +++- frontend/src/app/app.component.html | 27 ++- frontend/src/app/app.component.ts | 2 + .../db-table-ai-panel.component.css | 16 +- .../db-table-ai-panel.component.html | 8 +- .../db-table-ai-panel.component.ts | 6 +- .../db-table-view/db-table-view.component.css | 160 ++++++++++++++++++ .../db-table-view/db-table-view.component.ts | 7 +- .../db-table-widgets.component.css | 156 +++++++++++++++++ .../db-table-widgets.component.html | 13 +- .../widgets-empty-state.component.css | 31 ++++ .../db-table-row-edit.component.css | 3 +- .../placeholder-table-data.component.css | 56 ++++++ .../placeholder-table-data.component.html | 11 +- .../placeholder-table-data.component.ts | 1 + frontend/src/styles.scss | 4 + 16 files changed, 494 insertions(+), 39 deletions(-) diff --git a/frontend/src/app/app.component.css b/frontend/src/app/app.component.css index 3c436c2c5..47eda27f3 100644 --- a/frontend/src/app/app.component.css +++ b/frontend/src/app/app.component.css @@ -22,6 +22,34 @@ width: 60vw; } +.main-menu-sidenav .account-section-item { + --mdc-list-list-item-label-text-size: 13px; + --mat-list-list-item-leading-icon-size: 18px; + --mdc-list-list-item-one-line-container-height: 40px; + --mdc-list-list-item-two-line-container-height: 52px; +} + +.main-menu-sidenav .account-section-item .mat-icon { + font-size: 18px; + width: 18px; + height: 18px; +} + +@media (width <= 600px) { + .main-menu-sidenav.mat-drawer { + top: 44px !important; + height: calc(100vh - 44px) !important; + } + + .main-menu-container ::ng-deep .mat-drawer-backdrop.mat-drawer-shown { + top: 44px !important; + } + + .main-menu-sidenav mat-toolbar { + display: none !important; + } +} + .nav-bar { position: sticky; top: 0; @@ -218,8 +246,6 @@ display: flex; flex-direction: column; align-items: flex-start; - border-top: var(--mat-table-row-item-outline-width, 1px) solid - var(--mat-table-row-item-outline-color, rgba(0, 0, 0, 0.12)); border-bottom: var(--mat-table-row-item-outline-width, 1px) solid var(--mat-table-row-item-outline-color, rgba(0, 0, 0, 0.12)); margin-left: 8px; @@ -243,7 +269,7 @@ } .connection-navigation__upgrade-button { - margin-top: 8px; + margin-top: -12px; margin-left: 8px; width: calc(100% - 16px); } diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 9cb503268..d0e0a4af2 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -21,7 +21,7 @@ {{navigationTabs[tab].caption}} - + - + - + + + database + +
Hosted databases
+
+ - + + - + @@ -94,7 +102,7 @@ class="logo__image"> - + Rocketadmin logo @@ -131,6 +139,11 @@ [ngClass]="{'connection_active': connectionID === connection.connection.id}"> {{connection.displayTitle}} + + + database + Hosted databases + demo diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index cf303915b..ae0ccfcaa 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component } from '@angular/core'; import { MatBadgeModule } from '@angular/material/badge'; import { MatButtonModule } from '@angular/material/button'; +import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule, MatIconRegistry } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; @@ -50,6 +51,7 @@ amplitude.getInstance().init('9afd282be91f94da735c11418d5ff4f5'); MatButtonModule, MatBadgeModule, MatMenuModule, + MatDividerModule, MatTooltipModule, Angulartics2OnModule, FeatureNotificationComponent, diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css index e358e68aa..6882fbdeb 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css @@ -73,7 +73,7 @@ .ai-panel-sidebar-content_open.ai-panel-sidebar-content_expanded { left: 65px; width: calc(100vw - 65px); - padding-left: 175px; + padding-left: 0; border-left: none; z-index: 100; transition: @@ -99,6 +99,7 @@ .ai-panel-sidebar-content_open.ai-panel-sidebar-content_expanded { left: 0; width: 100%; + padding-left: 0; } } @@ -677,19 +678,8 @@ } .ai-panel-sidebar-content_expanded .ai-panel-sidebar__header { - position: relative; width: 100%; - max-width: 800px; - margin: 0 auto; - padding-left: 0; - padding-right: 0; - justify-content: flex-start; -} - -.ai-panel-sidebar-content_expanded .ai-panel-sidebar__actions { - position: fixed; - right: 16px; - top: 64px; + justify-content: space-between; } .ai-panel-sidebar-content_expanded .ai-welcome__section { diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.html index 4007b624e..e7faaa311 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.html @@ -4,14 +4,8 @@
-

- AI insights for {{displayName}} - AI insights -

+

AI insights

- diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts index 9a84da51a..a99d1d9cc 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts @@ -65,7 +65,7 @@ export class DbTableAiPanelComponent implements OnInit, AfterViewInit, OnDestroy public activeCompletions: string[] = []; public showCompletions: boolean = false; public submitting: boolean = false; - public isExpanded: boolean = false; + public isExpanded: boolean = true; public textareaRows: number = 4; public currentLoadingStep: string = ''; @@ -97,8 +97,8 @@ export class DbTableAiPanelComponent implements OnInit, AfterViewInit, OnDestroy this.isAIpanelOpened = isAIpanelOpened; }); - this._tableState.aiPanelExpandedCast.subscribe((isExpanded) => { - this.isExpanded = isExpanded; + this._tableState.aiPanelExpandedCast.subscribe(() => { + this.isExpanded = true; }); this.adjustTextareaRows(); diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css index 4be4b813f..ccc800828 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css @@ -677,6 +677,161 @@ height: 18px; } + .search-row__add-quick-filter { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 32px; + height: 32px; + min-width: 32px; + min-height: 32px; + padding: 0 !important; + --mdc-icon-button-state-layer-size: 32px; + --mat-icon-button-state-layer-size: 32px; + --mdc-icon-button-icon-size: 20px; + } + + .search-row__add-quick-filter .mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + margin: 0; + } + + .search-row__add-quick-filter ::ng-deep .mat-mdc-button-touch-target { + width: 32px; + height: 32px; + } + + .search-row__add-quick-filter ::ng-deep .mat-mdc-button-persistent-ripple, + .search-row__add-quick-filter ::ng-deep .mat-mdc-focus-indicator { + inset: 0; + border-radius: 50%; + } + + .search-row__create-fast-filter { + display: inline-flex; + flex: 0 0 auto; + height: 32px; + line-height: 32px; + padding: 0 12px; + border-radius: 999px; + font-size: 13px; + gap: 4px; + } + + .search-row__create-fast-filter .mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } + + .db-table-header, + .db-table-actions, + .search-row { + min-width: 0; + } + + .search-row__buttons { + min-width: 0; + max-width: 100%; + overflow: hidden; + } + + .search-row__quick-chips-slot { + position: relative; + display: block; + flex: 1 1 0; + min-width: 0; + width: 0; + max-width: 100%; + height: 32px; + overflow: hidden; + } + + .search-row__quick-chips { + position: absolute; + inset: 0; + display: flex; + align-items: center; + gap: 6px; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + } + + .search-row__quick-chips::-webkit-scrollbar { + display: none; + } + + .search-row__quick-chip { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + border: 1px solid var(--mdc-outlined-button-outline-color, rgba(0, 0, 0, 0.12)); + background: transparent; + color: rgba(0, 0, 0, 0.87); + font-size: 13px; + font-weight: 500; + padding: 0 8px 0 12px; + height: 32px; + line-height: 30px; + border-radius: 999px; + white-space: nowrap; + cursor: pointer; + } + + .search-row__quick-chip_active { + background: var(--color-accentedPalette-500); + border-color: var(--color-accentedPalette-500); + color: var(--color-accentedPalette-500-contrast); + } + + .search-row__quick-chip-menu { + background: transparent; + border: 0; + padding: 0; + margin-left: 4px; + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: inherit; + opacity: 0.7; + } + + .search-row__quick-chip-menu .mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + } + + @media (prefers-color-scheme: dark) { + .search-row__quick-chip { + border-color: var(--mdc-outlined-button-outline-color, rgba(255, 255, 255, 0.24)); + color: rgba(255, 255, 255, 0.87); + } + } + + .search-row__divider { + display: inline-block; + flex: 0 0 auto; + width: 1px; + align-self: stretch; + margin: 4px 4px; + background: var(--color-primaryPalette-200); + } + + @media (prefers-color-scheme: dark) { + .search-row__divider { + background: var(--color-primaryPalette-700); + } + } + .saved-filters-row__columns { display: inline-flex; flex: 0 0 auto; @@ -764,6 +919,11 @@ } @media (width <= 600px) { + .skeleton.mat-elevation-z4 { + box-shadow: none !important; + background: transparent !important; + } + .saved-filters-row { display: flex; flex-direction: column; diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index 8f517b709..10e6caa4c 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -195,7 +195,7 @@ export class DbTableViewComponent implements OnInit, OnChanges { private cdr: ChangeDetectorRef, private paginatorIntl: MatPaginatorIntl, ) { - this.paginatorIntl.itemsPerPageLabel = 'Per page:'; + this.paginatorIntl.itemsPerPageLabel = 'per page:'; this.paginatorIntl.changes.next(); } @@ -299,6 +299,11 @@ export class DbTableViewComponent implements OnInit, OnChanges { this.searchString = this.route.snapshot.queryParams.search; // this.hasSavedFilterActive = !!this.route.snapshot.queryParams.saved_filter; + // On mobile, never restore a previously open AI panel — user should land on the table, not the chat. + if (this.isMobileView) { + this._tableState.closeAIpanel(); + } + const connectionType = this._connections.currentConnection.type; this.displayCellComponents = tableDisplayTypes[connectionType]; diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.css index 916c16fc9..849c191f9 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.css @@ -31,6 +31,10 @@ gap: 16px; } +.mobile-header { + display: none; +} + .widget-settings { display: grid; grid-template-columns: minmax(10%, 130px) 1fr 2fr 1fr 2fr 50px; @@ -71,3 +75,155 @@ --background-color: #fff; } } + +@media (width <= 600px) { + .wrapper { + width: 100%; + margin: 0 auto 88px; + padding: 0 16px; + min-width: 0; + } + + .row-breadcrumbs { + display: none !important; + } + + .mobile-header { + display: flex; + align-items: center; + gap: 8px; + margin: 20px 0 12px -12px; + } + + .mobile-header__back { + margin-right: 0; + } + + .mobile-header__titles { + display: flex; + flex-direction: column; + min-width: 0; + } + + .mobile-header__title { + margin: 0 !important; + padding: 0 !important; + font-size: 18px !important; + font-weight: 600 !important; + line-height: 18px !important; + } + + .mobile-header__subtitle { + display: block; + margin: 4px 0 0 !important; + padding: 0 !important; + font-size: 12px; + line-height: 14px; + color: rgba(0, 0, 0, 0.54); + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .actions__back { + display: none !important; + } + + .actions { + justify-content: flex-end; + } + + .header { + flex-direction: column; + gap: 12px; + margin: 16px 0 16px; + } + + .header-actions-box { + width: 100%; + align-items: stretch; + gap: 8px; + } + + .header-actions { + flex-wrap: wrap; + gap: 8px; + width: 100%; + } + + .header-actions > button { + flex: 1 1 auto; + } + + .header-links { + flex-direction: column; + align-items: flex-start; + gap: 0; + } + + .widget-settings { + display: flex; + flex-direction: column; + gap: 16px; + } + + .widget-item { + display: block; + position: relative; + background: var(--mat-table-background-color, #fff); + border-radius: 12px; + padding: 16px 16px 8px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04); + } + + .widget-item ::ng-deep .mat-mdc-form-field { + width: 100%; + } + + .widget-item ::ng-deep .widget-field-name { + display: block; + font-weight: 600; + margin: 0 0 12px 0; + font-size: 14px; + } + + .widget-item ::ng-deep .widget-delete-button { + display: none !important; + } + + .widget-item ::ng-deep .code-editor-box { + min-height: 140px; + margin-bottom: 16px; + } + + .actions { + padding: 0 16px; + justify-content: space-between; + gap: 8px; + } + + .actions > a, + .actions > button { + flex: 1 1 auto; + } + + .actions_empty-state { + display: none !important; + } + + .wrapper:has(app-widgets-empty-state) { + margin-bottom: 80px; + } +} + +@media (prefers-color-scheme: dark) and (width <= 600px) { + .widget-item { + background: var(--surface-dark-color); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2); + } + + .mobile-header__subtitle { + color: rgba(255, 255, 255, 0.6); + } +} diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.html index ddb9fe69f..63a58aaa7 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.html @@ -3,6 +3,15 @@
+
+ + arrow_back + +
+

Widgets

+ {{ tableName }} +
+
@@ -61,7 +70,7 @@
- + Back @@ -75,7 +84,7 @@ -
+
Back diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widgets-empty-state/widgets-empty-state.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widgets-empty-state/widgets-empty-state.component.css index 847de74da..cf19415af 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widgets-empty-state/widgets-empty-state.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/widgets-empty-state/widgets-empty-state.component.css @@ -357,3 +357,34 @@ color: rgba(255, 255, 255, 0.54); } } + +@media (width <= 600px) { + .empty-state { + flex-direction: column; + } + + .empty-divider, + .empty-right, + .empty-icon-box { + display: none !important; + } + + .empty-left { + width: 100%; + max-width: 100%; + padding: 16px 0; + } + + .empty-cta { + justify-content: flex-end; + gap: 12px; + } + + .empty-cta > button { + order: 2; + } + + .empty-cta > .empty-docs-link { + order: 1; + } +} diff --git a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css index 100d0bb99..f3f9fe005 100644 --- a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css +++ b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css @@ -189,8 +189,7 @@ } .widget { - display: grid; - grid-template-columns: 0 1fr 36px; + display: block; } .widget-info { diff --git a/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.css b/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.css index 435716ab7..1b2301f0d 100644 --- a/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.css +++ b/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.css @@ -14,3 +14,59 @@ .table-cell-content { height: 24px; } + +.data-table_mobile { + display: none; +} + +@media (width <= 600px) { + .data-table_desktop { + display: none; + } + + :host { + display: block; + box-shadow: none !important; + } + + .data-table_mobile { + display: flex; + flex-direction: column; + gap: 12px; + padding: 0 0 16px; + } + + .mobile-card { + background: var(--mat-table-background-color, #fff); + border: none; + border-radius: 12px; + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 8px; + } + + .mobile-card__value { + width: 100%; + height: 14px; + border-radius: 4px; + } + + .mobile-card__value_short { + width: 30%; + } + + .mobile-card__value_medium { + width: 60%; + } + + .mobile-card__value_long { + width: 80%; + } +} + +@media (prefers-color-scheme: dark) and (width <= 600px) { + .mobile-card { + background: var(--surface-dark-color); + } +} diff --git a/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.html b/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.html index c9d962ac8..20d8b6c0f 100644 --- a/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.html +++ b/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.html @@ -1,5 +1,14 @@ -
+
+ +
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.ts b/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.ts index 06ae1ed10..a15ac8a0d 100644 --- a/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.ts +++ b/frontend/src/app/components/skeletons/placeholder-table-data/placeholder-table-data.component.ts @@ -9,4 +9,5 @@ import { Component } from '@angular/core'; }) export class PlaceholderTableDataComponent { public numberOfDivs = Array.from({ length: 42 }, (_, index) => index); + public numberOfCards = Array.from({ length: 6 }, (_, index) => index); } diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index ee99bb913..07e745d9b 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -89,6 +89,10 @@ body { } } +body:has(.ai-panel-sidebar-content_open) .add-row-fab { + display: none !important; +} + @keyframes shimmer { 100% { transform: translateX(100%); From 933cfbceaed9ef075ea0ec086878408a6e45b633 Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Tue, 2 Jun 2026 15:27:00 +0300 Subject: [PATCH 4/8] feat(mobile): inline quick-filter chips with horizontal scroll + paginator polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a "+" / "Create fast filter" trigger after the Filter button on mobile, with a vertical divider in between - Horizontally scroll the saved-filter chips inside the search row (custom chip render, absolutely positioned strip so it can't push the page wide) - Each chip has a three-dots menu (Edit / Delete) wired to the saved-filters-panel handlers via @ViewChild - Hide the panel's own mobile dropdown/list — its conditions editor remains visible below the row when a fast filter is selected - When conditions show, the panel takes the full second row (:has) and Columns/Sort stay right-aligned on the row above - Tighten spacing between conditions and the Columns/Sort row; align the "where" label and chips vertically (min-height: 32px) - Make Sort by default black/white, accent only when sort is active; pull the swap_vert icon tight to the label - Inline preview divider only spans the text area (::after with 20px insets) instead of edge-to-edge - Paginator on mobile: items-per-page flush left, range/buttons right, single row, "Rows per page:" label, looser select for readability - Drop the trailing period from the "No records match…" empty-state strings - Open the filter dialog as a mobile bottom sheet when the active filter chip is tapped (was a centered modal) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../db-table-row-view.component.css | 15 ++- .../db-table-view/db-table-view.component.css | 119 +++++++++++++++++- .../db-table-view.component.html | 52 +++++++- .../db-table-view/db-table-view.component.ts | 10 +- .../saved-filters-panel.component.css | 21 ++-- 5 files changed, 197 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css index a4cc7de50..c0dbacf53 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css @@ -125,6 +125,7 @@ } .row-preview-sidebar__field { + position: relative; display: flex; flex-direction: column; align-items: flex-start; @@ -132,13 +133,19 @@ padding: 12px 20px; } -.row-preview-sidebar__field:not(:last-child) { - border-bottom: solid 1px rgba(0, 0, 0, 0.12); +.row-preview-sidebar__field:not(:last-child)::after { + content: ''; + position: absolute; + left: 20px; + right: 20px; + bottom: 0; + height: 1px; + background: rgba(0, 0, 0, 0.12); } @media (prefers-color-scheme: dark) { - .row-preview-sidebar__field:not(:last-child) { - border-bottom: solid 1px rgba(255, 255, 255, 0.04); + .row-preview-sidebar__field:not(:last-child)::after { + background: rgba(255, 255, 255, 0.04); } } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css index ccc800828..a2f9e7e38 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css @@ -613,7 +613,11 @@ display: contents; } -.search-row__filter { +.search-row__filter, +.search-row__add-quick-filter, +.search-row__create-fast-filter, +.search-row__divider, +.search-row__quick-chips-slot { display: none; } @@ -772,7 +776,7 @@ align-items: center; border: 1px solid var(--mdc-outlined-button-outline-color, rgba(0, 0, 0, 0.12)); background: transparent; - color: rgba(0, 0, 0, 0.87); + color: var(--color-primaryPalette-700); font-size: 13px; font-weight: 500; padding: 0 8px 0 12px; @@ -813,7 +817,7 @@ @media (prefers-color-scheme: dark) { .search-row__quick-chip { border-color: var(--mdc-outlined-button-outline-color, rgba(255, 255, 255, 0.24)); - color: rgba(255, 255, 255, 0.87); + color: var(--color-primaryPalette-100); } } @@ -926,15 +930,32 @@ .saved-filters-row { display: flex; - flex-direction: column; + flex-wrap: wrap; align-items: flex-start; - gap: 8px; + justify-content: flex-end; + gap: 4px 8px; + width: 100%; + max-width: 100%; + min-width: 0; + overflow: hidden; } .saved-filters-row > app-saved-filters-panel { flex: 0 1 auto; min-width: 0; + max-width: 100%; display: block; + overflow: hidden; + } + + .saved-filters-row:has(.filters-container) > app-saved-filters-panel { + flex: 0 0 100%; + order: 1; + } + + .saved-filters-row:has(.filters-container) > .saved-filters-row__columns, + .saved-filters-row:has(.filters-container) > .mobile-sort-button { + order: 2; } .saved-filters-row__mobile-actions { @@ -951,11 +972,38 @@ .mobile-sort-button { display: inline-flex !important; align-items: center; + gap: 2px; flex: 0 0 auto; min-height: 36px; padding: 0 4px; font-size: 13px; font-weight: 500; + --mdc-text-button-label-text-color: rgba(0, 0, 0, 0.87); + --mat-mdc-button-persistent-ripple-color: rgba(0, 0, 0, 0.87); + color: rgba(0, 0, 0, 0.87) !important; + } + + .mobile-sort-button .mat-icon, + .mobile-sort-button .mat-mdc-button-touch-target ~ * { + color: inherit !important; + } + + .mobile-sort-button ::ng-deep .mat-mdc-button > .mat-icon, + .mobile-sort-button ::ng-deep .mdc-button__label > .mat-icon, + .mobile-sort-button .mat-icon { + margin-right: 2px !important; + margin-left: 0 !important; + } + + .mobile-sort-button ::ng-deep .mdc-button__label { + display: inline-flex; + align-items: center; + gap: 2px; + } + + .mobile-sort-button.mobile-sort-button_active { + --mdc-text-button-label-text-color: var(--color-accentedPalette-700); + color: var(--color-accentedPalette-700) !important; } .saved-filters-row__columns { @@ -1005,6 +1053,10 @@ } @media (prefers-color-scheme: dark) and (width <= 600px) { + .mobile-sort-button { + color: rgba(255, 255, 255, 0.87); + } + .mobile-sort-button_active { background: var(--color-accentedPalette-900); border-color: var(--color-accentedPalette-600) !important; @@ -1176,6 +1228,63 @@ margin-bottom: 72px; } +@media (width <= 600px) { + .table-surface mat-paginator { + margin-top: 12px; + } + + .table-surface mat-paginator ::ng-deep .mat-mdc-paginator-outer-container, + .table-surface mat-paginator ::ng-deep .mat-mdc-paginator-container { + justify-content: space-between !important; + flex-wrap: nowrap !important; + min-height: 48px; + padding: 0 !important; + } + + .table-surface mat-paginator ::ng-deep .mat-mdc-paginator-page-size { + margin: 0 !important; + padding: 0 !important; + margin-right: auto !important; + flex: 0 0 auto; + } + + .table-surface mat-paginator ::ng-deep .mat-mdc-paginator-page-size-label { + margin: 0 4px 0 0 !important; + } + + .table-surface mat-paginator ::ng-deep .mat-mdc-paginator-page-size-select { + margin: 0 8px 0 0 !important; + width: auto !important; + } + + .table-surface mat-paginator ::ng-deep .mat-mdc-paginator-page-size-select .mat-mdc-form-field-infix { + width: auto !important; + min-width: 36px !important; + padding-right: 4px !important; + } + + .table-surface mat-paginator ::ng-deep .mat-mdc-paginator-page-size-select .mat-mdc-select { + width: auto !important; + } + + .table-surface mat-paginator ::ng-deep .mat-mdc-paginator-page-size-select .mat-mdc-select-arrow-wrapper { + padding-left: 6px !important; + } + + .table-surface mat-paginator ::ng-deep .mat-mdc-paginator-page-size-select .mat-mdc-text-field-wrapper { + padding: 0 10px !important; + } + + .table-surface mat-paginator ::ng-deep .mat-mdc-paginator-range-actions { + flex: 0 0 auto; + margin: 0 !important; + } + + .table-surface mat-paginator ::ng-deep .mat-mdc-paginator-range-label { + margin: 0 4px !important; + } +} + @media (prefers-color-scheme: dark) { .table-surface { --mat-table-background-color: var(--surface-dark-color); diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index 5bedbdcb0..319bb13d1 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -98,6 +98,56 @@

{{ displayName }}

filter_list Filter + + + + +
+
+ +
+ {{ f.name }} + +
+
+
+ + + +
@@ -506,7 +556,7 @@

{{ displayName }}

filter_list_off

- {{ hasSavedFilterActive ? 'No records match the selected fast filter.' : 'No records match this filter.' }} + {{ hasSavedFilterActive ? 'No records match the selected fast filter' : 'No records match this filter' }}

diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index 10e6caa4c..8f151871c 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -179,6 +179,9 @@ export class DbTableViewComponent implements OnInit, OnChanges { @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; + @ViewChild(SavedFiltersPanelComponent) savedFiltersPanel?: SavedFiltersPanelComponent; + + public chipFilterForMenu: any = null; public defaultSort: { column: string; direction: 'asc' | 'desc' } | null = null; private sortInitialized: boolean = false; @@ -195,7 +198,7 @@ export class DbTableViewComponent implements OnInit, OnChanges { private cdr: ChangeDetectorRef, private paginatorIntl: MatPaginatorIntl, ) { - this.paginatorIntl.itemsPerPageLabel = 'per page:'; + this.paginatorIntl.itemsPerPageLabel = 'Rows per page:'; this.paginatorIntl.changes.next(); } @@ -595,6 +598,10 @@ export class DbTableViewComponent implements OnInit, OnChanges { this.searchString = ''; } + handleOpenCreateQuickFilter() { + this.savedFiltersPanel?.handleOpenSavedFiltersDialog(); + } + handleSearch() { this.searchString = this.searchString.trim(); this.staticSearchString = this.searchString; @@ -907,6 +914,7 @@ export class DbTableViewComponent implements OnInit, OnChanges { handleActiveFilterClick(filterKey: string) { const dialogRef = this.dialog.open(DbTableFiltersDialogComponent, { width: '56em', + panelClass: 'mobile-bottom-sheet-dialog', data: { connectionID: this.connectionID, tableName: this.name, diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css index 2180b4c10..8a3ae464b 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css @@ -26,10 +26,6 @@ } @media (width <= 600px) { - .saved-filters-trigger { - display: inline-flex; - } - .saved-filters-trigger_active { width: calc(100vw - 130px - 2*24px); box-sizing: border-box; @@ -49,8 +45,7 @@ flex: 1 1 auto; } - .create-filter-button, - .saved-filters-tabs { + .saved-filters-list { display: none !important; } } @@ -316,12 +311,12 @@ @media (width <= 600px) { .filters-container { + display: flex; align-items: center; flex-direction: row; flex-wrap: wrap; - gap: 6px; - margin-top: 0; - margin-bottom: 16px; + gap: 6px 8px; + margin: 4px 0 0; padding: 8px 16px; border-radius: 10px; background: rgba(0, 0, 0, 0.03); @@ -332,6 +327,11 @@ overflow-y: visible; } + .filters-container > .static-filters { + align-items: center; + min-height: 32px; + } + .dynamic-column-editor { flex: 1 1 100%; @@ -432,6 +432,9 @@ @media (width <= 600px) { .filters-where-label { margin-top: 0 !important; + min-height: 32px; + align-items: center; + line-height: 1; } } From b44ebf2f000cbebc61fc9c1059b99ce5cde02c13 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Thu, 4 Jun 2026 12:28:47 +0000 Subject: [PATCH 5/8] table view: fix columns and sort by buttons in mobile version --- .../dashboard/db-table-view/db-table-view.component.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css index a2f9e7e38..0b642e84e 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css @@ -962,11 +962,11 @@ display: flex; align-items: center; justify-content: flex-end; + gap: 8px; /* grow to fill the row so the buttons sit on the far right; panel keeps its natural width to avoid overlapping .saved-filters-trigger_active */ flex: 1 1 auto; min-width: 0; width: 100%; - margin-bottom: -44px; } .mobile-sort-button { From 8439cccb5a7692e10916d574aa1af6f9670d8f34 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Wed, 3 Jun 2026 12:10:20 +0000 Subject: [PATCH 6/8] connection settings mobile version: fix colors inputs --- .../connection-settings/connection-settings.component.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/connection-settings/connection-settings.component.css b/frontend/src/app/components/connection-settings/connection-settings.component.css index a34b0039d..3b32833ba 100644 --- a/frontend/src/app/components/connection-settings/connection-settings.component.css +++ b/frontend/src/app/components/connection-settings/connection-settings.component.css @@ -181,13 +181,13 @@ @media (width <= 600px) { .color-theme { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - grid-gap: 8px; - align-items: center; + display: flex; + flex-direction: column; + gap: 0; } .color-item { + width: 100%; min-width: 0; } From e2cf2996f55a8e7f166c26245cb58e9622470f0e Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Wed, 3 Jun 2026 13:11:33 +0000 Subject: [PATCH 7/8] permissions: update users names and email styling --- .../app/components/users/users.component.css | 32 ++++++++++++----- .../app/components/users/users.component.html | 34 ++++++++++--------- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/frontend/src/app/components/users/users.component.css b/frontend/src/app/components/users/users.component.css index 14d9166d3..625bbec2a 100644 --- a/frontend/src/app/components/users/users.component.css +++ b/frontend/src/app/components/users/users.component.css @@ -112,6 +112,29 @@ header { } } +.user { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 0; +} + +.user__label { + display: flex; + flex-direction: column; +} + +.user__email { + font-size: 12px; + color: rgba(0, 0, 0, 0.64); +} + +@media (prefers-color-scheme: dark) { + .user__email { + color: rgba(255, 255, 255, 0.64); + } +} + .group-members-count { font-size: 11px; opacity: 0.45; @@ -119,15 +142,6 @@ header { white-space: nowrap; } -.user { - display: flex; - align-items: center; - justify-content: space-between; - box-sizing: border-box; - padding-right: 1.5em; - width: calc(100% + 1em); -} - .no-access { margin-top: 32px !important; } diff --git a/frontend/src/app/components/users/users.component.html b/frontend/src/app/components/users/users.component.html index 09adaf50e..803628337 100644 --- a/frontend/src/app/components/users/users.component.html +++ b/frontend/src/app/components/users/users.component.html @@ -96,30 +96,32 @@

User groups

@if (groupUsers()[groupItem.group.id] === 'empty') {

No users in the group

} - +
    @if (getGroupUsers(groupItem.group.id); as usersList) { @for (user of usersList; track user.email) { - -
    +
  • +
    @if (user.name) { - {{user.name}} ({{user.email}}) + {{user.name}} + } @else { - {{user.email}} - } - @if (currentUser()?.email !== user.email && canManage()) { - + {{user.email}} }
    - + + @if (currentUser()?.email !== user.email && canManage()) { + + } +
  • } } - +
} From fafa80af7237177f8a54591210e17dc9ad9d640c Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Thu, 4 Jun 2026 12:48:56 +0000 Subject: [PATCH 8/8] fix(cedar): resolve ExpressionChangedAfterItHasBeenCheckedError in policy editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Add policy" button binding read policyList?.policies()?.length and policyList?.showAddForm — a @ViewChild query that resolves mid change-detection cycle, causing the value to change after it was checked. Use the parent-owned policyItems().length instead, and convert showAddForm to a signal so it reads reactively with a stable initial value. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../cedar-policy-editor-dialog.component.html | 2 +- .../cedar-policy-editor-dialog.component.ts | 2 +- .../cedar-policy-list.component.html | 6 +++--- .../cedar-policy-list.component.spec.ts | 20 +++++++++---------- .../cedar-policy-list.component.ts | 10 +++++----- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.html b/frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.html index 8bdbe20c6..8c033dcb3 100644 --- a/frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.html +++ b/frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.html @@ -50,7 +50,7 @@

Policy — {{ data.groupTitle }}

- @if (editorMode() === 'form' && !formParseError() && !loading() && !policyList?.showAddForm && policyList?.policies()?.length) { + @if (editorMode() === 'form' && !formParseError() && !loading() && !policyList?.showAddForm() && policyItems().length) { @@ -120,7 +120,7 @@ } - @if (showAddForm) { + @if (showAddForm()) {
diff --git a/frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts b/frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts index 41b4f8990..9e04bdf87 100644 --- a/frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts +++ b/frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts @@ -82,21 +82,21 @@ describe('CedarPolicyListComponent', () => { let emitted: { action: string; tableName?: string; dashboardId?: string }[] | null = null; component.policiesChange.subscribe((v) => (emitted = v)); - component.showAddForm = true; + component.showAddForm.set(true); component.newAction = 'connection:read'; component.addPolicy(); expect(emitted).not.toBeNull(); expect(emitted!.length).toBe(1); expect(emitted![0].action).toBe('connection:read'); - expect(component.showAddForm).toBe(false); + expect(component.showAddForm()).toBe(false); }); it('should add a table policy with tableName', () => { let emitted: { action: string; tableName?: string; dashboardId?: string }[] | null = null; component.policiesChange.subscribe((v) => (emitted = v)); - component.showAddForm = true; + component.showAddForm.set(true); component.newAction = 'table:read'; component.newTableName = 'customers'; component.addPolicy(); @@ -110,7 +110,7 @@ describe('CedarPolicyListComponent', () => { let emitted: { action: string; tableName?: string; dashboardId?: string }[] | null = null; component.policiesChange.subscribe((v) => (emitted = v)); - component.showAddForm = true; + component.showAddForm.set(true); component.newAction = 'table:edit'; component.newTableName = '*'; component.addPolicy(); @@ -124,7 +124,7 @@ describe('CedarPolicyListComponent', () => { let emitted = false; component.policiesChange.subscribe(() => (emitted = true)); - component.showAddForm = true; + component.showAddForm.set(true); component.newAction = ''; component.addPolicy(); @@ -135,7 +135,7 @@ describe('CedarPolicyListComponent', () => { let emitted = false; component.policiesChange.subscribe(() => (emitted = true)); - component.showAddForm = true; + component.showAddForm.set(true); component.newAction = 'table:read'; component.newTableName = ''; component.addPolicy(); @@ -211,12 +211,12 @@ describe('CedarPolicyListComponent', () => { }); it('should reset add form', () => { - component.showAddForm = true; + component.showAddForm.set(true); component.newAction = 'connection:read'; component.newTableName = 'test'; component.resetAddForm(); - expect(component.showAddForm).toBe(false); + expect(component.showAddForm()).toBe(false); expect(component.newAction).toBe(''); expect(component.newTableName).toBe(''); }); @@ -225,7 +225,7 @@ describe('CedarPolicyListComponent', () => { let emitted: { action: string; tableName?: string; dashboardId?: string }[] | null = null; component.policiesChange.subscribe((v) => (emitted = v)); - component.showAddForm = true; + component.showAddForm.set(true); component.newAction = 'dashboard:read'; component.newDashboardId = 'dash-1'; component.addPolicy(); @@ -239,7 +239,7 @@ describe('CedarPolicyListComponent', () => { let emitted = false; component.policiesChange.subscribe(() => (emitted = true)); - component.showAddForm = true; + component.showAddForm.set(true); component.newAction = 'dashboard:edit'; component.newDashboardId = ''; component.addPolicy(); diff --git a/frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts b/frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts index d15edceee..5d416cd5d 100644 --- a/frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts +++ b/frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, computed, inject, input, output } from '@angular/core'; +import { Component, computed, inject, input, output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -53,7 +53,7 @@ export class CedarPolicyListComponent { readonly loading = input(false); readonly policiesChange = output(); - showAddForm = false; + readonly showAddForm = signal(false); newAction = ''; newTableName = ''; newDashboardId = ''; @@ -156,11 +156,11 @@ export class CedarPolicyListComponent { } hasPendingChanges(): boolean { - return (this.showAddForm && !!this.newAction) || this.editingIndex !== null; + return (this.showAddForm() && !!this.newAction) || this.editingIndex !== null; } discardPending() { - if (this.showAddForm) this.resetAddForm(); + if (this.showAddForm()) this.resetAddForm(); if (this.editingIndex !== null) this.cancelEdit(); } @@ -225,7 +225,7 @@ export class CedarPolicyListComponent { } resetAddForm() { - this.showAddForm = false; + this.showAddForm.set(false); this.newAction = ''; this.newTableName = ''; this.newDashboardId = '';