mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-26 01:02:16 +00:00
feat(console): onboarding flow (#5225)
Implements an onboarding UI for users
This commit is contained in:
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user