feat(console-v2): save table filters as queryparams, smtp update (#3624)

* show warn for missing smtp

* org table, failed events, views table fallback, org table filters

* log notification providers, user filter, copy to clip fix

* lint

Co-authored-by: Livio Amstutz <[email protected]>
This commit is contained in:
Max Peintner
2022-05-13 14:19:06 +00:00
committed by GitHub
co-authored by Livio Amstutz
parent a674f99c2d
commit d0c1ad2c69
43 changed files with 669 additions and 227 deletions
@@ -1,20 +1,19 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterUserComponent } from './filter-user.component';
import { FilterOrgComponent } from './filter-org.component';
describe('FilterUserComponent', () => {
let component: FilterUserComponent;
let fixture: ComponentFixture<FilterUserComponent>;
describe('FilterOrgComponent', () => {
let component: FilterOrgComponent;
let fixture: ComponentFixture<FilterOrgComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FilterUserComponent ]
})
.compileComponents();
declarations: [FilterOrgComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterUserComponent);
fixture = TestBed.createComponent(FilterOrgComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -1,5 +1,7 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { OrgNameQuery, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { UserNameQuery } from 'src/app/proto/generated/zitadel/user_pb';
@@ -15,13 +17,44 @@ enum SubQuery {
templateUrl: './filter-org.component.html',
styleUrls: ['./filter-org.component.scss'],
})
export class FilterOrgComponent extends FilterComponent {
export class FilterOrgComponent extends FilterComponent implements OnInit {
public SubQuery: any = SubQuery;
public searchQueries: OrgQuery[] = [];
public states: OrgState[] = [OrgState.ORG_STATE_ACTIVE, OrgState.ORG_STATE_INACTIVE];
constructor() {
super();
constructor(router: Router, protected route: ActivatedRoute) {
super(router, route);
}
ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe((params) => {
const { filter } = params;
if (filter) {
const stringifiedFilters = filter as string;
const filters: OrgQuery.AsObject[] = JSON.parse(stringifiedFilters) as OrgQuery.AsObject[];
const orgQueries = filters.map((filter) => {
if (filter.nameQuery) {
const orgQuery = new OrgQuery();
const orgNameQuery = new OrgNameQuery();
orgNameQuery.setName(filter.nameQuery.name);
orgNameQuery.setMethod(filter.nameQuery.method);
orgQuery.setNameQuery(orgNameQuery);
return orgQuery;
} else {
return undefined;
}
});
this.searchQueries = orgQueries.filter((q) => q !== undefined) as OrgQuery[];
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
// this.showFilter = true;
// this.filterOpen.emit(true);
}
});
}
public changeCheckbox(subquery: SubQuery, event: MatCheckboxChange) {
@@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { FilterModule } from '../filter/filter.module';
@@ -21,6 +22,7 @@ import { FilterOrgComponent } from './filter-org.component';
MatButtonModule,
MatIconModule,
TranslateModule,
RouterModule,
],
exports: [FilterOrgComponent],
})
@@ -1,5 +1,7 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { ProjectNameQuery, ProjectQuery, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
import { UserNameQuery } from 'src/app/proto/generated/zitadel/user_pb';
@@ -16,13 +18,43 @@ enum SubQuery {
templateUrl: './filter-project.component.html',
styleUrls: ['./filter-project.component.scss'],
})
export class FilterProjectComponent extends FilterComponent {
export class FilterProjectComponent extends FilterComponent implements OnInit {
public SubQuery: any = SubQuery;
public searchQueries: ProjectQuery[] = [];
public states: ProjectState[] = [ProjectState.PROJECT_STATE_ACTIVE, ProjectState.PROJECT_STATE_INACTIVE];
constructor() {
super();
constructor(router: Router, route: ActivatedRoute) {
super(router, route);
}
ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe((params) => {
const { filter } = params;
if (filter) {
const stringifiedFilters = filter as string;
const filters: ProjectQuery.AsObject[] = JSON.parse(stringifiedFilters) as ProjectQuery.AsObject[];
const projectQueries = filters.map((filter) => {
if (filter.nameQuery) {
const nameQuery = new ProjectNameQuery();
const projectQuery = new ProjectQuery();
nameQuery.setName(filter.nameQuery.name);
nameQuery.setMethod(filter.nameQuery.method);
projectQuery.setNameQuery(nameQuery);
return projectQuery;
} else {
return undefined;
}
});
this.searchQueries = projectQueries.filter((q) => q !== undefined) as ProjectQuery[];
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
// this.showFilter = true;
// this.filterOpen.emit(true);
}
});
}
public changeCheckbox(subquery: SubQuery, event: MatCheckboxChange) {
@@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { FilterModule } from '../filter/filter.module';
@@ -21,6 +22,7 @@ import { FilterProjectComponent } from './filter-project.component';
MatButtonModule,
MatIconModule,
TranslateModule,
RouterModule,
],
exports: [FilterProjectComponent],
})
@@ -1,82 +1,105 @@
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()">
<div class="filter-row" id="filtercomp">
<div class="name-query">
<mat-checkbox id="displayname" class="cb" [checked]="getSubFilter(SubQuery.DISPLAYNAME)"
(change)="changeCheckbox(SubQuery.DISPLAYNAME, $event )">{{'FILTER.DISPLAYNAME' | translate}}
<mat-checkbox
id="displayname"
class="cb"
[checked]="getSubFilter(SubQuery.DISPLAYNAME)"
(change)="changeCheckbox(SubQuery.DISPLAYNAME, $event)"
>{{ 'FILTER.DISPLAYNAME' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.DISPLAYNAME) as dnq">
<cnsl-form-field class="filter-select-method">
<mat-select [value]="dnq.getMethod()" (selectionChange)="setMethod(dnq, $event)">
<mat-option *ngFor="let method of methods" [value]="method">
{{ 'FILTER.METHODS.'+method | translate }}
{{ 'FILTER.METHODS.' + method | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="filter-input-value">
<input cnslInput name="value" [value]="dnq.getDisplayName()"
(change)="setValue(SubQuery.DISPLAYNAME, dnq, $event)" />
<input
cnslInput
name="value"
[value]="dnq.getDisplayName()"
(change)="setValue(SubQuery.DISPLAYNAME, dnq, $event)"
/>
</cnsl-form-field>
</div>
</div>
<div class="usernane-query">
<mat-checkbox id="username" class="cb" [checked]="getSubFilter(SubQuery.USERNAME)"
(change)="changeCheckbox(SubQuery.USERNAME, $event )">{{'FILTER.USERNAME' | translate}}
<mat-checkbox
id="username"
class="cb"
[checked]="getSubFilter(SubQuery.USERNAME)"
(change)="changeCheckbox(SubQuery.USERNAME, $event)"
>{{ 'FILTER.USERNAME' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.USERNAME) as unq">
<cnsl-form-field class="filter-select-method">
<mat-select [value]="unq.getMethod()" (selectionChange)="setMethod(unq, $event)">
<mat-option *ngFor="let method of methods" [value]="method">
{{ 'FILTER.METHODS.'+method | translate}}
{{ 'FILTER.METHODS.' + method | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="filter-input-value">
<input cnslInput name="value" [value]="unq.getUserName()"
(change)="setValue(SubQuery.USERNAME, unq, $event)" />
<input cnslInput name="value" [value]="unq.getUserName()" (change)="setValue(SubQuery.USERNAME, unq, $event)" />
</cnsl-form-field>
</div>
</div>
<div class="email-query">
<mat-checkbox id="email" class="cb" [checked]="getSubFilter(SubQuery.ORGNAME)"
(change)="changeCheckbox(SubQuery.ORGNAME, $event )">{{'FILTER.ORGNAME' | translate}}
<mat-checkbox
id="email"
class="cb"
[checked]="getSubFilter(SubQuery.ORGNAME)"
(change)="changeCheckbox(SubQuery.ORGNAME, $event)"
>{{ 'FILTER.ORGNAME' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.ORGNAME) as onq">
<cnsl-form-field class="filter-select-method">
<mat-select [value]="onq.getMethod()" (selectionChange)="setMethod(onq, $event)">
<mat-option *ngFor="let method of methods" [value]="method">
{{ 'FILTER.METHODS.'+method | translate}}
{{ 'FILTER.METHODS.' + method | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="filter-input-value">
<input cnslInput name="value" [value]="onq.getOrgName()" (change)="setValue(SubQuery.ORNAME, onq, $event)" />
<input cnslInput name="value" [value]="onq.getOrgName()" (change)="setValue(SubQuery.ORGNAME, onq, $event)" />
</cnsl-form-field>
</div>
</div>
<div class="email-query">
<mat-checkbox id="projectname" class="cb" [checked]="getSubFilter(SubQuery.PROJECTNAME)"
(change)="changeCheckbox(SubQuery.PROJECTNAME, $event )">{{'FILTER.PROJECTNAME' | translate}}
<mat-checkbox
id="projectname"
class="cb"
[checked]="getSubFilter(SubQuery.PROJECTNAME)"
(change)="changeCheckbox(SubQuery.PROJECTNAME, $event)"
>{{ 'FILTER.PROJECTNAME' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.PROJECTNAME) as pnq">
<cnsl-form-field class="filter-select-method">
<mat-select [value]="pnq.getMethod()" (selectionChange)="setMethod(pnq, $event)">
<mat-option *ngFor="let method of methods" [value]="method">
{{ 'FILTER.METHODS.'+method | translate}}
{{ 'FILTER.METHODS.' + method | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="filter-input-value">
<input cnslInput name="value" [value]="pnq.getProjectName()"
(change)="setValue(SubQuery.PROJECTNAME, pnq, $event)" />
<input
cnslInput
name="value"
[value]="pnq.getProjectName()"
(change)="setValue(SubQuery.PROJECTNAME, pnq, $event)"
/>
</cnsl-form-field>
</div>
</div>
</div>
</cnsl-filter>
</cnsl-filter>
@@ -1,12 +1,14 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import {
DisplayNameQuery,
UserGrantOrgNameQuery,
UserGrantProjectNameQuery,
UserGrantQuery,
UserNameQuery,
DisplayNameQuery,
UserGrantOrgNameQuery,
UserGrantProjectNameQuery,
UserGrantQuery,
UserNameQuery,
} from 'src/app/proto/generated/zitadel/user_pb';
import { FilterComponent } from '../filter/filter.component';
@@ -23,11 +25,68 @@ enum SubQuery {
templateUrl: './filter-user-grants.component.html',
styleUrls: ['./filter-user-grants.component.scss'],
})
export class FilterUserGrantsComponent extends FilterComponent {
export class FilterUserGrantsComponent extends FilterComponent implements OnInit {
public SubQuery: any = SubQuery;
public searchQueries: UserGrantQuery[] = [];
constructor() {
super();
constructor(router: Router, route: ActivatedRoute) {
super(router, route);
}
ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe((params) => {
const { filter } = params;
if (filter) {
const stringifiedFilters = filter as string;
const filters: UserGrantQuery.AsObject[] = JSON.parse(stringifiedFilters) as UserGrantQuery.AsObject[];
const userQueries = filters.map((filter) => {
if (filter.userNameQuery) {
const userGrantQuery = new UserGrantQuery();
const userNameQuery = new UserNameQuery();
userNameQuery.setUserName(filter.userNameQuery.userName);
userNameQuery.setMethod(filter.userNameQuery.method);
userGrantQuery.setUserNameQuery(userNameQuery);
return userGrantQuery;
} else if (filter.displayNameQuery) {
const userGrantQuery = new UserGrantQuery();
const displayNameQuery = new DisplayNameQuery();
displayNameQuery.setDisplayName(filter.displayNameQuery.displayName);
displayNameQuery.setMethod(filter.displayNameQuery.method);
userGrantQuery.setDisplayNameQuery(displayNameQuery);
return userGrantQuery;
} else if (filter.orgNameQuery) {
const userGrantQuery = new UserGrantQuery();
const orgNameQuery = new UserGrantOrgNameQuery();
orgNameQuery.setOrgName(filter.orgNameQuery.orgName);
orgNameQuery.setMethod(filter.orgNameQuery.method);
userGrantQuery.setOrgNameQuery(orgNameQuery);
return userGrantQuery;
} else if (filter.projectNameQuery) {
const userGrantQuery = new UserGrantQuery();
const projectNameQuery = new UserGrantProjectNameQuery();
projectNameQuery.setProjectName(filter.projectNameQuery.projectName);
userGrantQuery.setProjectNameQuery(projectNameQuery);
return userGrantQuery;
} else {
return undefined;
}
});
this.searchQueries = userQueries.filter((q) => q !== undefined) as UserGrantQuery[];
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
// this.showFilter = true;
// this.filterOpen.emit(true);
}
});
}
public changeCheckbox(subquery: SubQuery, event: MatCheckboxChange) {
@@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { FilterModule } from '../filter/filter.module';
@@ -21,6 +22,7 @@ import { FilterUserGrantsComponent } from './filter-user-grants.component';
MatButtonModule,
MatIconModule,
TranslateModule,
RouterModule,
],
exports: [FilterUserGrantsComponent],
})
@@ -1,13 +1,15 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import {
DisplayNameQuery,
EmailQuery,
SearchQuery as UserSearchQuery,
StateQuery,
UserNameQuery,
UserState,
DisplayNameQuery,
EmailQuery,
SearchQuery as UserSearchQuery,
StateQuery,
UserNameQuery,
UserState,
} from 'src/app/proto/generated/zitadel/user_pb';
import { FilterComponent } from '../filter/filter.component';
@@ -24,7 +26,7 @@ enum SubQuery {
templateUrl: './filter-user.component.html',
styleUrls: ['./filter-user.component.scss'],
})
export class FilterUserComponent extends FilterComponent {
export class FilterUserComponent extends FilterComponent implements OnInit {
public SubQuery: any = SubQuery;
public searchQueries: UserSearchQuery[] = [];
@@ -36,8 +38,64 @@ export class FilterUserComponent extends FilterComponent {
UserState.USER_STATE_LOCKED,
UserState.USER_STATE_SUSPEND,
];
constructor() {
super();
constructor(router: Router, route: ActivatedRoute) {
super(router, route);
}
ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe((params) => {
const { filter } = params;
if (filter) {
const stringifiedFilters = filter as string;
const filters: UserSearchQuery.AsObject[] = JSON.parse(stringifiedFilters) as UserSearchQuery.AsObject[];
const userQueries = filters.map((filter) => {
if (filter.userNameQuery) {
const userQuery = new UserSearchQuery();
const userNameQuery = new UserNameQuery();
userNameQuery.setUserName(filter.userNameQuery.userName);
userNameQuery.setMethod(filter.userNameQuery.method);
userQuery.setUserNameQuery(userNameQuery);
return userQuery;
} else if (filter.displayNameQuery) {
const userQuery = new UserSearchQuery();
const displayNameQuery = new DisplayNameQuery();
displayNameQuery.setDisplayName(filter.displayNameQuery.displayName);
displayNameQuery.setMethod(filter.displayNameQuery.method);
userQuery.setDisplayNameQuery(displayNameQuery);
return userQuery;
} else if (filter.emailQuery) {
const userQuery = new UserSearchQuery();
const emailQuery = new EmailQuery();
emailQuery.setEmailAddress(filter.emailQuery.emailAddress);
emailQuery.setMethod(filter.emailQuery.method);
userQuery.setEmailQuery(emailQuery);
return userQuery;
} else if (filter.stateQuery) {
const userQuery = new UserSearchQuery();
const stateQuery = new StateQuery();
stateQuery.setState(filter.stateQuery.state);
userQuery.setStateQuery(stateQuery);
return userQuery;
} else {
return undefined;
}
});
this.searchQueries = userQueries.filter((q) => q !== undefined) as UserSearchQuery[];
this.filterChanged.emit(this.filterCount ? this.searchQueries : undefined);
// this.showFilter = true;
// this.filterOpen.emit(true);
}
});
}
public changeCheckbox(subquery: SubQuery, event: MatCheckboxChange) {
@@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { FilterModule } from '../filter/filter.module';
@@ -19,6 +20,7 @@ import { FilterUserComponent } from './filter-user.component';
MatSelectModule,
MatCheckboxModule,
MatButtonModule,
RouterModule,
MatIconModule,
TranslateModule,
],
@@ -1,6 +1,7 @@
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay';
import { Component, EventEmitter, OnDestroy, Output } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Subject, takeUntil } from 'rxjs';
import { SearchQuery as MemberSearchQuery } from 'src/app/proto/generated/zitadel/member_pb';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { OrgQuery } from 'src/app/proto/generated/zitadel/org_pb';
@@ -10,6 +11,12 @@ import { SearchQuery as UserSearchQuery, UserGrantQuery } from 'src/app/proto/ge
import { ActionKeysType } from '../action-keys/action-keys.component';
type FilterSearchQuery = UserSearchQuery | MemberSearchQuery | UserGrantQuery | ProjectQuery | OrgQuery;
type FilterSearchQueryAsObject =
| UserSearchQuery.AsObject
| MemberSearchQuery.AsObject
| UserGrantQuery.AsObject
| ProjectQuery.AsObject
| OrgQuery.AsObject;
@Component({
selector: 'cnsl-filter',
@@ -23,6 +30,8 @@ export class FilterComponent implements OnDestroy {
@Output() public resetted: EventEmitter<void> = new EventEmitter();
@Output() public trigger: EventEmitter<void> = new EventEmitter();
private destroy$: Subject<void> = new Subject();
public filterCount$: BehaviorSubject<number> = new BehaviorSubject(0);
public showFilter: boolean = false;
@@ -51,5 +60,33 @@ export class FilterComponent implements OnDestroy {
public ngOnDestroy(): void {
this.filterCount$.complete();
this.destroy$.next();
this.destroy$.complete();
}
constructor(private router: Router, protected route: ActivatedRoute) {
const changes$ = this.filterChanged.asObservable();
changes$.pipe(takeUntil(this.destroy$)).subscribe((queries) => {
const filters: Array<FilterSearchQueryAsObject | {}> | undefined = queries
?.map((q) => q.toObject())
.map((query) =>
Object.keys(query).reduce((acc, key) => {
const _acc = acc;
if ((query as any)[key] !== undefined) (_acc as any)[key] = (query as any)[key];
return _acc as FilterSearchQueryAsObject;
}, {}),
);
if (filters && Object.keys(filters)) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: {
['filter']: JSON.stringify(filters),
},
queryParamsHandling: 'merge',
skipLocationChange: false,
});
}
});
}
}
@@ -1,8 +1,12 @@
<div class="info-section-row" [ngClass]="{'info': type === 'INFO', 'warn': type === 'WARN'}">
<i *ngIf="type === 'INFO'" class="icon las la-info"></i>
<i *ngIf="type === 'WARN'" class="icon las la-exclamation"></i>
<div
class="info-section-row"
[ngClass]="{ info: type === 'INFO', warn: type === 'WARN', alert: type === 'ALERT', fit: fitWidth }"
>
<i *ngIf="type === 'INFO'" class="icon las la-info"></i>
<i *ngIf="type === 'WARN'" class="icon las la-exclamation"></i>
<i *ngIf="type === 'ALERT'" class="icon las la-exclamation"></i>
<div class="info-section-content">
<ng-content></ng-content>
</div>
<div class="info-section-content">
<ng-content></ng-content>
</div>
</div>
@@ -10,23 +10,27 @@
.info-section-row {
display: flex;
border-radius: 4px;
padding: .5rem 0;
padding: 0.5rem 0;
padding-right: 1rem;
font-size: 14px;
margin: .5rem 0;
margin: 0.5rem 0;
&.fit {
width: fit-content;
}
.icon {
margin-right: 1rem;
height: 1.2rem;
line-height: 1.2rem;
font-size: 1.2rem;
margin-left: .5rem;
padding: .25rem 0;
margin-left: 0.5rem;
padding: 0.25rem 0;
}
.info-section-content {
flex: 1;
padding: .25rem 0;
padding: 0.25rem 0;
}
&.info {
@@ -55,5 +59,14 @@
color: map-get($foreground, warninfosection);
}
}
&.alert {
background-color: map-get($background, alertinfosection);
color: map-get($foreground, alertinfosection);
.icon {
color: map-get($foreground, alertinfosection);
}
}
}
}
@@ -4,6 +4,7 @@ export enum InfoSectionType {
INFO = 'INFO',
SUCCESS = 'SUCCESS',
WARN = 'WARN',
ALERT = 'ALERT',
}
@Component({
@@ -12,6 +13,6 @@ export enum InfoSectionType {
styleUrls: ['./info-section.component.scss'],
})
export class InfoSectionComponent {
@Input() type: InfoSectionType = InfoSectionType.INFO;
@Input() fitWidth: boolean = false;
}
@@ -1,5 +1,4 @@
<cnsl-refresh-table
*ngIf="dataSource"
[hideRefresh]="true"
(refreshed)="refresh()"
[dataSize]="dataSource.data.length"
@@ -33,7 +32,7 @@
<ng-container matColumnDef="primaryDomain">
<th mat-header-cell *matHeaderCellDef>{{ 'ORG.PAGES.PRIMARYDOMAIN' | translate }}</th>
<td mat-cell *matCellDef="let org">
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">
<span>{{ org.primaryDomain }}</span>
<button
color="primary"
@@ -101,6 +100,6 @@
[length]="totalResult || 0"
[pageSize]="initialLimit"
[pageSizeOptions]="[10, 20, 50, 100]"
(page)="changePage($event)"
(page)="changePage()"
></cnsl-paginator>
</cnsl-refresh-table>
@@ -2,16 +2,19 @@ import { Component, Input, ViewChild } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, catchError, finalize, from, map, Observable, of } from 'rxjs';
import { BehaviorSubject, catchError, finalize, from, map, Observable, of, Subject, switchMap, takeUntil } from 'rxjs';
import { Org, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { PageEvent, PaginatorComponent } from '../paginator/paginator.component';
import { PaginatorComponent } from '../paginator/paginator.component';
enum OrgListSearchKey {
NAME = 'NAME',
}
type Request = { limit: number; offset: number; queries: OrgQuery[] };
@Component({
selector: 'cnsl-org-table',
templateUrl: './org-table.component.html',
@@ -23,7 +26,7 @@ export class OrgTableComponent {
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
@ViewChild('input') public filter!: Input;
public dataSource!: MatTableDataSource<Org.AsObject>;
public dataSource: MatTableDataSource<Org.AsObject> = new MatTableDataSource<Org.AsObject>([]);
public displayedColumns: string[] = ['name', 'state', 'primaryDomain', 'creationDate', 'changeDate'];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@@ -35,27 +38,40 @@ export class OrgTableComponent {
public filterOpen: boolean = false;
public OrgState: any = OrgState;
public copied: string = '';
constructor(private authService: GrpcAuthService, private router: Router) {
this.loadOrgs(this.initialLimit, 0);
private searchQueries: OrgQuery[] = [];
private destroy$: Subject<void> = new Subject();
private requestOrgs$: BehaviorSubject<Request> = new BehaviorSubject<Request>({
limit: this.initialLimit,
offset: 0,
queries: [],
});
private requestOrgsObservable$ = this.requestOrgs$.pipe(takeUntil(this.destroy$));
constructor(private authService: GrpcAuthService, private router: Router, private toast: ToastService) {
this.requestOrgs$.next({ limit: this.initialLimit, offset: 0, queries: this.searchQueries });
this.authService.getActiveOrg().then((org) => (this.activeOrg = org));
this.requestOrgsObservable$.pipe(switchMap((req) => this.loadOrgs(req))).subscribe((orgs) => {
this.dataSource = new MatTableDataSource<Org.AsObject>(orgs);
});
}
public loadOrgs(limit: number, offset: number, queries?: OrgQuery[]): void {
public loadOrgs(request: Request): Observable<Org.AsObject[]> {
this.loadingSubject.next(true);
from(this.authService.listMyProjectOrgs(limit, offset, queries))
.pipe(
map((resp) => {
this.timestamp = resp.details?.viewTimestamp;
this.totalResult = resp.details?.totalResult ?? 0;
return resp.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
)
.subscribe((views) => {
this.dataSource = new MatTableDataSource(views);
});
return from(this.authService.listMyProjectOrgs(request.limit, request.offset, request.queries)).pipe(
map((resp) => {
this.timestamp = resp.details?.viewTimestamp;
this.totalResult = resp.details?.totalResult ?? 0;
return resp.resultList;
}),
catchError((error) => {
this.toast.showError(error);
return of([]);
}),
finalize(() => this.loadingSubject.next(false)),
);
}
public selectOrg(item: Org.AsObject, event?: any): void {
@@ -63,11 +79,20 @@ export class OrgTableComponent {
}
public refresh(): void {
this.loadOrgs(this.paginator.length, this.paginator.pageSize * this.paginator.pageIndex);
this.requestOrgs$.next({
limit: this.paginator.length,
offset: this.paginator.pageSize * this.paginator.pageIndex,
queries: this.searchQueries,
});
}
public applySearchQuery(searchQueries: OrgQuery[]): void {
this.loadOrgs(this.paginator.pageSize, this.paginator.pageSize * this.paginator.pageIndex, searchQueries);
this.searchQueries = searchQueries;
this.requestOrgs$.next({
limit: this.paginator ? this.paginator.pageSize : this.initialLimit,
offset: this.paginator ? this.paginator.pageSize * this.paginator.pageIndex : 0,
queries: this.searchQueries,
});
}
public setFilter(key: OrgListSearchKey): void {
@@ -90,8 +115,8 @@ export class OrgTableComponent {
this.router.navigate(['/org']);
}
public changePage(event: PageEvent): void {
this.loadOrgs(event.pageSize, event.pageIndex * event.pageSize);
public changePage(): void {
this.refresh();
}
public gotoRouterLink(rL: any) {
@@ -1,20 +1,20 @@
<h1 mat-dialog-title class="title">
<span>{{ 'SETTING.SMS.ADDPROVIDER' | translate }}</span>
<span>{{ provider === SMSProviderType.Twilio ? 'Twilio' : ('SETTING.SMS.ADDPROVIDER' | translate) }}</span>
</h1>
<div mat-dialog-content>
<p class="desc cnsl-secondary-text">{{ 'SETTING.SMS.ADDPROVIDERDESCRIPTION' | translate }}</p>
<!-- <p class="desc cnsl-secondary-text">{{ 'SETTING.SMS.ADDPROVIDERDESCRIPTION' | translate }}</p> -->
<cnsl-form-field class="form-field" label="Access Code" required="true">
<!-- <cnsl-form-field class="form-field" label="Access Code" required="true">
<cnsl-label>{{ 'MFA.TYPE' | translate }}</cnsl-label>
<mat-select [(ngModel)]="provider">
<mat-option *ngFor="let prov of availableSMSProviders" [value]="prov">
<span *ngIf="prov === SMSProviderType.Twilio">Twilio</span>
</mat-option>
</mat-select>
</cnsl-form-field>
</cnsl-form-field> -->
<form *ngIf="provider === SMSProviderType.Twilio" (ngSubmit)="closeDialogWithRequest()" [formGroup]="twilioForm">
<h2>Twilio</h2>
<!-- <h2>Twilio</h2> -->
<cnsl-form-field class="sms-form-field" label="sid">
<cnsl-label>{{ 'SETTING.SMS.TWILIO.SID' | translate }}</cnsl-label>
@@ -43,6 +43,6 @@
color="primary"
(click)="closeDialogWithRequest()"
>
<span>{{ 'ACTIONS.OK' | translate }}</span>
<span>{{ 'ACTIONS.SAVE' | translate }}</span>
</button>
</div>
@@ -4,6 +4,10 @@
<h2>{{ 'SETTING.SMTP.TITLE' | translate }}</h2>
<cnsl-info-section *ngIf="!form.valid" class="info-section-warn" [fitWidth]="true" [type]="InfoSectionType.ALERT">{{
'SETTING.SMTP.REQUIREDWARN' | translate
}}</cnsl-info-section>
<form (ngSubmit)="savePolicy()" [formGroup]="form" autocomplete="off">
<cnsl-form-field class="smtp-form-field" label="Sender Address" required="true">
<cnsl-label>{{ 'SETTING.SMTP.SENDERADDRESS' | translate }}</cnsl-label>
@@ -50,26 +54,28 @@
<br />
<h2>{{ 'SETTING.SMS.TITLE' | translate }}</h2>
<h3>{{ 'SETTING.SMS.PROVIDERS' | translate }}</h3>
<div class="sms-providers">
<cnsl-card *ngFor="let provider of smsProviders" class="sms-card">
<div *ngIf="provider.twilio" class="sms-provider">
<cnsl-card class="sms-card" [nomargin]="true">
<div class="sms-provider">
<h4 class="title">Twilio</h4>
<span class="cnsl-secondary-text">{{ 'SETTING.SMS.PROVIDER' | translate }}</span>
<span
*ngIf="twilio"
class="state"
[ngClass]="{
active: provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE,
inactive: provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE
active: twilio?.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE,
inactive: twilio?.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE
}"
></span>
<span class="fill-space"></span>
<button mat-icon-button (click)="addSMSProvider()"><i class="las la-pen"></i></button>
</div>
</cnsl-card>
<button mat-stroked-button (click)="addSMSProvider()">
<!-- <button mat-stroked-button (click)="addSMSProvider()">
<div class="sms-card add">
<mat-icon>add</mat-icon>
<span>{{ 'ACTIONS.ADD' | translate }}</span>
</div>
</button>
</button> -->
</div>
@@ -2,11 +2,16 @@
margin: 0.5rem 0;
}
.smtp-form-field {
.smtp-form-field,
.info-section-warn {
max-width: 400px;
display: block;
}
.info-section-warn {
margin-bottom: 0.5rem;
}
.smtp-checkbox {
max-width: 400px;
display: block;
@@ -40,7 +45,6 @@
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: -0.5rem;
.title {
font-size: 16px;
@@ -3,13 +3,15 @@ import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/fo
import { MatDialog } from '@angular/material/dialog';
import {
AddSMSProviderTwilioRequest,
UpdateSMTPConfigPasswordRequest,
UpdateSMTPConfigPasswordResponse,
UpdateSMTPConfigRequest,
} from 'src/app/proto/generated/zitadel/admin_pb';
import { SMSProvider, SMSProviderConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
import { DebugNotificationProvider, SMSProvider, SMSProviderConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
import { InfoSectionType } from '../../info-section/info-section.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
@@ -21,11 +23,16 @@ import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-
export class NotificationSettingsComponent implements OnInit {
@Input() public serviceType!: PolicyComponentServiceType;
public smsProviders: SMSProvider.AsObject[] = [];
public logNotificationProvider!: DebugNotificationProvider.AsObject;
public fileNotificationProvider!: DebugNotificationProvider.AsObject;
public loading: boolean = false;
public form!: FormGroup;
public SMSProviderConfigState: any = SMSProviderConfigState;
public InfoSectionType: any = InfoSectionType;
// show available providers
constructor(
private service: AdminService,
private dialog: MatDialog,
@@ -66,6 +73,30 @@ export class NotificationSettingsComponent implements OnInit {
console.log(this.smsProviders);
}
});
this.service
.getLogNotificationProvider()
.then((logNotificationProvider) => {
if (logNotificationProvider.provider) {
this.logNotificationProvider = logNotificationProvider.provider;
}
})
.catch((error) => {
this.toast.showError(error);
});
this.service
.getFileSystemNotificationProvider()
.then((fileNotificationProvider) => {
if (fileNotificationProvider.provider) {
console.log(fileNotificationProvider);
this.fileNotificationProvider = fileNotificationProvider.provider;
}
})
.catch((error) => {
console.log('hehe');
this.toast.showError(error);
});
}
private updateData(): Promise<UpdateSMTPConfigPasswordResponse.AsObject> | any {
@@ -76,18 +107,16 @@ export class NotificationSettingsComponent implements OnInit {
req.setTls(this.tls?.value ?? false);
req.setUser(this.user?.value ?? '');
console.log(req.toObject());
// return this.service.updateSMTPConfig(req).then(() => {
// let passwordReq: UpdateSMTPConfigPasswordRequest;
// if (this.password) {
// passwordReq = new UpdateSMTPConfigPasswordRequest();
// passwordReq.setPassword(this.password.value);
// return this.service.updateSMTPConfigPassword(passwordReq);
// } else {
// return;
// }
// });
return this.service.updateSMTPConfig(req).then(() => {
let passwordReq: UpdateSMTPConfigPasswordRequest;
if (this.password) {
passwordReq = new UpdateSMTPConfigPasswordRequest();
passwordReq.setPassword(this.password.value);
return this.service.updateSMTPConfigPassword(passwordReq);
} else {
return;
}
});
}
public savePolicy(): void {
@@ -126,6 +155,10 @@ export class NotificationSettingsComponent implements OnInit {
});
}
public get twilio(): SMSProvider.AsObject | undefined {
return this.smsProviders.find((p) => p.twilio);
}
public get senderAddress(): AbstractControl | null {
return this.form.get('senderAddress');
}
@@ -10,6 +10,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CardModule } from '../../card/card.module';
import { FormFieldModule } from '../../form-field/form-field.module';
import { InfoSectionModule } from '../../info-section/info-section.module';
import { InputModule } from '../../input/input.module';
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
import { NotificationSettingsComponent } from './notification-settings.component';
@@ -19,6 +20,7 @@ import { NotificationSettingsComponent } from './notification-settings.component
imports: [
CommonModule,
CardModule,
InfoSectionModule,
FormsModule,
ReactiveFormsModule,
MatButtonModule,
@@ -32,6 +32,7 @@
[ngClass]="{ active: currentSetting === setting.id, show: currentSetting === undefined }"
>
<span>{{ setting.i18nKey | translate }}</span>
<mat-icon *ngIf="setting.showWarn" class="warn-icon" svgIcon="mdi_shield_alert"></mat-icon>
</button>
</ng-container>
<ng-template #btn>
@@ -67,21 +67,35 @@
background: none;
text-align: left;
padding: 0.75rem 0;
opacity: 0.6;
font-size: 15px;
cursor: pointer;
color: map-get($foreground, base);
display: flex;
align-items: center;
span {
opacity: 0.6;
}
i {
font-size: 1.2rem;
height: 1.2rem;
line-height: 1.2rem;
}
.warn-icon {
color: map-get($background, alert);
margin-left: 0.5rem;
font-size: 1.2rem;
height: 1.2rem;
width: 1.2rem;
flex-shrink: 0;
}
&:hover {
opacity: 1;
span {
opacity: 1;
}
}
&.mob-only {
@@ -108,7 +122,10 @@
&.active {
font-weight: 600;
opacity: 1;
span {
opacity: 1;
}
}
}
}
@@ -9,6 +9,7 @@ export interface SidenavSetting {
i18nKey: string;
groupI18nKey?: string;
requiredRoles?: { [serviceType in PolicyComponentServiceType]: string[] };
showWarn?: boolean;
}
@Component({