mirror of https://github.com/OpenVidu/openvidu.git
openvidu-components: Added admin dashboard
parent
d37809ac5b
commit
15a7037b04
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"include": [
|
||||
"../src/lib/admin/**/*.ts",
|
||||
"../src/lib/components/**/*.ts",
|
||||
"../src/lib/directives/**/*.ts",
|
||||
"../src/lib/services/**/*.ts",
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
.dashboard-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 50px;
|
||||
background-color: var(--ov-secondary-color);
|
||||
color: var(--ov-text-color);
|
||||
}
|
||||
.dashboard-body {
|
||||
height: calc(100% - 75px);
|
||||
}
|
||||
|
||||
#toolbar-search {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: var(--ov-light-color);
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#toolbar-sort-div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#sort-menu-btn {
|
||||
margin-left: 5px;
|
||||
background-color: var(--ov-panel-background);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
height: 95%;
|
||||
width: 30%;
|
||||
display: flex;
|
||||
background-color: var(--ov-panel-background);
|
||||
padding: 0px 10px;
|
||||
border-radius: var(--ov-panel-radius);
|
||||
}
|
||||
|
||||
#search-input {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
margin: auto;
|
||||
background-color: transparent;
|
||||
display: block;
|
||||
border: none;
|
||||
padding: 0;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
resize: none;
|
||||
outline: none;
|
||||
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
font-family: 'Roboto', 'RobotoDraft', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
#refresh-btn {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#refresh-btn mat-icon {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.recordings-container {
|
||||
height: calc(100% - 51px);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.recording-card {
|
||||
background-color: var(--ov-panel-background);
|
||||
}
|
||||
|
||||
.video-div-container {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 42%;
|
||||
overflow: hidden;
|
||||
background-color: var(--ov-light-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.video-div-container img {
|
||||
border-radius: var(--ov-video-radius);
|
||||
display: inline;
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.item {
|
||||
flex-grow: 1;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.item + .item {
|
||||
margin-left: 2%;
|
||||
}
|
||||
|
||||
.video-btns {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
margin-right: -50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
background-color: var(--ov-logo-background-color);
|
||||
border-radius: var(--ov-panel-radius);
|
||||
}
|
||||
|
||||
.video-btns button #play {
|
||||
color: var(--ov-text-color);
|
||||
}
|
||||
.video-btns button #download {
|
||||
color: var(--ov-tertiary-color);
|
||||
}
|
||||
.video-btns button #delete {
|
||||
color: var(--ov-warn-color);
|
||||
}
|
||||
|
||||
.video-info-container > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
box-sizing: border-box;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.video-div-tag:first-child {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.video-div-tag {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.video-card-tag {
|
||||
font-size: 13px;
|
||||
color: var(--ov-panel-text-color);
|
||||
}
|
||||
.video-card-value {
|
||||
float: right;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 25px;
|
||||
background-color: var(--ov-secondary-color);
|
||||
color: var(--ov-text-color);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.footer a {
|
||||
color: var(--ov-tertiary-color);
|
||||
}
|
||||
|
||||
.no-recordings-warn {
|
||||
height: calc(100% - 52px);
|
||||
width: 100%;
|
||||
display: table;
|
||||
text-align: center;
|
||||
}
|
||||
::ng-deep .mat-form-field-appearance-fill .mat-form-field-flex {
|
||||
padding: 0px !important;
|
||||
background-color: var(--ov-light-color) !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-form-field-wrapper {
|
||||
height: 100% !important;
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
<div class="dashboard-container">
|
||||
<mat-toolbar class="header">
|
||||
<span>{{ 'ADMIN.DASHBOARD' | translate }}</span>
|
||||
</mat-toolbar>
|
||||
|
||||
<div class="dashboard-body">
|
||||
<div id="toolbar-search">
|
||||
<div class="search-bar">
|
||||
<textarea
|
||||
id="search-input"
|
||||
maxlength="100"
|
||||
rows="4"
|
||||
placeholder="{{ 'ADMIN.SEARCH' | translate }}"
|
||||
autocomplete="off"
|
||||
[(ngModel)]="searchValue"
|
||||
></textarea>
|
||||
<div>
|
||||
<button *ngIf="searchValue" matSuffix mat-icon-button aria-label="Clear" (click)="searchValue = ''">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="!searchValue" matSuffix mat-icon-button aria-label="Search">
|
||||
<mat-icon>search</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button id="sort-menu-btn" color="primary" mat-flat-button [matMenuTriggerFor]="sortMenu">
|
||||
{{ sortByLegend }}
|
||||
<mat-icon>arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #sortMenu="matMenu">
|
||||
<button mat-menu-item class="order-select-btn" (click)="sortRecordingsByDate()">{{ 'ADMIN.DATE' | translate }}</button>
|
||||
<button mat-menu-item class="order-select-btn" (click)="sortRecordingsByDuration()">
|
||||
{{ 'ADMIN.DURATION' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item class="order-select-btn" (click)="sortRecordingsBySize()">{{ 'ADMIN.SIZE' | translate }}</button>
|
||||
</mat-menu>
|
||||
|
||||
<div class="refresh-btn">
|
||||
<button matSuffix mat-icon-button aria-label="Refresh">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="recordings.length === 0" class="no-recordings-warn">
|
||||
<span>{{ 'ADMIN.NO_RECORDINGS' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="recordings-container">
|
||||
<div
|
||||
*ngFor="
|
||||
let recording of recordings
|
||||
| searchByStringProperty: { properties: ['sessionId', 'properties.name'], filter: searchValue }
|
||||
"
|
||||
class="item"
|
||||
>
|
||||
<mat-card class="recording-card">
|
||||
<mat-card-content>
|
||||
<div class="video-div-container">
|
||||
<img
|
||||
*ngIf="
|
||||
!!recording.url &&
|
||||
recording.properties.hasVideo &&
|
||||
(recording.properties.outputMode === 'COMPOSED' ||
|
||||
recording.properties.outputMode === 'COMPOSED_QUICK_START')
|
||||
"
|
||||
[src]="getThumbnailSrc(recording)"
|
||||
/>
|
||||
<div class="video-btns">
|
||||
<button
|
||||
*ngIf="
|
||||
recording.status !== 'failed' &&
|
||||
recording.status !== 'stopped' &&
|
||||
recording.properties.outputMode !== 'INDIVIDUAL'
|
||||
"
|
||||
mat-icon-button
|
||||
(click)="play(recording)"
|
||||
>
|
||||
<mat-icon id="play" aria-label="Play" title="{{ 'PANEL.RECORDING.PLAY' | translate }}"
|
||||
>play_arrow</mat-icon
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="download(recording.id)"
|
||||
*ngIf="!!recording.url && recording.status !== 'failed' && recording.status !== 'stopped'"
|
||||
mat-icon-button
|
||||
aria-label="Download"
|
||||
title="{{ 'PANEL.RECORDING.DOWNLOAD' | translate }}"
|
||||
>
|
||||
<mat-icon id="download">download</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button class="delete-recording-btn" (click)="deleteRecording(recording.id)">
|
||||
<mat-icon id="delete" aria-label="Delete" title="{{ 'PANEL.RECORDING.DELETE' | translate }}"
|
||||
>delete</mat-icon
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-info-container">
|
||||
<div>
|
||||
<div class="video-div-tag">
|
||||
<span class="video-card-tag">{{ 'ADMIN.NAME' | translate }}</span
|
||||
><span class="video-card-value">{{ recording.properties.name }}</span>
|
||||
</div>
|
||||
<div class="video-div-tag">
|
||||
<span class="video-card-tag">{{ 'ADMIN.SESSION' | translate }}</span
|
||||
><span class="video-card-value">{{ recording.sessionId }}</span>
|
||||
</div>
|
||||
<div class="video-div-tag">
|
||||
<span class="video-card-tag">{{ 'ADMIN.OUTPUT' | translate }}</span
|
||||
><span class="video-card-value">{{ recording.properties.outputMode }}</span>
|
||||
</div>
|
||||
<div class="video-div-tag">
|
||||
<span class="video-card-tag">{{ 'ADMIN.DATE' | translate }}</span
|
||||
><span class="video-card-value">{{ recording.createdAt | date: 'M/d/yy, H:mm' }}</span>
|
||||
</div>
|
||||
<div class="video-div-tag">
|
||||
<span class="video-card-tag">{{ 'ADMIN.DURATION' | translate }}</span
|
||||
><span class="video-card-value">{{ recording.duration | duration }}</span>
|
||||
</div>
|
||||
<div class="video-div-tag">
|
||||
<span class="video-card-tag">{{ 'ADMIN.SIZE' | translate }}</span
|
||||
><span class="video-card-value">{{ recording.size / 1024 / 1024 | number: '1.1-2' }} MBs</span>
|
||||
</div>
|
||||
<div class="video-div-tag" style="margin-top: 11px">
|
||||
<span class="video-card-tag">{{ 'ADMIN.STATUS' | translate }}</span
|
||||
><span class="video-card-value status-value" [ngClass]="recording.status">{{ recording.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-toolbar class="footer" fxLayout fxLayout.xs="row" fxLayoutGap="2px" id="footer" role="heading">
|
||||
<span>{{ 'ADMIN.POWERED_BY' | translate }}</span>
|
||||
<a href="https://openvidu.io/">OpenVidu</a>
|
||||
</mat-toolbar>
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
|
||||
describe('DashboardComponent', () => {
|
||||
let component: DashboardComponent;
|
||||
let fixture: ComponentFixture<DashboardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DashboardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,153 @@
|
|||
import { Component, OnInit, Output, EventEmitter, OnDestroy } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { RecordingInfo } from '../../models/recording.model';
|
||||
import { ActionService } from '../../services/action/action.service';
|
||||
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
|
||||
import { RecordingService } from '../../services/recording/recording.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ov-admin-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.css']
|
||||
})
|
||||
export class AdminDashboardComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Provides event notifications that fire when download recording button has been clicked.
|
||||
* The recording should be downloaded using the REST API.
|
||||
*/
|
||||
@Output() onDownloadRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
/**
|
||||
* Provides event notifications that fire when delete recording button has been clicked.
|
||||
* The recording should be deleted using the REST API.
|
||||
*/
|
||||
@Output() onDeleteRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
/**
|
||||
* Provides event notifications that fire when play recording button has been clicked.
|
||||
*/
|
||||
@Output() onPlayRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
recordings: RecordingInfo[] = [];
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
sortDescendent = true;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
sortByLegend = 'Sort by';
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
searchValue = '';
|
||||
private adminSubscription: Subscription;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(
|
||||
private actionService: ActionService,
|
||||
private recordingService: RecordingService,
|
||||
private libService: OpenViduAngularConfigService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.subscribeToAdminDirectives();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
if (this.adminSubscription) this.adminSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
sortRecordingsByDate() {
|
||||
this.recordings.sort((a, b) => {
|
||||
if (a.createdAt > b.createdAt) {
|
||||
return this.sortDescendent ? -1 : 1;
|
||||
} else if (a.createdAt < b.createdAt) {
|
||||
return this.sortDescendent ? 1 : -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
this.sortByLegend = 'Date';
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
sortRecordingsByDuration() {
|
||||
this.recordings.sort((a, b) => {
|
||||
if (a.duration > b.duration) {
|
||||
return this.sortDescendent ? -1 : 1;
|
||||
} else if (a.duration < b.duration) {
|
||||
return this.sortDescendent ? 1 : -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
this.sortByLegend = 'Duration';
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
sortRecordingsBySize() {
|
||||
this.recordings.sort((a, b) => {
|
||||
if (a.size > b.size) {
|
||||
return this.sortDescendent ? -1 : 1;
|
||||
} else if (a.size < b.size) {
|
||||
return this.sortDescendent ? 1 : -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
this.sortByLegend = 'Size';
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getThumbnailSrc(recording: RecordingInfo): string {
|
||||
return !recording.url ? undefined : recording.url.substring(0, recording.url.lastIndexOf('/')) + '/' + recording.id + '.jpg';
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
deleteRecording(recordingId: string) {
|
||||
const succsessCallback = () => {
|
||||
this.onDeleteRecordingClicked.emit(recordingId);
|
||||
};
|
||||
this.actionService.openDeleteRecordingDialog(succsessCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
download(recordingId: string) {
|
||||
//TODO solucionar el tema del login.
|
||||
// TODO Si soy capaz de loguearme en openvidu al hacer login en el dashboard, no necesitaria emitir evento
|
||||
this.onDownloadRecordingClicked.emit(recordingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async play(recording: RecordingInfo) {
|
||||
this.actionService.openRecordingPlayerDialog(recording.url, 'video/mp4', true);
|
||||
}
|
||||
|
||||
private subscribeToAdminDirectives() {
|
||||
this.adminSubscription = this.libService.adminRecordingsListObs.subscribe((recordings: RecordingInfo[]) => {
|
||||
this.recordings = recordings;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
mat-card {
|
||||
max-width: 220px;
|
||||
margin: auto;
|
||||
margin-top: 10vh;
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
mat-card-actions {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 50px;
|
||||
background-color: var(--ov-secondary-color);
|
||||
color: var(--ov-text-color);
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: auto;
|
||||
}
|
||||
.mat-card-actions {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.outer {
|
||||
display: table;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.middle {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.inner {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#login-btn {
|
||||
text-transform: none;
|
||||
font-size: 17px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::ng-deep .mat-input-element {
|
||||
caret-color: #000000;
|
||||
}
|
||||
::ng-deep .mat-primary .mat-option.mat-selected:not(.mat-option-disabled) {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
::ng-deep .mat-form-field-label {
|
||||
color: var(--ov-panel-text-color) !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-form-field.mat-focused .mat-form-field-ripple {
|
||||
background-color: var(--ov-panel-text-color) !important;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<mat-toolbar class="header">
|
||||
</mat-toolbar>
|
||||
|
||||
<div *ngIf="checkingLogged" class="outer">
|
||||
<div class="middle">
|
||||
<div class="inner">
|
||||
<mat-spinner *ngIf="checkingLogged"></mat-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-card *ngIf="!checkingLogged">
|
||||
<mat-card-content>
|
||||
<form ngNativeValidate #loginForm (ngSubmit)="login()">
|
||||
<table class="full-width" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<mat-form-field id="secret-field" class="full-width" appearance="outline">
|
||||
<mat-label>{{ 'ADMIN.SECRET' | translate }}</mat-label>
|
||||
<input
|
||||
id="secret-input"
|
||||
matInput
|
||||
[(ngModel)]="secret"
|
||||
[disabled]="showSpinner"
|
||||
type="password"
|
||||
name="secret"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
<mat-error *ngIf="loginFormControl.hasError('required')"> {{ 'ADMIN.SECRET_REQURED' | translate }} </mat-error>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<input #submitBtn type="submit" [style.display]="'none'" />
|
||||
</form>
|
||||
<mat-spinner [style.display]="showSpinner ? 'block' : 'none'"></mat-spinner>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-flat-button id="login-btn" type="submit" (click)="submitForm()" color="primary" class="full-width">
|
||||
{{ 'ADMIN.LOGIN' | translate }}
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
|
@ -0,0 +1,25 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminLoginComponent } from './login.component';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: AdminLoginComponent;
|
||||
let fixture: ComponentFixture<AdminLoginComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AdminLoginComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminLoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { FormControl, Validators, FormGroupDirective, NgForm } from '@angular/forms';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ActionService } from '../../services/action/action.service';
|
||||
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ov-admin-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.css']
|
||||
})
|
||||
export class AdminLoginComponent implements OnInit {
|
||||
/**
|
||||
* Provides event notifications that fire when login button has been clicked.
|
||||
* The event will contain the password value.
|
||||
*/
|
||||
@Output() onLoginButtonClicked: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
checkingLogged = false;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
secret: string;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
showSpinner = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
loginFormControl = new FormControl('', [Validators.required]);
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
matcher = new FormErrorStateMatcher();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ViewChild('submitBtn') submitBtn: ElementRef;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ViewChild('loginForm', { read: ElementRef }) loginForm: ElementRef;
|
||||
|
||||
private errorSub: Subscription;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(private libService: OpenViduAngularConfigService, private actionService: ActionService) {}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.subscribeToAdminLoginDirectives();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.showSpinner = false;
|
||||
if (this.errorSub) this.errorSub.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
login() {
|
||||
this.showSpinner = true;
|
||||
this.onLoginButtonClicked.emit(this.secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
submitForm() {
|
||||
if (this.loginForm.nativeElement.checkValidity()) {
|
||||
this.login();
|
||||
} else {
|
||||
this.submitBtn.nativeElement.click();
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToAdminLoginDirectives() {
|
||||
this.errorSub = this.libService.adminLoginErrorObs.subscribe((value) => {
|
||||
const errorExists = !!value;
|
||||
if (errorExists) {
|
||||
this.showSpinner = false;
|
||||
this.actionService.openDialog(value.error, value.message, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class FormErrorStateMatcher implements ErrorStateMatcher {
|
||||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||
const isSubmitted = form && form.submitted;
|
||||
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import { Directive, AfterViewInit, OnDestroy, Input, ElementRef } from '@angular/core';
|
||||
import { RecordingInfo } from '../../models/recording.model';
|
||||
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
|
||||
|
||||
/**
|
||||
* The **recordingsList** directive allows show all recordings saved in your OpenVidu deployment in {@link AdminDashboardComponent}.
|
||||
*
|
||||
* Default: `[]`
|
||||
*
|
||||
* @example
|
||||
* <ov-admin-dashboard [recordingsList]="recordings"></ov-admin-dashboard>
|
||||
*
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'ov-admin-dashboard[recordingsList]'
|
||||
})
|
||||
export class AdminRecordingsListDirective implements AfterViewInit, OnDestroy {
|
||||
|
||||
@Input() set recordingsList(value: RecordingInfo[]) {
|
||||
this.recordingsValue = value;
|
||||
this.update(this.recordingsValue);
|
||||
}
|
||||
|
||||
recordingsValue: RecordingInfo [] = [];
|
||||
|
||||
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.update(this.recordingsValue);
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.clear();
|
||||
}
|
||||
clear() {
|
||||
this.recordingsValue = null;
|
||||
this.update(null);
|
||||
}
|
||||
|
||||
update(value: RecordingInfo[]) {
|
||||
if (this.libService.adminRecordingsList.getValue() !== value) {
|
||||
this.libService.adminRecordingsList.next(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The **error** directive allows show the authentication error in {@link AdminLoginComponent}.
|
||||
*
|
||||
* Default: `null`
|
||||
*
|
||||
* @example
|
||||
* <ov-admin-login [error]="error"></ov-admin-login>
|
||||
*
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'ov-admin-login[error]'
|
||||
})
|
||||
export class AdminLoginDirective implements AfterViewInit, OnDestroy {
|
||||
|
||||
@Input() set error(value: any) {
|
||||
this.errorValue = value;
|
||||
this.update(this.errorValue);
|
||||
}
|
||||
|
||||
errorValue: any = null;
|
||||
|
||||
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.update(this.errorValue);
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.clear();
|
||||
}
|
||||
clear() {
|
||||
this.errorValue = null;
|
||||
this.update(null);
|
||||
}
|
||||
|
||||
update(value: any) {
|
||||
if (this.libService.adminLoginError.getValue() !== value) {
|
||||
this.libService.adminLoginError.next(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { ActivitiesPanelRecordingActivityDirective } from './activities-panel.directive';
|
||||
import { AdminLoginDirective, AdminRecordingsListDirective } from './admin.directive';
|
||||
import { LogoDirective } from './internals.directive';
|
||||
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
|
||||
import { RecordingActivityRecordingErrorDirective, RecordingActivityRecordingsListDirective } from './recording-activity.directive';
|
||||
|
@ -29,7 +30,6 @@ import {
|
|||
LangDirective
|
||||
} from './videoconference.directive';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
MinimalDirective,
|
||||
|
@ -55,7 +55,9 @@ import {
|
|||
ParticipantNameDirective,
|
||||
ActivitiesPanelRecordingActivityDirective,
|
||||
RecordingActivityRecordingsListDirective,
|
||||
RecordingActivityRecordingErrorDirective
|
||||
RecordingActivityRecordingErrorDirective,
|
||||
AdminRecordingsListDirective,
|
||||
AdminLoginDirective
|
||||
],
|
||||
exports: [
|
||||
MinimalDirective,
|
||||
|
@ -81,7 +83,9 @@ import {
|
|||
ParticipantNameDirective,
|
||||
ActivitiesPanelRecordingActivityDirective,
|
||||
RecordingActivityRecordingsListDirective,
|
||||
RecordingActivityRecordingErrorDirective
|
||||
RecordingActivityRecordingErrorDirective,
|
||||
AdminRecordingsListDirective,
|
||||
AdminLoginDirective
|
||||
]
|
||||
})
|
||||
export class ApiDirectiveModule {}
|
||||
|
|
|
@ -40,7 +40,7 @@ import { DeleteDialogComponent } from './components/dialogs/delete-recording.com
|
|||
import { LinkifyPipe } from './pipes/linkify.pipe';
|
||||
import { TranslatePipe } from './pipes/translate.pipe';
|
||||
import { StreamTypesEnabledPipe, ParticipantStreamsPipe } from './pipes/participant.pipe';
|
||||
import { DurationFromSecondsPipe } from './pipes/recording.pipe';
|
||||
import { DurationFromSecondsPipe, SearchByStringPropertyPipe } from './pipes/recording.pipe';
|
||||
|
||||
import { OpenViduAngularConfig } from './config/openvidu-angular.config';
|
||||
import { CdkOverlayContainer } from './config/custom-cdk-overlay';
|
||||
|
@ -72,6 +72,8 @@ import { ApiDirectiveModule } from './directives/api/api.directive.module';
|
|||
import { BackgroundEffectsPanelComponent } from './components/panel/background-effects-panel/background-effects-panel.component';
|
||||
import { ActivitiesPanelComponent } from './components/panel/activities-panel/activities-panel.component';
|
||||
import { RecordingActivityComponent } from './components/panel/activities-panel/recording-activity-panel/recording-activity.component';
|
||||
import { AdminDashboardComponent } from './admin/dashboard/dashboard.component';
|
||||
import { AdminLoginComponent } from './admin/login/login.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -87,6 +89,7 @@ import { RecordingActivityComponent } from './components/panel/activities-panel/
|
|||
LinkifyPipe,
|
||||
ParticipantStreamsPipe,
|
||||
DurationFromSecondsPipe,
|
||||
SearchByStringPropertyPipe,
|
||||
StreamTypesEnabledPipe,
|
||||
TranslatePipe,
|
||||
ParticipantPanelItemComponent,
|
||||
|
@ -98,7 +101,9 @@ import { RecordingActivityComponent } from './components/panel/activities-panel/
|
|||
PreJoinComponent,
|
||||
BackgroundEffectsPanelComponent,
|
||||
ActivitiesPanelComponent,
|
||||
RecordingActivityComponent
|
||||
RecordingActivityComponent,
|
||||
AdminDashboardComponent,
|
||||
AdminLoginComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -162,6 +167,8 @@ import { RecordingActivityComponent } from './components/panel/activities-panel/
|
|||
VideoComponent,
|
||||
AudioWaveComponent,
|
||||
PreJoinComponent,
|
||||
AdminDashboardComponent,
|
||||
AdminLoginComponent,
|
||||
ParticipantStreamsPipe,
|
||||
DurationFromSecondsPipe,
|
||||
StreamTypesEnabledPipe,
|
||||
|
|
|
@ -19,3 +19,34 @@ export class DurationFromSecondsPipe implements PipeTransform {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@Pipe({
|
||||
name: 'searchByStringProperty'
|
||||
})
|
||||
export class SearchByStringPropertyPipe implements PipeTransform {
|
||||
transform(items: any[], props: { properties: string[], filter: string }): any {
|
||||
if (!items || !props || props.properties.length === 0 || !props.filter) {
|
||||
return items;
|
||||
}
|
||||
return items.filter(item => {
|
||||
return props.properties.some(prop => {
|
||||
const multipleProps = prop.split('.');
|
||||
let recursiveProp = item;
|
||||
try {
|
||||
multipleProps.forEach(p => {
|
||||
recursiveProp = recursiveProp[p];
|
||||
if (recursiveProp === null || recursiveProp === undefined) {
|
||||
throw new Error('Property not found');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return recursiveProp.indexOf(props.filter) !== -1;
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,10 @@ export class OpenViduAngularConfigService {
|
|||
recordingActivityObs: Observable<boolean>;
|
||||
recordingError = <BehaviorSubject<any>>new BehaviorSubject(null);
|
||||
recordingErrorObs: Observable<any>;
|
||||
adminRecordingsList = <BehaviorSubject<RecordingInfo[]>>new BehaviorSubject([]);
|
||||
adminRecordingsListObs: Observable<RecordingInfo[]>;
|
||||
adminLoginError = <BehaviorSubject<any>>new BehaviorSubject(null);
|
||||
adminLoginErrorObs: Observable<any>;
|
||||
|
||||
constructor(@Inject('OPENVIDU_ANGULAR_CONFIG') config: OpenViduAngularConfig) {
|
||||
this.configuration = config;
|
||||
|
@ -94,6 +98,9 @@ export class OpenViduAngularConfigService {
|
|||
this.recordingActivityObs = this.recordingActivity.asObservable();
|
||||
this.recordingsListObs = this.recordingsList.asObservable();
|
||||
this.recordingErrorObs = this.recordingError.asObservable();
|
||||
// Admin dashboard
|
||||
this.adminRecordingsListObs = this.adminRecordingsList.asObservable();
|
||||
this.adminLoginErrorObs = this.adminLoginError.asObservable();
|
||||
}
|
||||
|
||||
getConfig(): OpenViduAngularConfig {
|
||||
|
|
|
@ -36,6 +36,8 @@ export * from './lib/components/stream/stream.component';
|
|||
export * from './lib/components/video/video.component';
|
||||
export * from './lib/components/audio-wave/audio-wave.component';
|
||||
export * from './lib/components/pre-join/pre-join.component';
|
||||
export * from './lib/admin/dashboard/dashboard.component';
|
||||
export * from './lib/admin/login/login.component';
|
||||
|
||||
// Models
|
||||
export * from './lib/models/participant.model';
|
||||
|
@ -63,3 +65,4 @@ export * from './lib/directives/api/videoconference.directive';
|
|||
export * from './lib/directives/api/participant-panel-item.directive';
|
||||
export * from './lib/directives/api/activities-panel.directive';
|
||||
export * from './lib/directives/api/recording-activity.directive';
|
||||
export * from './lib/directives/api/admin.directive';
|
||||
|
|
Loading…
Reference in New Issue