Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7b275f3
feat(audit): improve date format and add status badge styling
karinakharchenko Jan 15, 2026
3796745
feat(audit): add action icons and improve UI
karinakharchenko Jan 15, 2026
ddab401
feat(audit): display user name above email in User column
karinakharchenko Jan 19, 2026
9f3a1a0
feat(audit): use user-friendly action labels
karinakharchenko Jan 19, 2026
2042ed3
feat(audit): use outlined visibility icon
karinakharchenko Jan 19, 2026
bb5b867
feat(audit): add export rows logging to audit
karinakharchenko Jan 19, 2026
1af6e31
feat(audit): make User the first column
karinakharchenko Jan 19, 2026
c87f628
Swap Date and Status columns in audit table
karinakharchenko Jan 19, 2026
99ac210
Merge main into audit_v3
karinakharchenko Jan 19, 2026
1f468cd
feat: add profile sidebar navigation for settings pages
karinakharchenko Feb 9, 2026
ae209cd
Merge branch 'main' into audit_v3
lyubov-voloshko Feb 9, 2026
2f5536d
branding page: fix top margain, fix tab title input visibility, add s…
lyubov-voloshko Feb 10, 2026
ec7fa70
company page: fix alignment and add skeleton loaders
lyubov-voloshko Feb 10, 2026
c331e7d
Secrets: fix top spacing on Add secret dialog and update s3 widget hint
lyubov-voloshko Feb 10, 2026
84ec1a9
fix unit tests
lyubov-voloshko Feb 10, 2026
c6723f8
Merge branch 'main' into audit_v3
lyubov-voloshko Feb 10, 2026
584fc82
change saas option for dev config
lyubov-voloshko Feb 10, 2026
1cd9188
Merge branch 'audit_v3' of https://github.com/karinakharchenko/rocket…
lyubov-voloshko Feb 10, 2026
ebf9cb2
add saas key to prod config file
lyubov-voloshko Feb 10, 2026
4f7e926
Merge branch 'rocket-admin:main' into main
karinakharchenko Feb 11, 2026
7b74026
fix importRows action naming
lyubov-voloshko Feb 11, 2026
152a030
Merge branch 'main' into audit_v3
lyubov-voloshko Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,6 +28,7 @@ export class ExportCSVFromTableUseCase
constructor(
@Inject(BaseType.GLOBAL_DB_CONTEXT)
protected _dbContext: IGlobalDatabaseContext,
private tableLogsService: TableLogsService,
) {
super();
}
Expand All @@ -47,6 +50,8 @@ export class ExportCSVFromTableUseCase
throw new NonAvailableInFreePlanException(Messages.CONNECTION_IS_FROZEN);
}

let operationResult = OperationResultStatusEnum.unknown;

try {
const dao = getDataAccessObject(connection);

Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand All @@ -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);
}
}
}
17 changes: 16 additions & 1 deletion frontend/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
200 changes: 200 additions & 0 deletions frontend/src/app/components/api-keys/api-keys.component.css
Original file line number Diff line number Diff line change
@@ -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;
}
81 changes: 81 additions & 0 deletions frontend/src/app/components/api-keys/api-keys.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<div class="profile-layout">
<app-profile-sidebar activeTab="api"></app-profile-sidebar>

<div class="profile-main">
<app-alert></app-alert>

<div class="api-keys-page">
<h1 class="mat-h1">API Keys</h1>
<p class="api-keys-description">
Generate and manage API keys to access Rocketadmin programmatically.
</p>

<div class="api-keys-content">
<form class="api-key-form" (ngSubmit)="generateAPIkey()">
<mat-form-field appearance="outline" class="api-key-form__input">
<mat-label>API key title</mat-label>
<input matInput name="api-key-title" #apiKeyTitle="ngModel"
data-testid="api-key-input"
angulartics2On="change"
angularticsAction="API Keys: api key title is edited"
[(ngModel)]="generatingAPIkeyTitle">
</mat-form-field>
<button type="submit" mat-flat-button color="primary" data-testid="generate-api-key-button"
class="api-key-form__generate-button"
[disabled]="!generatingAPIkeyTitle || submitting">
{{submitting ? 'Generating...' : 'Generate'}}
</button>
</form>

<div *ngIf="generatedAPIkeyHash" class="api-key-value">
<mat-form-field appearance="outline" class="api-key-value__input">
<mat-label>API key</mat-label>
<input matInput name="token" #token="ngModel" readonly
data-testid="api-key-value-input"
[(ngModel)]="generatedAPIkeyHash">
<mat-hint>
Please save this API key. You won't be able to see it again.
</mat-hint>
</mat-form-field>
<button type="button" mat-icon-button
data-testid="api-key-value-copy-button"
class="api-key-value__copy-button"
matTooltip="Copy API key"
[cdkCopyToClipboard]="generatedAPIkeyHash"
(cdkCopyToClipboardCopied)="showCopyNotification('API key was copied to clipboard.')">
<mat-icon>content_copy</mat-icon>
</button>
</div>

<div class="api-keys-list">
<h3 class="api-keys-list__heading">Your API keys</h3>

<app-placeholder-api-keys-list *ngIf="!apiKeys"></app-placeholder-api-keys-list>

<div *ngIf="apiKeys && apiKeys.length === 0" class="api-keys-empty">
<mat-icon class="api-keys-empty__icon">vpn_key_off</mat-icon>
<p>You don't have any API keys yet.</p>
</div>

<mat-list role="list" *ngIf="apiKeys && apiKeys.length" class="api-keys-items">
<mat-list-item role="listitem" *ngFor="let apiKey of apiKeys; let i=index" class="api-key-list-item">
<div class="api-key-item">
<div class="api-key-item__info">
<mat-icon class="api-key-item__icon">vpn_key</mat-icon>
<span class="api-key-item__title">{{apiKey.title}}</span>
</div>
<button type="button" mat-icon-button
attr.data-testid="api-key-{{i}}-delete-button"
matTooltip="Delete API key"
color="warn"
(click)="deleteAPIkey(apiKey)">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-list-item>
</mat-list>
</div>
</div>
</div>
</div>
</div>
Loading
Loading