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/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..bd83623bb --- /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: var(--top-margin) 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..e72126991 --- /dev/null +++ b/frontend/src/app/components/api-keys/api-keys.component.html @@ -0,0 +1,81 @@ +
+ + +
+ + +
+

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..5e3fa7a53 --- /dev/null +++ b/frontend/src/app/components/api-keys/api-keys.component.ts @@ -0,0 +1,104 @@ +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'; +import { PlaceholderApiKeysListComponent } from '../skeletons/placeholder-api-keys-list/placeholder-api-keys-list.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, + PlaceholderApiKeysListComponent, + ] +}) +export class ApiKeysComponent implements OnInit { + public apiKeys: ApiKey[] = null; + 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/audit/audit-data-source.ts b/frontend/src/app/components/audit/audit-data-source.ts index 1bc694875..df1fa1770 100644 --- a/frontend/src/app/components/audit/audit-data-source.ts +++ b/frontend/src/app/components/audit/audit-data-source.ts @@ -62,25 +62,46 @@ 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', + importRows: 'Imported', + exportRows: 'Exported' } + + const actionIcons = { + addRow: 'add', + deleteRow: 'delete', + updateRow: 'edit', + rowReceived: 'visibility', + rowsReceived: 'visibility', + importRows: 'upload', + exportRows: 'download' + } + 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"); + const dateOnly = format(date, "d MMM yyyy"); + const timeOnly = format(date, "h:mm a"); + return { "Table": log.table_name, "User": log.email, + "UserEmail": 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 b638496bb..cceef52e4 100644 --- a/frontend/src/app/components/audit/audit.component.css +++ b/frontend/src/app/components/audit/audit.component.css @@ -59,10 +59,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; @@ -112,4 +127,89 @@ 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; +} + +.action-cell { + display: flex; + align-items: center; + gap: 8px; +} + +.action-icon { + font-size: 16px; + width: 16px; + height: 16px; +} + +.action-icon--add { + color: #1A5E20; +} + +.action-icon--delete { + color: #C62828; +} + +.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: 0; +} + +.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); + } +} + +.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 37b4601e2..e5a170145 100644 --- a/frontend/src/app/components/audit/audit.component.html +++ b/frontend/src/app/components/audit/audit.component.html @@ -72,20 +72,39 @@

Rocketadmin can not find any tables

{{ column }} -
- {{element[column] || '—'}} +
+ + {{element[column] || '—'}} + + + {{element.ActionIcon}} + visibility + delete + {{element[column] || '—'}} + + + {{getUserName(element.UserEmail)}} + {{element.UserEmail}} + + + {{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 851f29ff5..4bfb7ad20 100644 --- a/frontend/src/app/components/audit/audit.component.ts +++ b/frontend/src/app/components/audit/audit.component.ts @@ -2,6 +2,7 @@ import { AsyncPipe, NgClass, NgForOf, NgIf } from '@angular/common'; import { Component, OnInit, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; 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, MatPaginatorModule } from '@angular/material/paginator'; @@ -38,6 +39,7 @@ import { InfoDialogComponent } from './info-dialog/info-dialog.component'; MatFormFieldModule, MatSelectModule, MatButtonModule, + MatIconModule, MatTableModule, MatPaginatorModule, FormsModule, @@ -93,8 +95,8 @@ export class AuditComponent implements OnInit { }); this.connectionID = this._connections.currentConnectionID; this.accesLevel = this._connections.currentConnectionAccessLevel; - this.columns = ['Table', 'User', 'Action', 'Date', 'Status', 'Details']; - this.dataColumns = ['Table', 'User', '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(); @@ -139,4 +141,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; + } } 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..ae156a034 --- /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: var(--top-margin) 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..b6fdf3181 --- /dev/null +++ b/frontend/src/app/components/branding/branding.component.html @@ -0,0 +1,229 @@ +
+ + +
+ + + + +
+

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..7c095ccb6 --- /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 { PlaceholderBrandingComponent } from '../skeletons/placeholder-branding/placeholder-branding.component'; + +@Component({ + selector: 'app-branding', + templateUrl: './branding.component.html', + styleUrls: ['./branding.component.css'], + imports: [ + NgIf, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatIconModule, + MatTooltipModule, + Angulartics2OnModule, + AlertComponent, + ProfileSidebarComponent, + PlaceholderBrandingComponent, + ], + 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..a51f4234f 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); + 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/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/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/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!
- - + - - - - + + + + + diff --git a/frontend/src/app/components/secrets/secrets.component.css b/frontend/src/app/components/secrets/secrets.component.css index fe5ed3b60..10f6b7ecd 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); + 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.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/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/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-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/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 71d362f55..8ef305607 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: var(--top-margin) 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..44f63c41c 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.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/sso/sso.component.ts b/frontend/src/app/components/sso/sso.component.ts index 8853a4fba..8cf2f4925 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 { PlaceholderSsoComponent } from '../skeletons/placeholder-sso/placeholder-sso.component'; @Component({ selector: 'app-sso', @@ -23,12 +27,15 @@ import { CompanyService } from 'src/app/services/company.service'; FormsModule, RouterModule, MatFormFieldModule, + AlertComponent, + ProfileSidebarComponent, + PlaceholderSsoComponent, ], 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..93eb52c4a 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 { 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..4df8164f2 100644 --- a/frontend/src/app/components/user-settings/user-settings.component.css +++ b/frontend/src/app/components/user-settings/user-settings.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; } .page { @@ -9,12 +21,13 @@ 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..1078d7e79 100644 --- a/frontend/src/app/components/zapier/zapier.component.css +++ b/frontend/src/app/components/zapier/zapier.component.css @@ -1,10 +1,28 @@ +.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; - padding: 32px max(calc(50vw - 325px), 10%); + margin: var(--top-margin) auto; + padding: 0 clamp(40px, 5vw, 100px); + max-width: 1200px; } .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.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(); 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 { 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