Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions core/frontend/src/components/kraken/KrakenManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ExtensionData,
ExtensionUploadResponse,
InstalledExtensionData,
Manifest,
ManifestSource,
Expand Down Expand Up @@ -335,6 +336,57 @@ export async function getContainerLogs(
})
}

/**
* Upload a tar file containing a Docker image and extract metadata
* @param {File} file The tar file to upload
* @returns {Promise<{temp_tag: string, metadata: any, image_name: string}>}
*/
export async function uploadExtensionTarFile(file: File): Promise<ExtensionUploadResponse> {
const formData = new FormData()
formData.append('file', file)

const response = await back_axios({
method: 'POST',
url: `${KRAKEN_API_V2_URL}/extension/upload`,
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
timeout: 120000,
})

return response.data
}

/**
* Finalize a temporary extension by assigning a valid identifier and installing it
* @param {InstalledExtensionData} extension The extension data to finalize
* @param {string} tempTag The temporary tag from upload response
* @param {function} progressHandler The progress handler for the download
* @returns {Promise<void>}
*/
export async function finalizeExtension(
extension: InstalledExtensionData,
tempTag: string,
progressHandler: (event: any) => void,
): Promise<void> {
await back_axios({
method: 'POST',
url: `${KRAKEN_API_V2_URL}/extension/finalize?temp_tag=${tempTag}`,
data: {
identifier: extension.identifier,
name: extension.name,
docker: extension.docker,
tag: extension.tag,
enabled: true,
permissions: extension?.permissions ?? '',
user_permissions: extension?.user_permissions ?? '',
},
timeout: 120000,
onDownloadProgress: progressHandler,
})
}

