Added history to frontend

This commit is contained in:
Sven Heidemann 2025-04-18 11:03:13 +02:00
parent 1824ff7564
commit 165c140a25
25 changed files with 461 additions and 29 deletions

View File

@ -54,7 +54,7 @@ class DbModelABC(ABC):
from data.schemas.administration.user_dao import userDao
return await userDao.get_by_id(self._editor_id)
return await userDao.find_single_by({"id": self._editor_id})
@property
def created(self) -> datetime:

View File

@ -1,6 +1,6 @@
import { DbModel } from 'src/app/model/entities/db-model';
import { DbModelWithHistory } from 'src/app/model/entities/db-model';
export interface Domain extends DbModel {
export interface Domain extends DbModelWithHistory {
name: string;
}

View File

@ -1,7 +1,7 @@
import { DbModel } from 'src/app/model/entities/db-model';
import { DbModelWithHistory } from 'src/app/model/entities/db-model';
import { Role } from 'src/app/model/entities/role';
export interface Group extends DbModel {
export interface Group extends DbModelWithHistory {
name: string;
roles: Role[];
}

View File

@ -1,8 +1,8 @@
import { DbModel } from 'src/app/model/entities/db-model';
import { DbModelWithHistory } from 'src/app/model/entities/db-model';
import { Group } from 'src/app/model/entities/group';
import { Domain } from 'src/app/model/entities/domain';
export interface ShortUrl extends DbModel {
export interface ShortUrl extends DbModelWithHistory {
shortUrl: string;
targetUrl: string;
description: string;

View File

@ -11,7 +11,10 @@ import { Filter } from 'src/app/model/graphql/filter/filter.model';
import { Sort } from 'src/app/model/graphql/filter/sort.model';
import { Apollo, gql } from 'apollo-angular';
import { QueryResult } from 'src/app/model/entities/query-result';
import { DB_MODEL_FRAGMENT } from 'src/app/model/graphql/db-model.query';
import {
DB_HISTORY_MODEL_FRAGMENT,
DB_MODEL_FRAGMENT,
} from 'src/app/model/graphql/db-model.query';
import { catchError, map } from 'rxjs/operators';
import { SpinnerService } from 'src/app/service/spinner.service';
import {
@ -19,10 +22,11 @@ import {
DomainCreateInput,
DomainUpdateInput,
} from 'src/app/model/entities/domain';
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
@Injectable()
export class DomainsDataService
extends PageDataService<Domain>
extends PageWithHistoryDataService<Domain>
implements
Create<Domain, DomainCreateInput>,
Update<Domain, DomainUpdateInput>,
@ -109,6 +113,40 @@ export class DomainsDataService
.pipe(map(result => result.data.domains.nodes[0]));
}
loadHistory(id: number) {
return this.apollo
.query<{ domains: QueryResult<Domain> }>({
query: gql`
query getDomainHistory($id: Int) {
domains(filter: { id: { equal: $id } }) {
count
totalCount
nodes {
history {
id
name
...DB_HISTORY_MODEL
}
}
}
}
${DB_HISTORY_MODEL_FRAGMENT}
`,
variables: {
id,
},
})
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.pipe(map(result => result.data?.domains?.nodes?.[0]?.history ?? []));
}
onChange(): Observable<void> {
return this.apollo
.subscribe<{ domainChange: void }>({
@ -238,6 +276,10 @@ export class DomainsDataService
provide: PageDataService,
useClass: DomainsDataService,
},
{
provide: PageWithHistoryDataService,
useClass: DomainsDataService,
},
DomainsDataService,
];
}

View File

@ -8,6 +8,7 @@ import { DomainsPage } from 'src/app/modules/admin/domains/domains.page';
import { DomainFormPageComponent } from 'src/app/modules/admin/domains/form-page/domain-form-page.component';
import { DomainsDataService } from 'src/app/modules/admin/domains/domains.data.service';
import { DomainsColumns } from 'src/app/modules/admin/domains/domains.columns';
import { HistoryComponent } from 'src/app/modules/admin/domains/history/history.component';
const routes: Routes = [
{
@ -31,12 +32,20 @@ const routes: Routes = [
permissions: [PermissionsEnum.apiKeysUpdate],
},
},
{
path: 'history/:historyId',
component: HistoryComponent,
canActivate: [PermissionGuard],
data: {
permissions: [PermissionsEnum.domains],
},
},
],
},
];
@NgModule({
declarations: [DomainsPage, DomainFormPageComponent],
declarations: [DomainsPage, DomainFormPageComponent, HistoryComponent],
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
providers: [DomainsDataService.provide(), DomainsColumns.provide()],
})

View File

@ -11,6 +11,7 @@
[(skip)]="skip"
[(take)]="take"
(load)="load()"
[history]="true"
[create]="true"
[update]="true"
(delete)="delete($event)"

View File

@ -0,0 +1,3 @@
<app-history-sidebar
[loadHistory]="loadHistory"
[columns]="columns"></app-history-sidebar>

View File

@ -0,0 +1,65 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HistoryComponent } from './history.component';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from 'src/app/service/auth.service';
import { KeycloakService } from 'keycloak-angular';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { PageDataService } from 'src/app/core/base/page.data.service';
import { IpListDataService } from 'src/app/modules/admin/tools/ip-list/ip-list.data.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { PageColumns } from 'src/app/core/base/page.columns';
import { MockPageColumns } from 'src/app/modules/shared/test/page.columns.mock';
describe('HistoryComponent', () => {
let component: HistoryComponent<unknown>;
let fixture: ComponentFixture<HistoryComponent<unknown>>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HistoryComponent],
imports: [
BrowserAnimationsModule,
SharedModule,
TranslateModule.forRoot(),
],
providers: [
AuthService,
KeycloakService,
ErrorHandlingService,
ToastService,
MessageService,
ConfirmationService,
{
provide: ActivatedRoute,
useValue: {
snapshot: { params: { historyId: '3' } },
},
},
{
provide: PageDataService,
useClass: IpListDataService,
},
{
provide: PageColumns,
useClass: MockPageColumns,
},
],
}).compileComponents();
fixture = TestBed.createComponent(HistoryComponent);
component = fixture.componentInstance;
//eslint-disable-next-line @typescript-eslint/no-unused-vars
component.loadHistory = (id: number) => of([]);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,18 @@
import { Component } from '@angular/core';
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
import { PageColumns } from 'src/app/core/base/page.columns';
@Component({
selector: 'app-history',
templateUrl: './history.component.html',
styleUrl: './history.component.scss',
})
export class HistoryComponent<T> {
loadHistory = (id: number) => this.data.loadHistory(id);
columns = this._columns.get();
constructor(
public data: PageWithHistoryDataService<T>,
private _columns: PageColumns<T>
) {}
}

