mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-09 23:55:12 +00:00
perf: query projected milestones for onboarding view (#6760)
* feat: support list milestones api * show milestones in onboarding view * add authenticated milestone * add icon to login milestone * update main * lint * fix import * fix import * lint * reuse proto milestone type mapping
This commit is contained in:
@@ -20,15 +20,18 @@
|
||||
[routerLink]="action[1].link"
|
||||
[queryParams]="{ id: action[1].fragment }"
|
||||
class="action-element"
|
||||
[ngClass]="{ done: action[1].event !== undefined }"
|
||||
[ngClass]="{ done: action[1].reached !== undefined }"
|
||||
>
|
||||
<div class="state-circle">
|
||||
<mat-icon *ngIf="action[1]?.event !== undefined" class="success-icon" matTooltip="{{ action[1].event | event }}"
|
||||
<mat-icon
|
||||
*ngIf="action[1]?.reached !== undefined"
|
||||
class="success-icon"
|
||||
matTooltip="{{ action[1].reached | milestone }}"
|
||||
>check_circle</mat-icon
|
||||
>
|
||||
</div>
|
||||
|
||||
<span class="name">{{ 'ONBOARDING.EVENTS.' + action[0] + '.title' | translate }}</span>
|
||||
<span class="name">{{ 'ONBOARDING.MILESTONES.' + action[0] + '.title' | translate }}</span>
|
||||
<mat-icon class="arrow-right">keyboard_arrow_right</mat-icon>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
import { ONBOARDING_MILESTONES } from 'src/app/utils/onboarding';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-onboarding-card',
|
||||
@@ -11,7 +11,7 @@ import { ONBOARDING_EVENTS } from 'src/app/utils/onboarding';
|
||||
export class OnboardingCardComponent implements OnInit {
|
||||
public percentageChanged: EventEmitter<number> = new EventEmitter<number>();
|
||||
public loading$: BehaviorSubject<any> = new BehaviorSubject(false);
|
||||
public actions = this.adminService.progressEvents;
|
||||
public actions = this.adminService.progressMilestones;
|
||||
@Output() public dismissedCard: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
constructor(public adminService: AdminService) {}
|
||||
@@ -21,6 +21,6 @@ export class OnboardingCardComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.adminService.loadEvents.next(ONBOARDING_EVENTS);
|
||||
this.adminService.loadMilestones.next(ONBOARDING_MILESTONES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/le
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { EventPipeModule } from 'src/app/pipes/event-pipe/event-pipe.module';
|
||||
import { MilestonePipeModule } from 'src/app/pipes/milestone-pipe/milestone-pipe.module';
|
||||
import { OnboardingCardComponent } from './onboarding-card.component';
|
||||
|
||||
@NgModule({
|
||||
@@ -17,7 +17,7 @@ import { OnboardingCardComponent } from './onboarding-card.component';
|
||||
TranslateModule,
|
||||
RouterModule,
|
||||
MatProgressSpinnerModule,
|
||||
EventPipeModule,
|
||||
MilestonePipeModule,
|
||||
MatTooltipModule,
|
||||
],
|
||||
exports: [OnboardingCardComponent],
|
||||
|
||||
@@ -27,10 +27,13 @@
|
||||
[routerLink]="action[1].link"
|
||||
[queryParams]="{ id: action[1].fragment }"
|
||||
class="action-card card"
|
||||
[ngClass]="{ done: action[1].event !== undefined }"
|
||||
[ngClass]="{ done: action[1].reached !== undefined }"
|
||||
>
|
||||
<div class="state-circle">
|
||||
<mat-icon *ngIf="action[1]?.event !== undefined" matTooltip="{{ action[1].event | event }}" class="success-icon"
|
||||
<mat-icon
|
||||
*ngIf="action[1]?.reached !== undefined"
|
||||
matTooltip="{{ action[1].reached | milestone }}"
|
||||
class="success-icon"
|
||||
>check_circle</mat-icon
|
||||
>
|
||||
</div>
|
||||
@@ -54,16 +57,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-block">
|
||||
<span class="name">{{ 'ONBOARDING.EVENTS.' + action[0] + '.title' | translate }}</span>
|
||||
<span class="name">{{ 'ONBOARDING.MILESTONES.' + action[0] + '.title' | translate }}</span>
|
||||
<span class="cnsl-secondary-text description">{{
|
||||
'ONBOARDING.EVENTS.' + action[0] + '.description' | translate
|
||||
'ONBOARDING.MILESTONES.' + action[0] + '.description' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="fill-space"></span>
|
||||
<div class="action-row">
|
||||
<span>{{ 'ONBOARDING.EVENTS.' + action[0] + '.action' | translate }}</span>
|
||||
<span>{{ 'ONBOARDING.MILESTONES.' + action[0] + '.action' | translate }}</span>
|
||||
<mat-icon class="icon">keyboard_arrow_right</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ThemeService } from 'src/app/services/theme.service';
|
||||
import { ONBOARDING_EVENTS } from 'src/app/utils/onboarding';
|
||||
import { ONBOARDING_MILESTONES } from 'src/app/utils/onboarding';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-onboarding',
|
||||
@@ -9,12 +9,12 @@ import { ONBOARDING_EVENTS } from 'src/app/utils/onboarding';
|
||||
styleUrls: ['./onboarding.component.scss'],
|
||||
})
|
||||
export class OnboardingComponent {
|
||||
public actions = this.adminService.progressEvents;
|
||||
public actions = this.adminService.progressMilestones;
|
||||
|
||||
constructor(
|
||||
public adminService: AdminService,
|
||||
public themeService: ThemeService,
|
||||
) {
|
||||
this.adminService.loadEvents.next(ONBOARDING_EVENTS);
|
||||
this.adminService.loadMilestones.next(ONBOARDING_MILESTONES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 { MilestonePipeModule } from 'src/app/pipes/milestone-pipe/milestone-pipe.module';
|
||||
import { OnboardingComponent } from './onboarding.component';
|
||||
|
||||
@NgModule({
|
||||
@@ -24,7 +24,7 @@ import { OnboardingComponent } from './onboarding.component';
|
||||
RouterModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatLegacyProgressBarModule,
|
||||
EventPipeModule,
|
||||
MilestonePipeModule,
|
||||
],
|
||||
exports: [OnboardingComponent],
|
||||
})
|
||||
|
||||
+4
-4
@@ -2,11 +2,11 @@ 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';
|
||||
import { MilestonePipe } from './milestonePipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [EventPipe],
|
||||
declarations: [MilestonePipe],
|
||||
imports: [CommonModule, TimestampToDatePipeModule, LocalizedDatePipeModule],
|
||||
exports: [EventPipe],
|
||||
exports: [MilestonePipe],
|
||||
})
|
||||
export class EventPipeModule {}
|
||||
export class MilestonePipeModule {}
|
||||
+6
-10
@@ -1,22 +1,18 @@
|
||||
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';
|
||||
import { Milestone } from '../../proto/generated/zitadel/milestone/v1/milestone_pb';
|
||||
|
||||
@Pipe({
|
||||
name: 'event',
|
||||
name: 'milestone',
|
||||
})
|
||||
export class EventPipe implements PipeTransform {
|
||||
export class MilestonePipe 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);
|
||||
public transform(milestone?: Milestone.AsObject): any {
|
||||
if (milestone && milestone.reachedDate) {
|
||||
const timestampToDate = new TimestampToDatePipe().transform(milestone.reachedDate);
|
||||
const datePipeOutput = new LocalizedDatePipe(this.translateService).transform(timestampToDate);
|
||||
return `done on ${datePipeOutput}`;
|
||||
} else {
|
||||
@@ -152,6 +152,8 @@ import {
|
||||
ListLoginPolicyMultiFactorsResponse,
|
||||
ListLoginPolicySecondFactorsRequest,
|
||||
ListLoginPolicySecondFactorsResponse,
|
||||
ListMilestonesRequest,
|
||||
ListMilestonesResponse,
|
||||
ListProvidersRequest,
|
||||
ListProvidersResponse,
|
||||
ListSecretGeneratorsRequest,
|
||||
@@ -296,85 +298,77 @@ 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';
|
||||
import {
|
||||
IsReachedQuery,
|
||||
Milestone,
|
||||
MilestoneQuery,
|
||||
MilestoneType,
|
||||
} from '../proto/generated/zitadel/milestone/v1/milestone_pb';
|
||||
|
||||
export interface OnboardingActions {
|
||||
order: number;
|
||||
eventType: string;
|
||||
oneof: string[];
|
||||
link: string | string[];
|
||||
milestoneType: MilestoneType;
|
||||
link: string;
|
||||
fragment?: string | undefined;
|
||||
iconClasses?: string;
|
||||
darkcolor: string;
|
||||
lightcolor: string;
|
||||
aggregateType: string;
|
||||
}
|
||||
|
||||
type OnboardingEvent = {
|
||||
type OnboardingMilestone = {
|
||||
order: number;
|
||||
link: string;
|
||||
fragment: string | undefined;
|
||||
event: Event.AsObject | undefined;
|
||||
reached: Milestone.AsObject | undefined;
|
||||
iconClasses?: string;
|
||||
darkcolor: string;
|
||||
lightcolor: string;
|
||||
};
|
||||
type OnboardingEventEntries = Array<[string, OnboardingEvent]> | [];
|
||||
type OnboardingMilestoneEntries = Array<[string, OnboardingMilestone]> | [];
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AdminService {
|
||||
private readonly milestoneTypePrefixLength = 'MILESTONE_TYPE_'.length;
|
||||
public hideOnboarding: boolean = false;
|
||||
public loadEvents: Subject<OnboardingActions[]> = new Subject();
|
||||
public loadMilestones: Subject<OnboardingActions[]> = new Subject();
|
||||
public onboardingLoading: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public progressEvents$: Observable<OnboardingEventEntries> = this.loadEvents.pipe(
|
||||
public progressMilestones$: Observable<OnboardingMilestoneEntries> = this.loadMilestones.pipe(
|
||||
tap(() => this.onboardingLoading.next(true)),
|
||||
switchMap((actions) => {
|
||||
const searchForTypes = actions.map((oe) => oe.oneof).flat();
|
||||
const aggregateTypes = actions.map((oe) => oe.aggregateType);
|
||||
const eventsReq = new ListEventsRequest()
|
||||
.setAsc(true)
|
||||
.setEventTypesList(searchForTypes)
|
||||
.setAggregateTypesList(aggregateTypes)
|
||||
.setAsc(false);
|
||||
return from(this.listEvents(eventsReq)).pipe(
|
||||
map((events) => {
|
||||
const el = events.toObject().eventsList.filter((e) => e.editor?.service !== 'System-API' && e.editor?.userId);
|
||||
|
||||
let obj: { [type: string]: OnboardingEvent } = {};
|
||||
const milestonesListQuery = new ListQuery();
|
||||
milestonesListQuery.setAsc(true);
|
||||
milestonesListQuery.setLimit(20);
|
||||
const milestoneIsReachedQuery = new IsReachedQuery().setReached(true);
|
||||
const milestonesQuery = new MilestoneQuery().setIsReachedQuery(milestoneIsReachedQuery);
|
||||
const milestonesReq = new ListMilestonesRequest().setQuery(milestonesListQuery).setQueriesList([milestonesQuery]);
|
||||
return from(this.listMilestones(milestonesReq)).pipe(
|
||||
map((reachedMilestones) => {
|
||||
let obj: { [type: string]: OnboardingMilestone } = {};
|
||||
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],
|
||||
iconClasses: action.iconClasses,
|
||||
darkcolor: action.darkcolor,
|
||||
lightcolor: action.lightcolor,
|
||||
}
|
||||
: {
|
||||
order: action.order,
|
||||
link: action.link,
|
||||
fragment: action.fragment,
|
||||
event: undefined,
|
||||
iconClasses: action.iconClasses,
|
||||
darkcolor: action.darkcolor,
|
||||
lightcolor: action.lightcolor,
|
||||
};
|
||||
obj[Object.keys(MilestoneType)[action.milestoneType].substring(this.milestoneTypePrefixLength)] = {
|
||||
order: action.order,
|
||||
link: action.link,
|
||||
fragment: action.fragment,
|
||||
iconClasses: action.iconClasses,
|
||||
darkcolor: action.darkcolor,
|
||||
lightcolor: action.lightcolor,
|
||||
reached: reachedMilestones.resultList.find((reached) => {
|
||||
return reached.type.valueOf() == action.milestoneType;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
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);
|
||||
const toDo = toArray.filter(([key, value]) => value.reached === undefined);
|
||||
const done = toArray.filter(([key, value]) => !!value.reached);
|
||||
|
||||
return [...toDo, ...done];
|
||||
}),
|
||||
tap((events) => {
|
||||
const total = events.length;
|
||||
const done = events.map(([type, value]) => value.event !== undefined).filter((res) => !!res).length;
|
||||
tap((milestones) => {
|
||||
const total = milestones.length;
|
||||
const done = milestones.map(([type, value]) => value.reached !== undefined).filter((res) => !!res).length;
|
||||
const percentage = Math.round((done / total) * 100);
|
||||
this.progressDone.next(done);
|
||||
this.progressTotal.next(total);
|
||||
@@ -390,7 +384,9 @@ export class AdminService {
|
||||
}),
|
||||
);
|
||||
|
||||
public progressEvents: BehaviorSubject<OnboardingEventEntries> = new BehaviorSubject<OnboardingEventEntries>([]);
|
||||
public progressMilestones: BehaviorSubject<OnboardingMilestoneEntries> = new BehaviorSubject<OnboardingMilestoneEntries>(
|
||||
[],
|
||||
);
|
||||
public progressPercentage: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||
public progressDone: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||
public progressTotal: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||
@@ -400,7 +396,7 @@ export class AdminService {
|
||||
private readonly grpcService: GrpcService,
|
||||
private storageService: StorageService,
|
||||
) {
|
||||
this.progressEvents$.subscribe(this.progressEvents);
|
||||
this.progressMilestones$.subscribe(this.progressMilestones);
|
||||
|
||||
this.hideOnboarding =
|
||||
this.storageService.getItem('onboarding-dismissed', StorageLocation.local) === 'true' ? true : false;
|
||||
@@ -1254,4 +1250,8 @@ export class AdminService {
|
||||
|
||||
return this.grpcService.admin.updateIAMMember(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public listMilestones(req: ListMilestonesRequest): Promise<ListMilestonesResponse.AsObject> {
|
||||
return this.grpcService.admin.listMilestones(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ export const COLORS = [
|
||||
{ 500: '#d946ef', 200: '#f5d0fe', 300: '#f0abfc', 600: '#c026d3', 700: '#a21caf', 900: '#701a75' },
|
||||
{ 500: '#ec4899', 200: '#fbcfe8', 300: '#f9a8d4', 600: '#db2777', 700: '#be185d', 900: '#831843' },
|
||||
{ 500: '#f43f5e', 200: '#fecdd3', 300: '#fda4af', 600: '#e11d48', 700: '#be123c', 900: '#881337' },
|
||||
{ 500: '#A89F91', 200: '#D4CDC6', 300: '#BFB6AC', 600: '#8F8378', 700: '#736A60', 900: '#4F4A40' },
|
||||
{ 500: '#BA9F88', 200: '#E8D3C5', 300: '#D4BAA7', 600: '#9C7A68', 700: '#8A6E5D', 900: '#5F4C42' },
|
||||
];
|
||||
|
||||
export const WEB_APP_COLOR: Color = COLORS[6];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { OnboardingActions } from '../services/admin.service';
|
||||
import { COLORS } from './color';
|
||||
import { MilestoneType } from '../proto/generated/zitadel/milestone/v1/milestone_pb';
|
||||
|
||||
const reddark: string = COLORS[0][700];
|
||||
const redlight = COLORS[0][200];
|
||||
@@ -19,67 +20,66 @@ const purplelight = COLORS[12][200];
|
||||
const pinkdark: string = COLORS[15][700];
|
||||
const pinklight = COLORS[15][200];
|
||||
|
||||
export const ONBOARDING_EVENTS: OnboardingActions[] = [
|
||||
const sthdark: string = COLORS[18][700];
|
||||
const sthlight = COLORS[18][200];
|
||||
|
||||
export const ONBOARDING_MILESTONES: OnboardingActions[] = [
|
||||
{
|
||||
order: 0,
|
||||
eventType: 'project.added',
|
||||
oneof: ['project.added'],
|
||||
link: ['/projects/create'],
|
||||
milestoneType: MilestoneType.MILESTONE_TYPE_PROJECT_CREATED,
|
||||
link: '/projects/create',
|
||||
iconClasses: 'las la-database',
|
||||
darkcolor: greendark,
|
||||
lightcolor: greenlight,
|
||||
aggregateType: 'project',
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
eventType: 'project.application.added',
|
||||
oneof: ['project.application.added'],
|
||||
link: ['/projects/app-create'],
|
||||
milestoneType: MilestoneType.MILESTONE_TYPE_APPLICATION_CREATED,
|
||||
link: '/projects/app-create',
|
||||
iconClasses: 'lab la-openid',
|
||||
darkcolor: purpledark,
|
||||
lightcolor: purplelight,
|
||||
aggregateType: 'project',
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
eventType: 'user.human.added',
|
||||
oneof: ['user.human.added'],
|
||||
link: ['/users/create'],
|
||||
iconClasses: 'las la-user',
|
||||
darkcolor: bluedark,
|
||||
lightcolor: bluelight,
|
||||
aggregateType: 'user',
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
eventType: 'user.grant.added',
|
||||
oneof: ['user.grant.added'],
|
||||
link: ['/grant-create'],
|
||||
milestoneType: MilestoneType.MILESTONE_TYPE_AUTHENTICATION_SUCCEEDED_ON_APPLICATION,
|
||||
link: 'https://zitadel.com/docs/guides/integrate/login-users',
|
||||
iconClasses: 'las la-sign-in-alt',
|
||||
darkcolor: sthdark,
|
||||
lightcolor: sthlight,
|
||||
} /*
|
||||
{
|
||||
order: 4,
|
||||
milestoneType: 'user.human.added',
|
||||
link: '/users/create',
|
||||
iconClasses: 'las la-user',
|
||||
darkcolor: bluedark,
|
||||
lightcolor: bluelight,
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
milestoneType: 'user.grant.added',
|
||||
link: '/grant-create',
|
||||
iconClasses: 'las la-shield-alt',
|
||||
darkcolor: reddark,
|
||||
lightcolor: redlight,
|
||||
aggregateType: 'user_grant',
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
eventType: 'instance.policy.label.added',
|
||||
oneof: ['instance.policy.label.added', 'instance.policy.label.changed'],
|
||||
link: ['/settings'],
|
||||
order: 6,
|
||||
milestoneType: 'instance.policy.label.added',
|
||||
link: '/settings',
|
||||
fragment: 'branding',
|
||||
iconClasses: 'las la-swatchbook',
|
||||
darkcolor: pinkdark,
|
||||
lightcolor: pinklight,
|
||||
aggregateType: 'instance',
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
eventType: 'instance.smtp.config.added',
|
||||
oneof: ['instance.smtp.config.added', 'instance.smtp.config.changed'],
|
||||
link: ['/settings'],
|
||||
order: 7,
|
||||
milestoneType: 'instance.smtp.config.added',
|
||||
link: '/settings',
|
||||
fragment: 'smtpprovider',
|
||||
iconClasses: 'las la-envelope',
|
||||
darkcolor: yellowdark,
|
||||
lightcolor: yellowlight,
|
||||
aggregateType: 'instance',
|
||||
},
|
||||
},*/,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user