feat(console): integrate app (#7417)

* docs, frameworks view

* project select, integrate app page

* fix search project autocomplete

* framework autocomplete

* framwork select component, integrate, mapping to oidc config

* param

* fix route handler

* setname projectid context

* app-create page without context

* show description of app type, info section

* redirects section

* updatevalue observable

* fix redirect uris section

* i18n

* setup config

* backbutton behavior, cleanup

* cleanup

* lint

* allow other framework jump off

* dev mode warning

* navigate to project

* rm import

* i18n, guide link

* edit name dialog

* show warning for duplicate name
This commit is contained in:
Max Peintner
2024-02-28 16:52:21 +00:00
committed by GitHub
parent f4c72cbe14
commit 0fcdfe460c
64 changed files with 2381 additions and 176 deletions
@@ -0,0 +1,35 @@
<form>
<cnsl-form-field class="full-width">
<cnsl-label>{{ 'QUICKSTART.FRAMEWORK' | translate }}</cnsl-label>
<input cnslInput type="text" placeholder="" #nameInput [formControl]="myControl" [matAutocomplete]="auto" />
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
<mat-option *ngIf="isLoading()" class="is-loading">
<mat-spinner diameter="30"></mat-spinner>
</mat-option>
<mat-option *ngFor="let framework of filteredOptions | async" [value]="framework.id">
<div class="framework-option">
<div class="framework-option-column">
<div class="img-wrapper">
<img class="dark-only" *ngIf="framework.imgSrcDark" [src]="framework.imgSrcDark" />
<img class="light-only" *ngIf="framework.imgSrcLight" [src]="framework.imgSrcLight" />
</div>
<span class="fill-space"></span>
<span>{{ framework.title }}</span>
</div>
</div>
</mat-option>
<mat-option *ngIf="withCustom" [value]="'other'">
<div class="framework-option">
<div class="framework-option-column">
<div class="img-wrapper"></div>
<span class="fill-space"></span>
<span>{{ 'QUICKSTART.FRAMEWORK_OTHER' | translate }}</span>
</div>
</div>
</mat-option>
</mat-autocomplete>
</cnsl-form-field>
</form>
@@ -0,0 +1,69 @@
@mixin framework-autocomplete-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: map-get($primary, 500);
$warn-color: map-get($warn, 500);
$accent-color: map-get($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
.full-width {
width: 100%;
}
input {
max-width: 500px;
}
.framework-option {
display: flex;
align-items: center;
.framework-option-column {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
width: 100%;
span {
line-height: normal;
}
.fill-space {
flex: 1;
}
.img-wrapper {
width: 50px;
margin-right: 1rem;
img {
width: 100%;
height: 100%;
max-width: 30px;
max-height: 30px;
object-fit: contain;
object-position: center;
}
}
.dark-only {
display: if($is-dark-theme, block, none);
}
.light-only {
display: if($is-dark-theme, none, block);
}
}
}
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FrameworkAutocompleteComponent } from './framework-autocomplete.component';
describe('FrameworkAutocompleteComponent', () => {
let component: FrameworkAutocompleteComponent;
let fixture: ComponentFixture<FrameworkAutocompleteComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FrameworkAutocompleteComponent],
});
fixture = TestBed.createComponent(FrameworkAutocompleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,63 @@
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 { MatSelectModule } from '@angular/material/select';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { InputModule } from 'src/app/modules/input/input.module';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Observable, map, of, startWith, switchMap, tap } from 'rxjs';
import { Framework } from '../quickstart/quickstart.component';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'cnsl-framework-autocomplete',
templateUrl: './framework-autocomplete.component.html',
styleUrls: ['./framework-autocomplete.component.scss'],
imports: [
TranslateModule,
RouterModule,
MatSelectModule,
MatAutocompleteModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
CommonModule,
MatButtonModule,
InputModule,
],
})
export class FrameworkAutocompleteComponent implements OnInit {
public isLoading = signal(false);
@Input() public frameworkId?: string;
@Input() public frameworks: Framework[] = [];
@Input() public withCustom: boolean = false;
public myControl: FormControl = new FormControl();
@Output() public selectionChanged: EventEmitter<string> = new EventEmitter();
public filteredOptions: Observable<Framework[]> = of([]);
constructor() {}
public ngOnInit() {
this.filteredOptions = this.myControl.valueChanges.pipe(
startWith(''),
map((value) => {
return this._filter(value || '');
}),
);
}
private _filter(value: string): Framework[] {
const filterValue = value.toLowerCase();
return this.frameworks
.filter((option) => option.id)
.filter((option) => option.title.toLowerCase().includes(filterValue));
}
public selected(event: MatAutocompleteSelectedEvent): void {
this.selectionChanged.emit(event.option.value);
}
}
@@ -0,0 +1,17 @@
<h2 mat-dialog-title>{{ 'QUICKSTART.DIALOG.CHANGE.TITLE' | translate }}</h2>
<mat-dialog-content>
{{ 'QUICKSTART.DIALOG.CHANGE.DESCRIPTION' | translate }}
<div class="framework-change-block">
<cnsl-framework-autocomplete
[frameworkId]="data.framework.id"
[frameworks]="data.frameworks"
(selectionChanged)="findFramework($event)"
></cnsl-framework-autocomplete>
</div>
</mat-dialog-content>
<div>
<mat-dialog-actions class="actions">
<button mat-stroked-button mat-dialog-close>{{ 'ACTIONS.CANCEL' | translate }}</button>
<button color="primary" mat-raised-button (click)="close()" cdkFocusInitial>{{ 'ACTIONS.CHANGE' | translate }}</button>
</mat-dialog-actions>
</div>
@@ -0,0 +1,10 @@
.framework-change-block {
display: flex;
flex-direction: column;
align-items: stretch;
}
.actions {
display: flex;
justify-content: space-between;
}
@@ -0,0 +1,41 @@
import { Component, Inject, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogModule,
MatDialogRef,
MatDialogTitle,
} from '@angular/material/dialog';
import { FrameworkAutocompleteComponent } from '../framework-autocomplete/framework-autocomplete.component';
import { Framework } from '../quickstart/quickstart.component';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'cnsl-framework-change-dialog',
templateUrl: './framework-change-dialog.component.html',
styleUrls: ['./framework-change-dialog.component.scss'],
standalone: true,
imports: [MatButtonModule, MatDialogModule, TranslateModule, FrameworkAutocompleteComponent],
})
export class FrameworkChangeDialogComponent {
public framework = signal<Framework | undefined>(undefined);
constructor(
public dialogRef: MatDialogRef<FrameworkChangeDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) {
this.framework.set(data.framework);
}
public findFramework(id: string) {
const temp = this.data.frameworks.find((f: Framework) => f.id === id);
this.framework.set(temp);
}
public close() {
this.dialogRef.close(this.framework());
}
}
@@ -0,0 +1,14 @@
<div class="framework-change-wrapper">
<div class="framework-card-wrapper">
<div class="framework-card card" *ngIf="framework | async as framework">
<div [routerLink]="['/app-create']" [queryParams]="{ id: framework.docsLink }" class="">
<img class="dark-only" *ngIf="framework.imgSrcDark" [src]="framework.imgSrcDark" />
<img class="light-only" *ngIf="framework.imgSrcLight" [src]="framework.imgSrcLight" />
</div>
<span>{{ framework.title }}</span>
</div>
<button (click)="openDialog()" mat-stroked-button>
{{ 'ACTIONS.CHANGE' | translate }}
</button>
</div>
</div>
@@ -0,0 +1,82 @@
@mixin framework-change-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: map-get($primary, 500);
$warn-color: map-get($warn, 500);
$accent-color: map-get($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
.framework-change-wrapper {
.framework-card-wrapper {
display: flex;
align-items: center;
gap: 1rem;
.framework-card {
position: relative;
flex-shrink: 0;
text-decoration: none;
border-radius: 0.5rem;
box-sizing: border-box;
transition: all 0.1s ease-in;
display: flex;
flex-direction: row;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
// background-color: if($is-dark-theme, map-get($background, state), #e4e7e4);
// box-shadow: 0 0 3px #0000001a;
border: 1px solid rgba(#8795a1, 0.2);
padding: 0 0.5rem;
img {
width: 100%;
height: 100%;
max-width: 40px;
max-height: 40px;
object-fit: contain;
object-position: center;
}
.dark-only {
display: if($is-dark-theme, block, none);
}
.light-only {
display: if($is-dark-theme, none, block);
}
span {
margin: 0.5rem;
text-align: center;
color: map-get($foreground, text);
}
.action-row {
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 14px;
margin-bottom: 0.5rem;
color: map-get($primary, 400);
.icon {
margin-left: 0rem;
}
}
}
}
}
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FrameworkChangeComponent } from './framework-change.component';
describe('FrameworkChangeComponent', () => {
let component: FrameworkChangeComponent;
let fixture: ComponentFixture<FrameworkChangeComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FrameworkChangeComponent],
});
fixture = TestBed.createComponent(FrameworkChangeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,88 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, OnDestroy, OnInit, Output, effect, signal } from '@angular/core';
import { ActivatedRoute, Params, RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import frameworkDefinition from '../../../../../docs/frameworks.json';
import { MatButtonModule } from '@angular/material/button';
import { listFrameworks, hasFramework, getFramework } from '@netlify/framework-info';
import { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames';
import { FrameworkAutocompleteComponent } from '../framework-autocomplete/framework-autocomplete.component';
import { Framework } from '../quickstart/quickstart.component';
import { BehaviorSubject, Subject, takeUntil } from 'rxjs';
import {
MatDialog,
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogModule,
MatDialogRef,
MatDialogTitle,
} from '@angular/material/dialog';
import { FrameworkChangeDialogComponent } from './framework-change-dialog.component';
@Component({
standalone: true,
selector: 'cnsl-framework-change',
templateUrl: './framework-change.component.html',
styleUrls: ['./framework-change.component.scss'],
imports: [TranslateModule, RouterModule, CommonModule, MatButtonModule, FrameworkAutocompleteComponent],
})
export class FrameworkChangeComponent implements OnInit, OnDestroy {
private destroy$: Subject<void> = new Subject();
public framework: BehaviorSubject<Framework | undefined> = new BehaviorSubject<Framework | undefined>(undefined);
public showFrameworkAutocomplete = signal<boolean>(false);
@Output() public frameworkChanged: EventEmitter<Framework> = new EventEmitter();
public frameworks: Framework[] = frameworkDefinition.map((f) => {
return {
...f,
fragment: '',
imgSrcDark: `assets${f.imgSrcDark}`,
imgSrcLight: `assets${f.imgSrcLight ? f.imgSrcLight : f.imgSrcDark}`,
};
});
constructor(
private activatedRoute: ActivatedRoute,
private dialog: MatDialog,
) {
this.framework.pipe(takeUntil(this.destroy$)).subscribe((value) => {
this.frameworkChanged.emit(value);
});
}
public ngOnInit() {
this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params: Params) => {
const { framework } = params;
if (framework) {
this.findFramework(framework);
}
});
}
public ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
public findFramework(id: string) {
const temp = this.frameworks.find((f) => f.id === id);
this.framework.next(temp);
this.frameworkChanged.emit(temp);
}
public openDialog(): void {
const ref = this.dialog.open(FrameworkChangeDialogComponent, {
width: '400px',
data: {
framework: this.framework.value,
frameworks: this.frameworks,
},
});
ref.afterClosed().subscribe((resp) => {
if (resp) {
this.framework.next(resp);
}
});
}
}
@@ -0,0 +1,53 @@
<div class="configuration-wrapper">
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.NAME' | translate }}
</span>
<span class="right name">
<span>{{ name }}</span>
<button (click)="changeName.emit()" mat-icon-button><i class="las la-pen"></i></button>
</span>
</div>
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.TYPE' | translate }}
</span>
<span class="right">
{{ 'APP.OIDC.APPTYPE.' + configuration.appType | translate }}
</span>
</div>
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.GRANT' | translate }}
</span>
<span class="right" *ngIf="configuration.grantTypesList && configuration.grantTypesList.length > 0">
[<span *ngFor="let element of configuration.grantTypesList ?? []; index as i">
{{ 'APP.OIDC.GRANT.' + element | translate }}
{{ i < configuration.grantTypesList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.OIDC.RESPONSETYPE' | translate }}
</span>
<span class="right" *ngIf="configuration.responseTypesList && configuration.responseTypesList.length > 0">
[<span *ngFor="let element of configuration.responseTypesList ?? []; index as i">
{{ 'APP.OIDC.RESPONSE.' + element | translate }}
{{ i < configuration.responseTypesList.length - 1 ? ', ' : '' }} </span
>]
</span>
</div>
<div class="row">
<span class="left cnsl-secondary-text">
{{ 'APP.AUTHMETHOD' | translate }}
</span>
<span class="right">
<span>
{{ 'APP.OIDC.AUTHMETHOD.' + configuration.authMethodType | translate }}
</span>
</span>
</div>
</div>
@@ -0,0 +1,23 @@
.configuration-wrapper {
.row {
display: flex;
justify-content: space-between;
align-items: center;
.left,
.right {
margin-bottom: 0.5rem;
font-size: 14px;
}
.name {
display: flex;
align-items: center;
button {
margin-right: -0.5rem;
margin-left: 0.25rem;
}
}
}
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QuickstartComponent } from './quickstart.component';
describe('QuickstartComponent', () => {
let component: QuickstartComponent;
let fixture: ComponentFixture<QuickstartComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [QuickstartComponent],
});
fixture = TestBed.createComponent(QuickstartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,35 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import frameworkDefinition from '../../../../../docs/frameworks.json';
import { MatButtonModule } from '@angular/material/button';
import { listFrameworks, hasFramework, getFramework } from '@netlify/framework-info';
import { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames';
import { AddOIDCAppRequest } from 'src/app/proto/generated/zitadel/management_pb';
export type FrameworkDefinition = {
id?: FrameworkName | string;
title: string;
imgSrcDark: string;
imgSrcLight?: string;
docsLink: string;
external?: boolean;
};
export type Framework = FrameworkDefinition & {
fragment: string;
};
@Component({
standalone: true,
selector: 'cnsl-oidc-app-configuration',
templateUrl: './oidc-configuration.component.html',
styleUrls: ['./oidc-configuration.component.scss'],
imports: [TranslateModule, RouterModule, CommonModule, MatButtonModule],
})
export class OIDCConfigurationComponent {
@Input() public name?: string;
@Input() public configuration: AddOIDCAppRequest.AsObject = new AddOIDCAppRequest().toObject();
@Output() public changeName: EventEmitter<string> = new EventEmitter();
}
@@ -0,0 +1,23 @@
<div class="quickstart-header">
<div class="quickstart-left">
<h2>{{ 'QUICKSTART.TITLE' | translate }}</h2>
<p class="description">{{ 'QUICKSTART.DESCRIPTION' | translate }}</p>
<div class="btn-wrapper">
<a mat-raised-button color="primary" [routerLink]="['/projects', 'app-create']">{{
'QUICKSTART.BTN_START' | translate
}}</a>
<a mat-stroked-button color="primary" href="https://zitadel.com/docs/sdk-examples/introduction" target="_blank">{{
'QUICKSTART.BTN_LEARNMORE' | translate
}}</a>
</div>
</div>
<div class="quickstart-card-wrapper">
<ng-container *ngFor="let framework of frameworks.slice(0, 18)">
<a [routerLink]="['/projects', 'app-create']" [queryParams]="{ framework: framework.id }" class="quickstart-card card">
<img class="dark-only" *ngIf="framework.imgSrcDark" [src]="framework.imgSrcDark" alt="{{ framework.title }}" />
<img class="light-only" *ngIf="framework.imgSrcLight" [src]="framework.imgSrcLight" alt="{{ framework.title }}" />
</a>
</ng-container>
</div>
</div>
@@ -0,0 +1,121 @@
@mixin quickstart-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: map-get($primary, 500);
$warn-color: map-get($warn, 500);
$accent-color: map-get($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
.quickstart-header {
display: flex;
flex-direction: row;
margin: 0 -2rem;
padding: 2rem;
margin-bottom: 2rem;
gap: 5rem;
justify-content: space-between;
background-color: map-get($background, metadata-section);
.quickstart-left {
display: flex;
flex-direction: column;
max-width: 400px;
.btn-wrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
margin: 1rem 0;
}
}
.quickstart-card-wrapper {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-column-gap: 1rem;
grid-row-gap: 1rem;
grid-auto-columns: 0;
overflow-x: hidden;
box-sizing: border-box;
max-width: 600px;
margin-left: auto;
.quickstart-card {
position: relative;
flex-shrink: 0;
text-decoration: none;
cursor: pointer;
border-radius: 0.5rem;
box-sizing: border-box;
transition: all 0.1s ease-in;
display: flex;
flex-direction: column;
height: 60px;
width: 60px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 3px #0000001a;
border: 1px solid rgba(#8795a1, 0.2);
color: var(--success);
opacity: 0.8;
&:hover {
border: 2px solid var(--success);
opacity: 1;
}
img {
width: 100%;
height: 100%;
max-width: 40px;
max-height: 40px;
object-fit: contain;
object-position: center;
}
.dark-only {
display: if($is-dark-theme, block, none);
}
.light-only {
display: if($is-dark-theme, none, block);
}
span {
margin: 0.5rem;
text-align: center;
color: map-get($foreground, text);
}
.action-row {
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 14px;
margin-bottom: 0.5rem;
color: map-get($primary, 400);
.icon {
margin-left: 0rem;
}
}
&:hover {
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
}
}
}
}
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QuickstartComponent } from './quickstart.component';
describe('QuickstartComponent', () => {
let component: QuickstartComponent;
let fixture: ComponentFixture<QuickstartComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [QuickstartComponent],
});
fixture = TestBed.createComponent(QuickstartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,42 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import frameworkDefinition from '../../../../../docs/frameworks.json';
import { MatButtonModule } from '@angular/material/button';
import { listFrameworks, hasFramework, getFramework } from '@netlify/framework-info';
import { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames';
import { OIDC_CONFIGURATIONS } from 'src/app/utils/framework';
export type FrameworkDefinition = {
id?: FrameworkName | string;
title: string;
description?: string;
imgSrcDark: string;
imgSrcLight?: string;
docsLink: string;
external?: boolean;
};
export type Framework = FrameworkDefinition & {
fragment: string;
};
@Component({
standalone: true,
selector: 'cnsl-quickstart',
templateUrl: './quickstart.component.html',
styleUrls: ['./quickstart.component.scss'],
imports: [TranslateModule, RouterModule, CommonModule, MatButtonModule],
})
export class QuickstartComponent {
public frameworks: FrameworkDefinition[] = frameworkDefinition
.filter((f) => f.id && OIDC_CONFIGURATIONS[f.id])
.map((f) => {
return {
...f,
imgSrcDark: `assets${f.imgSrcDark}`,
imgSrcLight: `assets${f.imgSrcLight ? f.imgSrcLight : f.imgSrcDark}`,
};
});
}
@@ -1,10 +1,17 @@
<div
class="info-section-row"
[ngClass]="{ info: type === 'INFO', warn: type === 'WARN', alert: type === 'ALERT', fit: fitWidth }"
[ngClass]="{
info: type === 'INFO',
warn: type === 'WARN',
alert: type === 'ALERT',
success: type === 'SUCCESS',
fit: fitWidth
}"
>
<i *ngIf="type === 'INFO'" class="icon las la-info"></i>
<i *ngIf="type === 'WARN'" class="icon las la-exclamation"></i>
<i *ngIf="type === 'ALERT'" class="icon las la-exclamation"></i>
<i *ngIf="type === 'SUCCESS'" class="icon las la-check-circle"></i>
<div class="info-section-content">
<ng-content></ng-content>
@@ -37,13 +37,19 @@ export class SearchProjectAutocompleteComponent implements OnInit, OnDestroy {
@Output() public selectionChanged: EventEmitter<{
project: Project.AsObject | GrantedProject.AsObject;
type: ProjectType;
name: string;
}> = new EventEmitter();
@Output() public valueChanged: EventEmitter<string> = new EventEmitter();
private unsubscribed$: Subject<void> = new Subject();
constructor(private mgmtService: ManagementService) {
this.myControl.valueChanges
.pipe(
takeUntil(this.unsubscribed$),
tap((value) => {
const name = typeof value === 'string' ? value : value.name ? value.name : '';
this.valueChanged.emit(name);
}),
debounceTime(200),
tap(() => (this.isLoading = true)),
switchMap((value) => {
@@ -124,44 +130,6 @@ export class SearchProjectAutocompleteComponent implements OnInit, OnDestroy {
return project && project.projectName ? `${project.projectName}` : project && project.name ? `${project.name}` : '';
}
public add(event: MatChipInputEvent): void {
if (!this.matAutocomplete.isOpen) {
const input = event.chipInput?.inputElement;
const value = event.value;
if ((value || '').trim()) {
const index = this.filteredProjects.findIndex((project) => {
if (project?.projectName) {
return project.projectName === value;
} else if (project?.name) {
return project.name === value;
} else {
return false;
}
});
if (index > -1) {
if (this.projects && this.projects.length > 0) {
this.projects.push(this.filteredProjects[index]);
} else {
this.projects = [this.filteredProjects[index]];
}
}
}
if (input) {
input.value = '';
}
}
}
public remove(project: GrantedProject.AsObject): void {
const index = this.projects.indexOf(project);
if (index >= 0) {
this.projects.splice(index, 1);
}
}
public selected(event: MatAutocompleteSelectedEvent): void {
const p: Project.AsObject | GrantedProject.AsObject = event.option.value;
const type = (p as Project.AsObject).id
@@ -170,8 +138,17 @@ export class SearchProjectAutocompleteComponent implements OnInit, OnDestroy {
? ProjectType.PROJECTTYPE_GRANTED
: ProjectType.PROJECTTYPE_OWNED;
const name = (p as Project.AsObject).name
? (p as Project.AsObject).name
: (p as GrantedProject.AsObject).projectName
? (p as GrantedProject.AsObject).projectName
: '';
console.log(name);
this.selectionChanged.emit({
project: p,
name,
type: type,
});
}
@@ -1,25 +1,53 @@
<cnsl-create-layout title="{{ 'APP.PAGES.CREATE' | translate }}" (closed)="close()">
<div class="app-create-main-content">
<h1>{{ 'APP.PAGES.CREATE_SELECT_PROJECT' | translate }}</h1>
<cnsl-framework-change *ngIf="initialParam()" (frameworkChanged)="framework.set($event)"></cnsl-framework-change>
<div class="content-wrapper" [ngClass]="{ reverse: initialParam() }">
<div>
<h1>{{ 'APP.PAGES.CREATE_SELECT_PROJECT' | translate }}</h1>
<cnsl-search-project-autocomplete
class="block"
[autocompleteType]="ProjectAutocompleteType.PROJECT_OWNED"
(selectionChanged)="selectProject($any($event.project))"
<cnsl-search-project-autocomplete
class="block"
[autocompleteType]="ProjectAutocompleteType.PROJECT_OWNED"
(selectionChanged)="selectProject($any($event))"
(valueChanged)="projectName = $any($event)"
>
</cnsl-search-project-autocomplete>
<p>{{ 'APP.PAGES.CREATE_NEW_PROJECT' | translate }}</p>
</div>
<div *ngIf="!initialParam()">
<h1>{{ 'QUICKSTART.SELECT_FRAMEWORK' | translate }}</h1>
<cnsl-framework-autocomplete
*ngIf="frameworks"
[frameworkId]="framework()?.id"
[frameworks]="frameworks"
[withCustom]="true"
(selectionChanged)="findFramework($event)"
></cnsl-framework-autocomplete>
</div>
</div>
<cnsl-info-section *ngIf="error()" [type]="InfoSectionType.WARN"
><span class="error-msg">{{ error() }}</span></cnsl-info-section
>
</cnsl-search-project-autocomplete>
<div [innerHtml]="'APP.PAGES.CREATE_NEW_PROJECT' | translate: { url: '/projects/create' }"></div>
<div class="app-create-btn-container">
<button
color="primary"
mat-raised-button
class="continue-button"
[disabled]="!projectId"
(click)="goToAppCreatePage()"
[disabled]="(!project && !projectName) || !(framework() || customFramework())"
(click)="project && project.name === projectName ? goToAppIntegratePage() : createProjectAndContinue()"
>
{{ 'ACTIONS.CONTINUE' | translate }}
{{
!project && !projectName
? ('ACTIONS.CONTINUE' | translate)
: project && project.name === projectName
? ('ACTIONS.CONTINUEWITH' | translate: { value: project.name })
: ('QUICKSTART.CREATEPROJECTFORAPP' | translate: { value: projectName })
}}
</button>
</div>
</div>
@@ -5,6 +5,20 @@ h1 {
.app-create-main-content {
max-width: 35rem;
.content-wrapper {
display: flex;
flex-direction: column;
&.reverse {
flex-direction: column-reverse;
}
}
.error-msg {
margin-top: 0.25rem;
display: block;
}
.app-create-btn-container {
display: flex;
align-items: center;
@@ -17,9 +31,4 @@ h1 {
padding: 0 4rem;
}
}
.complexity-view {
width: 100%;
margin: 0 0.5rem;
}
}
@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OrgCreateComponent } from './org-create.component';
import { AppCreateComponent } from './app-create.component';
describe('OrgCreateComponent', () => {
let component: OrgCreateComponent;
let fixture: ComponentFixture<OrgCreateComponent>;
describe('AppCreateComponent', () => {
let component: AppCreateComponent;
let fixture: ComponentFixture<AppCreateComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OrgCreateComponent],
declarations: [AppCreateComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OrgCreateComponent);
fixture = TestBed.createComponent(AppCreateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -1,40 +1,144 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Component, OnDestroy, signal } from '@angular/core';
import { ActivatedRoute, Navigation, Params, Router } from '@angular/router';
import { Subject, takeUntil } from 'rxjs';
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
import { ProjectType } from 'src/app/modules/project-members/project-members-datasource';
import { ProjectAutocompleteType } from 'src/app/modules/search-project-autocomplete/search-project-autocomplete.component';
import { Project } from 'src/app/proto/generated/zitadel/project_pb';
import { AddProjectRequest, AddProjectResponse } from 'src/app/proto/generated/zitadel/management_pb';
import { GrantedProject, Project } from 'src/app/proto/generated/zitadel/project_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { Framework } from 'src/app/components/quickstart/quickstart.component';
import frameworkDefinition from '../../../../../docs/frameworks.json';
import { NavigationService } from 'src/app/services/navigation.service';
import { Location } from '@angular/common';
@Component({
selector: 'cnsl-app-create',
templateUrl: './app-create.component.html',
styleUrls: ['./app-create.component.scss'],
})
export class AppCreateComponent {
public projectId: string = '';
export class AppCreateComponent implements OnDestroy {
public InfoSectionType: any = InfoSectionType;
public project?: {
project: Project.AsObject | GrantedProject.AsObject;
type: ProjectType;
name: string;
} = undefined;
public ProjectAutocompleteType: any = ProjectAutocompleteType;
public projectName: string = '';
public error = signal('');
public framework = signal<Framework | undefined>(undefined);
public customFramework = signal<boolean>(false);
public initialParam = signal<string>('');
public destroy$: Subject<void> = new Subject();
public frameworks: Framework[] = frameworkDefinition.map((f) => {
return {
...f,
fragment: '',
imgSrcDark: `assets${f.imgSrcDark}`,
imgSrcLight: `assets${f.imgSrcLight ? f.imgSrcLight : f.imgSrcDark}`,
};
});
constructor(
private router: Router,
private mgmtService: ManagementService,
breadcrumbService: BreadcrumbService,
activatedRoute: ActivatedRoute,
private _location: Location,
private navigation: NavigationService,
) {
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([bread]);
activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params: Params) => {
const { framework } = params;
if (framework) {
this.initialParam.set(framework);
}
});
}
public goToAppCreatePage(): void {
this.router.navigate(['/projects', this.projectId, 'apps', 'create']);
public findFramework(id: string) {
if (id !== 'other') {
this.customFramework.set(false);
const temp = this.frameworks.find((f) => f.id === id);
this.framework.set(temp);
} else {
this.customFramework.set(true);
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
public goToAppIntegratePage(): void {
if (this.project && this.customFramework()) {
const id = (this.project.project as Project.AsObject).id
? (this.project.project as Project.AsObject).id
: (this.project.project as GrantedProject.AsObject).projectId
? (this.project.project as GrantedProject.AsObject).projectId
: '';
this.router.navigate(['/projects', id, 'apps', 'create']);
} else if (this.project && this.framework()) {
const id = (this.project.project as Project.AsObject).id
? (this.project.project as Project.AsObject).id
: (this.project.project as GrantedProject.AsObject).projectId
? (this.project.project as GrantedProject.AsObject).projectId
: '';
this.router.navigate(['/projects', id, 'apps', 'integrate'], { queryParams: { framework: this.framework()?.id } });
}
}
public close(): void {
window.history.back();
}
public selectProject(project: Project.AsObject): void {
if (project.id) {
this.projectId = project.id;
if (this.navigation.isBackPossible) {
this._location.back();
} else {
if (this.project && this.framework()) {
const id = (this.project.project as Project.AsObject).id
? (this.project.project as Project.AsObject).id
: (this.project.project as GrantedProject.AsObject).projectId
? (this.project.project as GrantedProject.AsObject).projectId
: '';
this.router.navigate(['/projects', id]);
} else {
this.router.navigate(['/projects']);
}
}
}
public selectProject(project: {
project: Project.AsObject | GrantedProject.AsObject;
type: ProjectType;
name: string;
}): void {
if (project) {
this.project = project;
}
}
public createProjectAndContinue() {
const project = new AddProjectRequest();
project.setName(this.projectName);
return this.mgmtService
.addProject(project.toObject())
.then((resp: AddProjectResponse.AsObject) => {
this.error.set('');
this.router.navigate(['/projects', resp.id, 'apps', 'integrate'], {
queryParams: { framework: this.framework()?.id },
});
})
.catch((error) => {
const { message } = error;
this.error.set(message);
});
}
}
@@ -17,11 +17,15 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod
import { SearchProjectAutocompleteModule } from 'src/app/modules/search-project-autocomplete/search-project-autocomplete.module';
import { AppCreateRoutingModule } from './app-create-routing.module';
import { AppCreateComponent } from './app-create.component';
import { FrameworkAutocompleteComponent } from 'src/app/components/framework-autocomplete/framework-autocomplete.component';
import { FrameworkChangeComponent } from 'src/app/components/framework-change/framework-change.component';
@NgModule({
declarations: [AppCreateComponent],
imports: [
FrameworkChangeComponent,
AppCreateRoutingModule,
FrameworkAutocompleteComponent,
CommonModule,
FormsModule,
ReactiveFormsModule,
@@ -35,9 +39,6 @@ import { AppCreateComponent } from './app-create.component';
HasRolePipeModule,
TranslateModule,
HasRoleModule,
MatCheckboxModule,
PasswordComplexityViewModule,
MatSlideToggleModule,
],
})
export default class AppCreateModule {}
@@ -2,6 +2,8 @@
<h1 class="home-title" data-e2e="authenticated-welcome">{{ 'HOME.WELCOME' | translate }}</h1>
<div class="home-wrapper enlarged-container">
<cnsl-quickstart></cnsl-quickstart>
<ng-container *ngIf="['iam.read$'] | hasRole | async; else defaultHome">
<cnsl-onboarding></cnsl-onboarding>
</ng-container>
@@ -19,6 +19,7 @@
.home-title {
font-size: 2rem;
margin-bottom: 1rem;
margin-top: 3rem;
}
.home-wrapper {
@@ -13,10 +13,12 @@ import OnboardingModule from 'src/app/modules/onboarding/onboarding.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { HomeRoutingModule } from './home-routing.module';
import { HomeComponent } from './home.component';
import { QuickstartComponent } from 'src/app/components/quickstart/quickstart.component';
@NgModule({
declarations: [HomeComponent],
imports: [
QuickstartComponent,
CommonModule,
MatIconModule,
HasRoleModule,
@@ -57,6 +57,13 @@
</cnsl-top-view>
<div class="max-width-container">
<cnsl-info-section *ngIf="isNew()" class="problem" [type]="InfoSectionType.INFO">
<div class="jumptoproject-row">
<span class="jumptoproject">{{ 'APP.PAGES.JUMPTOPROJECT' | translate }}</span>
<a [routerLink]="['/projects', projectId]" color="primary" mat-raised-button>{{ 'ACTIONS.CONFIGURE' | translate }}</a>
</div>
</cnsl-info-section>
<div
class="compliance"
*ngIf="app && app.oidcConfig && app.oidcConfig.complianceProblemsList && app.oidcConfig.complianceProblemsList?.length"
@@ -125,6 +125,16 @@
}
}
.jumptoproject-row {
display: flex;
justify-content: space-between;
.jumptoproject {
margin-top: 0.25rem;
display: block;
}
}
.compliance {
padding-bottom: 1rem;
padding-top: 0.5rem;
@@ -1,6 +1,6 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewEncapsulation, signal } from '@angular/core';
import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog';
@@ -149,6 +149,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public settingsList: SidenavSetting[] = [{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' }];
public currentSetting: string | undefined = this.settingsList[0].id;
public isNew = signal<boolean>(false);
constructor(
private envSvc: EnvironmentService,
public translate: TranslateService,
@@ -245,6 +246,9 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public ngOnInit(): void {
const projectId = this.route.snapshot.paramMap.get('projectid');
const appId = this.route.snapshot.paramMap.get('appid');
const isNew = this.route.snapshot.queryParamMap.get('new');
this.isNew.set(isNew === 'true');
if (projectId && appId) {
this.projectId = projectId;
@@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
import { AppCreateComponent } from '../apps/app-create/app-create.component';
import { AppDetailComponent } from '../apps/app-detail/app-detail.component';
import { IntegrateAppComponent } from './integrate/integrate.component';
const routes: Routes = [
{
@@ -10,6 +11,11 @@ const routes: Routes = [
component: AppCreateComponent,
data: { animation: 'AddPage' },
},
{
path: 'integrate',
component: IntegrateAppComponent,
data: { animation: 'AddPage' },
},
{
path: ':appid',
component: AppDetailComponent,
@@ -43,6 +43,9 @@ import { AuthMethodDialogComponent } from './app-detail/auth-method-dialog/auth-
import { AppSecretDialogComponent } from './app-secret-dialog/app-secret-dialog.component';
import { AppsRoutingModule } from './apps-routing.module';
import { RedirectUrisComponent } from './redirect-uris/redirect-uris.component';
import { IntegrateAppComponent } from './integrate/integrate.component';
import { OIDCConfigurationComponent } from 'src/app/components/oidc-configuration/oidc-configuration.component';
import { FrameworkChangeComponent } from 'src/app/components/framework-change/framework-change.component';
@NgModule({
declarations: [
@@ -50,12 +53,15 @@ import { RedirectUrisComponent } from './redirect-uris/redirect-uris.component';
AppDetailComponent,
AppSecretDialogComponent,
RedirectUrisComponent,
IntegrateAppComponent,
AdditionalOriginsComponent,
AuthMethodDialogComponent,
],
imports: [
FrameworkChangeComponent,
CommonModule,
A11yModule,
OIDCConfigurationComponent,
RedirectPipeModule,
NameDialogModule,
AppRadioModule,
@@ -0,0 +1,138 @@
<div class="app-integrate-wrapper">
<div class="integrate-layout-container">
<div class="max-width-container">
<div class="top-control">
<button (click)="close()" mat-icon-button matTooltip="{{ 'ACTIONS.CLOSE' | translate }}">
<mat-icon *ngIf="navigation.isBackPossible">arrow_back</mat-icon>
<mat-icon *ngIf="!navigation.isBackPossible">close</mat-icon>
</button>
<span class="abort">{{ 'APP.PAGES.CREATE' | translate }}</span>
<mat-progress-spinner
class="progress-spinner"
color="primary"
*ngIf="loading"
diameter="30"
mode="indeterminate"
></mat-progress-spinner>
</div>
</div>
</div>
<div class="max-width-container">
<div class="offset-content">
<h1>{{ 'QUICKSTART.ALMOSTDONE' | translate }}</h1>
<p>{{ 'QUICKSTART.REVIEWCONFIGURATION_DESCRIPTION' | translate: { value: framework()?.title } }}</p>
<div class="grid-layout">
<div>
<cnsl-framework-change
*ngIf="framework"
class="framework-selector"
(frameworkChanged)="setFramework($event)"
></cnsl-framework-change>
<div class="steps">
<div class="step">
<span class="step-title">{{ 'PROJECT.PAGES.TITLE' | translate }}</span>
<span>{{ projectName$ | async }}</span>
</div>
<div class="step top-border">
<a *ngIf="framework()" [href]="'https://zitadel.com' + framework()?.docsLink" target="_blank"
>{{ framework()?.title }} {{ 'QUICKSTART.GUIDE' | translate }}</a
>
<a href="https://zitadel.com/docs/sdk-examples/introduction" target="_blank">{{
'QUICKSTART.BROWSEEXAMPLES' | translate
}}</a>
</div>
</div>
</div>
<div class="card-wrapper">
<cnsl-card class="review-card" title="{{ 'QUICKSTART.REVIEWCONFIGURATION' | translate }}">
<cnsl-info-section *ngIf="showRenameWarning | async" [type]="InfoSectionType.WARN"
><span class="duplicate-name-warning">{{
'QUICKSTART.DUPLICATEAPPRENAME' | translate
}}</span></cnsl-info-section
>
<cnsl-oidc-app-configuration
*ngIf="(oidcAppRequest | async)?.toObject() as config"
[name]="(oidcAppRequest | async)?.toObject()?.name"
(changeName)="editName()"
[configuration]="config"
></cnsl-oidc-app-configuration>
<cnsl-info-section *ngIf="framework()?.description as desc" [type]="InfoSectionType.INFO">
<p class="review-description">
{{ desc }}
</p>
</cnsl-info-section>
</cnsl-card>
<cnsl-card
title="{{ 'QUICKSTART.REDIRECTS' | translate }}"
description="{{ 'APP.OIDC.REDIRECTTITLE' | translate }}"
>
<cnsl-info-section [type]="InfoSectionType.ALERT">
<span class="redirect-description">
{{ 'QUICKSTART.DEVMODEWARN' | translate }}
</span>
</cnsl-info-section>
<cnsl-info-section *ngIf="(oidcAppRequest | async)?.toObject()?.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE">
<span class="redirect-description">
{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}
</span>
</cnsl-info-section>
<cnsl-info-section
*ngIf="
(oidcAppRequest | async)?.toObject()?.appType === OIDCAppType.OIDC_APP_TYPE_WEB ||
(oidcAppRequest | async)?.toObject()?.appType === OIDCAppType.OIDC_APP_TYPE_USER_AGENT
"
>
<span class="redirect-description">
{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}
</span>
</cnsl-info-section>
<cnsl-redirect-uris
*ngIf="requestRedirectValuesSubject$"
class="redirect-section"
[disabled]="false"
[isNative]="(oidcAppRequest | async)?.toObject()?.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
[(ngModel)]="redirectUris"
[getValues]="requestRedirectValuesSubject$"
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
[devMode]="true"
data-e2e="redirect-uris"
>
</cnsl-redirect-uris>
<cnsl-redirect-uris
*ngIf="requestRedirectValuesSubject$"
class="redirect-section"
[disabled]="false"
[(ngModel)]="postLogoutUrisList"
[getValues]="requestRedirectValuesSubject$"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[isNative]="(oidcAppRequest | async)?.toObject()?.appType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
[devMode]="true"
data-e2e="postlogout-uris"
>
</cnsl-redirect-uris>
</cnsl-card>
</div>
</div>
<div class="app-integrate-actions">
<button
mat-raised-button
[disabled]="loading"
class="create-button"
color="primary"
(click)="createApp()"
data-e2e="create-button"
>
{{ 'ACTIONS.CREATE' | translate }}
</button>
</div>
</div>
</div>
</div>
@@ -0,0 +1,214 @@
h1 {
font-weight: 500;
font-size: 2rem;
}
.integrate-layout-container {
margin: 0 -2rem 2rem -2rem;
padding: 3rem 2rem 14rem 2rem;
.top-control {
display: flex;
align-items: center;
.abort {
font-size: 1.2rem;
margin-left: 1.5rem;
text-transform: uppercase;
font-size: 14px;
opacity: 0.8;
letter-spacing: 0.05em;
}
.progress-spinner {
margin-left: 2rem;
}
}
}
.offset-content {
margin-top: -14rem;
}
.grid-layout {
margin-top: 2rem;
display: grid;
grid-template-columns: [first] 300px [second] auto;
grid-column-gap: 5rem;
.framework-selector {
margin-bottom: 2rem;
display: block;
}
.steps {
margin-top: 0.5rem;
.step {
display: flex;
flex-direction: column;
padding: 1rem 0;
.step-title {
font-weight: 700;
font-size: 12px;
opacity: 0.8;
margin-bottom: 0.5rem;
text-transform: uppercase;
}
a {
margin-bottom: 0.5rem;
}
}
}
.redirect-p {
margin-top: 0;
}
.redirect-description {
font-size: 14px;
margin: 0.25rem 0 0 0;
display: block;
}
}
@media only screen and (max-width: 900px) {
.integrate-layout-container {
margin: 0 -2rem 2rem -2rem;
background: transparent;
padding: 3rem 2rem 14rem 2rem;
}
.grid-layout {
display: flex;
flex-direction: column;
.steps {
display: none;
}
}
}
.card-wrapper {
margin-top: -1rem;
.duplicate-name-warning {
margin-top: 0.25rem;
display: block;
}
}
@mixin app-integrate-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: map-get($primary, 500);
$warn-color: map-get($warn, 500);
$accent-color: map-get($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
.integrate-layout-container {
background: map-get($background, metadata-section);
}
.review-description {
font-size: 14px;
margin: 0.25rem 0 0 0;
display: block;
}
.grid-layout {
.step {
&.top-border {
border-top: 2px solid $border-color;
}
}
a {
color: $primary-color;
}
}
.framework-card-wrapper {
display: flex;
align-items: center;
gap: 1rem;
.framework-card {
position: relative;
flex-shrink: 0;
text-decoration: none;
border-radius: 0.5rem;
box-sizing: border-box;
transition: all 0.1s ease-in;
display: flex;
flex-direction: row;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
border: 1px solid rgba(#8795a1, 0.2);
padding: 0 0.5rem;
img {
width: 100%;
height: 100%;
max-width: 40px;
max-height: 40px;
object-fit: contain;
object-position: center;
}
.dark-only {
display: if($is-dark-theme, block, none);
}
.light-only {
display: if($is-dark-theme, none, block);
}
span {
margin: 0.5rem;
text-align: center;
color: map-get($foreground, text);
}
.action-row {
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 14px;
margin-bottom: 0.5rem;
color: map-get($primary, 400);
.icon {
margin-left: 0rem;
}
}
}
}
}
.app-integrate-actions {
margin-top: 2rem;
display: flex;
align-items: center;
justify-content: flex-end;
.create-button {
height: 3.5rem;
padding: 0 4rem;
}
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IntegrateAppComponent } from './integrate.component';
describe('IntegrateAppComponent', () => {
let component: IntegrateAppComponent;
let fixture: ComponentFixture<IntegrateAppComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [IntegrateAppComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IntegrateAppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,220 @@
import { C, COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit, Signal, computed, effect, signal } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Buffer } from 'buffer';
import { BehaviorSubject, Subject, Subscription, combineLatest } from 'rxjs';
import { debounceTime, map, takeUntil } from 'rxjs/operators';
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
import { requiredValidator } from 'src/app/modules/form-field/validators/validators';
import {
APIAuthMethodType,
OIDCAppType,
OIDCAuthMethodType,
OIDCGrantType,
OIDCResponseType,
} from 'src/app/proto/generated/zitadel/app_pb';
import {
AddAPIAppRequest,
AddAPIAppResponse,
AddOIDCAppRequest,
AddOIDCAppResponse,
AddSAMLAppRequest,
} from 'src/app/proto/generated/zitadel/management_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
import { Framework } from 'src/app/components/quickstart/quickstart.component';
import { OIDC_CONFIGURATIONS } from 'src/app/utils/framework';
import { NavigationService } from 'src/app/services/navigation.service';
import { NameDialogComponent } from 'src/app/modules/name-dialog/name-dialog.component';
@Component({
selector: 'cnsl-integrate',
templateUrl: './integrate.component.html',
styleUrls: ['./integrate.component.scss'],
})
export class IntegrateAppComponent implements OnInit, OnDestroy {
private destroy$: Subject<void> = new Subject();
public projectId: string = '';
public loading: boolean = false;
public InfoSectionType: any = InfoSectionType;
public framework = signal<Framework | undefined>(undefined);
public showRenameWarning: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public oidcAppRequest: BehaviorSubject<AddOIDCAppRequest> = new BehaviorSubject(new AddOIDCAppRequest());
public OIDCAppType: any = OIDCAppType;
public requestRedirectValuesSubject$: Subject<void> = new Subject();
constructor(
private activatedRoute: ActivatedRoute,
private router: Router,
private toast: ToastService,
private dialog: MatDialog,
private mgmtService: ManagementService,
private _location: Location,
private breadcrumbService: BreadcrumbService,
public navigation: NavigationService,
) {
effect(() => {
const fwId = this.framework()?.id;
const fw = this.framework();
if (fw && fwId) {
const request = OIDC_CONFIGURATIONS[fwId];
request.setProjectId(this.projectId);
request.setName(fw.title);
request.setDevMode(true);
this.requestRedirectValuesSubject$.next();
this.showRenameWarning.next(false);
this.oidcAppRequest.next(request);
return request;
} else {
const request = new AddOIDCAppRequest();
this.oidcAppRequest.next(request);
return request;
}
});
}
public projectName$ = combineLatest([this.mgmtService.ownedProjects, this.mgmtService.grantedProjects]).pipe(
map(([projects, grantedProjects]) => {
const project = projects.find((project) => project.id === this.activatedRoute.snapshot.paramMap.get('projectid'));
const grantedproject = grantedProjects.find(
(grantedproject) => grantedproject.projectId === this.activatedRoute.snapshot.paramMap.get('projectid'),
);
return project?.name ?? grantedproject?.projectName ?? '';
}),
);
public setFramework(framework: Framework | undefined) {
this.framework.set(framework);
}
public ngOnInit(): void {
const projectId = this.activatedRoute.snapshot.paramMap.get('projectid');
if (projectId) {
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
new Breadcrumb({
type: BreadcrumbType.PROJECT,
name: '',
param: { key: 'projectid', value: projectId },
routerLink: ['/projects', projectId],
isZitadel: false,
}),
];
this.projectId = projectId;
this.breadcrumbService.setBreadcrumb(breadcrumbs);
}
}
public ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
public close(): void {
if (this.navigation.isBackPossible) {
this._location.back();
} else {
this.router.navigate(['/projects', this.projectId]);
}
}
public createApp(): void {
this.loading = true;
this.mgmtService
.addOIDCApp(this.oidcAppRequest.getValue())
.then((resp) => {
this.loading = false;
this.showRenameWarning.next(false);
this.toast.showInfo('APP.TOAST.CREATED', true);
if (resp.clientSecret) {
this.showSavedDialog(resp);
} else {
this.router.navigate(['projects', this.projectId, 'apps', resp.appId], { queryParams: { new: true } });
}
})
.catch((error) => {
if (error.code === 6) {
this.showRenameWarning.next(true);
}
this.loading = false;
this.toast.showError(error);
});
}
public editName() {
const dialogRef = this.dialog.open(NameDialogComponent, {
data: {
name: this.oidcAppRequest.getValue()?.getName() ?? '',
titleKey: 'APP.NAMEDIALOG.TITLE',
descKey: 'APP.NAMEDIALOG.DESCRIPTION',
labelKey: 'APP.NAMEDIALOG.NAME',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((name) => {
if (name && name !== this.framework()?.title) {
const request = this.oidcAppRequest.getValue();
request.setName(name);
this.showRenameWarning.next(false);
this.oidcAppRequest.next(request);
}
});
}
public showSavedDialog(added: AddOIDCAppResponse.AsObject | AddAPIAppResponse.AsObject): void {
let clientSecret = '';
if (added.clientSecret) {
clientSecret = added.clientSecret;
}
let clientId = '';
if (added.clientId) {
clientId = added.clientId;
}
const dialogRef = this.dialog.open(AppSecretDialogComponent, {
data: {
clientSecret: clientSecret,
clientId: clientId,
},
});
dialogRef.afterClosed().subscribe(() => {
this.router.navigate(['projects', this.projectId, 'apps', added.appId], { queryParams: { new: true } });
});
}
public get redirectUris() {
return this.oidcAppRequest.getValue().toObject().redirectUrisList;
}
public set redirectUris(value: string[]) {
const request = this.oidcAppRequest.getValue();
request.setRedirectUrisList(value);
this.oidcAppRequest.next(request);
}
public get postLogoutUrisList() {
return this.oidcAppRequest.getValue().toObject().postLogoutRedirectUrisList;
}
public set postLogoutUrisList(value: string[]) {
const request = this.oidcAppRequest.getValue();
request.setPostLogoutRedirectUrisList(value);
this.oidcAppRequest.next(request);
}
}
@@ -1,7 +1,7 @@
import { DatePipe } from '@angular/common';
import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import * as moment from 'moment';
import moment from 'moment';
import { supportedLanguages } from 'src/app/utils/language';
@Pipe({
+70
View File
@@ -0,0 +1,70 @@
import { Framework } from '@netlify/framework-info/lib/types';
import { AddOIDCAppRequest } from '../proto/generated/zitadel/management_pb';
import { FrameworkName } from '@netlify/framework-info/lib/generated/frameworkNames';
import { OIDCAppType, OIDCAuthMethodType, OIDCGrantType, OIDCResponseType } from '../proto/generated/zitadel/app_pb';
type OidcAppConfigurations = {
[framework: string]: AddOIDCAppRequest;
};
export const OIDC_CONFIGURATIONS: OidcAppConfigurations = {
// user agent applications
['angular']: new AddOIDCAppRequest()
.setAppType(OIDCAppType.OIDC_APP_TYPE_USER_AGENT)
.setAuthMethodType(OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE)
.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE])
.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE])
.setRedirectUrisList(['http://localhost:4200/callback'])
.setPostLogoutRedirectUrisList(['http://localhost:4200']),
['react']: new AddOIDCAppRequest()
.setAppType(OIDCAppType.OIDC_APP_TYPE_USER_AGENT)
.setAuthMethodType(OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE)
.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE])
.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE])
.setRedirectUrisList(['http://localhost:3000/callback'])
.setPostLogoutRedirectUrisList(['http://localhost:3000']),
['vue']: new AddOIDCAppRequest()
.setAppType(OIDCAppType.OIDC_APP_TYPE_USER_AGENT)
.setAuthMethodType(OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE)
.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE])
.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE])
.setRedirectUrisList(['http://localhost:5173/auth/signinwin/zitadel'])
.setPostLogoutRedirectUrisList(['http://localhost:5173']),
// web applications
['next']: new AddOIDCAppRequest()
.setAppType(OIDCAppType.OIDC_APP_TYPE_WEB)
.setAuthMethodType(OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC)
.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE])
.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE])
.setRedirectUrisList(['http://localhost:3000/callback'])
.setPostLogoutRedirectUrisList(['http://localhost:3000']),
['java']: new AddOIDCAppRequest()
.setAppType(OIDCAppType.OIDC_APP_TYPE_WEB)
.setAuthMethodType(OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE)
.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE])
.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE])
.setRedirectUrisList(['http://localhost:18080/webapp/login/oauth2/code/zitadel'])
.setPostLogoutRedirectUrisList(['http://localhost:18080/webapp']),
['symfony']: new AddOIDCAppRequest()
.setAppType(OIDCAppType.OIDC_APP_TYPE_WEB)
.setAuthMethodType(OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC)
.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE])
.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE])
.setRedirectUrisList(['http://localhost:8000/login_check'])
.setPostLogoutRedirectUrisList(['http://localhost:8000/logout']),
['django']: new AddOIDCAppRequest()
.setAppType(OIDCAppType.OIDC_APP_TYPE_WEB)
.setAuthMethodType(OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE)
.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE])
.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE])
.setRedirectUrisList(['http://localhost:8000/oidc/callback/'])
.setPostLogoutRedirectUrisList(['http://localhost:8000/oidc/logout/ ']),
// native
['flutter']: new AddOIDCAppRequest()
.setAppType(OIDCAppType.OIDC_APP_TYPE_NATIVE)
.setAuthMethodType(OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE)
.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE])
.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE])
.setRedirectUrisList(['http://localhost:4444/auth.html', 'com.example.zitadelflutter'])
.setPostLogoutRedirectUrisList(['http://localhost:4444', 'com.example.zitadelflutter']),
};