mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 17:57:33 +00:00
feat: Actions V2 improvements in console (#9759)
# Which Problems Are Solved
This PR allows one to edit the order of Actions V2 Targets in an
Execution. Editing of Targets was also added back again.
# How the Problems Are Solved
One of the changes is the addition of the CorrectlyTypedExecution which
restricts the Grpc types a bit more to make working with them easier.
Some fields may be optional in the Grpc Protobuf but in reality are
always set.
Typings were generally improved to make them more accurate and safer to
work with.
# Additional Changes
Removal of the Actions V2 Feature flag as it will be enabled by default
anyways.
# Additional Context
This pr used some advanced Angular Signals logic which is very
interesting for future PR's.
- Part of the tasks from #7248
---------
Co-authored-by: Max Peintner <peintnerm@gmail.com>
(cherry picked from commit 56e0df67d5
)
This commit is contained in:
@@ -27,7 +27,6 @@ import { LoginV2FeatureToggleComponent } from '../feature-toggle/login-v2-featur
|
|||||||
|
|
||||||
// to add a new feature, add the key here and in the FEATURE_KEYS array
|
// to add a new feature, add the key here and in the FEATURE_KEYS array
|
||||||
const FEATURE_KEYS = [
|
const FEATURE_KEYS = [
|
||||||
'actions',
|
|
||||||
'consoleUseV2UserApi',
|
'consoleUseV2UserApi',
|
||||||
'debugOidcParentError',
|
'debugOidcParentError',
|
||||||
'disableUserTokenEvent',
|
'disableUserTokenEvent',
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
<cnsl-refresh-table (refreshed)="refresh.emit()" [loading]="(dataSource$ | async) === null">
|
<cnsl-refresh-table (refreshed)="refresh.emit()" [loading]="loading()">
|
||||||
<div actions>
|
<div actions>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table *ngIf="dataSource$ | async as dataSource" mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
|
<table mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
|
||||||
<ng-container matColumnDef="condition">
|
<ng-container matColumnDef="condition">
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.EXECUTION.TABLE.CONDITION' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.EXECUTION.TABLE.CONDITION' | translate }}</th>
|
||||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||||
<span *ngIf="row.condition?.conditionType?.value">
|
<span>
|
||||||
{{ row?.condition | condition }}
|
{{ row.execution.condition | condition }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<ng-container matColumnDef="type">
|
<ng-container matColumnDef="type">
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.EXECUTION.TABLE.TYPE' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.EXECUTION.TABLE.TYPE' | translate }}</th>
|
||||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||||
{{ 'ACTIONSTWO.EXECUTION.TYPES.' + row?.condition?.conditionType?.case | translate }}
|
{{ 'ACTIONSTWO.EXECUTION.TYPES.' + row.execution.condition.conditionType.case | translate }}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
@@ -24,16 +24,9 @@
|
|||||||
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.EXECUTION.TABLE.TARGET' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.EXECUTION.TABLE.TARGET' | translate }}</th>
|
||||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||||
<div class="target-key">
|
<div class="target-key">
|
||||||
<cnsl-project-role-chip *ngFor="let target of filteredTargetTypes(row.targets) | async" [roleName]="target.name"
|
<cnsl-project-role-chip *ngFor="let target of row.mappedTargets; trackBy: trackTarget" [roleName]="target.name"
|
||||||
>{{ target.name }}
|
>{{ target.name }}
|
||||||
</cnsl-project-role-chip>
|
</cnsl-project-role-chip>
|
||||||
<cnsl-project-role-chip
|
|
||||||
*ngFor="let condition of filteredIncludeConditions(row.targets)"
|
|
||||||
[roleName]="condition | condition"
|
|
||||||
>
|
|
||||||
<mat-icon class="icon">refresh</mat-icon>
|
|
||||||
{{ condition | condition }}
|
|
||||||
</cnsl-project-role-chip>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -43,7 +36,7 @@
|
|||||||
{{ 'ACTIONSTWO.EXECUTION.TABLE.CREATIONDATE' | translate }}
|
{{ 'ACTIONSTWO.EXECUTION.TABLE.CREATIONDATE' | translate }}
|
||||||
</th>
|
</th>
|
||||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||||
<span class="no-break">{{ row.creationDate | timestampToDate | localizedDate: 'regular' }}</span>
|
<span class="no-break">{{ row.execution.creationDate | timestampToDate | localizedDate: 'regular' }}</span>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
@@ -55,7 +48,7 @@
|
|||||||
actions
|
actions
|
||||||
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
|
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
|
||||||
color="warn"
|
color="warn"
|
||||||
(click)="$event.stopPropagation(); delete.emit(row)"
|
(click)="$event.stopPropagation(); delete.emit(row.execution)"
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
>
|
>
|
||||||
<i class="las la-trash"></i>
|
<i class="las la-trash"></i>
|
||||||
@@ -69,6 +62,7 @@
|
|||||||
class="highlight pointer"
|
class="highlight pointer"
|
||||||
mat-row
|
mat-row
|
||||||
*matRowDef="let row; columns: ['condition', 'type', 'target', 'creationDate', 'actions']"
|
*matRowDef="let row; columns: ['condition', 'type', 'target', 'creationDate', 'actions']"
|
||||||
|
(click)="selected.emit(row.execution)"
|
||||||
></tr>
|
></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, effect, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { Observable, ReplaySubject } from 'rxjs';
|
import { combineLatestWith, Observable, ReplaySubject } from 'rxjs';
|
||||||
import { filter, map } from 'rxjs/operators';
|
import { filter, map, startWith } from 'rxjs/operators';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { Condition, Execution, ExecutionTargetType } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
|
||||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { CorrectlyTypedExecution } from '../../actions-two-add-action/actions-two-add-action-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-actions-two-actions-table',
|
selector: 'cnsl-actions-two-actions-table',
|
||||||
@@ -16,10 +17,13 @@ export class ActionsTwoActionsTableComponent {
|
|||||||
public readonly refresh = new EventEmitter<void>();
|
public readonly refresh = new EventEmitter<void>();
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
public readonly delete = new EventEmitter<Execution>();
|
public readonly selected = new EventEmitter<CorrectlyTypedExecution>();
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public readonly delete = new EventEmitter<CorrectlyTypedExecution>();
|
||||||
|
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
public set executions(executions: Execution[] | null) {
|
public set executions(executions: CorrectlyTypedExecution[] | null) {
|
||||||
this.executions$.next(executions);
|
this.executions$.next(executions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,33 +32,76 @@ export class ActionsTwoActionsTableComponent {
|
|||||||
this.targets$.next(targets);
|
this.targets$.next(targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Output()
|
private readonly executions$ = new ReplaySubject<CorrectlyTypedExecution[] | null>(1);
|
||||||
public readonly selected = new EventEmitter<Execution>();
|
|
||||||
|
|
||||||
private readonly executions$ = new ReplaySubject<Execution[] | null>(1);
|
|
||||||
private readonly targets$ = new ReplaySubject<Target[] | null>(1);
|
private readonly targets$ = new ReplaySubject<Target[] | null>(1);
|
||||||
|
|
||||||
protected readonly dataSource$ = this.executions$.pipe(
|
protected readonly dataSource = this.getDataSource();
|
||||||
filter(Boolean),
|
|
||||||
map((keys) => new MatTableDataSource(keys)),
|
|
||||||
);
|
|
||||||
|
|
||||||
protected filteredTargetTypes(targets: ExecutionTargetType[]): Observable<Target[]> {
|
protected readonly loading = this.getLoading();
|
||||||
const targetIds = targets
|
|
||||||
.map((t) => t.type)
|
|
||||||
.filter((t): t is Extract<ExecutionTargetType['type'], { case: 'target' }> => t.case === 'target')
|
|
||||||
.map((t) => t.value);
|
|
||||||
|
|
||||||
return this.targets$.pipe(
|
private getDataSource() {
|
||||||
filter(Boolean),
|
const executions$: Observable<CorrectlyTypedExecution[]> = this.executions$.pipe(filter(Boolean), startWith([]));
|
||||||
map((alltargets) => alltargets!.filter((target) => targetIds.includes(target.id))),
|
const executionsSignal = toSignal(executions$, { requireSync: true });
|
||||||
);
|
|
||||||
|
const targetsMapSignal = this.getTargetsMap();
|
||||||
|
|
||||||
|
const dataSignal = computed(() => {
|
||||||
|
const executions = executionsSignal();
|
||||||
|
const targetsMap = targetsMapSignal();
|
||||||
|
|
||||||
|
if (targetsMap.size === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return executions.map((execution) => {
|
||||||
|
const mappedTargets = execution.targets.map((target) => {
|
||||||
|
const targetType = targetsMap.get(target.type.value);
|
||||||
|
if (!targetType) {
|
||||||
|
throw new Error(`Target with id ${target.type.value} not found`);
|
||||||
|
}
|
||||||
|
return targetType;
|
||||||
|
});
|
||||||
|
return { execution, mappedTargets };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataSource = new MatTableDataSource(dataSignal());
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
const data = dataSignal();
|
||||||
|
if (dataSource.data !== data) {
|
||||||
|
dataSource.data = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected filteredIncludeConditions(targets: ExecutionTargetType[]): Condition[] {
|
private getTargetsMap() {
|
||||||
return targets
|
const targets$ = this.targets$.pipe(filter(Boolean), startWith([] as Target[]));
|
||||||
.map((t) => t.type)
|
const targetsSignal = toSignal(targets$, { requireSync: true });
|
||||||
.filter((t): t is Extract<ExecutionTargetType['type'], { case: 'include' }> => t.case === 'include')
|
|
||||||
.map(({ value }) => value);
|
return computed(() => {
|
||||||
|
const map = new Map<string, Target>();
|
||||||
|
for (const target of targetsSignal()) {
|
||||||
|
map.set(target.id, target);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLoading() {
|
||||||
|
const loading$ = this.executions$.pipe(
|
||||||
|
combineLatestWith(this.targets$),
|
||||||
|
map(([executions, targets]) => executions === null || targets === null),
|
||||||
|
startWith(true),
|
||||||
|
);
|
||||||
|
|
||||||
|
return toSignal(loading$, { requireSync: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected trackTarget(_: number, target: Target) {
|
||||||
|
return target.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.EXECUTION.DESCRIPTION' | translate }}</p>
|
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.EXECUTION.DESCRIPTION' | translate }}</p>
|
||||||
|
|
||||||
<cnsl-actions-two-actions-table
|
<cnsl-actions-two-actions-table
|
||||||
(refresh)="refresh.next(true)"
|
(refresh)="refresh$.next(true)"
|
||||||
(delete)="deleteExecution($event)"
|
(delete)="deleteExecution($event)"
|
||||||
(selected)="openDialog($event)"
|
(selected)="openDialog($event)"
|
||||||
[executions]="executions$ | async"
|
[executions]="executions$ | async"
|
||||||
|
@@ -1,19 +1,20 @@
|
|||||||
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core';
|
||||||
import { ActionService } from 'src/app/services/action.service';
|
import { ActionService } from 'src/app/services/action.service';
|
||||||
import { NewFeatureService } from 'src/app/services/new-feature.service';
|
import { lastValueFrom, Observable, of, Subject } from 'rxjs';
|
||||||
import { defer, firstValueFrom, Observable, of, shareReplay, Subject, TimeoutError } from 'rxjs';
|
import { catchError, map, startWith, switchMap } from 'rxjs/operators';
|
||||||
import { catchError, filter, map, startWith, switchMap, tap, timeout } from 'rxjs/operators';
|
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import {
|
||||||
import { ORGANIZATIONS } from '../../settings-list/settings';
|
ActionTwoAddActionDialogComponent,
|
||||||
import { ActionTwoAddActionDialogComponent } from '../actions-two-add-action/actions-two-add-action-dialog.component';
|
ActionTwoAddActionDialogData,
|
||||||
|
ActionTwoAddActionDialogResult,
|
||||||
|
CorrectlyTypedExecution,
|
||||||
|
correctlyTypeExecution,
|
||||||
|
} from '../actions-two-add-action/actions-two-add-action-dialog.component';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||||
import { Execution, ExecutionSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
|
||||||
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||||
import { Value } from 'google-protobuf/google/protobuf/struct_pb';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-actions-two-actions',
|
selector: 'cnsl-actions-two-actions',
|
||||||
@@ -21,123 +22,92 @@ import { Value } from 'google-protobuf/google/protobuf/struct_pb';
|
|||||||
styleUrls: ['./actions-two-actions.component.scss'],
|
styleUrls: ['./actions-two-actions.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ActionsTwoActionsComponent implements OnInit {
|
export class ActionsTwoActionsComponent {
|
||||||
protected readonly refresh = new Subject<true>();
|
protected readonly refresh$ = new Subject<true>();
|
||||||
private readonly actionsEnabled$: Observable<boolean>;
|
protected readonly executions$: Observable<CorrectlyTypedExecution[]>;
|
||||||
protected readonly executions$: Observable<Execution[]>;
|
|
||||||
protected readonly targets$: Observable<Target[]>;
|
protected readonly targets$: Observable<Target[]>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly actionService: ActionService,
|
private readonly actionService: ActionService,
|
||||||
private readonly featureService: NewFeatureService,
|
|
||||||
private readonly toast: ToastService,
|
private readonly toast: ToastService,
|
||||||
private readonly destroyRef: DestroyRef,
|
private readonly destroyRef: DestroyRef,
|
||||||
private readonly router: Router,
|
|
||||||
private readonly route: ActivatedRoute,
|
|
||||||
private readonly dialog: MatDialog,
|
private readonly dialog: MatDialog,
|
||||||
) {
|
) {
|
||||||
this.actionsEnabled$ = this.getActionsEnabled$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
this.executions$ = this.getExecutions$();
|
||||||
this.executions$ = this.getExecutions$(this.actionsEnabled$);
|
this.targets$ = this.getTargets$();
|
||||||
this.targets$ = this.getTargets$(this.actionsEnabled$);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
private getExecutions$() {
|
||||||
// this also preloads
|
return this.refresh$.pipe(
|
||||||
this.actionsEnabled$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async (enabled) => {
|
|
||||||
if (enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.router.navigate([], {
|
|
||||||
relativeTo: this.route,
|
|
||||||
queryParams: {
|
|
||||||
id: ORGANIZATIONS.id,
|
|
||||||
},
|
|
||||||
queryParamsHandling: 'merge',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getExecutions$(actionsEnabled$: Observable<boolean>) {
|
|
||||||
return this.refresh.pipe(
|
|
||||||
startWith(true),
|
startWith(true),
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
return this.actionService.listExecutions({});
|
return this.actionService.listExecutions({});
|
||||||
}),
|
}),
|
||||||
map(({ result }) => result),
|
map(({ result }) => result.map(correctlyTypeExecution)),
|
||||||
catchError(async (err) => {
|
catchError((err) => {
|
||||||
const actionsEnabled = await firstValueFrom(actionsEnabled$);
|
this.toast.showError(err);
|
||||||
if (actionsEnabled) {
|
return of([]);
|
||||||
this.toast.showError(err);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTargets$(actionsEnabled$: Observable<boolean>) {
|
private getTargets$() {
|
||||||
return this.refresh.pipe(
|
return this.refresh$.pipe(
|
||||||
startWith(true),
|
startWith(true),
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
return this.actionService.listTargets({});
|
return this.actionService.listTargets({});
|
||||||
}),
|
}),
|
||||||
map(({ result }) => result),
|
map(({ result }) => result),
|
||||||
catchError(async (err) => {
|
|
||||||
const actionsEnabled = await firstValueFrom(actionsEnabled$);
|
|
||||||
if (actionsEnabled) {
|
|
||||||
this.toast.showError(err);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getActionsEnabled$() {
|
|
||||||
return defer(() => this.featureService.getInstanceFeatures()).pipe(
|
|
||||||
map(({ actions }) => actions?.enabled ?? false),
|
|
||||||
timeout(1000),
|
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
if (!(err instanceof TimeoutError)) {
|
this.toast.showError(err);
|
||||||
this.toast.showError(err);
|
return of([]);
|
||||||
}
|
|
||||||
return of(false);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openDialog(execution?: Execution): void {
|
public async openDialog(execution?: CorrectlyTypedExecution): Promise<void> {
|
||||||
const ref = this.dialog.open<ActionTwoAddActionDialogComponent>(ActionTwoAddActionDialogComponent, {
|
const request$ = this.dialog
|
||||||
width: '400px',
|
.open<ActionTwoAddActionDialogComponent, ActionTwoAddActionDialogData, ActionTwoAddActionDialogResult>(
|
||||||
data: execution
|
ActionTwoAddActionDialogComponent,
|
||||||
? {
|
{
|
||||||
execution: execution,
|
width: '400px',
|
||||||
}
|
data: execution
|
||||||
: {},
|
? {
|
||||||
});
|
execution,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef));
|
||||||
|
|
||||||
ref.afterClosed().subscribe((request?: MessageInitShape<typeof SetExecutionRequestSchema>) => {
|
const request = await lastValueFrom(request$);
|
||||||
if (request) {
|
if (!request) {
|
||||||
this.actionService
|
return;
|
||||||
.setExecution(request)
|
}
|
||||||
.then(() => {
|
|
||||||
setTimeout(() => {
|
try {
|
||||||
this.refresh.next(true);
|
await this.actionService.setExecution(request);
|
||||||
}, 1000);
|
await new Promise((res) => setTimeout(res, 1000));
|
||||||
})
|
this.refresh$.next(true);
|
||||||
.catch((error) => {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteExecution(execution: Execution) {
|
public async deleteExecution(execution: CorrectlyTypedExecution) {
|
||||||
const deleteReq: MessageInitShape<typeof SetExecutionRequestSchema> = {
|
const deleteReq: MessageInitShape<typeof SetExecutionRequestSchema> = {
|
||||||
condition: execution.condition,
|
condition: execution.condition,
|
||||||
targets: [],
|
targets: [],
|
||||||
};
|
};
|
||||||
await this.actionService.setExecution(deleteReq);
|
try {
|
||||||
await new Promise((res) => setTimeout(res, 1000));
|
await this.actionService.setExecution(deleteReq);
|
||||||
this.refresh.next(true);
|
await new Promise((res) => setTimeout(res, 1000));
|
||||||
|
this.refresh$.next(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
this.toast.showError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,15 +14,15 @@
|
|||||||
*ngSwitchCase="Page.Condition"
|
*ngSwitchCase="Page.Condition"
|
||||||
[conditionType]="typeSignal()"
|
[conditionType]="typeSignal()"
|
||||||
(back)="back()"
|
(back)="back()"
|
||||||
(continue)="conditionSignal.set($event); continue()"
|
(continue)="conditionSignal.set({ conditionType: { case: typeSignal(), value: $event } }); continue()"
|
||||||
></cnsl-actions-two-add-action-condition>
|
></cnsl-actions-two-add-action-condition>
|
||||||
|
|
||||||
<cnsl-actions-two-add-action-target
|
<cnsl-actions-two-add-action-target
|
||||||
*ngSwitchCase="Page.Target"
|
*ngSwitchCase="Page.Target"
|
||||||
(back)="back()"
|
(back)="back()"
|
||||||
[hideBackButton]="!!data.execution"
|
[hideBackButton]="!!data.execution"
|
||||||
(continue)="targetSignal.set($event); continue()"
|
(continue)="targetsSignal.set($event); continue()"
|
||||||
[selectedCondition]="data.execution?.condition"
|
[preselectedTargetIds]="preselectedTargetIds"
|
||||||
></cnsl-actions-two-add-action-target>
|
></cnsl-actions-two-add-action-target>
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide {
|
.hide {
|
||||||
|
@@ -7,11 +7,15 @@ import { MessageInitShape } from '@bufbuild/protobuf';
|
|||||||
import {
|
import {
|
||||||
ActionsTwoAddActionConditionComponent,
|
ActionsTwoAddActionConditionComponent,
|
||||||
ConditionType,
|
ConditionType,
|
||||||
ConditionTypeValue,
|
|
||||||
} from './actions-two-add-action-condition/actions-two-add-action-condition.component';
|
} from './actions-two-add-action-condition/actions-two-add-action-condition.component';
|
||||||
import { ActionsTwoAddActionTargetComponent } from './actions-two-add-action-target/actions-two-add-action-target.component';
|
import { ActionsTwoAddActionTargetComponent } from './actions-two-add-action-target/actions-two-add-action-target.component';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Execution, ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
import {
|
||||||
|
Condition,
|
||||||
|
Execution,
|
||||||
|
ExecutionTargetType,
|
||||||
|
ExecutionTargetTypeSchema,
|
||||||
|
} from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||||
|
|
||||||
@@ -21,6 +25,41 @@ enum Page {
|
|||||||
Target,
|
Target,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CorrectlyTypedCondition = Condition & { conditionType: Extract<Condition['conditionType'], { case: string }> };
|
||||||
|
|
||||||
|
type CorrectlyTypedTargets = { type: Extract<ExecutionTargetType['type'], { case: 'target' }> };
|
||||||
|
|
||||||
|
export type CorrectlyTypedExecution = Omit<Execution, 'targets' | 'condition'> & {
|
||||||
|
condition: CorrectlyTypedCondition;
|
||||||
|
targets: CorrectlyTypedTargets[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExecution => {
|
||||||
|
if (!execution.condition?.conditionType?.case) {
|
||||||
|
throw new Error('Condition is required');
|
||||||
|
}
|
||||||
|
const conditionType = execution.condition.conditionType;
|
||||||
|
|
||||||
|
const condition = {
|
||||||
|
...execution.condition,
|
||||||
|
conditionType,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...execution,
|
||||||
|
condition,
|
||||||
|
targets: execution.targets
|
||||||
|
.map(({ type }) => ({ type }))
|
||||||
|
.filter((target): target is CorrectlyTypedTargets => target.type.case === 'target'),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActionTwoAddActionDialogData = {
|
||||||
|
execution?: CorrectlyTypedExecution;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActionTwoAddActionDialogResult = MessageInitShape<typeof SetExecutionRequestSchema>;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-actions-two-add-action-dialog',
|
selector: 'cnsl-actions-two-add-action-dialog',
|
||||||
templateUrl: './actions-two-add-action-dialog.component.html',
|
templateUrl: './actions-two-add-action-dialog.component.html',
|
||||||
@@ -37,45 +76,45 @@ enum Page {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ActionTwoAddActionDialogComponent {
|
export class ActionTwoAddActionDialogComponent {
|
||||||
public Page = Page;
|
protected readonly Page = Page;
|
||||||
public page = signal<Page | undefined>(Page.Type);
|
protected readonly page = signal<Page>(Page.Type);
|
||||||
|
|
||||||
public typeSignal = signal<ConditionType>('request');
|
protected readonly typeSignal = signal<ConditionType>('request');
|
||||||
public conditionSignal = signal<ConditionTypeValue<ConditionType> | undefined>(undefined); // TODO: fix this type
|
protected readonly conditionSignal = signal<MessageInitShape<typeof SetExecutionRequestSchema>['condition']>(undefined);
|
||||||
public targetSignal = signal<Array<MessageInitShape<typeof ExecutionTargetTypeSchema>> | undefined>(undefined);
|
protected readonly targetsSignal = signal<MessageInitShape<typeof ExecutionTargetTypeSchema>[]>([]);
|
||||||
|
|
||||||
public continueSubject = new Subject<void>();
|
protected readonly continueSubject = new Subject<void>();
|
||||||
|
|
||||||
public request = computed<MessageInitShape<typeof SetExecutionRequestSchema>>(() => {
|
protected readonly request = computed<MessageInitShape<typeof SetExecutionRequestSchema>>(() => {
|
||||||
return {
|
return {
|
||||||
condition: {
|
condition: this.conditionSignal(),
|
||||||
conditionType: {
|
targets: this.targetsSignal(),
|
||||||
case: this.typeSignal(),
|
|
||||||
value: this.conditionSignal() as any, // TODO: fix this type
|
|
||||||
},
|
|
||||||
},
|
|
||||||
targets: this.targetSignal(),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
protected readonly preselectedTargetIds: string[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public dialogRef: MatDialogRef<ActionTwoAddActionDialogComponent, MessageInitShape<typeof SetExecutionRequestSchema>>,
|
protected readonly dialogRef: MatDialogRef<ActionTwoAddActionDialogComponent, ActionTwoAddActionDialogResult>,
|
||||||
@Inject(MAT_DIALOG_DATA) protected readonly data: { execution?: Execution },
|
@Inject(MAT_DIALOG_DATA) protected readonly data: ActionTwoAddActionDialogData,
|
||||||
) {
|
) {
|
||||||
if (data?.execution) {
|
|
||||||
this.typeSignal.set(data.execution.condition?.conditionType.case ?? 'request');
|
|
||||||
this.conditionSignal.set((data.execution.condition?.conditionType as any)?.value ?? undefined);
|
|
||||||
this.targetSignal.set(data.execution.targets ?? []);
|
|
||||||
|
|
||||||
this.page.set(Page.Target); // Set the initial page based on the provided execution data
|
|
||||||
}
|
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const currentPage = this.page();
|
const currentPage = this.page();
|
||||||
if (currentPage === Page.Target) {
|
if (currentPage === Page.Target) {
|
||||||
this.continueSubject.next(); // Trigger the Subject to request condition form when the page changes to "Target"
|
this.continueSubject.next(); // Trigger the Subject to request condition form when the page changes to "Target"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!data?.execution) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.targetsSignal.set(data.execution.targets);
|
||||||
|
this.typeSignal.set(data.execution.condition.conditionType.case);
|
||||||
|
this.conditionSignal.set(data.execution.condition);
|
||||||
|
this.preselectedTargetIds = data.execution.targets.map((target) => target.type.value);
|
||||||
|
|
||||||
|
this.page.set(Page.Target); // Set the initial page based on the provided execution data
|
||||||
}
|
}
|
||||||
|
|
||||||
public continue() {
|
public continue() {
|
||||||
|
@@ -1,37 +1,71 @@
|
|||||||
<form *ngIf="targetForm" class="form-grid" [formGroup]="targetForm" (ngSubmit)="submit()">
|
<form class="form-grid" [formGroup]="form()" (ngSubmit)="submit()">
|
||||||
<p class="target-description">{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.DESCRIPTION' | translate }}</p>
|
<p class="target-description">{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.DESCRIPTION' | translate }}</p>
|
||||||
|
|
||||||
<cnsl-form-field class="full-width">
|
<cnsl-form-field class="full-width">
|
||||||
<cnsl-label>{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.TARGET.DESCRIPTION' | translate }}</cnsl-label>
|
<cnsl-label>{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.TARGET.DESCRIPTION' | translate }}</cnsl-label>
|
||||||
<mat-select formControlName="target">
|
<input
|
||||||
<mat-option *ngIf="(executionTargets$ | async) === null" class="is-loading">
|
cnslInput
|
||||||
|
#trigger="matAutocompleteTrigger"
|
||||||
|
#input
|
||||||
|
type="text"
|
||||||
|
[formControl]="form().controls.autocomplete"
|
||||||
|
[matAutocomplete]="autoservice"
|
||||||
|
(keydown.enter)="handleEnter($event); input.blur(); trigger.closePanel()"
|
||||||
|
/>
|
||||||
|
<mat-autocomplete #autoservice="matAutocomplete">
|
||||||
|
<mat-option *ngIf="targets().state === 'loading'" class="is-loading">
|
||||||
<mat-spinner diameter="30"></mat-spinner>
|
<mat-spinner diameter="30"></mat-spinner>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
<mat-option *ngFor="let target of executionTargets$ | async" [value]="target">
|
<mat-option
|
||||||
|
*ngFor="let target of selectableTargets(); trackBy: trackTarget"
|
||||||
|
#option
|
||||||
|
(click)="addTarget(target); option.deselect()"
|
||||||
|
[value]="target.name"
|
||||||
|
>
|
||||||
{{ target.name }}
|
{{ target.name }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-autocomplete>
|
||||||
</cnsl-form-field>
|
</cnsl-form-field>
|
||||||
|
|
||||||
<!-- <cnsl-form-field class="full-width">-->
|
<table mat-table cdkDropList (cdkDropListDropped)="drop($event)" [dataSource]="dataSource" [trackBy]="trackTarget">
|
||||||
<!-- <cnsl-label>{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.CONDITIONS.DESCRIPTION' | translate }}</cnsl-label>-->
|
<ng-container matColumnDef="order">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Reorder</th>
|
||||||
<!-- <mat-select [multiple]="true" formControlName="executionConditions">-->
|
<td mat-cell *cnslCellDef="let row; let i = index; dataSource: dataSource">
|
||||||
<!-- <mat-option *ngIf="(executionConditions$ | async) === null" class="is-loading">-->
|
<i class="las la-bars"></i>
|
||||||
<!-- <mat-spinner diameter="30"></mat-spinner>-->
|
</td>
|
||||||
<!-- </mat-option>-->
|
</ng-container>
|
||||||
<!-- <mat-option *ngFor="let condition of executionConditions$ | async" [value]="condition">-->
|
<ng-container matColumnDef="name">
|
||||||
<!-- <span>{{ condition | condition }}</span>-->
|
<th mat-header-cell *matHeaderCellDef>Name</th>
|
||||||
<!-- </mat-option>-->
|
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||||
<!-- </mat-select>-->
|
<cnsl-project-role-chip [roleName]="row.name">{{ row.name }}</cnsl-project-role-chip>
|
||||||
<!-- </cnsl-form-field>-->
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="deleteAction" stickyEnd>
|
||||||
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
|
<td mat-cell *cnslCellDef="let i = index; dataSource: dataSource">
|
||||||
|
<cnsl-table-actions>
|
||||||
|
<button
|
||||||
|
actions
|
||||||
|
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
|
||||||
|
color="warn"
|
||||||
|
(click)="removeTarget(i)"
|
||||||
|
mat-icon-button
|
||||||
|
>
|
||||||
|
<i class="las la-trash"></i>
|
||||||
|
</button>
|
||||||
|
</cnsl-table-actions>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
<tr mat-header-row *matHeaderRowDef="['order', 'name', 'deleteAction']"></tr>
|
||||||
|
<tr class="drag-row" cdkDrag mat-row *matRowDef="let row; columns: ['order', 'name', 'deleteAction']"></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button *ngIf="!hideBackButton" mat-stroked-button (click)="back.emit()">
|
<button *ngIf="!hideBackButton" mat-stroked-button (click)="back.emit()">
|
||||||
{{ 'ACTIONS.BACK' | translate }}
|
{{ 'ACTIONS.BACK' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<span class="fill-space"></span>
|
<span class="fill-space"></span>
|
||||||
<button color="primary" [disabled]="targetForm.invalid" mat-raised-button type="submit">
|
<button color="primary" [disabled]="form().invalid" mat-raised-button type="submit">
|
||||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -10,3 +10,27 @@
|
|||||||
font: 1;
|
font: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cdk-drag-preview {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drag-placeholder {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drag-animating {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdk-drop-list-dragging .mat-row:not(.cdk-drag-placeholder) {
|
||||||
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-row {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
@@ -1,10 +1,20 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
effect,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
signal,
|
||||||
|
Signal,
|
||||||
|
} from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { Observable, catchError, defer, map, of, shareReplay, ReplaySubject, combineLatestWith } from 'rxjs';
|
import { ReplaySubject, switchMap } from 'rxjs';
|
||||||
import { MatRadioModule } from '@angular/material/radio';
|
import { MatRadioModule } from '@angular/material/radio';
|
||||||
import { ActionService } from 'src/app/services/action.service';
|
import { ActionService } from 'src/app/services/action.service';
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
@@ -13,15 +23,18 @@ import { InputModule } from 'src/app/modules/input/input.module';
|
|||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||||
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
import { ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
||||||
import { Condition, ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { atLeastOneFieldValidator } from 'src/app/modules/form-field/validators/validators';
|
|
||||||
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
|
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
|
||||||
|
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||||
export type TargetInit = NonNullable<
|
import { startWith } from 'rxjs/operators';
|
||||||
NonNullable<MessageInitShape<typeof SetExecutionRequestSchema>['targets']>
|
import { TypeSafeCellDefModule } from 'src/app/directives/type-safe-cell-def/type-safe-cell-def.module';
|
||||||
>[number]['type'];
|
import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
|
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { minArrayLengthValidator } from '../../../form-field/validators/validators';
|
||||||
|
import { ProjectRoleChipModule } from '../../../project-role-chip/project-role-chip.module';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { TableActionsModule } from '../../../table-actions/table-actions.module';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -42,114 +55,172 @@ export type TargetInit = NonNullable<
|
|||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
|
MatTableModule,
|
||||||
|
TypeSafeCellDefModule,
|
||||||
|
CdkDrag,
|
||||||
|
CdkDropList,
|
||||||
|
ProjectRoleChipModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
TableActionsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ActionsTwoAddActionTargetComponent {
|
export class ActionsTwoAddActionTargetComponent {
|
||||||
protected readonly targetForm = this.buildActionTargetForm();
|
@Input() public hideBackButton = false;
|
||||||
|
@Input()
|
||||||
|
public set preselectedTargetIds(preselectedTargetIds: string[]) {
|
||||||
|
this.preselectedTargetIds$.next(preselectedTargetIds);
|
||||||
|
}
|
||||||
|
|
||||||
@Output() public readonly back = new EventEmitter<void>();
|
@Output() public readonly back = new EventEmitter<void>();
|
||||||
@Output() public readonly continue = new EventEmitter<MessageInitShape<typeof ExecutionTargetTypeSchema>[]>();
|
@Output() public readonly continue = new EventEmitter<MessageInitShape<typeof ExecutionTargetTypeSchema>[]>();
|
||||||
@Input() public hideBackButton = false;
|
|
||||||
@Input() set selectedCondition(selectedCondition: Condition | undefined) {
|
|
||||||
this.selectedCondition$.next(selectedCondition);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly selectedCondition$ = new ReplaySubject<Condition | undefined>(1);
|
private readonly preselectedTargetIds$ = new ReplaySubject<string[]>(1);
|
||||||
|
|
||||||
protected readonly executionTargets$: Observable<Target[]>;
|
protected readonly form: ReturnType<typeof this.buildForm>;
|
||||||
protected readonly executionConditions$: Observable<Condition[]>;
|
protected readonly targets: ReturnType<typeof this.listTargets>;
|
||||||
|
private readonly selectedTargetIds: Signal<string[]>;
|
||||||
|
protected readonly selectableTargets: Signal<Target[]>;
|
||||||
|
protected readonly dataSource: MatTableDataSource<Target>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly fb: FormBuilder,
|
private readonly fb: FormBuilder,
|
||||||
private readonly actionService: ActionService,
|
private readonly actionService: ActionService,
|
||||||
private readonly toast: ToastService,
|
private readonly toast: ToastService,
|
||||||
) {
|
) {
|
||||||
this.executionTargets$ = this.listExecutionTargets().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
this.form = this.buildForm();
|
||||||
this.executionConditions$ = this.listExecutionConditions().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
this.targets = this.listTargets();
|
||||||
|
|
||||||
|
this.selectedTargetIds = this.getSelectedTargetIds(this.form);
|
||||||
|
this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds);
|
||||||
|
this.dataSource = this.getDataSource(this.targets, this.selectedTargetIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildActionTargetForm() {
|
private buildForm() {
|
||||||
return this.fb.group(
|
const preselectedTargetIds = toSignal(this.preselectedTargetIds$, { initialValue: [] as string[] });
|
||||||
{
|
|
||||||
target: new FormControl<Target | null>(null, { validators: [] }),
|
return computed(() => {
|
||||||
executionConditions: new FormControl<Condition[]>([], { validators: [] }),
|
return this.fb.group({
|
||||||
},
|
autocomplete: new FormControl('', { nonNullable: true }),
|
||||||
{
|
selectedTargetIds: new FormControl(preselectedTargetIds(), {
|
||||||
validators: atLeastOneFieldValidator(['target', 'executionConditions']),
|
nonNullable: true,
|
||||||
},
|
validators: [minArrayLengthValidator(1)],
|
||||||
);
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private listExecutionTargets() {
|
private listTargets() {
|
||||||
return defer(() => this.actionService.listTargets({})).pipe(
|
const targetsSignal = signal({ state: 'loading' as 'loading' | 'loaded', targets: new Map<string, Target>() });
|
||||||
map(({ result }) => result.filter(this.targetHasDetailsAndConfig)),
|
|
||||||
catchError((error) => {
|
this.actionService
|
||||||
|
.listTargets({})
|
||||||
|
.then(({ result }) => {
|
||||||
|
const targets = result.reduce((acc, target) => {
|
||||||
|
acc.set(target.id, target);
|
||||||
|
return acc;
|
||||||
|
}, new Map<string, Target>());
|
||||||
|
|
||||||
|
targetsSignal.set({ state: 'loaded', targets });
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
return of([]);
|
});
|
||||||
|
|
||||||
|
return computed(targetsSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSelectedTargetIds(form: typeof this.form) {
|
||||||
|
const selectedTargetIds$ = toObservable(form).pipe(
|
||||||
|
startWith(form()),
|
||||||
|
switchMap((form) => {
|
||||||
|
const { selectedTargetIds } = form.controls;
|
||||||
|
return selectedTargetIds.valueChanges.pipe(startWith(selectedTargetIds.value));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
return toSignal(selectedTargetIds$, { requireSync: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
private listExecutionConditions(): Observable<Condition[]> {
|
private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal<string[]>) {
|
||||||
const selectedConditionJson$ = this.selectedCondition$.pipe(map((c) => JSON.stringify(c)));
|
return computed(() => {
|
||||||
|
const targetsCopy = new Map(targets().targets);
|
||||||
return defer(() => this.actionService.listExecutions({})).pipe(
|
for (const selectedTargetId of selectedTargetIds()) {
|
||||||
combineLatestWith(selectedConditionJson$),
|
targetsCopy.delete(selectedTargetId);
|
||||||
map(([executions, selectedConditionJson]) =>
|
|
||||||
executions.result.map((e) => e?.condition).filter(this.conditionIsDefinedAndNotCurrentOne(selectedConditionJson)),
|
|
||||||
),
|
|
||||||
|
|
||||||
catchError((error) => {
|
|
||||||
this.toast.showError(error);
|
|
||||||
return of([]);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private conditionIsDefinedAndNotCurrentOne(selectedConditionJson?: string) {
|
|
||||||
return (condition?: Condition): condition is Condition => {
|
|
||||||
if (!condition) {
|
|
||||||
// condition is undefined so it is not of type Condition
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if (!selectedConditionJson) {
|
return Array.from(targetsCopy.values());
|
||||||
// condition is defined, and we don't have a selectedCondition so we can return all conditions
|
});
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// we only return conditions that are not the same as the selectedCondition
|
|
||||||
return JSON.stringify(condition) !== selectedConditionJson;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private targetHasDetailsAndConfig(target: Target): target is Target {
|
private getDataSource(targetsSignal: typeof this.targets, selectedTargetIdsSignal: Signal<string[]>) {
|
||||||
return !!target.id && !!target.id;
|
const selectedTargets = computed(() => {
|
||||||
|
// get this out of the loop so angular can track this dependency
|
||||||
|
// even if targets is empty
|
||||||
|
const { targets, state } = targetsSignal();
|
||||||
|
const selectedTargetIds = selectedTargetIdsSignal();
|
||||||
|
|
||||||
|
if (state === 'loading') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedTargetIds.map((id) => {
|
||||||
|
const target = targets.get(id);
|
||||||
|
if (!target) {
|
||||||
|
throw new Error(`Target with id ${id} not found`);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataSource = new MatTableDataSource<Target>(selectedTargets());
|
||||||
|
effect(() => {
|
||||||
|
dataSource.data = selectedTargets();
|
||||||
|
});
|
||||||
|
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addTarget(target: Target) {
|
||||||
|
const { selectedTargetIds } = this.form().controls;
|
||||||
|
selectedTargetIds.setValue([target.id, ...selectedTargetIds.value]);
|
||||||
|
this.form().controls.autocomplete.setValue('');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected removeTarget(index: number) {
|
||||||
|
const { selectedTargetIds } = this.form().controls;
|
||||||
|
const data = [...selectedTargetIds.value];
|
||||||
|
data.splice(index, 1);
|
||||||
|
selectedTargetIds.setValue(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected drop(event: CdkDragDrop<undefined>) {
|
||||||
|
const { selectedTargetIds } = this.form().controls;
|
||||||
|
|
||||||
|
const data = [...selectedTargetIds.value];
|
||||||
|
moveItemInArray(data, event.previousIndex, event.currentIndex);
|
||||||
|
selectedTargetIds.setValue(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleEnter(event: Event) {
|
||||||
|
const selectableTargets = this.selectableTargets();
|
||||||
|
if (selectableTargets.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
this.addTarget(selectableTargets[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected submit() {
|
protected submit() {
|
||||||
const { target, executionConditions } = this.targetForm.getRawValue();
|
const selectedTargets = this.selectedTargetIds().map((value) => ({
|
||||||
|
type: {
|
||||||
|
case: 'target' as const,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
let valueToEmit: MessageInitShape<typeof ExecutionTargetTypeSchema>[] = target
|
this.continue.emit(selectedTargets);
|
||||||
? [
|
}
|
||||||
{
|
|
||||||
type: {
|
|
||||||
case: 'target',
|
|
||||||
value: target.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const includeConditions: MessageInitShape<typeof ExecutionTargetTypeSchema>[] = executionConditions
|
protected trackTarget(_: number, target: Target) {
|
||||||
? executionConditions.map((condition) => ({
|
return target.id;
|
||||||
type: {
|
|
||||||
case: 'include',
|
|
||||||
value: condition,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
valueToEmit = [...valueToEmit, ...includeConditions];
|
|
||||||
|
|
||||||
this.continue.emit(valueToEmit);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -59,7 +59,7 @@
|
|||||||
{{ 'ACTIONS.CANCEL' | translate }}
|
{{ 'ACTIONS.CANCEL' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button color="primary" [disabled]="targetForm.invalid" mat-raised-button (click)="closeWithResult()" cdkFocusInitial>
|
<button color="primary" [disabled]="targetForm.invalid" mat-raised-button (click)="closeWithResult()" cdkFocusInitial>
|
||||||
{{ 'ACTIONS.CREATE' | translate }}
|
{{ (data.target ? 'ACTIONS.CHANGE' : 'ACTIONS.CREATE') | translate }}
|
||||||
</button>
|
</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -44,7 +44,7 @@ export class ActionTwoAddTargetDialogComponent {
|
|||||||
ActionTwoAddTargetDialogComponent,
|
ActionTwoAddTargetDialogComponent,
|
||||||
MessageInitShape<typeof CreateTargetRequestSchema | typeof UpdateTargetRequestSchema>
|
MessageInitShape<typeof CreateTargetRequestSchema | typeof UpdateTargetRequestSchema>
|
||||||
>,
|
>,
|
||||||
@Inject(MAT_DIALOG_DATA) private readonly data: { target?: Target },
|
@Inject(MAT_DIALOG_DATA) public readonly data: { target?: Target },
|
||||||
) {
|
) {
|
||||||
this.targetForm = this.buildTargetForm();
|
this.targetForm = this.buildTargetForm();
|
||||||
|
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
<cnsl-refresh-table (refreshed)="refresh.emit()" [loading]="(dataSource$ | async) === null">
|
<cnsl-refresh-table (refreshed)="refresh.emit()" [loading]="(targets$ | async) === null">
|
||||||
<div actions>
|
<div actions>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table *ngIf="dataSource$ | async as dataSource" mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
|
<table mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
|
||||||
<!-- <ng-container matColumnDef="state">
|
<!-- <ng-container matColumnDef="state">
|
||||||
<th mat-header-cell *matHeaderCellDef>{{ 'APP.PAGES.STATE' | translate }}</th>
|
<th mat-header-cell *matHeaderCellDef>{{ 'APP.PAGES.STATE' | translate }}</th>
|
||||||
<td mat-cell *cnslCellDef="let row; let i = index; dataSource: dataSource">
|
<td mat-cell *cnslCellDef="let row; let i = index; dataSource: dataSource">
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, effect, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { ReplaySubject } from 'rxjs';
|
import { ReplaySubject } from 'rxjs';
|
||||||
import { filter, map } from 'rxjs/operators';
|
import { filter, startWith } from 'rxjs/operators';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-actions-two-targets-table',
|
selector: 'cnsl-actions-two-targets-table',
|
||||||
@@ -14,6 +15,9 @@ export class ActionsTwoTargetsTableComponent {
|
|||||||
@Output()
|
@Output()
|
||||||
public readonly refresh = new EventEmitter<void>();
|
public readonly refresh = new EventEmitter<void>();
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public readonly selected = new EventEmitter<Target>();
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
public readonly delete = new EventEmitter<Target>();
|
public readonly delete = new EventEmitter<Target>();
|
||||||
|
|
||||||
@@ -22,12 +26,24 @@ export class ActionsTwoTargetsTableComponent {
|
|||||||
this.targets$.next(targets);
|
this.targets$.next(targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Output()
|
protected readonly targets$ = new ReplaySubject<Target[] | null>(1);
|
||||||
public readonly selected = new EventEmitter<Target>();
|
protected readonly dataSource: MatTableDataSource<Target>;
|
||||||
|
|
||||||
private readonly targets$ = new ReplaySubject<Target[] | null>(1);
|
constructor() {
|
||||||
protected readonly dataSource$ = this.targets$.pipe(
|
this.dataSource = this.getDataSource();
|
||||||
filter(Boolean),
|
}
|
||||||
map((keys) => new MatTableDataSource(keys)),
|
|
||||||
);
|
private getDataSource() {
|
||||||
|
const targets$ = this.targets$.pipe(filter(Boolean), startWith<Target[]>([]));
|
||||||
|
const targetsSignal = toSignal(targets$, { requireSync: true });
|
||||||
|
|
||||||
|
const dataSource = new MatTableDataSource(targetsSignal());
|
||||||
|
effect(() => {
|
||||||
|
const targets = targetsSignal();
|
||||||
|
if (dataSource.data !== targets) {
|
||||||
|
dataSource.data = targets;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core';
|
||||||
import { defer, firstValueFrom, Observable, of, ReplaySubject, shareReplay, Subject, TimeoutError } from 'rxjs';
|
import { lastValueFrom, Observable, of, ReplaySubject } from 'rxjs';
|
||||||
import { ActionService } from 'src/app/services/action.service';
|
import { ActionService } from 'src/app/services/action.service';
|
||||||
import { NewFeatureService } from 'src/app/services/new-feature.service';
|
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { ORGANIZATIONS } from '../../settings-list/settings';
|
import { catchError, map, startWith, switchMap } from 'rxjs/operators';
|
||||||
import { catchError, filter, map, startWith, switchMap, timeout } from 'rxjs/operators';
|
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { ActionTwoAddTargetDialogComponent } from '../actions-two-add-target/actions-two-add-target-dialog.component';
|
import { ActionTwoAddTargetDialogComponent } from '../actions-two-add-target/actions-two-add-target-dialog.component';
|
||||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||||
@@ -22,66 +19,29 @@ import {
|
|||||||
styleUrls: ['./actions-two-targets.component.scss'],
|
styleUrls: ['./actions-two-targets.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ActionsTwoTargetsComponent implements OnInit {
|
export class ActionsTwoTargetsComponent {
|
||||||
private readonly actionsEnabled$: Observable<boolean>;
|
|
||||||
protected readonly targets$: Observable<Target[]>;
|
protected readonly targets$: Observable<Target[]>;
|
||||||
protected readonly refresh$ = new ReplaySubject<true>(1);
|
protected readonly refresh$ = new ReplaySubject<true>(1);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly actionService: ActionService,
|
private readonly actionService: ActionService,
|
||||||
private readonly featureService: NewFeatureService,
|
|
||||||
private readonly toast: ToastService,
|
private readonly toast: ToastService,
|
||||||
private readonly destroyRef: DestroyRef,
|
private readonly destroyRef: DestroyRef,
|
||||||
private readonly router: Router,
|
|
||||||
private readonly route: ActivatedRoute,
|
|
||||||
private readonly dialog: MatDialog,
|
private readonly dialog: MatDialog,
|
||||||
) {
|
) {
|
||||||
this.actionsEnabled$ = this.getActionsEnabled$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
this.targets$ = this.getTargets$();
|
||||||
this.targets$ = this.getTargets$(this.actionsEnabled$);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
private getTargets$() {
|
||||||
// this also preloads
|
|
||||||
this.actionsEnabled$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async (enabled) => {
|
|
||||||
if (enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.router.navigate([], {
|
|
||||||
relativeTo: this.route,
|
|
||||||
queryParams: {
|
|
||||||
id: ORGANIZATIONS.id,
|
|
||||||
},
|
|
||||||
queryParamsHandling: 'merge',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTargets$(actionsEnabled$: Observable<boolean>) {
|
|
||||||
return this.refresh$.pipe(
|
return this.refresh$.pipe(
|
||||||
startWith(true),
|
startWith(true),
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
return this.actionService.listTargets({});
|
return this.actionService.listTargets({});
|
||||||
}),
|
}),
|
||||||
map(({ result }) => result),
|
map(({ result }) => result),
|
||||||
catchError(async (err) => {
|
|
||||||
const actionsEnabled = await firstValueFrom(actionsEnabled$);
|
|
||||||
if (actionsEnabled) {
|
|
||||||
this.toast.showError(err);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getActionsEnabled$() {
|
|
||||||
return defer(() => this.featureService.getInstanceFeatures()).pipe(
|
|
||||||
map(({ actions }) => actions?.enabled ?? false),
|
|
||||||
timeout(1000),
|
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
if (!(err instanceof TimeoutError)) {
|
this.toast.showError(err);
|
||||||
this.toast.showError(err);
|
return of([]);
|
||||||
}
|
|
||||||
return of(false);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -92,30 +52,38 @@ export class ActionsTwoTargetsComponent implements OnInit {
|
|||||||
this.refresh$.next(true);
|
this.refresh$.next(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openDialog(target?: Target): void {
|
public async openDialog(target?: Target) {
|
||||||
const ref = this.dialog.open<
|
const request$ = this.dialog
|
||||||
ActionTwoAddTargetDialogComponent,
|
.open<
|
||||||
{ target?: Target },
|
ActionTwoAddTargetDialogComponent,
|
||||||
MessageInitShape<typeof UpdateTargetRequestSchema | typeof CreateTargetRequestSchema>
|
{ target?: Target },
|
||||||
>(ActionTwoAddTargetDialogComponent, {
|
MessageInitShape<typeof UpdateTargetRequestSchema | typeof CreateTargetRequestSchema>
|
||||||
width: '550px',
|
>(ActionTwoAddTargetDialogComponent, {
|
||||||
data: {
|
width: '550px',
|
||||||
target: target,
|
data: {
|
||||||
},
|
target: target,
|
||||||
});
|
},
|
||||||
|
})
|
||||||
ref
|
|
||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef));
|
||||||
.subscribe(async (dialogResponse) => {
|
|
||||||
if ('id' in dialogResponse) {
|
|
||||||
await this.actionService.updateTarget(dialogResponse);
|
|
||||||
} else {
|
|
||||||
await this.actionService.createTarget(dialogResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise((res) => setTimeout(res, 1000));
|
const request = await lastValueFrom(request$);
|
||||||
this.refresh$.next(true);
|
if (!request) {
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ('id' in request) {
|
||||||
|
await this.actionService.updateTarget(request);
|
||||||
|
} else {
|
||||||
|
await this.actionService.createTarget(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((res) => setTimeout(res, 1000));
|
||||||
|
this.refresh$.next(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
this.toast.showError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ import { WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
|
|||||||
})
|
})
|
||||||
export class OidcWebKeysInactiveTableComponent {
|
export class OidcWebKeysInactiveTableComponent {
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
public set InactiveWebKeys(webKeys: WebKey[] | null) {
|
public set inactiveWebKeys(webKeys: WebKey[] | null) {
|
||||||
this.inactiveWebKeys$.next(webKeys);
|
this.inactiveWebKeys$.next(webKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,4 +16,4 @@
|
|||||||
</button>
|
</button>
|
||||||
</cnsl-oidc-webkeys-table>
|
</cnsl-oidc-webkeys-table>
|
||||||
<cnsl-oidc-webkeys-create [loading]="createLoading()" (ngSubmit)="createWebKey($event)" />
|
<cnsl-oidc-webkeys-create [loading]="createLoading()" (ngSubmit)="createWebKey($event)" />
|
||||||
<cnsl-oidc-webkeys-inactive-table [InactiveWebKeys]="inactiveWebKeys$ | async" />
|
<cnsl-oidc-webkeys-inactive-table [inactiveWebKeys]="inactiveWebKeys$ | async" />
|
||||||
|
@@ -631,7 +631,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Име",
|
"NAME": "Име",
|
||||||
"ENDPOINT": "Крайна точка",
|
"ENDPOINT": "Крайна точка",
|
||||||
"CREATIONDATE": "Дата на създаване"
|
"CREATIONDATE": "Дата на създаване",
|
||||||
|
"REORDER": "Преоразмеряване"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Název",
|
"NAME": "Název",
|
||||||
"ENDPOINT": "Koncový bod",
|
"ENDPOINT": "Koncový bod",
|
||||||
"CREATIONDATE": "Datum vytvoření"
|
"CREATIONDATE": "Datum vytvoření",
|
||||||
|
"REORDER": "Změnit pořadí"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Name",
|
"NAME": "Name",
|
||||||
"ENDPOINT": "Endpunkt",
|
"ENDPOINT": "Endpunkt",
|
||||||
"CREATIONDATE": "Erstellungsdatum"
|
"CREATIONDATE": "Erstellungsdatum",
|
||||||
|
"REORDER": "Verschieben"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Name",
|
"NAME": "Name",
|
||||||
"ENDPOINT": "Endpoint",
|
"ENDPOINT": "Endpoint",
|
||||||
"CREATIONDATE": "Creation Date"
|
"CREATIONDATE": "Creation Date",
|
||||||
|
"REORDER": "Reorder"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Nombre",
|
"NAME": "Nombre",
|
||||||
"ENDPOINT": "Punto de conexión",
|
"ENDPOINT": "Punto de conexión",
|
||||||
"CREATIONDATE": "Fecha de creación"
|
"CREATIONDATE": "Fecha de creación",
|
||||||
|
"REORDER": "Reordenar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Nom",
|
"NAME": "Nom",
|
||||||
"ENDPOINT": "Point de terminaison",
|
"ENDPOINT": "Point de terminaison",
|
||||||
"CREATIONDATE": "Date de création"
|
"CREATIONDATE": "Date de création",
|
||||||
|
"REORDER": "Réorganiser"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Név",
|
"NAME": "Név",
|
||||||
"ENDPOINT": "Végpont",
|
"ENDPOINT": "Végpont",
|
||||||
"CREATIONDATE": "Létrehozás dátuma"
|
"CREATIONDATE": "Létrehozás dátuma",
|
||||||
|
"REORDER": "Újrarendelés"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -599,7 +599,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Nama",
|
"NAME": "Nama",
|
||||||
"ENDPOINT": "Titik Akhir",
|
"ENDPOINT": "Titik Akhir",
|
||||||
"CREATIONDATE": "Tanggal Pembuatan"
|
"CREATIONDATE": "Tanggal Pembuatan",
|
||||||
|
"REORDER": "Susun ulang"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -631,7 +631,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Nome",
|
"NAME": "Nome",
|
||||||
"ENDPOINT": "Endpoint",
|
"ENDPOINT": "Endpoint",
|
||||||
"CREATIONDATE": "Data di creazione"
|
"CREATIONDATE": "Data di creazione",
|
||||||
|
"REORDER": "Riordina"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "名前",
|
"NAME": "名前",
|
||||||
"ENDPOINT": "エンドポイント",
|
"ENDPOINT": "エンドポイント",
|
||||||
"CREATIONDATE": "作成日"
|
"CREATIONDATE": "作成日",
|
||||||
|
"REORDER": "順序を変更"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "이름",
|
"NAME": "이름",
|
||||||
"ENDPOINT": "엔드포인트",
|
"ENDPOINT": "엔드포인트",
|
||||||
"CREATIONDATE": "생성 날짜"
|
"CREATIONDATE": "생성 날짜",
|
||||||
|
"REORDER": "재정렬"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Име",
|
"NAME": "Име",
|
||||||
"ENDPOINT": "Крајна точка",
|
"ENDPOINT": "Крајна точка",
|
||||||
"CREATIONDATE": "Датум на создавање"
|
"CREATIONDATE": "Датум на создавање",
|
||||||
|
"REORDER": "Повторно нарачајте"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Naam",
|
"NAME": "Naam",
|
||||||
"ENDPOINT": "Eindpunt",
|
"ENDPOINT": "Eindpunt",
|
||||||
"CREATIONDATE": "Aanmaakdatum"
|
"CREATIONDATE": "Aanmaakdatum",
|
||||||
|
"REORDER": "Opnieuw ordenen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -631,7 +631,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Nazwa",
|
"NAME": "Nazwa",
|
||||||
"ENDPOINT": "Punkt końcowy",
|
"ENDPOINT": "Punkt końcowy",
|
||||||
"CREATIONDATE": "Data utworzenia"
|
"CREATIONDATE": "Data utworzenia",
|
||||||
|
"REORDER": "Zmień kolejność"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Nome",
|
"NAME": "Nome",
|
||||||
"ENDPOINT": "Ponto de Extremidade",
|
"ENDPOINT": "Ponto de Extremidade",
|
||||||
"CREATIONDATE": "Data de Criação"
|
"CREATIONDATE": "Data de Criação",
|
||||||
|
"REORDER": "Reordenar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Nume",
|
"NAME": "Nume",
|
||||||
"ENDPOINT": "Punct Final",
|
"ENDPOINT": "Punct Final",
|
||||||
"CREATIONDATE": "Data Creării"
|
"CREATIONDATE": "Data Creării",
|
||||||
|
"REORDER": "Reordonați"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Имя",
|
"NAME": "Имя",
|
||||||
"ENDPOINT": "Конечная точка",
|
"ENDPOINT": "Конечная точка",
|
||||||
"CREATIONDATE": "Дата создания"
|
"CREATIONDATE": "Дата создания",
|
||||||
|
"REORDER": "Изменить порядок"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Namn",
|
"NAME": "Namn",
|
||||||
"ENDPOINT": "Slutpunkt",
|
"ENDPOINT": "Slutpunkt",
|
||||||
"CREATIONDATE": "Skapat datum"
|
"CREATIONDATE": "Skapat datum",
|
||||||
|
"REORDER": "Ordna om"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -632,7 +632,8 @@
|
|||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "名称",
|
"NAME": "名称",
|
||||||
"ENDPOINT": "端点",
|
"ENDPOINT": "端点",
|
||||||
"CREATIONDATE": "创建日期"
|
"CREATIONDATE": "创建日期",
|
||||||
|
"REORDER": "重新排序"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user