mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-06 14:37:41 +00:00
feat(console): onboarding flow (#5225)
Implements an onboarding UI for users
This commit is contained in:
parent
a7cc907ab7
commit
f8ddc844f8
@ -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'),
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 {}
|
57
console/src/app/modules/onboarding/onboarding.component.html
Normal file
57
console/src/app/modules/onboarding/onboarding.component.html
Normal 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>
|
174
console/src/app/modules/onboarding/onboarding.component.scss
Normal file
174
console/src/app/modules/onboarding/onboarding.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
16
console/src/app/modules/onboarding/onboarding.component.ts
Normal file
16
console/src/app/modules/onboarding/onboarding.component.ts
Normal 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);
|
||||
}
|
||||
}
|
31
console/src/app/modules/onboarding/onboarding.module.ts
Normal file
31
console/src/app/modules/onboarding/onboarding.module.ts
Normal 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 {}
|
@ -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;
|
||||
|
@ -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 {}
|
26
console/src/app/pages/app-create/app-create.component.html
Normal file
26
console/src/app/pages/app-create/app-create.component.html
Normal 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>
|
25
console/src/app/pages/app-create/app-create.component.scss
Normal file
25
console/src/app/pages/app-create/app-create.component.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
45
console/src/app/pages/app-create/app-create.component.ts
Normal file
45
console/src/app/pages/app-create/app-create.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
43
console/src/app/pages/app-create/app-create.module.ts
Normal file
43
console/src/app/pages/app-create/app-create.module.ts
Normal 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 {}
|
@ -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>
|
||||
|
||||
|
@ -125,6 +125,7 @@
|
||||
font-size: 14px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 5rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
|
@ -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,
|
||||
],
|
||||
})
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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'),
|
||||
|
12
console/src/app/pipes/event-pipe/event-pipe.module.ts
Normal file
12
console/src/app/pipes/event-pipe/event-pipe.module.ts
Normal 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 {}
|
26
console/src/app/pipes/event-pipe/event.pipe.ts
Normal file
26
console/src/app/pipes/event-pipe/event.pipe.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
22
console/src/app/utils/onboarding.ts
Normal file
22
console/src/app/utils/onboarding.ts
Normal 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'] },
|
||||
];
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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é",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": "修改于",
|
||||
|
1
console/src/assets/mdi/shield-check.svg
Normal file
1
console/src/assets/mdi/shield-check.svg
Normal 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 |
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user