export default {
fetchManifestSources,
fetchManifestSource,
Expand All @@ -357,4 +409,6 @@ export default {
listContainers,
getContainersStats,
getContainerLogs,
uploadExtensionTarFile,
finalizeExtension,
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
>
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
<span>{{ is_editing ? 'Edit' : 'Create' }} Extension</span>
<span>{{ is_editing ? 'Edit' : (is_from_upload ? 'Configure Uploaded' : 'Create') }} Extension</span>
<v-btn
icon
x-small
Expand Down Expand Up @@ -89,7 +89,7 @@
:disabled="!valid_permissions"
@click="saveExtension"
>
{{ is_editing ? 'Save' : 'Create' }}
{{ is_editing ? 'Save' : (is_from_upload ? 'Install' : 'Create') }}
</v-btn>
</v-card-actions>
</v-card>
Expand Down Expand Up @@ -118,6 +118,10 @@ export default Vue.extend({
type: Object as PropType<InstalledExtensionData & { editing: boolean } | null>,
default: null,
},
tempTag: {
type: String,
default: null,
},
},

data() {
Expand All @@ -141,6 +145,9 @@ export default Vue.extend({
is_editing() {
return this.extension?.editing ?? false
},
is_from_upload() {
return Boolean(this.tempTag)
},
is_reset_editing_permissions_visible() {
return this.new_permissions !== this.extension?.permissions
},
Expand Down
16 changes: 15 additions & 1 deletion core/frontend/src/types/kraken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,18 @@ export interface ProgressEvent {
currentTarget: {
response: string
}
}
}

export interface ExtensionUploadMetadata {
identifier?: string
name?: string
docker?: string
tag?: string
permissions?: JSONValue
}

export interface ExtensionUploadResponse {
temp_tag: string
metadata: ExtensionUploadMetadata
image_name: string
}
195 changes: 184 additions & 11 deletions core/frontend/src/views/ExtensionManagerView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -154,27 +154,105 @@
</template>
</template>
<v-fab-transition>
<v-btn
<v-speed-dial
:key="'create_button'"
color="primary"
fab
large
dark
v-model="fab_menu"
fixed
bottom
right
class="v-btn--example"
@click="openCreationDialog"
direction="top"
transition="slide-y-reverse-transition"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
<template #activator>
<v-btn
v-model="fab_menu"
color="primary"
fab
large
dark
>
<v-icon v-if="fab_menu">
mdi-close
</v-icon>
<v-icon v-else>
mdi-plus
</v-icon>
</v-btn>
</template>
<v-btn
v-tooltip="'Create from scratch'"
fab
dark
small
color="green"
@click="openCreationDialog"
>
<v-icon>mdi-code-braces</v-icon>
</v-btn>
<v-btn
v-tooltip="'Upload from file'"
fab
dark
small
color="blue"
@click="openFileUploadDialog"
>
<v-icon>mdi-file-upload</v-icon>
</v-btn>
</v-speed-dial>
</v-fab-transition>
<ExtensionCreationModal
v-if="edited_extension"
:extension="edited_extension"
:temp-tag="upload_temp_tag"
@extensionChange="createOrUpdateExtension"
@closed="clearEditedExtension"
/>
<v-dialog
v-model="show_file_upload"
max-width="500px"
persistent
>
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
<span>Upload Extension File</span>
<v-btn
icon
@click="closeFileUploadDialog"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-file-input
v-model="selected_file"
label="Select .tar file"
accept=".tar"
prepend-icon="mdi-file-upload"
show-size
:rules="[validateTarFile]"
/>
<v-alert
v-if="upload_error"
type="error"
class="mt-3"
>
{{ upload_error }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
:disabled="!selected_file || uploading"
:loading="uploading"
@click="uploadFile"
>
Upload
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</v-container>
</template>
Expand Down Expand Up @@ -204,7 +282,7 @@ import PullTracker from '@/utils/pull_tracker'
import { aggregateStreamingResponse, parseStreamingResponse } from '@/utils/streaming'

import {
ExtensionData, InstalledExtensionData, ProgressEvent,
ExtensionData, ExtensionUploadMetadata, InstalledExtensionData, ProgressEvent,
RunningContainer,
} from '../types/kraken'

Expand Down Expand Up @@ -256,6 +334,13 @@ export default Vue.extend({
fetch_running_containers_task: new OneMoreTime({ delay: 10000, disposeWith: this }),
fetch_containers_stats_task: new OneMoreTime({ delay: 25000, disposeWith: this }),
outputBuffer: '',
fab_menu: false,
show_file_upload: false,
upload_temp_tag: null as null | string,
upload_metadata: null as null | ExtensionUploadMetadata,
selected_file: null as null | File,
uploading: false,
upload_error: null as null | string,
}
},
computed: {
Expand Down Expand Up @@ -342,10 +427,18 @@ export default Vue.extend({
return
}

await this.install(this.edited_extension)
if (this.upload_temp_tag) {
// This is from a file upload, use finalize endpoint
await this.finalizeUploadedExtension(this.edited_extension)
} else {
// This is a regular extension creation
await this.install(this.edited_extension)
}

this.show_dialog = false
this.edited_extension = null
this.upload_temp_tag = null
this.upload_metadata = null
},
openEditDialog(extension: InstalledExtensionData): void {
this.edited_extension = { ...extension, editing: true }
Expand Down Expand Up @@ -623,6 +716,86 @@ export default Vue.extend({
}
})
},
openFileUploadDialog(): void {
this.show_file_upload = true
this.selected_file = null
this.upload_error = null
this.fab_menu = false
},
closeFileUploadDialog(): void {
this.show_file_upload = false
this.selected_file = null
this.upload_error = null
},
validateTarFile(file: File | null): boolean | string {
if (!file) {
return 'Please select a file'
}
if (!file.name.endsWith('.tar')) {
return 'File must be a .tar file'
}
return true
},
async uploadFile(): Promise<void> {
if (!this.selected_file) {
return
}

this.uploading = true
this.upload_error = null

try {
const response = await kraken.uploadExtensionTarFile(this.selected_file)
this.upload_temp_tag = response.temp_tag
this.upload_metadata = response.metadata

// Close upload dialog and open creation modal with pre-filled data
this.show_file_upload = false
this.openCreationDialogFromUpload(response.metadata)
} catch (error) {
this.upload_error = String(error)
notifier.pushBackError('EXTENSION_UPLOAD_FAIL', error)
} finally {
this.uploading = false
}
},
openCreationDialogFromUpload(metadata: ExtensionUploadMetadata): void {
this.edited_extension = {
identifier: metadata.identifier || 'yourorganization.yourextension',
name: metadata.name || '',
docker: metadata.docker || '',
tag: metadata.tag || 'latest',
enabled: true,
permissions: JSON.stringify(metadata.permissions || {}),
user_permissions: JSON.stringify(metadata.permissions || {}),
editing: false,
}
},
async finalizeUploadedExtension(extension: InstalledExtensionData): Promise<void> {
if (!this.upload_temp_tag) {
return
}

this.show_pull_output = true
const tracker = this.getTracker()

kraken.finalizeExtension(
extension,
this.upload_temp_tag,
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
)
.then(() => {
this.fetchInstalledExtensions()
})
.catch((error) => {
this.alerter = true
this.alerter_error = String(error)
notifier.pushBackError('EXTENSION_FINALIZE_FAIL', error)
})
.finally(() => {
this.resetPullOutput()
})
},
},
})
</script>
Expand Down
Loading
Loading