Skip to content

Commit 9e04f46

Browse files
committed
frontend: kraken: add cancellation support for extension install and update
1 parent 9c087f4 commit 9e04f46

3 files changed

Lines changed: 116 additions & 27 deletions

File tree

core/frontend/src/components/kraken/KrakenManager.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export async function setManifestSourceOrder(identifier: string, order: number):
194194
export async function installExtension(
195195
extension: InstalledExtensionData,
196196
progressHandler: (event: any) => void,
197+
signal?: AbortSignal,
197198
): Promise<void> {
198199
await back_axios({
199200
url: `${KRAKEN_API_V2_URL}/extension/install`,
@@ -209,6 +210,7 @@ export async function installExtension(
209210
},
210211
timeout: 600000,
211212
onDownloadProgress: progressHandler,
213+
signal,
212214
})
213215
}
214216

@@ -248,6 +250,18 @@ export async function uninstallExtension(identifier: string): Promise<void> {
248250
})
249251
}
250252

253+
/**
254+
* Uninstall a specific version of an extension by its identifier and tag, uses API v2
255+
* @param {string} identifier The identifier of the extension
256+
* @param {string} tag The tag of the version to uninstall
257+
*/
258+
export async function uninstallExtensionVersion(identifier: string, tag: string): Promise<void> {
259+
await back_axios({
260+
method: 'DELETE',
261+
url: `${KRAKEN_API_V2_URL}/extension/${identifier}/${tag}`,
262+
})
263+
}
264+
251265
/**
252266
* Restart an extension by its identifier, uses API v2
253267
* @param {string} identifier The identifier of the extension
@@ -270,12 +284,18 @@ export async function updateExtensionToVersion(
270284
identifier: string,
271285
version: string,
272286
progressHandler: (event: any) => void,
287+
signal?: AbortSignal,
273288
): Promise<void> {
274289
await back_axios({
275290
url: `${KRAKEN_API_V2_URL}/extension/${identifier}/${version}`,
291+
params: {
292+
purge: false,
293+
should_enable: false,
294+
},
276295
method: 'PUT',
277296
timeout: 120000,
278297
onDownloadProgress: progressHandler,
298+
signal,
279299
})
280300
}
281301

@@ -387,6 +407,7 @@ export async function finalizeExtension(
387407
extension: InstalledExtensionData,
388408
tempTag: string,
389409
progressHandler: (event: any) => void,
410+
signal?: AbortSignal,
390411
): Promise<void> {
391412
await back_axios({
392413
method: 'POST',
@@ -402,6 +423,7 @@ export async function finalizeExtension(
402423
},
403424
timeout: 120000,
404425
onDownloadProgress: progressHandler,
426+
signal,
405427
})
406428
}
407429

@@ -423,6 +445,7 @@ export default {
423445
enableExtension,
424446
disableExtension,
425447
uninstallExtension,
448+
uninstallExtensionVersion,
426449
restartExtension,
427450
listContainers,
428451
getContainersStats,

core/frontend/src/components/utils/PullProgress.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@
5050
</v-expansion-panel>
5151
</v-expansion-panels>
5252
</v-card-text>
53+
<v-card-actions v-if="cancelable" class="justify-end">
54+
<v-btn color="primary" @click="$emit('cancel')">
55+
Cancel
56+
</v-btn>
57+
</v-card-actions>
5358
</v-card>
5459
</v-dialog>
5560
</template>
@@ -80,6 +85,10 @@ export default Vue.extend({
8085
type: String,
8186
required: true,
8287
},
88+
cancelable: {
89+
type: Boolean,
90+
default: false,
91+
},
8392
},
8493
data() {
8594
return {

core/frontend/src/views/ExtensionManagerView.vue

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
:download="download_percentage"
88
:extraction="extraction_percentage"
99
:statustext="status_text"
10+
:cancelable="!!active_abort_controller"
11+
@cancel="cancelInstallOperation"
1012
/>
1113
<v-dialog
1214
v-model="show_dialog"
@@ -538,6 +540,7 @@ export default Vue.extend({
538540
install_from_file_last_level: -1,
539541
active_operation_identifier: localStorage.getItem(ACTIVE_OPERATION_KEY) as null | string,
540542
active_operation_type: (localStorage.getItem(ACTIVE_OPERATION_TYPE_KEY) ?? null) as null | 'install' | 'update',
543+
active_abort_controller: null as null | AbortController,
541544
}
542545
},
543546
computed: {
@@ -666,11 +669,32 @@ export default Vue.extend({
666669
destroyed() {
667670
clearInterval(this.metrics_interval)
668671
this.stopUploadKeepAlive()
672+
this.active_abort_controller?.abort()
673+
this.active_abort_controller = null
669674
},
670675
methods: {
671676
clearEditedExtension() {
672677
this.edited_extension = null
673678
},
679+
beginInstallOperation(): AbortController {
680+
this.active_abort_controller?.abort()
681+
const controller = new AbortController()
682+
this.active_abort_controller = controller
683+
return controller
684+
},
685+
cancelInstallOperation(): void {
686+
this.active_abort_controller?.abort()
687+
},
688+
showAlertError(error: unknown): void {
689+
this.alerter = true
690+
this.alerter_error = String(error)
691+
},
692+
finishInstallOperation(): void {
693+
this.active_abort_controller = null
694+
this.clearInstallingState()
695+
this.resetPullOutput()
696+
this.fetchInstalledExtensions()
697+
},
674698
setInstallFromFilePhase(phase: TarInstallPhase) {
675699
this.install_from_file_phase = phase
676700
if (phase !== 'error') {
@@ -824,27 +848,44 @@ export default Vue.extend({
824848
this.file_uploading = false
825849
}
826850
},
851+
swapExtensionVersion(identifier: string, enableTag: string, removeTag: string): Promise<void> {
852+
return kraken.enableExtension(identifier, enableTag)
853+
.then(() => kraken.uninstallExtensionVersion(identifier, removeTag))
854+
.catch((error) => notifier.pushBackError('EXTENSION_VERSION_SWAP_FAIL', error))
855+
},
827856
async update(extension: InstalledExtensionData, version: string) {
857+
const installedExt = this.installed_extensions[extension.identifier]
858+
const previousTag = installedExt?.tag ?? extension.tag
828859
this.setInstallingState(extension.identifier, 'update')
829860
this.show_pull_output = true
830-
const tracker = this.getTracker()
861+
const controller = this.beginInstallOperation()
862+
const tracker = this.getTracker(controller.signal)
831863
kraken.updateExtensionToVersion(
832864
extension.identifier,
833865
version,
834866
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
867+
controller.signal,
835868
)
836-
.then(() => {
837-
this.fetchInstalledExtensions()
869+
.then(async () => {
838870
notifier.pushSuccess('EXTENSION_UPDATE_SUCCESS', `${extension.name} updated successfully.`, true)
871+
if (previousTag !== version) {
872+
await this.swapExtensionVersion(extension.identifier, version, previousTag)
873+
}
839874
})
840-
.catch((error) => {
841-
this.alerter = true
842-
this.alerter_error = String(error)
875+
.catch(async (error) => {
876+
if (axios.isCancel(error)) {
877+
if (controller !== this.active_abort_controller) return
878+
if (previousTag !== version) {
879+
await this.swapExtensionVersion(extension.identifier, previousTag, version)
880+
}
881+
notifier.pushInfo('EXTENSION_UPDATE_CANCELLED', 'Extension update was cancelled.', true)
882+
return
883+
}
884+
this.showAlertError(error)
843885
notifier.pushBackError('EXTENSION_UPDATE_FAIL', error)
844886
})
845887
.finally(() => {
846-
this.clearInstallingState()
847-
this.resetPullOutput()
888+
if (controller === this.active_abort_controller) this.finishInstallOperation()
848889
})
849890
},
850891
metricsFor(extension: InstalledExtensionData): { cpu: number, memory: number} | Record<string, never> {
@@ -993,24 +1034,30 @@ export default Vue.extend({
9931034
this.setInstallingState(extension.identifier, 'install')
9941035
this.show_dialog = false
9951036
this.show_pull_output = true
996-
const tracker = this.getTracker()
1037+
const controller = this.beginInstallOperation()
1038+
const tracker = this.getTracker(controller.signal)
9971039
9981040
kraken.installExtension(
9991041
extension,
10001042
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
1043+
controller.signal,
10011044
)
10021045
.then(() => {
1003-
this.fetchInstalledExtensions()
10041046
notifier.pushSuccess('EXTENSION_INSTALL_SUCCESS', `${extension.name} installed successfully.`, true)
10051047
})
1006-
.catch((error) => {
1007-
this.alerter = true
1008-
this.alerter_error = String(error)
1009-
notifier.pushBackError('EXTENSIONS_INSTALL_FAIL', error)
1048+
.catch(async (error) => {
1049+
if (axios.isCancel(error)) {
1050+
if (controller !== this.active_abort_controller) return
1051+
await kraken.uninstallExtension(extension.identifier)
1052+
.catch((err) => notifier.pushBackError('EXTENSION_UNINSTALL_FAIL', err))
1053+
notifier.pushInfo('EXTENSION_INSTALL_CANCELLED', 'Extension install was cancelled.', true)
1054+
return
1055+
}
1056+
this.showAlertError(error)
1057+
notifier.pushBackError('EXTENSION_INSTALL_FAIL', error)
10101058
})
10111059
.finally(() => {
1012-
this.clearInstallingState()
1013-
this.resetPullOutput()
1060+
if (controller === this.active_abort_controller) this.finishInstallOperation()
10141061
})
10151062
},
10161063
async performActionFromModal(
@@ -1119,17 +1166,17 @@ export default Vue.extend({
11191166
temp[extension.identifier].loading = loading
11201167
this.installed_extensions = temp
11211168
},
1122-
getTracker(): PullTracker {
1169+
getTracker(signal: AbortSignal): PullTracker {
11231170
return new PullTracker(
11241171
() => {
11251172
setTimeout(() => {
11261173
this.show_pull_output = false
11271174
}, 1000)
11281175
},
11291176
(error) => {
1130-
this.alerter = true
1131-
this.alerter_error = String(error)
1132-
notifier.pushBackError('EXTENSIONS_INSTALL_FAIL', error)
1177+
if (signal.aborted) return
1178+
this.showAlertError(error)
1179+
notifier.pushBackError('EXTENSION_INSTALL_FAIL', error)
11331180
this.show_pull_output = false
11341181
},
11351182
)
@@ -1206,7 +1253,8 @@ export default Vue.extend({
12061253
}
12071254
12081255
this.show_pull_output = true
1209-
const tracker = this.getTracker()
1256+
const controller = this.beginInstallOperation()
1257+
const tracker = this.getTracker(controller.signal)
12101258
this.setInstallFromFilePhase('installing')
12111259
this.install_from_file_install_progress = 0
12121260
this.install_from_file_status_text = 'Starting installation...'
@@ -1216,20 +1264,29 @@ export default Vue.extend({
12161264
extension,
12171265
this.upload_temp_tag,
12181266
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
1267+
controller.signal,
12191268
)
12201269
this.setInstallFromFilePhase('success')
12211270
this.install_from_file_status_text = 'Extension installed successfully'
12221271
this.stopUploadKeepAlive()
12231272
this.upload_temp_tag = null
12241273
this.upload_metadata = null
1225-
this.fetchInstalledExtensions()
12261274
} catch (error) {
1227-
this.applyInstallFromFileError(String(error))
1228-
this.alerter = true
1229-
this.alerter_error = String(error)
1230-
notifier.pushBackError('EXTENSION_FINALIZE_FAIL', error)
1275+
if (axios.isCancel(error)) {
1276+
if (controller === this.active_abort_controller) {
1277+
this.setInstallFromFilePhase('ready')
1278+
this.install_from_file_status_text = ''
1279+
await kraken.uninstallExtension(extension.identifier)
1280+
.catch((err) => notifier.pushBackError('EXTENSION_UNINSTALL_FAIL', err))
1281+
notifier.pushInfo('EXTENSION_INSTALL_CANCELLED', 'Installation from file was cancelled.', true)
1282+
}
1283+
} else {
1284+
this.applyInstallFromFileError(String(error))
1285+
this.showAlertError(error)
1286+
notifier.pushBackError('EXTENSION_FINALIZE_FAIL', error)
1287+
}
12311288
} finally {
1232-
this.resetPullOutput()
1289+
if (controller === this.active_abort_controller) this.finishInstallOperation()
12331290
}
12341291
},
12351292
setInstallingState(identifier: string, action: 'install' | 'update'): void {

0 commit comments

Comments
 (0)