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 <livio.a@gmail.com>

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

Co-authored-by: Livio Spring <livio.a@gmail.com>

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

Co-authored-by: Livio Spring <livio.a@gmail.com>

* 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 <livio.a@gmail.com>
This commit is contained in:
Max Peintner 2024-02-06 14:35:43 +01:00 committed by GitHub
parent 7f7fb55f34
commit ca49e0f532
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
178 changed files with 598 additions and 729 deletions

View File

@ -125,39 +125,6 @@ const routes: Routes = [
},
],
},
{
path: 'failed-events',
loadChildren: () => import('./pages/failed-events/failed-events.module'),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['iam.read'],
},
},
{
path: 'views',
loadChildren: () => import('./pages/iam-views/iam-views.module'),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['iam.read'],
},
},
{
path: 'events',
loadChildren: () => import('./pages/events/events.module'),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['iam.read'],
},
},
{
path: 'settings',
loadChildren: () => import('./pages/instance-settings/instance-settings.module'),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['iam.read', 'iam.policy.read'],
requiresAll: true,
},
},
{
path: 'org-settings',
loadChildren: () => import('./pages/org-settings/org-settings.module'),

View File

@ -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>

View File

@ -61,10 +61,6 @@
}
}
.events-title {
margin: 2rem 0 0 0;
}
.events-desc {
font-size: 14px;
}

View File

@ -26,13 +26,11 @@ import { DisplayJsonDialogModule } from 'src/app/modules/display-json-dialog/dis
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 { EventsRoutingModule } from './events-routing.module';
import { EventsComponent } from './events.component';
@NgModule({
declarations: [EventsComponent],
imports: [
EventsRoutingModule,
CommonModule,
TableActionsModule,
MatIconModule,
@ -60,6 +58,6 @@ import { EventsComponent } from './events.component';
MatSortModule,
OverlayModule,
],
exports: [],
exports: [EventsComponent],
})
export default class IamViewsModule {}
export default class EventsModule {}

View File

@ -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>

View File

@ -1,7 +1,3 @@
.failed-events-title {
margin: 2rem 0 0 0;
}
.failed-events-desc {
font-size: 14px;
}

View File

@ -18,13 +18,11 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod
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 { FailedEventsRoutingModule } from './failed-events-routing.module';
import { FailedEventsComponent } from './failed-events.component';
@NgModule({
declarations: [FailedEventsComponent],
imports: [
FailedEventsRoutingModule,
CommonModule,
TableActionsModule,
MatIconModule,
@ -44,5 +42,6 @@ import { FailedEventsComponent } from './failed-events.component';
MatTableModule,
MatSortModule,
],
exports: [FailedEventsComponent],
})
export default class FailedEventsModule {}

View File

@ -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>

View File

@ -0,0 +1,3 @@
.views-desc {
font-size: 14px;
}

View File

@ -18,13 +18,11 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod
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 { IamViewsRoutingModule } from './iam-views-routing.module';
import { IamViewsComponent } from './iam-views.component';
@NgModule({
declarations: [IamViewsComponent],
imports: [
IamViewsRoutingModule,
CommonModule,
TableActionsModule,
MatIconModule,
@ -44,6 +42,6 @@ import { IamViewsComponent } from './iam-views.component';
MatTableModule,
MatSortModule,
],
exports: [],
exports: [IamViewsComponent],
})
export default class IamViewsModule {}

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>

View File

@ -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,

View File

@ -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>

View File

@ -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],
})

View File

@ -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',

View File

