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 beec4e212..d0e0a4af2 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -5,8 +5,8 @@ class="main-menu-sidenav" > Rocketadmin - - + @@ -21,7 +21,7 @@ {{navigationTabs[tab].caption}} - + @@ -30,19 +30,27 @@
Account
{{currentUser.email}}
- + apartment
Company
- + + + database + +
Hosted databases
+
+ key
Secrets
- + electric_bolt @@ -53,17 +61,18 @@ chat_bubble Chat with support - + help
Help center
- + logout
Log out
+ routerLinkActive="nav-bar__button_active" + (click)="drawer.close()"> Upgrade @@ -93,7 +102,7 @@ class="logo__image"> - + Rocketadmin logo @@ -130,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/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/connection-settings/connection-settings.component.css b/frontend/src/app/components/connection-settings/connection-settings.component.css index da3d1c426..3b32833ba 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: flex; + flex-direction: column; + gap: 0; + } + + .color-item { + width: 100%; + 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/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..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,9 +73,9 @@ .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: 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), @@ -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-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 4be4b813f..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 @@ -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; } @@ -677,6 +681,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: var(--color-primaryPalette-700); + 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: var(--color-primaryPalette-100); + } + } + + .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,38 +923,87 @@ } @media (width <= 600px) { + .skeleton.mat-elevation-z4 { + box-shadow: none !important; + background: transparent !important; + } + .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 { 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 { 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 { @@ -845,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; @@ -1016,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 359a875e2..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 @@ -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'; @@ -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; @@ -193,7 +196,11 @@ export class DbTableViewComponent implements OnInit, OnChanges { public router: Router, public dialog: MatDialog, private cdr: ChangeDetectorRef, - ) {} + private paginatorIntl: MatPaginatorIntl, + ) { + this.paginatorIntl.itemsPerPageLabel = 'Rows per page:'; + this.paginatorIntl.changes.next(); + } ngAfterViewInit() { this.tableData.paginator = this.paginator; @@ -295,6 +302,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]; @@ -586,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; @@ -898,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/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/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; } } 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/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..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 @@ -48,14 +48,14 @@

Policy — {{ data.groupTitle }}

} - @if (editorMode() === 'form' && !formParseError() && !loading() && !policyList?.showAddForm) { + + + @if (editorMode() === 'form' && !formParseError() && !loading() && !policyList?.showAddForm() && policyItems().length) { } - -
} @@ -115,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 = ''; 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..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,28 @@ 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; } + +@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.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()) { + + } +
  • } } - +
} 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 }, }); } 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%);