arrange pages
This commit is contained in:
parent
f4a35f288f
commit
e4e6a45238
|
@ -26,4 +26,8 @@ export class InfraService {
|
||||||
public readAllByProbe(probe: Probe, pageParams: PageParams): Observable<Page> {
|
public readAllByProbe(probe: Probe, pageParams: PageParams): Observable<Page> {
|
||||||
return this.rpcService.call('InfraService.readAllByProbe', probe, pageParams);
|
return this.rpcService.call('InfraService.readAllByProbe', probe, pageParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public read(id: string): Observable<Infra> {
|
||||||
|
return this.rpcService.call('InfraService.read', id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
35
src/packages/infra/store/detail/detail.action.ts
Normal file
35
src/packages/infra/store/detail/detail.action.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
import { RPCClientError } from '@loafer/ng-rpc/protocol';
|
||||||
|
import { Infra } from '../../model';
|
||||||
|
|
||||||
|
export enum ActionType {
|
||||||
|
Read = '[Infra.list] Read',
|
||||||
|
ReadSuccess = '[Infra.list] ReadSuccess',
|
||||||
|
ReadFailure = '[Infra.list] ReadFailure',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Read implements Action {
|
||||||
|
readonly type = ActionType.Read;
|
||||||
|
|
||||||
|
constructor(public payload: { id: string }) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReadSuccess implements Action {
|
||||||
|
readonly type = ActionType.ReadSuccess;
|
||||||
|
|
||||||
|
constructor(public payload: Infra) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReadFailure implements Action {
|
||||||
|
readonly type = ActionType.ReadFailure;
|
||||||
|
|
||||||
|
constructor(public payload: RPCClientError) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type Actions =
|
||||||
|
| Read
|
||||||
|
| ReadSuccess
|
||||||
|
| ReadFailure
|
||||||
|
;
|
15
src/packages/infra/store/detail/detail.effect.spec.ts
Normal file
15
src/packages/infra/store/detail/detail.effect.spec.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Effects } from './list.effect';
|
||||||
|
|
||||||
|
describe('list.Effects', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [Effects]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', inject([Effects], (effects: Effects) => {
|
||||||
|
expect(effects).toBeTruthy();
|
||||||
|
}));
|
||||||
|
});
|
49
src/packages/infra/store/detail/detail.effect.ts
Normal file
49
src/packages/infra/store/detail/detail.effect.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { Effect, Actions, ofType } from '@ngrx/effects';
|
||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { of } from 'rxjs/observable/of';
|
||||||
|
|
||||||
|
import 'rxjs/add/operator/catch';
|
||||||
|
import 'rxjs/add/operator/do';
|
||||||
|
import 'rxjs/add/operator/exhaustMap';
|
||||||
|
import 'rxjs/add/operator/map';
|
||||||
|
import 'rxjs/add/operator/take';
|
||||||
|
|
||||||
|
import { RPCClientError } from '@loafer/ng-rpc/protocol';
|
||||||
|
|
||||||
|
import { Infra } from '../../model';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Read,
|
||||||
|
ReadSuccess,
|
||||||
|
ReadFailure,
|
||||||
|
ActionType,
|
||||||
|
} from './detail.action';
|
||||||
|
import { InfraService } from '../../service/infra.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class Effects {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private actions$: Actions,
|
||||||
|
private infraService: InfraService,
|
||||||
|
private router: Router
|
||||||
|
) { }
|
||||||
|
|
||||||
|
@Effect()
|
||||||
|
read$: Observable<Action> = this.actions$
|
||||||
|
.ofType(ActionType.Read)
|
||||||
|
.map((action: Read) => action.payload)
|
||||||
|
.switchMap(payload => this.infraService.read(payload.id))
|
||||||
|
.map(infra => {
|
||||||
|
return new ReadSuccess(infra);
|
||||||
|
})
|
||||||
|
.catch((error: RPCClientError) => {
|
||||||
|
return of(new ReadFailure(error));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
38
src/packages/infra/store/detail/detail.reducer.ts
Normal file
38
src/packages/infra/store/detail/detail.reducer.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { Actions, ActionType } from './detail.action';
|
||||||
|
import { State, initialState } from './detail.state';
|
||||||
|
|
||||||
|
import { Infra } from '../../model';
|
||||||
|
|
||||||
|
export function reducer(state = initialState, action: Actions): State {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionType.Read: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: null,
|
||||||
|
isPending: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionType.ReadSuccess: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: null,
|
||||||
|
isPending: false,
|
||||||
|
infra: action.payload
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionType.ReadFailure: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: action.payload,
|
||||||
|
isPending: false,
|
||||||
|
infra: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/packages/infra/store/detail/detail.state.ts
Normal file
16
src/packages/infra/store/detail/detail.state.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { RPCClientError } from '@loafer/ng-rpc/protocol';
|
||||||
|
|
||||||
|
import { Infra } from '../../model';
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
error: RPCClientError | null;
|
||||||
|
isPending: boolean;
|
||||||
|
infra: Infra | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialState: State = {
|
||||||
|
error: null,
|
||||||
|
isPending: false,
|
||||||
|
infra: null,
|
||||||
|
};
|
||||||
|
|
4
src/packages/infra/store/detail/index.ts
Normal file
4
src/packages/infra/store/detail/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './detail.action';
|
||||||
|
export * from './detail.effect';
|
||||||
|
export * from './detail.reducer';
|
||||||
|
export * from './detail.state';
|
|
@ -7,18 +7,22 @@ import {
|
||||||
import { MODULE } from '../infra.constant';
|
import { MODULE } from '../infra.constant';
|
||||||
|
|
||||||
import * as ListStore from './list';
|
import * as ListStore from './list';
|
||||||
|
import * as DetailStore from './detail';
|
||||||
import { StateSelector } from 'packages/core/ngrx/store';
|
import { StateSelector } from 'packages/core/ngrx/store';
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
list: ListStore.State;
|
list: ListStore.State;
|
||||||
|
sensor: DetailStore.State;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const REDUCERS = {
|
export const REDUCERS = {
|
||||||
list: ListStore.reducer,
|
list: ListStore.reducer,
|
||||||
|
sensor: DetailStore.reducer
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EFFECTS = [
|
export const EFFECTS = [
|
||||||
ListStore.Effects,
|
ListStore.Effects,
|
||||||
|
DetailStore.Effects
|
||||||
];
|
];
|
||||||
|
|
||||||
export const selectInfraState = createFeatureSelector<State>(MODULE.name);
|
export const selectInfraState = createFeatureSelector<State>(MODULE.name);
|
||||||
|
@ -28,3 +32,7 @@ export const ListSelector = new StateSelector<ListStore.State>(createSelector(
|
||||||
(state: State) => state.list
|
(state: State) => state.list
|
||||||
));
|
));
|
||||||
|
|
||||||
|
export const DetailSelector = new StateSelector<DetailStore.State>(createSelector(
|
||||||
|
selectInfraState,
|
||||||
|
(state: State) => state.sensor
|
||||||
|
));
|
||||||
|
|
|
@ -74,7 +74,7 @@ export class DetailComponent implements OnInit, AfterContentInit {
|
||||||
icon: 'fa fa-trash',
|
icon: 'fa fa-trash',
|
||||||
message: 'Are you sure to remove this Probe?',
|
message: 'Are you sure to remove this Probe?',
|
||||||
accept: () => {
|
accept: () => {
|
||||||
this.router.navigate(['probes']);
|
this.router.navigate(['probes/list']);
|
||||||
},
|
},
|
||||||
reject: () => {
|
reject: () => {
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,6 @@
|
||||||
import { Component, OnInit, AfterViewInit, AfterContentInit } from '@angular/core';
|
import { Component, OnInit, AfterViewInit, AfterContentInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MetaSensorDisplayItem } from '../../../meta/sensor-display-item/model/MetaSensorDisplayItem';
|
import { MetaSensorDisplayItem } from 'packages/meta/sensor-display-item/model/MetaSensorDisplayItem';
|
||||||
|
|
||||||
export interface Probe {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
ip: string;
|
|
||||||
os: string;
|
|
||||||
cidr: string;
|
|
||||||
targetCnt: number;
|
|
||||||
date: string;
|
|
||||||
authBy: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'of-sensor-item-list',
|
selector: 'of-sensor-item-list',
|
||||||
|
|
|
@ -1,12 +1,23 @@
|
||||||
<h1>Sensors</h1>
|
<h1>Sensors</h1>
|
||||||
|
|
||||||
|
<div *ngIf="paramTarget" class="card clearfix">
|
||||||
|
<span>
|
||||||
|
<i class="fa ui-icon-stop ui-status-icon ui-status-success"></i>Up
|
||||||
|
</span>
|
||||||
|
<of-key-value [key]="'Alias'" [value]="paramTarget.target.displayName"></of-key-value>
|
||||||
|
<of-key-value [key]="'Description'" [value]="paramTarget.target.description"></of-key-value>
|
||||||
|
<of-key-value [key]="'Type'" [value]="paramTarget.infraType.name"></of-key-value>
|
||||||
|
<of-key-value [key]="'Created at'" [value]="paramTarget.createDate | date: 'dd/MM/yyyy'"></of-key-value>
|
||||||
|
<of-key-value [key]="'Sensors'" [value]="'todo: count'"></of-key-value>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ui-g">
|
<div class="ui-g">
|
||||||
<div class="ui-g-12 ui-md-3">
|
<div class="ui-g-12 ui-md-3">
|
||||||
<div class="ui-bottom-space-10">
|
<div class="ui-bottom-space-10">
|
||||||
<p-dialog [modal]="true" [width]="800" [(visible)]="sensorSettingDisplay" [showHeader]="false" [closeOnEscape]="false">
|
<p-dialog [modal]="true" [width]="800" [(visible)]="sensorSettingDisplay" [showHeader]="false" [closeOnEscape]="false">
|
||||||
<of-sensor-setting [visible]="sensorSettingDisplay" [preTarget]="target" (close)="onSensorSettingClose()"></of-sensor-setting>
|
<of-sensor-setting [visible]="sensorSettingDisplay" [preTarget]="target" (close)="onSensorSettingClose()"></of-sensor-setting>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|
||||||
<button type="button" label="Add Sensor" icon="ui-icon-add" pButton class="ui-button-large" (click)="onAddSensor()"></button>
|
<button type="button" label="Add Sensor" icon="ui-icon-add" pButton class="ui-button-large" (click)="onAddSensor()"></button>
|
||||||
</div>
|
</div>
|
||||||
<p-panel [showHeader]="false">
|
<p-panel [showHeader]="false">
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import { Component, OnInit, AfterViewInit, ViewChild } from '@angular/core';
|
import { Component, OnInit, AfterViewInit, ViewChild, AfterContentInit } from '@angular/core';
|
||||||
import { AfterContentInit } from '@angular/core/src/metadata/lifecycle_hooks';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { Sensor } from '../../model';
|
import { Sensor } from '../../model';
|
||||||
|
|
||||||
import { Store, select } from '@ngrx/store';
|
import { Store, select } from '@ngrx/store';
|
||||||
|
|
||||||
import * as SensorStore from '../../store';
|
import * as SensorStore from '../../store';
|
||||||
|
|
||||||
import { RPCClientError } from '@loafer/ng-rpc/protocol';
|
import { RPCClientError } from '@loafer/ng-rpc/protocol';
|
||||||
import * as ListStore from '../../store/list';
|
import * as ListStore from '../../store/list';
|
||||||
import { sensorListSelector } from '../../store';
|
import { sensorListSelector } from '../../store';
|
||||||
|
@ -17,6 +13,11 @@ import { Target } from 'packages/target/model';
|
||||||
import { SettingComponent } from '../setting/setting.component';
|
import { SettingComponent } from '../setting/setting.component';
|
||||||
import { SelectItem } from 'primeng/primeng';
|
import { SelectItem } from 'primeng/primeng';
|
||||||
|
|
||||||
|
import { Infra } from 'packages/infra/model';
|
||||||
|
import * as InfraDetailStore from 'packages/infra/store/detail';
|
||||||
|
import { DetailSelector as InfraDetailSelector } from 'packages/infra/store';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'of-sensor-list',
|
selector: 'of-sensor-list',
|
||||||
templateUrl: './list.component.html',
|
templateUrl: './list.component.html',
|
||||||
|
@ -29,23 +30,37 @@ export class ListComponent implements OnInit, AfterContentInit {
|
||||||
totalLength = 0;
|
totalLength = 0;
|
||||||
sensorSettingDisplay = false;
|
sensorSettingDisplay = false;
|
||||||
|
|
||||||
|
paramTarget?: Infra = null;
|
||||||
|
infra$ = this.infraDetailStore.pipe(select(InfraDetailSelector.select('infra')));
|
||||||
|
|
||||||
sensors: Sensor[];
|
sensors: Sensor[];
|
||||||
target: Target = null;
|
target: Target = null;
|
||||||
|
|
||||||
targetSensor;
|
targetSensor;
|
||||||
|
|
||||||
// filter
|
// filter
|
||||||
targetOptions: SelectItem[];
|
targetOptions: SelectItem[];
|
||||||
|
|
||||||
filteredName: string;
|
filteredName: string;
|
||||||
selectedTargets: string[] = [];
|
selectedTargets: string[] = [];
|
||||||
selectedStatus: string[] = [];
|
selectedStatus: string[] = [];
|
||||||
|
|
||||||
constructor(private router: Router,
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
private store: Store<ListStore.State>,
|
private store: Store<ListStore.State>,
|
||||||
|
private infraDetailStore: Store<InfraDetailStore.State>,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
let infraID = null;
|
||||||
|
this.route.queryParams.subscribe((queryParams: any) => {
|
||||||
|
infraID = queryParams.target;
|
||||||
|
if (infraID) {
|
||||||
|
this.getInfraInfo(infraID);
|
||||||
|
} else {
|
||||||
|
this.paramTarget = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.sensorList$.subscribe(
|
this.sensorList$.subscribe(
|
||||||
(page: Page) => {
|
(page: Page) => {
|
||||||
if (page != null) {
|
if (page != null) {
|
||||||
|
@ -59,6 +74,24 @@ export class ListComponent implements OnInit, AfterContentInit {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInfraInfo(infraID: string) {
|
||||||
|
this.infraDetailStore.dispatch(
|
||||||
|
new InfraDetailStore.Read(
|
||||||
|
{ id: infraID }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.infra$.subscribe(
|
||||||
|
(infra: Infra) => {
|
||||||
|
console.log(infra);
|
||||||
|
this.paramTarget = infra;
|
||||||
|
},
|
||||||
|
(error: RPCClientError) => {
|
||||||
|
console.log(error.response.message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterContentInit() {
|
ngAfterContentInit() {
|
||||||
this.getSensors(0);
|
this.getSensors(0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<h1>Targets</h1>
|
<h1>Targets</h1>
|
||||||
|
|
||||||
<p-table [value]="infras" selectionMode="single" (onRowSelect)="onRowSelect($event)" [resizableColumns]="true" dataKey="id">
|
<p-table [value]="infras" selectionMode="single" (onRowSelect)="onRowSelect($event)" [resizableColumns]="true" >
|
||||||
<ng-template pTemplate="header" let-columns>
|
<ng-template pTemplate="header">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 3.5em"></th>
|
|
||||||
<th style="width: 4em">No.</th>
|
<th style="width: 4em">No.</th>
|
||||||
<th style="width: 8em">Status</th>
|
<th style="width: 8em">Status</th>
|
||||||
<th style="width: 8em">Type</th>
|
<th style="width: 8em">Type</th>
|
||||||
|
@ -13,13 +12,8 @@
|
||||||
<th style="width: 10em"></th>
|
<th style="width: 10em"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template pTemplate="body" let-infra let-rowIndex="rowIndex" let-expanded="expanded" let-columns="infras">
|
<ng-template pTemplate="body" let-infra let-rowIndex="rowIndex">
|
||||||
<tr>
|
<tr [pSelectableRow]="infra">
|
||||||
<td>
|
|
||||||
<a href="javascript:void(0)" [pRowToggler]="infra">
|
|
||||||
<i [ngClass]="expanded ? 'fa fa-fw fa-chevron-circle-down' : 'fa fa-fw fa-chevron-circle-right'"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>{{rowIndex}}</td>
|
<td>{{rowIndex}}</td>
|
||||||
<td>??</td>
|
<td>??</td>
|
||||||
<td>{{infra.infraType.name}}</td>
|
<td>{{infra.infraType.name}}</td>
|
||||||
|
@ -27,21 +21,10 @@
|
||||||
<td>??</td>
|
<td>??</td>
|
||||||
<td>{{infra.createDate | date: 'dd.MM.yyyy'}}</td>
|
<td>{{infra.createDate | date: 'dd.MM.yyyy'}}</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" label="Add Sensor" icon="ui-icon-add" pButton class="ui-s-button" (click)="onAddSensor(infra.target)"></button>
|
<!-- <button type="button" label="Add Sensor" icon="ui-icon-add" pButton class="ui-s-button" (click)="onAddSensor(infra.target)"></button> -->
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template pTemplate="rowexpansion" let-rowData let-columns="infras">
|
|
||||||
<tr>
|
|
||||||
<td [attr.colspan]="8">
|
|
||||||
<div class="ui-g ui-fluid" style="font-size:16px;padding:20px">
|
|
||||||
<div>요기다가 센서 리스트와 상태 출력</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
</p-table>
|
</p-table>
|
||||||
|
|
||||||
<p-dialog [modal]="true" [width]="800" [(visible)]="sensorSettingDisplay" [showHeader]="false" [closeOnEscape]="false">
|
<p-dialog [modal]="true" [width]="800" [(visible)]="sensorSettingDisplay" [showHeader]="false" [closeOnEscape]="false">
|
||||||
|
|
|
@ -124,8 +124,7 @@ export class ListComponent implements OnInit, AfterContentInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
onRowSelect(event) {
|
onRowSelect(event) {
|
||||||
console.log(event.data.target);
|
this.router.navigate(['sensors'], { queryParams: {target: event.data.id}});
|
||||||
// this.router.navigate(['notification']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddSensor(target: Target) {
|
onAddSensor(target: Target) {
|
||||||
|
@ -136,4 +135,5 @@ export class ListComponent implements OnInit, AfterContentInit {
|
||||||
onSensorSettingClose() {
|
onSensorSettingClose() {
|
||||||
this.sensorSettingDisplay = false;
|
this.sensorSettingDisplay = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,14 @@ import { COMPONENTS } from './component';
|
||||||
import { SERVICES } from './service';
|
import { SERVICES } from './service';
|
||||||
import { PrimeNGModules } from '../commons/prime-ng/prime-ng.module';
|
import { PrimeNGModules } from '../commons/prime-ng/prime-ng.module';
|
||||||
import { SensorModule } from '../sensor/sensor.module';
|
import { SensorModule } from '../sensor/sensor.module';
|
||||||
|
import { KeyValueModule } from 'app/commons/component/key-value/key-value.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
PrimeNGModules,
|
PrimeNGModules,
|
||||||
SensorModule
|
SensorModule,
|
||||||
|
KeyValueModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
COMPONENTS,
|
COMPONENTS,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user