diff options
Diffstat (limited to 'client/src/app/+admin/follows/video-redundancies-list')
7 files changed, 341 insertions, 0 deletions
diff --git a/client/src/app/+admin/follows/video-redundancies-list/index.ts b/client/src/app/+admin/follows/video-redundancies-list/index.ts new file mode 100644 index 000000000..6a7c7f483 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './video-redundancies-list.component' | |||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html new file mode 100644 index 000000000..80c66ec60 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html | |||
@@ -0,0 +1,82 @@ | |||
1 | <div class="admin-sub-header"> | ||
2 | <div i18n class="form-sub-title">Video redundancies list</div> | ||
3 | |||
4 | <div class="select-filter-block"> | ||
5 | <label for="displayType" i18n>Display</label> | ||
6 | |||
7 | <div class="peertube-select-container"> | ||
8 | <select id="displayType" name="displayType" [(ngModel)]="displayType" (ngModelChange)="onDisplayTypeChanged()"> | ||
9 | <option value="my-videos">My videos duplicated by remote instances</option> | ||
10 | <option value="remote-videos">Remote videos duplicated by my instance</option> | ||
11 | </select> | ||
12 | </div> | ||
13 | </div> | ||
14 | </div> | ||
15 | |||
16 | <p-table | ||
17 | [value]="videoRedundancies" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
18 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" | ||
19 | > | ||
20 | <ng-template pTemplate="header"> | ||
21 | <tr> | ||
22 | <th i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th> | ||
23 | <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th> | ||
24 | <th i18n>Video URL</th> | ||
25 | <th i18n *ngIf="isDisplayingRemoteVideos()">Total size</th> | ||
26 | <th></th> | ||
27 | </tr> | ||
28 | </ng-template> | ||
29 | |||
30 | <ng-template pTemplate="body" let-redundancy> | ||
31 | <tr class="expander" [pRowToggler]="redundancy"> | ||
32 | <td *ngIf="isDisplayingRemoteVideos()">{{ getRedundancyStrategy(redundancy) }}</td> | ||
33 | |||
34 | <td>{{ redundancy.name }}</td> | ||
35 | |||
36 | <td> | ||
37 | <a target="_blank" rel="noopener noreferrer" [href]="redundancy.url">{{ redundancy.url }}</a> | ||
38 | </td> | ||
39 | |||
40 | <td *ngIf="isDisplayingRemoteVideos()">{{ getTotalSize(redundancy) | bytes: 1 }}</td> | ||
41 | |||
42 | <td class="action-cell"> | ||
43 | <my-delete-button (click)="removeRedundancy(redundancy)"></my-delete-button> | ||
44 | </td> | ||
45 | </tr> | ||
46 | </ng-template> | ||
47 | |||
48 | <ng-template pTemplate="rowexpansion" let-redundancy> | ||
49 | <tr> | ||
50 | <td colspan="2"> | ||
51 | <div *ngFor="let file of redundancy.redundancies.files" class="expansion-block"> | ||
52 | <my-video-redundancy-information [redundancyElement]="file"></my-video-redundancy-information> | ||
53 | </div> | ||
54 | </td> | ||
55 | </tr> | ||
56 | |||
57 | <tr> | ||
58 | <td colspan="2"> | ||
59 | <div *ngFor="let playlist of redundancy.redundancies.streamingPlaylists"> | ||
60 | <my-video-redundancy-information [redundancyElement]="playlist"></my-video-redundancy-information> | ||
61 | </div> | ||
62 | </td> | ||
63 | </tr> | ||
64 | </ng-template> | ||
65 | </p-table> | ||
66 | |||
67 | |||
68 | <div class="redundancies-charts" *ngIf="isDisplayingRemoteVideos()"> | ||
69 | <div class="form-sub-title" i18n>Enabled strategies stats</div> | ||
70 | |||
71 | <div class="chart-blocks"> | ||
72 | |||
73 | <div *ngIf="noRedundancies" i18n class="no-results"> | ||
74 | No redundancy strategy is enabled on your instance. | ||
75 | </div> | ||
76 | |||
77 | <div class="chart-block" *ngFor="let r of redundanciesGraphsData"> | ||
78 | <p-chart type="pie" [data]="r.graphData" [options]="r.options" width="300px" height="300px"></p-chart> | ||
79 | </div> | ||
80 | |||
81 | </div> | ||
82 | </div> | ||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss new file mode 100644 index 000000000..05018c281 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss | |||
@@ -0,0 +1,37 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .expansion-block { | ||
5 | margin-bottom: 20px; | ||
6 | } | ||
7 | |||
8 | .admin-sub-header { | ||
9 | align-items: flex-end; | ||
10 | |||
11 | .select-filter-block { | ||
12 | &:not(:last-child) { | ||
13 | margin-right: 10px; | ||
14 | } | ||
15 | |||
16 | label { | ||
17 | margin-bottom: 2px; | ||
18 | } | ||
19 | |||
20 | .peertube-select-container { | ||
21 | @include peertube-select-container(auto); | ||
22 | } | ||
23 | } | ||
24 | } | ||
25 | |||
26 | .redundancies-charts { | ||
27 | margin-top: 50px; | ||
28 | |||
29 | .chart-blocks { | ||
30 | display: flex; | ||
31 | justify-content: center; | ||
32 | |||
33 | .chart-block { | ||
34 | margin: 0 20px; | ||
35 | } | ||
36 | } | ||
37 | } | ||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts new file mode 100644 index 000000000..4b41d1d86 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts | |||
@@ -0,0 +1,178 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Notifier, ServerService } from '@app/core' | ||
3 | import { SortMeta } from 'primeng/api' | ||
4 | import { ConfirmService } from '../../../core/confirm/confirm.service' | ||
5 | import { RestPagination, RestTable } from '../../../shared' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' | ||
8 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage' | ||
9 | import { VideosRedundancyStats } from '@shared/models/server' | ||
10 | import { BytesPipe } from 'ngx-pipes' | ||
11 | import { RedundancyService } from '@app/shared/video/redundancy.service' | ||
12 | |||
13 | @Component({ | ||
14 | selector: 'my-video-redundancies-list', | ||
15 | templateUrl: './video-redundancies-list.component.html', | ||
16 | styleUrls: [ './video-redundancies-list.component.scss' ] | ||
17 | }) | ||
18 | export class VideoRedundanciesListComponent extends RestTable implements OnInit { | ||
19 | private static LOCAL_STORAGE_DISPLAY_TYPE = 'video-redundancies-list-display-type' | ||
20 | |||
21 | videoRedundancies: VideoRedundancy[] = [] | ||
22 | totalRecords = 0 | ||
23 | rowsPerPage = 10 | ||
24 | |||
25 | sort: SortMeta = { field: 'name', order: 1 } | ||
26 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
27 | displayType: VideoRedundanciesTarget = 'my-videos' | ||
28 | |||
29 | redundanciesGraphsData: { stats: VideosRedundancyStats, graphData: object, options: object }[] = [] | ||
30 | |||
31 | noRedundancies = false | ||
32 | |||
33 | private bytesPipe: BytesPipe | ||
34 | |||
35 | constructor ( | ||
36 | private notifier: Notifier, | ||
37 | private confirmService: ConfirmService, | ||
38 | private redundancyService: RedundancyService, | ||
39 | private serverService: ServerService, | ||
40 | private i18n: I18n | ||
41 | ) { | ||
42 | super() | ||
43 | |||
44 | this.bytesPipe = new BytesPipe() | ||
45 | } | ||
46 | |||
47 | ngOnInit () { | ||
48 | this.loadSelectLocalStorage() | ||
49 | |||
50 | this.initialize() | ||
51 | |||
52 | this.serverService.getServerStats() | ||
53 | .subscribe(res => { | ||
54 | const redundancies = res.videosRedundancy | ||
55 | |||
56 | if (redundancies.length === 0) this.noRedundancies = true | ||
57 | |||
58 | for (const r of redundancies) { | ||
59 | this.buildPieData(r) | ||
60 | } | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | isDisplayingRemoteVideos () { | ||
65 | return this.displayType === 'remote-videos' | ||
66 | } | ||
67 | |||
68 | getTotalSize (redundancy: VideoRedundancy) { | ||
69 | return redundancy.redundancies.files.reduce((a, b) => a + b.size, 0) + | ||
70 | redundancy.redundancies.streamingPlaylists.reduce((a, b) => a + b.size, 0) | ||
71 | } | ||
72 | |||
73 | onDisplayTypeChanged () { | ||
74 | this.pagination.start = 0 | ||
75 | this.saveSelectLocalStorage() | ||
76 | |||
77 | this.loadData() | ||
78 | } | ||
79 | |||
80 | getRedundancyStrategy (redundancy: VideoRedundancy) { | ||
81 | if (redundancy.redundancies.files.length !== 0) return redundancy.redundancies.files[0].strategy | ||
82 | if (redundancy.redundancies.streamingPlaylists.length !== 0) return redundancy.redundancies.streamingPlaylists[0].strategy | ||
83 | |||
84 | return '' | ||
85 | } | ||
86 | |||
87 | buildPieData (stats: VideosRedundancyStats) { | ||
88 | const totalSize = stats.totalSize | ||
89 | ? stats.totalSize - stats.totalUsed | ||
90 | : stats.totalUsed | ||
91 | |||
92 | if (totalSize === 0) return | ||
93 | |||
94 | this.redundanciesGraphsData.push({ | ||
95 | stats, | ||
96 | graphData: { | ||
97 | labels: [ this.i18n('Used'), this.i18n('Available') ], | ||
98 | datasets: [ | ||
99 | { | ||
100 | data: [ stats.totalUsed, totalSize ], | ||
101 | backgroundColor: [ | ||
102 | '#FF6384', | ||
103 | '#36A2EB' | ||
104 | ], | ||
105 | hoverBackgroundColor: [ | ||
106 | '#FF6384', | ||
107 | '#36A2EB' | ||
108 | ] | ||
109 | } | ||
110 | ] | ||
111 | }, | ||
112 | options: { | ||
113 | title: { | ||
114 | display: true, | ||
115 | text: stats.strategy | ||
116 | }, | ||
117 | |||
118 | tooltips: { | ||
119 | callbacks: { | ||
120 | label: (tooltipItem: any, data: any) => { | ||
121 | const dataset = data.datasets[tooltipItem.datasetIndex] | ||
122 | let label = data.labels[tooltipItem.index] | ||
123 | if (label) label += ': ' | ||
124 | else label = '' | ||
125 | |||
126 | label += this.bytesPipe.transform(dataset.data[tooltipItem.index], 1) | ||
127 | return label | ||
128 | } | ||
129 | } | ||
130 | } | ||
131 | } | ||
132 | }) | ||
133 | } | ||
134 | |||
135 | async removeRedundancy (redundancy: VideoRedundancy) { | ||
136 | const message = this.i18n('Do you really want to remove this video redundancy?') | ||
137 | const res = await this.confirmService.confirm(message, this.i18n('Remove redundancy')) | ||
138 | if (res === false) return | ||
139 | |||
140 | this.redundancyService.removeVideoRedundancies(redundancy) | ||
141 | .subscribe( | ||
142 | () => { | ||
143 | this.notifier.success(this.i18n('Video redundancies removed!')) | ||
144 | this.loadData() | ||
145 | }, | ||
146 | |||
147 | err => this.notifier.error(err.message) | ||
148 | ) | ||
149 | |||
150 | } | ||
151 | |||
152 | protected loadData () { | ||
153 | const options = { | ||
154 | pagination: this.pagination, | ||
155 | sort: this.sort, | ||
156 | target: this.displayType | ||
157 | } | ||
158 | |||
159 | this.redundancyService.listVideoRedundancies(options) | ||
160 | .subscribe( | ||
161 | resultList => { | ||
162 | this.videoRedundancies = resultList.data | ||
163 | this.totalRecords = resultList.total | ||
164 | }, | ||
165 | |||
166 | err => this.notifier.error(err.message) | ||
167 | ) | ||
168 | } | ||
169 | |||
170 | private loadSelectLocalStorage () { | ||
171 | const displayType = peertubeLocalStorage.getItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE) | ||
172 | if (displayType) this.displayType = displayType as VideoRedundanciesTarget | ||
173 | } | ||
174 | |||
175 | private saveSelectLocalStorage () { | ||
176 | peertubeLocalStorage.setItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE, this.displayType) | ||
177 | } | ||
178 | } | ||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html new file mode 100644 index 000000000..a379520e3 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html | |||
@@ -0,0 +1,24 @@ | |||
1 | <div> | ||
2 | <span class="label">Url</span> | ||
3 | <a target="_blank" rel="noopener noreferrer" [href]="redundancyElement.fileUrl">{{ redundancyElement.fileUrl }}</a> | ||
4 | </div> | ||
5 | |||
6 | <div> | ||
7 | <span class="label">Created on</span> | ||
8 | <span>{{ redundancyElement.createdAt | date: 'medium' }}</span> | ||
9 | </div> | ||
10 | |||
11 | <div> | ||
12 | <span class="label">Expires on</span> | ||
13 | <span>{{ redundancyElement.expiresOn | date: 'medium' }}</span> | ||
14 | </div> | ||
15 | |||
16 | <div> | ||
17 | <span class="label">Size</span> | ||
18 | <span>{{ redundancyElement.size | bytes: 1 }}</span> | ||
19 | </div> | ||
20 | |||
21 | <div *ngIf="redundancyElement.strategy"> | ||
22 | <span class="label">Strategy</span> | ||
23 | <span>{{ redundancyElement.strategy }}</span> | ||
24 | </div> | ||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss new file mode 100644 index 000000000..6b09fbb01 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss | |||
@@ -0,0 +1,8 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .label { | ||
5 | display: inline-block; | ||
6 | min-width: 100px; | ||
7 | font-weight: $font-semibold; | ||
8 | } | ||
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts new file mode 100644 index 000000000..6f3090c08 --- /dev/null +++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { FileRedundancyInformation, StreamingPlaylistRedundancyInformation } from '@shared/models' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-video-redundancy-information', | ||
6 | templateUrl: './video-redundancy-information.component.html', | ||
7 | styleUrls: [ './video-redundancy-information.component.scss' ] | ||
8 | }) | ||
9 | export class VideoRedundancyInformationComponent { | ||
10 | @Input() redundancyElement: FileRedundancyInformation | StreamingPlaylistRedundancyInformation | ||
11 | } | ||