View File

@ -11,7 +11,10 @@ import { Filter } from 'src/app/model/graphql/filter/filter.model';
import { Sort } from 'src/app/model/graphql/filter/sort.model';
import { Apollo, gql } from 'apollo-angular';
import { QueryResult } from 'src/app/model/entities/query-result';
import { DB_MODEL_FRAGMENT } from 'src/app/model/graphql/db-model.query';
import {
DB_HISTORY_MODEL_FRAGMENT,
DB_MODEL_FRAGMENT,
} from 'src/app/model/graphql/db-model.query';
import { catchError, map } from 'rxjs/operators';
import { SpinnerService } from 'src/app/service/spinner.service';
import {
@ -19,10 +22,12 @@ import {
GroupCreateInput,
GroupUpdateInput,
} from 'src/app/model/entities/group';
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
import { Domain } from 'src/app/model/entities/domain';
@Injectable()
export class GroupsDataService
extends PageDataService<Group>
extends PageWithHistoryDataService<Group>
implements
Create<Group, GroupCreateInput>,
Update<Group, GroupUpdateInput>,
@ -117,6 +122,40 @@ export class GroupsDataService
.pipe(map(result => result.data.groups.nodes[0]));
}
loadHistory(id: number) {
return this.apollo
.query<{ groups: QueryResult<Group> }>({
query: gql`
query getGroupHistory($id: Int) {
groups(filter: { id: { equal: $id } }) {
count
totalCount
nodes {
history {
id
name
...DB_HISTORY_MODEL
}
}
}
}
${DB_HISTORY_MODEL_FRAGMENT}
`,
variables: {
id,
},
})
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.pipe(map(result => result.data?.groups?.nodes?.[0]?.history ?? []));
}
onChange(): Observable<void> {
return this.apollo
.subscribe<{ groupChange: void }>({
@ -248,6 +287,10 @@ export class GroupsDataService
provide: PageDataService,
useClass: GroupsDataService,
},
{
provide: PageWithHistoryDataService,
useClass: GroupsDataService,
},
GroupsDataService,
];
}

View File

