Skip to content

Commit a757ee0

Browse files
committed
frontend: kraken: add cancellation support for extension install and update
1 parent 0586b3d commit a757ee0

3 files changed

Lines changed: 100 additions & 26 deletions

File tree

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

Lines changed: 19 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,14 @@ 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}`,
276291
method: 'PUT',
277292
timeout: 120000,
278293
onDownloadProgress: progressHandler,
294+
signal,
279295
})
280296
}
281297

@@ -387,6 +403,7 @@ export async function finalizeExtension(
387403
extension: InstalledExtensionData,
388404
tempTag: string,
389405
progressHandler: (event: any) => void,
406+
signal?: AbortSignal,
390407
): Promise<void> {
391408
await back_axios({
392409
method: 'POST',
@@ -402,6 +419,7 @@ export async function finalizeExtension(
402419
},
403420
timeout: 120000,
404421
onDownloadProgress: progressHandler,
422+
signal,
405423
})
406424
}
407425

@@ -423,6 +441,7 @@ export default {
423441
enableExtension,
424442
disableExtension,
425443
uninstallExtension,
444+
uninstallExtensionVersion,
426445
restartExtension,
427446
listContainers,
428447
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: 72 additions & 26 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') {
@@ -827,24 +851,30 @@ export default Vue.extend({
827851
async update(extension: InstalledExtensionData, version: string) {
828852
this.setInstallingState(extension.identifier, 'update')
829853
this.show_pull_output = true
830-
const tracker = this.getTracker()
854+
const controller = this.beginInstallOperation()
855+
const tracker = this.getTracker(controller.signal)
831856
kraken.updateExtensionToVersion(
832857
extension.identifier,
833858
version,
834859
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
860+
controller.signal,
835861
)
836862
.then(() => {
837-
this.fetchInstalledExtensions()
838863
notifier.pushSuccess('EXTENSION_UPDATE_SUCCESS', `${extension.name} updated successfully.`, true)
839864
})
840-
.catch((error) => {
841-
this.alerter = true
842-
this.alerter_error = String(error)
865+
.catch(async (error) => {
866+
if (axios.isCancel(error)) {
867+
if (controller !== this.active_abort_controller) return
868+
await kraken.uninstallExtensionVersion(extension.identifier, version)
869+
.catch(() => { /* version may not be registered yet */ })
870+
notifier.pushInfo('EXTENSION_UPDATE_CANCELLED', 'Extension update was cancelled.', true)
871+
return
872+
}
873+
this.showAlertError(error)
843874
notifier.pushBackError('EXTENSION_UPDATE_FAIL', error)
844875
})
845876
.finally(() => {
846-
this.clearInstallingState()
847-
this.resetPullOutput()
877+
if (controller === this.active_abort_controller) this.finishInstallOperation()
848878
})
849879
},
850880
metricsFor(extension: InstalledExtensionData): { cpu: number, memory: number} | Record<string, never> {
@@ -993,24 +1023,30 @@ export default Vue.extend({
9931023
this.setInstallingState(extension.identifier, 'install')
9941024
this.show_dialog = false
9951025
this.show_pull_output = true
996-
const tracker = this.getTracker()
1026+
const controller = this.beginInstallOperation()
1027+
const tracker = this.getTracker(controller.signal)
9971028
9981029
kraken.installExtension(
9991030
extension,
10001031
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
1032+
controller.signal,
10011033
)
10021034
.then(() => {
1003-
this.fetchInstalledExtensions()
10041035
notifier.pushSuccess('EXTENSION_INSTALL_SUCCESS', `${extension.name} installed successfully.`, true)
10051036
})
1006-
.catch((error) => {
1007-
this.alerter = true
1008-
this.alerter_error = String(error)
1009-
notifier.pushBackError('EXTENSIONS_INSTALL_FAIL', error)
1037+
.catch(async (error) => {
1038+
if (axios.isCancel(error)) {
1039+
if (controller !== this.active_abort_controller) return
1040+
await kraken.uninstallExtensionVersion(extension.identifier, extension.tag)
1041+
.catch(() => { /* version may not be registered yet */ })
1042+
notifier.pushInfo('EXTENSION_INSTALL_CANCELLED', 'Extension install was cancelled.', true)
1043+
return
1044+
}
1045+
this.showAlertError(error)
1046+
notifier.pushBackError('EXTENSION_INSTALL_FAIL', error)
10101047
})
10111048
.finally(() => {
1012-
this.clearInstallingState()
1013-
this.resetPullOutput()
1049+
if (controller === this.active_abort_controller) this.finishInstallOperation()
10141050
})
10151051
},
10161052
async performActionFromModal(
@@ -1119,17 +1155,17 @@ export default Vue.extend({
11191155
temp[extension.identifier].loading = loading
11201156
this.installed_extensions = temp
11211157
},
1122-
getTracker(): PullTracker {
1158+
getTracker(signal: AbortSignal): PullTracker {
11231159
return new PullTracker(
11241160
() => {
11251161
setTimeout(() => {
11261162
this.show_pull_output = false
11271163
}, 1000)
11281164
},
11291165
(error) => {
1130-
this.alerter = true
1131-
this.alerter_error = String(error)
1132-
notifier.pushBackError('EXTENSIONS_INSTALL_FAIL', error)
1166+
if (signal.aborted) return
1167+
this.showAlertError(error)
1168+
notifier.pushBackError('EXTENSION_INSTALL_FAIL', error)
11331169
this.show_pull_output = false
11341170
},
11351171
)
@@ -1206,7 +1242,8 @@ export default Vue.extend({
12061242
}
12071243
12081244
this.show_pull_output = true
1209-
const tracker = this.getTracker()
1245+
const controller = this.beginInstallOperation()
1246+
const tracker = this.getTracker(controller.signal)
12101247
this.setInstallFromFilePhase('installing')
12111248
this.install_from_file_install_progress = 0
12121249
this.install_from_file_status_text = 'Starting installation...'
@@ -1216,20 +1253,29 @@ export default Vue.extend({
12161253
extension,
12171254
this.upload_temp_tag,
12181255
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
1256+
controller.signal,
12191257
)
12201258
this.setInstallFromFilePhase('success')
12211259
this.install_from_file_status_text = 'Extension installed successfully'
12221260
this.stopUploadKeepAlive()
12231261
this.upload_temp_tag = null
12241262
this.upload_metadata = null
1225-
this.fetchInstalledExtensions()
12261263
} catch (error) {
1227-
this.applyInstallFromFileError(String(error))
1228-
this.alerter = true
1229-
this.alerter_error = String(error)
1230-
notifier.pushBackError('EXTENSION_FINALIZE_FAIL', error)
1264+
if (axios.isCancel(error)) {
1265+
if (controller === this.active_abort_controller) {
1266+
this.setInstallFromFilePhase('ready')
1267+
this.install_from_file_status_text = ''
1268+
await kraken.uninstallExtensionVersion(extension.identifier, extension.tag)
1269+
.catch(() => { /* version may not be registered yet */ })
1270+
notifier.pushInfo('EXTENSION_INSTALL_CANCELLED', 'Installation from file was cancelled.', true)
1271+
}
1272+
} else {
1273+
this.applyInstallFromFileError(String(error))
1274+
this.showAlertError(error)
1275+
notifier.pushBackError('EXTENSION_FINALIZE_FAIL', error)
1276+
}
12311277
} finally {
1232-
this.resetPullOutput()
1278+
if (controller === this.active_abort_controller) this.finishInstallOperation()
12331279
}
12341280
},
12351281
setInstallingState(identifier: string, action: 'install' | 'update'): void {

0 commit comments

Comments
 (0)