@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { EventsComponent } from './events.component';
const routes: Routes = [
{
path: '',
component: EventsComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class EventsRoutingModule {}

View File

@ -1,131 +0,0 @@
<div class="max-width-container">
<h1 class="events-title">{{ 'IAM.EVENTS.TITLE' | translate }}</h1>
<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>
</div>

View File

@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { FailedEventsComponent } from './failed-events.component';
const routes: Routes = [
{
path: '',
component: FailedEventsComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class FailedEventsRoutingModule {}

View File

@ -1,70 +0,0 @@
<div class="max-width-container">
<h1 class="failed-events-title">{{ 'IAM.FAILEDEVENTS.TITLE' | translate }}</h1>
<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>
</div>

View File

@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { IamViewsComponent } from './iam-views.component';
const routes: Routes = [
{
path: '',
component: IamViewsComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class IamViewsRoutingModule {}

View File

@ -1,42 +0,0 @@
<div class="max-width-container">
<h1 class="views-title">{{ 'IAM.VIEWS.TITLE' | translate }}</h1>
<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>
</div>

View File

@ -1,7 +0,0 @@
.views-title {
margin: 2rem 0 0 0;
}
.views-desc {
font-size: 14px;
}

View File

@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { InstanceSettingsComponent } from './instance-settings.component';
const routes: Routes = [
{
path: '',
component: InstanceSettingsComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class InstanceSettingsRoutingModule {}

View File

@ -1,18 +0,0 @@
<div class="max-width-container">
<div class="settings-top-view">
<div>
<div class="settings-title-row">
<h1>{{ 'SETTINGS.INSTANCE.TITLE' | translate }}</h1>
</div>
<p class="desc cnsl-secondary-text">{{ 'SETTINGS.INSTANCE.DESCRIPTION' | translate }}</p>
</div>
<span class="fill-space"></span>
</div>
<ng-container *ngIf="settingsList | async as list">
<cnsl-settings-list
[selectedId]="id"
[serviceType]="PolicyComponentServiceType.ADMIN"
[settingsList]="list"
></cnsl-settings-list>
</ng-container>
</div>

View File

@ -1,34 +0,0 @@
.settings-top-view {
display: flex;
align-items: center;
padding-top: 2rem;
.settings-title-row {
display: flex;
align-items: center;
h1 {
margin: 0;
}
a i {
font-size: 1.2rem;
height: 1.2rem;
line-height: 1.2rem;
}
}
.fill-space {
flex: 1;
}
.actions {
display: flex;
align-items: center;
}
}
.desc {
margin-bottom: 2rem;
font-size: 14px;
}

View File

@ -1,24 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InstanceSettingsComponent } from './instance-settings.component';
describe('InstanceSettingsComponent', () => {
let component: InstanceSettingsComponent;
let fixture: ComponentFixture<InstanceSettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [InstanceSettingsComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InstanceSettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,97 +0,0 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Observable, of, Subject, takeUntil } from 'rxjs';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import {
BRANDING,
COMPLEXITY,
DOMAIN,
LANGUAGES,
IDP,
LOCKOUT,
LOGIN,
LOGINTEXTS,
MESSAGETEXTS,
NOTIFICATIONS,
OIDC,
PRIVACYPOLICY,
SECRETS,
SECURITY,
SMS_PROVIDER,
SMTP_PROVIDER,
} from '../../modules/settings-list/settings';
@Component({
selector: 'cnsl-instance-settings',
templateUrl: './instance-settings.component.html',
styleUrls: ['./instance-settings.component.scss'],
})
export class InstanceSettingsComponent implements OnInit, OnDestroy {
public id: string = '';
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public defaultSettingsList: SidenavSetting[] = [
// notifications
// { showWarn: true, ...NOTIFICATIONS },
NOTIFICATIONS,
SMTP_PROVIDER,
SMS_PROVIDER,
// login
LOGIN,
IDP,
COMPLEXITY,
LOCKOUT,
DOMAIN,
// appearance
BRANDING,
MESSAGETEXTS,
LOGINTEXTS,
// others
PRIVACYPOLICY,
LANGUAGES,
OIDC,
SECRETS,
SECURITY,
];
public settingsList: Observable<SidenavSetting[]> = of([]);
private destroy$: Subject<void> = new Subject();
constructor(
breadcrumbService: BreadcrumbService,
activatedRoute: ActivatedRoute,
public authService: GrpcAuthService,
) {
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.INSTANCE,
name: 'Instance',
routerLink: ['/instance'],
}),
];
breadcrumbService.setBreadcrumb(breadcrumbs);
activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params: Params) => {
const { id } = params;
if (id) {
this.id = id;
}
});
}
ngOnInit(): void {
this.settingsList = this.authService.isAllowedMapper(
this.defaultSettingsList,
(setting) => setting.requiredRoles.admin || [],
);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -1,14 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { SettingsListModule } from 'src/app/modules/settings-list/settings-list.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { InstanceSettingsRoutingModule } from './instance-settings-routing.module';
import { InstanceSettingsComponent } from './instance-settings.component';
@NgModule({
declarations: [InstanceSettingsComponent],
imports: [CommonModule, InstanceSettingsRoutingModule, SettingsListModule, HasRolePipeModule, TranslateModule],
})
export default class InstanceSettingsModule {}

View File

@ -7,29 +7,45 @@
[hasContributors]="true"
stateTooltip="{{ 'INSTANCE.STATE.' + instance?.state | translate }}"
>
<cnsl-contributors
topContributors
[totalResult]="totalMemberResult"
[loading]="loading$ | async"
[membersSubject]="membersSubject"
title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}"
(addClicked)="openAddMember()"
(showDetailClicked)="showDetail()"
(refreshClicked)="loadMembers()"
[disabled]="(['iam.member.write'] | hasRole | async) === false"
>
</cnsl-contributors>
<div topContributors class="instance-action-wrapper">
<a
mat-raised-button
color="primary"
*ngIf="customerPortalLink$ | async as customerPortalLink"
class="portal-link external-link"
[href]="customerPortalLink"
target="_blank"
rel="noreferrer"
>
<div class="cnsl-action-button">
<span class="portal-span">{{ 'MENU.CUSTOMERPORTAL' | translate }}</span>
<i class="las la-external-link-alt"></i>
</div>
</a>
<cnsl-contributors
[totalResult]="totalMemberResult"
[loading]="loading$ | async"
[membersSubject]="membersSubject"
title="{{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.TITLEDESC' | translate }}"
(addClicked)="openAddMember()"
(showDetailClicked)="showDetail()"
(refreshClicked)="loadMembers()"
[disabled]="(['iam.member.write'] | hasRole | async) === false"
>
</cnsl-contributors>
</div>
<cnsl-info-row topContent *ngIf="instance" [instance]="instance"></cnsl-info-row>
</cnsl-top-view>
<div class="max-width-container">
<h2 class="instance-table-title">{{ 'ORG.LIST.TITLE' | translate }}</h2>
<p class="instance-table-desc cnsl-secondary-text">{{ 'ORG.LIST.DESCRIPTION' | translate }}</p>
<cnsl-org-table></cnsl-org-table>
<cnsl-settings-grid [type]="PolicyComponentServiceType.ADMIN"></cnsl-settings-grid>
<div class="instance-settings-wrapper">
<ng-container *ngIf="settingsList | async as list">
<cnsl-settings-list
[selectedId]="id"
[serviceType]="PolicyComponentServiceType.ADMIN"
[settingsList]="list"
></cnsl-settings-list>
</ng-container>
</div>
</div>

View File

@ -11,6 +11,23 @@
margin-top: 2rem;
}
.instance-action-wrapper {
display: flex;
align-items: center;
.portal-link {
margin-right: 1rem;
.portal-span {
margin-right: 0.5rem;
}
}
}
.instance-table-desc {
font-size: 14px;
}
.instance-settings-wrapper {
margin-top: 2rem;
}

View File

@ -1,8 +1,8 @@
import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { BehaviorSubject, from, Observable, of, Subject } from 'rxjs';
import { catchError, finalize, map, takeUntil } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { InstanceDetail, State } from 'src/app/proto/generated/zitadel/instance_pb';
@ -11,7 +11,31 @@ import { User } from 'src/app/proto/generated/zitadel/user_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';
import {
BRANDING,
COMPLEXITY,
DOMAIN,
LANGUAGES,
IDP,
LOCKOUT,
LOGIN,
LOGINTEXTS,
MESSAGETEXTS,
NOTIFICATIONS,
OIDC,
PRIVACYPOLICY,
SECRETS,
SECURITY,
SMS_PROVIDER,
SMTP_PROVIDER,
VIEWS,
FAILEDEVENTS,
EVENTS,
ORGANIZATIONS,
} from '../../modules/settings-list/settings';
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { EnvironmentService } from 'src/app/services/environment.service';
@Component({
selector: 'cnsl-instance',
templateUrl: './instance.component.html',
@ -25,12 +49,51 @@ export class InstanceComponent {
public totalMemberResult: number = 0;
public membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]);
public State: any = State;
public id: string = '';
public defaultSettingsList: SidenavSetting[] = [
ORGANIZATIONS,
// notifications
// { showWarn: true, ...NOTIFICATIONS },
NOTIFICATIONS,
SMTP_PROVIDER,
SMS_PROVIDER,
// login
LOGIN,
IDP,
COMPLEXITY,
LOCKOUT,
DOMAIN,
// appearance
BRANDING,
MESSAGETEXTS,
LOGINTEXTS,
// storage
VIEWS,
EVENTS,
FAILEDEVENTS,
// others
PRIVACYPOLICY,
LANGUAGES,
OIDC,
SECRETS,
SECURITY,
];
public settingsList: Observable<SidenavSetting[]> = of([]);
public customerPortalLink$ = this.envService.env.pipe(map((env) => env.customer_portal));
private destroy$: Subject<void> = new Subject();
constructor(
public adminService: AdminService,
private dialog: MatDialog,
private toast: ToastService,
breadcrumbService: BreadcrumbService,
private router: Router,
private authService: GrpcAuthService,
private envService: EnvironmentService,
activatedRoute: ActivatedRoute,
) {
this.loadMembers();
@ -52,6 +115,13 @@ export class InstanceComponent {
.catch((error) => {
this.toast.showError(error);
});
activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params: Params) => {
const { id } = params;
if (id) {
this.id = id;
}
});
}
public loadMembers(): void {
@ -113,4 +183,16 @@ export class InstanceComponent {
public showDetail(): void {
this.router.navigate(['/instance', 'members']);
}
ngOnInit(): void {
this.settingsList = this.authService.isAllowedMapper(
this.defaultSettingsList,
(setting) => setting.requiredRoles.admin || [],
);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -29,6 +29,7 @@ import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/
import { IamRoutingModule } from './instance-routing.module';
import { InstanceComponent } from './instance.component';
import { SettingsListModule } from 'src/app/modules/settings-list/settings-list.module';
@NgModule({
declarations: [InstanceComponent],
@ -61,6 +62,7 @@ import { InstanceComponent } from './instance.component';
TimestampToDatePipeModule,
RefreshTableModule,
HasRolePipeModule,
SettingsListModule,
MatSortModule,
SettingsGridModule,
],

View File

@ -21,6 +21,7 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { LanguagesService } from '../../services/languages.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
@Component({
selector: 'cnsl-org-create',
@ -59,6 +60,7 @@ export class OrgCreateComponent {
private _location: Location,
private fb: UntypedFormBuilder,
private mgmtService: ManagementService,
private authService: GrpcAuthService,
public langSvc: LanguagesService,
breadcrumbService: BreadcrumbService,
) {
@ -101,6 +103,7 @@ export class OrgCreateComponent {
this.adminService
.SetUpOrg(createOrgRequest, humanRequest)
.then(() => {
this.authService.revalidateOrgs();
this.router.navigate(['/orgs']);
})
.catch((error) => {
@ -191,6 +194,7 @@ export class OrgCreateComponent {
this.mgmtService
.addOrg(this.name.value)
.then(() => {
this.authService.revalidateOrgs();
this.router.navigate(['/orgs']);
})
.catch((error) => {

View File

@ -9,5 +9,6 @@ import { OrgListComponent } from './org-list.component';
@NgModule({
declarations: [OrgListComponent],
imports: [CommonModule, OrgListRoutingModule, OrgTableModule, TranslateModule],
exports: [OrgListComponent],
})
export default class OrgListModule {}

View File

@ -140,14 +140,13 @@ export class GrpcAuthService {
public zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
public readonly fetchedZitadelPermissions: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private cachedOrgs: Org.AsObject[] = [];
public cachedOrgs: BehaviorSubject<Org.AsObject[]> = new BehaviorSubject<Org.AsObject[]>([]);
private cachedLabelPolicies: { [orgId: string]: LabelPolicy.AsObject } = {};
constructor(
private readonly grpcService: GrpcService,
private oauthService: OAuthService,
private storage: StorageService,
themeService: ThemeService,
) {
this.zitadelPermissions$.subscribe(this.zitadelPermissions);
@ -221,15 +220,14 @@ export class GrpcAuthService {
public async getActiveOrg(id?: string): Promise<Org.AsObject> {
if (id) {
const find = this.cachedOrgs.find((tmp) => tmp.id === id);
const find = this.cachedOrgs.getValue().find((tmp) => tmp.id === id);
if (find) {
this.setActiveOrg(find);
return Promise.resolve(find);
} else {
const orgs = (await this.listMyProjectOrgs(10, 0)).resultList;
this.cachedOrgs = orgs;
const toFind = this.cachedOrgs.find((tmp) => tmp.id === id);
this.cachedOrgs.next(orgs);
const toFind = orgs.find((tmp) => tmp.id === id);
if (toFind) {
this.setActiveOrg(toFind);
return Promise.resolve(toFind);
@ -238,10 +236,10 @@ export class GrpcAuthService {
}
}
} else {
let orgs = this.cachedOrgs;
let orgs = this.cachedOrgs.getValue();
if (orgs.length === 0) {
orgs = (await this.listMyProjectOrgs()).resultList;
this.cachedOrgs = orgs;
this.cachedOrgs.next(orgs);
}
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization, StorageLocation.local);
@ -373,6 +371,11 @@ export class GrpcAuthService {
return this.grpcService.auth.listMyAuthFactors(new ListMyAuthFactorsRequest(), null).then((resp) => resp.toObject());
}
public async revalidateOrgs() {
const orgs = (await this.listMyProjectOrgs()).resultList;
this.cachedOrgs.next(orgs);
}
public listMyProjectOrgs(
limit?: number,
offset?: number,

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "Инстанция",
"INSTANCE": "настройките по подразбиране",
"DASHBOARD": "Табло",
"PERSONAL_INFO": "Лична информация",
"DOCUMENTATION": "Документация",
@ -1017,6 +1017,7 @@
"DESCRIPTION": "Тези настройки разширяват и презаписват настройките на вашия екземпляр."
},
"LIST": {
"ORGS": "Организации",
"LANGUAGES": "Езици",
"LOGIN": "Поведение при влизане и сигурност",
"LOCKOUT": "Блокиране",
@ -1034,15 +1035,20 @@
"PRIVACYPOLICY": "Политика за бедност",
"OIDC": "Живот и изтичане на OIDC Token",
"SECRETS": "Тайна поява",
"SECURITY": "Настройки на сигурността"
"SECURITY": "Настройки на сигурността",
"EVENTS": "Събития",
"FAILEDEVENTS": "Неуспешни събития",
"VIEWS": "Изгледи"
},
"GROUPS": {
"GENERAL": "Главна информация",
"NOTIFICATIONS": "Известия",
"LOGIN": "Вход и достъп",
"DOMAIN": "Домейн",
"TEXTS": "Текстове и езици",
"APPEARANCE": "Външен вид",
"OTHER": "други"
"OTHER": "други",
"STORAGE": "Съхранение"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "Instance",
"INSTANCE": "Výchozí nastavení",
"DASHBOARD": "Domů",
"PERSONAL_INFO": "Osobní informace",
"DOCUMENTATION": "Dokumentace",
@ -1024,6 +1024,7 @@
"DESCRIPTION": "Tato nastavení rozšiřují a přepisují nastavení vaší instance."
},
"LIST": {
"ORGS": "Organizace",
"LANGUAGES": "Jazyky",
"LOGIN": "Chování při přihlášení a bezpečnost",
"LOCKOUT": "Blokování",
@ -1041,15 +1042,20 @@
"PRIVACYPOLICY": "Zásady ochrany osobních údajů",
"OIDC": "Životnost a expirace OIDC tokenu",
"SECRETS": "Generátor tajemství",
"SECURITY": "Bezpečnostní nastavení"
"SECURITY": "Bezpečnostní nastavení",
"EVENTS": "Události",
"FAILEDEVENTS": "Selhané události",
"VIEWS": "Pohledy"
},
"GROUPS": {
"GENERAL": "Obecné informace",
"NOTIFICATIONS": "Oznámení",
"LOGIN": "Přihlášení a přístup",
"DOMAIN": "Doména",
"TEXTS": "Texty a jazyky",
"APPEARANCE": "Vzhled",
"OTHER": "Ostatní"
"OTHER": "Ostatní",
"STORAGE": "Data"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "Instanz",
"INSTANCE": "Standardeinstellungen",
"DASHBOARD": "Startseite",
"PERSONAL_INFO": "Persönliche Informationen",
"DOCUMENTATION": "Dokumentation",
@ -1023,6 +1023,7 @@
"DESCRIPTION": "Diese Einstellungen erweitern bzw. überschreiben die Einstellungen Ihrer Instanz."
},
"LIST": {
"ORGS": "Organisationen",
"LANGUAGES": "Sprachen",
"LOGIN": "Loginverhalten und Sicherheit",
"LOCKOUT": "Sperrmechanismen",
@ -1040,15 +1041,20 @@
"PRIVACYPOLICY": "Datenschutzrichtlinie",
"OIDC": "OIDC Token Lifetime und Expiration",
"SECRETS": "Secret Generator",
"SECURITY": "Sicherheitseinstellungen"
"SECURITY": "Sicherheitseinstellungen",
"EVENTS": "Events",
"FAILEDEVENTS": "Fehlerhafte Events",
"VIEWS": "Views"
},
"GROUPS": {
"GENERAL": "Allgemein",
"NOTIFICATIONS": "Benachrichtigungen",
"LOGIN": "Login und Zugriff",
"DOMAIN": "Domain",
"TEXTS": "Texte und Sprachen",
"APPEARANCE": "Erscheinungsbild",
"OTHER": "Anderes"
"OTHER": "Anderes",
"STORAGE": "Speicher"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "Instance",
"INSTANCE": "Default settings",
"DASHBOARD": "Home",
"PERSONAL_INFO": "Personal Information",
"DOCUMENTATION": "Documentation",
@ -1024,6 +1024,7 @@
"DESCRIPTION": "These settings extend and overwrite your instance settings."
},
"LIST": {
"ORGS": "Organizations",
"LANGUAGES": "Languages",
"LOGIN": "Login Behavior and Security",
"LOCKOUT": "Lockout",
@ -1041,15 +1042,20 @@
"PRIVACYPOLICY": "Privacy Policy",
"OIDC": "OIDC Token lifetime and expiration",
"SECRETS": "Secret Generator",
"SECURITY": "Security settings"
"SECURITY": "Security settings",
"EVENTS": "Events",
"FAILEDEVENTS": "Failed Events",
"VIEWS": "Views"
},
"GROUPS": {
"GENERAL": "General Information",
"NOTIFICATIONS": "Notifications",
"LOGIN": "Login and Access",
"DOMAIN": "Domain",
"TEXTS": "Texts and Languages",
"APPEARANCE": "Appearance",
"OTHER": "Other"
"OTHER": "Other",
"STORAGE": "Storage"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "Instancia",
"INSTANCE": "configuración por defecto",
"DASHBOARD": "Inicio",
"PERSONAL_INFO": "Información personal",
"DOCUMENTATION": "Documentación",
@ -1025,6 +1025,7 @@
"DESCRIPTION": "Estas configuraciones amplían y sobrescriben tus configuraciones de instancia."
},
"LIST": {
"ORGS": "Organizaciones",
"LANGUAGES": "Idiomas",
"LOGIN": "Comportamiento del inicio de sesión y de la seguridad",
"LOCKOUT": "Bloqueo",
@ -1042,15 +1043,20 @@
"PRIVACYPOLICY": "Política de privacidad",
"OIDC": "OIDC Token lifetime and expiration",
"SECRETS": "Apariencia del secreto",
"SECURITY": "Ajustes de seguridad"
"SECURITY": "Ajustes de seguridad",
"EVENTS": "Eventos",
"FAILEDEVENTS": "Eventos fallidos",
"VIEWS": "Vistas"
},
"GROUPS": {
"GENERAL": "General",
"NOTIFICATIONS": "Notificaciones",
"LOGIN": "Inicio de sesión y acceso",
"DOMAIN": "Dominio",
"TEXTS": "Textos e idiomas",
"APPEARANCE": "Apariencia",
"OTHER": "Otros"
"OTHER": "Otros",
"STORAGE": "Datos"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "Instance",
"INSTANCE": "paramètres par défaut",
"DASHBOARD": "Accueil",
"PERSONAL_INFO": "Informations personnelles",
"DOCUMENTATION": "Documentation",
@ -1023,6 +1023,7 @@
"DESCRIPTION": "Ces paramètres étendent et remplacent les paramètres de votre instance."
},
"LIST": {
"ORGS": "Organisations",
"LANGUAGES": "Langues",
"LOGIN": "Comportement de connexion et sécurité",
"LOCKOUT": "Verrouillage",
@ -1040,15 +1041,20 @@
"PRIVACYPOLICY": "Politique de confidentialité",
"OIDC": "Durée de vie et expiration des jetons OIDC",
"SECRETS": "Apparence secrète",
"SECURITY": "Paramètres de sécurité"
"SECURITY": "Paramètres de sécurité",
"EVENTS": "Événements",
"FAILEDEVENTS": "Événements échoués",
"VIEWS": "Vues"
},
"GROUPS": {
"GENERAL": "Général",
"NOTIFICATIONS": "Notifications",
"LOGIN": "Connexion et accès",
"DOMAIN": "Domaine",
"TEXTS": "Textes et langues",
"APPEARANCE": "Apparence",
"OTHER": "Autres"
"OTHER": "Autres",
"STORAGE": "Stockage"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "Istanza",
"INSTANCE": "Impostazioni default",
"DASHBOARD": "Pagina iniziale",
"PERSONAL_INFO": "Informazioni personali",
"DOCUMENTATION": "Documentazione",
@ -1023,6 +1023,7 @@
"DESCRIPTION": "Queste impostazioni si applicheranno alla organizzazione corrente."
},
"LIST": {
"ORGS": "Organizzazioni",
"LANGUAGES": "Lingue",
"LOGIN": "Comportamento login e sicurezza",
"LOCKOUT": "Meccanismi di bloccaggio",
@ -1040,15 +1041,20 @@
"PRIVACYPOLICY": "Informativa sulla privacy e TOS",
"OIDC": "OIDC Token lifetime e scadenza",
"SECRETS": "Aspetto dei segreti",
"SECURITY": "Impostazioni di sicurezza"
"SECURITY": "Impostazioni di sicurezza",
"EVENTS": "Eventi",
"FAILEDEVENTS": "Eventi falliti",
"VIEWS": "Views"
},
"GROUPS": {
"GENERAL": "Generale",
"NOTIFICATIONS": "Notifiche",
"LOGIN": "Accesso e login",
"DOMAIN": "Dominio",
"TEXTS": "Testi e lingue",
"APPEARANCE": "Aspetto",
"OTHER": "Altro"
"OTHER": "Altro",
"STORAGE": "Dati"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "インスタンス",
"INSTANCE": "デフォルトの設定",
"DASHBOARD": "ホーム",
"PERSONAL_INFO": "個人情報",
"DOCUMENTATION": "ドキュメント",
@ -1024,6 +1024,7 @@
"DESCRIPTION": "これらの設定は、インスタンス設定を拡張・上書きします。"
},
"LIST": {
"ORGS": "組織",
"LANGUAGES": "一般設定",
"LOGIN": "ログイン動作とセキュリティ",
"LOCKOUT": "ロックアウト",
@ -1041,15 +1042,20 @@
"PRIVACYPOLICY": "プライバシーポリシー",
"OIDC": "OIDCトークンのライフタイムと有効期限",
"SECRETS": "シークレット設定",
"SECURITY": "セキュリティ設定"
"SECURITY": "セキュリティ設定",
"EVENTS": "イベント",
"FAILEDEVENTS": "失敗したイベント",
"VIEWS": "ビュー"
},
"GROUPS": {
"GENERAL": "一般",
"NOTIFICATIONS": "通知",
"LOGIN": "ログインとアクセス",
"DOMAIN": "ドメイン",
"TEXTS": "テキストと言語",
"APPEARANCE": "設定",
"OTHER": "その他"
"OTHER": "その他",
"STORAGE": "ストレージ"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "Инстанца",
"INSTANCE": "стандардни поставки",
"DASHBOARD": "Дома",
"PERSONAL_INFO": "Лични информации",
"DOCUMENTATION": "Документација",
@ -1025,6 +1025,7 @@
"DESCRIPTION": "Овие подесувања ги прошируваат и препишуваат подесувањата на вашата инстанца."
},
"LIST": {
"ORGS": "Организации",
"LANGUAGES": "Општо",
"LOGIN": "Правила и безбедност при најава",
"LOCKOUT": "Забрана на пристап",
@ -1042,15 +1043,20 @@
"PRIVACYPOLICY": "Политика за приватност",
"OIDC": "OIDC времетраење и истекување на токени",
"SECRETS": "Изглед на тајни",
"SECURITY": "Подесувања за безбедност"
"SECURITY": "Подесувања за безбедност",
"EVENTS": "Настани",
"FAILEDEVENTS": "Неуспешни настани",
"VIEWS": "Прегледи"
},
"GROUPS": {
"GENERAL": "Општи информации",
"NOTIFICATIONS": "Известувања",
"LOGIN": "Најава и пристап",
"DOMAIN": "Домен",
"TEXTS": "Текстови и јазици",
"APPEARANCE": "Изглед",
"OTHER": "Друго"
"OTHER": "Друго",
"STORAGE": "складирање"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "Instantie",
"INSTANCE": "Standaard instellingen",
"DASHBOARD": "Home",
"PERSONAL_INFO": "Persoonlijke informatie",
"DOCUMENTATION": "Documentatie",
@ -1024,6 +1024,7 @@
"DESCRIPTION": "Deze instellingen breiden uw instantie instellingen uit en overschrijven deze."
},
"LIST": {
"ORGS": "Organisaties",
"LANGUAGES": "Talen",
"LOGIN": "Login Gedrag en Beveiliging",
"LOCKOUT": "Lockout",
@ -1041,15 +1042,20 @@
"PRIVACYPOLICY": "Privacybeleid",
"OIDC": "OIDC Token levensduur en vervaldatum",
"SECRETS": "Secret Generator",
"SECURITY": "Beveiligingsinstellingen"
"SECURITY": "Beveiligingsinstellingen",
"EVENTS": "Evenementen",
"FAILEDEVENTS": "Mislukte evenementen",
"VIEWS": "Weergaves"
},
"GROUPS": {
"GENERAL": "Algemeen",
"NOTIFICATIONS": "Notificaties",
"LOGIN": "Login en Toegang",
"DOMAIN": "Domein",
"TEXTS": "Teksten en Talen",
"APPEARANCE": "Verschijning",
"OTHER": "Andere"
"OTHER": "Andere",
"STORAGE": "opslag"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "Instancja",
"INSTANCE": "Ustawienia domyślne",
"DASHBOARD": "Strona główna",
"PERSONAL_INFO": "Informacje Osobiste",
"DOCUMENTATION": "Dokumentacja",
@ -1023,6 +1023,7 @@
"DESCRIPTION": "Te ustawienia rozszerzają i nadpisują ustawienia instancji."
},
"LIST": {
"ORGS": "Organizacje",
"LANGUAGES": "Języki",
"LOGIN": "Zachowanie logowania i bezpieczeństwo",
"LOCKOUT": "Blokada",
@ -1040,15 +1041,20 @@
"PRIVACYPOLICY": "Polityka prywatności",
"OIDC": "Czas trwania tokenów OIDC i wygaśnięcie",
"SECRETS": "Wygląd sekretów",
"SECURITY": "Ustawienia bezpieczeństwa"
"SECURITY": "Ustawienia bezpieczeństwa",
"EVENTS": "Zdarzenia",
"FAILEDEVENTS": "Nieudane Zdarzenia",
"VIEWS": "Widoki"
},
"GROUPS": {
"GENERAL": "Informacje ogólne",
"NOTIFICATIONS": "Powiadomienia",
"LOGIN": "Logowanie i dostęp",
"DOMAIN": "Domena",
"TEXTS": "Teksty i języki",
"APPEARANCE": "Wygląd",
"OTHER": "Inne"
"OTHER": "Inne",
"STORAGE": "składowanie"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "Instância",
"INSTANCE": "Configurações padrão",
"DASHBOARD": "Início",
"PERSONAL_INFO": "Informações Pessoais",
"DOCUMENTATION": "Documentação",
@ -1025,6 +1025,7 @@
"DESCRIPTION": "Essas configurações estendem e sobrescrevem as configurações da sua instância."
},
"LIST": {
"ORGS": "Organizações",
"LANGUAGES": "Idiomas",
"LOGIN": "Comportamento de Login e Segurança",
"LOCKOUT": "Bloqueio",
@ -1042,15 +1043,20 @@
"PRIVACYPOLICY": "Política de Privacidade",
"OIDC": "Tempo de Vida e Expiração do Token OIDC",
"SECRETS": "Aparência de Segredo",
"SECURITY": "Configurações de Segurança"
"SECURITY": "Configurações de Segurança",
"EVENTS": "Eventos",
"FAILEDEVENTS": "Eventos com Falha",
"VIEWS": "Visualizações"
},
"GROUPS": {
"GENERAL": "Geral",
"NOTIFICATIONS": "Notificações",
"LOGIN": "Login e Acesso",
"DOMAIN": "Domínio",
"TEXTS": "Textos e Idiomas",
"APPEARANCE": "Aparência",
"OTHER": "Outro"
"OTHER": "Outro",
"STORAGE": "armazenar"
}
},
"SETTING": {

View File

@ -85,7 +85,7 @@
}
},
"MENU": {
"INSTANCE": "Система",
"INSTANCE": "настройки по умолчанию",
"DASHBOARD": "Главная",
"PERSONAL_INFO": "Персональная информация",
"DOCUMENTATION": "Документация",
@ -1020,6 +1020,7 @@
"DESCRIPTION": "Эти настройки расширяют и перезаписывают настройки вашего экземпляра."
},
"LIST": {
"ORGS": "Организации",
"LANGUAGES": "Языки",
"LOGIN": "Поведение при входе и безопасность",
"LOCKOUT": "Блокировка",
@ -1034,15 +1035,20 @@
"PRIVACYPOLICY": "Политика конфиденциальности",
"OIDC": "Срок действия и срок действия токена OIDC",
"SECRETS": "Тайное появление",
"SECURITY": "Настройки безопасности"
"SECURITY": "Настройки безопасности",
"EVENTS": "События",
"FAILEDEVENTS": "Неудачные события",
"VIEWS": "Отображение"
},
"GROUPS": {
"GENERAL": "Общая информация",
"NOTIFICATIONS": "Уведомления",
"LOGIN": "Вход и доступ",
"DOMAIN": "Домен",
"TEXTS": "Тексты и языки",
"APPEARANCE": "Внешний вид",
"OTHER": "Другой"
"OTHER": "Другой",
"STORAGE": "хранилище"
}
},
"SETTING": {

View File

@ -90,7 +90,7 @@
}
},
"MENU": {
"INSTANCE": "实例",
"INSTANCE": "默认设置",
"DASHBOARD": "首页",
"PERSONAL_INFO": "个人信息",
"DOCUMENTATION": "文档",
@ -1023,6 +1023,7 @@
"DESCRIPTION": "这些设置将扩展或覆盖您的实例设置。"
},
"LIST": {
"ORGS": "组织",
"LANGUAGES": "语言",
"LOGIN": "登录行为和安全",
"LOCKOUT": "安全锁策略",
@ -1040,15 +1041,20 @@
"PRIVACYPOLICY": "隐私政策",
"OIDC": "OIDC 令牌有效期和过期时间",
"SECRETS": "验证码外观",
"SECURITY": "安全设置"
"SECURITY": "安全设置",
"EVENTS": "活动",
"FAILEDEVENTS": "失败事件",
"VIEWS": "数据表"
},
"GROUPS": {
"GENERAL": "通用",
"NOTIFICATIONS": "通知",
"LOGIN": "登录和访问",
"DOMAIN": "域名",
"TEXTS": "文本和语言",
"APPEARANCE": "外观",
"OTHER": "其他"
"OTHER": "其他",
"STORAGE": "贮存"
}
},
"SETTING": {

View File

@ -57,7 +57,7 @@
@import 'src/app/modules/shortcuts/shortcuts.component.scss';
@import 'src/app/modules/policies/idp-settings/idp-settings.component.scss';
@import 'src/app/modules/project-role-chip/project-role-chip.component';
@import 'src/app/pages/events/events.component';
@import 'src/app/modules/events/events.component';
@import 'src/app/pages/home/home.component.scss';
@import 'src/app/modules/policies/security-policy/security-policy.component.scss';
@import 'src/app/modules/idp-table/idp-table.component.scss';

View File

@ -1,6 +1,6 @@
The login policy can be configured on two levels. Once as default on the instance and this can be overwritten for each organization.
The only difference is where you configure it. Go either to the settings page of a specific organization or to the settings page of your instance.
Instance: $YOUR-DOMAIN/ui/console/settings?id=general
Instance: $YOUR-DOMAIN/ui/console/instance?id=general
Organization: Choose the organization in the menu and go to $YOUR-DOMAIN/ui/console/org-settings?id=login
1. Go to the Settings

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Some files were not shown because too many files have changed in this diff Show More