@ -1,22 +1,23 @@
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { SharedModule } from "src/app/modules/shared/shared.module";
import { RouterModule, Routes } from "@angular/router";
import { PermissionGuard } from "src/app/core/guard/permission.guard";
import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { GroupsPage } from "src/app/modules/admin/groups/groups.page";
import { GroupFormPageComponent } from "src/app/modules/admin/groups/form-page/group-form-page.component";
import { GroupsDataService } from "src/app/modules/admin/groups/groups.data.service";
import { GroupsColumns } from "src/app/modules/admin/groups/groups.columns";
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { RouterModule, Routes } from '@angular/router';
import { PermissionGuard } from 'src/app/core/guard/permission.guard';
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { GroupsPage } from 'src/app/modules/admin/groups/groups.page';
import { GroupFormPageComponent } from 'src/app/modules/admin/groups/form-page/group-form-page.component';
import { GroupsDataService } from 'src/app/modules/admin/groups/groups.data.service';
import { GroupsColumns } from 'src/app/modules/admin/groups/groups.columns';
import { HistoryComponent } from 'src/app/modules/admin/groups/history/history.component';
const routes: Routes = [
{
path: "",
title: "Groups",
path: '',
title: 'Groups',
component: GroupsPage,
children: [
{
path: "create",
path: 'create',
component: GroupFormPageComponent,
canActivate: [PermissionGuard],
data: {
@ -24,19 +25,27 @@ const routes: Routes = [
},
},
{
path: "edit/:id",
path: 'edit/:id',
component: GroupFormPageComponent,
canActivate: [PermissionGuard],
data: {
permissions: [PermissionsEnum.apiKeysUpdate],
},
},
{
path: 'history/:historyId',
component: HistoryComponent,
canActivate: [PermissionGuard],
data: {
permissions: [PermissionsEnum.domains],
},
},
],
},
];
@NgModule({
declarations: [GroupsPage, GroupFormPageComponent],
declarations: [GroupsPage, GroupFormPageComponent, HistoryComponent],
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
providers: [GroupsDataService.provide(), GroupsColumns.provide()],
})

View File

@ -11,6 +11,7 @@
[(skip)]="skip"
[(take)]="take"
(load)="load()"
[history]="true"
[create]="true"
[update]="true"
(delete)="delete($event)"

View File

@ -0,0 +1,3 @@
<app-history-sidebar
[loadHistory]="loadHistory"
[columns]="columns"></app-history-sidebar>

View File

@ -0,0 +1,65 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HistoryComponent } from './history.component';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from 'src/app/service/auth.service';
import { KeycloakService } from 'keycloak-angular';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { PageDataService } from 'src/app/core/base/page.data.service';
import { IpListDataService } from 'src/app/modules/admin/tools/ip-list/ip-list.data.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { PageColumns } from 'src/app/core/base/page.columns';
import { MockPageColumns } from 'src/app/modules/shared/test/page.columns.mock';
describe('HistoryComponent', () => {
let component: HistoryComponent<unknown>;
let fixture: ComponentFixture<HistoryComponent<unknown>>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HistoryComponent],
imports: [
BrowserAnimationsModule,
SharedModule,
TranslateModule.forRoot(),
],
providers: [
AuthService,
KeycloakService,
ErrorHandlingService,
ToastService,
MessageService,
ConfirmationService,
{
provide: ActivatedRoute,
useValue: {
snapshot: { params: { historyId: '3' } },
},
},
{
provide: PageDataService,
useClass: IpListDataService,
},
{
provide: PageColumns,
useClass: MockPageColumns,
},
],
}).compileComponents();
fixture = TestBed.createComponent(HistoryComponent);
component = fixture.componentInstance;
//eslint-disable-next-line @typescript-eslint/no-unused-vars
component.loadHistory = (id: number) => of([]);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,18 @@
import { Component } from '@angular/core';
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
import { PageColumns } from 'src/app/core/base/page.columns';
@Component({
selector: 'app-history',
templateUrl: './history.component.html',
styleUrl: './history.component.scss',
})
export class HistoryComponent<T> {
loadHistory = (id: number) => this.data.loadHistory(id);
columns = this._columns.get();
constructor(
public data: PageWithHistoryDataService<T>,
private _columns: PageColumns<T>
) {}
}

View File

@ -0,0 +1,3 @@
<app-history-sidebar
[loadHistory]="loadHistory"
[columns]="columns"></app-history-sidebar>

View File

