mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 14:47:33 +00:00
chore!: Introduce ZITADEL v3 (#9645)
This PR summarizes multiple changes specifically only available with ZITADEL v3: - feat: Web Keys management (https://github.com/zitadel/zitadel/pull/9526) - fix(cmd): ensure proper working of mirror (https://github.com/zitadel/zitadel/pull/9509) - feat(Authz): system user support for permission check v2 (https://github.com/zitadel/zitadel/pull/9640) - chore(license): change from Apache to AGPL (https://github.com/zitadel/zitadel/pull/9597) - feat(console): list v2 sessions (https://github.com/zitadel/zitadel/pull/9539) - fix(console): add loginV2 feature flag (https://github.com/zitadel/zitadel/pull/9682) - fix(feature flags): allow reading "own" flags (https://github.com/zitadel/zitadel/pull/9649) - feat(console): add Actions V2 UI (https://github.com/zitadel/zitadel/pull/9591) BREAKING CHANGE - feat(webkey): migrate to v2beta API (https://github.com/zitadel/zitadel/pull/9445) - chore!: remove CockroachDB Support (https://github.com/zitadel/zitadel/pull/9444) - feat(actions): migrate to v2beta API (https://github.com/zitadel/zitadel/pull/9489) --------- Co-authored-by: Livio Spring <livio.a@gmail.com> Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com> Co-authored-by: Ramon <mail@conblem.me> Co-authored-by: Elio Bischof <elio@zitadel.com> Co-authored-by: Kenta Yamaguchi <56732734+KEY60228@users.noreply.github.com> Co-authored-by: Harsha Reddy <harsha.reddy@klaviyo.com> Co-authored-by: Livio Spring <livio@zitadel.com> Co-authored-by: Max Peintner <max@caos.ch> Co-authored-by: Iraq <66622793+kkrime@users.noreply.github.com> Co-authored-by: Florian Forster <florian@zitadel.com> Co-authored-by: Tim Möhlmann <tim+github@zitadel.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Max Peintner <peintnerm@gmail.com>
This commit is contained in:
@@ -32,7 +32,7 @@
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@ngx-translate/core": "^15.0.0",
|
||||
"@zitadel/client": "^1.0.7",
|
||||
"@zitadel/proto": "^1.0.4",
|
||||
"@zitadel/proto": "1.0.5-sha-4118a9d",
|
||||
"angular-oauth2-oidc": "^15.0.1",
|
||||
"angularx-qrcode": "^16.0.2",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -65,6 +65,7 @@
|
||||
"@angular/compiler-cli": "^16.2.5",
|
||||
"@angular/language-service": "^18.2.4",
|
||||
"@bufbuild/buf": "^1.41.0",
|
||||
"@netlify/framework-info": "^9.8.13",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/google-protobuf": "^3.15.3",
|
||||
"@types/jasmine": "~5.1.4",
|
||||
@@ -86,7 +87,6 @@
|
||||
"karma-jasmine-html-reporter": "^2.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"typescript": "5.1",
|
||||
"@netlify/framework-info": "^9.8.13"
|
||||
"typescript": "5.1"
|
||||
}
|
||||
}
|
||||
|
@@ -1,29 +1,29 @@
|
||||
<div class="feature-row" *ngIf="$any(toggleStates)[toggleStateKey]">
|
||||
<span>{{ 'SETTING.FEATURES.' + toggleStateKey.toUpperCase() | translate }}</span>
|
||||
<div class="feature-row" *ngIf="toggleState$ | async as toggleState">
|
||||
<span>{{ 'SETTING.FEATURES.' + (toggleStateKey | uppercase) | translate }}</span>
|
||||
<div class="row">
|
||||
<mat-button-toggle-group
|
||||
class="theme-toggle"
|
||||
class="buttongroup"
|
||||
[(ngModel)]="$any(toggleStates)[toggleStateKey].state"
|
||||
(change)="onToggleChange()"
|
||||
[(ngModel)]="toggleState.enabled"
|
||||
(change)="toggleChange.emit(toggleState)"
|
||||
name="displayview"
|
||||
>
|
||||
<mat-button-toggle [value]="ToggleState.DISABLED">
|
||||
<mat-button-toggle [value]="false">
|
||||
<div class="toggle-row">
|
||||
<span>{{ 'SETTING.FEATURES.STATES.DISABLED' | translate }}</span>
|
||||
|
||||
<div
|
||||
*ngIf="!enabled && isInherited"
|
||||
*ngIf="!toggleState.enabled && (isInherited$ | async)"
|
||||
class="current-dot"
|
||||
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.DISABLED' | translate }}"
|
||||
></div>
|
||||
</div>
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle [value]="ToggleState.ENABLED">
|
||||
<mat-button-toggle [value]="true">
|
||||
<div class="toggle-row">
|
||||
<span>{{ 'SETTING.FEATURES.STATES.ENABLED' | translate }}</span>
|
||||
<div
|
||||
*ngIf="enabled && isInherited"
|
||||
*ngIf="toggleState.enabled && (isInherited$ | async)"
|
||||
class="current-dot"
|
||||
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.ENABLED' | translate }}"
|
||||
></div>
|
||||
@@ -34,7 +34,7 @@
|
||||
<ng-content></ng-content>
|
||||
<cnsl-info-section
|
||||
class="feature-info"
|
||||
*ngIf="'SETTING.FEATURES.' + toggleStateKey.toUpperCase() + '_DESCRIPTION' | translate as i18nDescription"
|
||||
*ngIf="'SETTING.FEATURES.' + (toggleStateKey | uppercase) + '_DESCRIPTION' | translate as i18nDescription"
|
||||
>{{ i18nDescription }}</cnsl-info-section
|
||||
>
|
||||
</div>
|
||||
|
@@ -1,16 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AsyncPipe, NgIf, UpperCasePipe } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { CopyToClipboardModule } from '../../directives/copy-to-clipboard/copy-to-clipboard.module';
|
||||
import { CopyRowComponent } from '../copy-row/copy-row.component';
|
||||
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
|
||||
import { ToggleState, ToggleStateKeys, ToggleStates } from '../features/features.component';
|
||||
import { ToggleStateKeys, ToggleStates } from '../features/features.component';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { GetInstanceFeaturesResponse } from '@zitadel/proto/zitadel/feature/v2/instance_pb';
|
||||
import { FeatureFlag, Source } from '@zitadel/proto/zitadel/feature/v2/feature_pb';
|
||||
import { Source } from '@zitadel/proto/zitadel/feature/v2/feature_pb';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -19,37 +17,29 @@ import { FeatureFlag, Source } from '@zitadel/proto/zitadel/feature/v2/feature_p
|
||||
styleUrls: ['./feature-toggle.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
TranslateModule,
|
||||
MatButtonModule,
|
||||
InfoSectionModule,
|
||||
MatTooltipModule,
|
||||
CopyToClipboardModule,
|
||||
CopyRowComponent,
|
||||
MatButtonToggleModule,
|
||||
UpperCasePipe,
|
||||
TranslateModule,
|
||||
FormsModule,
|
||||
MatTooltipModule,
|
||||
InfoSectionModule,
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
],
|
||||
})
|
||||
export class FeatureToggleComponent {
|
||||
@Input() featureData: Partial<GetInstanceFeaturesResponse> = {};
|
||||
@Input() toggleStates: Partial<ToggleStates> = {};
|
||||
@Input() toggleStateKey: string = '';
|
||||
@Output() toggleChange = new EventEmitter<void>();
|
||||
|
||||
protected ToggleState = ToggleState;
|
||||
protected Source = Source;
|
||||
|
||||
get isInherited(): boolean {
|
||||
const source = this.featureData[this.toggleStateKey as ToggleStateKeys]?.source;
|
||||
return source == Source.SYSTEM || source == Source.UNSPECIFIED;
|
||||
export class FeatureToggleComponent<TKey extends ToggleStateKeys, TValue extends ToggleStates[TKey]> {
|
||||
@Input({ required: true }) toggleStateKey!: TKey;
|
||||
@Input({ required: true })
|
||||
set toggleState(toggleState: TValue) {
|
||||
// we copy the toggleState so we can mutate it
|
||||
this.toggleState$.next(structuredClone(toggleState));
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
// TODO: remove casting as not all features are a FeatureFlag
|
||||
return (this.featureData[this.toggleStateKey as ToggleStateKeys] as FeatureFlag)?.enabled;
|
||||
}
|
||||
@Output() readonly toggleChange = new EventEmitter<TValue>();
|
||||
|
||||
onToggleChange() {
|
||||
this.toggleChange.emit();
|
||||
}
|
||||
protected readonly Source = Source;
|
||||
protected readonly toggleState$ = new ReplaySubject<TValue>(1);
|
||||
protected readonly isInherited$ = this.toggleState$.pipe(
|
||||
map(({ source }) => source == Source.SYSTEM || source == Source.UNSPECIFIED),
|
||||
);
|
||||
}
|
||||
|
@@ -0,0 +1,27 @@
|
||||
<cnsl-feature-toggle
|
||||
*ngIf="toggleState$ | async as toggleState"
|
||||
toggleStateKey="loginV2"
|
||||
[toggleState]="toggleState"
|
||||
(toggleChange)="toggleState$.next($event); !$event.enabled && toggleChanged.emit($event)"
|
||||
>
|
||||
<cnsl-form-field *ngIf="toggleState.enabled">
|
||||
<cnsl-label>{{ 'SETTING.FEATURES.LOGINV2_BASEURI' | translate }}</cnsl-label>
|
||||
<input cnslInput [formControl]="baseUri" />
|
||||
<button
|
||||
matTooltip="{{ 'ACTIONS.SAVE' | translate }}"
|
||||
mat-icon-button
|
||||
[disabled]="baseUri.invalid"
|
||||
color="primary"
|
||||
type="submit"
|
||||
(click)="
|
||||
toggleChanged.emit({
|
||||
source: toggleState.source,
|
||||
enabled: toggleState.enabled,
|
||||
baseUri: baseUri.value,
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="las la-save"></i>
|
||||
</button>
|
||||
</cnsl-form-field>
|
||||
</cnsl-feature-toggle>
|
@@ -0,0 +1,47 @@
|
||||
import { ChangeDetectionStrategy, Component, DestroyRef, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FeatureToggleComponent } from '../feature-toggle.component';
|
||||
import { ToggleStates } from 'src/app/components/features/features.component';
|
||||
import { distinctUntilKeyChanged, ReplaySubject } from 'rxjs';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { InputModule } from 'src/app/modules/input/input.module';
|
||||
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'cnsl-login-v2-feature-toggle',
|
||||
templateUrl: './login-v2-feature-toggle.component.html',
|
||||
imports: [
|
||||
FeatureToggleComponent,
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
ReactiveFormsModule,
|
||||
InputModule,
|
||||
HasRolePipeModule,
|
||||
MatButtonModule,
|
||||
TranslateModule,
|
||||
MatTooltipModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LoginV2FeatureToggleComponent {
|
||||
@Input({ required: true })
|
||||
set toggleState(toggleState: ToggleStates['loginV2']) {
|
||||
this.toggleState$.next(toggleState);
|
||||
}
|
||||
@Output()
|
||||
public toggleChanged = new EventEmitter<ToggleStates['loginV2']>();
|
||||
|
||||
protected readonly toggleState$ = new ReplaySubject<ToggleStates['loginV2']>(1);
|
||||
protected readonly baseUri = new FormControl('', { nonNullable: true, validators: [Validators.required] });
|
||||
|
||||
constructor(destroyRef: DestroyRef) {
|
||||
this.toggleState$.pipe(distinctUntilKeyChanged('baseUri'), takeUntilDestroyed(destroyRef)).subscribe(({ baseUri }) => {
|
||||
this.baseUri.setValue(baseUri);
|
||||
});
|
||||
}
|
||||
}
|
@@ -13,26 +13,20 @@
|
||||
<p class="events-desc cnsl-secondary-text">{{ 'DESCRIPTIONS.SETTINGS.FEATURES.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['iam.restrictions.write']">
|
||||
<button color="warn" (click)="resetSettings()" mat-stroked-button>
|
||||
<button color="warn" (click)="resetFeatures()" mat-stroked-button>
|
||||
{{ 'SETTING.FEATURES.RESET' | translate }}
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<cnsl-card *ngIf="toggleStates && featureData">
|
||||
<cnsl-card *ngIf="toggleStates$ | async as toggleStates">
|
||||
<div class="features">
|
||||
<cnsl-feature-toggle
|
||||
*ngFor="let key of toggleStateKeys"
|
||||
[featureData]="featureData"
|
||||
[toggleStates]="toggleStates"
|
||||
*ngFor="let key of FEATURE_KEYS"
|
||||
[toggleStateKey]="key"
|
||||
(toggleChange)="validateAndSave()"
|
||||
[toggleState]="toggleStates[key]"
|
||||
(toggleChange)="saveFeatures(key, $event)"
|
||||
></cnsl-feature-toggle>
|
||||
<cnsl-login-v2-feature-toggle [toggleState]="toggleStates.loginV2" (toggleChanged)="saveFeatures('loginV2', $event)" />
|
||||
</div>
|
||||
</cnsl-card>
|
||||
</div>
|
||||
|
||||
<ng-template #sourceLabel let-source="source" let-last="last">
|
||||
<span class="state" *ngIf="source === Source.SOURCE_SYSTEM">
|
||||
{{ 'SETTING.FEATURES.SOURCE.' + source | translate }}
|
||||
</span>
|
||||
</ng-template>
|
||||
|
@@ -19,16 +19,14 @@ import {
|
||||
GetInstanceFeaturesResponse,
|
||||
SetInstanceFeaturesRequestSchema,
|
||||
} from '@zitadel/proto/zitadel/feature/v2/instance_pb';
|
||||
import { FeatureFlag, Source } from '@zitadel/proto/zitadel/feature/v2/feature_pb';
|
||||
import { Source } from '@zitadel/proto/zitadel/feature/v2/feature_pb';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { firstValueFrom, Observable, ReplaySubject, shareReplay, switchMap } from 'rxjs';
|
||||
import { filter, map, startWith } from 'rxjs/operators';
|
||||
import { LoginV2FeatureToggleComponent } from '../feature-toggle/login-v2-feature-toggle/login-v2-feature-toggle.component';
|
||||
|
||||
export enum ToggleState {
|
||||
ENABLED = 'ENABLED',
|
||||
DISABLED = 'DISABLED',
|
||||
}
|
||||
|
||||
// TODO: to add a new feature, add the key here and in the FEATURE_KEYS array
|
||||
const FEATURE_KEYS: ToggleStateKeys[] = [
|
||||
// to add a new feature, add the key here and in the FEATURE_KEYS array
|
||||
const FEATURE_KEYS = [
|
||||
'actions',
|
||||
'consoleUseV2UserApi',
|
||||
'debugOidcParentError',
|
||||
@@ -36,23 +34,24 @@ const FEATURE_KEYS: ToggleStateKeys[] = [
|
||||
'enableBackChannelLogout',
|
||||
// 'improvedPerformance',
|
||||
'loginDefaultOrg',
|
||||
// 'loginV2',
|
||||
'oidcLegacyIntrospection',
|
||||
'oidcSingleV1SessionTermination',
|
||||
'oidcTokenExchange',
|
||||
'oidcTriggerIntrospectionProjections',
|
||||
'permissionCheckV2',
|
||||
'userSchema',
|
||||
// 'webKey',
|
||||
];
|
||||
|
||||
type FeatureState = { source: Source; state: ToggleState };
|
||||
export type ToggleStateKeys = Exclude<keyof GetInstanceFeaturesResponse, 'details' | '$typeName' | '$unknown'>;
|
||||
'webKey',
|
||||
] as const;
|
||||
|
||||
export type ToggleState = { source: Source; enabled: boolean };
|
||||
export type ToggleStates = {
|
||||
[key in ToggleStateKeys]: FeatureState;
|
||||
[key in (typeof FEATURE_KEYS)[number]]: ToggleState;
|
||||
} & {
|
||||
loginV2: ToggleState & { baseUri: string };
|
||||
};
|
||||
|
||||
export type ToggleStateKeys = keyof ToggleStates;
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -68,6 +67,7 @@ export type ToggleStates = {
|
||||
MatTooltipModule,
|
||||
HasRoleModule,
|
||||
FeatureToggleComponent,
|
||||
LoginV2FeatureToggleComponent,
|
||||
],
|
||||
standalone: true,
|
||||
selector: 'cnsl-features',
|
||||
@@ -75,16 +75,15 @@ export type ToggleStates = {
|
||||
styleUrls: ['./features.component.scss'],
|
||||
})
|
||||
export class FeaturesComponent {
|
||||
protected featureData: GetInstanceFeaturesResponse | undefined;
|
||||
|
||||
protected toggleStates: ToggleStates | undefined;
|
||||
protected Source: any = Source;
|
||||
protected ToggleState: any = ToggleState;
|
||||
private readonly refresh$ = new ReplaySubject<true>(1);
|
||||
protected readonly toggleStates$: Observable<ToggleStates>;
|
||||
protected readonly Source = Source;
|
||||
protected readonly FEATURE_KEYS = FEATURE_KEYS;
|
||||
|
||||
constructor(
|
||||
private featureService: NewFeatureService,
|
||||
private breadcrumbService: BreadcrumbService,
|
||||
private toast: ToastService,
|
||||
private readonly featureService: NewFeatureService,
|
||||
private readonly breadcrumbService: BreadcrumbService,
|
||||
private readonly toast: ToastService,
|
||||
) {
|
||||
const breadcrumbs = [
|
||||
new Breadcrumb({
|
||||
@@ -95,74 +94,84 @@ export class FeaturesComponent {
|
||||
];
|
||||
this.breadcrumbService.setBreadcrumb(breadcrumbs);
|
||||
|
||||
this.getFeatures();
|
||||
this.toggleStates$ = this.getToggleStates().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
}
|
||||
|
||||
public validateAndSave() {
|
||||
const req: MessageInitShape<typeof SetInstanceFeaturesRequestSchema> = {
|
||||
actions: this.toggleStates?.actions?.state === ToggleState.ENABLED,
|
||||
consoleUseV2UserApi: this.toggleStates?.consoleUseV2UserApi?.state === ToggleState.ENABLED,
|
||||
debugOidcParentError: this.toggleStates?.debugOidcParentError?.state === ToggleState.ENABLED,
|
||||
disableUserTokenEvent: this.toggleStates?.disableUserTokenEvent?.state === ToggleState.ENABLED,
|
||||
enableBackChannelLogout: this.toggleStates?.enableBackChannelLogout?.state === ToggleState.ENABLED,
|
||||
loginDefaultOrg: this.toggleStates?.loginDefaultOrg?.state === ToggleState.ENABLED,
|
||||
oidcLegacyIntrospection: this.toggleStates?.oidcLegacyIntrospection?.state === ToggleState.ENABLED,
|
||||
oidcSingleV1SessionTermination: this.toggleStates?.oidcSingleV1SessionTermination?.state === ToggleState.ENABLED,
|
||||
oidcTokenExchange: this.toggleStates?.oidcTokenExchange?.state === ToggleState.ENABLED,
|
||||
oidcTriggerIntrospectionProjections:
|
||||
this.toggleStates?.oidcTriggerIntrospectionProjections?.state === ToggleState.ENABLED,
|
||||
permissionCheckV2: this.toggleStates?.permissionCheckV2?.state === ToggleState.ENABLED,
|
||||
userSchema: this.toggleStates?.userSchema?.state === ToggleState.ENABLED,
|
||||
// webKey: this.toggleStates?.webKey?.state === ToggleState.ENABLED,
|
||||
};
|
||||
|
||||
this.featureService
|
||||
.setInstanceFeatures(req)
|
||||
.then(() => {
|
||||
this.toast.showInfo('POLICY.TOAST.SET', true);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
private getFeatures() {
|
||||
this.featureService.getInstanceFeatures().then((instanceFeaturesResponse) => {
|
||||
this.featureData = instanceFeaturesResponse;
|
||||
this.toggleStates = this.createToggleStates(this.featureData);
|
||||
});
|
||||
private getToggleStates() {
|
||||
return this.refresh$.pipe(
|
||||
startWith(true),
|
||||
switchMap(async () => {
|
||||
try {
|
||||
return await this.featureService.getInstanceFeatures();
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
filter(Boolean),
|
||||
map((res) => this.createToggleStates(res)),
|
||||
);
|
||||
}
|
||||
|
||||
private createToggleStates(featureData: GetInstanceFeaturesResponse): ToggleStates {
|
||||
const toggleStates: Partial<ToggleStates> = {};
|
||||
|
||||
FEATURE_KEYS.forEach((key) => {
|
||||
// TODO: Fix this type cast as not all keys are present as FeatureFlag
|
||||
const feature = featureData[key] as unknown as FeatureFlag;
|
||||
toggleStates[key] = {
|
||||
source: feature?.source || Source.SYSTEM,
|
||||
state: !!feature?.enabled ? ToggleState.ENABLED : ToggleState.DISABLED,
|
||||
};
|
||||
});
|
||||
|
||||
return toggleStates as ToggleStates;
|
||||
return FEATURE_KEYS.reduce(
|
||||
(acc, key) => {
|
||||
const feature = featureData[key];
|
||||
acc[key] = {
|
||||
source: feature?.source ?? Source.SYSTEM,
|
||||
enabled: !!feature?.enabled,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
// to add special feature flags they have to be mapped here
|
||||
loginV2: {
|
||||
source: featureData.loginV2?.source ?? Source.SYSTEM,
|
||||
enabled: !!featureData.loginV2?.required,
|
||||
baseUri: featureData.loginV2?.baseUri ?? '',
|
||||
},
|
||||
} as ToggleStates,
|
||||
);
|
||||
}
|
||||
|
||||
public resetSettings(): void {
|
||||
this.featureService
|
||||
.resetInstanceFeatures()
|
||||
.then(() => {
|
||||
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
|
||||
setTimeout(() => {
|
||||
this.getFeatures();
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
public async saveFeatures<TKey extends ToggleStateKeys, TValue extends ToggleStates[TKey]>(key: TKey, value: TValue) {
|
||||
const toggleStates = { ...(await firstValueFrom(this.toggleStates$)), [key]: value };
|
||||
|
||||
const req = FEATURE_KEYS.reduce<MessageInitShape<typeof SetInstanceFeaturesRequestSchema>>((acc, key) => {
|
||||
acc[key] = toggleStates[key].enabled;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// to save special flags they have to be handled here
|
||||
req.loginV2 = {
|
||||
required: toggleStates.loginV2.enabled,
|
||||
baseUri: toggleStates.loginV2.baseUri,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.featureService.setInstanceFeatures(req);
|
||||
|
||||
// needed because of eventual consistency
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
this.refresh$.next(true);
|
||||
|
||||
this.toast.showInfo('POLICY.TOAST.SET', true);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public get toggleStateKeys() {
|
||||
return Object.keys(this.toggleStates ?? {});
|
||||
public async resetFeatures() {
|
||||
try {
|
||||
await this.featureService.resetInstanceFeatures();
|
||||
|
||||
// needed because of eventual consistency
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
this.refresh$.next(true);
|
||||
|
||||
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,16 @@
|
||||
import { Directive, Input } from '@angular/core';
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { MatCellDef } from '@angular/material/table';
|
||||
import { CdkCellDef } from '@angular/cdk/table';
|
||||
|
||||
@Directive({
|
||||
selector: '[cnslCellDef]',
|
||||
providers: [{ provide: CdkCellDef, useExisting: TypeSafeCellDefDirective }],
|
||||
})
|
||||
export class TypeSafeCellDefDirective<T> extends MatCellDef {
|
||||
@Input({ required: true }) cnslCellDefDataSource!: DataSource<T>;
|
||||
|
||||
static ngTemplateContextGuard<T>(_dir: TypeSafeCellDefDirective<T>, _ctx: any): _ctx is { $implicit: T; index: number } {
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { TypeSafeCellDefDirective } from './type-safe-cell-def.directive';
|
||||
|
||||
@NgModule({
|
||||
declarations: [TypeSafeCellDefDirective],
|
||||
imports: [CommonModule],
|
||||
exports: [TypeSafeCellDefDirective],
|
||||
})
|
||||
export class TypeSafeCellDefModule {}
|
@@ -1,7 +1,6 @@
|
||||
<div class="accounts-card">
|
||||
<div class="accounts-card" *ngIf="user$ | async as user">
|
||||
<cnsl-avatar
|
||||
(click)="editUserProfile()"
|
||||
*ngIf="user"
|
||||
class="avatar"
|
||||
[ngClass]="{ 'iam-user': iamuser }"
|
||||
[forColor]="user.preferredLoginName"
|
||||
@@ -16,8 +15,8 @@
|
||||
|
||||
<button (click)="editUserProfile()" mat-stroked-button>{{ 'USER.EDITACCOUNT' | translate }}</button>
|
||||
<div class="l-accounts">
|
||||
<mat-progress-bar *ngIf="loadingUsers" color="primary" mode="indeterminate"></mat-progress-bar>
|
||||
<a class="row" *ngFor="let session of sessions" (click)="selectAccount(session.loginName)">
|
||||
<mat-progress-bar *ngIf="(sessions$ | async) === null" color="primary" mode="indeterminate"></mat-progress-bar>
|
||||
<a class="row" *ngFor="let session of sessions$ | async" (click)="selectAccount(session.loginName)">
|
||||
<cnsl-avatar
|
||||
*ngIf="session && session.displayName"
|
||||
class="small-avatar"
|
||||
@@ -31,7 +30,7 @@
|
||||
<div class="col">
|
||||
<span class="user-title">{{ session.displayName ? session.displayName : session.userName }} </span>
|
||||
<span class="loginname">{{ session.loginName }}</span>
|
||||
<span class="state inactive" *ngIf="session.authState === UserState.USER_STATE_INACTIVE">{{
|
||||
<span class="state inactive" *ngIf="$any(session.authState) === UserState.USER_STATE_INACTIVE">{{
|
||||
'USER.STATE.' + session.authState | translate
|
||||
}}</span>
|
||||
</div>
|
||||
|
@@ -1,64 +1,156 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthConfig } from 'angular-oauth2-oidc';
|
||||
import { Session, User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { SessionState as V1SessionState, User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { SessionService } from 'src/app/services/session.service';
|
||||
import {
|
||||
catchError,
|
||||
defer,
|
||||
from,
|
||||
map,
|
||||
mergeMap,
|
||||
Observable,
|
||||
of,
|
||||
ReplaySubject,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
timeout,
|
||||
TimeoutError,
|
||||
toArray,
|
||||
} from 'rxjs';
|
||||
import { NewFeatureService } from 'src/app/services/new-feature.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { SessionState as V2SessionState } from '@zitadel/proto/zitadel/user_pb';
|
||||
import { filter, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
interface V1AndV2Session {
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
loginName: string;
|
||||
userName: string;
|
||||
authState: V1SessionState | V2SessionState;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-accounts-card',
|
||||
templateUrl: './accounts-card.component.html',
|
||||
styleUrls: ['./accounts-card.component.scss'],
|
||||
})
|
||||
export class AccountsCardComponent implements OnInit {
|
||||
@Input() public user?: User.AsObject;
|
||||
@Input() public iamuser: boolean | null = false;
|
||||
|
||||
@Output() public closedCard: EventEmitter<void> = new EventEmitter();
|
||||
public sessions: Session.AsObject[] = [];
|
||||
public loadingUsers: boolean = false;
|
||||
public UserState: any = UserState;
|
||||
private labelpolicy = toSignal(this.userService.labelpolicy$, { initialValue: undefined });
|
||||
|
||||
constructor(
|
||||
public authService: AuthenticationService,
|
||||
private router: Router,
|
||||
private userService: GrpcAuthService,
|
||||
) {
|
||||
this.userService
|
||||
.listMyUserSessions()
|
||||
.then((sessions) => {
|
||||
this.sessions = sessions.resultList.filter((user) => user.loginName !== this.user?.preferredLoginName);
|
||||
this.loadingUsers = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.loadingUsers = false;
|
||||
});
|
||||
export class AccountsCardComponent {
|
||||
@Input({ required: true })
|
||||
public set user(user: User.AsObject) {
|
||||
this.user$.next(user);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadingUsers = true;
|
||||
@Input() public iamuser: boolean | null = false;
|
||||
|
||||
@Output() public closedCard = new EventEmitter<void>();
|
||||
|
||||
protected readonly user$ = new ReplaySubject<User.AsObject>(1);
|
||||
protected readonly UserState = UserState;
|
||||
private readonly labelpolicy = toSignal(this.userService.labelpolicy$, { initialValue: undefined });
|
||||
protected readonly sessions$: Observable<V1AndV2Session[] | undefined>;
|
||||
|
||||
constructor(
|
||||
protected readonly authService: AuthenticationService,
|
||||
private readonly router: Router,
|
||||
private readonly userService: GrpcAuthService,
|
||||
private readonly sessionService: SessionService,
|
||||
private readonly featureService: NewFeatureService,
|
||||
private readonly toast: ToastService,
|
||||
) {
|
||||
this.sessions$ = this.getSessions().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
}
|
||||
|
||||
private getUseLoginV2() {
|
||||
return defer(() => this.featureService.getInstanceFeatures()).pipe(
|
||||
map(({ loginV2 }) => loginV2?.required ?? false),
|
||||
timeout(1000),
|
||||
catchError((err) => {
|
||||
if (!(err instanceof TimeoutError)) {
|
||||
this.toast.showError(err);
|
||||
}
|
||||
return of(false);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getSessions(): Observable<V1AndV2Session[]> {
|
||||
const useLoginV2$ = this.getUseLoginV2();
|
||||
|
||||
return useLoginV2$.pipe(
|
||||
switchMap((useLoginV2) => {
|
||||
if (useLoginV2) {
|
||||
return this.getV2Sessions();
|
||||
} else {
|
||||
return this.getV1Sessions();
|
||||
}
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.toast.showError(err);
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getV1Sessions(): Observable<V1AndV2Session[]> {
|
||||
return defer(() => this.userService.listMyUserSessions()).pipe(
|
||||
mergeMap(({ resultList }) => from(resultList)),
|
||||
withLatestFrom(this.user$),
|
||||
filter(([{ loginName }, user]) => loginName !== user.preferredLoginName),
|
||||
map(([s]) => ({
|
||||
displayName: s.displayName,
|
||||
avatarUrl: s.avatarUrl,
|
||||
loginName: s.loginName,
|
||||
authState: s.authState,
|
||||
userName: s.userName,
|
||||
})),
|
||||
toArray(),
|
||||
);
|
||||
}
|
||||
|
||||
private getV2Sessions(): Observable<V1AndV2Session[]> {
|
||||
return defer(() =>
|
||||
this.sessionService.listSessions({
|
||||
queries: [
|
||||
{
|
||||
query: {
|
||||
case: 'userAgentQuery',
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).pipe(
|
||||
mergeMap(({ sessions }) => from(sessions)),
|
||||
withLatestFrom(this.user$),
|
||||
filter(([s, user]) => s.factors?.user?.loginName !== user.preferredLoginName),
|
||||
map(([s]) => ({
|
||||
displayName: s.factors?.user?.displayName ?? '',
|
||||
avatarUrl: '',
|
||||
loginName: s.factors?.user?.loginName ?? '',
|
||||
authState: V2SessionState.ACTIVE,
|
||||
userName: s.factors?.user?.loginName ?? '',
|
||||
})),
|
||||
toArray(),
|
||||
);
|
||||
}
|
||||
|
||||
public editUserProfile(): void {
|
||||
this.router.navigate(['users/me']);
|
||||
this.router.navigate(['users/me']).then();
|
||||
this.closedCard.emit();
|
||||
}
|
||||
|
||||
public closeCard(element: HTMLElement): void {
|
||||
if (!element.classList.contains('dontcloseonclick')) {
|
||||
this.closedCard.emit();
|
||||
}
|
||||
}
|
||||
|
||||
public selectAccount(loginHint: string): void {
|
||||
const configWithPrompt: Partial<AuthConfig> = {
|
||||
customQueryParams: {
|
||||
login_hint: loginHint,
|
||||
},
|
||||
};
|
||||
this.authService.authenticate(configWithPrompt);
|
||||
this.authService.authenticate(configWithPrompt).then();
|
||||
}
|
||||
|
||||
public selectNewAccount(): void {
|
||||
@@ -67,7 +159,7 @@ export class AccountsCardComponent implements OnInit {
|
||||
prompt: 'login',
|
||||
} as any,
|
||||
};
|
||||
this.authService.authenticate(configWithPrompt);
|
||||
this.authService.authenticate(configWithPrompt).then();
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
|
@@ -0,0 +1,75 @@
|
||||
<cnsl-refresh-table (refreshed)="refresh.emit()" [loading]="(dataSource$ | async) === null">
|
||||
<div actions>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table *ngIf="dataSource$ | async as dataSource" mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="condition">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.EXECUTION.TABLE.CONDITION' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
<span *ngIf="row.condition?.conditionType?.value">
|
||||
{{ row?.condition | condition }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.EXECUTION.TABLE.TYPE' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
{{ 'ACTIONSTWO.EXECUTION.TYPES.' + row?.condition?.conditionType?.case | translate }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="target">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.EXECUTION.TABLE.TARGET' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
<div class="target-key">
|
||||
<cnsl-project-role-chip *ngFor="let target of filteredTargetTypes(row.targets) | async" [roleName]="target.name"
|
||||
>{{ target.name }}
|
||||
</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>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="creationDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'ACTIONSTWO.EXECUTION.TABLE.CREATIONDATE' | translate }}
|
||||
</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
<span class="no-break">{{ row.creationDate | timestampToDate | localizedDate: 'regular' }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
<cnsl-table-actions>
|
||||
<button
|
||||
actions
|
||||
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
|
||||
color="warn"
|
||||
(click)="$event.stopPropagation(); delete.emit(row)"
|
||||
mat-icon-button
|
||||
>
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
</cnsl-table-actions>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['condition', 'type', 'target', 'creationDate', 'actions']"></tr>
|
||||
<tr
|
||||
class="highlight pointer"
|
||||
mat-row
|
||||
*matRowDef="let row; columns: ['condition', 'type', 'target', 'creationDate', 'actions']"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
</cnsl-refresh-table>
|
@@ -0,0 +1,12 @@
|
||||
.target-key {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
margin-right: 0.5rem;
|
||||
margin-left: -0.5rem;
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions-two-actions-table',
|
||||
templateUrl: './actions-two-actions-table.component.html',
|
||||
styleUrls: ['./actions-two-actions-table.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ActionsTwoActionsTableComponent {
|
||||
@Output()
|
||||
public readonly refresh = new EventEmitter<void>();
|
||||
|
||||
@Output()
|
||||
public readonly delete = new EventEmitter<Execution>();
|
||||
|
||||
@Input({ required: true })
|
||||
public set executions(executions: Execution[] | null) {
|
||||
this.executions$.next(executions);
|
||||
}
|
||||
|
||||
@Input({ required: true })
|
||||
public set targets(targets: Target[] | null) {
|
||||
this.targets$.next(targets);
|
||||
}
|
||||
|
||||
@Output()
|
||||
public readonly selected = new EventEmitter<Execution>();
|
||||
|
||||
private readonly executions$ = new ReplaySubject<Execution[] | null>(1);
|
||||
private readonly targets$ = new ReplaySubject<Target[] | null>(1);
|
||||
|
||||
protected readonly dataSource$ = this.executions$.pipe(
|
||||
filter(Boolean),
|
||||
map((keys) => new MatTableDataSource(keys)),
|
||||
);
|
||||
|
||||
protected filteredTargetTypes(targets: ExecutionTargetType[]): Observable<Target[]> {
|
||||
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(
|
||||
filter(Boolean),
|
||||
map((alltargets) => alltargets!.filter((target) => targetIds.includes(target.id))),
|
||||
);
|
||||
}
|
||||
|
||||
protected filteredIncludeConditions(targets: ExecutionTargetType[]): Condition[] {
|
||||
return targets
|
||||
.map((t) => t.type)
|
||||
.filter((t): t is Extract<ExecutionTargetType['type'], { case: 'include' }> => t.case === 'include')
|
||||
.map(({ value }) => value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
<h2>{{ 'ACTIONSTWO.EXECUTION.TITLE' | translate }}</h2>
|
||||
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.EXECUTION.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-actions-two-actions-table
|
||||
(refresh)="refresh.next(true)"
|
||||
(delete)="deleteExecution($event)"
|
||||
(selected)="openDialog($event)"
|
||||
[executions]="executions$ | async"
|
||||
[targets]="targets$ | async"
|
||||
>
|
||||
<button color="primary" mat-raised-button (click)="openDialog()">
|
||||
{{ 'ACTIONS.CREATE' | translate }}
|
||||
</button>
|
||||
</cnsl-actions-two-actions-table>
|
@@ -0,0 +1,143 @@
|
||||
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit } from '@angular/core';
|
||||
import { ActionService } from 'src/app/services/action.service';
|
||||
import { NewFeatureService } from 'src/app/services/new-feature.service';
|
||||
import { defer, firstValueFrom, Observable, of, shareReplay, Subject, TimeoutError } from 'rxjs';
|
||||
import { catchError, filter, map, startWith, switchMap, tap, timeout } from 'rxjs/operators';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ORGANIZATIONS } from '../../settings-list/settings';
|
||||
import { ActionTwoAddActionDialogComponent } from '../actions-two-add-action/actions-two-add-action-dialog.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
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 { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import { Value } from 'google-protobuf/google/protobuf/struct_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions-two-actions',
|
||||
templateUrl: './actions-two-actions.component.html',
|
||||
styleUrls: ['./actions-two-actions.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ActionsTwoActionsComponent implements OnInit {
|
||||
protected readonly refresh = new Subject<true>();
|
||||
private readonly actionsEnabled$: Observable<boolean>;
|
||||
protected readonly executions$: Observable<Execution[]>;
|
||||
protected readonly targets$: Observable<Target[]>;
|
||||
|
||||
constructor(
|
||||
private readonly actionService: ActionService,
|
||||
private readonly featureService: NewFeatureService,
|
||||
private readonly toast: ToastService,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly router: Router,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly dialog: MatDialog,
|
||||
) {
|
||||
this.actionsEnabled$ = this.getActionsEnabled$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.executions$ = this.getExecutions$(this.actionsEnabled$);
|
||||
this.targets$ = this.getTargets$(this.actionsEnabled$);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// 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 getExecutions$(actionsEnabled$: Observable<boolean>) {
|
||||
return this.refresh.pipe(
|
||||
startWith(true),
|
||||
switchMap(() => {
|
||||
return this.actionService.listExecutions({});
|
||||
}),
|
||||
map(({ result }) => result),
|
||||
catchError(async (err) => {
|
||||
const actionsEnabled = await firstValueFrom(actionsEnabled$);
|
||||
if (actionsEnabled) {
|
||||
this.toast.showError(err);
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getTargets$(actionsEnabled$: Observable<boolean>) {
|
||||
return this.refresh.pipe(
|
||||
startWith(true),
|
||||
switchMap(() => {
|
||||
return this.actionService.listTargets({});
|
||||
}),
|
||||
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) => {
|
||||
if (!(err instanceof TimeoutError)) {
|
||||
this.toast.showError(err);
|
||||
}
|
||||
return of(false);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public openDialog(execution?: Execution): void {
|
||||
const ref = this.dialog.open<ActionTwoAddActionDialogComponent>(ActionTwoAddActionDialogComponent, {
|
||||
width: '400px',
|
||||
data: execution
|
||||
? {
|
||||
execution: execution,
|
||||
}
|
||||
: {},
|
||||
});
|
||||
|
||||
ref.afterClosed().subscribe((request?: MessageInitShape<typeof SetExecutionRequestSchema>) => {
|
||||
if (request) {
|
||||
this.actionService
|
||||
.setExecution(request)
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
this.refresh.next(true);
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteExecution(execution: Execution) {
|
||||
const deleteReq: MessageInitShape<typeof SetExecutionRequestSchema> = {
|
||||
condition: execution.condition,
|
||||
targets: [],
|
||||
};
|
||||
await this.actionService.setExecution(deleteReq);
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
this.refresh.next(true);
|
||||
}
|
||||
}
|
@@ -0,0 +1,116 @@
|
||||
<form *ngIf="form$ | async as form" [formGroup]="form.form" class="form-grid" (ngSubmit)="submit(form)">
|
||||
<ng-container *ngIf="form.case === 'request' || form.case === 'response'">
|
||||
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.REQ_RESP_DESCRIPTION' | translate }}</p>
|
||||
|
||||
<div class="emailVerified">
|
||||
<mat-checkbox [formControl]="form.form.controls.all">
|
||||
<div class="execution-condition-text">
|
||||
<span>{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.TITLE' | translate }}</span>
|
||||
<span class="description cnsl-secondary-text">{{
|
||||
'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.DESCRIPTION' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<p class="condition-description">{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_SERVICE.TITLE' | translate }}</p>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_SERVICE.DESCRIPTION' | translate }}</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
type="text"
|
||||
placeholder=""
|
||||
[formControl]="form.form.controls.service"
|
||||
[matAutocomplete]="autoservice"
|
||||
/>
|
||||
|
||||
<mat-autocomplete #autoservice="matAutocomplete">
|
||||
<mat-option *ngIf="(executionServices$ | async) === null" class="is-loading">
|
||||
<mat-spinner diameter="30"></mat-spinner>
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let service of executionServices$ | async" [value]="service">
|
||||
<span>{{ service }}</span>
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</cnsl-form-field>
|
||||
|
||||
<p class="condition-description">{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_METHOD.TITLE' | translate }}</p>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_METHOD.DESCRIPTION' | translate }}</cnsl-label>
|
||||
<input cnslInput type="text" placeholder="" [formControl]="form.form.controls.method" [matAutocomplete]="automethod" />
|
||||
|
||||
<mat-autocomplete #automethod="matAutocomplete">
|
||||
<mat-option *ngIf="(executionMethods$ | async) === null" class="is-loading">
|
||||
<mat-spinner diameter="30"></mat-spinner>
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let method of executionMethods$ | async" [value]="method">
|
||||
<span>{{ method }}</span>
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</cnsl-form-field>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="form.case === 'function'">
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.FUNCTIONNAME.TITLE' | translate }}</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
type="text"
|
||||
placeholder=""
|
||||
[formControl]="form.form.controls.name"
|
||||
[matAutocomplete]="autofunctionname"
|
||||
/>
|
||||
|
||||
<mat-autocomplete #autofunctionname="matAutocomplete">
|
||||
<mat-option *ngIf="(executionFunctions$ | async) === null" class="is-loading">
|
||||
<mat-spinner diameter="30"></mat-spinner>
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let function of executionFunctions$ | async" [value]="function">
|
||||
<span>{{ function }}</span>
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
|
||||
<span class="name-hint cnsl-secondary-text" cnslHint>{{
|
||||
'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.FUNCTIONNAME.DESCRIPTION' | translate
|
||||
}}</span>
|
||||
</cnsl-form-field>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="form.case === 'event'">
|
||||
<div class="emailVerified">
|
||||
<mat-checkbox [formControl]="form.form.controls.all">
|
||||
<div class="execution-condition-text">
|
||||
<span>{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.TITLE' | translate }}</span>
|
||||
<span class="description cnsl-secondary-text">{{
|
||||
'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.DESCRIPTION' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<p class="condition-description">{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_GROUP.TITLE' | translate }}</p>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_GROUP.DESCRIPTION' | translate }}</cnsl-label>
|
||||
<input cnslInput type="text" placeholder="" #nameInput [formControl]="form.form.controls.group" />
|
||||
</cnsl-form-field>
|
||||
|
||||
<p class="condition-description">{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_EVENT.TITLE' | translate }}</p>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.SELECT_EVENT.DESCRIPTION' | translate }}</cnsl-label>
|
||||
<input cnslInput type="text" placeholder="" #nameInput [formControl]="form.form.controls.event" />
|
||||
</cnsl-form-field>
|
||||
</ng-container>
|
||||
|
||||
<div class="actions">
|
||||
<button mat-stroked-button (click)="back.emit()">
|
||||
{{ 'ACTIONS.BACK' | translate }}
|
||||
</button>
|
||||
<button [disabled]="form.form.invalid" color="primary" mat-raised-button type="submit">
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
@@ -0,0 +1,21 @@
|
||||
.execution-condition-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.description {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.condition-description {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.name-hint {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActionsTwoAddActionConditionComponent } from './actions-two-add-action-condition.component';
|
||||
|
||||
describe('ActionsTwoAddActionConditionComponent', () => {
|
||||
let component: ActionsTwoAddActionConditionComponent;
|
||||
let fixture: ComponentFixture<ActionsTwoAddActionConditionComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ActionsTwoAddActionConditionComponent],
|
||||
});
|
||||
fixture = TestBed.createComponent(ActionsTwoAddActionConditionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,343 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, DestroyRef, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { InputModule } from 'src/app/modules/input/input.module';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
ValidatorFn,
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
Observable,
|
||||
catchError,
|
||||
defer,
|
||||
map,
|
||||
of,
|
||||
shareReplay,
|
||||
ReplaySubject,
|
||||
ObservedValueOf,
|
||||
switchMap,
|
||||
combineLatestWith,
|
||||
OperatorFunction,
|
||||
} from 'rxjs';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { ActionService } from 'src/app/services/action.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { atLeastOneFieldValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
import { Message } from '@bufbuild/protobuf';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Condition } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
||||
import { startWith } from 'rxjs/operators';
|
||||
|
||||
export type ConditionType = NonNullable<Condition['conditionType']['case']>;
|
||||
export type ConditionTypeValue<T extends ConditionType> = Omit<
|
||||
NonNullable<Extract<Condition['conditionType'], { case: T }>['value']>,
|
||||
// we remove the message keys so $typeName is not required
|
||||
keyof Message
|
||||
>;
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'cnsl-actions-two-add-action-condition',
|
||||
templateUrl: './actions-two-add-action-condition.component.html',
|
||||
styleUrls: ['./actions-two-add-action-condition.component.scss'],
|
||||
imports: [
|
||||
TranslateModule,
|
||||
MatRadioModule,
|
||||
RouterModule,
|
||||
ReactiveFormsModule,
|
||||
InputModule,
|
||||
MatAutocompleteModule,
|
||||
MatCheckboxModule,
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
MatProgressSpinnerModule,
|
||||
],
|
||||
})
|
||||
export class ActionsTwoAddActionConditionComponent<T extends ConditionType = ConditionType> {
|
||||
@Input({ required: true }) public set conditionType(conditionType: T) {
|
||||
this.conditionType$.next(conditionType);
|
||||
}
|
||||
@Output() public readonly back = new EventEmitter<void>();
|
||||
@Output() public readonly continue = new EventEmitter<ConditionTypeValue<T>>();
|
||||
|
||||
private readonly conditionType$ = new ReplaySubject<T>(1);
|
||||
protected readonly form$: ReturnType<typeof this.buildForm>;
|
||||
|
||||
protected readonly executionServices$: Observable<string[]>;
|
||||
protected readonly executionMethods$: Observable<string[]>;
|
||||
protected readonly executionFunctions$: Observable<string[]>;
|
||||
|
||||
constructor(
|
||||
private readonly fb: FormBuilder,
|
||||
private readonly actionService: ActionService,
|
||||
private readonly toast: ToastService,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
) {
|
||||
this.form$ = this.buildForm().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.executionServices$ = this.listExecutionServices(this.form$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.executionMethods$ = this.listExecutionMethods(this.form$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.executionFunctions$ = this.listExecutionFunctions(this.form$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
}
|
||||
|
||||
public buildForm() {
|
||||
return this.conditionType$.pipe(
|
||||
switchMap((conditionType) => {
|
||||
if (conditionType === 'event') {
|
||||
return this.buildEventForm();
|
||||
}
|
||||
if (conditionType === 'function') {
|
||||
return this.buildFunctionForm();
|
||||
}
|
||||
return this.buildRequestOrResponseForm(conditionType);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private buildRequestOrResponseForm<T extends 'request' | 'response'>(requestOrResponse: T) {
|
||||
const formFactory = () => ({
|
||||
case: requestOrResponse,
|
||||
form: this.fb.group(
|
||||
{
|
||||
all: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
service: new FormControl<string>('', { nonNullable: true }),
|
||||
method: new FormControl<string>('', { nonNullable: true }),
|
||||
},
|
||||
{
|
||||
validators: atLeastOneFieldValidator(['all', 'service', 'method']),
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
return new Observable<ReturnType<typeof formFactory>>((obs) => {
|
||||
const form = formFactory();
|
||||
obs.next(form);
|
||||
|
||||
const { all, service, method } = form.form.controls;
|
||||
return all.valueChanges
|
||||
.pipe(
|
||||
map(() => all.value),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((all) => {
|
||||
this.toggleFormControl(service, !all);
|
||||
this.toggleFormControl(method, !all);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public buildFunctionForm() {
|
||||
return of({
|
||||
case: 'function' as const,
|
||||
form: this.fb.group({
|
||||
name: new FormControl<string>('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
public buildEventForm() {
|
||||
const formFactory = () => ({
|
||||
case: 'event' as const,
|
||||
form: this.fb.group({
|
||||
all: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
group: new FormControl<string>('', { nonNullable: true }),
|
||||
event: new FormControl<string>('', { nonNullable: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
return new Observable<ReturnType<typeof formFactory>>((obs) => {
|
||||
const form = formFactory();
|
||||
obs.next(form);
|
||||
|
||||
const { all, group, event } = form.form.controls;
|
||||
return all.valueChanges
|
||||
.pipe(
|
||||
map(() => all.value),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((all) => {
|
||||
this.toggleFormControl(group, !all);
|
||||
this.toggleFormControl(event, !all);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private toggleFormControl(control: FormControl, enabled: boolean) {
|
||||
if (enabled) {
|
||||
control.enable();
|
||||
} else {
|
||||
control.disable();
|
||||
}
|
||||
}
|
||||
|
||||
private listExecutionServices(form$: typeof this.form$) {
|
||||
return defer(() => this.actionService.listExecutionServices({})).pipe(
|
||||
map(({ services }) => services),
|
||||
this.formFilter(form$, (form) => {
|
||||
if ('service' in form.form.controls) {
|
||||
return form.form.controls.service;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
catchError((error) => {
|
||||
this.toast.showError(error);
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private listExecutionFunctions(form$: typeof this.form$) {
|
||||
return defer(() => this.actionService.listExecutionFunctions({})).pipe(
|
||||
map(({ functions }) => functions),
|
||||
this.formFilter(form$, (form) => {
|
||||
if (form.case !== 'function') {
|
||||
return undefined;
|
||||
}
|
||||
return form.form.controls.name;
|
||||
}),
|
||||
catchError((error) => {
|
||||
this.toast.showError(error);
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private listExecutionMethods(form$: typeof this.form$) {
|
||||
return defer(() => this.actionService.listExecutionMethods({})).pipe(
|
||||
map(({ methods }) => methods),
|
||||
this.formFilter(form$, (form) => {
|
||||
if ('method' in form.form.controls) {
|
||||
return form.form.controls.method;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
// we also filter by service name
|
||||
this.formFilter(form$, (form) => {
|
||||
if ('service' in form.form.controls) {
|
||||
return form.form.controls.service;
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
catchError((error) => {
|
||||
this.toast.showError(error);
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private formFilter(
|
||||
form$: typeof this.form$,
|
||||
getter: (form: ObservedValueOf<typeof this.form$>) => FormControl<string> | undefined,
|
||||
): OperatorFunction<string[], string[]> {
|
||||
const filterValue$ = form$.pipe(
|
||||
map(getter),
|
||||
switchMap((control) => {
|
||||
if (!control) {
|
||||
return of('');
|
||||
}
|
||||
|
||||
return control.valueChanges.pipe(
|
||||
startWith(control.value),
|
||||
map((value) => value.toLowerCase()),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return (obs) =>
|
||||
obs.pipe(
|
||||
combineLatestWith(filterValue$),
|
||||
map(([values, filterValue]) => values.filter((v) => v.toLowerCase().includes(filterValue))),
|
||||
);
|
||||
}
|
||||
|
||||
protected submit(form: ObservedValueOf<typeof this.form$>) {
|
||||
if (form.case === 'request' || form.case === 'response') {
|
||||
(this as unknown as ActionsTwoAddActionConditionComponent<'request' | 'response'>).submitRequestOrResponse(form);
|
||||
} else if (form.case === 'event') {
|
||||
(this as unknown as ActionsTwoAddActionConditionComponent<'event'>).submitEvent(form);
|
||||
} else if (form.case === 'function') {
|
||||
(this as unknown as ActionsTwoAddActionConditionComponent<'function'>).submitFunction(form);
|
||||
}
|
||||
}
|
||||
|
||||
private submitRequestOrResponse(
|
||||
this: ActionsTwoAddActionConditionComponent<'request' | 'response'>,
|
||||
{ form }: ObservedValueOf<ReturnType<typeof this.buildRequestOrResponseForm>>,
|
||||
) {
|
||||
const { all, service, method } = form.getRawValue();
|
||||
|
||||
if (all) {
|
||||
this.continue.emit({
|
||||
condition: {
|
||||
case: 'all',
|
||||
value: true,
|
||||
},
|
||||
});
|
||||
} else if (method) {
|
||||
this.continue.emit({
|
||||
condition: {
|
||||
case: 'method',
|
||||
value: method,
|
||||
},
|
||||
});
|
||||
} else if (service) {
|
||||
this.continue.emit({
|
||||
condition: {
|
||||
case: 'service',
|
||||
value: service,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private submitEvent(
|
||||
this: ActionsTwoAddActionConditionComponent<'event'>,
|
||||
{ form }: ObservedValueOf<ReturnType<typeof this.buildEventForm>>,
|
||||
) {
|
||||
const { all, event, group } = form.getRawValue();
|
||||
if (all) {
|
||||
this.continue.emit({
|
||||
condition: {
|
||||
case: 'all',
|
||||
value: true,
|
||||
},
|
||||
});
|
||||
} else if (event) {
|
||||
this.continue.emit({
|
||||
condition: {
|
||||
case: 'event',
|
||||
value: event,
|
||||
},
|
||||
});
|
||||
} else if (group) {
|
||||
this.continue.emit({
|
||||
condition: {
|
||||
case: 'group',
|
||||
value: group,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private submitFunction(
|
||||
this: ActionsTwoAddActionConditionComponent<'function'>,
|
||||
{ form }: ObservedValueOf<ReturnType<typeof this.buildFunctionForm>>,
|
||||
) {
|
||||
const { name } = form.getRawValue();
|
||||
this.continue.emit({
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
<h2 *ngIf="!data.execution" mat-dialog-title>{{ 'ACTIONSTWO.EXECUTION.DIALOG.CREATE_TITLE' | translate }}</h2>
|
||||
<h2 *ngIf="data.execution" mat-dialog-title>{{ 'ACTIONSTWO.EXECUTION.DIALOG.UPDATE_TITLE' | translate }}</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="framework-change-block" [ngSwitch]="page()">
|
||||
<cnsl-actions-two-add-action-type
|
||||
*ngSwitchCase="Page.Type"
|
||||
[initialValue]="typeSignal()"
|
||||
(back)="back()"
|
||||
(continue)="typeSignal.set($event); continue()"
|
||||
></cnsl-actions-two-add-action-type>
|
||||
|
||||
<cnsl-actions-two-add-action-condition
|
||||
*ngSwitchCase="Page.Condition"
|
||||
[conditionType]="typeSignal()"
|
||||
(back)="back()"
|
||||
(continue)="conditionSignal.set($event); continue()"
|
||||
></cnsl-actions-two-add-action-condition>
|
||||
|
||||
<cnsl-actions-two-add-action-target
|
||||
*ngSwitchCase="Page.Target"
|
||||
(back)="back()"
|
||||
[hideBackButton]="!!data.execution"
|
||||
(continue)="targetSignal.set($event); continue()"
|
||||
[selectedCondition]="data.execution?.condition"
|
||||
></cnsl-actions-two-add-action-target>
|
||||
</div>
|
||||
</mat-dialog-content>
|
@@ -0,0 +1,18 @@
|
||||
.framework-change-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.show {
|
||||
visibility: visible;
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
import { Component, computed, effect, Inject, signal } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ActionsTwoAddActionTypeComponent } from './actions-two-add-action-type/actions-two-add-action-type.component';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import {
|
||||
ActionsTwoAddActionConditionComponent,
|
||||
ConditionType,
|
||||
ConditionTypeValue,
|
||||
} 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 { CommonModule } from '@angular/common';
|
||||
import { Execution, ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
||||
import { Subject } from 'rxjs';
|
||||
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
|
||||
enum Page {
|
||||
Type,
|
||||
Condition,
|
||||
Target,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions-two-add-action-dialog',
|
||||
templateUrl: './actions-two-add-action-dialog.component.html',
|
||||
styleUrls: ['./actions-two-add-action-dialog.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
TranslateModule,
|
||||
ActionsTwoAddActionTypeComponent,
|
||||
ActionsTwoAddActionConditionComponent,
|
||||
ActionsTwoAddActionTargetComponent,
|
||||
],
|
||||
})
|
||||
export class ActionTwoAddActionDialogComponent {
|
||||
public Page = Page;
|
||||
public page = signal<Page | undefined>(Page.Type);
|
||||
|
||||
public typeSignal = signal<ConditionType>('request');
|
||||
public conditionSignal = signal<ConditionTypeValue<ConditionType> | undefined>(undefined); // TODO: fix this type
|
||||
public targetSignal = signal<Array<MessageInitShape<typeof ExecutionTargetTypeSchema>> | undefined>(undefined);
|
||||
|
||||
public continueSubject = new Subject<void>();
|
||||
|
||||
public request = computed<MessageInitShape<typeof SetExecutionRequestSchema>>(() => {
|
||||
return {
|
||||
condition: {
|
||||
conditionType: {
|
||||
case: this.typeSignal(),
|
||||
value: this.conditionSignal() as any, // TODO: fix this type
|
||||
},
|
||||
},
|
||||
targets: this.targetSignal(),
|
||||
};
|
||||
});
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ActionTwoAddActionDialogComponent, MessageInitShape<typeof SetExecutionRequestSchema>>,
|
||||
@Inject(MAT_DIALOG_DATA) protected readonly data: { execution?: Execution },
|
||||
) {
|
||||
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(() => {
|
||||
const currentPage = this.page();
|
||||
if (currentPage === Page.Target) {
|
||||
this.continueSubject.next(); // Trigger the Subject to request condition form when the page changes to "Target"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public continue() {
|
||||
const currentPage = this.page();
|
||||
if (currentPage === Page.Type) {
|
||||
this.page.set(Page.Condition);
|
||||
} else if (currentPage === Page.Condition) {
|
||||
this.page.set(Page.Target);
|
||||
} else {
|
||||
this.dialogRef.close(this.request());
|
||||
}
|
||||
}
|
||||
|
||||
public back() {
|
||||
const currentPage = this.page();
|
||||
if (currentPage === Page.Target) {
|
||||
this.page.set(Page.Condition);
|
||||
} else if (currentPage === Page.Condition) {
|
||||
this.page.set(Page.Type);
|
||||
} else {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
<form *ngIf="targetForm" class="form-grid" [formGroup]="targetForm" (ngSubmit)="submit()">
|
||||
<p class="target-description">{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.TARGET.DESCRIPTION' | translate }}</cnsl-label>
|
||||
<mat-select formControlName="target">
|
||||
<mat-option *ngIf="(executionTargets$ | async) === null" class="is-loading">
|
||||
<mat-spinner diameter="30"></mat-spinner>
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let target of executionTargets$ | async" [value]="target">
|
||||
{{ target.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
|
||||
<!-- <cnsl-form-field class="full-width">-->
|
||||
<!-- <cnsl-label>{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.CONDITIONS.DESCRIPTION' | translate }}</cnsl-label>-->
|
||||
|
||||
<!-- <mat-select [multiple]="true" formControlName="executionConditions">-->
|
||||
<!-- <mat-option *ngIf="(executionConditions$ | async) === null" class="is-loading">-->
|
||||
<!-- <mat-spinner diameter="30"></mat-spinner>-->
|
||||
<!-- </mat-option>-->
|
||||
<!-- <mat-option *ngFor="let condition of executionConditions$ | async" [value]="condition">-->
|
||||
<!-- <span>{{ condition | condition }}</span>-->
|
||||
<!-- </mat-option>-->
|
||||
<!-- </mat-select>-->
|
||||
<!-- </cnsl-form-field>-->
|
||||
|
||||
<div class="actions">
|
||||
<button *ngIf="!hideBackButton" mat-stroked-button (click)="back.emit()">
|
||||
{{ 'ACTIONS.BACK' | translate }}
|
||||
</button>
|
||||
<span class="fill-space"></span>
|
||||
<button color="primary" [disabled]="targetForm.invalid" mat-raised-button type="submit">
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
@@ -0,0 +1,12 @@
|
||||
.target-description {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.fill-space {
|
||||
font: 1;
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActionsTwoAddActionTargetComponent } from './actions-two-add-action-target.component';
|
||||
|
||||
describe('ActionsTwoAddActionTargetComponent', () => {
|
||||
let component: ActionsTwoAddActionTargetComponent;
|
||||
let fixture: ComponentFixture<ActionsTwoAddActionTargetComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ActionsTwoAddActionTargetComponent],
|
||||
});
|
||||
fixture = TestBed.createComponent(ActionsTwoAddActionTargetComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,155 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Observable, catchError, defer, map, of, shareReplay, ReplaySubject, combineLatestWith } from 'rxjs';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { ActionService } from 'src/app/services/action.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { InputModule } from 'src/app/modules/input/input.module';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
import { Condition, ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
||||
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';
|
||||
|
||||
export type TargetInit = NonNullable<
|
||||
NonNullable<MessageInitShape<typeof SetExecutionRequestSchema>['targets']>
|
||||
>[number]['type'];
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'cnsl-actions-two-add-action-target',
|
||||
templateUrl: './actions-two-add-action-target.component.html',
|
||||
styleUrls: ['./actions-two-add-action-target.component.scss'],
|
||||
imports: [
|
||||
TranslateModule,
|
||||
MatRadioModule,
|
||||
RouterModule,
|
||||
ReactiveFormsModule,
|
||||
InputModule,
|
||||
MatAutocompleteModule,
|
||||
FormsModule,
|
||||
ActionConditionPipeModule,
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSelectModule,
|
||||
],
|
||||
})
|
||||
export class ActionsTwoAddActionTargetComponent {
|
||||
protected readonly targetForm = this.buildActionTargetForm();
|
||||
|
||||
@Output() public readonly back = new EventEmitter<void>();
|
||||
@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);
|
||||
|
||||
protected readonly executionTargets$: Observable<Target[]>;
|
||||
protected readonly executionConditions$: Observable<Condition[]>;
|
||||
|
||||
constructor(
|
||||
private readonly fb: FormBuilder,
|
||||
private readonly actionService: ActionService,
|
||||
private readonly toast: ToastService,
|
||||
) {
|
||||
this.executionTargets$ = this.listExecutionTargets().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.executionConditions$ = this.listExecutionConditions().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
}
|
||||
|
||||
private buildActionTargetForm() {
|
||||
return this.fb.group(
|
||||
{
|
||||
target: new FormControl<Target | null>(null, { validators: [] }),
|
||||
executionConditions: new FormControl<Condition[]>([], { validators: [] }),
|
||||
},
|
||||
{
|
||||
validators: atLeastOneFieldValidator(['target', 'executionConditions']),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private listExecutionTargets() {
|
||||
return defer(() => this.actionService.listTargets({})).pipe(
|
||||
map(({ result }) => result.filter(this.targetHasDetailsAndConfig)),
|
||||
catchError((error) => {
|
||||
this.toast.showError(error);
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private listExecutionConditions(): Observable<Condition[]> {
|
||||
const selectedConditionJson$ = this.selectedCondition$.pipe(map((c) => JSON.stringify(c)));
|
||||
|
||||
return defer(() => this.actionService.listExecutions({})).pipe(
|
||||
combineLatestWith(selectedConditionJson$),
|
||||
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) {
|
||||
// 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 {
|
||||
return !!target.id && !!target.id;
|
||||
}
|
||||
|
||||
protected submit() {
|
||||
const { target, executionConditions } = this.targetForm.getRawValue();
|
||||
|
||||
let valueToEmit: MessageInitShape<typeof ExecutionTargetTypeSchema>[] = target
|
||||
? [
|
||||
{
|
||||
type: {
|
||||
case: 'target',
|
||||
value: target.id,
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const includeConditions: MessageInitShape<typeof ExecutionTargetTypeSchema>[] = executionConditions
|
||||
? executionConditions.map((condition) => ({
|
||||
type: {
|
||||
case: 'include',
|
||||
value: condition,
|
||||
},
|
||||
}))
|
||||
: [];
|
||||
|
||||
valueToEmit = [...valueToEmit, ...includeConditions];
|
||||
|
||||
this.continue.emit(valueToEmit);
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<form class="form-grid" [formGroup]="typeForm" (ngSubmit)="submit()">
|
||||
<div class="executionType">
|
||||
<mat-radio-group class="execution-radio-group" aria-label="Select an option" formControlName="executionType">
|
||||
<mat-radio-button class="execution-radio-button" [value]="'request'">
|
||||
<div class="execution-type-text">
|
||||
<span>{{ 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.REQUEST.TITLE' | translate }}</span>
|
||||
<span class="description cnsl-secondary-text">{{
|
||||
'ACTIONSTWO.EXECUTION.DIALOG.TYPE.REQUEST.DESCRIPTION' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</mat-radio-button>
|
||||
<mat-radio-button class="execution-radio-button" [value]="'response'"
|
||||
><div class="execution-type-text">
|
||||
<span>{{ 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.RESPONSE.TITLE' | translate }}</span>
|
||||
<span class="description cnsl-secondary-text">{{
|
||||
'ACTIONSTWO.EXECUTION.DIALOG.TYPE.RESPONSE.DESCRIPTION' | translate
|
||||
}}</span>
|
||||
</div></mat-radio-button
|
||||
>
|
||||
<mat-radio-button class="execution-radio-button" [value]="'event'"
|
||||
><div class="execution-type-text">
|
||||
<span>{{ 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.EVENTS.TITLE' | translate }}</span>
|
||||
<span class="description cnsl-secondary-text">{{
|
||||
'ACTIONSTWO.EXECUTION.DIALOG.TYPE.EVENTS.DESCRIPTION' | translate
|
||||
}}</span>
|
||||
</div></mat-radio-button
|
||||
>
|
||||
<mat-radio-button class="execution-radio-button" [value]="'function'"
|
||||
><div class="execution-type-text">
|
||||
<span>{{ 'ACTIONSTWO.EXECUTION.DIALOG.TYPE.FUNCTIONS.TITLE' | translate }}</span>
|
||||
<span class="description cnsl-secondary-text">{{
|
||||
'ACTIONSTWO.EXECUTION.DIALOG.TYPE.FUNCTIONS.DESCRIPTION' | translate
|
||||
}}</span>
|
||||
</div></mat-radio-button
|
||||
>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button mat-stroked-button (click)="back.emit()">
|
||||
{{ 'ACTIONS.CANCEL' | translate }}
|
||||
</button>
|
||||
<button color="primary" mat-raised-button type="submit">
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
@@ -0,0 +1,20 @@
|
||||
.execution-radio-group {
|
||||
.execution-radio-button {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.execution-type-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.description {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActionsTwoAddActionTypeComponent } from './actions-two-add-action-type.component';
|
||||
|
||||
describe('ActionsTwoAddActionTypeComponent', () => {
|
||||
let component: ActionsTwoAddActionTypeComponent;
|
||||
let fixture: ComponentFixture<ActionsTwoAddActionTypeComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ActionsTwoAddActionTypeComponent],
|
||||
});
|
||||
fixture = TestBed.createComponent(ActionsTwoAddActionTypeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,53 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, signal } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Observable, Subject, map, of, startWith, switchMap, tap } from 'rxjs';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { ConditionType } from '../actions-two-add-action-condition/actions-two-add-action-condition.component';
|
||||
|
||||
// export enum ExecutionType {
|
||||
// REQUEST = 'request',
|
||||
// RESPONSE = 'response',
|
||||
// EVENTS = 'event',
|
||||
// FUNCTIONS = 'function',
|
||||
// }
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'cnsl-actions-two-add-action-type',
|
||||
templateUrl: './actions-two-add-action-type.component.html',
|
||||
styleUrls: ['./actions-two-add-action-type.component.scss'],
|
||||
imports: [TranslateModule, MatRadioModule, RouterModule, ReactiveFormsModule, FormsModule, CommonModule, MatButtonModule],
|
||||
})
|
||||
export class ActionsTwoAddActionTypeComponent {
|
||||
protected readonly typeForm: ReturnType<typeof this.buildActionTypeForm> = this.buildActionTypeForm();
|
||||
@Output() public readonly typeChanges$: Observable<ConditionType>;
|
||||
|
||||
@Output() public readonly back = new EventEmitter<void>();
|
||||
@Output() public readonly continue = new EventEmitter<ConditionType>();
|
||||
@Input() public set initialValue(type: ConditionType) {
|
||||
this.typeForm.get('executionType')!.setValue(type);
|
||||
}
|
||||
|
||||
constructor(private readonly fb: FormBuilder) {
|
||||
this.typeChanges$ = this.typeForm.get('executionType')!.valueChanges.pipe(
|
||||
startWith(this.typeForm.get('executionType')!.value), // Emit the initial value
|
||||
);
|
||||
}
|
||||
|
||||
public buildActionTypeForm() {
|
||||
return this.fb.group({
|
||||
executionType: new FormControl<ConditionType>('request', {
|
||||
nonNullable: true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
public submit() {
|
||||
this.continue.emit(this.typeForm.get('executionType')!.value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
<h2 mat-dialog-title>{{ 'ACTIONSTWO.TARGET.CREATE.TITLE' | translate }}</h2>
|
||||
<mat-dialog-content>
|
||||
<p class="target-description">{{ 'ACTIONSTWO.TARGET.CREATE.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<form *ngIf="targetForm" class="form-grid" [formGroup]="targetForm" (ngSubmit)="closeWithResult()">
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.TARGET.CREATE.NAME' | translate }}</cnsl-label>
|
||||
<input cnslInput type="text" placeholder="" formControlName="name" />
|
||||
<span class="name-hint cnsl-secondary-text" cnslHint>{{
|
||||
'ACTIONSTWO.TARGET.CREATE.NAME_DESCRIPTION' | translate
|
||||
}}</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.TARGET.CREATE.ENDPOINT' | translate }}</cnsl-label>
|
||||
<input cnslInput type="text" placeholder="" formControlName="endpoint" />
|
||||
<span class="name-hint cnsl-secondary-text" cnslHint>{{
|
||||
'ACTIONSTWO.TARGET.CREATE.ENDPOINT_DESCRIPTION' | translate
|
||||
}}</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.TARGET.CREATE.TYPE' | translate }}</cnsl-label>
|
||||
<mat-select [formControl]="targetForm.controls.type" name="type">
|
||||
<mat-option *ngFor="let type of targetTypes" [value]="type">
|
||||
{{ 'ACTIONSTWO.TARGET.CREATE.TYPES.' + type | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.TARGET.CREATE.TIMEOUT' | translate }}</cnsl-label>
|
||||
<input cnslInput type="number" placeholder="10" [formControl]="targetForm.controls.timeout" />
|
||||
<span class="name-hint cnsl-secondary-text" cnslHint>{{
|
||||
'ACTIONSTWO.TARGET.CREATE.TIMEOUT_DESCRIPTION' | translate
|
||||
}}</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<mat-checkbox
|
||||
*ngIf="targetForm.controls.type.value === 'restWebhook' || targetForm.controls.type.value === 'restCall'"
|
||||
class="target-checkbox"
|
||||
[formControl]="targetForm.controls.interruptOnError"
|
||||
>
|
||||
<div class="target-condition-text">
|
||||
<span>{{ 'ACTIONSTWO.TARGET.CREATE.INTERRUPT_ON_ERROR' | translate }}</span>
|
||||
<span class="description cnsl-secondary-text">{{
|
||||
'ACTIONSTWO.TARGET.CREATE.INTERRUPT_ON_ERROR_DESCRIPTION' | translate
|
||||
}}</span>
|
||||
<span [style.color]="'var(--warn)'" class="description cnsl-secondary-text"
|
||||
>{{ 'ACTIONSTWO.TARGET.CREATE.INTERRUPT_ON_ERROR_WARNING' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</mat-checkbox>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<div>
|
||||
<mat-dialog-actions class="actions">
|
||||
<button mat-stroked-button mat-dialog-close>
|
||||
{{ 'ACTIONS.CANCEL' | translate }}
|
||||
</button>
|
||||
<button color="primary" [disabled]="targetForm.invalid" mat-raised-button (click)="closeWithResult()" cdkFocusInitial>
|
||||
{{ 'ACTIONS.CREATE' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
@@ -0,0 +1,25 @@
|
||||
.target-checkbox {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.target-condition-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.target-description {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name-hint {
|
||||
font-size: 12px;
|
||||
}
|
@@ -0,0 +1,115 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { InputModule } from '../../input/input.module';
|
||||
import { requiredValidator } from '../../form-field/validators/validators';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { DurationSchema } from '@bufbuild/protobuf/wkt';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import {
|
||||
CreateTargetRequestSchema,
|
||||
UpdateTargetRequestSchema,
|
||||
} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
|
||||
type TargetTypes = ActionTwoAddTargetDialogComponent['targetTypes'][number];
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions-two-add-target-dialog',
|
||||
templateUrl: './actions-two-add-target-dialog.component.html',
|
||||
styleUrls: ['./actions-two-add-target-dialog.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
InputModule,
|
||||
MatCheckboxModule,
|
||||
MatSelectModule,
|
||||
],
|
||||
})
|
||||
export class ActionTwoAddTargetDialogComponent {
|
||||
protected readonly targetTypes = ['restCall', 'restWebhook', 'restAsync'] as const;
|
||||
protected readonly targetForm: ReturnType<typeof this.buildTargetForm>;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
public dialogRef: MatDialogRef<
|
||||
ActionTwoAddTargetDialogComponent,
|
||||
MessageInitShape<typeof CreateTargetRequestSchema | typeof UpdateTargetRequestSchema>
|
||||
>,
|
||||
@Inject(MAT_DIALOG_DATA) private readonly data: { target?: Target },
|
||||
) {
|
||||
this.targetForm = this.buildTargetForm();
|
||||
|
||||
if (!data?.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.targetForm.patchValue({
|
||||
name: data.target.name,
|
||||
endpoint: data.target.endpoint,
|
||||
timeout: Number(data.target.timeout?.seconds),
|
||||
type: this.data.target?.targetType?.case ?? 'restWebhook',
|
||||
interruptOnError:
|
||||
data.target.targetType.case === 'restWebhook' || data.target.targetType.case === 'restCall'
|
||||
? data.target.targetType.value.interruptOnError
|
||||
: false,
|
||||
});
|
||||
}
|
||||
|
||||
public buildTargetForm() {
|
||||
return this.fb.group({
|
||||
name: new FormControl<string>('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
type: new FormControl<TargetTypes>('restWebhook', {
|
||||
nonNullable: true,
|
||||
validators: [requiredValidator],
|
||||
}),
|
||||
endpoint: new FormControl<string>('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
timeout: new FormControl<number>(10, { nonNullable: true, validators: [requiredValidator] }),
|
||||
interruptOnError: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
public closeWithResult() {
|
||||
if (this.targetForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, name, endpoint, timeout, interruptOnError } = this.targetForm.getRawValue();
|
||||
|
||||
const timeoutDuration: MessageInitShape<typeof DurationSchema> = {
|
||||
seconds: BigInt(timeout),
|
||||
nanos: 0,
|
||||
};
|
||||
|
||||
const targetType: Extract<MessageInitShape<typeof CreateTargetRequestSchema>['targetType'], { case: TargetTypes }> =
|
||||
type === 'restWebhook'
|
||||
? { case: type, value: { interruptOnError } }
|
||||
: type === 'restCall'
|
||||
? { case: type, value: { interruptOnError } }
|
||||
: { case: 'restAsync', value: {} };
|
||||
|
||||
const baseReq = {
|
||||
name,
|
||||
endpoint,
|
||||
timeout: timeoutDuration,
|
||||
targetType,
|
||||
};
|
||||
|
||||
this.dialogRef.close(
|
||||
this.data.target
|
||||
? {
|
||||
...baseReq,
|
||||
id: this.data.target.id,
|
||||
}
|
||||
: baseReq,
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { ActionsTwoActionsComponent } from './actions-two-actions/actions-two-actions.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ActionsTwoActionsComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ActionsTwoRoutingModule {}
|
@@ -0,0 +1,82 @@
|
||||
<cnsl-refresh-table (refreshed)="refresh.emit()" [loading]="(dataSource$ | async) === null">
|
||||
<div actions>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table *ngIf="dataSource$ | async as dataSource" mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
|
||||
<!-- <ng-container matColumnDef="state">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'APP.PAGES.STATE' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; let i = index; dataSource: dataSource">
|
||||
<span
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: i === 0,
|
||||
neutral: i === 1,
|
||||
}"
|
||||
[ngSwitch]="i"
|
||||
>
|
||||
<ng-container *ngSwitchCase="0">{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.ACTIVE' | translate }}</ng-container>
|
||||
<ng-container *ngSwitchCase="1">{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.NEXT' | translate }}</ng-container>
|
||||
<ng-container *ngSwitchDefault>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.FUTURE' | translate }}</ng-container>
|
||||
</span>
|
||||
</td>
|
||||
</ng-container> -->
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.TARGET.TABLE.ID' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
{{ row.id }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.TARGET.TABLE.NAME' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
<div class="target-key">
|
||||
<cnsl-project-role-chip [roleName]="row.name">{{ row.name }}</cnsl-project-role-chip>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="endpoint">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.TARGET.TABLE.ENDPOINT' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
{{ row.endpoint }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="creationDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'ACTIONSTWO.TARGET.TABLE.CREATIONDATE' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
<span class="no-break">{{ row.creationDate | timestampToDate | localizedDate: 'regular' }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
<!-- <ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TYPE.TITLE' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
{{ row.key.case | uppercase }}
|
||||
</td>
|
||||
</ng-container> -->
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
<cnsl-table-actions>
|
||||
<button
|
||||
actions
|
||||
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
|
||||
color="warn"
|
||||
(click)="$event.stopPropagation(); delete.emit(row)"
|
||||
mat-icon-button
|
||||
>
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
</cnsl-table-actions>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['name', 'endpoint', 'creationDate', 'actions']"></tr>
|
||||
<tr
|
||||
class="highlight pointer"
|
||||
(click)="selected.emit(row)"
|
||||
mat-row
|
||||
*matRowDef="let row; columns: ['name', 'endpoint', 'creationDate', 'actions']"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
</cnsl-refresh-table>
|
@@ -0,0 +1,4 @@
|
||||
.target-key {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions-two-targets-table',
|
||||
templateUrl: './actions-two-targets-table.component.html',
|
||||
styleUrls: ['./actions-two-targets-table.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ActionsTwoTargetsTableComponent {
|
||||
@Output()
|
||||
public readonly refresh = new EventEmitter<void>();
|
||||
|
||||
@Output()
|
||||
public readonly delete = new EventEmitter<Target>();
|
||||
|
||||
@Input({ required: true })
|
||||
public set targets(targets: Target[] | null) {
|
||||
this.targets$.next(targets);
|
||||
}
|
||||
|
||||
@Output()
|
||||
public readonly selected = new EventEmitter<Target>();
|
||||
|
||||
private readonly targets$ = new ReplaySubject<Target[] | null>(1);
|
||||
protected readonly dataSource$ = this.targets$.pipe(
|
||||
filter(Boolean),
|
||||
map((keys) => new MatTableDataSource(keys)),
|
||||
);
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
<h2>{{ 'ACTIONSTWO.TARGET.TITLE' | translate }}</h2>
|
||||
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.TARGET.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-actions-two-targets-table
|
||||
(refresh)="refresh$.next(true)"
|
||||
(delete)="deleteTarget($event)"
|
||||
(selected)="openDialog($event)"
|
||||
[targets]="targets$ | async"
|
||||
>
|
||||
<button color="primary" mat-raised-button (click)="openDialog()">
|
||||
{{ 'ACTIONS.CREATE' | translate }}
|
||||
</button>
|
||||
</cnsl-actions-two-targets-table>
|
@@ -0,0 +1,121 @@
|
||||
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit } from '@angular/core';
|
||||
import { defer, firstValueFrom, Observable, of, ReplaySubject, shareReplay, Subject, TimeoutError } from 'rxjs';
|
||||
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 { ActivatedRoute, Router } from '@angular/router';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ORGANIZATIONS } from '../../settings-list/settings';
|
||||
import { catchError, filter, map, startWith, switchMap, timeout } from 'rxjs/operators';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActionTwoAddTargetDialogComponent } from '../actions-two-add-target/actions-two-add-target-dialog.component';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import {
|
||||
CreateTargetRequestSchema,
|
||||
UpdateTargetRequestSchema,
|
||||
} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions-two-targets',
|
||||
templateUrl: './actions-two-targets.component.html',
|
||||
styleUrls: ['./actions-two-targets.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ActionsTwoTargetsComponent implements OnInit {
|
||||
private readonly actionsEnabled$: Observable<boolean>;
|
||||
protected readonly targets$: Observable<Target[]>;
|
||||
protected readonly refresh$ = new ReplaySubject<true>(1);
|
||||
|
||||
constructor(
|
||||
private readonly actionService: ActionService,
|
||||
private readonly featureService: NewFeatureService,
|
||||
private readonly toast: ToastService,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly router: Router,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly dialog: MatDialog,
|
||||
) {
|
||||
this.actionsEnabled$ = this.getActionsEnabled$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.targets$ = this.getTargets$(this.actionsEnabled$);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// 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(
|
||||
startWith(true),
|
||||
switchMap(() => {
|
||||
return this.actionService.listTargets({});
|
||||
}),
|
||||
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) => {
|
||||
if (!(err instanceof TimeoutError)) {
|
||||
this.toast.showError(err);
|
||||
}
|
||||
return of(false);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteTarget(target: Target) {
|
||||
await this.actionService.deleteTarget({ id: target.id });
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
this.refresh$.next(true);
|
||||
}
|
||||
|
||||
public openDialog(target?: Target): void {
|
||||
const ref = this.dialog.open<
|
||||
ActionTwoAddTargetDialogComponent,
|
||||
{ target?: Target },
|
||||
MessageInitShape<typeof UpdateTargetRequestSchema | typeof CreateTargetRequestSchema>
|
||||
>(ActionTwoAddTargetDialogComponent, {
|
||||
width: '550px',
|
||||
data: {
|
||||
target: target,
|
||||
},
|
||||
});
|
||||
|
||||
ref
|
||||
.afterClosed()
|
||||
.pipe(filter(Boolean), 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));
|
||||
this.refresh$.next(true);
|
||||
});
|
||||
}
|
||||
}
|
53
console/src/app/modules/actions-two/actions-two.module.ts
Normal file
53
console/src/app/modules/actions-two/actions-two.module.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActionsTwoActionsComponent } from './actions-two-actions/actions-two-actions.component';
|
||||
import { ActionsTwoTargetsComponent } from './actions-two-targets/actions-two-targets.component';
|
||||
import { ActionsTwoRoutingModule } from './actions-two-routing.module';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { ActionsTwoTargetsTableComponent } from './actions-two-targets/actions-two-targets-table/actions-two-targets-table.component';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TableActionsModule } from '../table-actions/table-actions.module';
|
||||
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ActionKeysModule } from '../action-keys/action-keys.module';
|
||||
import { ActionsTwoActionsTableComponent } from './actions-two-actions/actions-two-actions-table/actions-two-actions-table.component';
|
||||
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
|
||||
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
|
||||
import { TypeSafeCellDefModule } from 'src/app/directives/type-safe-cell-def/type-safe-cell-def.module';
|
||||
import { ProjectRoleChipModule } from '../project-role-chip/project-role-chip.module';
|
||||
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ActionsTwoActionsComponent,
|
||||
ActionsTwoTargetsComponent,
|
||||
ActionsTwoTargetsTableComponent,
|
||||
ActionsTwoActionsTableComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
TableActionsModule,
|
||||
TimestampToDatePipeModule,
|
||||
ActionsTwoRoutingModule,
|
||||
LocalizedDatePipeModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
MatTableModule,
|
||||
MatTooltipModule,
|
||||
MatSelectModule,
|
||||
RefreshTableModule,
|
||||
ActionKeysModule,
|
||||
MatIconModule,
|
||||
TypeSafeCellDefModule,
|
||||
ProjectRoleChipModule,
|
||||
ActionConditionPipeModule,
|
||||
],
|
||||
exports: [ActionsTwoActionsComponent, ActionsTwoTargetsComponent, ActionsTwoTargetsTableComponent],
|
||||
})
|
||||
export default class ActionsTwoModule {}
|
@@ -1,7 +1,6 @@
|
||||
<cnsl-refresh-table
|
||||
[loading]="loading$ | async"
|
||||
(refreshed)="refreshPage()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
[timestamp]="keyResult?.details?.viewTimestamp"
|
||||
[selection]="selection"
|
||||
>
|
||||
|
@@ -6,12 +6,7 @@
|
||||
</div>
|
||||
<p class="events-desc cnsl-secondary-text">{{ 'DESCRIPTIONS.SETTINGS.IAM_EVENTS.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-refresh-table
|
||||
[hideRefresh]="true"
|
||||
(refreshed)="refresh()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
[loading]="_loading | async"
|
||||
>
|
||||
<cnsl-refresh-table [hideRefresh]="true" (refreshed)="refresh()" [loading]="_loading | async">
|
||||
<div actions>
|
||||
<cnsl-filter-events (requestChanged)="filterChanged($event)"></cnsl-filter-events>
|
||||
</div>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<p class="failed-events-desc cnsl-secondary-text">{{ 'DESCRIPTIONS.SETTINGS.IAM_FAILED_EVENTS.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<cnsl-refresh-table (refreshed)="loadEvents()" [dataSize]="eventDataSource.data.length" [loading]="loading$ | async">
|
||||
<cnsl-refresh-table (refreshed)="loadEvents()" [loading]="loading$ | async">
|
||||
<table [dataSource]="eventDataSource" mat-table class="table" aria-label="Elements">
|
||||
<ng-container matColumnDef="viewName">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.FAILEDEVENTS.VIEWNAME' | translate }}</th>
|
||||
|
@@ -24,6 +24,17 @@ export function requiredValidator(c: AbstractControl): ValidationErrors | null {
|
||||
return i18nErr(Validators.required(c), 'ERRORS.REQUIRED');
|
||||
}
|
||||
|
||||
export function atLeastOneFieldValidator(fields: string[]): ValidatorFn {
|
||||
return (formGroup: AbstractControl): ValidationErrors | null => {
|
||||
const isValid = fields.some((field) => {
|
||||
const control = formGroup.get(field);
|
||||
return control && control.value;
|
||||
});
|
||||
|
||||
return isValid ? null : { atLeastOneRequired: true }; // Return an error if none are set
|
||||
};
|
||||
}
|
||||
|
||||
export function minArrayLengthValidator(minArrLength: number): ValidatorFn {
|
||||
return (c: AbstractControl): ValidationErrors | null => {
|
||||
return arrayLengthValidator(c, minArrLength, 'ERRORS.ATLEASTONE');
|
||||
|
@@ -15,7 +15,7 @@ import { ActionKeysType } from '../action-keys/action-keys.component';
|
||||
})
|
||||
export class HeaderComponent {
|
||||
@Input() public isDarkTheme: boolean = true;
|
||||
@Input() public user?: User.AsObject;
|
||||
@Input({ required: true }) public user!: User.AsObject;
|
||||
public showOrgContext: boolean = false;
|
||||
|
||||
@Input() public org!: Org.AsObject;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<h2>{{ 'DESCRIPTIONS.SETTINGS.IAM_VIEWS.TITLE' | translate }}</h2>
|
||||
<p class="views-desc cnsl-secondary-text">{{ 'DESCRIPTIONS.SETTINGS.IAM_VIEWS.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-refresh-table (refreshed)="loadViews()" [dataSize]="dataSource.data.length" [loading]="loading$ | async">
|
||||
<cnsl-refresh-table (refreshed)="loadViews()" [loading]="loading$ | async">
|
||||
<table [dataSource]="dataSource" mat-table class="table views-table" aria-label="Views" matSort>
|
||||
<ng-container matColumnDef="viewName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'IAM.VIEWS.VIEWNAME' | translate }}</th>
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<cnsl-refresh-table
|
||||
[loading]="loading$ | async"
|
||||
(refreshed)="refreshPage()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
[emitRefreshOnPreviousRoutes]="['/instance/idp/create']"
|
||||
[timestamp]="idpResult?.details?.viewTimestamp"
|
||||
[selection]="selection"
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<cnsl-refresh-table
|
||||
[loading]="loading$ | async"
|
||||
(refreshed)="refreshPage()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
[timestamp]="keyResult?.details?.viewTimestamp"
|
||||
[selection]="selection"
|
||||
>
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<cnsl-refresh-table
|
||||
*ngIf="dataSource"
|
||||
(refreshed)="changePage()"
|
||||
[dataSize]="dataSource.totalResult"
|
||||
[timestamp]="dataSource.viewTimestamp"
|
||||
[selection]="selection"
|
||||
[loading]="dataSource.loading$ | async"
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<cnsl-refresh-table
|
||||
*ngIf="dataSource"
|
||||
(refreshed)="changePage()"
|
||||
[dataSize]="dataSource.totalResult"
|
||||
[timestamp]="dataSource.viewTimestamp"
|
||||
[hideRefresh]="true"
|
||||
[selection]="selection"
|
||||
|
@@ -1,12 +1,7 @@
|
||||
<cnsl-card class="metadata-details" [title]="'DESCRIPTIONS.METADATA_TITLE' | translate" [description]="description">
|
||||
<mat-spinner card-actions class="spinner" diameter="20" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<cnsl-refresh-table
|
||||
*ngIf="dataSource$ | async as dataSource"
|
||||
[loading]="loading"
|
||||
(refreshed)="refresh.emit()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
>
|
||||
<cnsl-refresh-table *ngIf="dataSource$ | async as dataSource" [loading]="loading" (refreshed)="refresh.emit()">
|
||||
<button actions [disabled]="disabled" mat-raised-button color="primary" class="edit" (click)="editClicked.emit()">
|
||||
{{ 'ACTIONS.EDIT' | translate }}
|
||||
</button>
|
||||
|
@@ -1,9 +1,4 @@
|
||||
<cnsl-refresh-table
|
||||
[hideRefresh]="true"
|
||||
(refreshed)="refresh()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
[loading]="loading$ | async"
|
||||
>
|
||||
<cnsl-refresh-table [hideRefresh]="true" (refreshed)="refresh()" [loading]="loading$ | async">
|
||||
<cnsl-filter-org actions (filterChanged)="applySearchQuery($any($event))" (filterOpen)="filterOpen = $event">
|
||||
</cnsl-filter-org>
|
||||
|
||||
|
@@ -36,7 +36,6 @@ export class OrgTableComponent {
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
public activeOrg!: Org.AsObject;
|
||||
public OrgListSearchKey: any = OrgListSearchKey;
|
||||
public initialLimit: number = 20;
|
||||
public timestamp: Timestamp.AsObject | undefined = undefined;
|
||||
public totalResult: number = 0;
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<cnsl-refresh-table
|
||||
[loading]="loading$ | async"
|
||||
(refreshed)="refreshPage()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
[timestamp]="keyResult?.details?.viewTimestamp"
|
||||
[selection]="selection"
|
||||
>
|
||||
|
@@ -0,0 +1,60 @@
|
||||
<cnsl-card
|
||||
[title]="'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.TITLE' | translate"
|
||||
[description]="'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.DESCRIPTION' | translate"
|
||||
>
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.KEY_TYPE' | translate }}</cnsl-label>
|
||||
<mat-select [value]="keyType()" (valueChange)="keyType.set($event)">
|
||||
<mat-option value="rsa">RSA</mat-option>
|
||||
<mat-option value="ecdsa">ECDSA</mat-option>
|
||||
<mat-option value="ed25519">ED25519</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
<ng-container [ngSwitch]="keyType()">
|
||||
<form *ngSwitchCase="'rsa'" [formGroup]="rsaForm" (ngSubmit)="ngSubmit.emit(rsaForm.getRawValue())">
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.BITS' | translate }}</cnsl-label>
|
||||
<mat-select formControlName="bits">
|
||||
<ng-container *ngFor="let bit of RSABits | keyvalue">
|
||||
<mat-option *ngIf="Number(bit.key) as key" [value]="key">{{
|
||||
$any(bit.value).replace('RSA_BITS_', '')
|
||||
}}</mat-option>
|
||||
</ng-container>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.HASHER' | translate }}</cnsl-label>
|
||||
<mat-select formControlName="hasher">
|
||||
<ng-container *ngFor="let hasher of RSAHasher | keyvalue">
|
||||
<mat-option *ngIf="Number(hasher.key) as key" [value]="key">{{
|
||||
$any(hasher.value).replace('RSA_HASHER_', '')
|
||||
}}</mat-option>
|
||||
</ng-container>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
<ng-container *ngTemplateOutlet="submitButton; context: { form: rsaForm }" />
|
||||
</form>
|
||||
<form *ngSwitchCase="'ecdsa'" [formGroup]="ecdsaForm" (ngSubmit)="ngSubmit.emit(ecdsaForm.getRawValue())">
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.CREATE.CURVE' | translate }}</cnsl-label>
|
||||
<mat-select formControlName="curve">
|
||||
<ng-container *ngFor="let curve of ECDSACurve | keyvalue">
|
||||
<mat-option *ngIf="Number(curve.key) as key" [value]="key">{{
|
||||
$any(curve.value).replace('ECDSA_CURVE_', '')
|
||||
}}</mat-option>
|
||||
</ng-container>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
<ng-container *ngTemplateOutlet="submitButton; context: { form: ecdsaForm }" />
|
||||
</form>
|
||||
<form *ngSwitchCase="'ed25519'" (submit)="emitEd25519($event)">
|
||||
<ng-container *ngTemplateOutlet="submitButton" />
|
||||
</form>
|
||||
<ng-template #submitButton let-form="form">
|
||||
<button [disabled]="(loading$ | async) || form?.invalid" mat-raised-button color="primary" type="submit">
|
||||
<mat-spinner diameter="20" *ngIf="loading$ | async"></mat-spinner>
|
||||
<span *ngIf="(loading$ | async) === false || (loading$ | async) === null">{{ 'ACTIONS.CREATE' | translate }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</cnsl-card>
|
@@ -0,0 +1,60 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, signal, WritableSignal } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { RSAHasher, RSABits, ECDSACurve } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
|
||||
|
||||
type RawValue<T extends FormGroup> = ReturnType<T['getRawValue']>;
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-oidc-webkeys-create',
|
||||
templateUrl: './oidc-webkeys-create.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OidcWebKeysCreateComponent {
|
||||
protected readonly keyType: WritableSignal<NonNullable<WebKey['key']['case']>> = signal('rsa');
|
||||
protected readonly RSAHasher = RSAHasher;
|
||||
protected readonly RSABits = RSABits;
|
||||
protected readonly ECDSACurve = ECDSACurve;
|
||||
protected readonly Number = Number;
|
||||
protected readonly rsaForm = this.buildRsaForm();
|
||||
protected readonly ecdsaForm = this.buildEcdsaForm();
|
||||
protected readonly loading$ = new ReplaySubject<boolean>();
|
||||
|
||||
@Output()
|
||||
public readonly ngSubmit = new EventEmitter<RawValue<typeof this.rsaForm> | RawValue<typeof this.ecdsaForm> | void>();
|
||||
|
||||
@Input()
|
||||
public set loading(loading: boolean) {
|
||||
this.loading$.next(loading);
|
||||
}
|
||||
|
||||
constructor(private readonly fb: FormBuilder) {}
|
||||
|
||||
private buildRsaForm() {
|
||||
return this.fb.group({
|
||||
bits: new FormControl<RSABits>(RSABits.RSA_BITS_2048, {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required],
|
||||
}),
|
||||
hasher: new FormControl<RSAHasher>(RSAHasher.RSA_HASHER_SHA256, {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private buildEcdsaForm() {
|
||||
return this.fb.group({
|
||||
curve: new FormControl<ECDSACurve>(ECDSACurve.ECDSA_CURVE_P256, {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
protected emitEd25519(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
this.ngSubmit.emit();
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
<cnsl-card
|
||||
[title]="'DESCRIPTIONS.SETTINGS.WEB_KEYS.PREVIOUS_TABLE.TITLE' | translate"
|
||||
[description]="'DESCRIPTIONS.SETTINGS.WEB_KEYS.PREVIOUS_TABLE.DESCRIPTION' | translate"
|
||||
>
|
||||
<cnsl-refresh-table [hideRefresh]="true" [loading]="(dataSource$ | async) === null">
|
||||
<div class="table-wrapper">
|
||||
<table
|
||||
*ngIf="dataSource$ | async as dataSource"
|
||||
mat-table
|
||||
class="table"
|
||||
aria-label="Elements"
|
||||
[dataSource]="dataSource"
|
||||
>
|
||||
<ng-container matColumnDef="timestamp">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.PREVIOUS_TABLE.DEACTIVATED_ON' | translate }}
|
||||
</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
{{ row.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'APP.PAGES.ID' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
{{ row.id }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TYPE.TITLE' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
{{ row.key.case | uppercase }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['timestamp', 'id', 'type']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['timestamp', 'id', 'type']"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</cnsl-refresh-table>
|
||||
</cnsl-card>
|
@@ -0,0 +1,23 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-oidc-webkeys-inactive-table',
|
||||
templateUrl: './oidc-webkeys-inactive-table.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OidcWebKeysInactiveTableComponent {
|
||||
@Input({ required: true })
|
||||
public set InactiveWebKeys(webKeys: WebKey[] | null) {
|
||||
this.inactiveWebKeys$.next(webKeys);
|
||||
}
|
||||
|
||||
private inactiveWebKeys$ = new ReplaySubject<WebKey[] | null>(1);
|
||||
protected dataSource$ = this.inactiveWebKeys$.pipe(
|
||||
filter(Boolean),
|
||||
map((webKeys) => new MatTableDataSource(webKeys)),
|
||||
);
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
<cnsl-refresh-table (refreshed)="refresh.emit()" [loading]="(dataSource$ | async) === null">
|
||||
<div actions>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table *ngIf="dataSource$ | async as dataSource" mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="state">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'APP.PAGES.STATE' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; let i = index; dataSource: dataSource">
|
||||
<span
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: i === 0,
|
||||
neutral: i === 1,
|
||||
}"
|
||||
[ngSwitch]="i"
|
||||
>
|
||||
<ng-container *ngSwitchCase="0">{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.ACTIVE' | translate }}</ng-container>
|
||||
<ng-container *ngSwitchCase="1">{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.NEXT' | translate }}</ng-container>
|
||||
<ng-container *ngSwitchDefault>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.FUTURE' | translate }}</ng-container>
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'APP.PAGES.ID' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
{{ row.id }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'PROJECT.TYPE.TITLE' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
{{ row.key.case | uppercase }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
<cnsl-table-actions *ngIf="row.state === State.INITIAL">
|
||||
<button
|
||||
actions
|
||||
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
|
||||
color="warn"
|
||||
(click)="delete.emit(row)"
|
||||
mat-icon-button
|
||||
>
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
</cnsl-table-actions>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['state', 'id', 'type', 'actions']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['state', 'id', 'type', 'actions']"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</cnsl-refresh-table>
|
@@ -0,0 +1,4 @@
|
||||
.state.next {
|
||||
color: #0e6245;
|
||||
border: 1px solid #0e6245;
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { State, WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-oidc-webkeys-table',
|
||||
templateUrl: './oidc-webkeys-table.component.html',
|
||||
styleUrls: ['./oidc-webkeys-table.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OidcWebKeysTableComponent {
|
||||
@Output()
|
||||
public readonly refresh = new EventEmitter<void>();
|
||||
|
||||
@Output()
|
||||
public readonly delete = new EventEmitter<WebKey>();
|
||||
|
||||
@Input({ required: true })
|
||||
public set webKeys(webKeys: WebKey[] | null) {
|
||||
this.webKeys$.next(webKeys);
|
||||
}
|
||||
|
||||
private readonly webKeys$ = new ReplaySubject<WebKey[] | null>(1);
|
||||
protected readonly dataSource$ = this.webKeys$.pipe(
|
||||
filter(Boolean),
|
||||
map((keys) => new MatTableDataSource(keys)),
|
||||
);
|
||||
protected readonly State = State;
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
<h2>{{ 'SETTINGS.LIST.WEB_KEYS' | translate }}</h2>
|
||||
<p>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.DESCRIPTION' | translate }}</p>
|
||||
<h3>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.TITLE' | translate }}</h3>
|
||||
<p>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.DESCRIPTION' | translate }}</p>
|
||||
<p>{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.NOTE' | translate }}</p>
|
||||
<cnsl-oidc-webkeys-table (refresh)="refresh.next(true)" (delete)="deleteWebKey($event)" [webKeys]="webKeys$ | async">
|
||||
<button
|
||||
*ngIf="nextWebKeyCandidate$ | async as nextWebKeyCandidate"
|
||||
[disabled]="activateLoading()"
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="activateWebKey(nextWebKeyCandidate)"
|
||||
>
|
||||
<mat-spinner diameter="20" *ngIf="activateLoading()"></mat-spinner>
|
||||
<span *ngIf="!activateLoading()">{{ 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.ACTIVATE' | translate }}</span>
|
||||
</button>
|
||||
</cnsl-oidc-webkeys-table>
|
||||
<cnsl-oidc-webkeys-create [loading]="createLoading()" (ngSubmit)="createWebKey($event)" />
|
||||
<cnsl-oidc-webkeys-inactive-table [InactiveWebKeys]="inactiveWebKeys$ | async" />
|
@@ -0,0 +1,217 @@
|
||||
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, signal } from '@angular/core';
|
||||
import { WebKeysService } from 'src/app/services/webkeys.service';
|
||||
import { defer, EMPTY, firstValueFrom, Observable, ObservedValueOf, of, shareReplay, Subject, switchMap } from 'rxjs';
|
||||
import { catchError, map, startWith, withLatestFrom } from 'rxjs/operators';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { OidcWebKeysCreateComponent } from './oidc-webkeys-create/oidc-webkeys-create.component';
|
||||
import { TimestampToDatePipe } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date.pipe';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { State, WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
|
||||
import { CreateWebKeyRequestSchema } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb';
|
||||
import { RSAHasher, RSABits, ECDSACurve } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb';
|
||||
import { NewFeatureService } from 'src/app/services/new-feature.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
const CACHE_WARNING_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-oidc-webkeys',
|
||||
templateUrl: './oidc-webkeys.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OidcWebKeysComponent implements OnInit {
|
||||
protected readonly refresh = new Subject<true>();
|
||||
protected readonly webKeysEnabled$: Observable<boolean>;
|
||||
protected readonly webKeys$: Observable<WebKey[]>;
|
||||
protected readonly inactiveWebKeys$: Observable<WebKey[]>;
|
||||
protected readonly nextWebKeyCandidate$: Observable<WebKey | undefined>;
|
||||
|
||||
protected readonly activateLoading = signal(false);
|
||||
protected readonly createLoading = signal(false);
|
||||
|
||||
constructor(
|
||||
private readonly webKeysService: WebKeysService,
|
||||
private readonly featureService: NewFeatureService,
|
||||
private readonly toast: ToastService,
|
||||
private readonly timestampToDatePipe: TimestampToDatePipe,
|
||||
private readonly dialog: MatDialog,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly router: Router,
|
||||
private readonly route: ActivatedRoute,
|
||||
) {
|
||||
this.webKeysEnabled$ = this.getWebKeysEnabled().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
|
||||
const webKeys$ = this.getWebKeys(this.webKeysEnabled$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
|
||||
this.webKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state !== State.INACTIVE)));
|
||||
this.inactiveWebKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state === State.INACTIVE)));
|
||||
|
||||
this.nextWebKeyCandidate$ = this.getNextWebKeyCandidate(this.webKeys$);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// redirect away from this page if web keys are not enabled
|
||||
// this also preloads the web keys enabled state
|
||||
this.webKeysEnabled$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async (webKeysEnabled) => {
|
||||
if (webKeysEnabled) {
|
||||
return;
|
||||
}
|
||||
await this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParamsHandling: 'merge',
|
||||
queryParams: {
|
||||
id: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getWebKeysEnabled() {
|
||||
return defer(() => this.featureService.getInstanceFeatures()).pipe(
|
||||
map((features) => features.webKey?.enabled ?? false),
|
||||
catchError((err) => {
|
||||
this.toast.showError(err);
|
||||
return of(false);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getWebKeys(webKeysEnabled$: Observable<boolean>) {
|
||||
return this.refresh.pipe(
|
||||
startWith(true),
|
||||
switchMap(() => {
|
||||
return this.webKeysService.ListWebKeys();
|
||||
}),
|
||||
map(({ webKeys }) => webKeys),
|
||||
catchError(async (err) => {
|
||||
const webKeysEnabled = await firstValueFrom(webKeysEnabled$);
|
||||
// suppress errors if web keys are not enabled
|
||||
if (!webKeysEnabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.toast.showError(err);
|
||||
return [];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getNextWebKeyCandidate(webKeys$: Observable<WebKey[]>) {
|
||||
return webKeys$.pipe(
|
||||
map((webKeys) => {
|
||||
if (webKeys.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
const [webKey, nextWebKey] = webKeys;
|
||||
if (webKey.state !== State.ACTIVE) {
|
||||
return undefined;
|
||||
}
|
||||
if (nextWebKey.state !== State.INITIAL) {
|
||||
return undefined;
|
||||
}
|
||||
return nextWebKey;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
protected async createWebKey(event: ObservedValueOf<OidcWebKeysCreateComponent['ngSubmit']>) {
|
||||
try {
|
||||
this.createLoading.set(true);
|
||||
|
||||
const req = !event
|
||||
? this.createEd25519()
|
||||
: 'curve' in event
|
||||
? this.createEcdsa(event.curve)
|
||||
: this.createRsa(event.bits, event.hasher);
|
||||
await this.webKeysService.CreateWebKey(req);
|
||||
|
||||
this.refresh.next(true);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
} finally {
|
||||
this.createLoading.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private createEd25519(): MessageInitShape<typeof CreateWebKeyRequestSchema> {
|
||||
return {
|
||||
key: {
|
||||
case: 'ed25519',
|
||||
value: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private createEcdsa(curve: ECDSACurve): MessageInitShape<typeof CreateWebKeyRequestSchema> {
|
||||
return {
|
||||
key: {
|
||||
case: 'ecdsa',
|
||||
value: {
|
||||
curve,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private createRsa(bits: RSABits, hasher: RSAHasher): MessageInitShape<typeof CreateWebKeyRequestSchema> {
|
||||
return {
|
||||
key: {
|
||||
case: 'rsa',
|
||||
value: {
|
||||
bits,
|
||||
hasher,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected async deleteWebKey(row: WebKey) {
|
||||
try {
|
||||
await this.webKeysService.DeleteWebKey(row.id);
|
||||
this.refresh.next(true);
|
||||
} catch (err) {
|
||||
this.toast.showError(err);
|
||||
}
|
||||
}
|
||||
|
||||
protected async activateWebKey(nextWebKey: WebKey) {
|
||||
try {
|
||||
this.activateLoading.set(true);
|
||||
const creationDate = this.timestampToDatePipe.transform(nextWebKey.creationDate);
|
||||
if (!creationDate) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error('Invalid creation date');
|
||||
}
|
||||
|
||||
const diffToCurrentTime = Date.now() - creationDate.getTime();
|
||||
if (diffToCurrentTime < CACHE_WARNING_MS && !(await this.openCacheWarnDialog())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.webKeysService.ActivateWebKey(nextWebKey.id);
|
||||
this.refresh.next(true);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
} finally {
|
||||
this.activateLoading.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private openCacheWarnDialog() {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.ACTIVATE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
titleKey: 'Web Key is less then 5 min old',
|
||||
descriptionKey: 'DESCRIPTIONS.SETTINGS.WEB_KEYS.TABLE.NOTE',
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
const obs = dialogRef.afterClosed().pipe(map(Boolean), takeUntilDestroyed(this.destroyRef));
|
||||
return firstValueFrom(obs);
|
||||
}
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { OidcWebKeysComponent } from './oidc-webkeys.component';
|
||||
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { FormFieldModule } from 'src/app/modules/form-field/form-field.module';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { CardModule } from 'src/app/modules/card/card.module';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { OidcWebKeysCreateComponent } from './oidc-webkeys-create/oidc-webkeys-create.component';
|
||||
import { OidcWebKeysTableComponent } from './oidc-webkeys-table/oidc-webkeys-table.component';
|
||||
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
|
||||
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
|
||||
import { OidcWebKeysInactiveTableComponent } from './oidc-webkeys-inactive-table/oidc-webkeys-inactive-table.component';
|
||||
import { TypeSafeCellDefDirective } from './type-safe-cell-def.directive';
|
||||
import { TimestampToDatePipe } from '../../../pipes/timestamp-to-date-pipe/timestamp-to-date.pipe';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
OidcWebKeysComponent,
|
||||
OidcWebKeysCreateComponent,
|
||||
OidcWebKeysTableComponent,
|
||||
OidcWebKeysInactiveTableComponent,
|
||||
TypeSafeCellDefDirective,
|
||||
],
|
||||
providers: [TimestampToDatePipe],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslateModule,
|
||||
RefreshTableModule,
|
||||
MatCheckboxModule,
|
||||
MatTableModule,
|
||||
MatMenuModule,
|
||||
TableActionsModule,
|
||||
MatButtonModule,
|
||||
ActionKeysModule,
|
||||
MatIconModule,
|
||||
FormFieldModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule,
|
||||
CardModule,
|
||||
MatProgressSpinnerModule,
|
||||
TimestampToDatePipeModule,
|
||||
LocalizedDatePipeModule,
|
||||
MatTooltipModule,
|
||||
],
|
||||
exports: [OidcWebKeysComponent],
|
||||
})
|
||||
export class OidcWebkeysModule {}
|
@@ -0,0 +1,16 @@
|
||||
import { Directive, Input } from '@angular/core';
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { MatCellDef } from '@angular/material/table';
|
||||
import { CdkCellDef } from '@angular/cdk/table';
|
||||
|
||||
@Directive({
|
||||
selector: '[cnslCellDef]',
|
||||
providers: [{ provide: CdkCellDef, useExisting: TypeSafeCellDefDirective }],
|
||||
})
|
||||
export class TypeSafeCellDefDirective<T> extends MatCellDef {
|
||||
@Input({ required: true }) cnslCellDefDataSource!: DataSource<T>;
|
||||
|
||||
static ngTemplateContextGuard<T>(_dir: TypeSafeCellDefDirective<T>, _ctx: any): _ctx is { $implicit: T; index: number } {
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -2,7 +2,6 @@
|
||||
[showSelectionActionButton]="showSelectionActionButton"
|
||||
*ngIf="projectId"
|
||||
(refreshed)="refreshPage()"
|
||||
[dataSize]="dataSource.totalResult"
|
||||
[emitRefreshOnPreviousRoutes]="['/projects/' + projectId + '/roles/create']"
|
||||
[selection]="selection"
|
||||
[loading]="dataSource.loading$ | async"
|
||||
|
@@ -29,7 +29,6 @@ const rotate = animation([
|
||||
export class RefreshTableComponent implements OnInit {
|
||||
@Input() public selection: SelectionModel<any> = new SelectionModel<any>(true, []);
|
||||
@Input() public timestamp: Timestamp.AsObject | ConnectTimestamp | undefined = undefined;
|
||||
@Input() public dataSize: number = 0;
|
||||
@Input() public emitRefreshAfterTimeoutInMs: number = 0;
|
||||
@Input() public loading: boolean | null = false;
|
||||
@Input() public emitRefreshOnPreviousRoutes: string[] = [];
|
||||
|
@@ -1,82 +1,85 @@
|
||||
<cnsl-sidenav
|
||||
*ngIf="currentSetting"
|
||||
[title]="title"
|
||||
[description]="description"
|
||||
[indented]="true"
|
||||
[(ngModel)]="currentSetting"
|
||||
[settingsList]="settingsList"
|
||||
queryParam="id"
|
||||
>
|
||||
<ng-container *ngIf="currentSetting === 'organizations'">
|
||||
<cnsl-sidenav [indented]="true" [setting]="setting()" (settingChange)="setting.set($event)" [settingsList]="settingsList">
|
||||
<ng-container *ngIf="setting()?.id === 'organizations'">
|
||||
<h2>{{ 'ORG.PAGES.LIST' | translate }}</h2>
|
||||
<p class="org-desc cnsl-secondary-text">{{ 'ORG.PAGES.LISTDESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-org-table></cnsl-org-table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'features'">
|
||||
<ng-container *ngIf="setting()?.id === 'features'">
|
||||
<cnsl-features></cnsl-features>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'complexity'">
|
||||
<ng-container *ngIf="setting()?.id === 'complexity'">
|
||||
<cnsl-password-complexity-policy [serviceType]="serviceType"></cnsl-password-complexity-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'age'">
|
||||
<ng-container *ngIf="setting()?.id === 'age'">
|
||||
<cnsl-password-age-policy [serviceType]="serviceType"></cnsl-password-age-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'lockout'">
|
||||
<ng-container *ngIf="setting()?.id === 'lockout'">
|
||||
<cnsl-password-lockout-policy [serviceType]="serviceType"></cnsl-password-lockout-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'login'">
|
||||
<ng-container *ngIf="setting()?.id === 'login'">
|
||||
<cnsl-login-policy [serviceType]="serviceType"></cnsl-login-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'verified_domains'">
|
||||
<ng-container *ngIf="setting()?.id === 'verified_domains'">
|
||||
<cnsl-domains></cnsl-domains>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'domain' && (['iam.policy.write'] | hasRole | async) === true">
|
||||
<ng-container *ngIf="setting()?.id === 'domain' && (['iam.policy.write'] | hasRole | async) === true">
|
||||
<cnsl-domain-policy [serviceType]="serviceType"></cnsl-domain-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'idp'">
|
||||
<ng-container *ngIf="setting()?.id === 'idp'">
|
||||
<cnsl-idp-settings [serviceType]="serviceType"></cnsl-idp-settings>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'notifications'">
|
||||
<ng-container *ngIf="setting()?.id === 'notifications'">
|
||||
<cnsl-notification-policy [serviceType]="serviceType"></cnsl-notification-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'smtpprovider' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<ng-container *ngIf="setting()?.id === 'smtpprovider' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-notification-smtp-provider [serviceType]="serviceType"></cnsl-notification-smtp-provider>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'smsprovider' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<ng-container *ngIf="setting()?.id === 'smsprovider' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-notification-sms-provider [serviceType]="serviceType"></cnsl-notification-sms-provider>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'oidc' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<ng-container *ngIf="setting()?.id === 'oidc' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-oidc-configuration></cnsl-oidc-configuration>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'secrets' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<ng-container *ngIf="setting()?.id === 'webkeys' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-oidc-webkeys />
|
||||
</ng-container>
|
||||
<ng-container *ngIf="setting()?.id === 'secrets' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-secret-generator></cnsl-secret-generator>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'security' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<ng-container *ngIf="setting()?.id === 'security' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-security-policy></cnsl-security-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'branding'">
|
||||
<ng-container *ngIf="setting()?.id === 'branding'">
|
||||
<cnsl-private-labeling-policy [serviceType]="serviceType"></cnsl-private-labeling-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'messagetexts'">
|
||||
<ng-container *ngIf="setting()?.id === 'messagetexts'">
|
||||
<cnsl-message-texts [serviceType]="serviceType"></cnsl-message-texts>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'logintexts'">
|
||||
<ng-container *ngIf="setting()?.id === 'logintexts'">
|
||||
<cnsl-login-texts [serviceType]="serviceType"></cnsl-login-texts>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'privacypolicy'">
|
||||
<ng-container *ngIf="setting()?.id === 'privacypolicy'">
|
||||
<cnsl-privacy-policy [serviceType]="serviceType"></cnsl-privacy-policy>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'languages' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<ng-container *ngIf="setting()?.id === 'languages' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-language-settings></cnsl-language-settings>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'views' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<ng-container *ngIf="setting()?.id === 'views' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-iam-views></cnsl-iam-views>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'events' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<ng-container *ngIf="setting()?.id === 'events' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-events></cnsl-events>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'failedevents' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<ng-container *ngIf="setting()?.id === 'failedevents' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-iam-failed-events></cnsl-iam-failed-events>
|
||||
</ng-container>
|
||||
<!-- todo: figure out permissions -->
|
||||
<ng-container *ngIf="setting()?.id === 'actions'">
|
||||
<cnsl-actions-two-actions />
|
||||
</ng-container>
|
||||
<ng-container *ngIf="setting()?.id === 'actions_targets'">
|
||||
<cnsl-actions-two-targets />
|
||||
</ng-container>
|
||||
<ng-content></ng-content>
|
||||
</cnsl-sidenav>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { Component, effect, Input, OnInit, signal } from '@angular/core';
|
||||
|
||||
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
|
||||
import { SidenavSetting } from '../sidenav/sidenav.component';
|
||||
@@ -8,28 +8,40 @@ import { SidenavSetting } from '../sidenav/sidenav.component';
|
||||
templateUrl: './settings-list.component.html',
|
||||
styleUrls: ['./settings-list.component.scss'],
|
||||
})
|
||||
export class SettingsListComponent implements OnChanges, OnInit {
|
||||
@Input() public title: string = '';
|
||||
@Input() public description: string = '';
|
||||
@Input() public serviceType!: PolicyComponentServiceType;
|
||||
@Input() public selectedId: string | undefined = undefined;
|
||||
@Input() public settingsList: SidenavSetting[] = [];
|
||||
public currentSetting: string | undefined = '';
|
||||
public PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
constructor() {}
|
||||
export class SettingsListComponent implements OnInit {
|
||||
@Input({ required: true }) public serviceType!: PolicyComponentServiceType;
|
||||
@Input() public set selectedId(selectedId: string) {
|
||||
this.selectedId$.set(selectedId);
|
||||
}
|
||||
@Input({ required: true }) public settingsList: SidenavSetting[] = [];
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (this.settingsList && this.settingsList.length && changes['selectedId']?.currentValue) {
|
||||
this.currentSetting =
|
||||
this.settingsList && this.settingsList.find((l) => l.id === changes['selectedId'].currentValue)
|
||||
? changes['selectedId'].currentValue
|
||||
: '';
|
||||
}
|
||||
protected setting = signal<SidenavSetting | null>(null);
|
||||
private selectedId$ = signal<string | undefined>(undefined);
|
||||
protected PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
|
||||
constructor() {
|
||||
effect(
|
||||
() => {
|
||||
const selectedId = this.selectedId$();
|
||||
if (!selectedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setting = this.settingsList.find(({ id }) => id === selectedId);
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
this.setting.set(setting);
|
||||
},
|
||||
{ allowSignalWrites: true },
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.currentSetting) {
|
||||
this.currentSetting = this.settingsList ? this.settingsList[0].id : '';
|
||||
const firstSetting = this.settingsList[0];
|
||||
if (!firstSetting || this.setting()) {
|
||||
return;
|
||||
}
|
||||
this.setting.set(firstSetting);
|
||||
}
|
||||
}
|
||||
|
@@ -31,9 +31,13 @@ import { OrgTableModule } from '../org-table/org-table.module';
|
||||
import { NotificationSMTPProviderModule } from '../policies/notification-smtp-provider/notification-smtp-provider.module';
|
||||
import { FeaturesComponent } from 'src/app/components/features/features.component';
|
||||
import OrgListModule from 'src/app/pages/org-list/org-list.module';
|
||||
import ActionsTwoModule from '../actions-two/actions-two.module';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { OidcWebkeysModule } from '../policies/oidc-webkeys/oidc-webkeys.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [SettingsListComponent],
|
||||
providers: [provideRouter([])],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
@@ -62,10 +66,12 @@ import OrgListModule from 'src/app/pages/org-list/org-list.module';
|
||||
NotificationSMTPProviderModule,
|
||||
NotificationSMSProviderModule,
|
||||
OIDCConfigurationModule,
|
||||
OidcWebkeysModule,
|
||||
SecretGeneratorModule,
|
||||
FailedEventsModule,
|
||||
IamViewsModule,
|
||||
EventsModule,
|
||||
ActionsTwoModule,
|
||||
],
|
||||
exports: [SettingsListComponent],
|
||||
})
|
||||
|
@@ -35,6 +35,14 @@ export const OIDC: SidenavSetting = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WEBKEYS: SidenavSetting = {
|
||||
id: 'webkeys',
|
||||
i18nKey: 'SETTINGS.LIST.WEB_KEYS',
|
||||
requiredRoles: {
|
||||
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
||||
},
|
||||
};
|
||||
|
||||
export const SECRETS: SidenavSetting = {
|
||||
id: 'secrets',
|
||||
i18nKey: 'SETTINGS.LIST.SECRETS',
|
||||
@@ -214,3 +222,23 @@ export const BRANDING: SidenavSetting = {
|
||||
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ACTIONS: SidenavSetting = {
|
||||
id: 'actions',
|
||||
i18nKey: 'SETTINGS.LIST.ACTIONS',
|
||||
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
|
||||
requiredRoles: {
|
||||
// todo: figure out roles
|
||||
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ACTIONS_TARGETS: SidenavSetting = {
|
||||
id: 'actions_targets',
|
||||
i18nKey: 'SETTINGS.LIST.TARGETS',
|
||||
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
|
||||
requiredRoles: {
|
||||
// todo: figure out roles
|
||||
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
||||
},
|
||||
};
|
||||
|
@@ -1,12 +1,9 @@
|
||||
<div class="sidenav-container">
|
||||
<div class="sidenav-settings-list" [ngClass]="{ indented: indented }">
|
||||
<div class="sidenav-sticky-rel">
|
||||
<h1 *ngIf="title">{{ title }}</h1>
|
||||
<p *ngIf="description" class="cnsl-secondary-text">{{ description }}</p>
|
||||
|
||||
<button
|
||||
*ngIf="currentSetting"
|
||||
(click)="value = ''"
|
||||
*ngIf="setting$()"
|
||||
(click)="setting$.set(null)"
|
||||
class="sidenav-setting-list-element mob-only"
|
||||
[ngClass]="{ active: true }"
|
||||
>
|
||||
@@ -14,37 +11,25 @@
|
||||
<span>{{ 'USER.SETTINGS.TITLE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<ng-container *ngFor="let setting of settingsList; index as i">
|
||||
<ng-container>
|
||||
<span
|
||||
class="sidenav-setting-group hide-on-mobile"
|
||||
[ngClass]="{ show: !currentSetting }"
|
||||
*ngIf="
|
||||
(setting.groupI18nKey && i > 0 && setting.groupI18nKey !== settingsList[i - 1].groupI18nKey) ||
|
||||
(i === 0 && setting.groupI18nKey)
|
||||
"
|
||||
>{{ setting.groupI18nKey | translate }}</span
|
||||
>
|
||||
<ng-container *ngFor="let setting of settingsList; index as i; trackBy: trackSettings">
|
||||
<span
|
||||
class="sidenav-setting-group hide-on-mobile"
|
||||
*ngIf="
|
||||
(setting.groupI18nKey && i > 0 && setting.groupI18nKey !== settingsList[i - 1].groupI18nKey) ||
|
||||
(i === 0 && setting.groupI18nKey)
|
||||
"
|
||||
>{{ setting.groupI18nKey | translate }}</span
|
||||
>
|
||||
|
||||
<button
|
||||
(click)="value = setting.id"
|
||||
class="sidenav-setting-list-element hide-on-mobile"
|
||||
[ngClass]="{ active: currentSetting === setting.id, show: !currentSetting }"
|
||||
[attr.data-e2e]="'sidenav-element-' + setting.id"
|
||||
>
|
||||
<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>
|
||||
<button
|
||||
(click)="value = setting.id"
|
||||
class="sidenav-setting-list-element hide-on-mobile"
|
||||
[ngClass]="{ active: currentSetting === setting.id, show: !currentSetting }"
|
||||
>
|
||||
<span>{{ setting.i18nKey | translate }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
<button
|
||||
(click)="setting$.set(setting)"
|
||||
class="sidenav-setting-list-element hide-on-mobile"
|
||||
[ngClass]="{ active: setting$()?.id === setting.id }"
|
||||
[attr.data-e2e]="'sidenav-element-' + setting.id"
|
||||
>
|
||||
<span>{{ setting.i18nKey | translate }}</span>
|
||||
<mat-icon *ngIf="setting.showWarn" class="warn-icon" svgIcon="mdi_shield_alert"></mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { Component, forwardRef, Input, OnInit } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ChangeDetectionStrategy, Component, effect, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
|
||||
|
||||
export interface SidenavSetting {
|
||||
@@ -19,61 +17,61 @@ export interface SidenavSetting {
|
||||
selector: 'cnsl-sidenav',
|
||||
templateUrl: './sidenav.component.html',
|
||||
styleUrls: ['./sidenav.component.scss'],
|
||||
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SidenavComponent), multi: true }],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SidenavComponent implements ControlValueAccessor {
|
||||
@Input() public title: string = '';
|
||||
@Input() public description: string = '';
|
||||
export class SidenavComponent {
|
||||
@Input() public navigate: boolean = true;
|
||||
@Input() public indented: boolean = false;
|
||||
@Input() public currentSetting?: string | undefined = undefined;
|
||||
@Input() public settingsList: SidenavSetting[] = [];
|
||||
@Input() public queryParam: string = '';
|
||||
@Input({ required: true }) public settingsList: SidenavSetting[] = [];
|
||||
@Input({ required: true })
|
||||
public set setting(setting: SidenavSetting | null) {
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
this.setting$.set(setting);
|
||||
}
|
||||
|
||||
@Output()
|
||||
public settingChange = new EventEmitter<SidenavSetting>();
|
||||
|
||||
protected readonly setting$ = signal<SidenavSetting | null>(null);
|
||||
|
||||
protected PolicyComponentServiceType = PolicyComponentServiceType;
|
||||
|
||||
public PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
) {}
|
||||
private readonly router: Router,
|
||||
private readonly route: ActivatedRoute,
|
||||
) {
|
||||
effect(
|
||||
() => {
|
||||
const setting = this.setting$();
|
||||
if (setting === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
private onChange = (current: string | undefined) => {};
|
||||
private onTouch = (current: string | undefined) => {};
|
||||
this.settingChange.emit(setting);
|
||||
|
||||
@Input() get value(): string | undefined {
|
||||
return this.currentSetting;
|
||||
if (!this.navigate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: {
|
||||
id: setting ? setting.id : undefined,
|
||||
},
|
||||
replaceUrl: true,
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: false,
|
||||
})
|
||||
.then();
|
||||
},
|
||||
{ allowSignalWrites: true },
|
||||
);
|
||||
}
|
||||
|
||||
set value(setting: string | undefined) {
|
||||
this.currentSetting = setting;
|
||||
|
||||
if (setting || setting === undefined || setting === '') {
|
||||
this.onChange(setting);
|
||||
this.onTouch(setting);
|
||||
}
|
||||
|
||||
if (this.queryParam && setting) {
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: {
|
||||
[this.queryParam]: setting,
|
||||
},
|
||||
replaceUrl: true,
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: false,
|
||||
})
|
||||
.then();
|
||||
}
|
||||
}
|
||||
|
||||
public writeValue(value: any) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public registerOnChange(fn: any) {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
public registerOnTouched(fn: any) {
|
||||
this.onTouch = fn;
|
||||
protected trackSettings(_: number, setting: SidenavSetting): string {
|
||||
return setting.id;
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<cnsl-refresh-table
|
||||
[loading]="loading$ | async"
|
||||
(refreshed)="refreshPage()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
[emitRefreshOnPreviousRoutes]="[
|
||||
'/instance/smtpprovider/aws-ses/create',
|
||||
'/instance/smtpprovider/generic/create',
|
||||
|
@@ -4,7 +4,6 @@
|
||||
[hideRefresh]="true"
|
||||
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes"
|
||||
[timestamp]="dataSource.viewTimestamp"
|
||||
[dataSize]="dataSource.totalResult"
|
||||
[selection]="selection"
|
||||
>
|
||||
<button
|
||||
|
@@ -2,7 +2,6 @@
|
||||
[hideRefresh]="true"
|
||||
[loading]="loading$ | async"
|
||||
(refreshed)="refreshPage()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
[timestamp]="actionsResult?.details?.viewTimestamp"
|
||||
[selection]="selection"
|
||||
>
|
||||
|
@@ -14,6 +14,12 @@ const routes: Routes = [
|
||||
data: {
|
||||
roles: ['iam.read'],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'actions',
|
||||
loadChildren: () => import('src/app/modules/actions-two/actions-two.module'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'members',
|
||||
|
@@ -40,12 +40,10 @@
|
||||
|
||||
<div class="max-width-container">
|
||||
<div class="instance-settings-wrapper">
|
||||
<ng-container *ngIf="settingsList | async as list">
|
||||
<cnsl-settings-list
|
||||
[selectedId]="id"
|
||||
[serviceType]="PolicyComponentServiceType.ADMIN"
|
||||
[settingsList]="list"
|
||||
></cnsl-settings-list>
|
||||
<ng-container *ngIf="settingsList$ | async as list">
|
||||
<cnsl-settings-list [selectedId]="id" [serviceType]="PolicyComponentServiceType.ADMIN" [settingsList]="list">
|
||||
<router-outlet />
|
||||
</cnsl-settings-list>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, DestroyRef } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { BehaviorSubject, from, Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, finalize, map, takeUntil } from 'rxjs/operators';
|
||||
import { BehaviorSubject, defer, from, Observable, of, shareReplay, TimeoutError } from 'rxjs';
|
||||
import { catchError, finalize, map, timeout } from 'rxjs/operators';
|
||||
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
|
||||
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
|
||||
import { InstanceDetail, State } from 'src/app/proto/generated/zitadel/instance_pb';
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
MESSAGETEXTS,
|
||||
NOTIFICATIONS,
|
||||
OIDC,
|
||||
WEBKEYS,
|
||||
PRIVACYPOLICY,
|
||||
SECRETS,
|
||||
SECURITY,
|
||||
@@ -34,28 +35,35 @@ import {
|
||||
EVENTS,
|
||||
ORGANIZATIONS,
|
||||
FEATURESETTINGS,
|
||||
} from '../../modules/settings-list/settings';
|
||||
ACTIONS,
|
||||
ACTIONS_TARGETS,
|
||||
} from 'src/app/modules/settings-list/settings';
|
||||
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { EnvironmentService } from 'src/app/services/environment.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { NewFeatureService } from '../../services/new-feature.service';
|
||||
import { withLatestFromSynchronousFix } from '../../utils/withLatestFromSynchronousFix';
|
||||
@Component({
|
||||
selector: 'cnsl-instance',
|
||||
templateUrl: './instance.component.html',
|
||||
styleUrls: ['./instance.component.scss'],
|
||||
})
|
||||
export class InstanceComponent implements OnInit, OnDestroy {
|
||||
public instance?: InstanceDetail.AsObject;
|
||||
public PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
public totalMemberResult: number = 0;
|
||||
public membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]);
|
||||
public State: any = State;
|
||||
export class InstanceComponent {
|
||||
protected instance?: InstanceDetail.AsObject;
|
||||
protected readonly PolicyComponentServiceType = PolicyComponentServiceType;
|
||||
private readonly loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
protected readonly loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
protected totalMemberResult: number = 0;
|
||||
protected readonly membersSubject: BehaviorSubject<Member.AsObject[]> = new BehaviorSubject<Member.AsObject[]>([]);
|
||||
protected readonly State = State;
|
||||
|
||||
public id: string = '';
|
||||
public defaultSettingsList: SidenavSetting[] = [
|
||||
protected id: string = '';
|
||||
protected readonly defaultSettingsList: SidenavSetting[] = [
|
||||
ORGANIZATIONS,
|
||||
FEATURESETTINGS,
|
||||
ACTIONS,
|
||||
ACTIONS_TARGETS,
|
||||
// notifications
|
||||
// { showWarn: true, ...NOTIFICATIONS },
|
||||
NOTIFICATIONS,
|
||||
@@ -67,7 +75,6 @@ export class InstanceComponent implements OnInit, OnDestroy {
|
||||
COMPLEXITY,
|
||||
AGE,
|
||||
LOCKOUT,
|
||||
|
||||
DOMAIN,
|
||||
// appearance
|
||||
BRANDING,
|
||||
@@ -81,23 +88,25 @@ export class InstanceComponent implements OnInit, OnDestroy {
|
||||
PRIVACYPOLICY,
|
||||
LANGUAGES,
|
||||
OIDC,
|
||||
WEBKEYS,
|
||||
SECRETS,
|
||||
SECURITY,
|
||||
];
|
||||
|
||||
public settingsList: Observable<SidenavSetting[]> = of([]);
|
||||
public customerPortalLink$ = this.envService.env.pipe(map((env) => env.customer_portal));
|
||||
protected readonly settingsList$: Observable<SidenavSetting[]>;
|
||||
protected readonly customerPortalLink$ = this.envService.env.pipe(map((env) => env.customer_portal));
|
||||
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
constructor(
|
||||
public adminService: AdminService,
|
||||
private dialog: MatDialog,
|
||||
private toast: ToastService,
|
||||
protected readonly adminService: AdminService,
|
||||
private readonly dialog: MatDialog,
|
||||
private readonly toast: ToastService,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
private router: Router,
|
||||
private authService: GrpcAuthService,
|
||||
private envService: EnvironmentService,
|
||||
private readonly router: Router,
|
||||
private readonly authService: GrpcAuthService,
|
||||
private readonly envService: EnvironmentService,
|
||||
activatedRoute: ActivatedRoute,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly featureService: NewFeatureService,
|
||||
) {
|
||||
this.loadMembers();
|
||||
|
||||
@@ -106,7 +115,6 @@ export class InstanceComponent implements OnInit, OnDestroy {
|
||||
name: 'Instance',
|
||||
routerLink: ['/instance'],
|
||||
});
|
||||
|
||||
breadcrumbService.setBreadcrumb([instanceBread]);
|
||||
|
||||
this.adminService
|
||||
@@ -120,12 +128,43 @@ export class InstanceComponent implements OnInit, OnDestroy {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
|
||||
activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params: Params) => {
|
||||
const { id } = params;
|
||||
activatedRoute.queryParamMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
|
||||
const id = params.get('id');
|
||||
if (id) {
|
||||
this.id = id;
|
||||
}
|
||||
});
|
||||
|
||||
this.settingsList$ = this.getSettingsList();
|
||||
}
|
||||
|
||||
private getSettingsList(): Observable<SidenavSetting[]> {
|
||||
const features$ = this.getFeatures().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
|
||||
const actionsEnabled$ = features$.pipe(map((features) => features?.actions?.enabled));
|
||||
|
||||
return this.authService
|
||||
.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin || [])
|
||||
.pipe(
|
||||
withLatestFromSynchronousFix(actionsEnabled$),
|
||||
map(([settings, actionsEnabled]) =>
|
||||
settings
|
||||
.filter((setting) => actionsEnabled || setting.id !== ACTIONS.id)
|
||||
.filter((setting) => actionsEnabled || setting.id !== ACTIONS_TARGETS.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private getFeatures() {
|
||||
return defer(() => this.featureService.getInstanceFeatures()).pipe(
|
||||
timeout(1000),
|
||||
catchError((error) => {
|
||||
if (!(error instanceof TimeoutError)) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
return of(undefined);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public loadMembers(): void {
|
||||
@@ -185,18 +224,6 @@ export class InstanceComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public showDetail(): void {
|
||||
this.router.navigate(['/instance', 'members']);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsList = this.authService.isAllowedMapper(
|
||||
this.defaultSettingsList,
|
||||
(setting) => setting.requiredRoles.admin || [],
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.router.navigate(['/instance', 'members']).then();
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<cnsl-top-view
|
||||
title="{{ app?.name }}"
|
||||
[hasActions]="isZitadel === false && (['project.app.write:' + projectId, 'project.app.write'] | hasRole | async)"
|
||||
[hasActions]="!isZitadel && (['project.app.write:' + projectId, 'project.app.write'] | hasRole | async)"
|
||||
docLink="https://zitadel.com/docs/guides/manage/console/projects"
|
||||
[sub]="app?.oidcConfig ? ('APP.OIDC.APPTYPE.' + app?.oidcConfig?.appType | translate) : app?.apiConfig ? 'API' : 'SAML'"
|
||||
[isActive]="app?.state === AppState.APP_STATE_ACTIVE"
|
||||
@@ -10,43 +10,38 @@
|
||||
<ng-template topActions cnslHasRole [hasRole]="['project.app.write:' + projectId, 'project.app.write']">
|
||||
<div
|
||||
class="regen-secret"
|
||||
*ngIf="isZitadel === false && this.app?.oidcConfig && (currentAuthMethod === 'CODE' || currentAuthMethod === 'POST')"
|
||||
*ngIf="!isZitadel && this.app?.oidcConfig && (currentAuthMethod === 'CODE' || currentAuthMethod === 'POST')"
|
||||
>
|
||||
<button type="button" [disabled]="!canWrite" mat-menu-item (click)="regenerateOIDCClientSecret()">
|
||||
{{ 'APP.OIDC.REGENERATESECRET' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="regen-secret" *ngIf="isZitadel === false && this.app?.apiConfig && currentAuthMethod === 'BASIC'">
|
||||
<div class="regen-secret" *ngIf="!isZitadel && this.app?.apiConfig && currentAuthMethod === 'BASIC'">
|
||||
<button [disabled]="!canWrite" mat-menu-item (click)="regenerateAPIClientSecret()">
|
||||
{{ 'APP.API.REGENERATESECRET' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button *ngIf="isZitadel === false" mat-menu-item (click)="openNameDialog()" aria-label="Edit project name">
|
||||
<button *ngIf="!isZitadel" mat-menu-item (click)="openNameDialog()" aria-label="Edit project name">
|
||||
{{ 'ACTIONS.RENAME' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="isZitadel === false && app?.state !== AppState.APP_STATE_INACTIVE"
|
||||
*ngIf="!isZitadel && app?.state !== AppState.APP_STATE_INACTIVE"
|
||||
(click)="changeState(AppState.APP_STATE_INACTIVE)"
|
||||
>
|
||||
{{ 'ACTIONS.DEACTIVATE' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="isZitadel === false && app?.state === AppState.APP_STATE_INACTIVE"
|
||||
*ngIf="!isZitadel && app?.state === AppState.APP_STATE_INACTIVE"
|
||||
(click)="changeState(AppState.APP_STATE_ACTIVE)"
|
||||
>
|
||||
{{ 'ACTIONS.REACTIVATE' | translate }}
|
||||
</button>
|
||||
<ng-template cnslHasRole [hasRole]="['project.app.delete:' + projectId, 'project.app.delete']">
|
||||
<button
|
||||
*ngIf="isZitadel === false"
|
||||
mat-menu-item
|
||||
matTooltip="{{ 'APP.PAGES.DELETE' | translate }}"
|
||||
(click)="deleteApp()"
|
||||
>
|
||||
<button *ngIf="!isZitadel" mat-menu-item matTooltip="{{ 'APP.PAGES.DELETE' | translate }}" (click)="deleteApp()">
|
||||
<span [style.color]="'var(--warn)'">{{ 'APP.PAGES.DELETE' | translate }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -80,8 +75,8 @@
|
||||
|
||||
<div class="app-main-content">
|
||||
<cnsl-meta-layout>
|
||||
<cnsl-sidenav [(ngModel)]="currentSetting" [settingsList]="settingsList">
|
||||
<ng-container *ngIf="currentSetting === 'configuration'">
|
||||
<cnsl-sidenav [(setting)]="currentSetting" [settingsList]="settingsList">
|
||||
<ng-container *ngIf="currentSetting.id === 'configuration'">
|
||||
<cnsl-card *ngIf="oidcForm && app?.oidcConfig" title=" {{ 'APP.OIDC.TITLE' | translate }}">
|
||||
<div
|
||||
class="current-auth-method"
|
||||
@@ -274,7 +269,7 @@
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'token'">
|
||||
<ng-container *ngIf="currentSetting.id === 'token'">
|
||||
<cnsl-card *ngIf="oidcTokenForm && app?.oidcConfig" title=" {{ 'APP.OIDC.TOKENSECTIONTITLE' | translate }}">
|
||||
<form [formGroup]="oidcTokenForm" (ngSubmit)="saveOIDCApp()">
|
||||
<cnsl-form-field class="app-formfield">
|
||||
@@ -355,7 +350,7 @@
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'redirect-uris'">
|
||||
<ng-container *ngIf="currentSetting.id === 'redirect-uris'">
|
||||
<cnsl-card title=" {{ 'APP.OIDC.REDIRECTSECTIONTITLE' | translate }}">
|
||||
<cnsl-info-section *ngIf="appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE">
|
||||
<div class="dev-col">
|
||||
@@ -435,7 +430,7 @@
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'additional-origins'">
|
||||
<ng-container *ngIf="currentSetting.id === 'additional-origins'">
|
||||
<cnsl-card title=" {{ 'APP.ADDITIONALORIGINS' | translate }}">
|
||||
<p class="app-desc cnsl-secondary-text">{{ 'APP.ADDITIONALORIGINSDESC' | translate }}</p>
|
||||
<cnsl-additional-origins
|
||||
@@ -456,7 +451,7 @@
|
||||
</div>
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'urls'">
|
||||
<ng-container *ngIf="currentSetting.id === 'urls'">
|
||||
<cnsl-card title=" {{ 'APP.URLS' | translate }}">
|
||||
<cnsl-info-section *ngIf="issuer$ | async as issuer">
|
||||
<div
|
||||
@@ -502,7 +497,7 @@
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'configuration'">
|
||||
<ng-container *ngIf="currentSetting.id === 'configuration'">
|
||||
<cnsl-card
|
||||
*ngIf="initialAuthMethod === 'PK_JWT' && projectId && app && app.id"
|
||||
title="{{ 'USER.MACHINE.KEYSTITLE' | translate }}"
|
||||
|
@@ -73,7 +73,6 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
public canWrite: boolean = false;
|
||||
public errorMessage: string = '';
|
||||
public removable: boolean = true;
|
||||
public addOnBlur: boolean = true;
|
||||
|
||||
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
|
||||
|
||||
@@ -168,21 +167,20 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
public isZitadel: boolean = false;
|
||||
public docs!: GetOIDCInformationResponse.AsObject;
|
||||
|
||||
public OIDCAppType: any = OIDCAppType;
|
||||
public OIDCAuthMethodType: any = OIDCAuthMethodType;
|
||||
public APIAuthMethodType: any = APIAuthMethodType;
|
||||
public OIDCTokenType: any = OIDCTokenType;
|
||||
public OIDCGrantType: any = OIDCGrantType;
|
||||
public OIDCAppType = OIDCAppType;
|
||||
public OIDCAuthMethodType = OIDCAuthMethodType;
|
||||
public APIAuthMethodType = APIAuthMethodType;
|
||||
public OIDCTokenType = OIDCTokenType;
|
||||
public OIDCGrantType = OIDCGrantType;
|
||||
|
||||
public ChangeType: any = ChangeType;
|
||||
public ChangeType = ChangeType;
|
||||
|
||||
public requestRedirectValuesSubject$: Subject<void> = new Subject();
|
||||
public copiedKey: any = '';
|
||||
public InfoSectionType: any = InfoSectionType;
|
||||
public InfoSectionType = InfoSectionType;
|
||||
public copied: string = '';
|
||||
|
||||
public settingsList: SidenavSetting[] = [{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' }];
|
||||
public currentSetting: string | undefined = this.settingsList[0].id;
|
||||
public currentSetting = this.settingsList[0];
|
||||
|
||||
public isNew = signal<boolean>(false);
|
||||
|
||||
@@ -305,7 +303,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
if (projectId && appId) {
|
||||
this.projectId = projectId;
|
||||
this.appId = appId;
|
||||
this.getData(projectId, appId);
|
||||
this.getData(projectId, appId).then();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,7 +393,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (this.initialAuthMethod === 'BASIC') {
|
||||
this.settingsList = [{ id: 'urls', i18nKey: 'APP.URLS' }];
|
||||
this.currentSetting = 'urls';
|
||||
this.currentSetting = this.settingsList[0];
|
||||
} else {
|
||||
this.settingsList = [
|
||||
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' },
|
||||
@@ -742,13 +740,13 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (this.currentAuthMethod === 'BASIC') {
|
||||
this.settingsList = [{ id: 'urls', i18nKey: 'APP.URLS' }];
|
||||
this.currentSetting = 'urls';
|
||||
this.currentSetting = this.settingsList[0];
|
||||
} else {
|
||||
this.settingsList = [
|
||||
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' },
|
||||
{ id: 'urls', i18nKey: 'APP.URLS' },
|
||||
];
|
||||
this.currentSetting = 'configuration';
|
||||
this.currentSetting = this.settingsList[0];
|
||||
}
|
||||
}
|
||||
this.toast.showInfo('APP.TOAST.APIUPDATED', true);
|
||||
|
@@ -2,7 +2,6 @@
|
||||
[loading]="dataSource.loading$ | async"
|
||||
[selection]="selection"
|
||||
(refreshed)="refreshPage()"
|
||||
[dataSize]="dataSource.totalResult"
|
||||
[timestamp]="dataSource.viewTimestamp"
|
||||
>
|
||||
<ng-template cnslHasRole [hasRole]="['project.app.write']" actions>
|
||||
|
@@ -66,8 +66,8 @@
|
||||
|
||||
<div class="max-width-container">
|
||||
<cnsl-meta-layout>
|
||||
<cnsl-sidenav [(ngModel)]="currentSetting" [settingsList]="settingsList" [queryParam]="'id'">
|
||||
<ng-container *ngIf="currentSetting === 'general' && project">
|
||||
<cnsl-sidenav [(setting)]="currentSetting" [settingsList]="settingsList">
|
||||
<ng-container *ngIf="currentSetting.id === 'general' && project">
|
||||
<ng-template cnslHasRole [hasRole]="['project.app.read:' + project.id, 'project.app.read$']">
|
||||
<cnsl-application-grid
|
||||
*ngIf="grid"
|
||||
@@ -91,7 +91,7 @@
|
||||
</cnsl-card>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="isZitadel === false">
|
||||
<ng-container *ngIf="!isZitadel">
|
||||
<ng-template cnslHasRole [hasRole]="['project.role.read:' + project.id, 'project.role.read']">
|
||||
<cnsl-card
|
||||
id="roles"
|
||||
@@ -160,7 +160,7 @@
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'roles'">
|
||||
<ng-container *ngIf="currentSetting.id === 'roles'">
|
||||
<h2 class="section-h2">{{ 'PROJECT.ROLE.TITLE' | translate }}</h2>
|
||||
<p class="desc cnsl-secondary-text">{{ 'PROJECT.ROLE.DESCRIPTION' | translate }}</p>
|
||||
|
||||
@@ -172,12 +172,12 @@
|
||||
>
|
||||
</cnsl-project-roles-table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'projectgrants'">
|
||||
<ng-container *ngIf="currentSetting.id === 'projectgrants'">
|
||||
<h2 class="section-h2">{{ 'PROJECT.GRANT.TITLE' | translate }}</h2>
|
||||
<p class="desc cnsl-secondary-text">{{ 'PROJECT.GRANT.DESCRIPTION' | translate }}</p>
|
||||
<cnsl-project-grants [projectId]="projectId"></cnsl-project-grants>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="currentSetting === 'grants'">
|
||||
<ng-container *ngIf="currentSetting.id === 'grants'">
|
||||
<h2 class="section-h2">{{ 'GRANTS.TITLE' | translate }}</h2>
|
||||
<p class="desc cnsl-secondary-text">{{ 'GRANTS.DESC' | translate }}</p>
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, EventEmitter, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||
@@ -12,8 +11,7 @@ import { ProjectPrivateLabelingDialogComponent } from 'src/app/modules/project-p
|
||||
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
|
||||
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { App } from 'src/app/proto/generated/zitadel/app_pb';
|
||||
import { ListAppsResponse, UpdateProjectRequest } from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { UpdateProjectRequest } from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
|
||||
import { PrivateLabelingSetting, Project, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
|
||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
@@ -21,7 +19,7 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { NameDialogComponent } from '../../../../modules/name-dialog/name-dialog.component';
|
||||
import { NameDialogComponent } from 'src/app/modules/name-dialog/name-dialog.component';
|
||||
|
||||
const ROUTEPARAM = 'projectid';
|
||||
|
||||
@@ -39,11 +37,6 @@ export class OwnedProjectDetailComponent implements OnInit {
|
||||
public projectId: string = '';
|
||||
public project?: Project.AsObject;
|
||||
|
||||
public pageSizeApps: number = 10;
|
||||
public appsDataSource: MatTableDataSource<App.AsObject> = new MatTableDataSource<App.AsObject>();
|
||||
public appsResult!: ListAppsResponse.AsObject;
|
||||
public appsColumns: string[] = ['name'];
|
||||
|
||||
public ProjectState: any = ProjectState;
|
||||
public ChangeType: any = ChangeType;
|
||||
|
||||
@@ -61,22 +54,25 @@ export class OwnedProjectDetailComponent implements OnInit {
|
||||
public refreshChanges$: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
public settingsList: SidenavSetting[] = [GENERAL, ROLES, PROJECTGRANTS, GRANTS];
|
||||
public currentSetting: string = '';
|
||||
public currentSetting = this.settingsList[0];
|
||||
|
||||
constructor(
|
||||
public translate: TranslateService,
|
||||
private route: ActivatedRoute,
|
||||
private toast: ToastService,
|
||||
private mgmtService: ManagementService,
|
||||
private _location: Location,
|
||||
private dialog: MatDialog,
|
||||
private router: Router,
|
||||
private breadcrumbService: BreadcrumbService,
|
||||
) {
|
||||
this.currentSetting = 'general';
|
||||
route.queryParams.pipe(take(1)).subscribe((params: Params) => {
|
||||
const { id } = params;
|
||||
if (id) {
|
||||
this.currentSetting = id;
|
||||
route.queryParamMap.pipe(take(1)).subscribe((params) => {
|
||||
const id = params.get('id');
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const setting = this.settingsList.find((setting) => setting.id === id);
|
||||
if (setting) {
|
||||
this.currentSetting = setting;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -85,7 +81,7 @@ export class OwnedProjectDetailComponent implements OnInit {
|
||||
const projectId = this.route.snapshot.paramMap.get(ROUTEPARAM);
|
||||
if (projectId) {
|
||||
this.projectId = projectId;
|
||||
this.getData(projectId);
|
||||
this.getData(projectId).then();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +245,7 @@ export class OwnedProjectDetailComponent implements OnInit {
|
||||
const params: Params = {
|
||||
deferredReload: true,
|
||||
};
|
||||
this.router.navigate(['/projects'], { queryParams: params });
|
||||
this.router.navigate(['/projects'], { queryParams: params }).then();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
@@ -280,10 +276,6 @@ export class OwnedProjectDetailComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public navigateBack(): void {
|
||||
this._location.back();
|
||||
}
|
||||
|
||||
public updateName(): void {
|
||||
this.saveProject();
|
||||
}
|
||||
@@ -323,7 +315,7 @@ export class OwnedProjectDetailComponent implements OnInit {
|
||||
|
||||
public showDetail(): void {
|
||||
if (this.project) {
|
||||
this.router.navigate(['projects', this.project.id, 'members']);
|
||||
this.router.navigate(['projects', this.project.id, 'members']).then();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@
|
||||
[loading]="dataSource.loading$ | async"
|
||||
*ngIf="projectId"
|
||||
(refreshed)="refreshPage()"
|
||||
[dataSize]="dataSource.totalResult"
|
||||
[selection]="selection"
|
||||
[timestamp]="dataSource.viewTimestamp"
|
||||
(refreshed)="getRoleOptions(projectId)"
|
||||
|
@@ -2,7 +2,6 @@
|
||||
<cnsl-refresh-table
|
||||
[hideRefresh]="true"
|
||||
(refreshed)="refreshPage(type)"
|
||||
[dataSize]="totalResult"
|
||||
[timestamp]="viewTimestamp"
|
||||
[selection]="selection"
|
||||
[loading]="loading$ | async"
|
||||
|
@@ -21,6 +21,11 @@
|
||||
{{ 'USER.LOGINMETHODS.EMAIL.ISVERIFIED' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<cnsl-form-field class="username">
|
||||
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label>
|
||||
<input class="stretchInput" cnslInput formControlName="username" required />
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="givenName">
|
||||
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="givenName" required />
|
||||
@@ -29,14 +34,6 @@
|
||||
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="familyName" required />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="nickName">
|
||||
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="nickName" />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="username">
|
||||
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="username" required />
|
||||
</cnsl-form-field>
|
||||
|
||||
<div class="authenticationFactor">
|
||||
<mat-radio-group
|
||||
|
@@ -13,8 +13,8 @@
|
||||
grid-template-areas:
|
||||
'email email'
|
||||
'emailVerified emailVerified'
|
||||
'username username'
|
||||
'givenName familyName'
|
||||
'nickName username'
|
||||
'authenticationFactor authenticationFactor';
|
||||
column-gap: 1rem;
|
||||
}
|
||||
@@ -35,10 +35,6 @@
|
||||
grid-area: familyName;
|
||||
}
|
||||
|
||||
.nickName {
|
||||
grid-area: nickName;
|
||||
}
|
||||
|
||||
.username {
|
||||
grid-area: username;
|
||||
}
|
||||
|
@@ -97,7 +97,6 @@ export class UserCreateV2Component implements OnInit {
|
||||
username: new FormControl('', { nonNullable: true, validators: [requiredValidator, minLengthValidator(2)] }),
|
||||
givenName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
familyName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
nickName: new FormControl('', { nonNullable: true }),
|
||||
emailVerified: new FormControl(false, { nonNullable: true }),
|
||||
authenticationFactor: new FormControl<AuthenticationFactor['factor']>(authenticationFactor, {
|
||||
nonNullable: true,
|
||||
@@ -188,7 +187,6 @@ export class UserCreateV2Component implements OnInit {
|
||||
profile: {
|
||||
givenName: userValues.givenName,
|
||||
familyName: userValues.familyName,
|
||||
nickName: userValues.nickName,
|
||||
},
|
||||
email: {
|
||||
email: userValues.email,
|
||||
|
@@ -11,12 +11,7 @@
|
||||
>
|
||||
<mat-icon class="icon">refresh</mat-icon>
|
||||
</button>
|
||||
<cnsl-refresh-table
|
||||
[hideRefresh]="true"
|
||||
[loading]="loading$ | async"
|
||||
(refreshed)="getPasswordless()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
>
|
||||
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="getPasswordless()">
|
||||
<button
|
||||
actions
|
||||
class="button"
|
||||
|
@@ -24,13 +24,11 @@
|
||||
<div class="max-width-container">
|
||||
<cnsl-meta-layout *ngIf="user(userQuery) as user">
|
||||
<cnsl-sidenav
|
||||
*ngIf="currentSetting$ | async as currentSetting"
|
||||
[ngModel]="currentSetting"
|
||||
(ngModelChange)="goToSetting($event)"
|
||||
[setting]="currentSetting$()"
|
||||
(settingChange)="currentSetting$.set($event)"
|
||||
[settingsList]="settingsList"
|
||||
queryParam="id"
|
||||
>
|
||||
<ng-container *ngIf="currentSetting === 'general' && humanUser(userQuery) as humanUser">
|
||||
<ng-container *ngIf="currentSetting$().id === 'general' && humanUser(userQuery) as humanUser">
|
||||
<cnsl-card
|
||||
*ngIf="humanUser.type.value.profile as profile"
|
||||
class="app-card"
|
||||
@@ -91,11 +89,11 @@
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'idp'">
|
||||
<ng-container *ngIf="currentSetting$().id === 'idp'">
|
||||
<cnsl-external-idps [userId]="user.userId" [service]="grpcAuthService"></cnsl-external-idps>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'security'">
|
||||
<ng-container *ngIf="currentSetting$().id === 'security'">
|
||||
<cnsl-card *ngIf="humanUser(userQuery) as humanUser" title="{{ 'USER.PASSWORD.TITLE' | translate }}">
|
||||
<div class="contact-method-col">
|
||||
<div class="contact-method-row">
|
||||
@@ -127,7 +125,7 @@
|
||||
></cnsl-auth-user-mfa>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'grants'">
|
||||
<ng-container *ngIf="currentSetting$().id === 'grants'">
|
||||
<cnsl-card title="{{ 'GRANTS.USER.TITLE' | translate }}" description="{{ 'GRANTS.USER.DESCRIPTION' | translate }}">
|
||||
<cnsl-user-grants
|
||||
[userId]="user.userId"
|
||||
@@ -149,7 +147,7 @@
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'memberships'">
|
||||
<ng-container *ngIf="currentSetting$().id === 'memberships'">
|
||||
<cnsl-card
|
||||
title="{{ 'USER.MEMBERSHIPS.TITLE' | translate }}"
|
||||
description="{{ 'USER.MEMBERSHIPS.DESCRIPTION' | translate }}"
|
||||
@@ -158,7 +156,7 @@
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'metadata' && (metadata$ | async) as metadataQuery">
|
||||
<ng-container *ngIf="currentSetting$().id === 'metadata' && (metadata$ | async) as metadataQuery">
|
||||
<cnsl-metadata
|
||||
*ngIf="metadataQuery.state !== 'error'"
|
||||
[metadata]="metadataQuery.value"
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { MediaMatcher } from '@angular/cdk/layout';
|
||||
import { Component, DestroyRef, EventEmitter, OnInit } from '@angular/core';
|
||||
import { Component, DestroyRef, EventEmitter, OnInit, signal } from '@angular/core';
|
||||
import { Validators } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Buffer } from 'buffer';
|
||||
import { defer, EMPTY, fromEvent, mergeWith, Observable, of, shareReplay, Subject, switchMap, take } from 'rxjs';
|
||||
import { defer, EMPTY, mergeWith, Observable, of, shareReplay, Subject, switchMap, take } from 'rxjs';
|
||||
import { ChangeType } from 'src/app/modules/changes/changes.component';
|
||||
import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
import { InfoDialogComponent } from 'src/app/modules/info-dialog/info-dialog.component';
|
||||
@@ -25,7 +25,7 @@ import { formatPhone } from 'src/app/utils/formatPhone';
|
||||
import { EditDialogComponent, EditDialogData, EditDialogResult, EditDialogType } from './edit-dialog/edit-dialog.component';
|
||||
import { LanguagesService } from 'src/app/services/languages.service';
|
||||
import { Gender, HumanProfile, HumanUser, User, UserState } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { catchError, filter, map, startWith, withLatestFrom } from 'rxjs/operators';
|
||||
import { catchError, filter, map, startWith } from 'rxjs/operators';
|
||||
import { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith';
|
||||
import { NewAuthService } from 'src/app/services/new-auth.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
@@ -75,7 +75,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
protected readonly user$: Observable<UserQuery>;
|
||||
protected readonly metadata$: Observable<MetadataQuery>;
|
||||
private readonly savedLanguage$: Observable<string>;
|
||||
protected readonly currentSetting$: Observable<string | undefined>;
|
||||
protected readonly currentSetting$ = signal<SidenavSetting>(this.settingsList[0]);
|
||||
protected readonly loginPolicy$: Observable<LoginPolicy>;
|
||||
protected readonly userName$: Observable<string>;
|
||||
|
||||
@@ -86,7 +86,6 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
private dialog: MatDialog,
|
||||
private auth: AuthenticationService,
|
||||
private breadcrumbService: BreadcrumbService,
|
||||
private mediaMatcher: MediaMatcher,
|
||||
public langSvc: LanguagesService,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly newAuthService: NewAuthService,
|
||||
@@ -95,7 +94,6 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly router: Router,
|
||||
) {
|
||||
this.currentSetting$ = this.getCurrentSetting$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.user$ = this.getUser$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.userName$ = this.getUserName(this.user$);
|
||||
this.savedLanguage$ = this.getSavedLanguage$(this.user$);
|
||||
@@ -150,6 +148,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
this.user$.pipe(mergeWith(this.metadata$), takeUntilDestroyed(this.destroyRef)).subscribe((query) => {
|
||||
if (query.state == 'error') {
|
||||
this.toast.showError(query.error);
|
||||
@@ -159,22 +158,16 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
this.savedLanguage$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((savedLanguage) => this.translate.use(savedLanguage));
|
||||
}
|
||||
|
||||
private getCurrentSetting$(): Observable<string | undefined> {
|
||||
const mediaq: string = '(max-width: 500px)';
|
||||
const matcher = this.mediaMatcher.matchMedia(mediaq);
|
||||
const small$ = fromEvent(matcher, 'change', ({ matches }: MediaQueryListEvent) => matches).pipe(
|
||||
startWith(matcher.matches),
|
||||
);
|
||||
|
||||
return this.route.queryParamMap.pipe(
|
||||
map((params) => params.get('id')),
|
||||
filter(Boolean),
|
||||
startWith('general'),
|
||||
withLatestFrom(small$),
|
||||
map(([id, small]) => (small ? undefined : id)),
|
||||
);
|
||||
const param = this.route.snapshot.queryParamMap.get('id');
|
||||
if (!param) {
|
||||
return;
|
||||
}
|
||||
const setting = this.settingsList.find(({ id }) => id === param);
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
this.currentSetting$.set(setting);
|
||||
}
|
||||
|
||||
private getUser$(): Observable<UserQuery> {
|
||||
|
@@ -9,12 +9,7 @@
|
||||
<mat-icon class="icon">refresh</mat-icon>
|
||||
</button>
|
||||
|
||||
<cnsl-refresh-table
|
||||
[hideRefresh]="true"
|
||||
[loading]="loading$ | async"
|
||||
(refreshed)="getMFAs()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
>
|
||||
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="getMFAs()">
|
||||
<button
|
||||
actions
|
||||
class="button"
|
||||
|
@@ -8,13 +8,7 @@
|
||||
>
|
||||
<mat-icon class="icon">refresh</mat-icon>
|
||||
</button>
|
||||
<cnsl-refresh-table
|
||||
[hideRefresh]="true"
|
||||
[loading]="loading$ | async"
|
||||
[dataSize]="dataSource.data.length"
|
||||
[timestamp]="viewTimestamp"
|
||||
[selection]="selection"
|
||||
>
|
||||
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" [timestamp]="viewTimestamp" [selection]="selection">
|
||||
<div class="table-wrapper">
|
||||
<table class="table" mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="select">
|
||||
|
@@ -11,7 +11,7 @@
|
||||
>
|
||||
<mat-icon class="icon">refresh</mat-icon>
|
||||
</button>
|
||||
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" [dataSize]="dataSource.data.length">
|
||||
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async">
|
||||
<button
|
||||
actions
|
||||
[disabled]="disabled"
|
||||
|
@@ -52,10 +52,9 @@
|
||||
<cnsl-meta-layout>
|
||||
<cnsl-sidenav
|
||||
*ngIf="settingsList$ | async as settingsList"
|
||||
[ngModel]="currentSetting$ | async"
|
||||
(ngModelChange)="goToSetting($event)"
|
||||
[setting]="currentSetting$()"
|
||||
(settingChange)="currentSetting$.set($event)"
|
||||
[settingsList]="settingsList"
|
||||
queryParam="id"
|
||||
>
|
||||
<div class="max-width-container">
|
||||
<cnsl-info-section class="locked" *ngIf="user?.state === UserState.LOCKED" [type]="InfoSectionType.WARN">
|
||||
@@ -72,7 +71,7 @@
|
||||
</cnsl-info-section>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'general'">
|
||||
<ng-container *ngIf="currentSetting$().id === 'general'">
|
||||
<ng-template
|
||||
*ngIf="humanUser(user) as user"
|
||||
cnslHasRole
|
||||
@@ -141,13 +140,13 @@
|
||||
</ng-container>
|
||||
|
||||
<cnsl-external-idps
|
||||
*ngIf="(currentSetting$ | async) === 'idp' && user.type.case === 'human' && user.userId"
|
||||
*ngIf="currentSetting$().id === 'idp' && user.type.case === 'human' && user.userId"
|
||||
[userId]="user.userId"
|
||||
[service]="mgmtService"
|
||||
/>
|
||||
|
||||
<cnsl-card
|
||||
*ngIf="(currentSetting$ | async) === 'general' && user.type.case === 'machine'"
|
||||
*ngIf="currentSetting$().id === 'general' && user.type.case === 'machine'"
|
||||
title="{{ 'USER.MACHINE.TITLE' | translate }}"
|
||||
>
|
||||
<cnsl-detail-form-machine
|
||||
@@ -158,7 +157,7 @@
|
||||
/>
|
||||
</cnsl-card>
|
||||
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'pat'">
|
||||
<ng-container *ngIf="currentSetting$().id === 'pat'">
|
||||
<ng-template cnslHasRole [hasRole]="['user.read$', 'user.read:' + user.userId]">
|
||||
<cnsl-card
|
||||
*ngIf="user.type.case === 'machine' && user.userId"
|
||||
@@ -170,7 +169,7 @@
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'keys'">
|
||||
<ng-container *ngIf="currentSetting$().id === 'keys'">
|
||||
<ng-template cnslHasRole [hasRole]="['user.read$', 'user.read:' + user.userId]">
|
||||
<cnsl-card
|
||||
*ngIf="user.type.case === 'machine' && user.userId"
|
||||
@@ -182,7 +181,7 @@
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'security'">
|
||||
<ng-container *ngIf="currentSetting$().id === 'security'">
|
||||
<cnsl-card *ngIf="user.type.case === 'human'" title="{{ 'USER.PASSWORD.TITLE' | translate }}">
|
||||
<div class="contact-method-col">
|
||||
<div class="contact-method-row">
|
||||
@@ -214,7 +213,7 @@
|
||||
<cnsl-user-mfa *ngIf="user.type.case === 'human'" [user]="user"></cnsl-user-mfa>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'grants'">
|
||||
<ng-container *ngIf="currentSetting$().id === 'grants'">
|
||||
<cnsl-card
|
||||
*ngIf="user.userId"
|
||||
title="{{ 'GRANTS.USER.TITLE' | translate }}"
|
||||
@@ -239,7 +238,7 @@
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'memberships'">
|
||||
<ng-container *ngIf="currentSetting$().id === 'memberships'">
|
||||
<cnsl-card
|
||||
*ngIf="user.userId"
|
||||
title="{{ 'USER.MEMBERSHIPS.TITLE' | translate }}"
|
||||
@@ -249,7 +248,7 @@
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'metadata' && (metadata$ | async) as metadataQuery">
|
||||
<ng-container *ngIf="currentSetting$().id === 'metadata' && (metadata$ | async) as metadataQuery">
|
||||
<cnsl-metadata
|
||||
*ngIf="user.userId && metadataQuery.state !== 'error'"
|
||||
[metadata]="metadataQuery.value"
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import { MediaMatcher } from '@angular/cdk/layout';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, DestroyRef, EventEmitter, OnInit } from '@angular/core';
|
||||
import { Component, DestroyRef, EventEmitter, OnInit, signal } from '@angular/core';
|
||||
import { Validators } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Buffer } from 'buffer';
|
||||
import { catchError, filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { catchError, filter, map, startWith, take } from 'rxjs/operators';
|
||||
import { ChangeType } from 'src/app/modules/changes/changes.component';
|
||||
import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
|
||||
@@ -39,7 +38,7 @@ import {
|
||||
combineLatestWith,
|
||||
defer,
|
||||
EMPTY,
|
||||
fromEvent,
|
||||
identity,
|
||||
mergeWith,
|
||||
Observable,
|
||||
ObservedValueOf,
|
||||
@@ -79,6 +78,7 @@ type MetadataQuery =
|
||||
|
||||
type UserWithHumanType = Omit<UserV2, 'type'> & { type: { case: 'human'; value: HumanUser } };
|
||||
|
||||
// todo: figure out why media matcher is needed
|
||||
@Component({
|
||||
selector: 'cnsl-user-detail',
|
||||
templateUrl: './user-detail.component.html',
|
||||
@@ -98,7 +98,7 @@ export class UserDetailComponent implements OnInit {
|
||||
public refreshChanges$: EventEmitter<void> = new EventEmitter();
|
||||
public InfoSectionType: any = InfoSectionType;
|
||||
|
||||
public currentSetting$: Observable<string | undefined>;
|
||||
public currentSetting$ = signal<SidenavSetting>(GENERAL);
|
||||
public settingsList$: Observable<SidenavSetting[]>;
|
||||
public metadata$: Observable<MetadataQuery>;
|
||||
public loginPolicy$: Observable<LoginPolicy>;
|
||||
@@ -111,7 +111,6 @@ export class UserDetailComponent implements OnInit {
|
||||
private _location: Location,
|
||||
private dialog: MatDialog,
|
||||
private router: Router,
|
||||
private mediaMatcher: MediaMatcher,
|
||||
public langSvc: LanguagesService,
|
||||
private readonly userService: UserService,
|
||||
private readonly newMgmtService: NewMgmtService,
|
||||
@@ -126,9 +125,8 @@ export class UserDetailComponent implements OnInit {
|
||||
}),
|
||||
]);
|
||||
|
||||
this.currentSetting$ = this.getCurrentSetting$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.user$ = this.getUser$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.settingsList$ = this.getSettingsList$(this.user$);
|
||||
this.settingsList$ = this.getSettingsList$(this.user$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.metadata$ = this.getMetadata$(this.user$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
|
||||
this.loginPolicy$ = defer(() => this.newMgmtService.getLoginPolicy()).pipe(
|
||||
@@ -177,31 +175,6 @@ export class UserDetailComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
private getCurrentSetting$(): Observable<string | undefined> {
|
||||
const mediaq: string = '(max-width: 500px)';
|
||||
const matcher = this.mediaMatcher.matchMedia(mediaq);
|
||||
const small$ = fromEvent(matcher, 'change', ({ matches }: MediaQueryListEvent) => matches).pipe(
|
||||
startWith(matcher.matches),
|
||||
);
|
||||
|
||||
return this.route.queryParamMap.pipe(
|
||||
map((params) => params.get('id')),
|
||||
filter(Boolean),
|
||||
startWith('general'),
|
||||
withLatestFrom(small$),
|
||||
map(([id, small]) => (small ? undefined : id)),
|
||||
);
|
||||
}
|
||||
|
||||
public async goToSetting(setting: string) {
|
||||
await this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { id: setting },
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: true,
|
||||
});
|
||||
}
|
||||
|
||||
private getUserById(userId: string): Observable<UserQuery> {
|
||||
return defer(() => this.userService.getUserById(userId)).pipe(
|
||||
map(({ user }) => {
|
||||
@@ -244,6 +217,20 @@ export class UserDetailComponent implements OnInit {
|
||||
this.toast.showError(query.value);
|
||||
}
|
||||
});
|
||||
|
||||
const param = this.route.snapshot.queryParamMap.get('id');
|
||||
if (!param) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.settingsList$
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map((settings) => settings.find(({ id }) => id === param)),
|
||||
filter(Boolean),
|
||||
take(1),
|
||||
)
|
||||
.subscribe((setting) => this.currentSetting$.set(setting));
|
||||
}
|
||||
|
||||
public changeUsername(user: UserV2): void {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user