Skip to content

Commit

Permalink
feat(plugins): install/update/uninstall plugins in jobs
Browse files Browse the repository at this point in the history
* To avoid long running operations in HTTP requests.
* To not collidate with other install/update/uninstall operations.
  • Loading branch information
kontrollanten committed Jun 26, 2024
1 parent 014cdb5 commit c6a1114
Show file tree
Hide file tree
Showing 63 changed files with 755 additions and 152 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@
<my-edit-button
*ngIf="!isTheme(plugin)" [ptRouterLink]="getShowRouterLink(plugin)" label="Settings" i18n-label
[responsiveLabel]="true"
[disabled]="willUpdate(plugin) || willUninstall(plugin)"
></my-edit-button>

<my-button
class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)"
[attr.disabled]="isUpdating(plugin) || isUninstalling(plugin)"
class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)"
[label]="getUpdateLabel(plugin)" icon="refresh" [responsiveLabel]="true"
></my-button>

<my-delete-button
[loading]="willUpdate(plugin)"
[disabled]="willUpdate(plugin) || willUninstall(plugin)"
></my-button>

<my-delete-button
(click)="uninstall(plugin)"
label="Uninstall" i18n-label [responsiveLabel]="true"
[disabled]="isUpdating(plugin) || isUninstalling(plugin)"
[loading]="willUninstall(plugin)"
[disabled]="willUpdate(plugin) || willUninstall(plugin)"
></my-delete-button>
</div>
</my-plugin-card>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@
my-edit-button,
my-button {
@include margin-right(10px);

&[disabled=true] {
opacity: 0.6;
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Subject } from 'rxjs'
import { Component, OnInit } from '@angular/core'
import { Subject, Subscription, filter } from 'rxjs'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core'
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PeerTubeSocket } from '@app/core'
import { PluginService } from '@app/core/plugins/plugin.service'
import { compareSemVer } from '@peertube/peertube-core-utils'
import { PeerTubePlugin, PluginType, PluginType_Type } from '@peertube/peertube-models'
import { PeerTubePlugin, PluginManagePayload, PluginType, PluginType_Type, UserNotificationType } from '@peertube/peertube-models'
import { DeleteButtonComponent } from '../../../shared/shared-main/buttons/delete-button.component'
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
import { EditButtonComponent } from '../../../shared/shared-main/buttons/edit-button.component'
import { PluginCardComponent } from '../shared/plugin-card.component'
import { InfiniteScrollerDirective } from '../../../shared/shared-main/angular/infinite-scroller.directive'
import { NgIf, NgFor } from '@angular/common'
import { PluginNavigationComponent } from '../shared/plugin-navigation.component'
import { JobService } from '@app/+admin/system'
import { logger } from '@root-helpers/logger'

