From 7b275f380823aa253de4806a23fb7dc1cf7c4942 Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Thu, 15 Jan 2026 15:48:04 +0200 Subject: [PATCH 01/16] feat(audit): improve date format and add status badge styling - Change date format from "P p" to "d MMM yyyy 'at' h:mm a" - Add green status badge for successful operations Co-Authored-By: Claude Opus 4.5 --- .../src/app/components/audit/audit-data-source.ts | 2 +- frontend/src/app/components/audit/audit.component.css | 11 +++++++++++ .../src/app/components/audit/audit.component.html | 9 +++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/audit/audit-data-source.ts b/frontend/src/app/components/audit/audit-data-source.ts index 1bc694875..5738fe1c0 100644 --- a/frontend/src/app/components/audit/audit-data-source.ts +++ b/frontend/src/app/components/audit/audit-data-source.ts @@ -70,7 +70,7 @@ export class AuditDataSource implements DataSource { } const formattedLogs = res.logs.map(log => { const date = new Date(log.createdAt); - const formattedDate = format(date, "P p") + const formattedDate = format(date, "d MMM yyyy 'at' h:mm a") return { "Table": log.table_name, "User": log.email, diff --git a/frontend/src/app/components/audit/audit.component.css b/frontend/src/app/components/audit/audit.component.css index 4e5bf39de..38b199ba7 100644 --- a/frontend/src/app/components/audit/audit.component.css +++ b/frontend/src/app/components/audit/audit.component.css @@ -110,4 +110,15 @@ th.mat-header-cell, td.mat-cell { .hidden { display: none; +} + +.status-badge { + display: inline-block; + padding: 4px 12px; + border-radius: 4px; +} + +.status-succeed { + background-color: #EBF9EB; + color: #1A5E20; } \ No newline at end of file diff --git a/frontend/src/app/components/audit/audit.component.html b/frontend/src/app/components/audit/audit.component.html index 37b4601e2..306133064 100644 --- a/frontend/src/app/components/audit/audit.component.html +++ b/frontend/src/app/components/audit/audit.component.html @@ -72,8 +72,13 @@

Rocketadmin can not find any tables

{{ column }} -
- {{element[column] || '—'}} +
+ + {{element[column] || '—'}} + + {{element[column] || '—'}}
From 37967458cb5e88c93cbba5e3fe9add29cdfb54dc Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Thu, 15 Jan 2026 15:50:27 +0200 Subject: [PATCH 02/16] feat(audit): add action icons and improve UI - Add Material icons for actions (add, delete, create, visibility) - Rename Details column to Changes - Add new fields: IsAddAction, DateOnly, TimeOnly - Style icons with appropriate colors per action type Co-Authored-By: Claude Opus 4.5 --- .../app/components/audit/audit-data-source.ts | 18 +++++++++++- .../app/components/audit/audit.component.css | 28 +++++++++++++++++++ .../app/components/audit/audit.component.html | 6 +++- .../app/components/audit/audit.component.ts | 2 ++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/audit/audit-data-source.ts b/frontend/src/app/components/audit/audit-data-source.ts index 5738fe1c0..c5dc1f91d 100644 --- a/frontend/src/app/components/audit/audit-data-source.ts +++ b/frontend/src/app/components/audit/audit-data-source.ts @@ -68,19 +68,35 @@ export class AuditDataSource implements DataSource { rowReceived: 'received row', rowsReceived: 'received rows' } + + const actionIcons = { + addRow: 'add', + deleteRow: 'delete', + updateRow: 'create', + rowReceived: 'visibility', + rowsReceived: 'visibility' + } + const formattedLogs = res.logs.map(log => { const date = new Date(log.createdAt); - const formattedDate = format(date, "d MMM yyyy 'at' h:mm a") + const formattedDate = format(date, "d MMM yyyy 'at' h:mm a"); + const dateOnly = format(date, "d MMM yyyy"); + const timeOnly = format(date, "h:mm a"); + return { "Table": log.table_name, "User": log.email, "Action": actions[log.operationType], + "ActionIcon": actionIcons[log.operationType], "Date": formattedDate, + "DateOnly": dateOnly, + "TimeOnly": timeOnly, "Status": log.operationStatusResult, operationType: log.operationType, createdAt: log.createdAt, prevValue: log.old_data, currentValue: log.received_data, + IsAddAction: log.operationType === 'addRow' } }); this.rowsSubject.next(formattedLogs); diff --git a/frontend/src/app/components/audit/audit.component.css b/frontend/src/app/components/audit/audit.component.css index 38b199ba7..98a1e5dbc 100644 --- a/frontend/src/app/components/audit/audit.component.css +++ b/frontend/src/app/components/audit/audit.component.css @@ -121,4 +121,32 @@ th.mat-header-cell, td.mat-cell { .status-succeed { background-color: #EBF9EB; color: #1A5E20; +} + +.action-cell { + display: flex; + align-items: center; + gap: 8px; +} + +.action-icon { + font-size: 18px; + width: 18px; + height: 18px; +} + +.action-icon--add { + color: #1A5E20; +} + +.action-icon--delete { + color: #C62828; +} + +.action-icon--create { + color: #1565C0; +} + +.action-icon--visibility { + color: #6A6A6A; } \ No newline at end of file diff --git a/frontend/src/app/components/audit/audit.component.html b/frontend/src/app/components/audit/audit.component.html index 306133064..f664150f6 100644 --- a/frontend/src/app/components/audit/audit.component.html +++ b/frontend/src/app/components/audit/audit.component.html @@ -78,7 +78,11 @@

Rocketadmin can not find any tables

[ngClass]="{'status-succeed': element[column] === 'successfully'}"> {{element[column] || '—'}} - {{element[column] || '—'}} + + {{element.ActionIcon}} + {{element[column] || '—'}} + + {{element[column] || '—'}} diff --git a/frontend/src/app/components/audit/audit.component.ts b/frontend/src/app/components/audit/audit.component.ts index a598c3d6e..a837856c9 100644 --- a/frontend/src/app/components/audit/audit.component.ts +++ b/frontend/src/app/components/audit/audit.component.ts @@ -12,6 +12,7 @@ import { FormsModule } from '@angular/forms'; import { InfoDialogComponent } from './info-dialog/info-dialog.component'; import { Log } from 'src/app/models/logs'; import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatPaginator } from '@angular/material/paginator'; @@ -40,6 +41,7 @@ import { normalizeTableName } from 'src/app/lib/normalize'; MatFormFieldModule, MatSelectModule, MatButtonModule, + MatIconModule, MatTableModule, MatPaginatorModule, FormsModule, From ddab40151ef2457b0a7ca86b110acc1697bbf125 Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Mon, 19 Jan 2026 10:41:59 +0200 Subject: [PATCH 03/16] feat(audit): display user name above email in User column - Show user name on top with normal font weight - Show email below in smaller secondary text - Look up user name from usersList by email Co-Authored-By: Claude Opus 4.5 --- .../app/components/audit/audit-data-source.ts | 1 + .../app/components/audit/audit.component.css | 21 +++++++++++++++++++ .../app/components/audit/audit.component.html | 6 +++++- .../app/components/audit/audit.component.ts | 6 ++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/audit/audit-data-source.ts b/frontend/src/app/components/audit/audit-data-source.ts index c5dc1f91d..181466a7e 100644 --- a/frontend/src/app/components/audit/audit-data-source.ts +++ b/frontend/src/app/components/audit/audit-data-source.ts @@ -86,6 +86,7 @@ export class AuditDataSource implements DataSource { return { "Table": log.table_name, "User": log.email, + "UserEmail": log.email, "Action": actions[log.operationType], "ActionIcon": actionIcons[log.operationType], "Date": formattedDate, diff --git a/frontend/src/app/components/audit/audit.component.css b/frontend/src/app/components/audit/audit.component.css index 98a1e5dbc..378ecc962 100644 --- a/frontend/src/app/components/audit/audit.component.css +++ b/frontend/src/app/components/audit/audit.component.css @@ -149,4 +149,25 @@ th.mat-header-cell, td.mat-cell { .action-icon--visibility { color: #6A6A6A; +} + +.user-cell { + display: flex; + flex-direction: column; + gap: 2px; +} + +.user-name { + font-weight: 500; +} + +.user-email--secondary { + font-size: 12px; + color: rgba(0, 0, 0, 0.6); +} + +@media (prefers-color-scheme: dark) { + .user-email--secondary { + color: rgba(255, 255, 255, 0.6); + } } \ No newline at end of file diff --git a/frontend/src/app/components/audit/audit.component.html b/frontend/src/app/components/audit/audit.component.html index f664150f6..68570c2cb 100644 --- a/frontend/src/app/components/audit/audit.component.html +++ b/frontend/src/app/components/audit/audit.component.html @@ -82,7 +82,11 @@

Rocketadmin can not find any tables

{{element.ActionIcon}} {{element[column] || '—'}} - {{element[column] || '—'}} + + {{getUserName(element.UserEmail)}} + {{element.UserEmail}} + + {{element[column] || '—'}} diff --git a/frontend/src/app/components/audit/audit.component.ts b/frontend/src/app/components/audit/audit.component.ts index a837856c9..b3ece720c 100644 --- a/frontend/src/app/components/audit/audit.component.ts +++ b/frontend/src/app/components/audit/audit.component.ts @@ -144,4 +144,10 @@ export class AuditComponent implements OnInit { // @ts-expect-error Intercom('show'); } + + getUserName(email: string): string | null { + if (!this.usersList) return null; + const user = this.usersList.find(u => u.email === email); + return user?.name || null; + } } From 9f3a1a02c2ed23b3c065bf8f46abf099cc829e96 Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Mon, 19 Jan 2026 10:47:59 +0200 Subject: [PATCH 04/16] feat(audit): use user-friendly action labels - Created (was "added row") - Deleted (was "deleted row") - Edited (was "edit row") - Viewed (was "received row/rows") Co-Authored-By: Claude Opus 4.5 --- frontend/src/app/components/audit/audit-data-source.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/audit/audit-data-source.ts b/frontend/src/app/components/audit/audit-data-source.ts index 181466a7e..8290d28a1 100644 --- a/frontend/src/app/components/audit/audit-data-source.ts +++ b/frontend/src/app/components/audit/audit-data-source.ts @@ -62,11 +62,11 @@ export class AuditDataSource implements DataSource { .subscribe((res: any) => { const actions = { - addRow: 'added row', - deleteRow: 'deleted row', - updateRow: 'edit row', - rowReceived: 'received row', - rowsReceived: 'received rows' + addRow: 'Created', + deleteRow: 'Deleted', + updateRow: 'Edited', + rowReceived: 'Viewed', + rowsReceived: 'Viewed' } const actionIcons = { From 2042ed394f24b9b31687edda502273f1b55f0663 Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Mon, 19 Jan 2026 11:11:03 +0200 Subject: [PATCH 05/16] feat(audit): use outlined visibility icon - Use Material Icons Outlined for visibility icon Co-Authored-By: Claude Opus 4.5 --- .../app/components/audit/audit-data-source.ts | 10 ++-- .../app/components/audit/audit.component.css | 52 ++++++++++++++++--- .../app/components/audit/audit.component.html | 18 ++++--- .../app/components/audit/audit.component.ts | 2 +- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/components/audit/audit-data-source.ts b/frontend/src/app/components/audit/audit-data-source.ts index 8290d28a1..0733ff351 100644 --- a/frontend/src/app/components/audit/audit-data-source.ts +++ b/frontend/src/app/components/audit/audit-data-source.ts @@ -66,15 +66,19 @@ export class AuditDataSource implements DataSource { deleteRow: 'Deleted', updateRow: 'Edited', rowReceived: 'Viewed', - rowsReceived: 'Viewed' + rowsReceived: 'Viewed', + importRow: 'Imported', + exportRows: 'Exported' } const actionIcons = { addRow: 'add', deleteRow: 'delete', - updateRow: 'create', + updateRow: 'edit', rowReceived: 'visibility', - rowsReceived: 'visibility' + rowsReceived: 'visibility', + importRow: 'upload', + exportRows: 'download' } const formattedLogs = res.logs.map(log => { diff --git a/frontend/src/app/components/audit/audit.component.css b/frontend/src/app/components/audit/audit.component.css index 378ecc962..1e17caf6e 100644 --- a/frontend/src/app/components/audit/audit.component.css +++ b/frontend/src/app/components/audit/audit.component.css @@ -58,10 +58,25 @@ header { width: 100%; } +.mat-mdc-row:hover { + background-color: rgba(0, 0, 0, 0.02) !important; +} + +@media (prefers-color-scheme: dark) { + .mat-mdc-row:hover { + background-color: rgba(255, 255, 255, 0.04) !important; + } +} + th.mat-header-cell, td.mat-cell { padding-right: 20px; } +td.mat-cell { + padding-top: 12px; + padding-bottom: 12px; +} + .table-cell-content_break-words { padding: 16px 0; word-break: break-all; @@ -130,9 +145,9 @@ th.mat-header-cell, td.mat-cell { } .action-icon { - font-size: 18px; - width: 18px; - height: 18px; + font-size: 16px; + width: 16px; + height: 16px; } .action-icon--add { @@ -143,18 +158,26 @@ th.mat-header-cell, td.mat-cell { color: #C62828; } -.action-icon--create { - color: #1565C0; +.action-icon--edit { + color: var(--color-accentedPalette-500); } .action-icon--visibility { color: #6A6A6A; } +.action-icon--upload { + color: #2E7D32; +} + +.action-icon--download { + color: #1565C0; +} + .user-cell { display: flex; flex-direction: column; - gap: 2px; + gap: 0; } .user-name { @@ -170,4 +193,21 @@ th.mat-header-cell, td.mat-cell { .user-email--secondary { color: rgba(255, 255, 255, 0.6); } +} + +.date-cell { + display: flex; + flex-direction: column; + gap: 0; +} + +.date-time { + font-size: 12px; + color: rgba(0, 0, 0, 0.6); +} + +@media (prefers-color-scheme: dark) { + .date-time { + color: rgba(255, 255, 255, 0.6); + } } \ No newline at end of file diff --git a/frontend/src/app/components/audit/audit.component.html b/frontend/src/app/components/audit/audit.component.html index 68570c2cb..e5a170145 100644 --- a/frontend/src/app/components/audit/audit.component.html +++ b/frontend/src/app/components/audit/audit.component.html @@ -79,26 +79,32 @@

Rocketadmin can not find any tables

{{element[column] || '—'}} - {{element.ActionIcon}} + {{element.ActionIcon}} + visibility + delete {{element[column] || '—'}} {{getUserName(element.UserEmail)}} {{element.UserEmail}} - {{element[column] || '—'}} + + {{element.DateOnly}} + {{element.TimeOnly}} + + {{element[column] || '—'}} - - Details + + Changes - diff --git a/frontend/src/app/components/audit/audit.component.ts b/frontend/src/app/components/audit/audit.component.ts index b3ece720c..0fd843447 100644 --- a/frontend/src/app/components/audit/audit.component.ts +++ b/frontend/src/app/components/audit/audit.component.ts @@ -98,7 +98,7 @@ export class AuditComponent implements OnInit { }); this.connectionID = this._connections.currentConnectionID; this.accesLevel = this._connections.currentConnectionAccessLevel; - this.columns = ['Table', 'User', 'Action', 'Date', 'Status', 'Details']; + this.columns = ['Table', 'User', 'Action', 'Date', 'Status', 'Changes']; this.dataColumns = ['Table', 'User', 'Action', 'Date', 'Status']; this.dataSource = new AuditDataSource(this._connections); this.loadLogsPage(); From bb5b86753d72b8d673015f0eadc8bbd3567143f1 Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Mon, 19 Jan 2026 14:38:51 +0200 Subject: [PATCH 06/16] feat(audit): add export rows logging to audit - Add exportRows to LogOperationTypeEnum - Add audit logging to export CSV use case - Log export operations with success/failure status Co-Authored-By: Claude Opus 4.5 --- .../use-cases/export-csv-from-table.use.case.ts | 17 +++++++++++++++++ backend/src/enums/log-operation-type.enum.ts | 1 + 2 files changed, 18 insertions(+) diff --git a/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts b/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts index f4ce1e867..74a4d97c6 100644 --- a/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts +++ b/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts @@ -5,6 +5,8 @@ import { BaseType } from '../../../common/data-injection.tokens.js'; import { GetTableRowsDs } from '../application/data-structures/get-table-rows.ds.js'; import { IExportCSVFromTable } from './table-use-cases.interface.js'; import { Messages } from '../../../exceptions/text/messages.js'; +import { LogOperationTypeEnum, OperationResultStatusEnum } from '../../../enums/index.js'; +import { TableLogsService } from '../../table-logs/table-logs.service.js'; import { findFilteringFieldsUtil, parseFilteringFieldsFromBodyData } from '../utils/find-filtering-fields.util.js'; import { findOrderingFieldUtil } from '../utils/find-ordering-field.util.js'; import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js'; @@ -26,6 +28,7 @@ export class ExportCSVFromTableUseCase constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private tableLogsService: TableLogsService, ) { super(); } @@ -47,6 +50,8 @@ export class ExportCSVFromTableUseCase throw new NonAvailableInFreePlanException(Messages.CONNECTION_IS_FROZEN); } + let operationResult = OperationResultStatusEnum.unknown; + try { const dao = getDataAccessObject(connection); @@ -103,6 +108,8 @@ export class ExportCSVFromTableUseCase filteringFields, ); + operationResult = OperationResultStatusEnum.successfully; + //todo: rework as streams when node oracle driver will support it correctly //todo: agent return data as array of table rows, not as stream, because we cant //todo: transfer data as a stream from clint to server @@ -119,6 +126,7 @@ export class ExportCSVFromTableUseCase } return new StreamableFile(rowsStream.pipe(csv.stringify({ header: true }))); } catch (error) { + operationResult = OperationResultStatusEnum.unsuccessfully; if (error instanceof HttpException) { throw error; } @@ -136,6 +144,15 @@ export class ExportCSVFromTableUseCase }, HttpStatus.INTERNAL_SERVER_ERROR, ); + } finally { + const logRecord = { + table_name: tableName, + userId: userId, + connection: connection, + operationType: LogOperationTypeEnum.exportRows, + operationStatusResult: operationResult, + }; + await this.tableLogsService.crateAndSaveNewLogUtil(logRecord); } } } diff --git a/backend/src/enums/log-operation-type.enum.ts b/backend/src/enums/log-operation-type.enum.ts index 3f7f2a468..f00da0b50 100644 --- a/backend/src/enums/log-operation-type.enum.ts +++ b/backend/src/enums/log-operation-type.enum.ts @@ -6,4 +6,5 @@ export enum LogOperationTypeEnum { rowReceived = 'rowReceived', rowsReceived = 'rowsReceived', actionActivated = 'actionActivated', + exportRows = 'exportRows', } From 1af6e3127610c2038050d24e9b9db4cbcc07cf88 Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Mon, 19 Jan 2026 16:03:06 +0200 Subject: [PATCH 07/16] feat(audit): make User the first column Co-Authored-By: Claude Opus 4.5 --- frontend/src/app/components/audit/audit.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/audit/audit.component.ts b/frontend/src/app/components/audit/audit.component.ts index 0fd843447..2a84bf3b0 100644 --- a/frontend/src/app/components/audit/audit.component.ts +++ b/frontend/src/app/components/audit/audit.component.ts @@ -98,8 +98,8 @@ export class AuditComponent implements OnInit { }); this.connectionID = this._connections.currentConnectionID; this.accesLevel = this._connections.currentConnectionAccessLevel; - this.columns = ['Table', 'User', 'Action', 'Date', 'Status', 'Changes']; - this.dataColumns = ['Table', 'User', 'Action', 'Date', 'Status']; + this.columns = ['User', 'Table', 'Action', 'Date', 'Status', 'Changes']; + this.dataColumns = ['User', 'Table', 'Action', 'Date', 'Status']; this.dataSource = new AuditDataSource(this._connections); this.loadLogsPage(); From c87f628e7af00fe58cbbb9ce28c9b4dd37c1d25a Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Mon, 19 Jan 2026 16:05:26 +0200 Subject: [PATCH 08/16] Swap Date and Status columns in audit table Co-Authored-By: Claude Opus 4.5 --- frontend/src/app/components/audit/audit.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/audit/audit.component.ts b/frontend/src/app/components/audit/audit.component.ts index 2a84bf3b0..a68fc84ab 100644 --- a/frontend/src/app/components/audit/audit.component.ts +++ b/frontend/src/app/components/audit/audit.component.ts @@ -98,8 +98,8 @@ export class AuditComponent implements OnInit { }); this.connectionID = this._connections.currentConnectionID; this.accesLevel = this._connections.currentConnectionAccessLevel; - this.columns = ['User', 'Table', 'Action', 'Date', 'Status', 'Changes']; - this.dataColumns = ['User', 'Table', 'Action', 'Date', 'Status']; + this.columns = ['User', 'Table', 'Action', 'Status', 'Date', 'Changes']; + this.dataColumns = ['User', 'Table', 'Action', 'Status', 'Date']; this.dataSource = new AuditDataSource(this._connections); this.loadLogsPage(); From 1f468cd693843bca6292f8a158cd2c38c0758aff Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Mon, 9 Feb 2026 16:42:06 +0200 Subject: [PATCH 09/16] feat: add profile sidebar navigation for settings pages - Add ProfileSidebarComponent with collapsible state - Extract Branding section into separate /branding route - Extract API Keys section into separate /api-keys route - Move SAML SSO to /saml route (from /sso/:company-id) - Add Pricing tab linking to /upgrade page - Update all profile pages to use sidebar layout - Reduce sidebar icon size and spacing Co-Authored-By: Claude Opus 4.5 --- frontend/src/app/app-routing.module.ts | 17 +- .../api-keys/api-keys.component.css | 200 ++++++++++++ .../api-keys/api-keys.component.html | 79 +++++ .../components/api-keys/api-keys.component.ts | 102 ++++++ .../branding/branding.component.css | 173 ++++++++++ .../branding/branding.component.html | 230 +++++++++++++ .../components/branding/branding.component.ts | 270 ++++++++++++++++ .../components/company/company.component.css | 24 +- .../components/company/company.component.html | 250 +-------------- .../components/company/company.component.ts | 4 +- .../profile-sidebar.component.css | 112 +++++++ .../profile-sidebar.component.html | 104 ++++++ .../profile-sidebar.component.ts | 54 ++++ .../components/secrets/secrets.component.css | 24 +- .../components/secrets/secrets.component.html | 10 +- .../components/secrets/secrets.component.ts | 2 + .../src/app/components/sso/sso.component.css | 81 +++-- .../src/app/components/sso/sso.component.html | 301 +++++++++--------- .../src/app/components/sso/sso.component.ts | 41 ++- .../components/upgrade/upgrade.component.css | 18 +- .../components/upgrade/upgrade.component.html | 248 +++++++-------- .../components/upgrade/upgrade.component.ts | 4 +- .../user-settings/user-settings.component.css | 23 +- .../user-settings.component.html | 242 ++++++-------- .../user-settings/user-settings.component.ts | 47 +-- .../components/zapier/zapier.component.css | 18 +- .../components/zapier/zapier.component.html | 44 +-- .../app/components/zapier/zapier.component.ts | 4 +- frontend/src/app/models/ui-settings.ts | 1 + 29 files changed, 1928 insertions(+), 799 deletions(-) create mode 100644 frontend/src/app/components/api-keys/api-keys.component.css create mode 100644 frontend/src/app/components/api-keys/api-keys.component.html create mode 100644 frontend/src/app/components/api-keys/api-keys.component.ts create mode 100644 frontend/src/app/components/branding/branding.component.css create mode 100644 frontend/src/app/components/branding/branding.component.html create mode 100644 frontend/src/app/components/branding/branding.component.ts create mode 100644 frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.css create mode 100644 frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.html create mode 100644 frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index d7bf6121c..a775781e8 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -86,6 +86,20 @@ const routes: Routes = [ loadComponent: () => import('./components/company/company.component').then((m) => m.CompanyComponent), canActivate: [AuthGuard], }, + { + path: 'branding', + pathMatch: 'full', + loadComponent: () => import('./components/branding/branding.component').then((m) => m.BrandingComponent), + canActivate: [AuthGuard], + title: 'Branding | Rocketadmin', + }, + { + path: 'api-keys', + pathMatch: 'full', + loadComponent: () => import('./components/api-keys/api-keys.component').then((m) => m.ApiKeysComponent), + canActivate: [AuthGuard], + title: 'API Keys | Rocketadmin', + }, { path: 'secrets', pathMatch: 'full', @@ -94,10 +108,11 @@ const routes: Routes = [ title: 'Secrets | Rocketadmin', }, { - path: 'sso/:company-id', + path: 'saml', pathMatch: 'full', loadComponent: () => import('./components/sso/sso.component').then((m) => m.SsoComponent), canActivate: [AuthGuard], + title: 'SAML SSO | Rocketadmin', }, { path: 'change-password', diff --git a/frontend/src/app/components/api-keys/api-keys.component.css b/frontend/src/app/components/api-keys/api-keys.component.css new file mode 100644 index 000000000..f83265228 --- /dev/null +++ b/frontend/src/app/components/api-keys/api-keys.component.css @@ -0,0 +1,200 @@ +.profile-layout { + display: flex; + height: calc(100vh - 64px); +} + +.profile-main { + flex: 1; + overflow-y: auto; +} + +::ng-deep .profile-main > app-alert { + position: relative; + top: 0; + margin: 24px; +} + +.api-keys-page { + margin: 3em auto; + padding: 0 clamp(40px, 5vw, 100px); + max-width: 800px; +} + +@media (width <= 600px) { + .api-keys-page { + padding: 0 16px; + margin: 1.5em auto; + } +} + +.api-keys-description { + color: rgba(0, 0, 0, 0.64); + margin-top: 8px; + margin-bottom: 32px; +} + +@media (prefers-color-scheme: dark) { + .api-keys-description { + color: rgba(255, 255, 255, 0.7); + } +} + +.api-keys-content { + display: flex; + flex-direction: column; + gap: 24px; +} + +.api-key-form { + display: flex; + align-items: flex-start; + gap: 16px; +} + +@media (width <= 600px) { + .api-key-form { + flex-direction: column; + align-items: stretch; + } +} + +.api-key-form__input { + flex: 1; +} + +@media (width <= 600px) { + .api-key-form__input { + width: 100%; + } +} + +.api-key-form__generate-button { + margin-top: 4px; +} + +@media (width <= 600px) { + .api-key-form__generate-button { + margin-top: 0; + } +} + +.api-key-value { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 16px; + background-color: rgba(76, 175, 80, 0.08); + border-radius: 8px; + border: 1px solid rgba(76, 175, 80, 0.24); +} + +@media (prefers-color-scheme: dark) { + .api-key-value { + background-color: rgba(76, 175, 80, 0.12); + border-color: rgba(76, 175, 80, 0.32); + } +} + +.api-key-value__input { + flex: 1; +} + +.api-key-value__copy-button { + margin-top: 4px; +} + +.api-keys-list { + margin-top: 16px; +} + +.api-keys-list__heading { + margin-bottom: 16px; + font-weight: 500; +} + +.api-keys-empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 48px 24px; + text-align: center; + background-color: rgba(0, 0, 0, 0.02); + border-radius: 8px; +} + +@media (prefers-color-scheme: dark) { + .api-keys-empty { + background-color: rgba(255, 255, 255, 0.05); + } +} + +.api-keys-empty__icon { + font-size: 48px; + width: 48px; + height: 48px; + color: rgba(0, 0, 0, 0.26); + margin-bottom: 16px; +} + +@media (prefers-color-scheme: dark) { + .api-keys-empty__icon { + color: rgba(255, 255, 255, 0.3); + } +} + +.api-keys-empty p { + margin: 0; + color: rgba(0, 0, 0, 0.54); +} + +@media (prefers-color-scheme: dark) { + .api-keys-empty p { + color: rgba(255, 255, 255, 0.54); + } +} + +.api-keys-items { + padding: 0; +} + +.api-key-list-item { + border-radius: 8px; + margin-bottom: 8px; +} + +.api-key-list-item:hover { + background-color: rgba(0, 0, 0, 0.04); +} + +@media (prefers-color-scheme: dark) { + .api-key-list-item:hover { + background-color: rgba(255, 255, 255, 0.08); + } +} + +.api-key-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} + +.api-key-item__info { + display: flex; + align-items: center; + gap: 12px; +} + +.api-key-item__icon { + color: rgba(0, 0, 0, 0.54); +} + +@media (prefers-color-scheme: dark) { + .api-key-item__icon { + color: rgba(255, 255, 255, 0.54); + } +} + +.api-key-item__title { + font-weight: 500; +} diff --git a/frontend/src/app/components/api-keys/api-keys.component.html b/frontend/src/app/components/api-keys/api-keys.component.html new file mode 100644 index 000000000..038906493 --- /dev/null +++ b/frontend/src/app/components/api-keys/api-keys.component.html @@ -0,0 +1,79 @@ +
+ + +
+ + +
+

API Keys

+

+ Generate and manage API keys to access Rocketadmin programmatically. +

+ +
+
+ + API key title + + + +
+ +
+ + API key + + + Please save this API key. You won't be able to see it again. + + + +
+ +
+

Your API keys

+ +
+ vpn_key_off +

You don't have any API keys yet.

+
+ + + +
+
+ vpn_key + {{apiKey.title}} +
+ +
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/api-keys/api-keys.component.ts b/frontend/src/app/components/api-keys/api-keys.component.ts new file mode 100644 index 000000000..5314f27da --- /dev/null +++ b/frontend/src/app/components/api-keys/api-keys.component.ts @@ -0,0 +1,102 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +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 { MatInputModule } from '@angular/material/input'; +import { MatListModule } from '@angular/material/list'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { CdkCopyToClipboard } from '@angular/cdk/clipboard'; +import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; +import { Title } from '@angular/platform-browser'; + +import { AlertComponent } from '../ui-components/alert/alert.component'; +import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; +import { UserService } from 'src/app/services/user.service'; +import { CompanyService } from 'src/app/services/company.service'; +import { NotificationsService } from 'src/app/services/notifications.service'; +import { ApiKey } from 'src/app/models/user'; +import { ApiKeyDeleteDialogComponent } from '../user-settings/api-key-delete-dialog/api-key-delete-dialog.component'; + +@Component({ + selector: 'app-api-keys', + templateUrl: './api-keys.component.html', + styleUrls: ['./api-keys.component.css'], + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatListModule, + MatTooltipModule, + CdkCopyToClipboard, + Angulartics2OnModule, + AlertComponent, + ProfileSidebarComponent, + ] +}) +export class ApiKeysComponent implements OnInit { + public apiKeys: ApiKey[] = []; + public generatingAPIkeyTitle: string = ''; + public generatedAPIkeyHash: string = ''; + public submitting: boolean = false; + + constructor( + private _userService: UserService, + private _company: CompanyService, + private _notifications: NotificationsService, + private dialog: MatDialog, + private title: Title, + private angulartics2: Angulartics2, + ) {} + + ngOnInit(): void { + this._company.getCurrentTabTitle().subscribe(tabTitle => { + this.title.setTitle(`API Keys | ${tabTitle || 'Rocketadmin'}`); + }); + this.getAPIkeys(); + } + + getAPIkeys() { + this._userService.getAPIkeys().subscribe(res => this.apiKeys = res); + } + + generateAPIkey() { + this.submitting = true; + this._userService.generateAPIkey(this.generatingAPIkeyTitle).subscribe(res => { + this.generatedAPIkeyHash = res.hash; + this.generatingAPIkeyTitle = ''; + this.getAPIkeys(); + this.submitting = false; + this.angulartics2.eventTrack.next({ + action: 'API Keys: key generated successfully', + }); + }, () => { + this.submitting = false; + }); + } + + deleteAPIkey(apiKey: ApiKey) { + const deleteConfirmation = this.dialog.open(ApiKeyDeleteDialogComponent, { + width: '25em', + data: apiKey + }); + + deleteConfirmation.afterClosed().subscribe(action => { + if (action === 'delete') { + this.getAPIkeys(); + this.angulartics2.eventTrack.next({ + action: 'API Keys: key deleted successfully', + }); + } + }); + } + + showCopyNotification(message: string) { + this._notifications.showSuccessSnackbar(message); + } +} diff --git a/frontend/src/app/components/branding/branding.component.css b/frontend/src/app/components/branding/branding.component.css new file mode 100644 index 000000000..ffb923ac0 --- /dev/null +++ b/frontend/src/app/components/branding/branding.component.css @@ -0,0 +1,173 @@ +.profile-layout { + display: flex; + height: calc(100vh - 64px); +} + +.profile-main { + flex: 1; + overflow-y: auto; +} + +::ng-deep .profile-main > app-alert { + position: relative; + top: 0; + margin: 24px; +} + +.branding-page { + margin: 3em auto; + padding: 0 clamp(40px, 5vw, 100px); + max-width: 1200px; +} + +@media (width <= 600px) { + .branding-page { + padding: 0 16px; + margin: 1.5em auto; + } +} + +.branding-page-header { + margin-bottom: 8px; +} + +.branding-description { + color: rgba(0, 0, 0, 0.64); + margin-top: 0; + margin-bottom: 32px; +} + +@media (prefers-color-scheme: dark) { + .branding-description { + color: rgba(255, 255, 255, 0.7); + } +} + +.text-filed-edit { + position: relative; +} + +.text-filed-edit__input { + width: 100%; +} + +.text-filed-edit__buttons { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(calc(-50% - 10px)); + display: flex; + gap: 4px; +} + +.white-label { + display: grid; + grid-template-columns: 1fr 1fr; + grid-column-gap: 32px; + margin-bottom: 32px; +} + +@media (width <= 900px) { + .white-label { + grid-template-columns: 1fr; + grid-row-gap: 32px; + } +} + +.white-label-settings { + position: relative; +} + +.white-label-settings-images { + display: flex; + gap: 24px; + margin-top: 16px; + margin-bottom: 24px; +} + +.upload-logo-form { + display: flex; + align-items: center; + gap: 8px; +} + +.white-label-preview { + height: auto; + margin-top: 8px; + width: 100%; +} + +.white-label-how-to { + border: 1px solid var(--color-primaryPalette-100); + color: rgba(0,0,0,0.72); + padding: 20px; + height: fit-content; +} + +@media (prefers-color-scheme: dark) { + .white-label-how-to { + border: 1px solid var(--color-primaryPalette-700); + color: rgba(255,255,255,0.7); + } +} + +.white-label-settings__overlay { + position: absolute; + top: -8px; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255,255,255,0.64); + z-index: 2; +} + +@media (prefers-color-scheme: dark) { + .white-label-settings__overlay { + background-color: rgba(0,0,0,0.64); + } +} + +.cname-instructions { + list-style: none; + padding-left: 0; +} + +.cname-hint { + margin-top: 2px; +} + +.cname-hint_dimmed { + color: rgba(0,0,0,0.54); +} + +@media (prefers-color-scheme: dark) { + .cname-hint_dimmed { + color: rgba(255,255,255,0.7); + } +} + +.cname-value { + position: relative; + padding: 0 4px; +} + +.cname-value::after { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: var(--color-accentedPalette-200); + border-radius: 4px; + height: 100%; + opacity: 0.5; + width: 100%; + z-index: -1; +} + +@media (prefers-color-scheme: dark) { + .cname-value::after { + background-color: var(--color-accentedPalette-700); + } +} diff --git a/frontend/src/app/components/branding/branding.component.html b/frontend/src/app/components/branding/branding.component.html new file mode 100644 index 000000000..54243b1be --- /dev/null +++ b/frontend/src/app/components/branding/branding.component.html @@ -0,0 +1,230 @@ +
+ + +
+ + + + +
+

Branding

+

Customize your admin panel appearance with your brand identity.

+ +
+
+
+
+
+ + Custom domain + + + To change the domain, visit app.rocketadmin.com/company + + + Make your admin panel available at your domain. + + +
+ + + + Open + +
+
+ +
+
+
+ + +
+ + +
+ +
+
+ + +
+ + +
+
+ + +
+ + Browser tab title + + + Set the title that appears on the browser tab. + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Configure DNS Records

+

+ These instructions will guide you in setting up domain to have your admin panel on it. + After setup, the domain will respond on browser requests to Rocketadmin servers. +

+
    +
  1. + Step 1: Go to your domain registrar, sign in, and locate the domain DNS Manage section. +
  2. +
  3. + Step 2: Create a new type CNAME record with a name + {{companyCustomDomainThirdLevel}} + of the subdomain you want to use + and value cname.rocketadmin.com. Save changes. +
  4. +
+
+

Done! Now wait for the server's updates; it might take 1-2 hours.

+
+
+
+
+
diff --git a/frontend/src/app/components/branding/branding.component.ts b/frontend/src/app/components/branding/branding.component.ts new file mode 100644 index 000000000..1e7f308d8 --- /dev/null +++ b/frontend/src/app/components/branding/branding.component.ts @@ -0,0 +1,270 @@ +import { Component, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +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 { MatInputModule } from '@angular/material/input'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { NgIf } from '@angular/common'; +import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; +import { Subscription } from 'rxjs'; +import { Title } from '@angular/platform-browser'; + +import { AlertComponent } from '../ui-components/alert/alert.component'; +import { CompanyService } from 'src/app/services/company.service'; +import { UserService } from 'src/app/services/user.service'; +import { Company } from 'src/app/models/company'; +import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; +import { DeleteDomainDialogComponent } from '../company/delete-domain-dialog/delete-domain-dialog.component'; +import { environment } from 'src/environments/environment'; +import { PlaceholderCompanyComponent } from '../skeletons/placeholder-company/placeholder-company.component'; + +@Component({ + selector: 'app-branding', + templateUrl: './branding.component.html', + styleUrls: ['./branding.component.css'], + imports: [ + NgIf, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatIconModule, + MatTooltipModule, + Angulartics2OnModule, + AlertComponent, + ProfileSidebarComponent, + PlaceholderCompanyComponent, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class BrandingComponent implements OnInit, OnDestroy { + public isSaas = (environment as any).saas; + public company: Company = null; + public currentPlan: string; + public currentUser: any; + + public companyCustomDomain: { + id: string, + companyId: string, + hostname: string, + } = { + id: null, + companyId: '', + hostname: '', + }; + + public companyCustomDomainHostname: string; + public companyCustomDomainPlaceholder: string; + public companyCustomDomainThirdLevel: string; + public submittingCustomDomain: boolean = false; + public isCustomDomain: boolean = false; + + public submittingLogo: boolean = false; + public submittingFavicon: boolean = false; + + public companyTabTitle: string; + public submittingTabTitle: boolean = false; + + get whiteLabelSettings(): {logo: string, favicon: string, tabTitle: string} { + return this._company.whiteLabelSettings || { logo: '', favicon: '', tabTitle: '' }; + } + + get isDemo() { + return this._user.isDemo; + } + + private subscriptions: Subscription[] = []; + + constructor( + private _company: CompanyService, + private _user: UserService, + private dialog: MatDialog, + private angulartics2: Angulartics2, + private title: Title + ) {} + + ngOnInit() { + this.isCustomDomain = this._company.isCustomDomain() && this.isSaas; + + const titleSub = this._company.getCurrentTabTitle().subscribe(tabTitle => { + this.companyTabTitle = tabTitle; + this.title.setTitle(`Branding | ${tabTitle || 'Rocketadmin'}`); + }); + this.subscriptions.push(titleSub); + + const userSub = this._user.cast.subscribe(user => { + this.currentUser = user; + }); + this.subscriptions.push(userSub); + + this._company.fetchCompany().subscribe(res => { + this.company = res; + this.setCompanyPlan(res.subscriptionLevel); + if (this.isCustomDomain) { + this.companyCustomDomainHostname = res.custom_domain; + } else { + this.getCompanyCustomDomain(res.id); + } + }); + + const castSub = this._company.cast.subscribe(arg => { + if (arg === 'domain') { + this.getCompanyCustomDomain(this.company.id); + } else if (arg === 'updated-white-label-settings') { + this._company.getWhiteLabelProperties(this.company.id).subscribe(); + } + }); + this.subscriptions.push(castSub); + } + + ngOnDestroy() { + this.subscriptions.forEach(sub => sub.unsubscribe()); + } + + getCompanyCustomDomain(companyId: string) { + this._company.getCustomDomain(companyId).subscribe(res => { + if (res.success) { + this.companyCustomDomain = res.domain_info; + this.companyCustomDomainHostname = res.domain_info.hostname; + this.companyCustomDomainThirdLevel = this.companyCustomDomainHostname.split('.')[0]; + } else { + this.companyCustomDomain = { + id: null, + companyId: companyId, + hostname: '' + }; + this.companyCustomDomainHostname = ''; + this.companyCustomDomainPlaceholder = `${this.company.name.toLowerCase().replace(/[^a-z0-9]/g, '')}.example.com`; + this.companyCustomDomainThirdLevel = this.companyCustomDomainPlaceholder.split('.')[0]; + } + }); + } + + setCompanyPlan(subscriptionLevel: string) { + if (subscriptionLevel) { + this.currentPlan = subscriptionLevel.slice(0, -5).toLowerCase(); + } else { + this.currentPlan = 'free'; + } + } + + handleChangeCompanyDomain() { + this.submittingCustomDomain = true; + if (this.companyCustomDomain.id) { + this._company.updateCustomDomain(this.company.id, this.companyCustomDomain.id).subscribe(() => { + this.submittingCustomDomain = false; + this.angulartics2.eventTrack.next({ + action: 'Company: domain is updated successfully', + }); + }); + } else { + this._company.createCustomDomain(this.company.id, this.companyCustomDomainHostname).subscribe(() => { + this.submittingCustomDomain = false; + this.angulartics2.eventTrack.next({ + action: 'Company: domain is created successfully', + }); + }); + } + } + + handleDeleteDomainDialogOpen() { + this.dialog.open(DeleteDomainDialogComponent, { + width: '25em', + data: { companyId: this.company.id, domain: this.companyCustomDomainHostname } + }); + } + + onCompanyLogoSelected(event: any) { + this.submittingLogo = true; + + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + let companyLogoFile: File | null = null; + + if (file) { + companyLogoFile = file; + } else { + companyLogoFile = null; + } + + this._company.uploadLogo(this.company.id, companyLogoFile).subscribe(_res => { + this.submittingLogo = false; + this.angulartics2.eventTrack.next({ + action: 'Company: logo is uploaded successfully', + }); + }, _err => { + this.submittingLogo = false; + }); + } + + removeLogo() { + this.submittingLogo = true; + this._company.removeLogo(this.company.id).subscribe(_res => { + this.submittingLogo = false; + this.angulartics2.eventTrack.next({ + action: 'Company: logo is removed successfully', + }); + }, _err => { + this.submittingLogo = false; + }); + } + + onFaviconSelected(event: any) { + this.submittingFavicon = true; + + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + let faviconFile: File | null = null; + + if (file) { + faviconFile = file; + } else { + faviconFile = null; + } + + this._company.uploadFavicon(this.company.id, faviconFile).subscribe(_res => { + this.submittingFavicon = false; + this.angulartics2.eventTrack.next({ + action: 'Company: favicon is uploaded successfully', + }); + }, _err => { + this.submittingFavicon = false; + }); + } + + removeFavicon() { + this.submittingFavicon = true; + this._company.removeFavicon(this.company.id).subscribe(_res => { + this.submittingFavicon = false; + this.angulartics2.eventTrack.next({ + action: 'Company: favicon is removed successfully', + }); + }, _err => { + this.submittingFavicon = false; + }); + } + + updateTabTitle() { + this.submittingTabTitle = true; + this._company.updateTabTitle(this.company.id, this.companyTabTitle).subscribe(() => { + this.submittingTabTitle = false; + this.angulartics2.eventTrack.next({ + action: 'Company: tab title is updated successfully', + }); + }, _err => { + this.submittingTabTitle = false; + }); + } + + deleteTabTitle() { + this.submittingTabTitle = true; + this._company.removeTabTitle(this.company.id).subscribe(() => { + this.submittingTabTitle = false; + this.angulartics2.eventTrack.next({ + action: 'Company: tab title is deleted successfully', + }); + }); + } +} diff --git a/frontend/src/app/components/company/company.component.css b/frontend/src/app/components/company/company.component.css index 7ab24b864..88bdccfd3 100644 --- a/frontend/src/app/components/company/company.component.css +++ b/frontend/src/app/components/company/company.component.css @@ -1,15 +1,29 @@ -:host app-alert:not(:empty) { - --alert-margin: 24px; +.profile-layout { + display: flex; + height: calc(100vh - 64px); +} + +.profile-main { + flex: 1; + overflow-y: auto; +} + +::ng-deep .profile-main > app-alert { + position: relative; + top: 0; + margin: 24px; } .companyPage { - margin: var(--top-margin) auto; - padding: 0 clamp(200px, 20vw, 300px); + margin: 3em auto; + padding: 0 clamp(40px, 5vw, 100px); + max-width: 1200px; } @media (width <= 600px) { .companyPage { - padding: 0 9vw; + padding: 0 16px; + margin: 1.5em auto; } } diff --git a/frontend/src/app/components/company/company.component.html b/frontend/src/app/components/company/company.component.html index 66375f66b..b9d84b6e8 100644 --- a/frontend/src/app/components/company/company.component.html +++ b/frontend/src/app/components/company/company.component.html @@ -1,8 +1,12 @@ - +
+ - +
+ -
+ + +

{{ company.name }} company settings

@@ -58,17 +62,6 @@

Members (max 3)

-
- - - Open - -
- - -
-
-
- - -
- - -
- -
-
- - -
- - -
-
- - -
- - Browser tab title - - - Set the title that appears on the browser tab. - - -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-

Configure DNS Records

-

- These instructions will guide you in setting up domain to have your admin panel on it. - After setup, the domain will respond on browser requests to Rocketadmin servers. -

-
    -
  1. - Step 1: Go to your domain registrar, sign in, and locate the domain DNS Manage section. -
  2. -
  3. - Step 2: Create a new type CNAME record with a name - {{companyCustomDomainThirdLevel}} - of the subdomain you want to use - and value cname.rocketadmin.com. Save changes. -
  4. -
-
-

Done! Now wait for the server's updates; it might take 1-2 hours.

-
-
- - -
- Your admin panel address: - {{companyCustomDomainHostname}} -
- Configure DNS Records

(change)="changeShowTestConnections($event.checked)"> Show demo admin panels +
+
- diff --git a/frontend/src/app/components/company/company.component.ts b/frontend/src/app/components/company/company.component.ts index 0a7cba3a3..2e851bdbe 100644 --- a/frontend/src/app/components/company/company.component.ts +++ b/frontend/src/app/components/company/company.component.ts @@ -29,6 +29,7 @@ import { Title } from '@angular/platform-browser'; import { UserService } from 'src/app/services/user.service'; import { environment } from 'src/environments/environment'; import { orderBy } from "lodash-es"; +import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; @Component({ selector: 'app-company', @@ -50,7 +51,8 @@ import { orderBy } from "lodash-es"; Angulartics2OnModule, AlertComponent, PlaceholderCompanyComponent, - PlaceholderTableDataComponent + PlaceholderTableDataComponent, + ProfileSidebarComponent ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.css b/frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.css new file mode 100644 index 000000000..a12df152e --- /dev/null +++ b/frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.css @@ -0,0 +1,112 @@ +.profile-sidebar { + width: 220px; + min-width: 220px; + height: 100%; + border-right: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: column; + background-color: #fafafa; + overflow: hidden; +} + +.profile-sidebar_initialized { + transition: width 0.2s ease-in-out, min-width 0.2s ease-in-out; +} + +@media (prefers-color-scheme: dark) { + .profile-sidebar { + background-color: #1e1e1e; + border-right-color: rgba(255, 255, 255, 0.12); + } +} + +.profile-sidebar_collapsed { + width: 56px; + min-width: 56px; +} + +.sidebar-toggle-button { + margin: 12px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; +} + +.sidebar-toggle-button_collapsed { + justify-content: center; + margin: 12px 8px; +} + +.sidebar-nav { + padding-top: 0; + overflow: hidden; +} + +.sidebar-nav-item { + margin: 4px 8px; + border-radius: 8px; + overflow: hidden; +} + +.sidebar-nav-item ::ng-deep .mdc-list-item__primary-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sidebar-nav-item { + --mdc-list-list-item-leading-icon-end-space: 8px; +} + +.sidebar-nav-item mat-icon { + font-size: 20px; + width: 20px; + height: 20px; +} + +.sidebar-nav-item ::ng-deep .mdc-list-item__start { + margin-right: 8px; +} + +.sidebar-nav-item_collapsed { + margin: 4px; +} + +.sidebar-nav-item_collapsed ::ng-deep .mdc-list-item__content { + padding: 0; + display: flex; + justify-content: center; +} + +.active-link { + background-color: rgba(103, 58, 183, 0.12); +} + +@media (prefers-color-scheme: dark) { + .active-link { + background-color: rgba(179, 136, 255, 0.16); + } +} + +.active-link mat-icon { + color: #673ab7; +} + +@media (prefers-color-scheme: dark) { + .active-link mat-icon { + color: #b388ff; + } +} + +/* Mobile styles */ +@media (width <= 768px) { + .profile-sidebar { + width: 56px; + min-width: 56px; + } + + .sidebar-toggle-button:not(.sidebar-toggle-button_collapsed) { + display: none; + } +} diff --git a/frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.html b/frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.html new file mode 100644 index 000000000..c117fdf29 --- /dev/null +++ b/frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.html @@ -0,0 +1,104 @@ + diff --git a/frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.ts b/frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.ts new file mode 100644 index 000000000..dd4ae3b21 --- /dev/null +++ b/frontend/src/app/components/profile/profile-sidebar/profile-sidebar.component.ts @@ -0,0 +1,54 @@ +import { AfterViewInit, Component, inject, input, OnInit, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { MatListModule } from '@angular/material/list'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { UiSettingsService } from '../../../services/ui-settings.service'; +import { environment } from 'src/environments/environment'; + +@Component({ + selector: 'app-profile-sidebar', + templateUrl: './profile-sidebar.component.html', + styleUrls: ['./profile-sidebar.component.css'], + imports: [ + CommonModule, + RouterModule, + MatListModule, + MatIconModule, + MatButtonModule, + MatTooltipModule, + ], +}) +export class ProfileSidebarComponent implements OnInit, AfterViewInit { + activeTab = input<'account' | 'company' | 'pricing' | 'branding' | 'saml' | 'api' | 'secrets' | 'zapier'>('account'); + + collapsed = signal(false); + initialized = signal(false); + isSaas = environment.saas; + + private _uiSettings = inject(UiSettingsService); + + ngOnInit(): void { + this._loadCollapsedState(); + } + + ngAfterViewInit(): void { + setTimeout(() => this.initialized.set(true), 0); + } + + toggleCollapsed(): void { + this.collapsed.update((v) => { + const newValue = !v; + this._uiSettings.updateGlobalSetting('profileSidebarCollapsed', newValue); + return newValue; + }); + } + + private _loadCollapsedState(): void { + this._uiSettings.getUiSettings().subscribe((settings) => { + this.collapsed.set(settings?.globalSettings?.profileSidebarCollapsed ?? false); + }); + } +} diff --git a/frontend/src/app/components/secrets/secrets.component.css b/frontend/src/app/components/secrets/secrets.component.css index fe5ed3b60..613877347 100644 --- a/frontend/src/app/components/secrets/secrets.component.css +++ b/frontend/src/app/components/secrets/secrets.component.css @@ -1,15 +1,29 @@ -:host app-alert:not(:empty) { - --alert-margin: 24px; +.profile-layout { + display: flex; + height: calc(100vh - 64px); +} + +.profile-main { + flex: 1; + overflow-y: auto; +} + +::ng-deep .profile-main > app-alert { + position: relative; + top: 0; + margin: 24px; } .secrets-page { - margin: var(--top-margin) auto; - padding: 0 clamp(200px, 20vw, 300px); + margin: 3em auto; + padding: 0 clamp(40px, 5vw, 100px); + max-width: 1200px; } @media (width <= 600px) { .secrets-page { - padding: 0 9vw; + padding: 0 16px; + margin: 1.5em auto; } } diff --git a/frontend/src/app/components/secrets/secrets.component.html b/frontend/src/app/components/secrets/secrets.component.html index 4dff1326e..8177a1dcb 100644 --- a/frontend/src/app/components/secrets/secrets.component.html +++ b/frontend/src/app/components/secrets/secrets.component.html @@ -1,6 +1,10 @@ - +
+ -
+
+ + +

Company Secrets

@@ -148,4 +152,6 @@

No secrets found

(page)="onPageChange($event)" data-testid="secrets-paginator"> +
+
diff --git a/frontend/src/app/components/secrets/secrets.component.ts b/frontend/src/app/components/secrets/secrets.component.ts index 9f0ef6c7c..f1bad68e9 100644 --- a/frontend/src/app/components/secrets/secrets.component.ts +++ b/frontend/src/app/components/secrets/secrets.component.ts @@ -26,6 +26,7 @@ import { DeleteSecretDialogComponent } from './delete-secret-dialog/delete-secre import { AuditLogDialogComponent } from './audit-log-dialog/audit-log-dialog.component'; import { PlaceholderTableDataComponent } from '../skeletons/placeholder-table-data/placeholder-table-data.component'; import { AlertComponent } from '../ui-components/alert/alert.component'; +import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; @Component({ selector: 'app-secrets', @@ -46,6 +47,7 @@ import { AlertComponent } from '../ui-components/alert/alert.component'; MatDividerModule, PlaceholderTableDataComponent, AlertComponent, + ProfileSidebarComponent, ] }) export class SecretsComponent implements OnInit, OnDestroy { diff --git a/frontend/src/app/components/sso/sso.component.css b/frontend/src/app/components/sso/sso.component.css index 71d362f55..a0281eb6a 100644 --- a/frontend/src/app/components/sso/sso.component.css +++ b/frontend/src/app/components/sso/sso.component.css @@ -1,50 +1,63 @@ -.samlConfigForm { +.profile-layout { display: flex; - flex-direction: column; - margin: 3em auto; - max-width: 520px; - min-width: 300px; - width: 100%; + height: calc(100vh - 64px); } -.samlConfigForm__checkbox { - margin-top: -8px; - margin-bottom: 20px; +.profile-main { + flex: 1; + overflow-y: auto; } -.ssoPageHeader { - margin-bottom: 24px !important; +::ng-deep .profile-main > app-alert { + position: relative; + top: 0; + margin: 24px; } -.actions { - position: fixed; - left: 0; - bottom: 0; - display: flex; - align-items: center; - /* justify-content: space-between; */ - background-color: var(--mat-sidenav-content-background-color); - box-shadow: var(--shadow); - height: 64px; - padding: 0 max(calc(50vw - 260px), 2%); - width: 100vw; +.sso-page { + margin: 3em auto; + padding: 0 clamp(40px, 5vw, 100px); + max-width: 800px; } -@media (prefers-color-scheme: dark) { - .actions { - --shadow: 0 3px 1px -2px rgba(0,0,0,.5),0 2px 2px 0 rgba(0,0,0,.64),0 1px 5px 0 rgba(0,0,0,0.85); +@media (width <= 600px) { + .sso-page { + padding: 0 16px; + margin: 1.5em auto; } } -@media (prefers-color-scheme: light) { - .actions { - --shadow: 0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12); +.sso-description { + color: rgba(0, 0, 0, 0.64); + margin-top: 8px; + margin-bottom: 32px; +} + +@media (prefers-color-scheme: dark) { + .sso-description { + color: rgba(255, 255, 255, 0.7); } } -.actions-box { +.saml-config-form { display: flex; - align-items: center; - justify-content: space-between; - width: 100%; -} \ No newline at end of file + flex-direction: column; +} + +.saml-config-form__checkboxes { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 24px; +} + +.saml-config-form__checkbox { + margin: 0; +} + +.saml-config-form__actions { + display: flex; + justify-content: flex-end; + margin-top: 16px; + padding-bottom: 32px; +} diff --git a/frontend/src/app/components/sso/sso.component.html b/frontend/src/app/components/sso/sso.component.html index a6a8851ac..ec60e6d96 100644 --- a/frontend/src/app/components/sso/sso.component.html +++ b/frontend/src/app/components/sso/sso.component.html @@ -1,146 +1,159 @@ -
-

- SAML SSO Configuration -

- - Config name - - - - SSO identifier - - - - Entry point - - - - Issuer - - - - - - - Callback URL - - - - Certificate - - Certificate should not be empty - - - - Signature algorithm - - - - Digest algorithm - - - - - Active - - - - Assertions signed validation - - - - Authn response signed validation - - - - Allowed domains - - - - Display name - - - - Logo URL - - - - Expected issuer - - - -
-
- - Back - - - +
+ + +
+ + + + +
+

SAML SSO Configuration

+

Configure SAML Single Sign-On for your organization.

+ + + + Config name + + + + + SSO identifier + + + + + Entry point + + + + + Issuer + + + + + Callback URL + + + + + Certificate + + Certificate should not be empty + + + + Signature algorithm + + + + + Digest algorithm + + + +
+ + Active + + + + Assertions signed validation + + + + Authn response signed validation + +
+ + + Allowed domains + + + + + Display name + + + + + Logo URL + + + + + Expected issuer + + + +
+ +
+
+
- diff --git a/frontend/src/app/components/sso/sso.component.ts b/frontend/src/app/components/sso/sso.component.ts index 8853a4fba..c453e7b00 100644 --- a/frontend/src/app/components/sso/sso.component.ts +++ b/frontend/src/app/components/sso/sso.component.ts @@ -7,9 +7,13 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { Router, RouterModule } from '@angular/router'; -import { SamlConfig } from 'src/app/models/company'; +import { RouterModule } from '@angular/router'; +import { Title } from '@angular/platform-browser'; +import { SamlConfig, Company } from 'src/app/models/company'; import { CompanyService } from 'src/app/services/company.service'; +import { AlertComponent } from '../ui-components/alert/alert.component'; +import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; +import { PlaceholderCompanyComponent } from '../skeletons/placeholder-company/placeholder-company.component'; @Component({ selector: 'app-sso', @@ -23,12 +27,15 @@ import { CompanyService } from 'src/app/services/company.service'; FormsModule, RouterModule, MatFormFieldModule, + AlertComponent, + ProfileSidebarComponent, + PlaceholderCompanyComponent, ], templateUrl: './sso.component.html', styleUrl: './sso.component.css', }) export class SsoComponent implements OnInit { - public companyId: string; + public company: Company = null; public samlConfigInitial: SamlConfig = { name: '', @@ -50,29 +57,33 @@ export class SsoComponent implements OnInit { public samlConfig: SamlConfig = this.samlConfigInitial; public submitting: boolean = false; + public saved: boolean = false; constructor( - private router: Router, private _company: CompanyService, + private title: Title, ) {} ngOnInit() { - this.companyId = this.router.routerState.snapshot.root.firstChild.params['company-id']; + this._company.getCurrentTabTitle().subscribe(tabTitle => { + this.title.setTitle(`SAML SSO | ${tabTitle || 'Rocketadmin'}`); + }); - this._company.fetchSamlConfiguration(this.companyId).subscribe((config) => { - if (config.length) this.samlConfig = config[0]; + this._company.fetchCompany().subscribe(res => { + this.company = res; + this._company.fetchSamlConfiguration(res.id).subscribe((config) => { + if (config.length) this.samlConfig = config[0]; + }); }); } createSamlConfiguration() { this.submitting = true; - this._company.createSamlConfiguration(this.companyId, this.samlConfig).subscribe( - () => { - this.submitting = false; - this.router.navigate(['/company']); - }, + this._company.createSamlConfiguration(this.company.id, this.samlConfig).subscribe( () => { this.submitting = false; + this.saved = true; + setTimeout(() => this.saved = false, 3000); }, () => { this.submitting = false; @@ -85,10 +96,8 @@ export class SsoComponent implements OnInit { this._company.updateSamlConfiguration(this.samlConfig).subscribe( () => { this.submitting = false; - this.router.navigate(['/company']); - }, - () => { - this.submitting = false; + this.saved = true; + setTimeout(() => this.saved = false, 3000); }, () => { this.submitting = false; diff --git a/frontend/src/app/components/upgrade/upgrade.component.css b/frontend/src/app/components/upgrade/upgrade.component.css index f03b677b5..1b0a692a8 100644 --- a/frontend/src/app/components/upgrade/upgrade.component.css +++ b/frontend/src/app/components/upgrade/upgrade.component.css @@ -1,5 +1,17 @@ -:host app-alert:not(:empty) { - --alert-margin: 24px; +.profile-layout { + display: flex; + height: calc(100vh - 64px); +} + +.profile-main { + flex: 1; + overflow-y: auto; +} + +::ng-deep .profile-main > app-alert { + position: relative; + top: 0; + margin: 24px; } /* .upgrade-box { @@ -12,7 +24,7 @@ } */ .plans { - margin: var(--top-margin) auto; + margin: 24px auto; width: clamp(300px, 90%, 840px); } diff --git a/frontend/src/app/components/upgrade/upgrade.component.html b/frontend/src/app/components/upgrade/upgrade.component.html index eddd2e6e8..6b55fd8ba 100644 --- a/frontend/src/app/components/upgrade/upgrade.component.html +++ b/frontend/src/app/components/upgrade/upgrade.component.html @@ -1,146 +1,130 @@ - +
+ - +
+ -
-
-
- -
-

- Manage plans -

-
- -
-
- {{plan.name}} - Recommended -
-
- ${{ plan.price }}/monthly +
+
+
+ +
+

+ Manage plans +

- - per each 10 users - up to 3 users - - - - - + +
+
+ {{plan.name}} + Recommended +
+
+ ${{ plan.price }}/monthly +
+ + per each 10 users + up to 3 users + + + + + - - Upgrade - - - Downgrade - + + Upgrade + + + Downgrade + + +
-
- - - + -
-

Databases

+
- - - - - - - - +
+ + + + + + - - - - - -
- {{element[plan.key]}} - all_inclusive - {{element[plan.key]}}person -
+ + + {{element[plan.key]}} + all_inclusive + {{element[plan.key]}}person + + + + -

Users

+

Users

- - - - - - - - - - - -
- - {{element[plan.key]}} -
+ + + + + + + + + + + +
+ + {{element[plan.key]}} +
-

Features

+

Features

- - - - - - - - - - - -
- - {{element[plan.key]}} -
-
\ No newline at end of file + + + + + + + + + + + +
+ + {{element[plan.key]}} +
+
+
+
diff --git a/frontend/src/app/components/upgrade/upgrade.component.ts b/frontend/src/app/components/upgrade/upgrade.component.ts index 981cd27d8..a0185a946 100644 --- a/frontend/src/app/components/upgrade/upgrade.component.ts +++ b/frontend/src/app/components/upgrade/upgrade.component.ts @@ -9,6 +9,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatTableModule } from '@angular/material/table'; import { PaymentService } from 'src/app/services/payment.service'; import { PlanKey } from 'src/app/models/plans'; +import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; import { RouterModule } from '@angular/router'; import plans from '../../consts/plans'; @@ -22,7 +23,8 @@ import plans from '../../consts/plans'; MatTableModule, MatChipsModule, RouterModule, - AlertComponent + AlertComponent, + ProfileSidebarComponent ], templateUrl: './upgrade.component.html', styleUrls: ['./upgrade.component.css'] diff --git a/frontend/src/app/components/user-settings/user-settings.component.css b/frontend/src/app/components/user-settings/user-settings.component.css index 7706a72f6..61bbfdc59 100644 --- a/frontend/src/app/components/user-settings/user-settings.component.css +++ b/frontend/src/app/components/user-settings/user-settings.component.css @@ -1,20 +1,33 @@ -:host app-alert:not(:empty) { - --alert-margin: 24px; +.profile-layout { + display: flex; + height: calc(100vh - 64px); +} + +.profile-main { + flex: 1; + overflow-y: auto; +} + +::ng-deep .profile-main > app-alert { + position: relative; + top: 0; + margin: 24px; } .page { display: flex; flex-direction: column; - margin: var(--top-margin) auto; + margin: 3em auto; max-width: 520px; min-width: 300px; width: 96%; + padding: 0 24px; } @media (width <= 600px) { .page { - padding: 0 9vw; - width: 100vw; + padding: 0 16px; + margin: 1.5em auto; } } diff --git a/frontend/src/app/components/user-settings/user-settings.component.html b/frontend/src/app/components/user-settings/user-settings.component.html index 443a31ab2..5959b9cef 100644 --- a/frontend/src/app/components/user-settings/user-settings.component.html +++ b/frontend/src/app/components/user-settings/user-settings.component.html @@ -1,163 +1,111 @@ - - - +
+ -
-

- Account settings -

+
+ + + - diff --git a/frontend/src/app/components/user-settings/user-settings.component.ts b/frontend/src/app/components/user-settings/user-settings.component.ts index 957e728df..98866eb37 100644 --- a/frontend/src/app/components/user-settings/user-settings.component.ts +++ b/frontend/src/app/components/user-settings/user-settings.component.ts @@ -1,14 +1,12 @@ import { Alert, AlertActionType, AlertType } from 'src/app/models/alert'; import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; -import { ApiKey, User } from 'src/app/models/user'; +import { User } from 'src/app/models/user'; import { Component, OnInit } from '@angular/core'; import { AccountDeleteDialogComponent } from './account-delete-dialog/account-delete-dialog.component'; import { AlertComponent } from '../ui-components/alert/alert.component'; import { Angulartics2Module } from 'angulartics2'; -import { ApiKeyDeleteDialogComponent } from './api-key-delete-dialog/api-key-delete-dialog.component'; import { AuthService } from 'src/app/services/auth.service'; -import { CdkCopyToClipboard } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; import { CompanyService } from 'src/app/services/company.service'; import { EnableTwoFADialogComponent } from './enable-two-fa-dialog/enable-two-fa-dialog.component'; @@ -19,13 +17,12 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; -import { MatListModule } from '@angular/material/list'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { NotificationsService } from 'src/app/services/notifications.service'; import { RouterModule } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { UserService } from 'src/app/services/user.service'; +import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; @Component({ selector: 'app-user-settings', @@ -40,13 +37,12 @@ import { UserService } from 'src/app/services/user.service'; MatFormFieldModule, MatIconModule, MatInputModule, - MatListModule, MatSlideToggleModule, MatTooltipModule, - CdkCopyToClipboard, Angulartics2Module, AlertComponent, - Angulartics2OnModule + Angulartics2OnModule, + ProfileSidebarComponent ] }) export class UserSettingsComponent implements OnInit { @@ -77,10 +73,6 @@ export class UserSettingsComponent implements OnInit { public is2FAswitchingOffSettingsShown: boolean = false; public is2FAEnabledToggle: boolean; - public apiKeys: []; - public generatingAPIkeyTitle: string; - public generatedAPIkeyHash: string; - public isDemoAccountWarning: Alert = { id: 10000000, type: AlertType.Warning, @@ -90,7 +82,6 @@ export class UserSettingsComponent implements OnInit { constructor( private _userService: UserService, private _authService: AuthService, - private _notifications: NotificationsService, private _company: CompanyService, public dialog: MatDialog, private title: Title, @@ -106,12 +97,11 @@ export class UserSettingsComponent implements OnInit { this.currentUser = null; this._userService.cast .subscribe(user => { - this.currentUser =user; + this.currentUser = user; this.userName = user.name; this.is2FAEnabledToggle = user.is_2fa_enabled; this.showTestConnections = user.show_test_connections; }); - this.getAPIkeys(); } requestEmailVerification() { @@ -201,32 +191,5 @@ export class UserSettingsComponent implements OnInit { action: 'Company: show test connections is updated successfully', }); }); - - } - - getAPIkeys() { - this._userService.getAPIkeys().subscribe(res => this.apiKeys = res); - } - - generateAPIkey() { - this._userService.generateAPIkey(this.generatingAPIkeyTitle).subscribe(res => { - this.generatedAPIkeyHash = res.hash; - this.getAPIkeys(); - }); - } - - deleteAPIkey(apiKey: ApiKey) { - const deleteConfirmation = this.dialog.open(ApiKeyDeleteDialogComponent, { - width: '25em', - data: apiKey - }); - - deleteConfirmation.afterClosed().subscribe( action => { - if (action === 'delete') this.getAPIkeys(); - }); - } - - showCopyNotification(message: string) { - this._notifications.showSuccessSnackbar(message); } } diff --git a/frontend/src/app/components/zapier/zapier.component.css b/frontend/src/app/components/zapier/zapier.component.css index ab2ee1012..f31c025de 100644 --- a/frontend/src/app/components/zapier/zapier.component.css +++ b/frontend/src/app/components/zapier/zapier.component.css @@ -1,3 +1,19 @@ +.profile-layout { + display: flex; + height: calc(100vh - 64px); +} + +.profile-main { + flex: 1; + overflow-y: auto; +} + +::ng-deep .profile-main > app-alert { + position: relative; + top: 0; + margin: 24px; +} + .zapier-page { display: flex; flex-direction: column; @@ -7,4 +23,4 @@ .zapier-docs-button { align-self: flex-end; margin-bottom: -36px; -} \ No newline at end of file +} diff --git a/frontend/src/app/components/zapier/zapier.component.html b/frontend/src/app/components/zapier/zapier.component.html index 54754b120..9554a21b0 100644 --- a/frontend/src/app/components/zapier/zapier.component.html +++ b/frontend/src/app/components/zapier/zapier.component.html @@ -1,19 +1,27 @@ -
- - open_in_new - Docs - - @if (currentUser()) { - - } +
+ + +
+ + +
+ + open_in_new + Docs + + @if (currentUser()) { + + } +
+
diff --git a/frontend/src/app/components/zapier/zapier.component.ts b/frontend/src/app/components/zapier/zapier.component.ts index 074639c91..d7b6bbdad 100644 --- a/frontend/src/app/components/zapier/zapier.component.ts +++ b/frontend/src/app/components/zapier/zapier.component.ts @@ -4,10 +4,12 @@ import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { UserService } from 'src/app/services/user.service'; +import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; +import { AlertComponent } from '../ui-components/alert/alert.component'; @Component({ selector: 'app-zapier', - imports: [MatIconModule, MatButtonModule], + imports: [MatIconModule, MatButtonModule, ProfileSidebarComponent, AlertComponent], templateUrl: './zapier.component.html', styleUrl: './zapier.component.css', schemas: [CUSTOM_ELEMENTS_SCHEMA], diff --git a/frontend/src/app/models/ui-settings.ts b/frontend/src/app/models/ui-settings.ts index a4beffcb9..40a36187e 100644 --- a/frontend/src/app/models/ui-settings.ts +++ b/frontend/src/app/models/ui-settings.ts @@ -1,6 +1,7 @@ export interface GlobalSettingsUI { connectionsListCollapsed: boolean; lastFeatureNotificationId: string; + profileSidebarCollapsed?: boolean; } export interface TableSettingsUI { From 2f5536d361e298e0b0626595713cc2ebe1308477 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 10 Feb 2026 12:10:22 +0000 Subject: [PATCH 10/16] branding page: fix top margain, fix tab title input visibility, add skeleton loader --- .../branding/branding.component.css | 2 +- .../branding/branding.component.html | 5 +- .../components/branding/branding.component.ts | 4 +- .../placeholder-branding.component.css | 92 +++++++++++++++++++ .../placeholder-branding.component.html | 31 +++++++ .../placeholder-branding.component.ts | 10 ++ frontend/src/environments/environment.dev.ts | 2 +- 7 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.css create mode 100644 frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.html create mode 100644 frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.ts diff --git a/frontend/src/app/components/branding/branding.component.css b/frontend/src/app/components/branding/branding.component.css index ffb923ac0..ae156a034 100644 --- a/frontend/src/app/components/branding/branding.component.css +++ b/frontend/src/app/components/branding/branding.component.css @@ -15,7 +15,7 @@ } .branding-page { - margin: 3em auto; + margin: var(--top-margin) auto; padding: 0 clamp(40px, 5vw, 100px); max-width: 1200px; } diff --git a/frontend/src/app/components/branding/branding.component.html b/frontend/src/app/components/branding/branding.component.html index 54243b1be..b6fdf3181 100644 --- a/frontend/src/app/components/branding/branding.component.html +++ b/frontend/src/app/components/branding/branding.component.html @@ -4,7 +4,7 @@
- +

Branding

@@ -87,8 +87,7 @@

Branding

-
Browser tab title diff --git a/frontend/src/app/components/branding/branding.component.ts b/frontend/src/app/components/branding/branding.component.ts index 1e7f308d8..7c095ccb6 100644 --- a/frontend/src/app/components/branding/branding.component.ts +++ b/frontend/src/app/components/branding/branding.component.ts @@ -18,7 +18,7 @@ import { Company } from 'src/app/models/company'; import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; import { DeleteDomainDialogComponent } from '../company/delete-domain-dialog/delete-domain-dialog.component'; import { environment } from 'src/environments/environment'; -import { PlaceholderCompanyComponent } from '../skeletons/placeholder-company/placeholder-company.component'; +import { PlaceholderBrandingComponent } from '../skeletons/placeholder-branding/placeholder-branding.component'; @Component({ selector: 'app-branding', @@ -35,7 +35,7 @@ import { PlaceholderCompanyComponent } from '../skeletons/placeholder-company/pl Angulartics2OnModule, AlertComponent, ProfileSidebarComponent, - PlaceholderCompanyComponent, + PlaceholderBrandingComponent, ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.css b/frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.css new file mode 100644 index 000000000..9ed138e97 --- /dev/null +++ b/frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.css @@ -0,0 +1,92 @@ +.wrapper { + margin: var(--top-margin) auto; + padding: 0 clamp(40px, 5vw, 100px); + max-width: 1200px; +} + +@media (width <= 600px) { + .wrapper { + padding: 0 16px; + margin: 1.5em auto; + } +} + +.title { + height: 32px; + margin-bottom: 8px; + width: 120px; +} + +.description { + height: 20px; + margin-bottom: 32px; + width: 420px; + max-width: 100%; +} + +.white-label { + display: grid; + grid-template-columns: 1fr 1fr; + grid-column-gap: 32px; +} + +@media (width <= 900px) { + .white-label { + grid-template-columns: 1fr; + grid-row-gap: 32px; + } +} + +.form__field { + height: 56px; + width: 100%; + border-radius: 4px; + margin-bottom: 16px; +} + +.upload-buttons { + display: flex; + gap: 24px; + margin-top: 16px; + margin-bottom: 24px; +} + +.upload-button { + height: 36px; + width: 140px; + border-radius: 4px; +} + +.preview { + height: 240px; + width: 100%; + border-radius: 4px; + margin-top: 8px; +} + +.white-label-how-to { + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 4px; + padding: 20px; + height: fit-content; +} + +.how-to__title { + height: 24px; + width: 200px; + margin-bottom: 16px; +} + +.how-to__line { + height: 16px; + width: 100%; + margin-bottom: 12px; +} + +.how-to__line_short { + width: 60%; +} + +.how-to__line_medium { + width: 80%; +} diff --git a/frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.html b/frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.html new file mode 100644 index 000000000..c63fb67c1 --- /dev/null +++ b/frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.html @@ -0,0 +1,31 @@ +
+
+
+
+ +
+
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.ts b/frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.ts new file mode 100644 index 000000000..a6fa47880 --- /dev/null +++ b/frontend/src/app/components/skeletons/placeholder-branding/placeholder-branding.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-placeholder-branding', + templateUrl: './placeholder-branding.component.html', + styleUrls: ['./placeholder-branding.component.css'] +}) +export class PlaceholderBrandingComponent { + +} diff --git a/frontend/src/environments/environment.dev.ts b/frontend/src/environments/environment.dev.ts index 9490b3494..24d2881ea 100644 --- a/frontend/src/environments/environment.dev.ts +++ b/frontend/src/environments/environment.dev.ts @@ -1,6 +1,6 @@ export const environment = { production: false, - saas: false, + saas: true, apiRoot: 'https://app.rocketadmin.com/api', saasURL: 'https://app.rocketadmin.com', saasHostnames: ['localhost', '127.0.0.1'], From ec7fa709cf1a2ab813a17b02f02d68f26976c779 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 10 Feb 2026 15:14:56 +0000 Subject: [PATCH 11/16] company page: fix alignment and add skeleton loaders --- .../api-keys/api-keys.component.css | 2 +- .../api-keys/api-keys.component.html | 2 + .../components/api-keys/api-keys.component.ts | 4 +- .../components/company/company.component.css | 2 +- .../components/secrets/secrets.component.css | 2 +- .../placeholder-api-keys-list.component.css | 33 +++++++++ .../placeholder-api-keys-list.component.html | 9 +++ .../placeholder-api-keys-list.component.ts | 12 ++++ .../placeholder-company.component.css | 3 +- .../placeholder-sso.component.css | 70 +++++++++++++++++++ .../placeholder-sso.component.html | 32 +++++++++ .../placeholder-sso.component.ts | 10 +++ .../src/app/components/sso/sso.component.css | 2 +- .../src/app/components/sso/sso.component.html | 2 +- .../src/app/components/sso/sso.component.ts | 4 +- .../components/upgrade/upgrade.component.css | 2 +- .../user-settings/user-settings.component.css | 2 +- .../components/zapier/zapier.component.css | 4 +- 18 files changed, 185 insertions(+), 12 deletions(-) create mode 100644 frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.css create mode 100644 frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.html create mode 100644 frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.ts create mode 100644 frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.css create mode 100644 frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.html create mode 100644 frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.ts diff --git a/frontend/src/app/components/api-keys/api-keys.component.css b/frontend/src/app/components/api-keys/api-keys.component.css index f83265228..bd83623bb 100644 --- a/frontend/src/app/components/api-keys/api-keys.component.css +++ b/frontend/src/app/components/api-keys/api-keys.component.css @@ -15,7 +15,7 @@ } .api-keys-page { - margin: 3em auto; + margin: var(--top-margin) auto; padding: 0 clamp(40px, 5vw, 100px); max-width: 800px; } diff --git a/frontend/src/app/components/api-keys/api-keys.component.html b/frontend/src/app/components/api-keys/api-keys.component.html index 038906493..e72126991 100644 --- a/frontend/src/app/components/api-keys/api-keys.component.html +++ b/frontend/src/app/components/api-keys/api-keys.component.html @@ -50,6 +50,8 @@

API Keys

Your API keys

+ +
vpn_key_off

You don't have any API keys yet.

diff --git a/frontend/src/app/components/api-keys/api-keys.component.ts b/frontend/src/app/components/api-keys/api-keys.component.ts index 5314f27da..5e3fa7a53 100644 --- a/frontend/src/app/components/api-keys/api-keys.component.ts +++ b/frontend/src/app/components/api-keys/api-keys.component.ts @@ -19,6 +19,7 @@ import { CompanyService } from 'src/app/services/company.service'; import { NotificationsService } from 'src/app/services/notifications.service'; import { ApiKey } from 'src/app/models/user'; import { ApiKeyDeleteDialogComponent } from '../user-settings/api-key-delete-dialog/api-key-delete-dialog.component'; +import { PlaceholderApiKeysListComponent } from '../skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component'; @Component({ selector: 'app-api-keys', @@ -37,10 +38,11 @@ import { ApiKeyDeleteDialogComponent } from '../user-settings/api-key-delete-dia Angulartics2OnModule, AlertComponent, ProfileSidebarComponent, + PlaceholderApiKeysListComponent, ] }) export class ApiKeysComponent implements OnInit { - public apiKeys: ApiKey[] = []; + public apiKeys: ApiKey[] = null; public generatingAPIkeyTitle: string = ''; public generatedAPIkeyHash: string = ''; public submitting: boolean = false; diff --git a/frontend/src/app/components/company/company.component.css b/frontend/src/app/components/company/company.component.css index 88bdccfd3..a51f4234f 100644 --- a/frontend/src/app/components/company/company.component.css +++ b/frontend/src/app/components/company/company.component.css @@ -15,7 +15,7 @@ } .companyPage { - margin: 3em auto; + margin: var(--top-margin) auto; padding: 0 clamp(40px, 5vw, 100px); max-width: 1200px; } diff --git a/frontend/src/app/components/secrets/secrets.component.css b/frontend/src/app/components/secrets/secrets.component.css index 613877347..10f6b7ecd 100644 --- a/frontend/src/app/components/secrets/secrets.component.css +++ b/frontend/src/app/components/secrets/secrets.component.css @@ -15,7 +15,7 @@ } .secrets-page { - margin: 3em auto; + margin: var(--top-margin) auto; padding: 0 clamp(40px, 5vw, 100px); max-width: 1200px; } diff --git a/frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.css b/frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.css new file mode 100644 index 000000000..178e796e2 --- /dev/null +++ b/frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.css @@ -0,0 +1,33 @@ +.list-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 16px; + border-radius: 8px; + margin-bottom: 0; + min-height: 40px; +} + +.list-item__info { + display: flex; + align-items: center; + gap: 12px; +} + +.list-item__icon { + height: 24px; + width: 24px; + border-radius: 4px; +} + +.list-item__title { + height: 20px; + width: 160px; + border-radius: 4px; +} + +.list-item__button { + height: 32px; + width: 32px; + border-radius: 50%; +} diff --git a/frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.html b/frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.html new file mode 100644 index 000000000..cae6b0e90 --- /dev/null +++ b/frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.html @@ -0,0 +1,9 @@ +
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.ts b/frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.ts new file mode 100644 index 000000000..879b5b3af --- /dev/null +++ b/frontend/src/app/components/skeletons/placeholder-api-keys-list/placeholder-api-keys-list.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { NgFor } from '@angular/common'; + +@Component({ + selector: 'app-placeholder-api-keys-list', + templateUrl: './placeholder-api-keys-list.component.html', + styleUrls: ['./placeholder-api-keys-list.component.css'], + imports: [NgFor] +}) +export class PlaceholderApiKeysListComponent { + +} diff --git a/frontend/src/app/components/skeletons/placeholder-company/placeholder-company.component.css b/frontend/src/app/components/skeletons/placeholder-company/placeholder-company.component.css index ab2225faa..ebcdc1107 100644 --- a/frontend/src/app/components/skeletons/placeholder-company/placeholder-company.component.css +++ b/frontend/src/app/components/skeletons/placeholder-company/placeholder-company.component.css @@ -1,6 +1,7 @@ .wrapper { margin: var(--top-margin) auto; - padding: 0 clamp(200px, 20vw, 300px); + padding: 0 clamp(40px, 5vw, 100px); + max-width: 1200px; width: 100%; } diff --git a/frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.css b/frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.css new file mode 100644 index 000000000..b97741ee3 --- /dev/null +++ b/frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.css @@ -0,0 +1,70 @@ +.wrapper { + margin: var(--top-margin) auto; + padding: 0 clamp(40px, 5vw, 100px); + max-width: 800px; +} + +@media (width <= 600px) { + .wrapper { + padding: 0 16px; + margin: 1.5em auto; + } +} + +.title { + height: 32px; + margin-bottom: 8px; + width: 280px; +} + +.description { + height: 20px; + margin-bottom: 32px; + width: 400px; + max-width: 100%; +} + +.form { + display: flex; + flex-direction: column; +} + +.form__field { + height: 56px; + width: 100%; + border-radius: 4px; + margin-bottom: 16px; +} + +.form__textarea { + height: 160px; + width: 100%; + border-radius: 4px; + margin-bottom: 16px; +} + +.form__checkboxes { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 24px; +} + +.form__checkbox { + height: 20px; + width: 240px; + border-radius: 2px; +} + +.form__actions { + display: flex; + justify-content: flex-end; + margin-top: 16px; + padding-bottom: 32px; +} + +.form__button { + height: 36px; + width: 80px; + border-radius: 4px; +} diff --git a/frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.html b/frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.html new file mode 100644 index 000000000..0556519e6 --- /dev/null +++ b/frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.html @@ -0,0 +1,32 @@ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
diff --git a/frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.ts b/frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.ts new file mode 100644 index 000000000..513363f8d --- /dev/null +++ b/frontend/src/app/components/skeletons/placeholder-sso/placeholder-sso.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-placeholder-sso', + templateUrl: './placeholder-sso.component.html', + styleUrls: ['./placeholder-sso.component.css'] +}) +export class PlaceholderSsoComponent { + +} diff --git a/frontend/src/app/components/sso/sso.component.css b/frontend/src/app/components/sso/sso.component.css index a0281eb6a..8ef305607 100644 --- a/frontend/src/app/components/sso/sso.component.css +++ b/frontend/src/app/components/sso/sso.component.css @@ -15,7 +15,7 @@ } .sso-page { - margin: 3em auto; + margin: var(--top-margin) auto; padding: 0 clamp(40px, 5vw, 100px); max-width: 800px; } diff --git a/frontend/src/app/components/sso/sso.component.html b/frontend/src/app/components/sso/sso.component.html index ec60e6d96..44f63c41c 100644 --- a/frontend/src/app/components/sso/sso.component.html +++ b/frontend/src/app/components/sso/sso.component.html @@ -4,7 +4,7 @@
- +

SAML SSO Configuration

diff --git a/frontend/src/app/components/sso/sso.component.ts b/frontend/src/app/components/sso/sso.component.ts index c453e7b00..8cf2f4925 100644 --- a/frontend/src/app/components/sso/sso.component.ts +++ b/frontend/src/app/components/sso/sso.component.ts @@ -13,7 +13,7 @@ import { SamlConfig, Company } from 'src/app/models/company'; import { CompanyService } from 'src/app/services/company.service'; import { AlertComponent } from '../ui-components/alert/alert.component'; import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; -import { PlaceholderCompanyComponent } from '../skeletons/placeholder-company/placeholder-company.component'; +import { PlaceholderSsoComponent } from '../skeletons/placeholder-sso/placeholder-sso.component'; @Component({ selector: 'app-sso', @@ -29,7 +29,7 @@ import { PlaceholderCompanyComponent } from '../skeletons/placeholder-company/pl MatFormFieldModule, AlertComponent, ProfileSidebarComponent, - PlaceholderCompanyComponent, + PlaceholderSsoComponent, ], templateUrl: './sso.component.html', styleUrl: './sso.component.css', diff --git a/frontend/src/app/components/upgrade/upgrade.component.css b/frontend/src/app/components/upgrade/upgrade.component.css index 1b0a692a8..93eb52c4a 100644 --- a/frontend/src/app/components/upgrade/upgrade.component.css +++ b/frontend/src/app/components/upgrade/upgrade.component.css @@ -24,7 +24,7 @@ } */ .plans { - margin: 24px auto; + margin: var(--top-margin) auto; width: clamp(300px, 90%, 840px); } diff --git a/frontend/src/app/components/user-settings/user-settings.component.css b/frontend/src/app/components/user-settings/user-settings.component.css index 61bbfdc59..4df8164f2 100644 --- a/frontend/src/app/components/user-settings/user-settings.component.css +++ b/frontend/src/app/components/user-settings/user-settings.component.css @@ -17,7 +17,7 @@ .page { display: flex; flex-direction: column; - margin: 3em auto; + margin: var(--top-margin) auto; max-width: 520px; min-width: 300px; width: 96%; diff --git a/frontend/src/app/components/zapier/zapier.component.css b/frontend/src/app/components/zapier/zapier.component.css index f31c025de..1078d7e79 100644 --- a/frontend/src/app/components/zapier/zapier.component.css +++ b/frontend/src/app/components/zapier/zapier.component.css @@ -17,7 +17,9 @@ .zapier-page { display: flex; flex-direction: column; - padding: 32px max(calc(50vw - 325px), 10%); + margin: var(--top-margin) auto; + padding: 0 clamp(40px, 5vw, 100px); + max-width: 1200px; } .zapier-docs-button { From c331e7d2e0d3c749b87c334ea732ac009e0f7cfb Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 10 Feb 2026 15:25:25 +0000 Subject: [PATCH 12/16] Secrets: fix top spacing on Add secret dialog and update s3 widget hint --- .../db-table-widgets.component.ts | 7 ++--- .../create-secret-dialog.component.html | 30 +++++++++---------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts index 7b8143c10..010edd9fe 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts @@ -270,12 +270,11 @@ export class DbTableWidgetsComponent implements OnInit { // prefix: Optional path prefix for uploaded files // region: AWS region (default: us-east-1) // type: "file" (default) - accepts all file types, "image" - accepts only images -// aws_access_key_id_secret_name: Slug of the secret containing AWS Access Key ID -// aws_secret_access_key_secret_name: Slug of the secret containing AWS Secret Access Key +// aws_access_key_id_secret_name: Unique identifier of the secret containing AWS Access Key ID +// aws_secret_access_key_secret_name: Unique identifier of the secret containing AWS Secret Access Key // // ⚠️ IMPORTANT: DO NOT INCLUDE AWS SECRETS DIRECTLY IN WIDGET SETTINGS! -// Store your AWS credentials as secrets in Settings -> Secrets first, -// then reference them by their slug names in the settings below. +// Store your AWS credentials as secrets in Rocketadmin and reference them here by their unique identifiers to ensure security and prevent exposure of sensitive information. { "bucket": "your-bucket-name", diff --git a/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.html b/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.html index 4f65be248..425b50c97 100644 --- a/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.html +++ b/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.html @@ -1,7 +1,7 @@ -

Create Secret

+

Create Secret

- - + + Unique Identifier Create Secret Remember this password - it cannot be recovered!
- - + - - - - + + + + + From 84ec1a9ff4f2a02e72ff7601ce91c75e7a9baa34 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 10 Feb 2026 15:59:45 +0000 Subject: [PATCH 13/16] fix unit tests --- .../secrets/secrets.component.spec.ts | 3 +- .../app/components/sso/sso.component.spec.ts | 72 +++++++------------ .../zapier/zapier.component.spec.ts | 3 +- 3 files changed, 28 insertions(+), 50 deletions(-) diff --git a/frontend/src/app/components/secrets/secrets.component.spec.ts b/frontend/src/app/components/secrets/secrets.component.spec.ts index e795ad5c6..84a24ea63 100644 --- a/frontend/src/app/components/secrets/secrets.component.spec.ts +++ b/frontend/src/app/components/secrets/secrets.component.spec.ts @@ -5,6 +5,7 @@ import { MatDialog } from '@angular/material/dialog'; import { PageEvent } from '@angular/material/paginator'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterModule } from '@angular/router'; import { Angulartics2Module } from 'angulartics2'; import { BehaviorSubject, of } from 'rxjs'; import { Secret } from 'src/app/models/secret'; @@ -55,7 +56,7 @@ describe('SecretsComponent', () => { } as any; await TestBed.configureTestingModule({ - imports: [SecretsComponent, BrowserAnimationsModule, MatSnackBarModule, Angulartics2Module.forRoot()], + imports: [SecretsComponent, BrowserAnimationsModule, MatSnackBarModule, Angulartics2Module.forRoot(), RouterModule.forRoot([])], providers: [ provideHttpClient(), provideHttpClientTesting(), diff --git a/frontend/src/app/components/sso/sso.component.spec.ts b/frontend/src/app/components/sso/sso.component.spec.ts index 4381e8765..3380fbb92 100644 --- a/frontend/src/app/components/sso/sso.component.spec.ts +++ b/frontend/src/app/components/sso/sso.component.spec.ts @@ -1,6 +1,6 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, convertToParamMap, Router, RouterModule } from '@angular/router'; +import { RouterModule } from '@angular/router'; import { of } from 'rxjs'; import { SamlConfig } from 'src/app/models/company'; import { CompanyService } from 'src/app/services/company.service'; @@ -10,14 +10,17 @@ describe('SsoComponent', () => { let component: SsoComponent; let fixture: ComponentFixture; let companyServiceSpy: any; - let mockRouter: any; - - const mockActivatedRoute = { - snapshot: { - paramMap: convertToParamMap({ - 'company-id': '123', - }), - }, + + const mockCompany = { + id: '123', + name: 'Test Company', + address: {}, + portal_link: '', + subscriptionLevel: 'FREE_PLAN', + connections: [], + invitations: [], + is_payment_method_added: false, + show_test_connections: false, }; const mockSamlConfig: SamlConfig = { @@ -39,36 +42,18 @@ describe('SsoComponent', () => { }; beforeEach(async () => { - mockRouter = { - routerState: { - snapshot: { - root: { - firstChild: { - params: { - 'company-id': '123', - }, - }, - }, - }, - }, - navigate: vi.fn(), - events: of(null), - url: '/company', - createUrlTree: vi.fn().mockReturnValue({}), - serializeUrl: vi.fn().mockReturnValue('company'), - }; - companyServiceSpy = { + fetchCompany: vi.fn().mockReturnValue(of(mockCompany)), fetchSamlConfiguration: vi.fn().mockReturnValue(of([])), createSamlConfiguration: vi.fn().mockReturnValue(of({})), updateSamlConfiguration: vi.fn().mockReturnValue(of({})), + getCurrentTabTitle: vi.fn().mockReturnValue(of('Rocketadmin')), }; await TestBed.configureTestingModule({ - imports: [SsoComponent, HttpClientTestingModule, RouterModule.forRoot([])], + imports: [SsoComponent, RouterModule.forRoot([])], providers: [ - { provide: ActivatedRoute, useValue: mockActivatedRoute }, - { provide: Router, useValue: mockRouter }, + provideHttpClient(), { provide: CompanyService, useValue: companyServiceSpy }, ], }).compileComponents(); @@ -82,8 +67,8 @@ describe('SsoComponent', () => { expect(component).toBeTruthy(); }); - it('should initialize company ID from router state', () => { - expect(component.companyId).toBe('123'); + it('should set company from fetchCompany', () => { + expect(component.company).toEqual(mockCompany); }); it('should fetch SAML configuration on init', () => { @@ -114,33 +99,24 @@ describe('SsoComponent', () => { expect(component.samlConfigInitial.assertionsSignedValidation).toBe(false); }); - it('should create SAML configuration and navigate to company', () => { + it('should create SAML configuration', () => { component.samlConfig = mockSamlConfig; component.createSamlConfiguration(); - expect(component.submitting).toBe(false); expect(companyServiceSpy.createSamlConfiguration).toHaveBeenCalledWith('123', mockSamlConfig); - expect(mockRouter.navigate).toHaveBeenCalledWith(['/company']); - }); - - it('should set submitting to true while creating configuration', () => { - companyServiceSpy.createSamlConfiguration.mockReturnValue(of({})); - - component.createSamlConfiguration(); - - // After subscription completes, submitting should be false expect(component.submitting).toBe(false); + expect(component.saved).toBe(true); }); - it('should update SAML configuration and navigate to company', () => { + it('should update SAML configuration', () => { component.samlConfig = mockSamlConfig; component.updateSamlConfiguration(); - expect(component.submitting).toBe(false); expect(companyServiceSpy.updateSamlConfiguration).toHaveBeenCalledWith(mockSamlConfig); - expect(mockRouter.navigate).toHaveBeenCalledWith(['/company']); + expect(component.submitting).toBe(false); + expect(component.saved).toBe(true); }); it('should initialize with submitting as false', () => { diff --git a/frontend/src/app/components/zapier/zapier.component.spec.ts b/frontend/src/app/components/zapier/zapier.component.spec.ts index 6b76d7b32..75e427ffd 100644 --- a/frontend/src/app/components/zapier/zapier.component.spec.ts +++ b/frontend/src/app/components/zapier/zapier.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterModule } from '@angular/router'; import { ZapierComponent } from './zapier.component'; import { provideHttpClient } from '@angular/common/http'; @@ -9,7 +10,7 @@ describe('ZapierComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ZapierComponent], + imports: [ZapierComponent, RouterModule.forRoot([])], providers: [provideHttpClient()] }) .compileComponents(); From 584fc822fdd8efa427f0add984d4f29dffcd58f3 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 10 Feb 2026 16:08:54 +0000 Subject: [PATCH 14/16] change saas option for dev config --- frontend/src/environments/environment.dev.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/environments/environment.dev.ts b/frontend/src/environments/environment.dev.ts index 24d2881ea..9490b3494 100644 --- a/frontend/src/environments/environment.dev.ts +++ b/frontend/src/environments/environment.dev.ts @@ -1,6 +1,6 @@ export const environment = { production: false, - saas: true, + saas: false, apiRoot: 'https://app.rocketadmin.com/api', saasURL: 'https://app.rocketadmin.com', saasHostnames: ['localhost', '127.0.0.1'], From ebf9cb2ad9a9bdf78e46456ba0ada4f2180241b8 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 10 Feb 2026 16:22:57 +0000 Subject: [PATCH 15/16] add saas key to prod config file --- frontend/src/environments/environment.prod.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/environments/environment.prod.ts b/frontend/src/environments/environment.prod.ts index acfb70db2..662d75906 100644 --- a/frontend/src/environments/environment.prod.ts +++ b/frontend/src/environments/environment.prod.ts @@ -1,5 +1,6 @@ export const environment = { production: true, + saas: false, apiRoot: "/api", saasURL: "", stagingHost: "rocketadmin-dev.tail9f8b2.ts.net", // Tailscale host From 7b74026bf0f079c0659eea0dba9399ff73c960d2 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Wed, 11 Feb 2026 09:48:02 +0000 Subject: [PATCH 16/16] fix importRows action naming --- frontend/src/app/components/audit/audit-data-source.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/audit/audit-data-source.ts b/frontend/src/app/components/audit/audit-data-source.ts index 0733ff351..df1fa1770 100644 --- a/frontend/src/app/components/audit/audit-data-source.ts +++ b/frontend/src/app/components/audit/audit-data-source.ts @@ -67,7 +67,7 @@ export class AuditDataSource implements DataSource { updateRow: 'Edited', rowReceived: 'Viewed', rowsReceived: 'Viewed', - importRow: 'Imported', + importRows: 'Imported', exportRows: 'Exported' } @@ -77,7 +77,7 @@ export class AuditDataSource implements DataSource { updateRow: 'edit', rowReceived: 'visibility', rowsReceived: 'visibility', - importRow: 'upload', + importRows: 'upload', exportRows: 'download' }