fix(console): simplify instance page (#7274)

* move settings, rm nav for single org

* move instance pages to settings

* i18n

* revalidate orgs on create

* Update bg.json

* show custome portal link

* Update console/src/app/modules/settings-list/settings.ts

Co-authored-by: Livio Spring <[email protected]>

* Update console/src/app/modules/settings-list/settings.ts

Co-authored-by: Livio Spring <[email protected]>

* Update console/src/app/modules/settings-list/settings.ts

Co-authored-by: Livio Spring <[email protected]>

* add org page to instance settings

* iam.read for org list

* i18n

* instance imgs, cleanup

* rm unused imgs

* remove unused imgs, replace default settings imgs

* event image

* e2e url

* instance url

---------

Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
Max Peintner
2024-02-06 14:35:43 +01:00
committed by GitHub
co-authored by Livio Spring
parent 7f7fb55f34
commit ca49e0f532
178 changed files with 598 additions and 729 deletions
@@ -0,0 +1,129 @@
<h2>{{ 'IAM.EVENTS.TITLE' | translate }}</h2>
<p class="events-desc cnsl-secondary-text">{{ 'IAM.EVENTS.DESCRIPTION' | translate }}</p>
<cnsl-refresh-table
[hideRefresh]="true"
(refreshed)="refresh()"
[dataSize]="dataSource.data.length"
[loading]="_loading | async"
>
<div actions>
<cnsl-filter-events (requestChanged)="filterChanged($event)"></cnsl-filter-events>
</div>
<table
[dataSource]="dataSource"
mat-table
class="table views-table"
aria-label="Views"
matSort
(matSortChange)="sortChange($event)"
>
<ng-container matColumnDef="editor">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.EDITOR' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<div class="editor-row" *ngIf="event.editor as editor">
<!-- <cnsl-avatar
*ngIf="editor && editor.displayName; else cog"
class="avatar"
[name]="editor.displayName"
[avatarUrl]="editor.avatarUrl || ''"
[forColor]="editor.preferredLoginName ?? editor.displayName"
[size]="32"
>
</cnsl-avatar>
<ng-template #cog>
<cnsl-avatar [forColor]="editor?.preferredLoginName ?? 'franz'" [isMachine]="true">
<i class="las la-robot"></i>
</cnsl-avatar> </ng-template
> -->
<span class="name" *ngIf="editor.displayName">{{ editor.displayName }}</span>
<span class="state" *ngIf="editor.service">{{ editor.service }}</span>
</div>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="aggregate">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.AGGREGATE' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<div class="aggregate-row">
<span class="id">{{ event.aggregate.id }}</span
><span class="state" *ngIf="event.aggregate?.type?.localized?.localizedMessage">{{
event.aggregate.type.localized.localizedMessage
}}</span>
</div>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="resourceOwner">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.RESOURCEOWNER' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<span *ngIf="event.aggregate.resourceOwner">{{ event.aggregate.resourceOwner }}</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="sequence">
<th mat-header-cell *matHeaderCellDef>
{{ 'IAM.EVENTS.SEQUENCE' | translate }}
</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
{{ event.sequence }}
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header [start]="'desc'" [disableClear]="true">
{{ 'IAM.EVENTS.CREATIONDATE' | translate }}
</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<span>{{ event?.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm:ss' }}</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.TYPE' | translate }}</th>
<td mat-cell *matCellDef="let event" data-e2e="event-type-cell">
<ng-container *ngIf="event | toobject as event">
<span *ngIf="event.type?.localized?.localizedMessage">{{ event.type.localized.localizedMessage }}</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="payload">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.PAYLOAD' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | topayload as payload">
<span>{{ payload | json }}</span>
<div class="btn-wrapper">
<button class="open-in-dialog-btn" mat-icon-button (click)="openDialog(event)">
<mat-icon svgIcon="mdi_arrow_expand"></mat-icon>
</button>
</div>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<cnsl-paginator
#paginator
class="paginator"
[hidePagination]="true"
[showMoreButton]="true"
[disableShowMore]="_done | async"
(moreRequested)="more()"
[length]="dataSource.data.length"
>
</cnsl-paginator>
</cnsl-refresh-table>
@@ -0,0 +1,91 @@
@mixin events-theme($theme) {
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$primary: map-get($theme, primary);
$primary-color: map-get($primary, 500);
.mat-column-payload {
position: relative;
white-space: nowrap;
max-width: 200px;
overflow: hidden;
text-overflow: hidden;
.btn-wrapper {
background-color: $card-background-color;
transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
border: 1px solid $border-color;
box-sizing: border-box;
border-radius: 0.5rem;
outline: none;
box-shadow: 0 0 3px #0000001a;
height: 36px;
width: 36px;
position: absolute;
display: none;
justify-content: center;
align-items: center;
right: 0;
top: 50%;
transform: translateY(-50%);
.open-in-dialog-btn {
display: flex;
align-items: center;
justify-content: center;
}
}
&:hover {
.btn-wrapper {
display: flex;
}
}
}
.filter-button-wrapper {
display: flex;
justify-content: space-between;
position: relative;
user-select: none;
margin-left: 1rem;
.filter-count {
font-size: 14px;
color: $primary-color;
margin-left: 0.5rem;
}
}
}
.events-desc {
font-size: 14px;
}
.editor-row {
display: flex;
flex-direction: column;
padding: 2px 0;
.name,
.id {
margin-bottom: 0.25rem;
}
.state {
align-self: flex-start;
}
}
.aggregate-row {
display: flex;
flex-direction: column;
align-items: flex-start;
.id {
margin-bottom: 0.25rem;
}
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { EventsComponent } from './events.component';
describe('EventsComponent', () => {
let component: EventsComponent;
let fixture: ComponentFixture<EventsComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [EventsComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EventsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,198 @@
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { Component, OnDestroy, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSort, Sort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { BehaviorSubject, Observable, Subject, takeUntil } from 'rxjs';
import { DisplayJsonDialogComponent } from 'src/app/modules/display-json-dialog/display-json-dialog.component';
import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { ListEventsRequest, ListEventsResponse } from 'src/app/proto/generated/zitadel/admin_pb';
import { Event } from 'src/app/proto/generated/zitadel/event_pb';
import { AdminService } from 'src/app/services/admin.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ToastService } from 'src/app/services/toast.service';
enum EventFieldName {
EDITOR = 'editor',
AGGREGATE = 'aggregate',
RESOURCEOWNER = 'resourceOwner',
SEQUENCE = 'sequence',
CREATIONDATE = 'creationDate',
TYPE = 'type',
PAYLOAD = 'payload',
}
type LoadRequest = {
req: ListEventsRequest;
override: boolean;
};
@Component({
selector: 'cnsl-events',
templateUrl: './events.component.html',
styleUrls: ['./events.component.scss'],
})
export class EventsComponent implements OnDestroy {
public INITPAGESIZE = 20;
public sortAsc = false;
private destroy$: Subject<void> = new Subject();
public displayedColumns: string[] = [
EventFieldName.TYPE,
EventFieldName.AGGREGATE,
EventFieldName.RESOURCEOWNER,
EventFieldName.EDITOR,
EventFieldName.SEQUENCE,
EventFieldName.CREATIONDATE,
EventFieldName.PAYLOAD,
];
public currentRequest$: BehaviorSubject<LoadRequest> = new BehaviorSubject<LoadRequest>({
req: new ListEventsRequest().setLimit(this.INITPAGESIZE),
override: true,
});
@ViewChild(MatSort) public sort!: MatSort;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public dataSource: MatTableDataSource<Event> = new MatTableDataSource<Event>([]);
public _done: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public done: Observable<boolean> = this._done.asObservable();
public _loading: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private _data: BehaviorSubject<Event[]> = new BehaviorSubject<Event[]>([]);
constructor(
private adminService: AdminService,
private breadcrumbService: BreadcrumbService,
private _liveAnnouncer: LiveAnnouncer,
private toast: ToastService,
private dialog: MatDialog,
) {
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.INSTANCE,
name: 'Instance',
routerLink: ['/instance'],
}),
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
this.currentRequest$
.pipe(
// this would compare the requests if a duplicate and redundant request would be made
// distinctUntilChanged(({ req: prev }, { req: next }) => {
// return JSON.stringify(prev.toObject()) === JSON.stringify(next.toObject());
// }),
takeUntil(this.destroy$),
)
.subscribe(({ req, override }) => {
this._loading.next(true);
this.adminService
.listEvents(req)
.then((res: ListEventsResponse) => {
if (override) {
this._data = new BehaviorSubject<Event[]>([]);
this.dataSource = new MatTableDataSource<Event>([]);
}
const eventList = res.getEventsList();
this._data.next(eventList);
const concat = this.dataSource.data.concat(eventList);
this.dataSource = new MatTableDataSource<Event>(concat);
this._loading.next(false);
if (eventList.length === 0) {
this._done.next(true);
} else {
this._done.next(false);
}
})
.catch((error) => {
this.toast.showError(error);
this._loading.next(false);
this._data.next([]);
});
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
public loadEvents(filteredRequest: ListEventsRequest, override: boolean = false): void {
this.currentRequest$.next({ req: filteredRequest, override });
}
public refresh(): void {
const req = new ListEventsRequest();
req.setLimit(this.paginator.pageSize);
}
public sortChange(sortState: Sort) {
if (sortState.direction && sortState.active) {
this._liveAnnouncer.announce(`Sorted ${sortState.direction}ending`);
this.sortAsc = sortState.direction === 'asc';
const { req } = this.currentRequest$.value;
req.setLimit(this.INITPAGESIZE);
req.setAsc(this.sortAsc ? true : false);
this.loadEvents(req, true);
} else {
this._liveAnnouncer.announce('Sorting cleared');
}
}
public openDialog(event: Event): void {
this.dialog.open(DisplayJsonDialogComponent, {
data: {
event: event,
},
width: '450px',
});
}
public more(): void {
const sequence = this.getCursor();
const { req } = this.currentRequest$.value;
req.setSequence(sequence);
this.loadEvents(req);
}
public filterChanged(filterRequest: ListEventsRequest) {
const req = new ListEventsRequest();
req.setLimit(this.INITPAGESIZE);
req.setAsc(this.sortAsc ? true : false);
req.setAggregateTypesList(filterRequest.getAggregateTypesList());
req.setAggregateId(filterRequest.getAggregateId());
req.setEventTypesList(filterRequest.getEventTypesList());
req.setEditorUserId(filterRequest.getEditorUserId());
req.setResourceOwner(filterRequest.getResourceOwner());
req.setSequence(filterRequest.getSequence());
req.setRange(filterRequest.getRange());
req.setFrom(filterRequest.getFrom());
const isAsc: boolean = filterRequest.getAsc();
req.setAsc(isAsc);
if (this.sortAsc !== isAsc) {
this.sort.sort({ id: 'creationDate', start: isAsc ? 'asc' : 'desc', disableClear: true });
}
this.loadEvents(req, true);
}
private getCursor(): number {
const current = this._data.value;
if (current.length) {
const sequence = current[current.length - 1].toObject().sequence;
return sequence;
}
return 0;
}
}
@@ -0,0 +1,63 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { OverlayModule } from '@angular/cdk/overlay';
import { MatDialogModule } from '@angular/material/dialog';
import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module';
import { AvatarModule } from 'src/app/modules/avatar/avatar.module';
import { DisplayJsonDialogModule } from 'src/app/modules/display-json-dialog/display-json-dialog.module';
import { FilterEventsModule } from 'src/app/modules/filter-events/filter-events.module';
import { ToObjectPipeModule } from 'src/app/pipes/to-object/to-object.module';
import { ToPayloadPipeModule } from 'src/app/pipes/to-payload/to-payload.module';
import { EventsComponent } from './events.component';
@NgModule({
declarations: [EventsComponent],
imports: [
CommonModule,
TableActionsModule,
MatIconModule,
CardModule,
FilterEventsModule,
ToObjectPipeModule,
ToPayloadPipeModule,
HasRolePipeModule,
MatDialogModule,
MatButtonModule,
CopyToClipboardModule,
InputModule,
TranslateModule,
InfoSectionModule,
AvatarModule,
MatTooltipModule,
MatProgressSpinnerModule,
RefreshTableModule,
ActionKeysModule,
PaginatorModule,
TimestampToDatePipeModule,
LocalizedDatePipeModule,
DisplayJsonDialogModule,
MatTableModule,
MatSortModule,
OverlayModule,
],
exports: [EventsComponent],
})
export default class EventsModule {}
@@ -0,0 +1,68 @@
<h2>{{ 'IAM.FAILEDEVENTS.TITLE' | translate }}</h2>
<p class="failed-events-desc cnsl-secondary-text">{{ 'IAM.FAILEDEVENTS.DESCRIPTION' | translate }}</p>
<div class="table-wrapper">
<cnsl-refresh-table (refreshed)="loadEvents()" [dataSize]="eventDataSource.data.length" [loading]="loading$ | async">
<table [dataSource]="eventDataSource" mat-table class="table" aria-label="Elements">
<ng-container matColumnDef="viewName">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.FAILEDEVENTS.VIEWNAME' | translate }}</th>
<td mat-cell *matCellDef="let event">{{ event.viewName }}</td>
</ng-container>
<ng-container matColumnDef="database">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.FAILEDEVENTS.DATABASE' | translate }}</th>
<td mat-cell *matCellDef="let event">{{ event.database }}</td>
</ng-container>
<ng-container matColumnDef="failedSequence">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.FAILEDEVENTS.FAILEDSEQUENCE' | translate }}</th>
<td mat-cell *matCellDef="let event">
<span>{{ event?.failedSequence }}</span>
</td>
</ng-container>
<ng-container matColumnDef="failureCount">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.FAILEDEVENTS.FAILURECOUNT' | translate }}</th>
<td mat-cell *matCellDef="let event">
<span>{{ event?.failureCount }}</span>
</td>
</ng-container>
<ng-container matColumnDef="lastFailed">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.FAILEDEVENTS.LASTFAILED' | translate }}</th>
<td mat-cell *matCellDef="let event">
<span>{{ event?.lastFailed | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<ng-container matColumnDef="errorMessage">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.FAILEDEVENTS.ERRORMESSAGE' | translate }}</th>
<td mat-cell *matCellDef="let event">
<span class="failed-event-error-message">{{ event?.errorMessage }}</span>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td class="back" mat-cell *matCellDef="let event">
<cnsl-table-actions>
<button
actions
color="warn"
mat-icon-button
matTooltip="{{ 'IAM.FAILEDEVENTS.DELETE' | translate }}"
(click)="cancelEvent(event.viewName, event.database, event.failedSequence)"
>
<i class="las la-minus-circle"></i>
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="eventDisplayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: eventDisplayedColumns"></tr>
</table>
<cnsl-paginator #paginator class="paginator" [hidePagination]="true" [length]="eventDataSource.data.length || 0">
</cnsl-paginator>
</cnsl-refresh-table>
</div>
@@ -0,0 +1,8 @@
.failed-events-desc {
font-size: 14px;
}
.failed-event-error-message {
padding: 0.5rem 0;
display: block;
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FailedEventsComponent } from './failed-events.component';
describe('FailedEventsComponent', () => {
let component: FailedEventsComponent;
let fixture: ComponentFixture<FailedEventsComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [FailedEventsComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FailedEventsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,74 @@
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { FailedEvent } from 'src/app/proto/generated/zitadel/admin_pb';
import { AdminService } from 'src/app/services/admin.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'cnsl-iam-failed-events',
templateUrl: './failed-events.component.html',
styleUrls: ['./failed-events.component.scss'],
})
export class FailedEventsComponent implements AfterViewInit {
@ViewChild(MatPaginator) public eventPaginator!: MatPaginator;
public eventDataSource: MatTableDataSource<FailedEvent.AsObject> = new MatTableDataSource<FailedEvent.AsObject>([]);
public eventDisplayedColumns: string[] = [
'viewName',
'database',
'failedSequence',
'failureCount',
'lastFailed',
'errorMessage',
'actions',
];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
constructor(
private adminService: AdminService,
private breadcrumbService: BreadcrumbService,
private toast: ToastService,
) {
this.loadEvents();
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.INSTANCE,
name: 'Instance',
routerLink: ['/instance'],
}),
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
}
ngAfterViewInit(): void {
this.loadEvents();
}
public loadEvents(): void {
this.loadingSubject.next(true);
from(this.adminService.listFailedEvents())
.pipe(
map((resp) => {
return resp?.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
)
.subscribe((events) => {
this.eventDataSource = new MatTableDataSource<FailedEvent.AsObject>(events);
this.eventDataSource.paginator = this.eventPaginator;
});
}
public cancelEvent(viewname: string, db: string, seq: number): void {
this.adminService.removeFailedEvent(viewname, db, seq).then(() => {
this.toast.showInfo('IAM.FAILEDEVENTS.DELETESUCCESS', true);
});
}
}
@@ -0,0 +1,47 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { FailedEventsComponent } from './failed-events.component';
@NgModule({
declarations: [FailedEventsComponent],
imports: [
CommonModule,
TableActionsModule,
MatIconModule,
CardModule,
HasRolePipeModule,
PaginatorModule,
MatButtonModule,
CopyToClipboardModule,
InputModule,
TranslateModule,
InfoSectionModule,
MatProgressSpinnerModule,
MatTooltipModule,
RefreshTableModule,
LocalizedDatePipeModule,
TimestampToDatePipeModule,
MatTableModule,
MatSortModule,
],
exports: [FailedEventsComponent],
})
export default class FailedEventsModule {}
@@ -0,0 +1,40 @@
<h2>{{ 'IAM.VIEWS.TITLE' | translate }}</h2>
<p class="views-desc cnsl-secondary-text">{{ 'IAM.VIEWS.DESCRIPTION' | translate }}</p>
<cnsl-refresh-table (refreshed)="loadViews()" [dataSize]="dataSource.data.length" [loading]="loading$ | async">
<table [dataSource]="dataSource" mat-table class="table views-table" aria-label="Views" matSort>
<ng-container matColumnDef="viewName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'IAM.VIEWS.VIEWNAME' | translate }}</th>
<td mat-cell *matCellDef="let view">{{ view.viewName }}</td>
</ng-container>
<ng-container matColumnDef="database">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'IAM.VIEWS.DATABASE' | translate }}</th>
<td mat-cell *matCellDef="let view">{{ view.database }}</td>
</ng-container>
<ng-container matColumnDef="sequence">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.VIEWS.SEQUENCE' | translate }}</th>
<td mat-cell *matCellDef="let view">{{ view.processedSequence }}</td>
</ng-container>
<ng-container matColumnDef="eventTimestamp">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.VIEWS.EVENTTIMESTAMP' | translate }}</th>
<td mat-cell *matCellDef="let view">
<span>{{ view?.eventTimestamp | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<ng-container matColumnDef="lastSuccessfulSpoolerRun">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.VIEWS.LASTSPOOL' | translate }}</th>
<td mat-cell *matCellDef="let view">
<span>{{ view?.lastSuccessfulSpoolerRun | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<cnsl-paginator #paginator class="paginator" [hidePagination]="true" [length]="dataSource.data.length || 0">
</cnsl-paginator>
</cnsl-refresh-table>
@@ -0,0 +1,3 @@
.views-desc {
font-size: 14px;
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IamViewsComponent } from './iam-views.component';
describe('IamViewsComponent', () => {
let component: IamViewsComponent;
let fixture: ComponentFixture<IamViewsComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [IamViewsComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IamViewsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,62 @@
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { View } from 'src/app/proto/generated/zitadel/admin_pb';
import { AdminService } from 'src/app/services/admin.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
@Component({
selector: 'cnsl-iam-views',
templateUrl: './iam-views.component.html',
styleUrls: ['./iam-views.component.scss'],
})
export class IamViewsComponent implements AfterViewInit {
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
public dataSource: MatTableDataSource<View.AsObject> = new MatTableDataSource<View.AsObject>([]);
public displayedColumns: string[] = ['viewName', 'database', 'sequence', 'eventTimestamp', 'lastSuccessfulSpoolerRun'];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
constructor(
private adminService: AdminService,
private breadcrumbService: BreadcrumbService,
) {
this.loadViews();
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.INSTANCE,
name: 'Instance',
routerLink: ['/instance'],
}),
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
}
ngAfterViewInit(): void {
this.loadViews();
}
public loadViews(): void {
this.loadingSubject.next(true);
from(this.adminService.listViews())
.pipe(
map((resp) => {
return resp.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
)
.subscribe((views) => {
this.dataSource = new MatTableDataSource<View.AsObject>(views);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
});
}
}
@@ -0,0 +1,47 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { IamViewsComponent } from './iam-views.component';
@NgModule({
declarations: [IamViewsComponent],
imports: [
CommonModule,
TableActionsModule,
MatIconModule,
CardModule,
HasRolePipeModule,
MatButtonModule,
CopyToClipboardModule,
InputModule,
TranslateModule,
InfoSectionModule,
MatTooltipModule,
MatProgressSpinnerModule,
RefreshTableModule,
PaginatorModule,
TimestampToDatePipeModule,
LocalizedDatePipeModule,
MatTableModule,
MatSortModule,
],
exports: [IamViewsComponent],
})
export default class IamViewsModule {}
+3 -90
View File
@@ -7,99 +7,12 @@
*ngIf="
breadc[breadc.length - 1] &&
!breadc[breadc.length - 1].hideNav &&
breadc[breadc.length - 1].type !== BreadcrumbType.AUTHUSER
breadc[breadc.length - 1].type !== BreadcrumbType.AUTHUSER &&
breadc[breadc.length - 1].type !== BreadcrumbType.INSTANCE
"
[ngSwitch]="breadc[0].type"
>
<div class="nav-row" @navrow>
<ng-container *ngSwitchCase="BreadcrumbType.INSTANCE">
<div class="nav-row-abs" @navroworg>
<ng-template cnslHasRole [hasRole]="['iam.read']">
<a
class="nav-item"
[routerLinkActiveOptions]="{ exact: false }"
[routerLinkActive]="['active']"
[routerLink]="['/instance']"
>
<div class="c_label">
<span> {{ 'MENU.INSTANCEOVERVIEW' | translate }} </span>
</div>
</a>
<a
class="nav-item"
[routerLinkActiveOptions]="{ exact: false }"
[routerLinkActive]="['active']"
[routerLink]="['/orgs']"
>
<div class="c_label">
<span> {{ 'MENU.ORGS' | translate }} </span>
</div>
</a>
<a
class="nav-item"
[routerLinkActiveOptions]="{ exact: false }"
[routerLinkActive]="['active']"
[routerLink]="['/events']"
>
<div class="c_label">
<span> {{ 'MENU.EVENTS' | translate }} </span>
</div>
</a>
<a
class="nav-item"
[routerLinkActiveOptions]="{ exact: false }"
[routerLinkActive]="['active']"
[routerLink]="['/views']"
>
<div class="c_label">
<span> {{ 'MENU.VIEWS' | translate }} </span>
</div>
</a>
<a
class="nav-item"
[routerLinkActiveOptions]="{ exact: false }"
[routerLinkActive]="['active']"
[routerLink]="['/failed-events']"
>
<div class="c_label">
<span> {{ 'MENU.FAILEDEVENTS' | translate }} </span>
</div>
</a>
<a
class="nav-item"
[routerLinkActiveOptions]="{ exact: false }"
[routerLinkActive]="['active']"
[routerLink]="['/settings']"
*ngIf="['iam.read', 'iam.policy.read'] | hasRole: true | async"
>
<div class="c_label">
<span> {{ 'MENU.SETTINGS' | translate }} </span>
</div>
</a>
<a
*ngIf="customerPortalLink$ | async as customerPortalLink"
class="nav-item external-link"
[href]="customerPortalLink"
target="_blank"
rel="noreferrer"
>
<div class="c_label">
<span> {{ 'MENU.CUSTOMERPORTAL' | translate }} </span>
</div>
<i class="las la-external-link-alt"></i>
</a>
</ng-template>
<template [ngTemplateOutlet]="shortcutKeyRef"></template>
</div>
</ng-container>
<ng-container *ngSwitchCase="BreadcrumbType.ORG">
<div class="nav-row-abs" @navrowproject>
<a
@@ -183,7 +96,7 @@
[routerLinkActive]="['active']"
[routerLinkActiveOptions]="{ exact: false }"
[routerLink]="['/org-settings']"
*ngIf="['policy.read'] | hasRole | async"
*ngIf="(['policy.read'] | hasRole | async) && ((authService.cachedOrgs | async)?.length ?? 1) > 1"
>
<span class="label">{{ 'MENU.SETTINGS' | translate }}</span>
</a>
@@ -90,7 +90,6 @@ export class NavComponent implements OnDestroy {
private destroy$: Subject<void> = new Subject();
public BreadcrumbType: any = BreadcrumbType;
public customerPortalLink$ = this.envService.env.pipe(map((env) => env.customer_portal));
public positions: ConnectedPosition[] = [
new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }, 0, 10),
@@ -98,7 +97,6 @@ export class NavComponent implements OnDestroy {
];
constructor(
private envService: EnvironmentService,
public authService: GrpcAuthService,
public adminService: AdminService,
public authenticationService: AuthenticationService,
@@ -6,6 +6,12 @@
[settingsList]="settingsList"
queryParam="id"
>
<ng-container *ngIf="currentSetting === 'organizations'">
<h2>{{ 'ORG.PAGES.LIST' | translate }}</h2>
<p class="org-desc cnsl-secondary-text">{{ 'ORG.PAGES.LISTDESCRIPTION' | translate }}</p>
<cnsl-org-table></cnsl-org-table>
</ng-container>
<ng-container *ngIf="currentSetting === 'complexity'">
<cnsl-password-complexity-policy [serviceType]="serviceType"></cnsl-password-complexity-policy>
</ng-container>
@@ -57,4 +63,13 @@
<ng-container *ngIf="currentSetting === 'languages' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-language-settings></cnsl-language-settings>
</ng-container>
<ng-container *ngIf="currentSetting === 'views' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-iam-views></cnsl-iam-views>
</ng-container>
<ng-container *ngIf="currentSetting === 'events' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-events></cnsl-events>
</ng-container>
<ng-container *ngIf="currentSetting === 'failedevents' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-iam-failed-events></cnsl-iam-failed-events>
</ng-container>
</cnsl-sidenav>
@@ -24,6 +24,11 @@ import { SecretGeneratorModule } from '../policies/secret-generator/secret-gener
import { SecurityPolicyModule } from '../policies/security-policy/security-policy.module';
import { SidenavModule } from '../sidenav/sidenav.module';
import { SettingsListComponent } from './settings-list.component';
import FailedEventsModule from '../failed-events/failed-events.module';
import IamViewsModule from '../iam-views/iam-views.module';
import EventsModule from '../events/events.module';
import OrgListModule from 'src/app/pages/org-list/org-list.module';
import { OrgTableModule } from '../org-table/org-table.module';
@NgModule({
declarations: [SettingsListComponent],
@@ -44,6 +49,7 @@ import { SettingsListComponent } from './settings-list.component';
SecurityPolicyModule,
DomainsModule,
LoginTextsPolicyModule,
OrgTableModule,
DomainPolicyModule,
TranslateModule,
HasRolePipeModule,
@@ -51,6 +57,9 @@ import { SettingsListComponent } from './settings-list.component';
NotificationSMSProviderModule,
OIDCConfigurationModule,
SecretGeneratorModule,
FailedEventsModule,
IamViewsModule,
EventsModule,
],
exports: [SettingsListComponent],
})
@@ -1,6 +1,15 @@
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
import { SidenavSetting } from '../sidenav/sidenav.component';
export const ORGANIZATIONS: SidenavSetting = {
id: 'organizations',
i18nKey: 'SETTINGS.LIST.ORGS',
groupI18nKey: 'SETTINGS.GROUPS.GENERAL',
requiredRoles: {
[PolicyComponentServiceType.ADMIN]: ['iam.read'],
},
};
export const LANGUAGES: SidenavSetting = {
id: 'languages',
i18nKey: 'SETTINGS.LIST.LANGUAGES',
@@ -33,6 +42,33 @@ export const SECURITY: SidenavSetting = {
},
};
export const VIEWS: SidenavSetting = {
id: 'views',
i18nKey: 'SETTINGS.LIST.VIEWS',
groupI18nKey: 'SETTINGS.GROUPS.STORAGE',
requiredRoles: {
[PolicyComponentServiceType.ADMIN]: ['iam.read'],
},
};
export const FAILEDEVENTS: SidenavSetting = {
id: 'failedevents',
i18nKey: 'SETTINGS.LIST.FAILEDEVENTS',
groupI18nKey: 'SETTINGS.GROUPS.STORAGE',
requiredRoles: {
[PolicyComponentServiceType.ADMIN]: ['iam.read'],
},
};
export const EVENTS: SidenavSetting = {
id: 'events',
i18nKey: 'SETTINGS.LIST.EVENTS',
groupI18nKey: 'SETTINGS.GROUPS.STORAGE',
requiredRoles: {
[PolicyComponentServiceType.ADMIN]: ['events.read'],
},
};
export const LOGIN: SidenavSetting = {
id: 'login',
i18nKey: 'SETTINGS.LIST.LOGIN',