@ -0,0 +1,65 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HistoryComponent } from './history.component';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from 'src/app/service/auth.service';
import { KeycloakService } from 'keycloak-angular';
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { PageDataService } from 'src/app/core/base/page.data.service';
import { IpListDataService } from 'src/app/modules/admin/tools/ip-list/ip-list.data.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { PageColumns } from 'src/app/core/base/page.columns';
import { MockPageColumns } from 'src/app/modules/shared/test/page.columns.mock';
describe('HistoryComponent', () => {
let component: HistoryComponent<unknown>;
let fixture: ComponentFixture<HistoryComponent<unknown>>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HistoryComponent],
imports: [
BrowserAnimationsModule,
SharedModule,
TranslateModule.forRoot(),
],
providers: [
AuthService,
KeycloakService,
ErrorHandlingService,
ToastService,
MessageService,
ConfirmationService,
{
provide: ActivatedRoute,
useValue: {
snapshot: { params: { historyId: '3' } },
},
},
{
provide: PageDataService,
useClass: IpListDataService,
},
{
provide: PageColumns,
useClass: MockPageColumns,
},
],
}).compileComponents();
fixture = TestBed.createComponent(HistoryComponent);
component = fixture.componentInstance;
//eslint-disable-next-line @typescript-eslint/no-unused-vars
component.loadHistory = (id: number) => of([]);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,18 @@
import { Component } from '@angular/core';
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
import { PageColumns } from 'src/app/core/base/page.columns';
@Component({
selector: 'app-history',
templateUrl: './history.component.html',
styleUrl: './history.component.scss',
})
export class HistoryComponent<T> {
loadHistory = (id: number) => this.data.loadHistory(id);
columns = this._columns.get();
constructor(
public data: PageWithHistoryDataService<T>,
private _columns: PageColumns<T>
) {}
}

View File

@ -11,7 +11,10 @@ import { Filter } from 'src/app/model/graphql/filter/filter.model';
import { Sort } from 'src/app/model/graphql/filter/sort.model';
import { Apollo, gql } from 'apollo-angular';
import { QueryResult } from 'src/app/model/entities/query-result';
import { DB_MODEL_FRAGMENT } from 'src/app/model/graphql/db-model.query';
import {
DB_HISTORY_MODEL_FRAGMENT,
DB_MODEL_FRAGMENT,
} from 'src/app/model/graphql/db-model.query';
import { catchError, map } from 'rxjs/operators';
import { SpinnerService } from 'src/app/service/spinner.service';
import {
@ -21,6 +24,7 @@ import {
} from 'src/app/model/entities/short-url';
import { Group } from 'src/app/model/entities/group';
import { Domain } from 'src/app/model/entities/domain';
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
@Injectable()
export class ShortUrlsDataService
@ -132,6 +136,52 @@ export class ShortUrlsDataService
.pipe(map(result => result.data.shortUrls.nodes[0]));
}
loadHistory(id: number) {
return this.apollo
.query<{ shortUrls: QueryResult<ShortUrl> }>({
query: gql`
query getDomainHistory($id: Int) {
shortUrls(filter: { id: { equal: $id } }) {
count
totalCount
nodes {
history {
id
shortUrl
targetUrl
description
loadingScreen
visits
group {
id
name
}
domain {
id
name
}
...DB_HISTORY_MODEL
}
}
}
}
${DB_HISTORY_MODEL_FRAGMENT}
`,
variables: {
id,
},
})
.pipe(
catchError(err => {
this.spinner.hide();
throw err;
})
)
.pipe(map(result => result.data?.shortUrls?.nodes?.[0]?.history ?? []));
}
onChange(): Observable<void> {
return merge(
this.apollo
@ -329,6 +379,10 @@ export class ShortUrlsDataService
provide: PageDataService,
useClass: ShortUrlsDataService,
},
{
provide: PageWithHistoryDataService,
useClass: ShortUrlsDataService,
},
ShortUrlsDataService,
];
}

View File

@ -8,6 +8,7 @@ import { ShortUrlsPage } from 'src/app/modules/admin/short-urls/short-urls.page'
import { ShortUrlFormPageComponent } from 'src/app/modules/admin/short-urls/form-page/short-url-form-page.component';
import { ShortUrlsDataService } from 'src/app/modules/admin/short-urls/short-urls.data.service';
import { ShortUrlsColumns } from 'src/app/modules/admin/short-urls/short-urls.columns';
import { HistoryComponent } from 'src/app/modules/admin/short-urls/history/history.component';
const routes: Routes = [
{
@ -31,12 +32,20 @@ const routes: Routes = [
permissions: [PermissionsEnum.shortUrlsUpdate],
},
},
{
path: 'history/:historyId',
component: HistoryComponent,
canActivate: [PermissionGuard],
data: {
permissions: [PermissionsEnum.domains],
},
},
],
},
];
@NgModule({
declarations: [ShortUrlsPage, ShortUrlFormPageComponent],
declarations: [ShortUrlsPage, ShortUrlFormPageComponent, HistoryComponent],
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
providers: [ShortUrlsDataService.provide(), ShortUrlsColumns.provide()],
})

View File

@ -67,6 +67,12 @@
tooltipPosition="left"
pTooltip="{{ 'table.restore' | translate }}"
(click)="restore(url)"></p-button>
<p-button
class="icon-btn btn"
icon="pi pi-history"
tooltipPosition="left"
pTooltip="{{ 'table.history' | translate }}"
routerLink="history/{{ url.id }}"></p-button>
</div>
</div>
</div>