diff --git a/src/assets/scss/element/element-reset.scss b/src/assets/scss/element/element-reset.scss index 6c62b49a5..74e2f03da 100644 --- a/src/assets/scss/element/element-reset.scss +++ b/src/assets/scss/element/element-reset.scss @@ -1,3 +1,5 @@ +@import '~@/assets/scss/variable.scss'; + /* Divider */ .el-divider { background-color: var(--color-border-default); @@ -180,6 +182,15 @@ .el-dialog { background: var(--color-bg-normal); border-radius: 8px; + .el-dialog__header { + padding: 0 20px; + line-height: 56px; + border-bottom: 1px solid var(--color-border-default); + .el-dialog__title { + color: var(--color-text-title); + font-size: $font-size--subtitle; + } + } } /* Input */ @@ -341,3 +352,17 @@ color: var(--color-text-tips); } } + +/* Progress */ +.el-progress { + .el-progress-bar__outer { + background-color: var(--color-bg-code); + border-radius: 4px; + .el-progress-bar__inner { + border-radius: 4px; + } + } + .el-progress__text { + color: var(--color-text-default); + } +} diff --git a/src/components/ImportData.vue b/src/components/ImportData.vue index f91d85a5f..bbb3e2a50 100644 --- a/src/components/ImportData.vue +++ b/src/components/ImportData.vue @@ -33,14 +33,12 @@ - - {{ $t('connections.importConnectionsTip') }} - + - + @@ -64,6 +62,15 @@ + + + @@ -110,8 +117,10 @@ export default class ImportData extends Vue { @Prop({ default: false }) public visible!: boolean + private importMsgsProgress = 0 private showDialog: boolean = this.visible private confirmLoading: boolean = false + private progressVisible = false private record: ImportForm = { importFormat: 'JSON', filePath: '', @@ -124,10 +133,21 @@ export default class ImportData extends Vue { this.showDialog = val } + private handleFilePathChange(val: string) { + if (!val) { + return + } + this.readFilePath(val, this.getExtensionName()) + } + + private getExtensionName() { + const lowerFormat = this.record.importFormat.toLowerCase() + return lowerFormat === 'excel' ? 'xlsx' : lowerFormat + } + private getFileData() { let loading: ElLoadingComponent | undefined = undefined - const lowerFormat = this.record.importFormat.toLowerCase() - const extensionName = lowerFormat === 'excel' ? 'xlsx' : lowerFormat + const extensionName = this.getExtensionName() remote.dialog .showOpenDialog({ properties: ['openFile'], @@ -141,11 +161,7 @@ export default class ImportData extends Vue { spinner: 'el-icon-loading', }) const filePath = filePaths[0] - if (extensionName === 'xlsx') { - this.getExcelContentByXlsx(filePath) - } else { - this.getFileContentByFs(filePath) - } + this.readFilePath(filePath, extensionName) } }) .catch(() => {}) @@ -174,7 +190,8 @@ export default class ImportData extends Vue { properties = JSON.parse(properties) will = JSON.parse(will) } catch (err) { - this.$message.error(err.toString()) + const error = err as unknown as Error + this.$message.error(error.toString()) caughtError = true } return Object.assign(connection, { messages, subscriptions, properties, will }) @@ -185,6 +202,14 @@ export default class ImportData extends Vue { } } + private readFilePath(filePath: string, extensionName: string) { + if (extensionName === 'xlsx') { + this.getExcelContentByXlsx(filePath) + } else { + this.getFileContentByFs(filePath) + } + } + private getFileContentByFs(filePath: string) { fs.readFile(filePath, 'utf-8', (err, content) => { if (err) { @@ -195,7 +220,8 @@ export default class ImportData extends Vue { const fileContent = this.getDiffFormatData(content) this.assignValueToRecord(filePath, fileContent) } catch (err) { - this.$message.error(err.toString()) + const error = err as unknown as Error + this.$message.error(error.toString()) } }) } @@ -277,7 +303,8 @@ export default class ImportData extends Vue { const keyName = Object.keys(parentElement._parent)[keyNameIndex] parentElement._parent[keyName] = nativeType(keyName, value) } catch (err) { - this.$message.error(err.toString()) + const error = err as unknown as Error + this.$message.error(error.toString()) } } const convertRightStringAndArray = (data: string) => { @@ -301,7 +328,8 @@ export default class ImportData extends Vue { if (!Array.isArray(subscriptions)) connection.subscriptions = [subscriptions] }) } catch (err) { - this.$message.error(err.toString()) + const error = err as unknown as Error + this.$message.error(error.toString()) } return fileContent } @@ -356,7 +384,8 @@ export default class ImportData extends Vue { }) fileContent.push({ messages, subscriptions, properties, will, ...otherProps }) } catch (err) { - this.$message.error(err.toString()) + const error = err as unknown as Error + this.$message.error(error.toString()) } }) return fileContent @@ -364,25 +393,35 @@ export default class ImportData extends Vue { private async importData() { this.confirmLoading = true - const { connectionService } = useServices() - if (!this.record.fileContent.length) { - this.$message.error(this.$tc('connections.uploadFileTip')) - return - } - const importDataResult = await connectionService.import(this.record.fileContent) - this.confirmLoading = false - if (importDataResult === 'ok') { - this.$message.success(this.$tc('common.importSuccess')) - this.resetData() - setTimeout(() => { - location.reload() - }, 1000) - } else { - this.$message.error(importDataResult) + try { + const { connectionService } = useServices() + if (!this.record.fileContent.length) { + this.$message.error(this.$tc('connections.uploadFileTip')) + return + } + this.progressVisible = true + const importDataResult = await connectionService.import(this.record.fileContent, (progress) => { + this.importMsgsProgress = progress + }) + if (importDataResult === 'ok') { + this.$message.success(this.$tc('common.importSuccess')) + setTimeout(() => { + this.resetData() + location.reload() + }, 2000) + } else { + this.$message.error(importDataResult) + } + } catch (err) { + const error = err as unknown as Error + this.$message.error(error.toString()) + } finally { + this.confirmLoading = false } } private resetData() { + this.progressVisible = false this.showDialog = false this.$emit('update:visible', false) this.record = { @@ -391,6 +430,11 @@ export default class ImportData extends Vue { fileName: '', fileContent: [], } + this.importMsgsProgress = 0 + } + + private getProgressNumber(progress: number | string) { + return Number((typeof progress === 'string' ? Number(progress) * 100 : progress * 100).toFixed(1)) } } diff --git a/src/components/MyDialog.vue b/src/components/MyDialog.vue index 1d37cf773..a60da86de 100644 --- a/src/components/MyDialog.vue +++ b/src/components/MyDialog.vue @@ -79,15 +79,6 @@ export default class MyDialog extends Vue { @import '~@/assets/scss/variable.scss'; .my-dialog { - .el-dialog__header { - padding: 0 20px; - line-height: 56px; - border-bottom: 1px solid var(--color-border-default); - .el-dialog__title { - color: var(--color-text-title); - font-size: $font-size--subtitle; - } - } .el-dialog--center .el-dialog__body { padding: 32px 24px 0; } diff --git a/src/database/services/ConnectionService.ts b/src/database/services/ConnectionService.ts index b3108ff2f..ae8af93b0 100644 --- a/src/database/services/ConnectionService.ts +++ b/src/database/services/ConnectionService.ts @@ -125,18 +125,52 @@ export default class ConnectionService { ) } - // import single connection - public async importOneConnection(id: string, data: ConnectionModel) { + /** + * Imports a single connection with the specified ID and data. + * + * @param id - The ID of the connection to import. + * @param data - The data of the connection to import. + * @param getImportProgress - Optional callback function to receive import progress updates. + * @returns A Promise that resolves when the import is complete. + */ + public async importOneConnection( + id: string, + data: ConnectionModel, + getImportOneConnProgress?: (progress: number) => void, + ) { const { connectionService, subscriptionService, messageService } = useServices() + let progress = 0 + // Update connection, update subscriptions, and update messages are each considered as a step + const totalSteps = 3 // Connection table & Will Message table await connectionService.update(id, data) + progress += 1 / totalSteps + if (getImportOneConnProgress) { + getImportOneConnProgress(progress) + } // Subscriptions table if (Array.isArray(data.subscriptions) && data.subscriptions.length) { await subscriptionService.updateSubscriptions(id, data.subscriptions) } + progress += 1 / totalSteps + if (getImportOneConnProgress) { + getImportOneConnProgress(progress) + } // Messages table if (Array.isArray(data.messages) && data.messages.length) { - await messageService.pushToConnection(data.messages, id) + await messageService.importMsgsToConnection(data.messages, id, (msgProgress) => { + if (getImportOneConnProgress) { + // Combine message import progress with total progress proportionally + const combinedProgress = progress + msgProgress / totalSteps + getImportOneConnProgress(combinedProgress) + } + }) + } else { + // No messages to import, mark progress as 100% for this connection + progress += 1 / totalSteps + if (getImportOneConnProgress) { + getImportOneConnProgress(progress) + } } } @@ -153,14 +187,33 @@ export default class ConnectionService { return await this.get(id) } - public async import(data: ConnectionModel[]): Promise { + /** + * Imports backup connection data into the database. + * + * @param data - An array of ConnectionModel objects to import. + * @param getImportAllProgress - A callback function to track the import progress. + * @returns A Promise that resolves to a string indicating the import status. + */ + public async import(data: ConnectionModel[], getImportAllProgress?: (progress: number) => void): Promise { try { + let overallProgress = 0 + // Each connection is considered as a step + const totalSteps = data.length + for (let i = 0; i < data.length; i++) { const { id } = data[i] if (id) { // FIXME: remove it after support collection importing data[i].parentId = null - await this.importOneConnection(id, data[i]) + await this.importOneConnection(id, data[i], (progress) => { + if (getImportAllProgress) { + // Calculate the progress of a single connection + const connectionProgress = progress / totalSteps + getImportAllProgress(overallProgress + connectionProgress) + } + }) + // Increase progress after processing each connection + overallProgress += 1 / totalSteps } } } catch (err) { diff --git a/src/database/services/MessageService.ts b/src/database/services/MessageService.ts index 93c77fab6..3d1bea523 100644 --- a/src/database/services/MessageService.ts +++ b/src/database/services/MessageService.ts @@ -158,16 +158,78 @@ export default class MessageService { return { list, moreMsg } } - public async pushToConnection( + private async saveMessages( + messages: MessageModel[], + connectionId: string, + ): Promise { + const entities = messages.map((m) => MessageService.modelToEntity(m, connectionId)) + return await this.messageRepository.save(entities) + } + + public async pushMsgsToConnection( message: MessageModel | MessageModel[], connectionId: string, ): Promise { if (!Array.isArray(message)) { - return await this.messageRepository.save(MessageService.modelToEntity({ ...message }, connectionId)) - } else { - const res = message.map((m) => MessageService.modelToEntity(m, connectionId)) - return await this.messageRepository.save(res) + return await this.saveMessages([message], connectionId) + } + return await this.saveMessages(message, connectionId) + } + + /** + * Imports messages to a connection. + * @param message - The message or array of messages to import. + * @param connectionId - The ID of the connection. + * @param getImportMsgsProgress - Optional callback function to track the import progress. + * @returns A promise that resolves to the imported message(s) or undefined. + */ + public async importMsgsToConnection( + message: MessageModel | MessageModel[], + connectionId: string, + getImportMsgsProgress?: (progress: number) => void, + ): Promise { + const BATCH_SIZE = 999 // Threshold for batch processing + + const saveMessages = async (messages: MessageModel[]) => { + const entities = messages.map((m) => MessageService.modelToEntity(m, connectionId)) + return await this.messageRepository.save(entities) + } + + if (!Array.isArray(message)) { + // Single message processing + const result = await saveMessages([message]) + if (getImportMsgsProgress) { + getImportMsgsProgress(1) + } + return result + } + + // If the number of messages is less than or equal to the batch processing threshold, process directly + if (message.length <= BATCH_SIZE) { + const result = await saveMessages(message) + if (getImportMsgsProgress) { + getImportMsgsProgress(1) + } + return result + } + + // If the number of messages exceeds the batch processing threshold, perform batch processing + const results: MessageModel[] = [] + for (let i = 0; i < message.length; i += BATCH_SIZE) { + const batch = message.slice(i, i + BATCH_SIZE) + const savedBatch = await saveMessages(batch) + results.push(...savedBatch) + + // Calculate progress and call callback function + if (getImportMsgsProgress) { + const progress = Math.min((i + BATCH_SIZE) / message.length, 1) + getImportMsgsProgress(progress) + } + } + if (getImportMsgsProgress) { + getImportMsgsProgress(1) } + return results } public async delete(id: string): Promise { diff --git a/src/lang/settings.ts b/src/lang/settings.ts index d4ba9ce36..fa8136d85 100644 --- a/src/lang/settings.ts +++ b/src/lang/settings.ts @@ -181,4 +181,11 @@ export default { ja: 'Copilotを有効にする', hu: 'Copilot engedélyezése', }, + importProgress: { + zh: '导入进度', + en: 'Import Progress', + tr: 'İlerleme İçe Aktar', + ja: '進捗をインポート', + hu: 'Importálás folyamatban', + }, } diff --git a/src/views/connections/ConnectionsDetail.vue b/src/views/connections/ConnectionsDetail.vue index af4c1b0d0..cc17d2f62 100644 --- a/src/views/connections/ConnectionsDetail.vue +++ b/src/views/connections/ConnectionsDetail.vue @@ -1313,7 +1313,7 @@ export default class ConnectionsDetail extends Vue { try { if (messages.length) { const { messageService } = useServices() - await messageService.pushToConnection(messages, id) + await messageService.pushMsgsToConnection(messages, id) } } catch (error) { this.$log.error((error as Error).toString()) @@ -1654,7 +1654,7 @@ export default class ConnectionsDetail extends Vue { if (this.record.id) { const { messageService } = useServices() - await messageService.pushToConnection({ ...publishMessage }, this.record.id) + await messageService.pushMsgsToConnection({ ...publishMessage }, this.record.id) this.renderMessage(this.curConnectionId, publishMessage, 'publish') this.logSuccessfulPublish(publishMessage) }