@Component({
selector: 'my-plugin-list-installed',
Expand All @@ -30,7 +32,7 @@ import { PluginNavigationComponent } from '../shared/plugin-navigation.component
DeleteButtonComponent
]
})
export class PluginListInstalledComponent implements OnInit {
export class PluginListInstalledComponent implements OnInit, OnDestroy {
pluginType: PluginType_Type

pagination: ComponentPagination = {
Expand All @@ -41,18 +43,22 @@ export class PluginListInstalledComponent implements OnInit {
sort = 'name'

plugins: PeerTubePlugin[] = []
updating: { [name: string]: boolean } = {}
uninstalling: { [name: string]: boolean } = {}
toBeUpdated: { [name: string]: boolean } = {}
toBeUninstalled: { [name: string]: boolean } = {}

onDataSubject = new Subject<any[]>()

private notificationSub: Subscription

constructor (
private pluginService: PluginService,
private pluginApiService: PluginApiService,
private notifier: Notifier,
private confirmService: ConfirmService,
private router: Router,
private route: ActivatedRoute
private route: ActivatedRoute,
private jobService: JobService,
private peertubeSocket: PeerTubeSocket
) {
}

Expand All @@ -63,13 +69,56 @@ export class PluginListInstalledComponent implements OnInit {
this.router.navigate([], { queryParams, replaceUrl: true })
}

this.jobService.listUnfinishedJobs({
jobType: 'plugin-manage',
pagination: {
count: 10,
start: 0
},
sort: {
field: 'createdAt',
order: -1
}
}).subscribe({
next: resultList => {
const jobs = resultList.data

jobs.forEach((job) => {
let payload: PluginManagePayload

try {
payload = JSON.parse(job.data)
} catch (err) {}

if (payload.action === 'update') {
this.toBeUpdated[payload.npmName] = true
}

if (payload.action === 'uninstall') {
this.toBeUninstalled[payload.npmName] = true
}
})
},

error: err => {
logger.error('Could not fetch status of installed plugins.', { err })
this.notifier.error($localize`Could not fetch status of installed plugins.`)
}
})

this.route.queryParams.subscribe(query => {
if (!query['pluginType']) return

this.pluginType = parseInt(query['pluginType'], 10) as PluginType_Type

this.reloadPlugins()
})

this.subscribeToNotifications()
}

ngOnDestroy () {
if (this.notificationSub) this.notificationSub.unsubscribe()
}

reloadPlugins () {
Expand Down Expand Up @@ -117,12 +166,12 @@ export class PluginListInstalledComponent implements OnInit {
return $localize`Update to ${plugin.latestVersion}`
}

isUpdating (plugin: PeerTubePlugin) {
return !!this.updating[this.getPluginKey(plugin)]
willUpdate (plugin: PeerTubePlugin) {
return !!this.toBeUpdated[this.getPluginKey(plugin)]
}

isUninstalling (plugin: PeerTubePlugin) {
return !!this.uninstalling[this.getPluginKey(plugin)]
willUninstall (plugin: PeerTubePlugin) {
return !!this.toBeUninstalled[this.getPluginKey(plugin)]
}

isTheme (plugin: PeerTubePlugin) {
Expand All @@ -131,37 +180,32 @@ export class PluginListInstalledComponent implements OnInit {

async uninstall (plugin: PeerTubePlugin) {
const pluginKey = this.getPluginKey(plugin)
if (this.uninstalling[pluginKey]) return
if (this.toBeUninstalled[pluginKey]) return

const res = await this.confirmService.confirm(
$localize`Do you really want to uninstall ${plugin.name}?`,
$localize`Uninstall`
)
if (res === false) return

this.uninstalling[pluginKey] = true
this.toBeUninstalled[pluginKey] = true

this.pluginApiService.uninstall(plugin.name, plugin.type)
.subscribe({
next: () => {
this.notifier.success($localize`${plugin.name} uninstalled.`)

this.plugins = this.plugins.filter(p => p.name !== plugin.name)
this.pagination.totalItems--

this.uninstalling[pluginKey] = false
this.notifier.success($localize`${plugin.name} will be uninstalled.`)
},

error: err => {
this.notifier.error(err.message)
this.uninstalling[pluginKey] = false
this.toBeUninstalled[pluginKey] = false
}
})
}

async update (plugin: PeerTubePlugin) {
const pluginKey = this.getPluginKey(plugin)
if (this.updating[pluginKey]) return
if (this.toBeUpdated[pluginKey]) return

if (this.isMajorUpgrade(plugin)) {
const res = await this.confirmService.confirm(
Expand All @@ -173,22 +217,18 @@ export class PluginListInstalledComponent implements OnInit {
if (res === false) return
}

this.updating[pluginKey] = true
this.toBeUpdated[pluginKey] = true

this.pluginApiService.update(plugin.name, plugin.type)
.pipe()
.subscribe({
next: res => {
this.updating[pluginKey] = false

this.notifier.success($localize`${plugin.name} updated.`)

Object.assign(plugin, res)
this.notifier.success($localize`${plugin.name} will be updated.`)
},

error: err => {
this.notifier.error(err.message)
this.updating[pluginKey] = false
this.toBeUpdated[pluginKey] = false
}
})
}
Expand All @@ -201,8 +241,36 @@ export class PluginListInstalledComponent implements OnInit {
return this.pluginApiService.getPluginOrThemeHref(this.pluginType, name)
}

private getPluginKey (plugin: PeerTubePlugin) {
return plugin.name + plugin.type
private async subscribeToNotifications () {
const obs = await this.peertubeSocket.getMyNotificationsSocket()

this.notificationSub = obs
.pipe(
filter(d => d.notification?.type === UserNotificationType.PLUGIN_MANAGE_FINISHED)
).subscribe(data => {
const pluginName = data.notification.plugin?.name

if (pluginName) {
const npmName = this.getPluginKey(data.notification.plugin)

if (this.toBeUninstalled[npmName]) {
this.toBeUninstalled[npmName] = false

if (!data.notification.hasOperationFailed) {
this.plugins = this.plugins.filter(p => p.name !== pluginName)
}
}

if (this.toBeUpdated[npmName]) {
this.toBeUpdated[npmName] = false
this.reloadPlugins()
}
}
})
}

private getPluginKey (plugin: Pick<PeerTubePlugin, 'name' | 'type'>) {
return this.pluginService.nameToNpmName(plugin.name, plugin.type)
}

private isMajorUpgrade (plugin: PeerTubePlugin) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
<my-plugin-navigation [pluginType]="pluginType"></my-plugin-navigation>

<div class="alert pt-alert-primary" i18n *ngIf="pluginInstalled">
To load your new installed plugins or themes, refresh the page.
</div>

<div class="result-and-search">
<ng-container *ngIf="!search">
<my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
Expand Down Expand Up @@ -51,8 +47,9 @@

<my-button
*ngIf="plugin.installed === false" (click)="install(plugin)"
[loading]="isInstalling(plugin)" label="Install" [responsiveLabel]="true"
icon="cloud-download" [attr.disabled]="isInstalling(plugin)"
label="Install" [responsiveLabel]="true"
[loading]="willInstall(plugin)"
icon="cloud-download" [attr.disabled]="willInstall(plugin)"
></my-button>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@
.alert {
margin-top: 15px;
}

my-button[disabled=true] {
opacity: 0.6;
}
Loading

0 comments on commit c6a1114

Please sign in to comment.