Commit b764380a authored by Chocobozzz's avatar Chocobozzz Committed by Chocobozzz
Browse files

Add ability to list redundancies

parent 3ae0bbd2
......@@ -77,6 +77,7 @@
"bootstrap": "^4.1.3",
"buffer": "^5.1.0",
"cache-chunk-store": "^3.0.0",
"chart.js": "^2.9.3",
"codelyzer": "^5.0.1",
"core-js": "^3.1.4",
"css-loader": "^3.1.0",
......
......@@ -5,7 +5,7 @@
</a>
<a i18n *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page">
Manage follows
Follows & redundancies
</a>
<a i18n *ngIf="hasVideoAbusesRight() || hasVideoBlacklistRight()" routerLink="/admin/moderation" routerLinkActive="active" class="title-page">
......
......@@ -5,7 +5,7 @@ import { TableModule } from 'primeng/table'
import { SharedModule } from '../shared'
import { AdminRoutingModule } from './admin-routing.module'
import { AdminComponent } from './admin.component'
import { FollowersListComponent, FollowingAddComponent, FollowsComponent } from './follows'
import { FollowersListComponent, FollowingAddComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
import { FollowingListComponent } from './follows/following-list/following-list.component'
import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
import {
......@@ -16,7 +16,6 @@ import {
} from './moderation'
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
import { JobsComponent } from '@app/+admin/system/jobs/jobs.component'
import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system'
......@@ -27,13 +26,18 @@ import { PluginSearchComponent } from '@app/+admin/plugins/plugin-search/plugin-
import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component'
import { SelectButtonModule } from 'primeng/selectbutton'
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component'
import { ChartModule } from 'primeng/chart'
@NgModule({
imports: [
AdminRoutingModule,
SharedModule,
TableModule,
SelectButtonModule,
SharedModule
ChartModule
],
declarations: [
......@@ -44,6 +48,8 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
FollowersListComponent,
FollowingListComponent,
RedundancyCheckboxComponent,
VideoRedundanciesListComponent,
VideoRedundancyInformationComponent,
UsersComponent,
UserCreateComponent,
......@@ -78,7 +84,6 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
],
providers: [
RedundancyService,
JobService,
LogsService,
DebugService,
......
<div class="admin-sub-header">
<div i18n class="form-sub-title">Manage follows</div>
<div i18n class="form-sub-title">Follows & redundancies</div>
<div class="admin-sub-nav">
<a i18n routerLink="following-list" routerLinkActive="active">Following</a>
......@@ -7,7 +7,9 @@
<a i18n routerLink="following-add" routerLinkActive="active">Follow</a>
<a i18n routerLink="followers-list" routerLinkActive="active">Followers</a>
<a i18n routerLink="video-redundancies-list" routerLinkActive="active">Video redundancies</a>
</div>
</div>
<router-outlet></router-outlet>
\ No newline at end of file
<router-outlet></router-outlet>
......@@ -6,6 +6,7 @@ import { FollowingAddComponent } from './following-add'
import { FollowersListComponent } from './followers-list'
import { UserRight } from '../../../../../shared'
import { FollowingListComponent } from './following-list/following-list.component'
import { VideoRedundanciesListComponent } from '@app/+admin/follows/video-redundancies-list'
export const FollowsRoutes: Routes = [
{
......@@ -47,6 +48,10 @@ export const FollowsRoutes: Routes = [
title: 'Add follow'
}
}
},
{
path: 'video-redundancies-list',
component: VideoRedundanciesListComponent
}
]
}
......
export * from './following-add'
export * from './followers-list'
export * from './following-list'
export * from './video-redundancies-list'
export * from './follows.component'
export * from './follows.routes'
import { Component, Input } from '@angular/core'
import { Notifier } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
import { RedundancyService } from '@app/shared/video/redundancy.service'
@Component({
selector: 'my-redundancy-checkbox',
......
import { catchError, map } from 'rxjs/operators'
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { RestExtractor } from '@app/shared'
import { environment } from '../../../../environments/environment'
@Injectable()
export class RedundancyService {
static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/server/redundancy'
constructor (
private authHttp: HttpClient,
private restExtractor: RestExtractor
) { }
updateRedundancy (host: string, redundancyAllowed: boolean) {
const url = RedundancyService.BASE_USER_SUBSCRIPTIONS_URL + '/' + host
const body = { redundancyAllowed }
return this.authHttp.put(url, body)
.pipe(
map(this.restExtractor.extractDataBool),
catchError(err => this.restExtractor.handleError(err))
)
}
}
export * from './video-redundancies-list.component'
<div class="admin-sub-header">
<div i18n class="form-sub-title">Video redundancies list</div>
<div class="select-filter-block">
<label for="displayType" i18n>Display</label>
<div class="peertube-select-container">
<select id="displayType" name="displayType" [(ngModel)]="displayType" (ngModelChange)="onDisplayTypeChanged()">
<option value="my-videos">My videos duplicated by remote instances</option>
<option value="remote-videos">Remote videos duplicated by my instance</option>
</select>
</div>
</div>
</div>
<p-table
[value]="videoRedundancies" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
>
<ng-template pTemplate="header">
<tr>
<th i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th>
<th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
<th i18n>Video URL</th>
<th i18n *ngIf="isDisplayingRemoteVideos()">Total size</th>
<th></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-redundancy>
<tr class="expander" [pRowToggler]="redundancy">
<td *ngIf="isDisplayingRemoteVideos()">{{ getRedundancyStrategy(redundancy) }}</td>
<td>{{ redundancy.name }}</td>
<td>
<a target="_blank" rel="noopener noreferrer" [href]="redundancy.url">{{ redundancy.url }}</a>
</td>
<td *ngIf="isDisplayingRemoteVideos()">{{ getTotalSize(redundancy) | bytes: 1 }}</td>
<td class="action-cell">
<my-delete-button (click)="removeRedundancy(redundancy)"></my-delete-button>
</td>
</tr>
</ng-template>
<ng-template pTemplate="rowexpansion" let-redundancy>
<tr>
<td colspan="2">
<div *ngFor="let file of redundancy.redundancies.files" class="expansion-block">
<my-video-redundancy-information [redundancyElement]="file"></my-video-redundancy-information>
</div>
</td>
</tr>
<tr>
<td colspan="2">
<div *ngFor="let playlist of redundancy.redundancies.streamingPlaylists">
<my-video-redundancy-information [redundancyElement]="playlist"></my-video-redundancy-information>
</div>
</td>
</tr>
</ng-template>
</p-table>
<div class="redundancies-charts" *ngIf="isDisplayingRemoteVideos()">
<div class="form-sub-title" i18n>Enabled strategies stats</div>
<div class="chart-blocks">
<div *ngIf="noRedundancies" i18n class="no-results">
No redundancy strategy is enabled on your instance.
</div>
<div class="chart-block" *ngFor="let r of redundanciesGraphsData">
<p-chart type="pie" [data]="r.graphData" [options]="r.options" width="300px" height="300px"></p-chart>
</div>
</div>
</div>
@import '_variables';
@import '_mixins';
.expansion-block {
margin-bottom: 20px;
}
.admin-sub-header {
align-items: flex-end;
.select-filter-block {
&:not(:last-child) {
margin-right: 10px;
}
label {
margin-bottom: 2px;
}
.peertube-select-container {
@include peertube-select-container(auto);
}
}
}
.redundancies-charts {
margin-top: 50px;
.chart-blocks {
display: flex;
justify-content: center;
.chart-block {
margin: 0 20px;
}
}
}
import { Component, OnInit } from '@angular/core'
import { Notifier, ServerService } from '@app/core'
import { SortMeta } from 'primeng/api'
import { ConfirmService } from '../../../core/confirm/confirm.service'
import { RestPagination, RestTable } from '../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage'
import { VideosRedundancyStats } from '@shared/models/server'
import { BytesPipe } from 'ngx-pipes'
import { RedundancyService } from '@app/shared/video/redundancy.service'
@Component({
selector: 'my-video-redundancies-list',
templateUrl: './video-redundancies-list.component.html',
styleUrls: [ './video-redundancies-list.component.scss' ]
})
export class VideoRedundanciesListComponent extends RestTable implements OnInit {
private static LOCAL_STORAGE_DISPLAY_TYPE = 'video-redundancies-list-display-type'
videoRedundancies: VideoRedundancy[] = []
totalRecords = 0
rowsPerPage = 10
sort: SortMeta = { field: 'name', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
displayType: VideoRedundanciesTarget = 'my-videos'
redundanciesGraphsData: { stats: VideosRedundancyStats, graphData: object, options: object }[] = []
noRedundancies = false
private bytesPipe: BytesPipe
constructor (
private notifier: Notifier,
private confirmService: ConfirmService,
private redundancyService: RedundancyService,
private serverService: ServerService,
private i18n: I18n
) {
super()
this.bytesPipe = new BytesPipe()
}
ngOnInit () {
this.loadSelectLocalStorage()
this.initialize()
this.serverService.getServerStats()
.subscribe(res => {
const redundancies = res.videosRedundancy
if (redundancies.length === 0) this.noRedundancies = true
for (const r of redundancies) {
this.buildPieData(r)
}
})
}
isDisplayingRemoteVideos () {
return this.displayType === 'remote-videos'
}
getTotalSize (redundancy: VideoRedundancy) {
return redundancy.redundancies.files.reduce((a, b) => a + b.size, 0) +
redundancy.redundancies.streamingPlaylists.reduce((a, b) => a + b.size, 0)
}
onDisplayTypeChanged () {
this.pagination.start = 0
this.saveSelectLocalStorage()
this.loadData()
}
getRedundancyStrategy (redundancy: VideoRedundancy) {
if (redundancy.redundancies.files.length !== 0) return redundancy.redundancies.files[0].strategy
if (redundancy.redundancies.streamingPlaylists.length !== 0) return redundancy.redundancies.streamingPlaylists[0].strategy
return ''
}
buildPieData (stats: VideosRedundancyStats) {
const totalSize = stats.totalSize
? stats.totalSize - stats.totalUsed
: stats.totalUsed
if (totalSize === 0) return
this.redundanciesGraphsData.push({
stats,
graphData: {
labels: [ this.i18n('Used'), this.i18n('Available') ],
datasets: [
{
data: [ stats.totalUsed, totalSize ],
backgroundColor: [
'#FF6384',
'#36A2EB'
],
hoverBackgroundColor: [
'#FF6384',
'#36A2EB'
]
}
]
},
options: {
title: {
display: true,
text: stats.strategy
},
tooltips: {
callbacks: {
label: (tooltipItem: any, data: any) => {
const dataset = data.datasets[tooltipItem.datasetIndex]
let label = data.labels[tooltipItem.index]
if (label) label += ': '
else label = ''
label += this.bytesPipe.transform(dataset.data[tooltipItem.index], 1)
return label
}
}
}
}
})
}
async removeRedundancy (redundancy: VideoRedundancy) {
const message = this.i18n('Do you really want to remove this video redundancy?')
const res = await this.confirmService.confirm(message, this.i18n('Remove redundancy'))
if (res === false) return
this.redundancyService.removeVideoRedundancies(redundancy)
.subscribe(
() => {
this.notifier.success(this.i18n('Video redundancies removed!'))
this.loadData()
},
err => this.notifier.error(err.message)
)
}
protected loadData () {
const options = {
pagination: this.pagination,
sort: this.sort,
target: this.displayType
}
this.redundancyService.listVideoRedundancies(options)
.subscribe(
resultList => {
this.videoRedundancies = resultList.data
this.totalRecords = resultList.total
},
err => this.notifier.error(err.message)
)
}
private loadSelectLocalStorage () {
const displayType = peertubeLocalStorage.getItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE)
if (displayType) this.displayType = displayType as VideoRedundanciesTarget
}
private saveSelectLocalStorage () {
peertubeLocalStorage.setItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE, this.displayType)
}
}
<div>
<span class="label">Url</span>
<a target="_blank" rel="noopener noreferrer" [href]="redundancyElement.fileUrl">{{ redundancyElement.fileUrl }}</a>
</div>
<div>
<span class="label">Created on</span>
<span>{{ redundancyElement.createdAt | date: 'medium' }}</span>
</div>
<div>
<span class="label">Expires on</span>
<span>{{ redundancyElement.expiresOn | date: 'medium' }}</span>
</div>
<div>
<span class="label">Size</span>
<span>{{ redundancyElement.size | bytes: 1 }}</span>
</div>
<div *ngIf="redundancyElement.strategy">
<span class="label">Strategy</span>
<span>{{ redundancyElement.strategy }}</span>
</div>
@import '_variables';
@import '_mixins';
.label {
display: inline-block;
min-width: 100px;
font-weight: $font-semibold;
}
import { Component, Input } from '@angular/core'
import { FileRedundancyInformation, StreamingPlaylistRedundancyInformation } from '@shared/models'
@Component({
selector: 'my-video-redundancy-information',
templateUrl: './video-redundancy-information.component.html',
styleUrls: [ './video-redundancy-information.component.scss' ]
})
export class VideoRedundancyInformationComponent {
@Input() redundancyElement: FileRedundancyInformation | StreamingPlaylistRedundancyInformation
}
......@@ -16,8 +16,8 @@ import { JobTypeClient } from '../../../../types/job-type-client.type'
styleUrls: [ './jobs.component.scss' ]
})
export class JobsComponent extends RestTable implements OnInit {
private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state'
private static JOB_STATE_LOCAL_STORAGE_TYPE = 'jobs-list-type'
private static LOCAL_STORAGE_STATE = 'jobs-list-state'
private static LOCAL_STORAGE_TYPE = 'jobs-list-type'
jobState: JobStateClient = 'waiting'
jobStates: JobStateClient[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed' ]
......@@ -34,7 +34,8 @@ export class JobsComponent extends RestTable implements OnInit {
'video-file-import',
'video-import',
'videos-views',
'activitypub-refresher'
'activitypub-refresher',
'video-redundancy'
]
jobs: Job[] = []
......@@ -77,15 +78,15 @@ export class JobsComponent extends RestTable implements OnInit {
}
private loadJobStateAndType () {
const state = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE)
const state = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_STATE)
if (state) this.jobState = state as JobState
const type = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_TYPE)
const type = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_TYPE)
if (type) this.jobType = type as JobType
}
private saveJobStateAndType () {
peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState)
peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_TYPE, this.jobType)
peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_STATE, this.jobState)
peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_TYPE, this.jobType)
}
}
......@@ -9,6 +9,7 @@ import { VideoConstant } from '../../../../../shared/models/videos'
import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n'
import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
import { sortBy } from '@app/shared/misc/utils'
import { ServerStats } from '@shared/models/server'
@Injectable()
export class ServerService {
......@@ -16,6 +17,8 @@ export class ServerService {
private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/'
private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
configReloaded = new Subject<void>()
......@@ -235,6 +238,10 @@ export class ServerService {
return this.localeObservable.pipe(first())
}
getServerStats () {
return this.http.get<ServerStats>(ServerService.BASE_STATS_URL)
}
private loadAttributeEnum <T extends string | number> (
baseUrl: string,
attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
......
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core'
import { HooksService } from '@app/core/plugins/hooks.service'