feat: rehauled console (#3525)

* new console

* move npm ci to angular build

* rel path for assets

* local grpc copy

* login policy, rm clear views, features rel path

* lock

Co-authored-by: Livio Amstutz <[email protected]>
This commit is contained in:
Max Peintner
2022-04-28 10:35:02 +00:00
committed by GitHub
co-authored by Livio Amstutz
parent 00f7dbe875
commit 08ae39ae19
668 changed files with 47746 additions and 19117 deletions
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -84,8 +84,8 @@ export class AssetService {
.get('./assets/environment.json')
.toPromise()
.then((data: any) => {
if (data && data.api) {
return data.api;
if (data && data.assetServiceUrl) {
return data.assetServiceUrl;
}
})
.catch((error) => {
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
import { BehaviorSubject, from, Observable } from 'rxjs';
import { StatehandlerService } from './statehandler.service';
import { StatehandlerService } from './statehandler/statehandler.service';
@Injectable({
providedIn: 'root',
@@ -10,14 +10,9 @@ import { StatehandlerService } from './statehandler.service';
export class AuthenticationService {
private authConfig!: AuthConfig;
private _authenticated: boolean = false;
private readonly _authenticationChanged: BehaviorSubject<
boolean
> = new BehaviorSubject(this.authenticated);
private readonly _authenticationChanged: BehaviorSubject<boolean> = new BehaviorSubject(this.authenticated);
constructor(
private oauthService: OAuthService,
private statehandler: StatehandlerService,
) { }
constructor(private oauthService: OAuthService, private statehandler: StatehandlerService) {}
public initConfig(data: AuthConfig): void {
this.authConfig = data;
@@ -35,10 +30,7 @@ export class AuthenticationService {
return from(this.oauthService.loadUserProfile());
}
public async authenticate(
partialConfig?: Partial<AuthConfig>,
force: boolean = false,
): Promise<boolean> {
public async authenticate(partialConfig?: Partial<AuthConfig>, force: boolean = false): Promise<boolean> {
if (partialConfig) {
Object.assign(this.authConfig, partialConfig);
}
@@ -63,4 +55,3 @@ export class AuthenticationService {
this._authenticationChanged.next(false);
}
}
@@ -0,0 +1,71 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, map } from 'rxjs';
import { ManagementService } from './mgmt.service';
export enum BreadcrumbType {
IAM,
ORG,
PROJECT,
GRANTEDPROJECT,
PROJECTGRANT,
APP,
IDP,
AUTHUSER,
}
export class Breadcrumb {
type: BreadcrumbType = BreadcrumbType.PROJECT;
name?: string = '';
param?: {
key: 'projectid' | 'appid' | 'grantid' | 'id';
value: string;
} = {
key: 'projectid',
value: '',
};
routerLink: any[] = [];
isZitadel?: boolean = false;
hideNav?: boolean = false;
constructor(init: Partial<Breadcrumb>) {
Object.assign(this, init);
}
}
@Injectable({
providedIn: 'root',
})
export class BreadcrumbService {
public readonly breadcrumbs$: BehaviorSubject<Breadcrumb[]> = new BehaviorSubject<Breadcrumb[]>([]);
public readonly breadcrumbsExtended$ = combineLatest([
this.breadcrumbs$,
this.mgmtService.ownedProjects,
this.mgmtService.grantedProjects,
]).pipe(
map(([breadcrumbs, projects, grantedProjects]) => {
const newValues = breadcrumbs.map((b) => {
if (!b.name && b.type === BreadcrumbType.PROJECT) {
const project = projects.find((project) => b.param && project.id === b.param.value);
b.name = project?.name ?? '';
return b;
} else if (!b.name && b.type === BreadcrumbType.GRANTEDPROJECT) {
const grantedproject = grantedProjects.find(
(grantedproject) => b.param && grantedproject.projectId === b.param.value,
);
b.name = grantedproject?.projectName ?? '';
return b;
} else {
return b;
}
});
return newValues;
}),
);
constructor(private mgmtService: ManagementService) {}
public setBreadcrumb(breadcrumbs: Breadcrumb[]) {
this.breadcrumbs$.next(breadcrumbs);
}
}
@@ -167,7 +167,7 @@ export class GrpcAuthService {
} else {
let orgs = this.cachedOrgs;
if (orgs.length === 0) {
orgs = (await this.listMyProjectOrgs(10, 0)).resultList;
orgs = (await this.listMyProjectOrgs()).resultList;
this.cachedOrgs = orgs;
}
@@ -310,22 +310,24 @@ export class GrpcAuthService {
}
public listMyProjectOrgs(
limit: number,
offset: number,
limit?: number,
offset?: number,
queryList?: OrgQuery[],
): Promise<ListMyProjectOrgsResponse.AsObject> {
const req = new ListMyProjectOrgsRequest();
const metadata = new ListQuery();
const query = new ListQuery();
if (offset) {
metadata.setOffset(offset);
query.setOffset(offset);
}
if (limit) {
metadata.setLimit(limit);
query.setLimit(limit);
}
if (queryList) {
req.setQueriesList(queryList);
}
req.setQuery(query);
return this.grpcService.auth.listMyProjectOrgs(req, null).then((resp) => resp.toObject());
}
@@ -0,0 +1,147 @@
import { Injectable, OnDestroy, Renderer2, RendererFactory2 } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { BehaviorSubject, Observable, of, pairwise, Subject, takeUntil } from 'rxjs';
import { KeyboardShortcutsComponent } from '../../modules/keyboard-shortcuts/keyboard-shortcuts.component';
import { GrpcAuthService } from '../grpc-auth.service';
import {
ACTIONS,
DOMAINS,
HOME,
KeyboardShortcut,
ME,
ORG,
PROJECTS,
SYSTEM,
USERGRANTS,
USERS,
} from './keyboard-shortcuts';
@Injectable({
providedIn: 'root',
})
export class KeyboardShortcutsService implements OnDestroy {
private renderer: Renderer2;
private keyPressed: BehaviorSubject<any> = new BehaviorSubject<any>(null);
private destroy$: Subject<void> = new Subject();
constructor(
private rendererFactory2: RendererFactory2,
private dialog: MatDialog,
private router: Router,
private authService: GrpcAuthService,
) {
this.renderer = this.rendererFactory2.createRenderer(null, null);
this.renderer.listen('document', 'keydown', (event) => {
this.keyPressed.next(event);
});
this.keyPressed.pipe(pairwise(), takeUntil(this.destroy$)).subscribe(([firstKey, secondKey]) => {
const firstTagname = firstKey?.target?.tagName;
const secondTagname = secondKey?.target?.tagName;
const exclude = ['input', 'textarea'];
if (
firstKey &&
secondKey &&
exclude.indexOf(firstTagname?.toLowerCase()) === -1 &&
exclude.indexOf(secondTagname?.toLowerCase()) === -1
) {
if (secondKey.key === '?') {
this.openOverviewDialog();
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyH') {
if (this.hasPermission(HOME) && this.hasFeature(HOME)) {
this.router.navigate(HOME.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyS') {
if (this.hasPermission(SYSTEM) && this.hasFeature(SYSTEM)) {
this.router.navigate(SYSTEM.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyO') {
if (this.hasPermission(ORG) && this.hasFeature(ORG)) {
this.router.navigate(ORG.link);
}
}
if (firstKey.code === 'KeyM' && secondKey.code === 'KeyE') {
if (this.hasPermission(ME) && this.hasFeature(ME)) {
this.router.navigate(ME.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyP') {
if (this.hasPermission(PROJECTS) && this.hasFeature(PROJECTS)) {
this.router.navigate(PROJECTS.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyU') {
if (this.hasPermission(USERS) && this.hasFeature(USERS)) {
this.router.navigate(USERS.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyA') {
if (this.hasPermission(USERGRANTS) && this.hasFeature(USERGRANTS)) {
this.router.navigate(USERGRANTS.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyF') {
if (this.hasPermission(ACTIONS) && this.hasFeature(ACTIONS)) {
this.router.navigate(ACTIONS.link);
}
}
if (firstKey.code === 'KeyG' && secondKey.code === 'KeyD') {
if (this.hasPermission(DOMAINS) && this.hasFeature(DOMAINS)) {
this.router.navigate(DOMAINS.link);
}
}
// if (secondKey.shiftKey && (secondKey.code === 'Digit7' || secondKey.key==='/')) {
// }
// SIDEWIDESHORTCUTS.f
} else if (secondKey && exclude.indexOf(secondTagname?.toLowerCase()) === -1) {
if (secondKey.key === '?') {
this.openOverviewDialog();
}
}
});
}
public ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
public openOverviewDialog(): void {
const dialogRef = this.dialog.open(KeyboardShortcutsComponent, {
data: {},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
}
});
}
private hasPermission(shortcut: KeyboardShortcut): Observable<boolean> {
if (shortcut.permissions) {
return this.authService.isAllowed(shortcut.permissions);
} else {
return of(true);
}
}
private hasFeature(shortcut: KeyboardShortcut): Observable<boolean> {
if (shortcut.features) {
return this.authService.canUseFeature(shortcut.features);
} else {
return of(true);
}
}
}
@@ -0,0 +1,79 @@
export interface KeyboardShortcut {
keyboardKeys: string[];
link: any[];
i18nKey: string;
features?: string[] | RegExp[];
permissions?: string[] | RegExp[];
}
export const HOME: KeyboardShortcut = {
i18nKey: 'KEYBOARDSHORTCUTS.SHORTCUTS.HOME',
link: ['/'],
keyboardKeys: ['g', 'h'],
};
export const SYSTEM: KeyboardShortcut = {
i18nKey: 'KEYBOARDSHORTCUTS.SHORTCUTS.SYSTEM',
link: ['/system'],
keyboardKeys: ['g', 's'],
permissions: ['iam.read'],
};
export const ORG: KeyboardShortcut = {
i18nKey: 'KEYBOARDSHORTCUTS.SHORTCUTS.ORG',
link: ['/org'],
keyboardKeys: ['g', 'o'],
permissions: ['org.read'],
};
export const ME: KeyboardShortcut = {
i18nKey: 'KEYBOARDSHORTCUTS.SHORTCUTS.ME',
link: ['/users/me'],
keyboardKeys: ['m', 'e'],
};
export const PROJECTS: KeyboardShortcut = {
i18nKey: 'KEYBOARDSHORTCUTS.SHORTCUTS.PROJECTS',
link: ['/projects'],
keyboardKeys: ['g', 'p'],
permissions: ['project.read'],
};
export const USERS: KeyboardShortcut = {
i18nKey: 'KEYBOARDSHORTCUTS.SHORTCUTS.USERS',
link: ['/users'],
keyboardKeys: ['g', 'u'],
permissions: ['user.read'],
};
export const USERGRANTS: KeyboardShortcut = {
i18nKey: 'KEYBOARDSHORTCUTS.SHORTCUTS.USERGRANTS',
link: ['/grants'],
keyboardKeys: ['g', 'a'],
permissions: ['usergrant.read'],
};
export const ACTIONS: KeyboardShortcut = {
i18nKey: 'KEYBOARDSHORTCUTS.SHORTCUTS.ACTIONS',
link: ['/actions'],
keyboardKeys: ['g', 'f'],
features: ['actions'],
permissions: ['org.action.read'],
};
export const DOMAINS: KeyboardShortcut = {
i18nKey: 'KEYBOARDSHORTCUTS.SHORTCUTS.DOMAINS',
link: ['/domains'],
keyboardKeys: ['g', 'd'],
permissions: ['org.read'],
};
export const SIDEWIDESHORTCUTS = [ME, HOME, SYSTEM, ORG, PROJECTS, USERS, USERGRANTS, ACTIONS, DOMAINS];
export const ORGSWITCHER: KeyboardShortcut = {
i18nKey: 'KEYBOARDSHORTCUTS.SHORTCUTS.ORGSWITCHER',
link: ['/org'],
keyboardKeys: ['/'],
};
export const ORGSHORTCUTS = [ORGSWITCHER];
+45 -6
View File
@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
import { Empty } from 'google-protobuf/google/protobuf/empty_pb';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject } from 'rxjs';
@@ -191,6 +192,8 @@ import {
ListAppKeysResponse,
ListAppsRequest,
ListAppsResponse,
ListGrantedProjectRolesRequest,
ListGrantedProjectRolesResponse,
ListGrantedProjectsRequest,
ListGrantedProjectsResponse,
ListHumanAuthFactorsRequest,
@@ -432,7 +435,7 @@ import { MetadataQuery } from '../proto/generated/zitadel/metadata_pb';
import { ListQuery } from '../proto/generated/zitadel/object_pb';
import { DomainSearchQuery, DomainValidationType } from '../proto/generated/zitadel/org_pb';
import { PasswordComplexityPolicy } from '../proto/generated/zitadel/policy_pb';
import { ProjectQuery, RoleQuery } from '../proto/generated/zitadel/project_pb';
import { GrantedProject, Project, ProjectQuery, RoleQuery } from '../proto/generated/zitadel/project_pb';
import {
Gender,
MembershipQuery,
@@ -448,7 +451,9 @@ export type ResponseMapper<TResp, TMappedResp> = (resp: TResp) => TMappedResp;
providedIn: 'root',
})
export class ManagementService {
public ownedProjects: BehaviorSubject<Project.AsObject[]> = new BehaviorSubject<Project.AsObject[]>([]);
public ownedProjectsCount: BehaviorSubject<number> = new BehaviorSubject(0);
public grantedProjects: BehaviorSubject<GrantedProject.AsObject[]> = new BehaviorSubject<GrantedProject.AsObject[]>([]);
public grantedProjectsCount: BehaviorSubject<number> = new BehaviorSubject(0);
constructor(private readonly grpcService: GrpcService) {}
@@ -634,14 +639,15 @@ export class ManagementService {
public listOrgIDPs(limit?: number, offset?: number, queryList?: IDPQuery[]): Promise<ListOrgIDPsResponse.AsObject> {
const req = new ListOrgIDPsRequest();
const metadata = new ListQuery();
const query = new ListQuery();
if (limit) {
metadata.setLimit(limit);
query.setLimit(limit);
}
if (offset) {
metadata.setOffset(offset);
query.setOffset(offset);
}
req.setQuery(query);
if (queryList) {
req.setQueriesList(queryList);
}
@@ -1633,6 +1639,7 @@ export class ManagementService {
offset: number,
queriesList?: UserSearchQuery[],
sortingColumn?: UserFieldName,
sortingDirection?: SortDirection,
): Promise<ListUsersResponse.AsObject> {
const req = new ListUsersRequest();
const query = new ListQuery();
@@ -1642,10 +1649,14 @@ export class ManagementService {
if (offset) {
query.setOffset(offset);
}
if (sortingDirection) {
query.setAsc(sortingDirection === 'asc');
}
req.setQuery(query);
if (sortingColumn) {
req.setSortingColumn(sortingColumn);
}
if (queriesList) {
req.setQueriesList(queriesList);
}
@@ -1811,6 +1822,7 @@ export class ManagementService {
const obj = value.toObject();
const count = obj.resultList.length;
if (count >= 0) {
this.ownedProjects.next(obj.resultList);
this.ownedProjectsCount.next(count);
}
@@ -1819,8 +1831,8 @@ export class ManagementService {
}
public listGrantedProjects(
limit: number,
offset: number,
limit?: number,
offset?: number,
queryList?: ProjectQuery[],
): Promise<ListGrantedProjectsResponse.AsObject> {
const req = new ListGrantedProjectsRequest();
@@ -1839,6 +1851,7 @@ export class ManagementService {
}
return this.grpcService.mgmt.listGrantedProjects(req, null).then((value) => {
const obj = value.toObject();
this.grantedProjects.next(obj.resultList);
this.grantedProjectsCount.next(obj.resultList.length);
return obj;
});
@@ -2110,6 +2123,32 @@ export class ManagementService {
return this.grpcService.mgmt.listProjectRoles(req, null).then((resp) => resp.toObject());
}
public listGrantedProjectRoles(
projectId: string,
grantId: string,
limit?: number,
offset?: number,
queryList?: RoleQuery[],
): Promise<ListGrantedProjectRolesResponse.AsObject> {
const req = new ListGrantedProjectRolesRequest();
req.setProjectId(projectId);
req.setGrantId(grantId);
const query = new ListQuery();
if (limit) {
query.setLimit(limit);
}
if (offset) {
query.setOffset(offset);
}
req.setQuery(query);
if (queryList) {
req.setQueriesList(queryList);
}
return this.grpcService.mgmt.listGrantedProjectRoles(req, null).then((resp) => resp.toObject());
}
public bulkAddProjectRoles(
projectId: string,
rolesList: BulkAddProjectRolesRequest.Role[],
@@ -0,0 +1,32 @@
import { Location } from '@angular/common';
import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class NavigationService {
private history: string[] = [];
constructor(private router: Router, private location: Location) {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.history.push(event.urlAfterRedirects);
}
});
}
public back(): void {
if (this.isBackPossible) {
this.location.back();
this.history.pop();
} else {
this.router.navigateByUrl('/');
this.history.pop();
}
}
public get isBackPossible(): boolean {
return this.history.length > 1;
}
}
@@ -0,0 +1,9 @@
import { OverlayRef } from '@angular/cdk/overlay';
export class CnslOverlayRef {
constructor(private overlayRef: OverlayRef) {}
close(): void {
this.overlayRef.dispose();
}
}
@@ -0,0 +1,200 @@
import { MediaMatcher } from '@angular/cdk/layout';
import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, of, Subject, switchMap, take, takeUntil, zip } from 'rxjs';
import { GrpcAuthService } from '../grpc-auth.service';
import { CnslOverlayRef } from './overlay-ref';
import { OverlayService } from './overlay.service';
export interface OverlayWorkflow {
currentIndex: number;
currentRef?: CnslOverlayRef;
overlays: CnslOverlay[];
}
export interface CnslOverlay {
id: string;
origin: string;
toHighlight: string[];
content: {
count?: number;
number?: number;
i18nText: string;
};
requirements?: {
media?: string;
permission?: string[];
feature?: string[];
};
}
@Injectable({
providedIn: 'root',
})
export class OverlayWorkflowService implements OnDestroy {
public readonly currentWorkflow$: BehaviorSubject<OverlayWorkflow | null> = new BehaviorSubject<OverlayWorkflow | null>(
null,
);
public destroy$: Subject<void> = new Subject();
public openRef: CnslOverlayRef | null = null;
public highlightedIds: { [id: string]: number } = {};
public callback: Function | null = null;
constructor(private mediaMatcher: MediaMatcher, overlayService: OverlayService, private authService: GrpcAuthService) {
this.currentWorkflow$.pipe(takeUntil(this.destroy$)).subscribe((workflow) => {
if (this.openRef) {
this.openRef.close();
this.openRef = null;
}
Object.keys(this.highlightedIds).forEach((id) => {
const element = document.getElementById(id);
if (element) {
element.style.removeProperty('z-index');
delete this.highlightedIds[id];
}
});
const overlay = workflow?.overlays[workflow.currentIndex];
if (overlay) {
this.meetsRequirements(overlay)
.pipe(take(1))
.subscribe((can) => {
if (can) {
this.openRef = overlayService.open(overlay);
overlay.toHighlight.forEach((id) => {
const element = document.getElementById(id);
if (element) {
const oldZ = element.style.zIndex;
this.highlightedIds[id] = Number(oldZ);
element.style.zIndex = '1001';
}
});
} else {
this.nextStep();
}
});
}
});
}
public meetsRequirements(overlay: CnslOverlay): Observable<boolean> {
return zip([
this.meetsMediaRequirements(overlay),
this.meetsPermissionRequirements(overlay),
this.meetsFeatureRequirements(overlay),
] as Observable<boolean>[]).pipe(
switchMap(([media, permission, feature]) => {
return of(media && permission && feature);
}),
);
}
public meetsMediaRequirements(overlay: CnslOverlay): Observable<boolean> {
const media = overlay.requirements?.media;
if (media) {
const matches = this.mediaMatcher.matchMedia(media).matches;
if (!matches) {
return of(false);
} else {
return of(true);
}
} else {
return of(true);
}
}
public meetsPermissionRequirements(overlay: CnslOverlay): Observable<boolean> {
const regexArray = overlay.requirements?.permission;
if (regexArray && regexArray.length) {
return this.authService.isAllowed(regexArray);
} else {
return of(true);
}
}
public meetsFeatureRequirements(overlay: CnslOverlay): Observable<boolean> {
const regexArray = overlay.requirements?.feature;
if (regexArray && regexArray.length) {
return this.authService.canUseFeature(regexArray);
} else {
return of(true);
}
}
public reset(): void {
this.currentWorkflow$.next(null);
if (this.callback) {
this.callback();
}
}
public ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
public startWorkflow(overlays: CnslOverlay[], cb: Function | null): void {
if (cb) {
this.callback = cb;
}
this.currentWorkflow$.next({ overlays, currentIndex: 0 });
}
public nextStep(): void {
const currentWorkflow = this.currentWorkflow$.value;
if (this.nextPossible && currentWorkflow) {
const nextIndex = currentWorkflow?.currentIndex + 1;
this.currentWorkflow$.next({ ...currentWorkflow, currentIndex: nextIndex });
} else {
this.currentWorkflow$.next(null);
if (this.callback) {
this.callback();
}
}
}
public previousStep(): void {
const currentWorkflow = this.currentWorkflow$.value;
if (this.previousPossible && currentWorkflow) {
const nextIndex = currentWorkflow?.currentIndex - 1;
this.currentWorkflow$.next({ ...currentWorkflow, currentIndex: nextIndex });
}
}
public get nextPossible(): boolean {
const currentWorkflow = this.currentWorkflow$.value;
if (currentWorkflow) {
const nextIndex = currentWorkflow?.currentIndex + 1;
if (nextIndex < currentWorkflow?.overlays.length) {
return true;
}
}
return false;
}
public get isLast(): boolean {
const currentWorkflow = this.currentWorkflow$.value;
if (currentWorkflow) {
const currentIndex = currentWorkflow?.currentIndex;
if (currentIndex == currentWorkflow?.overlays.length - 1) {
return true;
}
}
return false;
}
public get previousPossible(): boolean {
const currentWorkflow = this.currentWorkflow$.value;
if (currentWorkflow && currentWorkflow.currentIndex > 0) {
return true;
} else {
return false;
}
}
}
@@ -0,0 +1,93 @@
import { ConnectedPosition, ConnectionPositionPair, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ComponentRef, Injectable, Injector } from '@angular/core';
import { InfoOverlayComponent, OVERLAY_DATA } from '../../modules/info-overlay/info-overlay.component';
import { CnslOverlayRef } from './overlay-ref';
import { CnslOverlay } from './overlay-workflow.service';
interface InfoOverlayConfig extends CnslOverlay {
backdropClass?: string;
panelClass?: string;
}
const DEFAULT_CONFIG: Partial<InfoOverlayConfig> = {
backdropClass: 'cnsl-overlay-backdrop',
panelClass: 'cnsl-overlay-panel',
};
@Injectable({
providedIn: 'root',
})
export class OverlayService {
constructor(private overlay: Overlay, private injector: Injector) {}
public open(overlay: CnslOverlay) {
const dialogConfig: InfoOverlayConfig = { ...DEFAULT_CONFIG, ...overlay };
const overlayRef = this.createOverlay(dialogConfig);
const dialogRef = new CnslOverlayRef(overlayRef);
const overlayComponent = this.attachOverlayContainer(overlayRef, dialogConfig, overlayRef);
overlayRef.backdropClick().subscribe((_) => dialogRef.close());
return dialogRef;
}
private attachOverlayContainer(overlayRef: OverlayRef, config: InfoOverlayConfig, dialogRef: OverlayRef) {
const injector = this.createInjector(config, dialogRef);
const containerPortal = new ComponentPortal(InfoOverlayComponent, null, injector);
const containerRef: ComponentRef<InfoOverlayComponent> = overlayRef.attach(containerPortal);
return containerRef.instance;
}
private createInjector(config: InfoOverlayConfig, dialogRef: OverlayRef): PortalInjector {
const injectionTokens = new WeakMap();
injectionTokens.set(OverlayRef, dialogRef);
injectionTokens.set(OVERLAY_DATA, config.content);
return new PortalInjector(this.injector, injectionTokens);
}
private createOverlay(config: InfoOverlayConfig): OverlayRef {
const overlayConfig = this.getOverlayConfig(config);
return this.overlay.create(overlayConfig);
}
private getOverlayConfig(config: InfoOverlayConfig): OverlayConfig {
// const positionStrategy = this.overlay.position().global().centerHorizontally().centerVertically();
const htmlOrigin: HTMLElement | null = document.getElementById(config.origin);
const 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),
];
let positionStrategy;
if (htmlOrigin) {
positionStrategy = this.overlay
.position()
.flexibleConnectedTo(htmlOrigin)
.withPositions(positions)
.withFlexibleDimensions(true)
.withPush(false);
} else {
positionStrategy = this.overlay.position().global().centerHorizontally().centerVertically();
}
const overlayConfig = new OverlayConfig({
hasBackdrop: true,
backdropClass: config.backdropClass,
panelClass: config.panelClass,
scrollStrategy: this.overlay.scrollStrategies.block(),
positionStrategy,
});
return overlayConfig;
}
}
@@ -0,0 +1,68 @@
import { CnslOverlay } from './overlay-workflow.service';
export const IntroWorkflowOverlays: CnslOverlay[] = [
{
id: 'orgswitcher',
origin: 'orgswitchbutton',
toHighlight: ['orgswitchbutton', 'orglink'],
content: {
number: 1,
count: 4,
i18nText: 'OVERLAYS.ORGSWITCHER.TEXT',
},
requirements: {
permission: ['org.read'],
},
},
{
id: 'systembutton',
origin: 'systembutton',
toHighlight: ['systembutton'],
content: {
number: 2,
count: 4,
i18nText: 'OVERLAYS.SYSTEM.TEXT',
},
requirements: {
media: '(min-width: 600px)',
permission: ['iam.read'],
},
},
{
id: 'profilebutton',
origin: 'avatartoggle',
toHighlight: ['avatartoggle'],
content: {
number: 3,
count: 4,
i18nText: 'OVERLAYS.PROFILE.TEXT',
},
},
{
id: 'mainnav',
origin: 'mainnav',
toHighlight: ['mainnav'],
content: {
number: 4,
count: 4,
i18nText: 'OVERLAYS.NAV.TEXT',
},
requirements: {
permission: ['org.read'],
},
},
];
export const OrgContextChangedWorkflowOverlays: CnslOverlay[] = [
{
id: 'orgswitcher',
origin: 'orgswitchbutton',
toHighlight: ['orgswitchbutton', 'orglink'],
content: {
i18nText: 'OVERLAYS.CONTEXTCHANGED.TEXT',
},
requirements: {
permission: ['org.read'],
},
},
];
@@ -20,24 +20,28 @@ export interface StripeCustomer {
providedIn: 'root',
})
export class SubscriptionService {
constructor(private http: HttpClient, private storageService: StorageService) { }
constructor(private http: HttpClient, private storageService: StorageService) {}
public getLink(orgId: string, redirectURI: string): Promise<any> {
return this.http.get('./assets/environment.json')
.toPromise().then((data: any) => {
return this.http
.get('./assets/environment.json')
.toPromise()
.then((data: any) => {
if (data && data.subscriptionServiceUrl) {
const serviceUrl = data.api;
const serviceUrl = data.subscriptionServiceUrl;
const accessToken = this.storageService.getItem(accessTokenStorageKey);
return this.http.get(`${serviceUrl}/redirect`, {
headers: {
[authorizationKey]: `${bearerPrefix} ${accessToken}`,
},
params: {
'org': orgId,
'return_url': encodeURI(redirectURI),
'country': 'ch',
},
}).toPromise();
return this.http
.get(`${serviceUrl}/redirect`, {
headers: {
[authorizationKey]: `${bearerPrefix} ${accessToken}`,
},
params: {
org: orgId,
return_url: encodeURI(redirectURI),
country: 'ch',
},
})
.toPromise();
} else {
return Promise.reject('Could not load environment');
}
@@ -45,19 +49,23 @@ export class SubscriptionService {
}
public getCustomer(orgId: string): Promise<any> {
return this.http.get('./assets/environment.json')
.toPromise().then((data: any) => {
return this.http
.get('../../assets/environment.json')
.toPromise()
.then((data: any) => {
if (data && data.subscriptionServiceUrl) {
const serviceUrl = data.api;
const serviceUrl = data.subscriptionServiceUrl;
const accessToken = this.storageService.getItem(accessTokenStorageKey);
return this.http.get(`${serviceUrl}/customer`, {
headers: {
[authorizationKey]: `${bearerPrefix} ${accessToken}`,
},
params: {
'org': orgId,
},
}).toPromise();
return this.http
.get(`${serviceUrl}/customer`, {
headers: {
[authorizationKey]: `${bearerPrefix} ${accessToken}`,
},
params: {
org: orgId,
},
})
.toPromise();
} else {
return Promise.reject('Could not load environment');
}
@@ -65,19 +73,23 @@ export class SubscriptionService {
}
public setCustomer(orgId: string, body: StripeCustomer): Promise<any> {
return this.http.get('./assets/environment.json')
.toPromise().then((data: any) => {
return this.http
.get('./assets/environment.json')
.toPromise()
.then((data: any) => {
if (data && data.subscriptionServiceUrl) {
const serviceUrl = data.api;
const serviceUrl = data.subscriptionServiceUrl;
const accessToken = this.storageService.getItem(accessTokenStorageKey);
return this.http.post(`${serviceUrl}/customer`, body, {
headers: {
[authorizationKey]: `${bearerPrefix} ${accessToken}`,
},
params: {
'org': orgId,
},
}).toPromise();
return this.http
.post(`${serviceUrl}/customer`, body, {
headers: {
[authorizationKey]: `${bearerPrefix} ${accessToken}`,
},
params: {
org: orgId,
},
})
.toPromise();
} else {
return Promise.reject('Could not load environment');
}
+28 -8
View File
@@ -1,23 +1,35 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';
declare const tinycolor: any;
export interface Color {
name: string;
hex: string;
darkContrast: boolean;
rgb: string;
contrastColor: string;
}
@Injectable()
export class ThemeService {
private _darkTheme: Subject<boolean> = new Subject<boolean>();
private _darkTheme: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
public isDarkTheme: Observable<boolean> = this._darkTheme.asObservable();
private primaryColorPalette: Color[] = [];
private warnColorPalette: Color[] = [];
private backgroundColorPalette: Color[] = [];
constructor() {
const theme = localStorage.getItem('theme');
if (theme) {
if (theme === 'light-theme') {
this.setDarkTheme(false);
} else {
this.setDarkTheme(true);
}
}
}
setDarkTheme(isDarkTheme: boolean): void {
this._darkTheme.next(isDarkTheme);
}
@@ -25,10 +37,7 @@ export class ThemeService {
public updateTheme(colors: Color[], type: string, theme: string): void {
colors.forEach((color) => {
document.documentElement.style.setProperty(`--theme-${theme}-${type}-${color.name}`, color.hex);
document.documentElement.style.setProperty(
`--theme-${theme}-${type}-contrast-${color.name}`,
color.darkContrast ? 'hsla(0, 0%, 0%, 0.87)' : '#ffffff',
);
document.documentElement.style.setProperty(`--theme-${theme}-${type}-contrast-${color.name}`, color.contrastColor);
});
}
@@ -77,7 +86,8 @@ export class ThemeService {
return {
name: name,
hex: c.toHexString(),
darkContrast: c.isLight(),
rgb: c.toRgbString(),
contrastColor: this.getContrast(c.toHexString()),
};
}
@@ -90,4 +100,14 @@ export class ThemeService {
const color = tinycolor(hex);
return color.isDark();
}
public getContrast(color: string): string {
const onBlack = tinycolor.readability('#000', color);
const onWhite = tinycolor.readability('#fff', color);
if (onBlack > onWhite) {
return 'hsla(0, 0%, 0%, 0.87)';
} else {
return '#ffffff';
}
}
}
+9 -6
View File
@@ -11,7 +11,7 @@ export class ToastService {
constructor(
private snackBar: MatSnackBar,
private translate: TranslateService,
) { }
) {}
public showInfo(message: string, i18nkey: boolean = false): void {
if (i18nkey) {
@@ -19,12 +19,12 @@ export class ToastService {
.get(message)
.subscribe(data => {
this.translate.get('ACTIONS.CLOSE').pipe(take(1)).subscribe(value => {
this.showMessage(data, value);
this.showMessage(data, value, true);
});
});
} else {
this.translate.get('ACTIONS.CLOSE').pipe(take(1)).subscribe(value => {
this.showMessage(message, value);
this.showMessage(message, value, true);
});
}
}
@@ -33,13 +33,16 @@ export class ToastService {
const { message, code, metadata } = grpcError;
if (code !== 16) {
this.translate.get('ACTIONS.CLOSE').pipe(take(1)).subscribe(value => {
this.showMessage(decodeURI(message), value);
this.showMessage(decodeURI(message), value, false);
});
}
}
private showMessage(message: string, action: string): Observable<void> {
const ref = this.snackBar.open(message, action, { duration: 4000 });
private showMessage(message: string, action: string, success: boolean): Observable<void> {
const ref = this.snackBar.open(message, action, {
duration: 4000,
panelClass: success ? "data-e2e-success" : "data-e2e-failure"
});
return ref.onAction();
}