fix(console): Implement generic feature toggle (#9516)

This PR implements a component which is used to render the feature
settings in a generic way.
All features besides`loginV2` and `improvedPerformance` are rendered.

Note: The feature flags are not autogenerated as the console needs
descriptions for the respective features and there are multiple types
where additional fields like text input is required

Closes #9090

---------

Co-authored-by: conblem <[email protected]>
This commit is contained in:
Max Peintner
2025-03-19 15:52:09 +00:00
committed by GitHub
co-authored by conblem
parent b418ea75bb
commit 83108d1dc3
27 changed files with 426 additions and 589 deletions
@@ -0,0 +1,40 @@
<div class="feature-row" *ngIf="$any(toggleStates)[toggleStateKey]">
<span>{{ 'SETTING.FEATURES.' + toggleStateKey.toUpperCase() | translate }}</span>
<div class="row">
<mat-button-toggle-group
class="theme-toggle"
class="buttongroup"
[(ngModel)]="$any(toggleStates)[toggleStateKey].state"
(change)="onToggleChange()"
name="displayview"
>
<mat-button-toggle [value]="ToggleState.DISABLED">
<div class="toggle-row">
<span>{{ 'SETTING.FEATURES.STATES.DISABLED' | translate }}</span>
<div
*ngIf="!enabled && isInherited"
class="current-dot"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.DISABLED' | translate }}"
></div>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.ENABLED">
<div class="toggle-row">
<span>{{ 'SETTING.FEATURES.STATES.ENABLED' | translate }}</span>
<div
*ngIf="enabled && isInherited"
class="current-dot"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.ENABLED' | translate }}"
></div>
</div>
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<ng-content></ng-content>
<cnsl-info-section
class="feature-info"
*ngIf="'SETTING.FEATURES.' + toggleStateKey.toUpperCase() + '_DESCRIPTION' | translate as i18nDescription"
>{{ i18nDescription }}</cnsl-info-section
>
</div>
@@ -0,0 +1,39 @@
.feature-row {
display: flex;
flex-direction: column;
padding-bottom: 1rem;
.row {
display: flex;
align-items: center;
justify-content: space-between;
.buttongroup {
margin-right: 0.5rem;
margin-top: 0.5rem;
.toggle-row {
display: flex;
align-items: center;
i {
margin-right: 0.5rem;
}
.info-i {
font-size: 1.2rem;
margin-left: 0.5rem;
margin-right: 0;
}
.current-dot {
height: 8px;
width: 8px;
border-radius: 50%;
margin-left: 0.5rem;
background-color: rgb(59, 128, 247);
}
}
}
}
}
@@ -0,0 +1,55 @@
import { CommonModule } 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 { 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';
@Component({
standalone: true,
selector: 'cnsl-feature-toggle',
templateUrl: './feature-toggle.component.html',
styleUrls: ['./feature-toggle.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
FormsModule,
TranslateModule,
MatButtonModule,
InfoSectionModule,
MatTooltipModule,
CopyToClipboardModule,
CopyRowComponent,
MatButtonToggleModule,
],
})
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;
}
get enabled() {
// TODO: remove casting as not all features are a FeatureFlag
return (this.featureData[this.toggleStateKey as ToggleStateKeys] as FeatureFlag)?.enabled;
}
onToggleChange() {
this.toggleChange.emit();
}
}
@@ -20,389 +20,13 @@
<cnsl-card *ngIf="toggleStates && featureData">
<div class="features">
<div class="feature-row" *ngIf="toggleStates.loginDefaultOrg">
<span>{{ 'SETTING.FEATURES.LOGINDEFAULTORG' | translate }}</span>
<div class="row">
<mat-button-toggle-group
class="theme-toggle"
class="buttongroup"
[(ngModel)]="toggleStates.loginDefaultOrg.state"
(change)="validateAndSave()"
name="displayview"
aria-label="Display View"
>
<mat-button-toggle [value]="ToggleState.INHERITED">
<div class="toggle-row">
<span>{{ 'SETTING.FEATURES.STATES.INHERITED' | translate }}</span>
<i
class="info-i las la-question-circle"
matTooltip="{{ 'SETTING.FEATURES.INHERITED_DESCRIPTION' | translate }}"
></i>
<div
*ngIf="
!!featureData.loginDefaultOrg?.enabled &&
(featureData.loginDefaultOrg?.source === Source.SOURCE_SYSTEM ||
featureData.loginDefaultOrg?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot enabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.ENABLED' | translate }}"
></div>
<div
*ngIf="
!featureData.loginDefaultOrg?.enabled &&
(featureData.loginDefaultOrg?.source === Source.SOURCE_SYSTEM ||
featureData.loginDefaultOrg?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot disabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.DISABLED' | translate }}"
></div>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.DISABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }}</span>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.ENABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }}</span>
</div>
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<cnsl-info-section class="feature-info">{{
'SETTING.FEATURES.LOGINDEFAULTORG_DESCRIPTION' | translate
}}</cnsl-info-section>
</div>
<div class="feature-row" *ngIf="toggleStates.oidcLegacyIntrospection">
<span>{{ 'SETTING.FEATURES.OIDCLEGACYINTROSPECTION' | translate }}</span>
<div class="row">
<mat-button-toggle-group
class="theme-toggle"
class="buttongroup"
[(ngModel)]="toggleStates.oidcLegacyIntrospection.state"
(change)="validateAndSave()"
name="displayview"
aria-label="Display View"
>
<mat-button-toggle [value]="ToggleState.INHERITED">
<div class="toggle-row">
<span>{{ 'SETTING.FEATURES.STATES.INHERITED' | translate }}</span>
<i
class="info-i las la-question-circle"
matTooltip="{{ 'SETTING.FEATURES.INHERITED_DESCRIPTION' | translate }}"
></i>
<div
*ngIf="
!!featureData.oidcLegacyIntrospection?.enabled &&
(featureData.oidcLegacyIntrospection?.source === Source.SOURCE_SYSTEM ||
featureData.oidcLegacyIntrospection?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot enabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.ENABLED' | translate }}"
></div>
<div
*ngIf="
!featureData.oidcLegacyIntrospection?.enabled &&
(featureData.oidcLegacyIntrospection?.source === Source.SOURCE_SYSTEM ||
featureData.oidcLegacyIntrospection?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot disabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.DISABLED' | translate }}"
></div>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.DISABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }}</span>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.ENABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }}</span>
</div>
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<cnsl-info-section class="feature-info">{{
'SETTING.FEATURES.OIDCLEGACYINTROSPECTION_DESCRIPTION' | translate
}}</cnsl-info-section>
</div>
<div class="feature-row" *ngIf="toggleStates.oidcTokenExchange">
<span>{{ 'SETTING.FEATURES.OIDCTOKENEXCHANGE' | translate }}</span>
<div class="row">
<mat-button-toggle-group
class="theme-toggle"
class="buttongroup"
[(ngModel)]="toggleStates.oidcTokenExchange.state"
(change)="validateAndSave()"
name="displayview"
aria-label="Display View"
>
<mat-button-toggle [value]="ToggleState.INHERITED">
<div class="toggle-row">
<span>{{ 'SETTING.FEATURES.STATES.INHERITED' | translate }}</span>
<i
class="info-i las la-question-circle"
matTooltip="{{ 'SETTING.FEATURES.INHERITED_DESCRIPTION' | translate }}"
></i>
<div
*ngIf="
!!featureData.oidcTokenExchange?.enabled &&
(featureData.oidcTokenExchange?.source === Source.SOURCE_SYSTEM ||
featureData.oidcTokenExchange?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot enabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.ENABLED' | translate }}"
></div>
<div
*ngIf="
!featureData.oidcTokenExchange?.enabled &&
(featureData.oidcTokenExchange?.source === Source.SOURCE_SYSTEM ||
featureData.oidcTokenExchange?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot disabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.DISABLED' | translate }}"
></div>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.DISABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }}</span>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.ENABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }}</span>
</div>
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<cnsl-info-section class="feature-info">{{
'SETTING.FEATURES.OIDCTOKENEXCHANGE_DESCRIPTION' | translate
}}</cnsl-info-section>
</div>
<div class="feature-row" *ngIf="toggleStates.oidcTriggerIntrospectionProjections">
<span>{{ 'SETTING.FEATURES.OIDCTRIGGERINTROSPECTIONPROJECTIONS' | translate }}</span>
<div class="row">
<mat-button-toggle-group
class="theme-toggle"
class="buttongroup"
[(ngModel)]="toggleStates.oidcTriggerIntrospectionProjections.state"
(change)="validateAndSave()"
name="displayview"
aria-label="Display View"
>
<mat-button-toggle [value]="ToggleState.INHERITED">
<div class="toggle-row">
<span>{{ 'SETTING.FEATURES.STATES.INHERITED' | translate }}</span>
<i
class="info-i las la-question-circle"
matTooltip="{{ 'SETTING.FEATURES.INHERITED_DESCRIPTION' | translate }}"
></i>
<div
*ngIf="
!!featureData.oidcTriggerIntrospectionProjections?.enabled &&
(featureData.oidcTriggerIntrospectionProjections?.source === Source.SOURCE_SYSTEM ||
featureData.oidcTriggerIntrospectionProjections?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot enabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.ENABLED' | translate }}"
></div>
<div
*ngIf="
!featureData.oidcTriggerIntrospectionProjections?.enabled &&
(featureData.oidcTriggerIntrospectionProjections?.source === Source.SOURCE_SYSTEM ||
featureData.oidcTriggerIntrospectionProjections?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot disabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.DISABLED' | translate }}"
></div>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.DISABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }}</span>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.ENABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }}</span>
</div>
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<cnsl-info-section class="feature-info">{{
'SETTING.FEATURES.OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION' | translate
}}</cnsl-info-section>
</div>
<div class="feature-row" *ngIf="toggleStates.userSchema">
<span>{{ 'SETTING.FEATURES.USERSCHEMA' | translate }}</span>
<div class="row">
<mat-button-toggle-group
class="theme-toggle"
class="buttongroup"
[(ngModel)]="toggleStates.userSchema.state"
(change)="validateAndSave()"
name="displayview"
aria-label="Display View"
>
<mat-button-toggle [value]="ToggleState.INHERITED">
<div class="toggle-row">
<span>{{ 'SETTING.FEATURES.STATES.INHERITED' | translate }}</span>
<i
class="info-i las la-question-circle"
matTooltip="{{ 'SETTING.FEATURES.INHERITED_DESCRIPTION' | translate }}"
></i>
<div
*ngIf="
!!featureData.userSchema?.enabled &&
(featureData.userSchema?.source === Source.SOURCE_SYSTEM ||
featureData.userSchema?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot enabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.ENABLED' | translate }}"
></div>
<div
*ngIf="
!featureData.userSchema?.enabled &&
(featureData.userSchema?.source === Source.SOURCE_SYSTEM ||
featureData.userSchema?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot disabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.DISABLED' | translate }}"
></div>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.DISABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }}</span>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.ENABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }}</span>
</div>
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<cnsl-info-section class="feature-info">{{
'SETTING.FEATURES.USERSCHEMA_DESCRIPTION' | translate
}}</cnsl-info-section>
</div>
<div class="feature-row" *ngIf="toggleStates.actions">
<span>{{ 'SETTING.FEATURES.ACTIONS' | translate }}</span>
<div class="row">
<mat-button-toggle-group
class="theme-toggle"
class="buttongroup"
[(ngModel)]="toggleStates.actions.state"
(change)="validateAndSave()"
name="displayview"
aria-label="Display View"
>
<mat-button-toggle [value]="ToggleState.INHERITED">
<div class="toggle-row">
<span>{{ 'SETTING.FEATURES.STATES.INHERITED' | translate }}</span>
<i
class="info-i las la-question-circle"
matTooltip="{{ 'SETTING.FEATURES.INHERITED_DESCRIPTION' | translate }}"
></i>
<div
*ngIf="
!!featureData.actions?.enabled &&
(featureData.actions?.source === Source.SOURCE_SYSTEM ||
featureData.actions?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot enabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.ENABLED' | translate }}"
></div>
<div
*ngIf="
!featureData.actions?.enabled &&
(featureData.actions?.source === Source.SOURCE_SYSTEM ||
featureData.actions?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot disabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.DISABLED' | translate }}"
></div>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.DISABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }}</span>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.ENABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }}</span>
</div>
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<cnsl-info-section class="feature-info">{{ 'SETTING.FEATURES.ACTIONS_DESCRIPTION' | translate }}</cnsl-info-section>
</div>
<div class="feature-row" *ngIf="toggleStates.oidcSingleV1SessionTermination">
<span>{{ 'SETTING.FEATURES.OIDCSINGLEV1SESSIONTERMINATION' | translate }}</span>
<div class="row">
<mat-button-toggle-group
class="theme-toggle"
class="buttongroup"
[(ngModel)]="toggleStates.oidcSingleV1SessionTermination.state"
(change)="validateAndSave()"
name="displayview"
aria-label="Display View"
>
<mat-button-toggle [value]="ToggleState.INHERITED">
<div class="toggle-row">
<span>{{ 'SETTING.FEATURES.STATES.INHERITED' | translate }}</span>
<i
class="info-i las la-question-circle"
matTooltip="{{ 'SETTING.FEATURES.INHERITED_DESCRIPTION' | translate }}"
></i>
<div
*ngIf="
!!featureData.oidcSingleV1SessionTermination?.enabled &&
(featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_SYSTEM ||
featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot enabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.ENABLED' | translate }}"
></div>
<div
*ngIf="
!featureData.oidcSingleV1SessionTermination?.enabled &&
(featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_SYSTEM ||
featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_UNSPECIFIED)
"
class="current-dot disabled"
matTooltip="{{ 'SETTING.FEATURES.INHERITEDINDICATOR_DESCRIPTION.DISABLED' | translate }}"
></div>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.DISABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }}</span>
</div>
</mat-button-toggle>
<mat-button-toggle [value]="ToggleState.ENABLED">
<div class="toggle-row">
<span> {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }}</span>
</div>
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<cnsl-info-section class="feature-info">{{
'SETTING.FEATURES.OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION' | translate
}}</cnsl-info-section>
</div>
<cnsl-feature-toggle
*ngFor="let key of toggleStateKeys"
[featureData]="featureData"
[toggleStates]="toggleStates"
[toggleStateKey]="key"
(toggleChange)="validateAndSave()"
></cnsl-feature-toggle>
</div>
</cnsl-card>
</div>
@@ -11,30 +11,46 @@ import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { Source } from 'src/app/proto/generated/zitadel/feature/v2beta/feature_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { FeatureService } from 'src/app/services/feature.service';
import { ToastService } from 'src/app/services/toast.service';
import { FeatureToggleComponent } from '../feature-toggle/feature-toggle.component';
import { NewFeatureService } from 'src/app/services/new-feature.service';
import {
GetInstanceFeaturesResponse,
SetInstanceFeaturesRequest,
} from 'src/app/proto/generated/zitadel/feature/v2/instance_pb';
SetInstanceFeaturesRequestSchema,
} from '@zitadel/proto/zitadel/feature/v2/instance_pb';
import { FeatureFlag, Source } from '@zitadel/proto/zitadel/feature/v2/feature_pb';
import { MessageInitShape } from '@bufbuild/protobuf';
enum ToggleState {
export enum ToggleState {
ENABLED = 'ENABLED',
DISABLED = 'DISABLED',
INHERITED = 'INHERITED',
}
// TODO: to add a new feature, add the key here and in the FEATURE_KEYS array
const FEATURE_KEYS: ToggleStateKeys[] = [
'actions',
'consoleUseV2UserApi',
'debugOidcParentError',
'disableUserTokenEvent',
'enableBackChannelLogout',
// 'improvedPerformance',
'loginDefaultOrg',
// 'loginV2',
'oidcLegacyIntrospection',
'oidcSingleV1SessionTermination',
'oidcTokenExchange',
'oidcTriggerIntrospectionProjections',
'permissionCheckV2',
'userSchema',
// 'webKey',
];
type FeatureState = { source: Source; state: ToggleState };
type ToggleStates = {
loginDefaultOrg?: FeatureState;
oidcTriggerIntrospectionProjections?: FeatureState;
oidcLegacyIntrospection?: FeatureState;
userSchema?: FeatureState;
oidcTokenExchange?: FeatureState;
actions?: FeatureState;
oidcSingleV1SessionTermination?: FeatureState;
export type ToggleStateKeys = Exclude<keyof GetInstanceFeaturesResponse, 'details' | '$typeName' | '$unknown'>;
export type ToggleStates = {
[key in ToggleStateKeys]: FeatureState;
};
@Component({
@@ -51,6 +67,7 @@ type ToggleStates = {
InfoSectionModule,
MatTooltipModule,
HasRoleModule,
FeatureToggleComponent,
],
standalone: true,
selector: 'cnsl-features',
@@ -58,14 +75,14 @@ type ToggleStates = {
styleUrls: ['./features.component.scss'],
})
export class FeaturesComponent {
protected featureData: GetInstanceFeaturesResponse.AsObject | undefined;
protected featureData: GetInstanceFeaturesResponse | undefined;
protected toggleStates: ToggleStates | undefined;
protected Source: any = Source;
protected ToggleState: any = ToggleState;
constructor(
private featureService: FeatureService,
private featureService: NewFeatureService,
private breadcrumbService: BreadcrumbService,
private toast: ToastService,
) {
@@ -78,140 +95,57 @@ export class FeaturesComponent {
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
this.getFeatures(true);
this.getFeatures();
}
public validateAndSave() {
this.featureService.resetInstanceFeatures().then(() => {
const req = new SetInstanceFeaturesRequest();
let changed = false;
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,
};
console.log(this.toggleStates);
this.featureService
.setInstanceFeatures(req)
.then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
if (this.toggleStates?.loginDefaultOrg?.state !== ToggleState.INHERITED) {
req.setLoginDefaultOrg(this.toggleStates?.loginDefaultOrg?.state === ToggleState.ENABLED);
changed = true;
}
if (this.toggleStates?.oidcTriggerIntrospectionProjections?.state !== ToggleState.INHERITED) {
req.setOidcTriggerIntrospectionProjections(
this.toggleStates?.oidcTriggerIntrospectionProjections?.state === ToggleState.ENABLED,
);
changed = true;
}
if (this.toggleStates?.oidcLegacyIntrospection?.state !== ToggleState.INHERITED) {
req.setOidcLegacyIntrospection(this.toggleStates?.oidcLegacyIntrospection?.state === ToggleState.ENABLED);
changed = true;
}
if (this.toggleStates?.userSchema?.state !== ToggleState.INHERITED) {
req.setUserSchema(this.toggleStates?.userSchema?.state === ToggleState.ENABLED);
changed = true;
}
if (this.toggleStates?.oidcTokenExchange?.state !== ToggleState.INHERITED) {
req.setOidcTokenExchange(this.toggleStates?.oidcTokenExchange?.state === ToggleState.ENABLED);
changed = true;
}
if (this.toggleStates?.actions?.state !== ToggleState.INHERITED) {
req.setActions(this.toggleStates?.actions?.state === ToggleState.ENABLED);
changed = true;
}
if (this.toggleStates?.oidcSingleV1SessionTermination?.state !== ToggleState.INHERITED) {
req.setOidcSingleV1SessionTermination(
this.toggleStates?.oidcSingleV1SessionTermination?.state === ToggleState.ENABLED,
);
changed = true;
}
if (changed) {
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 getFeatures(inheritance: boolean) {
this.featureService.getInstanceFeatures(inheritance).then((instanceFeaturesResponse) => {
this.featureData = instanceFeaturesResponse.toObject();
console.log(this.featureData);
private createToggleStates(featureData: GetInstanceFeaturesResponse): ToggleStates {
const toggleStates: Partial<ToggleStates> = {};
this.toggleStates = {
loginDefaultOrg: {
source: this.featureData.loginDefaultOrg?.source || Source.SOURCE_SYSTEM,
state:
this.featureData.loginDefaultOrg?.source === Source.SOURCE_SYSTEM ||
this.featureData.loginDefaultOrg?.source === Source.SOURCE_UNSPECIFIED
? ToggleState.INHERITED
: !!this.featureData.loginDefaultOrg?.enabled
? ToggleState.ENABLED
: ToggleState.DISABLED,
},
oidcTriggerIntrospectionProjections: {
source: this.featureData.oidcTriggerIntrospectionProjections?.source || Source.SOURCE_SYSTEM,
state:
this.featureData.oidcTriggerIntrospectionProjections?.source === Source.SOURCE_SYSTEM ||
this.featureData.oidcTriggerIntrospectionProjections?.source === Source.SOURCE_UNSPECIFIED
? ToggleState.INHERITED
: !!this.featureData.oidcTriggerIntrospectionProjections?.enabled
? ToggleState.ENABLED
: ToggleState.DISABLED,
},
oidcLegacyIntrospection: {
source: this.featureData.oidcLegacyIntrospection?.source || Source.SOURCE_SYSTEM,
state:
this.featureData.oidcLegacyIntrospection?.source === Source.SOURCE_SYSTEM ||
this.featureData.oidcLegacyIntrospection?.source === Source.SOURCE_UNSPECIFIED
? ToggleState.INHERITED
: !!this.featureData.oidcLegacyIntrospection?.enabled
? ToggleState.ENABLED
: ToggleState.DISABLED,
},
userSchema: {
source: this.featureData.userSchema?.source || Source.SOURCE_SYSTEM,
state:
this.featureData.userSchema?.source === Source.SOURCE_SYSTEM ||
this.featureData.userSchema?.source === Source.SOURCE_UNSPECIFIED
? ToggleState.INHERITED
: !!this.featureData.userSchema?.enabled
? ToggleState.ENABLED
: ToggleState.DISABLED,
},
oidcTokenExchange: {
source: this.featureData.oidcTokenExchange?.source || Source.SOURCE_SYSTEM,
state:
this.featureData.oidcTokenExchange?.source === Source.SOURCE_SYSTEM ||
this.featureData.oidcTokenExchange?.source === Source.SOURCE_UNSPECIFIED
? ToggleState.INHERITED
: !!this.featureData.oidcTokenExchange?.enabled
? ToggleState.ENABLED
: ToggleState.DISABLED,
},
actions: {
source: Source.SOURCE_SYSTEM,
state:
this.featureData.actions?.source === Source.SOURCE_SYSTEM ||
this.featureData.actions?.source === Source.SOURCE_UNSPECIFIED
? ToggleState.INHERITED
: !!this.featureData.actions?.enabled
? ToggleState.ENABLED
: ToggleState.DISABLED,
},
oidcSingleV1SessionTermination: {
source: this.featureData.oidcSingleV1SessionTermination?.source || Source.SOURCE_SYSTEM,
state:
this.featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_SYSTEM ||
this.featureData.oidcSingleV1SessionTermination?.source === Source.SOURCE_UNSPECIFIED
? ToggleState.INHERITED
: !!this.featureData.oidcSingleV1SessionTermination?.enabled
? ToggleState.ENABLED
: ToggleState.DISABLED,
},
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;
}
public resetSettings(): void {
@@ -220,11 +154,15 @@ export class FeaturesComponent {
.then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
setTimeout(() => {
this.getFeatures(true);
this.getFeatures();
}, 1000);
})
.catch((error) => {
this.toast.showError(error);
});
}
public get toggleStateKeys() {
return Object.keys(this.toggleStates ?? {});
}
}
@@ -1,57 +0,0 @@
import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import {
GetOrganizationFeaturesRequest,
GetOrganizationFeaturesResponse,
} from '../proto/generated/zitadel/feature/v2beta/organization_pb';
import { GetUserFeaturesRequest, GetUserFeaturesResponse } from '../proto/generated/zitadel/feature/v2beta/user_pb';
import { GetSystemFeaturesRequest, GetSystemFeaturesResponse } from '../proto/generated/zitadel/feature/v2beta/system_pb';
import {
GetInstanceFeaturesRequest,
GetInstanceFeaturesResponse,
ResetInstanceFeaturesRequest,
SetInstanceFeaturesRequest,
SetInstanceFeaturesResponse,
} from '../proto/generated/zitadel/feature/v2/instance_pb';
@Injectable({
providedIn: 'root',
})
export class FeatureService {
constructor(private readonly grpcService: GrpcService) {}
public getInstanceFeatures(inheritance: boolean): Promise<GetInstanceFeaturesResponse> {
const req = new GetInstanceFeaturesRequest();
req.setInheritance(inheritance);
return this.grpcService.feature.getInstanceFeatures(req, null).then((resp) => resp);
}
public setInstanceFeatures(req: SetInstanceFeaturesRequest): Promise<SetInstanceFeaturesResponse> {
return this.grpcService.feature.setInstanceFeatures(req, null);
}
public resetInstanceFeatures(): Promise<SetInstanceFeaturesResponse> {
const req = new ResetInstanceFeaturesRequest();
return this.grpcService.feature.resetInstanceFeatures(req, null);
}
public getOrganizationFeatures(orgId: string, inheritance: boolean): Promise<GetOrganizationFeaturesResponse> {
const req = new GetOrganizationFeaturesRequest();
req.setOrganizationId(orgId);
req.setInheritance(inheritance);
return this.grpcService.feature.getOrganizationFeatures(req, null).then((resp) => resp);
}
public getSystemFeatures(): Promise<GetSystemFeaturesResponse> {
const req = new GetSystemFeaturesRequest();
return this.grpcService.feature.getSystemFeatures(req, null).then((resp) => resp);
}
public getUserFeatures(userId: string, inheritance: boolean): Promise<GetUserFeaturesResponse> {
const req = new GetUserFeaturesRequest();
req.setInheritance(inheritance);
req.setUserId(userId);
return this.grpcService.feature.getUserFeatures(req, null).then((resp) => resp);
}
}
-8
View File
@@ -22,7 +22,6 @@ import { createFeatureServiceClient, createUserServiceClient } from '@zitadel/cl
//@ts-ignore
import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
import { createGrpcWebTransport } from '@connectrpc/connect-web';
import { FeatureServiceClient } from '../proto/generated/zitadel/feature/v2/Feature_serviceServiceClientPb';
@Injectable({
providedIn: 'root',
@@ -31,7 +30,6 @@ export class GrpcService {
public auth!: AuthServiceClient;
public mgmt!: ManagementServiceClient;
public admin!: AdminServiceClient;
public feature!: FeatureServiceClient;
public user!: UserServiceClient;
public userNew!: ReturnType<typeof createUserServiceClient>;
public mgmtNew!: ReturnType<typeof createManagementServiceClient>;
@@ -88,12 +86,6 @@ export class GrpcService {
// @ts-ignore
interceptors,
);
this.feature = new FeatureServiceClient(
env.api,
null,
// @ts-ignore
interceptors,
);
this.user = new UserServiceClient(
env.api,
null,
@@ -1,6 +1,13 @@
import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import { GetInstanceFeaturesResponse } from '@zitadel/proto/zitadel/feature/v2/instance_pb';
import {
GetInstanceFeaturesResponse,
ResetInstanceFeaturesResponse,
SetInstanceFeaturesRequest,
SetInstanceFeaturesRequestSchema,
SetInstanceFeaturesResponse,
} from '@zitadel/proto/zitadel/feature/v2/instance_pb';
import { MessageInitShape } from '@bufbuild/protobuf';
@Injectable({
providedIn: 'root',
@@ -11,4 +18,14 @@ export class NewFeatureService {
public getInstanceFeatures(): Promise<GetInstanceFeaturesResponse> {
return this.grpcService.featureNew.getInstanceFeatures({});
}
public setInstanceFeatures(
req: MessageInitShape<typeof SetInstanceFeaturesRequestSchema>,
): Promise<SetInstanceFeaturesResponse> {
return this.grpcService.featureNew.setInstanceFeatures(req);
}
public resetInstanceFeatures(): Promise<ResetInstanceFeaturesResponse> {
return this.grpcService.featureNew.resetInstanceFeatures({});
}
}