openvidu-components: Added admin dashboard

pull/739/head
csantosm 2022-06-10 11:27:43 +02:00
parent d37809ac5b
commit 15a7037b04
15 changed files with 900 additions and 5 deletions

View File

@ -1,5 +1,6 @@
{ {
"include": [ "include": [
"../src/lib/admin/**/*.ts",
"../src/lib/components/**/*.ts", "../src/lib/components/**/*.ts",
"../src/lib/directives/**/*.ts", "../src/lib/directives/**/*.ts",
"../src/lib/services/**/*.ts", "../src/lib/services/**/*.ts",

View File

@ -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;
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
});
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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));
}
}

View File

@ -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);
}
}
}

View File

@ -1,5 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ActivitiesPanelRecordingActivityDirective } from './activities-panel.directive'; import { ActivitiesPanelRecordingActivityDirective } from './activities-panel.directive';
import { AdminLoginDirective, AdminRecordingsListDirective } from './admin.directive';
import { LogoDirective } from './internals.directive'; import { LogoDirective } from './internals.directive';
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive'; import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
import { RecordingActivityRecordingErrorDirective, RecordingActivityRecordingsListDirective } from './recording-activity.directive'; import { RecordingActivityRecordingErrorDirective, RecordingActivityRecordingsListDirective } from './recording-activity.directive';
@ -29,7 +30,6 @@ import {
LangDirective LangDirective
} from './videoconference.directive'; } from './videoconference.directive';
@NgModule({ @NgModule({
declarations: [ declarations: [
MinimalDirective, MinimalDirective,
@ -55,7 +55,9 @@ import {
ParticipantNameDirective, ParticipantNameDirective,
ActivitiesPanelRecordingActivityDirective, ActivitiesPanelRecordingActivityDirective,
RecordingActivityRecordingsListDirective, RecordingActivityRecordingsListDirective,
RecordingActivityRecordingErrorDirective RecordingActivityRecordingErrorDirective,
AdminRecordingsListDirective,
AdminLoginDirective
], ],
exports: [ exports: [
MinimalDirective, MinimalDirective,
@ -81,7 +83,9 @@ import {
ParticipantNameDirective, ParticipantNameDirective,
ActivitiesPanelRecordingActivityDirective, ActivitiesPanelRecordingActivityDirective,
RecordingActivityRecordingsListDirective, RecordingActivityRecordingsListDirective,
RecordingActivityRecordingErrorDirective RecordingActivityRecordingErrorDirective,
AdminRecordingsListDirective,
AdminLoginDirective
] ]
}) })
export class ApiDirectiveModule {} export class ApiDirectiveModule {}

View File

@ -40,7 +40,7 @@ import { DeleteDialogComponent } from './components/dialogs/delete-recording.com
import { LinkifyPipe } from './pipes/linkify.pipe'; import { LinkifyPipe } from './pipes/linkify.pipe';
import { TranslatePipe } from './pipes/translate.pipe'; import { TranslatePipe } from './pipes/translate.pipe';
import { StreamTypesEnabledPipe, ParticipantStreamsPipe } from './pipes/participant.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 { OpenViduAngularConfig } from './config/openvidu-angular.config';
import { CdkOverlayContainer } from './config/custom-cdk-overlay'; 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 { BackgroundEffectsPanelComponent } from './components/panel/background-effects-panel/background-effects-panel.component';
import { ActivitiesPanelComponent } from './components/panel/activities-panel/activities-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 { 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({ @NgModule({
declarations: [ declarations: [
@ -87,6 +89,7 @@ import { RecordingActivityComponent } from './components/panel/activities-panel/
LinkifyPipe, LinkifyPipe,
ParticipantStreamsPipe, ParticipantStreamsPipe,
DurationFromSecondsPipe, DurationFromSecondsPipe,
SearchByStringPropertyPipe,
StreamTypesEnabledPipe, StreamTypesEnabledPipe,
TranslatePipe, TranslatePipe,
ParticipantPanelItemComponent, ParticipantPanelItemComponent,
@ -98,7 +101,9 @@ import { RecordingActivityComponent } from './components/panel/activities-panel/
PreJoinComponent, PreJoinComponent,
BackgroundEffectsPanelComponent, BackgroundEffectsPanelComponent,
ActivitiesPanelComponent, ActivitiesPanelComponent,
RecordingActivityComponent RecordingActivityComponent,
AdminDashboardComponent,
AdminLoginComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -162,6 +167,8 @@ import { RecordingActivityComponent } from './components/panel/activities-panel/
VideoComponent, VideoComponent,
AudioWaveComponent, AudioWaveComponent,
PreJoinComponent, PreJoinComponent,
AdminDashboardComponent,
AdminLoginComponent,
ParticipantStreamsPipe, ParticipantStreamsPipe,
DurationFromSecondsPipe, DurationFromSecondsPipe,
StreamTypesEnabledPipe, StreamTypesEnabledPipe,

View File

@ -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;
})
});
}
}

View File

@ -63,6 +63,10 @@ export class OpenViduAngularConfigService {
recordingActivityObs: Observable<boolean>; recordingActivityObs: Observable<boolean>;
recordingError = <BehaviorSubject<any>>new BehaviorSubject(null); recordingError = <BehaviorSubject<any>>new BehaviorSubject(null);
recordingErrorObs: Observable<any>; 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) { constructor(@Inject('OPENVIDU_ANGULAR_CONFIG') config: OpenViduAngularConfig) {
this.configuration = config; this.configuration = config;
@ -94,6 +98,9 @@ export class OpenViduAngularConfigService {
this.recordingActivityObs = this.recordingActivity.asObservable(); this.recordingActivityObs = this.recordingActivity.asObservable();
this.recordingsListObs = this.recordingsList.asObservable(); this.recordingsListObs = this.recordingsList.asObservable();
this.recordingErrorObs = this.recordingError.asObservable(); this.recordingErrorObs = this.recordingError.asObservable();
// Admin dashboard
this.adminRecordingsListObs = this.adminRecordingsList.asObservable();
this.adminLoginErrorObs = this.adminLoginError.asObservable();
} }
getConfig(): OpenViduAngularConfig { getConfig(): OpenViduAngularConfig {

View File

@ -36,6 +36,8 @@ export * from './lib/components/stream/stream.component';
export * from './lib/components/video/video.component'; export * from './lib/components/video/video.component';
export * from './lib/components/audio-wave/audio-wave.component'; export * from './lib/components/audio-wave/audio-wave.component';
export * from './lib/components/pre-join/pre-join.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 // Models
export * from './lib/models/participant.model'; 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/participant-panel-item.directive';
export * from './lib/directives/api/activities-panel.directive'; export * from './lib/directives/api/activities-panel.directive';
export * from './lib/directives/api/recording-activity.directive'; export * from './lib/directives/api/recording-activity.directive';
export * from './lib/directives/api/admin.directive';