feat(console): onboarding flow (#5225)

Implements an onboarding UI for users
This commit is contained in:
Max Peintner 2023-02-24 18:01:05 +01:00 committed by GitHub
parent a7cc907ab7
commit f8ddc844f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1248 additions and 26 deletions

View File

@ -165,6 +165,11 @@ export class AppComponent implements OnDestroy {
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/shield-alert.svg'),
);
this.matIconRegistry.addSvgIcon(
'mdi_shield_check',
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/shield-check.svg'),
);
this.matIconRegistry.addSvgIcon(
'mdi_arrow_expand',
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/arrow-expand.svg'),

View File

@ -212,7 +212,46 @@
<ng-template #shortcutKeyRef>
<ng-container *ngIf="(isHandset$ | async) === false">
<span class="fill-space"></span>
<ng-template cnslHasRole [hasRole]="['iam.read']">
<span class="fill-space"></span>
<ng-container *ngIf="!adminService.hideOnboarding && (adminService.progressAllDone | async) === false">
<button
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
matRipple
class="progress-bar"
(click)="showInstanceProgress = !showInstanceProgress"
>
<mat-progress-bar
class="progress"
mode="determinate"
[value]="adminService.progressPercentage | async"
></mat-progress-bar>
</button>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[flexibleDimensions]="true"
[lockPosition]="true"
[cdkConnectedOverlayOffsetY]="10"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayPositions]="positions"
cdkConnectedOverlayBackdropClass="transparent-backdrop"
[cdkConnectedOverlayOpen]="showInstanceProgress"
(backdropClick)="showInstanceProgress = false"
(detach)="showInstanceProgress = false"
>
<cnsl-onboarding-card
(dismissedCard)="dismissOnboarding()"
class="onboarding_card"
*ngIf="org && showInstanceProgress"
>
</cnsl-onboarding-card>
</ng-template>
</ng-container>
</ng-template>
<div (click)="openHelp()" class="nav-shortcut-action-key" matTooltip="{{ 'MENU.OPENSHORTCUTSTOOLTIP' | translate }}">
<div class="nav-key-overlay"></div>
<span>?</span>

View File

@ -181,8 +181,39 @@
flex: 1;
}
.progress-bar {
margin-right: 0.5rem;
width: 100px;
min-width: 50px;
background: if($is-dark-theme, #ffffff20, #00000010);
border: none;
padding: 2px;
border-radius: 50vw;
&:hover {
background: if($is-dark-theme, #ffffff, $primary-color);
transition: background ease 0.15s;
}
.progress {
border-radius: 50vw;
height: 8px;
.mat-progress-bar-buffer {
background-color: if($is-dark-theme, rgb(69, 91, 84), #cccccc) !important;
}
.mat-progress-bar-background {
fill: if($is-dark-theme, rgb(69, 91, 84), #cccccc) !important;
}
.mat-progress-bar-fill:after {
background-color: var(--success);
}
}
}
.nav-shortcut-action-key {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
font-size: 12px;
@ -213,3 +244,9 @@
}
}
}
.onboarding_card {
border-radius: 0.5rem;
box-shadow: 0 0 15px 0 rgb(0 0 0 / 10%);
border: 1px solid rgba(#8795a1, 0.2);
}

View File

@ -1,17 +1,20 @@
import { animate, keyframes, style, transition, trigger } from '@angular/animations';
import { BreakpointObserver } from '@angular/cdk/layout';
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay';
import { HttpClient } from '@angular/common/http';
import { Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { Router } from '@angular/router';
import { BehaviorSubject, combineLatest, forkJoin, map, merge, Observable, Subject, switchMap, take } from 'rxjs';
import { BehaviorSubject, combineLatest, map, Observable, Subject, take, tap } from 'rxjs';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { User } from 'src/app/proto/generated/zitadel/user_pb';
import { AdminService } from 'src/app/services/admin.service';
import { AuthenticationService } from 'src/app/services/authentication.service';
import { BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { KeyboardShortcutsService } from 'src/app/services/keyboard-shortcuts/keyboard-shortcuts.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { StorageLocation, StorageService } from 'src/app/services/storage.service';
@Component({
selector: 'cnsl-nav',
@ -73,6 +76,7 @@ export class NavComponent implements OnDestroy {
@Input() public isDarkTheme: boolean = true;
@Input() public user!: User.AsObject;
public showInstanceProgress: boolean = false;
public isHandset$: Observable<boolean> = this.breakpointObserver.observe('(max-width: 599px)').pipe(
map((result) => {
return result.matches;
@ -83,14 +87,19 @@ export class NavComponent implements OnDestroy {
public filterControl: UntypedFormControl = new UntypedFormControl('');
public orgLoading$: BehaviorSubject<any> = new BehaviorSubject(false);
public showAccount: boolean = false;
public hideAdminWarn: boolean = true;
private destroy$: Subject<void> = new Subject();
public BreadcrumbType: any = BreadcrumbType;
public customerPortalLink: string = '';
public positions: ConnectedPosition[] = [
new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }, 0, 10),
new ConnectionPositionPair({ originX: 'end', originY: 'bottom' }, { overlayX: 'end', overlayY: 'top' }, 0, 10),
];
constructor(
public authService: GrpcAuthService,
public adminService: AdminService,
public authenticationService: AuthenticationService,
public breadcrumbService: BreadcrumbService,
public mgmtService: ManagementService,
@ -98,8 +107,8 @@ export class NavComponent implements OnDestroy {
private breakpointObserver: BreakpointObserver,
private http: HttpClient,
private shortcutService: KeyboardShortcutsService,
private storageService: StorageService,
) {
this.hideAdminWarn = localStorage.getItem('hideAdministratorWarning') === 'true' ? true : false;
this.loadEnvironment();
}
@ -114,16 +123,18 @@ export class NavComponent implements OnDestroy {
});
}
public toggleAdminHide(): void {
this.hideAdminWarn = !this.hideAdminWarn;
localStorage.setItem('hideAdministratorWarning', this.hideAdminWarn.toString());
}
public ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
public dismissOnboarding(): void {
this.showInstanceProgress = false;
this.adminService.hideOnboarding = true;
this.storageService.setItem('onboarding-dismissed', 'true', StorageLocation.local);
this.adminService.progressAllDone.next(true);
}
public get isUserLinkActive(): boolean {
const url = this.router.url;
return url.substring(0, 6) === '/users';

View File

@ -12,12 +12,15 @@ import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { MatLegacyProgressBarModule } from '@angular/material/legacy-progress-bar';
import OnboardingCardModule from '../onboarding-card/onboarding-card.module';
import { NavComponent } from './nav.component';
@NgModule({
declarations: [NavComponent],
imports: [
CommonModule,
OnboardingCardModule,
FormsModule,
ReactiveFormsModule,
TranslateModule,
@ -25,6 +28,7 @@ import { NavComponent } from './nav.component';
RouterModule,
MatTooltipModule,
HasRolePipeModule,
MatLegacyProgressBarModule,
HasRoleModule,
MatMenuModule,
MatButtonModule,

View File

@ -0,0 +1,39 @@
<div class="onboarding-card" cdkTrapFocus>
<div class="spinner-w">
<mat-spinner diameter="20" *ngIf="loading$ | async" color="accent"> </mat-spinner>
</div>
<div class="progress-header">
<h2>{{ 'ONBOARDING.CARD.TITLE' | translate }}</h2>
<p class="cnsl-secondary-text">
{{ 'ONBOARDING.CARD.DESCRIPTION' | translate }}
</p>
</div>
<div class="prog-desc cnsl-secondary-text">
{{ adminService.progressDone | async }} / {{ adminService.progressTotal | async }}
{{ 'ONBOARDING.COMPLETED' | translate }}
</div>
<div class="actions-list">
<ng-container *ngFor="let action of actions | async">
<a
[routerLink]="action[1].link"
[queryParams]="{ id: action[1].fragment }"
class="action-element"
[ngClass]="{ done: action[1].event !== undefined }"
>
<div class="state-circle">
<mat-icon *ngIf="action[1]?.event !== undefined" class="success-icon" matTooltip="{{ action[1].event | event }}"
>check_circle</mat-icon
>
</div>
<span class="name">{{ 'ONBOARDING.EVENTS.' + action[0] + '.title' | translate }}</span>
<mat-icon class="arrow-right">keyboard_arrow_right</mat-icon>
</a>
</ng-container>
</div>
<div class="no-thanks-wrapper">
<a class="no-thanks-btn" (click)="dismiss()">{{ 'ONBOARDING.DISMISS' | translate }}</a>
</div>
</div>

View File

@ -0,0 +1,144 @@
@mixin onboarding-card-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: mat.get-color-from-palette($primary, 500);
$warn-color: mat.get-color-from-palette($warn, 500);
$accent-color: mat.get-color-from-palette($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
.onboarding-card {
border-radius: 0.5rem;
z-index: 200;
position: relative;
min-width: 220px;
max-width: 280px;
padding-bottom: 0.5rem;
position: relative;
color: map-get($foreground, text);
background: map-get($background, cards);
.spinner-w {
top: 1rem;
left: 0;
right: 0;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
.progress-header {
padding: 1rem 1rem 0 1rem;
h2 {
margin-top: 0;
}
p {
font-size: 14px;
}
}
.prog-desc {
font-style: italic;
font-size: 14px;
padding: 0 1rem 1rem 1rem;
}
.actions-list {
display: flex;
flex-direction: column;
padding: 0 0 1rem 0;
.action-element {
display: flex;
align-items: center;
padding: 0.25rem 0.5rem 0.25rem 1rem;
text-decoration: none;
color: map-get($foreground, text);
.state-circle {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
height: 1.5rem;
width: 1.5rem;
border-radius: 50vw;
margin-right: 1rem;
background-color: if($is-dark-theme, map-get($background, state), #e4e7e4);
box-shadow: 0 0 3px #0000001a;
i {
font-size: 1rem;
}
.success-icon {
font-size: 1.2rem;
height: 1.2rem;
width: 1.2rem;
color: var(--success);
}
.warn-icon {
font-size: 1.2rem;
height: 1.2rem;
width: 1.2rem;
color: map-get($background, alert);
}
}
.name {
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
flex: 1;
}
.fill-space {
flex: 1;
}
.arrow-right {
visibility: hidden;
}
&.done {
.state-circle i {
color: var(--success);
}
.name {
text-decoration: line-through;
opacity: 0.5;
}
}
&:hover {
cursor: pointer;
background-color: #00000010;
.arrow-right {
visibility: visible;
}
}
}
}
.no-thanks-wrapper {
display: flex;
justify-content: flex-end;
padding: 0 1rem 0.5rem 1rem;
.no-thanks-btn {
font-style: italic;
cursor: pointer;
}
}
}
}

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OnboardingCardComponent } from './onboarding-card.component';
describe('OnboardingCardComponent', () => {
let component: OnboardingCardComponent;
let fixture: ComponentFixture<OnboardingCardComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OnboardingCardComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OnboardingCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { AdminService } from 'src/app/services/admin.service';
import { ONBOARDING_EVENTS } from 'src/app/utils/onboarding';
@Component({
selector: 'cnsl-onboarding-card',
templateUrl: './onboarding-card.component.html',
styleUrls: ['./onboarding-card.component.scss'],
})
export class OnboardingCardComponent implements OnInit {
public percentageChanged: EventEmitter<number> = new EventEmitter<number>();
public loading$: BehaviorSubject<any> = new BehaviorSubject(false);
public actions = this.adminService.progressEvents;
@Output() public dismissedCard: EventEmitter<void> = new EventEmitter();
constructor(public adminService: AdminService) {}
public dismiss(): void {
this.dismissedCard.emit();
}
ngOnInit() {
this.adminService.loadEvents.next(ONBOARDING_EVENTS);
}
}

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatRippleModule } from '@angular/material/core';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { ShortcutsModule } from 'src/app/modules/shortcuts/shortcuts.module';
import { RouterModule } from '@angular/router';
import { EventPipeModule } from 'src/app/pipes/event-pipe/event-pipe.module';
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 { OnboardingCardComponent } from './onboarding-card.component';
@NgModule({
declarations: [OnboardingCardComponent],
imports: [
CommonModule,
MatIconModule,
TranslateModule,
RouterModule,
MatProgressSpinnerModule,
EventPipeModule,
MatTooltipModule,
],
exports: [OnboardingCardComponent],
})
export default class OnboardingCardModule {}

View File

@ -0,0 +1,57 @@
<div class="onboarding-header">
<h1 class="title" data-e2e="authenticated-welcome">{{ 'HOME.WELCOME' | translate }}</h1>
<p class="desc cnsl-secondary-text">{{ 'ONBOARDING.DESCRIPTION' | translate }}</p>
<ng-container *ngIf="!adminService.hideOnboarding && (adminService.progressAllDone | async) === false">
<div class="onboarding-progress-bar-wrapper">
<mat-progress-bar
class="progress"
mode="determinate"
[value]="adminService.progressPercentage | async"
></mat-progress-bar>
<div class="prog-desc cnsl-secondary-text">
{{ adminService.progressDone | async }} / {{ adminService.progressTotal | async }}
{{ 'ONBOARDING.COMPLETED' | translate }}
</div>
<mat-spinner diameter="20" *ngIf="adminService.onboardingLoading | async"></mat-spinner>
</div>
</ng-container>
<div
class="action-card-wrapper"
[ngClass]="{ alldone: adminService.hideOnboarding || (adminService.progressAllDone | async) }"
>
<ng-container *ngFor="let action of actions | async">
<a
[routerLink]="action[1].link"
[queryParams]="{ id: action[1].fragment }"
class="action-card card"
[ngClass]="{ done: action[1].event !== undefined }"
>
<div class="state-circle">
<mat-icon *ngIf="action[1]?.event !== undefined" matTooltip="{{ action[1].event | event }}" class="success-icon"
>check_circle</mat-icon
>
</div>
<div class="action-content">
<div class="text-block">
<span class="name">{{ 'ONBOARDING.EVENTS.' + action[0] + '.title' | translate }}</span>
<span class="cnsl-secondary-text description">{{
'ONBOARDING.EVENTS.' + action[0] + '.description' | translate
}}</span>
</div>
<span class="fill-space"></span>
<div class="action-row">
<span>{{ 'ACTIONS.SETUP' | translate }}</span>
<mat-icon class="icon">keyboard_arrow_right</mat-icon>
</div>
</div>
</a>
</ng-container>
</div>
</div>

View File

@ -0,0 +1,174 @@
@use '@angular/material' as mat;
@mixin onboarding-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$accent: map-get($theme, accent);
$primary-color: mat.get-color-from-palette($primary, 500);
$warn-color: mat.get-color-from-palette($warn, 500);
$accent-color: mat.get-color-from-palette($accent, 500);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
$list-background-color: mat.get-color-from-palette($background, 300);
$card-background-color: mat.get-color-from-palette($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);
.onboarding-header {
display: flex;
flex-direction: column;
margin-bottom: 2rem;
.title {
font-size: 2rem;
margin-bottom: 1rem;
}
.desc {
font-size: 14px;
margin-top: 0;
}
.onboarding-progress-bar-wrapper {
display: flex;
align-items: center;
padding: 1rem 0;
min-height: 20px;
.progress {
border-radius: 50vw;
height: 8px;
max-width: 300px;
margin-right: 1rem;
.mat-progress-bar-buffer {
background-color: if($is-dark-theme, rgb(69, 91, 84), #cccccc) !important;
}
.mat-progress-bar-background {
fill: if($is-dark-theme, rgb(69, 91, 84), #cccccc) !important;
}
.mat-progress-bar-fill:after {
background-color: var(--success);
}
}
.prog-desc {
font-style: italic;
font-size: 14px;
margin-right: 1rem;
}
}
.action-card-wrapper {
display: flex;
flex-wrap: wrap;
margin: 0 -1rem;
box-sizing: border-box;
.action-card {
position: relative;
z-index: 100;
margin: 1rem;
flex-basis: 270px;
text-decoration: none;
cursor: pointer;
padding-top: 0;
padding-right: 0;
padding-bottom: 0;
padding-left: 1rem;
border-radius: 0.5rem;
box-sizing: border-box;
min-height: 166px;
transition: box-shadow 0.1s ease-in;
.action-content {
display: flex;
flex-direction: column;
height: 100%;
padding-right: 0.5rem;
.text-block {
display: flex;
flex-direction: column;
color: map-get($foreground, text);
padding-top: 1rem;
.name {
margin-bottom: 1rem;
}
.description {
font-size: 14px;
}
}
.fill-space {
flex: 1;
}
}
.state-circle {
position: absolute;
top: 0;
left: 0;
transform: translateX(-50%) translateY(-50%);
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
height: 1.5rem;
width: 1.5rem;
border-radius: 50vw;
margin-right: 1rem;
background-color: if($is-dark-theme, map-get($background, state), #e4e7e4);
box-shadow: 0 0 3px #0000001a;
i {
font-size: 1rem;
}
.success-icon {
font-size: 1.2rem;
height: 1.2rem;
width: 1.2rem;
color: var(--success);
}
.warn-icon {
font-size: 1.2rem;
height: 1.2rem;
width: 1.2rem;
color: map-get($foreground, text);
opacity: 0.3;
}
}
.action-row {
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 14px;
margin-bottom: 0.5rem;
.icon {
margin-left: 0rem;
}
}
&:hover {
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12);
}
}
&.alldone {
.state-circle {
display: none;
}
}
}
}
}

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OnboardingComponent } from './onboarding.component';
describe('OnboardingComponent', () => {
let component: OnboardingComponent;
let fixture: ComponentFixture<OnboardingComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OnboardingComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OnboardingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { Component } from '@angular/core';
import { AdminService } from 'src/app/services/admin.service';
import { ONBOARDING_EVENTS } from 'src/app/utils/onboarding';
@Component({
selector: 'cnsl-onboarding',
templateUrl: './onboarding.component.html',
styleUrls: ['./onboarding.component.scss'],
})
export class OnboardingComponent {
public actions = this.adminService.progressEvents;
constructor(public adminService: AdminService) {
this.adminService.loadEvents.next(ONBOARDING_EVENTS);
}
}

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatRippleModule } from '@angular/material/core';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { ShortcutsModule } from 'src/app/modules/shortcuts/shortcuts.module';
import { MatLegacyProgressBarModule } from '@angular/material/legacy-progress-bar';
import { RouterModule } from '@angular/router';
import { EventPipeModule } from 'src/app/pipes/event-pipe/event-pipe.module';
import { OnboardingComponent } from './onboarding.component';
@NgModule({
declarations: [OnboardingComponent],
imports: [
CommonModule,
MatIconModule,
TranslateModule,
MatTooltipModule,
ShortcutsModule,
MatRippleModule,
RouterModule,
MatProgressSpinnerModule,
MatLegacyProgressBarModule,
EventPipeModule,
],
exports: [OnboardingComponent],
})
export default class OnboardingModule {}

View File

@ -14,7 +14,6 @@
.org-context-card {
border-radius: 0.5rem;
z-index: 200;
border: 1px solid #ffffff30;
display: flex;
flex-direction: column;
align-items: center;

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AppCreateComponent } from './app-create.component';
const routes: Routes = [
{
path: '',
component: AppCreateComponent,
data: { animation: 'DetailPage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AppCreateRoutingModule {}

View File

@ -0,0 +1,26 @@
<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-search-project-autocomplete
class="block"
[autocompleteType]="ProjectAutocompleteType.PROJECT_OWNED"
(selectionChanged)="selectProject($any($event.project))"
>
</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()"
>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>
</div>
</cnsl-create-layout>

View File

@ -0,0 +1,25 @@
h1 {
font-weight: 500;
}
.app-create-main-content {
max-width: 35rem;
.app-create-btn-container {
display: flex;
align-items: center;
margin-top: 3rem;
.continue-button {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}
}
.complexity-view {
width: 100%;
margin: 0 0.5rem;
}
}

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { OrgCreateComponent } from './org-create.component';
describe('OrgCreateComponent', () => {
let component: OrgCreateComponent;
let fixture: ComponentFixture<OrgCreateComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OrgCreateComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OrgCreateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,45 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
import { MatLegacySlideToggleChange as MatSlideToggleChange } from '@angular/material/legacy-slide-toggle';
import { Router } from '@angular/router';
import { take } from 'rxjs/operators';
import { ProjectAutocompleteType } from 'src/app/modules/search-project-autocomplete/search-project-autocomplete.component';
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from 'src/app/pages/validators';
import { SetUpOrgRequest } from 'src/app/proto/generated/zitadel/admin_pb';
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { Project } from 'src/app/proto/generated/zitadel/project_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
@Component({
selector: 'cnsl-app-create',
templateUrl: './app-create.component.html',
styleUrls: ['./app-create.component.scss'],
})
export class AppCreateComponent {
public projectId: string = '';
public ProjectAutocompleteType: any = ProjectAutocompleteType;
constructor(private router: Router, breadcrumbService: BreadcrumbService) {
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([bread]);
}
public goToAppCreatePage(): void {
this.router.navigate(['/projects', this.projectId, 'apps', 'create']);
}
public close(): void {
window.history.back();
}
public selectProject(project: Project.AsObject): void {
if (project.id) {
this.projectId = project.id;
}
}
}

View File

@ -0,0 +1,43 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { CreateLayoutModule } from 'src/app/modules/create-layout/create-layout.module';
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { PasswordComplexityViewModule } from 'src/app/modules/password-complexity-view/password-complexity-view.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
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';
@NgModule({
declarations: [AppCreateComponent],
imports: [
AppCreateRoutingModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
InfoSectionModule,
InputModule,
MatButtonModule,
MatIconModule,
SearchProjectAutocompleteModule,
MatSelectModule,
CreateLayoutModule,
HasRolePipeModule,
TranslateModule,
HasRoleModule,
MatCheckboxModule,
PasswordComplexityViewModule,
MatSlideToggleModule,
],
})
export default class AppCreateModule {}

View File

@ -1,10 +1,11 @@
<div class="max-width-container">
<div class="home-wrapper enlarged-container">
<div class="header">
<h1 class="title" data-e2e="authenticated-welcome">{{ 'HOME.WELCOME' | translate }}</h1>
</div>
<cnsl-shortcuts></cnsl-shortcuts>
<ng-container *ngIf="['iam.read$'] | hasRole | async; else defaultHome">
<cnsl-onboarding></cnsl-onboarding>
</ng-container>
<ng-template #defaultHome>
<cnsl-shortcuts></cnsl-shortcuts>
</ng-template>
<p class="disclaimer cnsl-secondary-text">{{ 'HOME.DISCLAIMER' | translate }}</p>

View File

@ -125,6 +125,7 @@
font-size: 14px;
margin-top: 0;
margin-bottom: 5rem;
font-style: italic;
}
.fill-space {

View File

@ -9,6 +9,8 @@ import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { ShortcutsModule } from 'src/app/modules/shortcuts/shortcuts.module';
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';
@ -20,10 +22,12 @@ import { HomeComponent } from './home.component';
HasRoleModule,
HomeRoutingModule,
MatButtonModule,
HasRolePipeModule,
TranslateModule,
MatTooltipModule,
MatProgressSpinnerModule,
ShortcutsModule,
OnboardingModule,
MatRippleModule,
],
})

View File

@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { take } from 'rxjs';
import { Subject, takeUntil } from 'rxjs';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
@ -27,7 +27,7 @@ import {
templateUrl: './instance-settings.component.html',
styleUrls: ['./instance-settings.component.scss'],
})
export class InstanceSettingsComponent {
export class InstanceSettingsComponent implements OnDestroy {
public id: string = '';
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public settingsList: SidenavSetting[] = [
@ -52,6 +52,8 @@ export class InstanceSettingsComponent {
SECRETS,
SECURITY,
];
private destroy$: Subject<void> = new Subject();
constructor(breadcrumbService: BreadcrumbService, activatedRoute: ActivatedRoute) {
const breadcrumbs = [
new Breadcrumb({
@ -62,11 +64,16 @@ export class InstanceSettingsComponent {
];
breadcrumbService.setBreadcrumb(breadcrumbs);
activatedRoute.queryParams.pipe(take(1)).subscribe((params: Params) => {
activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params: Params) => {
const { id } = params;
if (id) {
this.id = id;
}
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -19,6 +19,15 @@ const routes: Routes = [
roles: ['project.create'],
},
},
{
path: 'app-create',
canActivate: [RoleGuard],
data: {
animation: 'AddPage',
roles: ['project.app.write'],
},
loadChildren: () => import('../app-create/app-create.module'),
},
{
path: ':projectid',
loadChildren: () => import('./owned-projects/owned-projects.module'),

View File

@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { LocalizedDatePipeModule } from '../localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from '../timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { EventPipe } from './event.pipe';
@NgModule({
declarations: [EventPipe],
imports: [CommonModule, TimestampToDatePipeModule, LocalizedDatePipeModule],
exports: [EventPipe],
})
export class EventPipeModule {}

View File

@ -0,0 +1,26 @@
import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Event } from 'src/app/proto/generated/zitadel/event_pb';
import { LocalizedDatePipe } from '../localized-date-pipe/localized-date.pipe';
import { TimestampToDatePipe } from '../timestamp-to-date-pipe/timestamp-to-date.pipe';
@Pipe({
name: 'event',
})
export class EventPipe implements PipeTransform {
constructor(private translateService: TranslateService) {}
public transform(event?: Event.AsObject): any {
if (event && event.editor?.displayName && event.creationDate) {
const timestampToDate = new TimestampToDatePipe().transform(event.creationDate);
const datePipeOutput = new LocalizedDatePipe(this.translateService).transform(timestampToDate);
return `${event.editor?.displayName} last changed it on ${datePipeOutput}`;
} else if (event && event.creationDate) {
const timestampToDate = new TimestampToDatePipe().transform(event.creationDate);
const datePipeOutput = new LocalizedDatePipe(this.translateService).transform(timestampToDate);
return `done on ${datePipeOutput}`;
} else {
return '';
}
}
}

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, catchError, finalize, from, map, Observable, of, Subject, switchMap, tap } from 'rxjs';
import {
ActivateLabelPolicyRequest,
@ -225,15 +226,84 @@ import {
UpdateSMTPConfigRequest,
UpdateSMTPConfigResponse,
} from '../proto/generated/zitadel/admin_pb';
import { Event } from '../proto/generated/zitadel/event_pb';
import { SearchQuery } from '../proto/generated/zitadel/member_pb';
import { ListQuery } from '../proto/generated/zitadel/object_pb';
import { GrpcService } from './grpc.service';
import { StorageLocation, StorageService } from './storage.service';
export interface OnboardingActions {
order: number;
eventType: string;
oneof: string[];
link: string | string[];
fragment?: string | undefined;
}
type OnboardingEvent = { order: number; link: string; fragment: string | undefined; event: Event.AsObject | undefined };
type OnboardingEventEntries = Array<[string, OnboardingEvent]> | [];
@Injectable({
providedIn: 'root',
})
export class AdminService {
constructor(private readonly grpcService: GrpcService) {}
public hideOnboarding: boolean = false;
public loadEvents: Subject<OnboardingActions[]> = new Subject();
public onboardingLoading: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public progressEvents$: Observable<OnboardingEventEntries> = this.loadEvents.pipe(
tap(() => this.onboardingLoading.next(true)),
switchMap((actions) => {
const searchForTypes = actions.map((oe) => oe.oneof).flat();
const eventsReq = new ListEventsRequest().setAsc(true).setEventTypesList(searchForTypes).setAsc(false);
return from(this.listEvents(eventsReq)).pipe(
map((events) => {
const el = events.toObject().eventsList.filter((e) => e.editor?.service !== 'System-API');
let obj: { [type: string]: OnboardingEvent } = {};
actions.map((action) => {
const filtered = el.filter((event) => event.type?.type && action.oneof.includes(event.type.type));
(obj as any)[action.eventType] = filtered.length
? { order: action.order, link: action.link, fragment: action.fragment, event: filtered[0] }
: { order: action.order, link: action.link, fragment: action.fragment, event: undefined };
});
const toArray = Object.entries(obj).sort(([key0, a], [key1, b]) => a.order - b.order);
const toDo = toArray.filter(([key, value]) => value.event === undefined);
const done = toArray.filter(([key, value]) => !!value.event);
return [...toDo, ...done];
}),
tap((events) => {
const total = events.length;
const done = events.map(([type, value]) => value.event !== undefined).filter((res) => !!res).length;
const percentage = Math.round((done / total) * 100);
this.progressDone.next(done);
this.progressTotal.next(total);
this.progressPercentage.next(percentage);
this.progressAllDone.next(done === total);
}),
catchError((error) => {
console.error(error);
return of([]);
}),
finalize(() => this.onboardingLoading.next(false)),
);
}),
);
public progressEvents: BehaviorSubject<OnboardingEventEntries> = new BehaviorSubject<OnboardingEventEntries>([]);
public progressPercentage: BehaviorSubject<number> = new BehaviorSubject(0);
public progressDone: BehaviorSubject<number> = new BehaviorSubject(0);
public progressTotal: BehaviorSubject<number> = new BehaviorSubject(0);
public progressAllDone: BehaviorSubject<boolean> = new BehaviorSubject(true);
constructor(private readonly grpcService: GrpcService, private storageService: StorageService) {
this.progressEvents$.subscribe(this.progressEvents);
this.hideOnboarding =
this.storageService.getItem('onboarding-dismissed', StorageLocation.local) === 'true' ? true : false;
}
public setDefaultOrg(orgId: string): Promise<SetDefaultOrgResponse.AsObject> {
const req = new SetDefaultOrgRequest();

View File

@ -0,0 +1,22 @@
import { OnboardingActions } from '../services/admin.service';
export const ONBOARDING_EVENTS: OnboardingActions[] = [
{
order: 0,
eventType: 'instance.policy.label.added',
oneof: ['instance.policy.label.added', 'instance.policy.label.changed'],
link: ['/settings'],
fragment: 'branding',
},
{ order: 1, eventType: 'project.added', oneof: ['project.added'], link: ['/projects/create'] },
{ order: 2, eventType: 'project.application.added', oneof: ['project.application.added'], link: ['/projects/app-create'] },
{ order: 3, eventType: 'user.human.added', oneof: ['user.human.added'], link: ['/users/create'] },
{
order: 4,
eventType: 'instance.smtp.config.added',
oneof: ['instance.smtp.config.added', 'instance.smtp.config.changed'],
link: ['/settings'],
fragment: 'notifications',
},
{ order: 5, eventType: 'user.grant.added', oneof: ['user.grant.added'], link: ['/grant-create'] },
];

View File

@ -42,6 +42,41 @@
"ADD": "Zum Hinzufügen Kachel halten und ziehen"
}
},
"ONBOARDING": {
"DESCRIPTION": "Dein Onboarding-prozess",
"COMPLETED": "abgeschlossen",
"DISMISS": "schließen",
"CARD": {
"TITLE": "Bringe deine Instanz zum Laufen",
"DESCRIPTION": "Diese Checkliste hilft bei der Einrichtung Ihrer Instanz und führt Sie durch die wichtigsten Schritte"
},
"EVENTS": {
"instance.policy.label.added": {
"title": "Branding anpassen",
"description": "Definiere Farben und Form des Login-UIs und uploade deine Logos und Icons."
},
"instance.smtp.config.added": {
"title": "SMTP Benachrichtigungseinstellungen",
"description": "Konfiguriere deinen Mailserver."
},
"project.added": {
"title": "Erstelle ein Projekt",
"description": "Erstelle dein erstes Projekt und definiere Rollen"
},
"project.application.added": {
"title": "Erstelle eine App",
"description": "Erstelle deine erste Web-, native, API oder SAML-applikation und konfiguriere den Authentification-flow."
},
"user.human.added": {
"title": "Erfasse Benutzer",
"description": "Erstelle Benutzer die später deine Apps nutzen können."
},
"user.grant.added": {
"title": "Berechtige Benutzer",
"description": "Erlaube es deinen Nutzern auf deine Apps zuzugreifen und gebe ihnen Rollen."
}
}
},
"MENU": {
"INSTANCE": "Instance",
"DASHBOARD": "Home",
@ -1725,6 +1760,8 @@
"ID": "ID",
"DESCRIPTION": "Hier kannst Du Deine Applikationen bearbeiten und deren Konfiguration anpassen.",
"CREATE": "Applikation erstellen",
"CREATE_SELECT_PROJECT": "Wähle zuerst dein Projekt aus",
"CREATE_NEW_PROJECT": "oder erstelle ein neues <a href='{{url}}' title='Create project'>hier</a>.",
"CREATE_DESC_TITLE": "Gebe die Daten der Anwendung Schritt für Schritt ein.",
"CREATE_DESC_SUB": "Es wird automatisch eine empfohlene Konfiguration generiert.",
"STATE": "Status",

View File

@ -42,6 +42,41 @@
"ADD": "Hold and drag a tile to add"
}
},
"ONBOARDING": {
"DESCRIPTION": "Your onboarding process",
"COMPLETED": "completed",
"DISMISS": "No thanks, I'm a pro.",
"CARD": {
"TITLE": "Get your ZITADEL running",
"DESCRIPTION": "This checklist helps to setup your instance and guides your through the most essential steps"
},
"EVENTS": {
"instance.policy.label.added": {
"title": "Setup your brand",
"description": "Define coloring and shape of your login and upload your logo and icons."
},
"instance.smtp.config.added": {
"title": "Setup your SMTP settings",
"description": "Set your own mail server settings."
},
"project.added": {
"title": "Create your first project",
"description": "Add your first project and define its roles and authorizations."
},
"project.application.added": {
"title": "Create your first application",
"description": "Create a web, native, api or saml application and setup your authentication flow."
},
"user.human.added": {
"title": "Add users",
"description": "Add your application users"
},
"user.grant.added": {
"title": "Grant users",
"description": "Allow users to access your application and setup their role."
}
}
},
"MENU": {
"INSTANCE": "Instance",
"DASHBOARD": "Home",
@ -124,6 +159,7 @@
"NEXT": "Next",
"MORE": "more",
"STEP": "Step",
"SETUP": "Setup",
"TABLE": {
"SHOWUSER": "Show user {{value}}"
}
@ -1725,6 +1761,8 @@
"ID": "ID",
"DESCRIPTION": "Here you can edit your application data and it's configuration.",
"CREATE": "Create application",
"CREATE_SELECT_PROJECT": "Select your project first",
"CREATE_NEW_PROJECT": "or create a new one <a href='{{url}}' title='Create project'>here</a>.",
"CREATE_DESC_TITLE": "Enter Your Application Details Step by Step",
"CREATE_DESC_SUB": "A recommended configuration will be automatically generated.",
"STATE": "Status",

View File

@ -42,6 +42,41 @@
"ADD": "Maintenir et faire glisser une tuile pour ajouter"
}
},
"ONBOARDING": {
"DESCRIPTION": "Votre processus d'intégration",
"COMPLETED": "terminé",
"DISMISS": "fermer",
"CARD": {
"TITLE": "Faites fonctionner votre ZITADEL",
"DESCRIPTION": "Cette liste de contrôle vous aide à configurer votre instance et vous guide à travers les étapes les plus essentielles."
},
"EVENTS": {
"instance.policy.label.added": {
"title": "Créez votre marque",
"description": "Définissez la couleur et la forme de votre connexion et téléchargez votre logo et vos icônes."
},
"instance.smtp.config.added": {
"title": "Configurez vos paramètres SMTP",
"description": "Définissez vos propres paramètres de serveur de messagerie"
},
"project.added": {
"title": "Créez votre premier projet",
"description": "Ajoutez votre premier projet et définissez ses rôles et autorisations."
},
"project.application.added": {
"title": "Créez votre première application",
"description": "Créez une application web, native, api ou saml et configurez votre flux d'authentification."
},
"user.human.added": {
"title": "Ajouter des utilisateurs",
"description": "Ajouter les utilisateurs de votre application"
},
"user.grant.added": {
"title": "Utilisateurs de subventions",
"description": "Autorisez les utilisateurs à accéder à votre application et définissez leur rôle."
}
}
},
"MENU": {
"INSTANCE": "Instance",
"DASHBOARD": "Accueil",
@ -1724,9 +1759,11 @@
"TITLE": "Application",
"ID": "ID",
"DESCRIPTION": "Ici vous pouvez modifier les données de votre application et sa configuration.",
"CREATE_OIDC": "Application OIDC",
"CREATE_OIDC_DESC_TITLE": "Entrez les détails de votre application étape par étape",
"CREATE_OIDC_DESC_SUB": "Une configuration recommandée sera automatiquement générée.",
"CREATE": "Application OIDC",
"CREATE_SELECT_PROJECT": "Sélectionnez d'abord votre projet",
"CREATE_NEW_PROJECT": "ou créez-en un nouveau <a href='{{url}}' title='Create project'>here</a>.",
"CREATE_DESC_TITLE": "Entrez les détails de votre application étape par étape",
"CREATE_DESC_SUB": "Une configuration recommandée sera automatiquement générée.",
"STATE": "Statut",
"DATECREATED": "Créé",
"DATECHANGED": "Modifié",

View File

@ -42,6 +42,41 @@
"ADD": "Per aggiungere, tieni premuto e trascina il riquadro"
}
},
"ONBOARDING": {
"DESCRIPTION": "Your onboarding process",
"COMPLETED": "completed",
"DISMISS": "chiudi",
"CARD": {
"TITLE": "Fate funzionare il vostro ZITADEL",
"DESCRIPTION": "Questa lista di azioni aiuta a configurare la vostra istanza e vi guida attraverso i passaggi più essenziali."
},
"EVENTS": {
"instance.policy.label.added": {
"title": "Imposta il tuo marchio",
"description": "Definisci la colorazione e il design del vostro login e caricate il vostro logo e le vostre icone."
},
"instance.smtp.config.added": {
"title": "Configura le impostazioni SMTP",
"description": "Imposta il proprio server di posta"
},
"project.added": {
"title": "Crea il tuo primo progetto",
"description": "Aggiungere il primo progetto e definire i ruoli e le autorizzazioni."
},
"project.application.added": {
"title": "Crea la tua prima applicazione",
"description": "Crea un'applicazione web, nativa, api o saml e imposta il flusso di autenticazione."
},
"user.human.added": {
"title": "Aggiungi utenti",
"description": "Aggiungi gli utenti dell'applicazione"
},
"user.grant.added": {
"title": "Crea autorizzazioni per gli utenti",
"description": "Consenti agli utenti di accedere alla tua applicazione e imposta il loro ruolo."
}
}
},
"MENU": {
"INSTANCE": "Istanza",
"DASHBOARD": "Pagina iniziale",
@ -1726,6 +1761,8 @@
"ID": "ID",
"DESCRIPTION": "Qui puoi modificare i dati della tua applicazione e la sua configurazione.",
"CREATE": "Crea Applicazione",
"CREATE_SELECT_PROJECT": "Seleziona il tuo progetto",
"CREATE_NEW_PROJECT": "o crea uno nuovo <a href='{{url}}' title='Crea progetto'>qui</a>.",
"CREATE_DESC_TITLE": "Inserisci i dettagli della tua applicazione passo dopo passo",
"CREATE_DESC_SUB": "Una configurazione raccomandata sar\u00e0 generata automaticamente.",
"STATE": "Stato",

View File

@ -42,6 +42,41 @@
"ADD": "Przytrzymaj i przeciągnij kafel, aby go dodać"
}
},
"ONBOARDING": {
"DESCRIPTION": "Twój proces wprowadzania na rynek",
"COMPLETED": "zakończone",
"DISMISS": "zamknąć",
"CARD": {
"TITLE": "Uruchom swój ZITADEL",
"DESCRIPTION": "Ta lista kontrolna pomoże Ci skonfigurować instancję i poprowadzi Cię przez najważniejsze kroki."
},
"EVENTS": {
"instance.policy.label.added": {
"title": "Skonfiguruj swoją markę",
"description": "Zdefiniuj kolorystykę i kształt swojego loginu oraz wgraj swoje logo i ikony."
},
"instance.smtp.config.added": {
"title": "Ustawienia SMTP",
"description": "Ustawienie własnego serwera pocztowego"
},
"project.added": {
"title": "Stwórz swój pierwszy projekt",
"description": "Dodaj swój pierwszy projekt i określ jego role i uprawnienia."
},
"project.application.added": {
"title": "Utwórz swoją pierwszą aplikację",
"description": "Utwórz aplikację internetową, natywną, api lub saml i skonfiguruj swój przepływ uwierzytelniania."
},
"user.human.added": {
"title": "Dodaj użytkowników",
"description": "Dodaj użytkowników aplikacji"
},
"user.grant.added": {
"title": "Użytkownicy dotacji",
"description": "Pozwól użytkownikom na dostęp do Twojej aplikacji i ustaw ich rolę."
}
}
},
"MENU": {
"INSTANCE": "Instancja",
"DASHBOARD": "Strona główna",
@ -1725,6 +1760,8 @@
"ID": "ID",
"DESCRIPTION": "Tutaj możesz edytować dane swojej aplikacji i jej konfigurację.",
"CREATE": "Utwórz aplikację",
"CREATE_SELECT_PROJECT": "Najpierw wybierz swój projekt",
"CREATE_NEW_PROJECT": "lub utworzyć nowy <a href='{{url}}' title='Utwórz projekt'>tutaj</a>.",
"CREATE_DESC_TITLE": "Wprowadź szczegóły swojej aplikacji krok po kroku",
"CREATE_DESC_SUB": "Automatycznie zostanie wygenerowana zalecana konfiguracja.",
"STATE": "Status",

View File

@ -42,6 +42,41 @@
"ADD": "按住并拖动来添加"
}
},
"ONBOARDING": {
"DESCRIPTION": "你的入职过程",
"COMPLETED": "已完成",
"DISMISS": "隐藏",
"CARD": {
"TITLE": "让你的ZITADEL运转起来",
"DESCRIPTION": "这份清单有助于设置你的实例,并指导你完成最重要的步骤"
},
"EVENTS": {
"instance.policy.label.added": {
"title": "设置你的品牌",
"description": "定义你的登录的颜色和形状,上传你的标志和图标。"
},
"instance.smtp.config.added": {
"title": "SMTP设置",
"description": "设置你自己的邮件服务器设置"
},
"project.added": {
"title": "创建你的第一个项目",
"description": "添加你的第一个项目并定义其角色和授权。"
},
"project.application.added": {
"title": "创建你的第一个应用程序",
"description": "创建一个web、native、api或saml应用程序并设置你的认证流程。"
},
"user.human.added": {
"title": "添加用户",
"description": "添加你的应用程序用户"
},
"user.grant.added": {
"title": "授予用户",
"description": "允许用户访问你的应用程序并设置他们的角色。"
}
}
},
"MENU": {
"INSTANCE": "实例",
"DASHBOARD": "首页",
@ -1723,9 +1758,11 @@
"TITLE": "应用",
"ID": "ID",
"DESCRIPTION": "在这里您可以编辑您的应用程序数据及其配置。",
"CREATE_OIDC": "OIDC 应用",
"CREATE_OIDC_DESC_TITLE": "逐步输入您的应用详情",
"CREATE_OIDC_DESC_SUB": "将自动生成推荐的配置。",
"CREATE": "OIDC 应用",
"CREATE_SELECT_PROJECT": "首先选择你的项目",
"CREATE_NEW_PROJECT": "或创建一个新的<a href='{url}}' title='创建项目'>这里</a>。",
"CREATE_DESC_TITLE": "逐步输入您的应用详情",
"CREATE_DESC_SUB": "将自动生成推荐的配置。",
"STATE": "状态",
"DATECREATED": "创建于",
"DATECHANGED": "修改于",

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1Z" /></svg>

After

Width:  |  Height:  |  Size: 198 B

View File

@ -44,6 +44,8 @@
@import 'src/app/modules/meta-layout/meta.scss';
@import 'src/app/pages/projects/owned-projects/project-grant-detail/project-grant-illustration/project-grant-illustration.component';
@import 'src/app/modules/accounts-card/accounts-card.component.scss';
@import 'src/app/modules/onboarding-card/onboarding-card.component.scss';
@import 'src/app/modules/onboarding/onboarding.component.scss';
@import 'src/app/modules/filter/filter.component.scss';
@import 'src/app/modules/policies/message-texts/message-texts.component.scss';
@import 'src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.scss';
@ -110,6 +112,8 @@
@include toast-theme($theme);
@include keyboard-shortcuts-theme($theme);
@include project-grant-illustration-theme($theme);
@include onboarding-card-theme($theme);
@include onboarding-theme($theme);
@include refresh-table-theme($theme);
@include accounts-card-theme($theme);
@include sidenav-theme($theme);