factorize account/server blocklists for users and instance (#2875)
This commit is contained in:
parent
7dfe352886
commit
228393302d
30 changed files with 326 additions and 341 deletions
|
@ -27,7 +27,6 @@ import { SelectButtonModule } from 'primeng/selectbutton'
|
||||||
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
|
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
|
||||||
import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component'
|
import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component'
|
||||||
import { ChartModule } from 'primeng/chart'
|
import { ChartModule } from 'primeng/chart'
|
||||||
import { BatchDomainsModalComponent } from './config/shared/batch-domains-modal.component'
|
|
||||||
import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-abuse-details.component'
|
import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-abuse-details.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -76,9 +75,7 @@ import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-
|
||||||
DebugComponent,
|
DebugComponent,
|
||||||
|
|
||||||
ConfigComponent,
|
ConfigComponent,
|
||||||
EditCustomConfigComponent,
|
EditCustomConfigComponent
|
||||||
|
|
||||||
BatchDomainsModalComponent
|
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
<ng-template pTemplate="emptymessage">
|
<ng-template pTemplate="emptymessage">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<div class="empty-table-message">
|
<div class="no-results">
|
||||||
<ng-container *ngIf="search" i18n>No follower found matching current filters.</ng-container>
|
<ng-container *ngIf="search" i18n>No follower found matching current filters.</ng-container>
|
||||||
<ng-container *ngIf="!search" i18n>Your instance doesn't have any follower.</ng-container>
|
<ng-container *ngIf="!search" i18n>Your instance doesn't have any follower.</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
<ng-template pTemplate="emptymessage">
|
<ng-template pTemplate="emptymessage">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<div class="empty-table-message">
|
<div class="no-results">
|
||||||
<ng-container *ngIf="search" i18n>No host found matching current filters.</ng-container>
|
<ng-container *ngIf="search" i18n>No host found matching current filters.</ng-container>
|
||||||
<ng-container *ngIf="!search" i18n>Your instance is not following anyone.</ng-container>
|
<ng-container *ngIf="!search" i18n>Your instance is not following anyone.</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,3 @@
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
margin-right: 30px;
|
margin-right: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-table-message {
|
|
||||||
@include empty-state;
|
|
||||||
}
|
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
<ng-template pTemplate="emptymessage">
|
<ng-template pTemplate="emptymessage">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<div class="empty-table-message">
|
<div class="no-results">
|
||||||
<ng-container *ngIf="isDisplayingRemoteVideos()" i18n>Your instance doesn't mirror any video.</ng-container>
|
<ng-container *ngIf="isDisplayingRemoteVideos()" i18n>Your instance doesn't mirror any video.</ng-container>
|
||||||
<ng-container *ngIf="!isDisplayingRemoteVideos()" i18n>Your instance has no mirrored videos.</ng-container>
|
<ng-container *ngIf="!isDisplayingRemoteVideos()" i18n>Your instance has no mirrored videos.</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
<ng-template pTemplate="emptymessage">
|
<ng-template pTemplate="emptymessage">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<div class="empty-table-message">
|
<div class="no-results">
|
||||||
<ng-container *ngIf="search" i18n>No account found matching current filters.</ng-container>
|
<ng-container *ngIf="search" i18n>No account found matching current filters.</ng-container>
|
||||||
<ng-container *ngIf="!search" i18n>No account found.</ng-container>
|
<ng-container *ngIf="!search" i18n>No account found.</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,70 +1,15 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { Notifier } from '@app/core'
|
import { GenericAccountBlocklistComponent, BlocklistComponentType } from '@app/shared/blocklist'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
|
||||||
import { RestPagination, RestTable } from '@app/shared'
|
|
||||||
import { SortMeta } from 'primeng/api'
|
|
||||||
import { AccountBlock, BlocklistService } from '@app/shared/blocklist'
|
|
||||||
import { Actor } from '@app/shared/actor/actor.model'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-instance-account-blocklist',
|
selector: 'my-instance-account-blocklist',
|
||||||
styleUrls: [ '../moderation.component.scss', './instance-account-blocklist.component.scss' ],
|
styleUrls: [ '../moderation.component.scss', '../../../shared/blocklist/account-blocklist.component.scss' ],
|
||||||
templateUrl: './instance-account-blocklist.component.html'
|
templateUrl: '../../../shared/blocklist/account-blocklist.component.html'
|
||||||
})
|
})
|
||||||
export class InstanceAccountBlocklistComponent extends RestTable implements OnInit {
|
export class InstanceAccountBlocklistComponent extends GenericAccountBlocklistComponent {
|
||||||
blockedAccounts: AccountBlock[] = []
|
mode = BlocklistComponentType.Instance
|
||||||
totalRecords = 0
|
|
||||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
|
||||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
|
||||||
|
|
||||||
constructor (
|
|
||||||
private notifier: Notifier,
|
|
||||||
private blocklistService: BlocklistService,
|
|
||||||
private i18n: I18n
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
getIdentifier () {
|
getIdentifier () {
|
||||||
return 'InstanceAccountBlocklistComponent'
|
return 'InstanceAccountBlocklistComponent'
|
||||||
}
|
}
|
||||||
|
|
||||||
switchToDefaultAvatar ($event: Event) {
|
|
||||||
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
|
|
||||||
}
|
|
||||||
|
|
||||||
unblockAccount (accountBlock: AccountBlock) {
|
|
||||||
const blockedAccount = accountBlock.blockedAccount
|
|
||||||
|
|
||||||
this.blocklistService.unblockAccountByInstance(blockedAccount)
|
|
||||||
.subscribe(
|
|
||||||
() => {
|
|
||||||
this.notifier.success(
|
|
||||||
this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost })
|
|
||||||
)
|
|
||||||
|
|
||||||
this.loadData()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadData () {
|
|
||||||
return this.blocklistService.getInstanceAccountBlocklist({
|
|
||||||
pagination: this.pagination,
|
|
||||||
sort: this.sort,
|
|
||||||
search: this.search
|
|
||||||
})
|
|
||||||
.subscribe(
|
|
||||||
resultList => {
|
|
||||||
this.blockedAccounts = resultList.data
|
|
||||||
this.totalRecords = resultList.total
|
|
||||||
},
|
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,84 +1,15 @@
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { Notifier } from '@app/core'
|
import { GenericServerBlocklistComponent, BlocklistComponentType } from '@app/shared/blocklist'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
|
||||||
import { RestPagination, RestTable } from '@app/shared'
|
|
||||||
import { SortMeta } from 'primeng/api'
|
|
||||||
import { BlocklistService } from '@app/shared/blocklist'
|
|
||||||
import { ServerBlock } from '../../../../../../shared'
|
|
||||||
import { BatchDomainsModalComponent } from '@app/+admin/config/shared/batch-domains-modal.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-instance-server-blocklist',
|
selector: 'my-instance-server-blocklist',
|
||||||
styleUrls: [ '../moderation.component.scss', './instance-server-blocklist.component.scss' ],
|
styleUrls: [ '../../../shared/blocklist/server-blocklist.component.scss' ],
|
||||||
templateUrl: './instance-server-blocklist.component.html'
|
templateUrl: '../../../shared/blocklist/server-blocklist.component.html'
|
||||||
})
|
})
|
||||||
export class InstanceServerBlocklistComponent extends RestTable implements OnInit {
|
export class InstanceServerBlocklistComponent extends GenericServerBlocklistComponent {
|
||||||
@ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent
|
mode = BlocklistComponentType.Instance
|
||||||
|
|
||||||
blockedServers: ServerBlock[] = []
|
|
||||||
totalRecords = 0
|
|
||||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
|
||||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
|
||||||
|
|
||||||
constructor (
|
|
||||||
private notifier: Notifier,
|
|
||||||
private blocklistService: BlocklistService,
|
|
||||||
private i18n: I18n
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
getIdentifier () {
|
getIdentifier () {
|
||||||
return 'InstanceServerBlocklistComponent'
|
return 'InstanceServerBlocklistComponent'
|
||||||
}
|
}
|
||||||
|
|
||||||
unblockServer (serverBlock: ServerBlock) {
|
|
||||||
const host = serverBlock.blockedServer.host
|
|
||||||
|
|
||||||
this.blocklistService.unblockServerByInstance(host)
|
|
||||||
.subscribe(
|
|
||||||
() => {
|
|
||||||
this.notifier.success(this.i18n('Instance {{host}} unmuted by your instance.', { host }))
|
|
||||||
|
|
||||||
this.loadData()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
addServersToBlock () {
|
|
||||||
this.batchDomainsModal.openModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
onDomainsToBlock (domains: string[]) {
|
|
||||||
domains.forEach(domain => {
|
|
||||||
this.blocklistService.blockServerByInstance(domain)
|
|
||||||
.subscribe(
|
|
||||||
() => {
|
|
||||||
this.notifier.success(this.i18n('Instance {{domain}} muted by your instance.', { domain }))
|
|
||||||
|
|
||||||
this.loadData()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadData () {
|
|
||||||
return this.blocklistService.getInstanceServerBlocklist({
|
|
||||||
pagination: this.pagination,
|
|
||||||
sort: this.sort,
|
|
||||||
search: this.search
|
|
||||||
})
|
|
||||||
.subscribe(
|
|
||||||
resultList => {
|
|
||||||
this.blockedServers = resultList.data
|
|
||||||
this.totalRecords = resultList.total
|
|
||||||
},
|
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-table-message {
|
|
||||||
@include empty-state;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moderation-expanded {
|
.moderation-expanded {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
|
|
||||||
|
|
|
@ -137,7 +137,7 @@
|
||||||
<ng-template pTemplate="emptymessage">
|
<ng-template pTemplate="emptymessage">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<div class="empty-table-message">
|
<div class="no-results">
|
||||||
<ng-container *ngIf="search" i18n>No video abuses found matching current filters.</ng-container>
|
<ng-container *ngIf="search" i18n>No video abuses found matching current filters.</ng-container>
|
||||||
<ng-container *ngIf="!search" i18n>No video abuses found.</ng-container>
|
<ng-container *ngIf="!search" i18n>No video abuses found.</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
<ng-template pTemplate="emptymessage">
|
<ng-template pTemplate="emptymessage">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<div class="empty-table-message">
|
<div class="no-results">
|
||||||
<ng-container *ngIf="search" i18n>No blocked video found matching current filters.</ng-container>
|
<ng-container *ngIf="search" i18n>No blocked video found matching current filters.</ng-container>
|
||||||
<ng-container *ngIf="!search" i18n>No blocked video found.</ng-container>
|
<ng-container *ngIf="!search" i18n>No blocked video found.</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
<div class="admin-sub-header">
|
|
||||||
<h1 i18n class="form-sub-title">Muted accounts</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p-table
|
|
||||||
[value]="blockedAccounts" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage"
|
|
||||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
|
|
||||||
>
|
|
||||||
|
|
||||||
<ng-template pTemplate="header">
|
|
||||||
<tr>
|
|
||||||
<th i18n>Account</th>
|
|
||||||
<th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
|
|
||||||
<th></th> <!-- column for action buttons -->
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template pTemplate="body" let-accountBlock>
|
|
||||||
<tr>
|
|
||||||
<td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
|
|
||||||
<td>{{ accountBlock.createdAt }}</td>
|
|
||||||
<td class="action-cell">
|
|
||||||
<button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
</p-table>
|
|
|
@ -1,7 +0,0 @@
|
||||||
@import '_variables';
|
|
||||||
@import '_mixins';
|
|
||||||
|
|
||||||
.unblock-button {
|
|
||||||
@include peertube-button;
|
|
||||||
@include grey-button;
|
|
||||||
}
|
|
|
@ -1,59 +1,15 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { Notifier } from '@app/core'
|
import { GenericAccountBlocklistComponent, BlocklistComponentType } from '@app/shared/blocklist'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
|
||||||
import { RestPagination, RestTable } from '@app/shared'
|
|
||||||
import { SortMeta } from 'primeng/api'
|
|
||||||
import { AccountBlock, BlocklistService } from '@app/shared/blocklist'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-blocklist',
|
selector: 'my-account-blocklist',
|
||||||
styleUrls: [ './my-account-blocklist.component.scss' ],
|
styleUrls: [ '../../shared/blocklist/account-blocklist.component.scss' ],
|
||||||
templateUrl: './my-account-blocklist.component.html'
|
templateUrl: '../../shared/blocklist/account-blocklist.component.html'
|
||||||
})
|
})
|
||||||
export class MyAccountBlocklistComponent extends RestTable implements OnInit {
|
export class MyAccountBlocklistComponent extends GenericAccountBlocklistComponent {
|
||||||
blockedAccounts: AccountBlock[] = []
|
mode = BlocklistComponentType.Account
|
||||||
totalRecords = 0
|
|
||||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
|
||||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
|
||||||
|
|
||||||
constructor (
|
|
||||||
private notifier: Notifier,
|
|
||||||
private blocklistService: BlocklistService,
|
|
||||||
private i18n: I18n
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
getIdentifier () {
|
getIdentifier () {
|
||||||
return 'MyAccountBlocklistComponent'
|
return 'MyAccountBlocklistComponent'
|
||||||
}
|
}
|
||||||
|
|
||||||
unblockAccount (accountBlock: AccountBlock) {
|
|
||||||
const blockedAccount = accountBlock.blockedAccount
|
|
||||||
|
|
||||||
this.blocklistService.unblockAccountByUser(blockedAccount)
|
|
||||||
.subscribe(
|
|
||||||
() => {
|
|
||||||
this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost }))
|
|
||||||
|
|
||||||
this.loadData()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadData () {
|
|
||||||
return this.blocklistService.getUserAccountBlocklist(this.pagination, this.sort)
|
|
||||||
.subscribe(
|
|
||||||
resultList => {
|
|
||||||
this.blockedAccounts = resultList.data
|
|
||||||
this.totalRecords = resultList.total
|
|
||||||
},
|
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
<div class="admin-sub-header">
|
|
||||||
<h1 i18n class="form-sub-title">Muted instances</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p-table
|
|
||||||
[value]="blockedServers" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage"
|
|
||||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
|
|
||||||
>
|
|
||||||
|
|
||||||
<ng-template pTemplate="header">
|
|
||||||
<tr>
|
|
||||||
<th i18n>Instance</th>
|
|
||||||
<th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
|
|
||||||
<th></th> <!-- column for action buttons -->
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template pTemplate="body" let-serverBlock>
|
|
||||||
<tr>
|
|
||||||
<td>{{ serverBlock.blockedServer.host }}</td>
|
|
||||||
<td>{{ serverBlock.createdAt }}</td>
|
|
||||||
<td class="action-cell">
|
|
||||||
<button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
</p-table>
|
|
|
@ -1,7 +0,0 @@
|
||||||
@import '_variables';
|
|
||||||
@import '_mixins';
|
|
||||||
|
|
||||||
.unblock-button {
|
|
||||||
@include peertube-button;
|
|
||||||
@include grey-button;
|
|
||||||
}
|
|
|
@ -1,60 +1,15 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { Notifier } from '@app/core'
|
import { GenericServerBlocklistComponent, BlocklistComponentType } from '@app/shared/blocklist'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
|
||||||
import { RestPagination, RestTable } from '@app/shared'
|
|
||||||
import { SortMeta } from 'primeng/api'
|
|
||||||
import { ServerBlock } from '../../../../../shared'
|
|
||||||
import { BlocklistService } from '@app/shared/blocklist'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-server-blocklist',
|
selector: 'my-account-server-blocklist',
|
||||||
styleUrls: [ './my-account-server-blocklist.component.scss' ],
|
styleUrls: [ '../../+admin/moderation/moderation.component.scss', '../../shared/blocklist/server-blocklist.component.scss' ],
|
||||||
templateUrl: './my-account-server-blocklist.component.html'
|
templateUrl: '../../shared/blocklist/server-blocklist.component.html'
|
||||||
})
|
})
|
||||||
export class MyAccountServerBlocklistComponent extends RestTable implements OnInit {
|
export class MyAccountServerBlocklistComponent extends GenericServerBlocklistComponent {
|
||||||
blockedServers: ServerBlock[] = []
|
mode = BlocklistComponentType.Account
|
||||||
totalRecords = 0
|
|
||||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
|
||||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
|
||||||
|
|
||||||
constructor (
|
|
||||||
private notifier: Notifier,
|
|
||||||
private blocklistService: BlocklistService,
|
|
||||||
private i18n: I18n
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
getIdentifier () {
|
getIdentifier () {
|
||||||
return 'MyAccountServerBlocklistComponent'
|
return 'MyAccountServerBlocklistComponent'
|
||||||
}
|
}
|
||||||
|
|
||||||
unblockServer (serverBlock: ServerBlock) {
|
|
||||||
const host = serverBlock.blockedServer.host
|
|
||||||
|
|
||||||
this.blocklistService.unblockServerByUser(host)
|
|
||||||
.subscribe(
|
|
||||||
() => {
|
|
||||||
this.notifier.success(this.i18n('Instance {{host}} unmuted.', { host }))
|
|
||||||
|
|
||||||
this.loadData()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadData () {
|
|
||||||
return this.blocklistService.getUserServerBlocklist(this.pagination, this.sort)
|
|
||||||
.subscribe(
|
|
||||||
resultList => {
|
|
||||||
this.blockedServers = resultList.data
|
|
||||||
this.totalRecords = resultList.total
|
|
||||||
},
|
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,7 +140,7 @@ const myAccountRoutes: Routes = [
|
||||||
component: MyAccountServerBlocklistComponent,
|
component: MyAccountServerBlocklistComponent,
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Muted instances'
|
title: 'Muted servers'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -72,7 +72,7 @@ export class MyAccountComponent implements OnInit {
|
||||||
iconName: 'user'
|
iconName: 'user'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Muted instances'),
|
label: this.i18n('Muted servers'),
|
||||||
routerLink: '/my-account/blocklist/servers',
|
routerLink: '/my-account/blocklist/servers',
|
||||||
iconName: 'server'
|
iconName: 'server'
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
<p-table
|
||||||
|
[value]="blockedAccounts" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
|
||||||
|
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" (onPage)="onPage($event)"
|
||||||
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} muted accounts"
|
||||||
|
>
|
||||||
|
<ng-template pTemplate="caption">
|
||||||
|
<div class="caption">
|
||||||
|
<div class="ml-auto has-feedback has-clear">
|
||||||
|
<input
|
||||||
|
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
||||||
|
(keyup)="onSearch($event)"
|
||||||
|
>
|
||||||
|
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
|
||||||
|
<span class="sr-only" i18n>Clear filters</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 100%;" i18n>Account</th>
|
||||||
|
<th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
|
<th style="width: 150px;"></th> <!-- column for action buttons -->
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="body" let-accountBlock>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="chip two-lines">
|
||||||
|
<img
|
||||||
|
class="avatar"
|
||||||
|
[src]="accountBlock.blockedAccount.avatar?.path"
|
||||||
|
(error)="switchToDefaultAvatar($event)"
|
||||||
|
alt="Avatar"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{{ accountBlock.blockedAccount.displayName }}
|
||||||
|
<span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>{{ accountBlock.createdAt | date: 'short' }}</td>
|
||||||
|
<td class="action-cell">
|
||||||
|
<button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="emptymessage">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6">
|
||||||
|
<div class="no-results">
|
||||||
|
<ng-container *ngIf="search" i18n>No account found matching current filters.</ng-container>
|
||||||
|
<ng-container *ngIf="!search" i18n>No account found.</ng-container>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
|
@ -0,0 +1,16 @@
|
||||||
|
@import '_variables';
|
||||||
|
@import '_mixins';
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
input {
|
||||||
|
@include peertube-input-text(250px);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.unblock-button {
|
||||||
|
@include peertube-button;
|
||||||
|
@include grey-button;
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { OnInit } from '@angular/core'
|
||||||
|
import { Notifier } from '@app/core'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { RestPagination, RestTable } from '@app/shared/rest'
|
||||||
|
import { SortMeta } from 'primeng/api'
|
||||||
|
import { AccountBlock } from './account-block.model'
|
||||||
|
import { BlocklistService, BlocklistComponentType } from './blocklist.service'
|
||||||
|
import { Actor } from '@app/shared/actor/actor.model'
|
||||||
|
|
||||||
|
export class GenericAccountBlocklistComponent extends RestTable implements OnInit {
|
||||||
|
// @ts-ignore: "Abstract methods can only appear within an abstract class"
|
||||||
|
abstract mode: BlocklistComponentType
|
||||||
|
|
||||||
|
blockedAccounts: AccountBlock[] = []
|
||||||
|
totalRecords = 0
|
||||||
|
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||||
|
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private notifier: Notifier,
|
||||||
|
private blocklistService: BlocklistService,
|
||||||
|
private i18n: I18n
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore: "Abstract methods can only appear within an abstract class"
|
||||||
|
abstract getIdentifier (): string
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
switchToDefaultAvatar ($event: Event) {
|
||||||
|
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
|
||||||
|
}
|
||||||
|
|
||||||
|
unblockAccount (accountBlock: AccountBlock) {
|
||||||
|
const blockedAccount = accountBlock.blockedAccount
|
||||||
|
const operation = this.mode === BlocklistComponentType.Account
|
||||||
|
? this.blocklistService.unblockAccountByUser(blockedAccount)
|
||||||
|
: this.blocklistService.unblockAccountByInstance(blockedAccount)
|
||||||
|
|
||||||
|
operation.subscribe(
|
||||||
|
() => {
|
||||||
|
this.notifier.success(
|
||||||
|
this.mode === BlocklistComponentType.Account
|
||||||
|
? this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost })
|
||||||
|
: this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost })
|
||||||
|
)
|
||||||
|
|
||||||
|
this.loadData()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected loadData () {
|
||||||
|
const operation = this.mode === BlocklistComponentType.Account
|
||||||
|
? this.blocklistService.getUserAccountBlocklist({
|
||||||
|
pagination: this.pagination,
|
||||||
|
sort: this.sort,
|
||||||
|
search: this.search
|
||||||
|
})
|
||||||
|
: this.blocklistService.getInstanceAccountBlocklist({
|
||||||
|
pagination: this.pagination,
|
||||||
|
sort: this.sort,
|
||||||
|
search: this.search
|
||||||
|
})
|
||||||
|
|
||||||
|
return operation.subscribe(
|
||||||
|
resultList => {
|
||||||
|
this.blockedAccounts = resultList.data
|
||||||
|
this.totalRecords = resultList.total
|
||||||
|
},
|
||||||
|
|
||||||
|
err => this.notifier.error(err.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '../
|
||||||
import { Account } from '@app/shared/account/account.model'
|
import { Account } from '@app/shared/account/account.model'
|
||||||
import { AccountBlock } from '@app/shared/blocklist/account-block.model'
|
import { AccountBlock } from '@app/shared/blocklist/account-block.model'
|
||||||
|
|
||||||
|
export enum BlocklistComponentType { Account, Instance }
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BlocklistService {
|
export class BlocklistService {
|
||||||
static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
|
static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
|
||||||
|
@ -21,10 +23,14 @@ export class BlocklistService {
|
||||||
|
|
||||||
/*********************** User -> Account blocklist ***********************/
|
/*********************** User -> Account blocklist ***********************/
|
||||||
|
|
||||||
getUserAccountBlocklist (pagination: RestPagination, sort: SortMeta) {
|
getUserAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
|
||||||
|
const { pagination, sort, search } = options
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
|
||||||
|
if (search) params = params.append('search', search)
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', { params })
|
return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||||
|
@ -49,10 +55,14 @@ export class BlocklistService {
|
||||||
|
|
||||||
/*********************** User -> Server blocklist ***********************/
|
/*********************** User -> Server blocklist ***********************/
|
||||||
|
|
||||||
getUserServerBlocklist (pagination: RestPagination, sort: SortMeta) {
|
getUserServerBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
|
||||||
|
const { pagination, sort, search } = options
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
|
||||||
|
if (search) params = params.append('search', search)
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params })
|
return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||||
|
@ -76,7 +86,7 @@ export class BlocklistService {
|
||||||
|
|
||||||
/*********************** Instance -> Account blocklist ***********************/
|
/*********************** Instance -> Account blocklist ***********************/
|
||||||
|
|
||||||
getInstanceAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search: string }) {
|
getInstanceAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
|
||||||
const { pagination, sort, search } = options
|
const { pagination, sort, search } = options
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
|
@ -108,7 +118,7 @@ export class BlocklistService {
|
||||||
|
|
||||||
/*********************** Instance -> Server blocklist ***********************/
|
/*********************** Instance -> Server blocklist ***********************/
|
||||||
|
|
||||||
getInstanceServerBlocklist (options: { pagination: RestPagination, sort: SortMeta, search: string }) {
|
getInstanceServerBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
|
||||||
const { pagination, sort, search } = options
|
const { pagination, sort, search } = options
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
export * from './blocklist.service'
|
export * from './blocklist.service'
|
||||||
export * from './account-block.model'
|
export * from './account-block.model'
|
||||||
|
export * from './server-blocklist.component'
|
||||||
|
export * from './account-blocklist.component'
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
<ng-template pTemplate="emptymessage">
|
<ng-template pTemplate="emptymessage">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<div class="empty-table-message">
|
<div class="no-results">
|
||||||
<ng-container *ngIf="search" i18n>No server found matching current filters.</ng-container>
|
<ng-container *ngIf="search" i18n>No server found matching current filters.</ng-container>
|
||||||
<ng-container *ngIf="!search" i18n>No server found.</ng-container>
|
<ng-container *ngIf="!search" i18n>No server found.</ng-container>
|
||||||
</div>
|
</div>
|
|
@ -15,6 +15,15 @@ a {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
input {
|
||||||
|
@include peertube-input-text(250px);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.unblock-button {
|
.unblock-button {
|
||||||
@include peertube-button;
|
@include peertube-button;
|
||||||
@include grey-button;
|
@include grey-button;
|
101
client/src/app/shared/blocklist/server-blocklist.component.ts
Normal file
101
client/src/app/shared/blocklist/server-blocklist.component.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { OnInit, ViewChild } from '@angular/core'
|
||||||
|
import { Notifier } from '@app/core'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { RestPagination, RestTable } from '@app/shared/rest'
|
||||||
|
import { SortMeta } from 'primeng/api'
|
||||||
|
import { BlocklistService, BlocklistComponentType } from './blocklist.service'
|
||||||
|
import { ServerBlock } from '../../../../../shared/models/blocklist/server-block.model'
|
||||||
|
import { BatchDomainsModalComponent } from '@app/+admin/config/shared/batch-domains-modal.component'
|
||||||
|
|
||||||
|
export class GenericServerBlocklistComponent extends RestTable implements OnInit {
|
||||||
|
@ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent
|
||||||
|
|
||||||
|
// @ts-ignore: "Abstract methods can only appear within an abstract class"
|
||||||
|
public abstract mode: BlocklistComponentType
|
||||||
|
|
||||||
|
blockedServers: ServerBlock[] = []
|
||||||
|
totalRecords = 0
|
||||||
|
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||||
|
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
protected notifier: Notifier,
|
||||||
|
protected blocklistService: BlocklistService,
|
||||||
|
protected i18n: I18n
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore: "Abstract methods can only appear within an abstract class"
|
||||||
|
public abstract getIdentifier (): string
|
||||||
|
|
||||||
|
unblockServer (serverBlock: ServerBlock) {
|
||||||
|
const operation = (host: string) => this.mode === BlocklistComponentType.Account
|
||||||
|
? this.blocklistService.unblockServerByUser(host)
|
||||||
|
: this.blocklistService.unblockServerByInstance(host)
|
||||||
|
const host = serverBlock.blockedServer.host
|
||||||
|
|
||||||
|
operation(host).subscribe(
|
||||||
|
() => {
|
||||||
|
this.notifier.success(
|
||||||
|
this.mode === BlocklistComponentType.Account
|
||||||
|
? this.i18n('Instance {{host}} unmuted.', { host })
|
||||||
|
: this.i18n('Instance {{host}} unmuted by your instance.', { host })
|
||||||
|
)
|
||||||
|
|
||||||
|
this.loadData()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addServersToBlock () {
|
||||||
|
this.batchDomainsModal.openModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
onDomainsToBlock (domains: string[]) {
|
||||||
|
const operation = (domain: string) => this.mode === BlocklistComponentType.Account
|
||||||
|
? this.blocklistService.blockServerByUser(domain)
|
||||||
|
: this.blocklistService.blockServerByInstance(domain)
|
||||||
|
|
||||||
|
domains.forEach(domain => {
|
||||||
|
operation(domain).subscribe(
|
||||||
|
() => {
|
||||||
|
this.notifier.success(
|
||||||
|
this.mode === BlocklistComponentType.Account
|
||||||
|
? this.i18n('Instance {{domain}} muted.', { domain })
|
||||||
|
: this.i18n('Instance {{domain}} muted by your instance.', { domain })
|
||||||
|
)
|
||||||
|
|
||||||
|
this.loadData()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected loadData () {
|
||||||
|
const operation = this.mode === BlocklistComponentType.Account
|
||||||
|
? this.blocklistService.getUserServerBlocklist({
|
||||||
|
pagination: this.pagination,
|
||||||
|
sort: this.sort,
|
||||||
|
search: this.search
|
||||||
|
})
|
||||||
|
: this.blocklistService.getInstanceServerBlocklist({
|
||||||
|
pagination: this.pagination,
|
||||||
|
sort: this.sort,
|
||||||
|
search: this.search
|
||||||
|
})
|
||||||
|
|
||||||
|
return operation.subscribe(
|
||||||
|
resultList => {
|
||||||
|
this.blockedServers = resultList.data
|
||||||
|
this.totalRecords = resultList.total
|
||||||
|
},
|
||||||
|
|
||||||
|
err => this.notifier.error(err.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { NgModule } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
import { BatchDomainsValidatorsService } from '@app/+admin/config/shared/batch-domains-validators.service'
|
import { BatchDomainsValidatorsService } from '@app/+admin/config/shared/batch-domains-validators.service'
|
||||||
|
import { BatchDomainsModalComponent } from '@app/+admin/config/shared/batch-domains-modal.component'
|
||||||
import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
|
import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
|
||||||
import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings'
|
import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings'
|
||||||
import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
|
import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
|
||||||
|
@ -192,7 +193,8 @@ import { VideoService } from './video/video.service'
|
||||||
|
|
||||||
MyAccountVideoSettingsComponent,
|
MyAccountVideoSettingsComponent,
|
||||||
MyAccountInterfaceSettingsComponent,
|
MyAccountInterfaceSettingsComponent,
|
||||||
ActorAvatarInfoComponent
|
ActorAvatarInfoComponent,
|
||||||
|
BatchDomainsModalComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -274,7 +276,8 @@ import { VideoService } from './video/video.service'
|
||||||
|
|
||||||
MyAccountVideoSettingsComponent,
|
MyAccountVideoSettingsComponent,
|
||||||
MyAccountInterfaceSettingsComponent,
|
MyAccountInterfaceSettingsComponent,
|
||||||
ActorAvatarInfoComponent
|
ActorAvatarInfoComponent,
|
||||||
|
BatchDomainsModalComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
@ -258,6 +258,8 @@ table {
|
||||||
|
|
||||||
.no-results {
|
.no-results {
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
|
max-height: 500px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -856,15 +856,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin empty-state {
|
|
||||||
min-height: 40vh;
|
|
||||||
max-height: 500px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin admin-sub-header-responsive ($horizontal-margins) {
|
@mixin admin-sub-header-responsive ($horizontal-margins) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue