Skip to content

Commit ec8b927

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

3 files changed

Lines changed: 115 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: 83 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,43 @@ 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+
await kraken.uninstallExtensionVersion(extension.identifier, version)
879+
.catch(() => { /* version may not be registered yet */ })
880+
notifier.pushInfo('EXTENSION_UPDATE_CANCELLED', 'Extension update was cancelled.', true)
881+
return
882+
}
883+
this.showAlertError(error)
843884
notifier.pushBackError('EXTENSION_UPDATE_FAIL', error)
844885
})
845886
.finally(() => {
846-
this.clearInstallingState()
847-
this.resetPullOutput()
887+
if (controller === this.active_abort_controller) this.finishInstallOperation()
848888
})
849889
},
850890
metricsFor(extension: InstalledExtensionData): { cpu: number, memory: number} | Record<string, never> {
@@ -993,24 +1033,30 @@ export default Vue.extend({
9931033
this.setInstallingState(extension.identifier, 'install')
9941034
this.show_dialog = false
9951035
this.show_pull_output = true
996-
const tracker = this.getTracker()
1036+
const controller = this.beginInstallOperation()
1037+
const tracker = this.getTracker(controller.signal)
9971038
9981039
kraken.installExtension(
9991040
extension,
10001041
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
1042+
controller.signal,
10011043
)
10021044
.then(() => {
1003-
this.fetchInstalledExtensions()
10041045
notifier.pushSuccess('EXTENSION_INSTALL_SUCCESS', `${extension.name} installed successfully.`, true)
10051046
})
1006-
.catch((error) => {
1007-
this.alerter = true
1008-
this.alerter_error = String(error)
1009-
notifier.pushBackError('EXTENSIONS_INSTALL_FAIL', error)
1047+
.catch(async (error) => {
1048+
if (axios.isCancel(error)) {
1049+
if (controller !== this.active_abort_controller) return
1050+
await kraken.uninstallExtensionVersion(extension.identifier, extension.tag)
1051+
.catch(() => { /* version may not be registered yet */ })
1052+
notifier.pushInfo('EXTENSION_INSTALL_CANCELLED', 'Extension install was cancelled.', true)
1053+
return
1054+
}
1055+
this.showAlertError(error)
1056+
notifier.pushBackError('EXTENSION_INSTALL_FAIL', error)
10101057
})
10111058
.finally(() => {
1012-
this.clearInstallingState()
1013-
this.resetPullOutput()
1059+
if (controller === this.active_abort_controller) this.finishInstallOperation()
10141060
})
10151061
},
10161062
async performActionFromModal(
@@ -1119,17 +1165,17 @@ export default Vue.extend({
11191165
temp[extension.identifier].loading = loading
11201166
this.installed_extensions = temp
11211167
},
1122-
getTracker(): PullTracker {
1168+
getTracker(signal: AbortSignal): PullTracker {
11231169
return new PullTracker(
11241170
() => {
11251171
setTimeout(() => {
11261172
this.show_pull_output = false
11271173
}, 1000)
11281174
},
11291175
(error) => {
1130-
this.alerter = true
1131-
this.alerter_error = String(error)
1132-
notifier.pushBackError('EXTENSIONS_INSTALL_FAIL', error)
1176+
if (signal.aborted) return
1177+
this.showAlertError(error)
1178+
notifier.pushBackError('EXTENSION_INSTALL_FAIL', error)
11331179
this.show_pull_output = false
11341180
},
11351181
)
@@ -1206,7 +1252,8 @@ export default Vue.extend({
12061252
}
12071253
12081254
this.show_pull_output = true
1209-
const tracker = this.getTracker()
1255+
const controller = this.beginInstallOperation()
1256+
const tracker = this.getTracker(controller.signal)
12101257
this.setInstallFromFilePhase('installing')
12111258
this.install_from_file_install_progress = 0
12121259
this.install_from_file_status_text = 'Starting installation...'
@@ -1216,20 +1263,29 @@ export default Vue.extend({
12161263
extension,
12171264
this.upload_temp_tag,
12181265
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
1266+
controller.signal,
12191267
)
12201268
this.setInstallFromFilePhase('success')
12211269
this.install_from_file_status_text = 'Extension installed successfully'
12221270
this.stopUploadKeepAlive()
12231271
this.upload_temp_tag = null
12241272
this.upload_metadata = null
1225-
this.fetchInstalledExtensions()
12261273
} catch (error) {
1227-
this.applyInstallFromFileError(String(error))
1228-
this.alerter = true
1229-
this.alerter_error = String(error)
1230-
notifier.pushBackError('EXTENSION_FINALIZE_FAIL', error)
1274+
if (axios.isCancel(error)) {
1275+
if (controller === this.active_abort_controller) {
1276+
this.setInstallFromFilePhase('ready')
1277+
this.install_from_file_status_text = ''
1278+
await kraken.uninstallExtensionVersion(extension.identifier, extension.tag)
1279+
.catch(() => { /* version may not be registered yet */ })
1280+
notifier.pushInfo('EXTENSION_INSTALL_CANCELLED', 'Installation from file was cancelled.', true)
1281+
}
1282+
} else {
1283+
this.applyInstallFromFileError(String(error))
1284+
this.showAlertError(error)
1285+
notifier.pushBackError('EXTENSION_FINALIZE_FAIL', error)
1286+
}
12311287
} finally {
1232-
this.resetPullOutput()
1288+
if (controller === this.active_abort_controller) this.finishInstallOperation()
12331289
}
12341290
},
12351291
setInstallingState(identifier: string, action: 'install' | 'update'): void {

0 commit comments

Comments
 (0)