From 19473ee32c84f161b0914a24600d0942c19814ea Mon Sep 17 00:00:00 2001 From: Carlos Emiliano Castro Trejo <102700317+ccastrotrejo@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:46:23 -0800 Subject: [PATCH] feat(vscode): Improve status step indicator for export experience (#4305) * Add spinner and checbox icon * Add styles to final text * Fix spinner * Update intl text to constants --- .../app/commands/workflows/exportLogicApp.ts | 46 +++++++++++------ apps/vs-code-react/src/app/export/export.less | 8 +++ .../src/app/export/status/status.tsx | 50 +++++++++++++------ 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/apps/vs-code-designer/src/app/commands/workflows/exportLogicApp.ts b/apps/vs-code-designer/src/app/commands/workflows/exportLogicApp.ts index 69f5f0b5deb..63db036dc59 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/exportLogicApp.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/exportLogicApp.ts @@ -50,6 +50,22 @@ interface Deployment { } class ExportEngine { + private intlText = { + SUCESSFULL_EXPORTED_MESSAGE: localize('workflowsExportedSuccessfully', 'The selected workflows exported successfully.'), + DONE: localize('done', 'Done.'), + DEPLOYING_CONNECTIONS: localize('deployConnections', 'Deploying connections ...'), + DOWNLOADING_PACKAGE: localize('downloadingPackage', 'Downloading package ...'), + UNZIP_PACKAGE: localize('unzipPackage', 'Unzipping package ...'), + FETCH_CONNECTION: localize('fetchConnectionKeys', 'Retrieving connection keys ...'), + UPDATE_FILES: localize('updateFiles', 'Updating parameters and settings ...'), + }; + + private finalStatus = { + InProgress: 'InProgress', + Succeeded: 'Succeeded', + Failed: 'Failed', + }; + public constructor( private getAccessToken: () => string, private packageUrl: string, @@ -64,31 +80,32 @@ class ExportEngine { public async export(): Promise { try { - this.setFinalStatus('InProgress'); - this.addStatus(localize('downloadPackage', 'Downloading package ...')); + this.setFinalStatus(this.finalStatus.InProgress); + this.addStatus(this.intlText.DOWNLOADING_PACKAGE); const flatFile = await axios.get(this.packageUrl, { responseType: 'arraybuffer', responseEncoding: 'binary', }); const buffer = Buffer.from(flatFile.data); - this.addStatus(localize('done', 'Done.')); - this.addStatus(localize('unzipPackage', 'Unzipping package ...')); + this.addStatus(this.intlText.DONE); + this.addStatus(this.intlText.UNZIP_PACKAGE); const zip = new AdmZip(buffer); zip.extractAllTo(/*target path*/ this.targetDirectory, /*overwrite*/ true); - this.addStatus(localize('done', 'Done.')); + this.addStatus(this.intlText.DONE); const templatePath = `${this.targetDirectory}/.development/deployment/LogicAppStandardConnections.template.json`; const templateExists = await fse.pathExists(templatePath); if (!this.resourceGroupName || !templateExists) { - this.setFinalStatus('Succeeded'); + this.setFinalStatus(this.finalStatus.Succeeded); + this.addStatus(this.intlText.SUCESSFULL_EXPORTED_MESSAGE); const uri: vscode.Uri = vscode.Uri.file(this.targetDirectory); vscode.commands.executeCommand('vscode.openFolder', uri, { forceNewWindow: true }); return; } - this.addStatus(localize('deployConnections', 'Deploying connections ...')); + this.addStatus(this.intlText.DEPLOYING_CONNECTIONS); const connectionsTemplate = await fse.readJSON(templatePath); const parametersFile = await fse.readJSON(`${this.targetDirectory}/parameters.json`); @@ -101,17 +118,18 @@ class ExportEngine { } const output = await this.deployConnectionsTemplate(connectionsTemplate); - this.addStatus(localize('done', 'Done.')); + this.addStatus(this.intlText.DONE); await this.fetchConnectionKeys(output); await this.updateParametersAndSettings(output, parametersFile, localSettingsFile); - this.setFinalStatus('Succeeded'); + this.setFinalStatus(this.finalStatus.Succeeded); + this.addStatus(this.intlText.SUCESSFULL_EXPORTED_MESSAGE); const uri: vscode.Uri = vscode.Uri.file(this.targetDirectory); vscode.commands.executeCommand('vscode.openFolder', uri, { forceNewWindow: true }); } catch (error) { this.addStatus(localize('exportFailed', 'Export failed. {0}', error?.message ?? '')); - this.setFinalStatus('Failed'); + this.setFinalStatus(this.finalStatus.Failed); } } @@ -205,12 +223,12 @@ class ExportEngine { } private async fetchConnectionKeys(output: ConnectionsDeploymentOutput): Promise { - this.addStatus(localize('fetchConnectionKeys', 'Retrieving connection keys ...')); + this.addStatus(this.intlText.FETCH_CONNECTION); for (const connectionKey of Object.keys(output?.connections?.value || {})) { const connectionItem = output.connections.value[connectionKey]; connectionItem.authKey = await this.getConnectionKey(connectionItem.connectionId); } - this.addStatus(localize('done', 'Done.')); + this.addStatus(this.intlText.DONE); } private async getConnectionKey(connectionId: string): Promise { @@ -260,7 +278,7 @@ class ExportEngine { parametersFile: any, localSettingsFile: any ): Promise { - this.addStatus(localize('updateFiles', 'Updating parameters and settings ...')); + this.addStatus(this.intlText.UPDATE_FILES); const { value } = output.connections; for (const key of Object.keys(value)) { @@ -278,7 +296,7 @@ class ExportEngine { writeFileSync(`${this.targetDirectory}/parameters.json`, JSON.stringify(parametersFile, null, 4)); writeFileSync(`${this.targetDirectory}/local.settings.json`, JSON.stringify(localSettingsFile, null, 4)); - this.addStatus(localize('done', 'Done.')); + this.addStatus(this.intlText.DONE); } } diff --git a/apps/vs-code-react/src/app/export/export.less b/apps/vs-code-react/src/app/export/export.less index 4d88f1855e2..c662a4b9084 100644 --- a/apps/vs-code-react/src/app/export/export.less +++ b/apps/vs-code-react/src/app/export/export.less @@ -221,4 +221,12 @@ } } } + + &-status { + &--item { + display: inline-flex; + align-items: center; + margin: 10px 0; + } + } } diff --git a/apps/vs-code-react/src/app/export/status/status.tsx b/apps/vs-code-react/src/app/export/status/status.tsx index 181509df077..b5689acd2d0 100644 --- a/apps/vs-code-react/src/app/export/status/status.tsx +++ b/apps/vs-code-react/src/app/export/status/status.tsx @@ -1,6 +1,6 @@ import { Status as FinalStatus } from '../../../state/WorkflowSlice'; import type { RootState } from '../../../state/store'; -import { Text, List } from '@fluentui/react'; +import { Text, List, Icon, Spinner, SpinnerSize } from '@fluentui/react'; import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; @@ -8,6 +8,7 @@ export const Status: React.FC = () => { const intl = useIntl(); const workflowState = useSelector((state: RootState) => state.workflow); const statuses = workflowState.statuses ?? []; + const finalStatus = workflowState.finalStatus; const intlText = { EXPORT_STATUS_TITLE: intl.formatMessage({ @@ -16,8 +17,20 @@ export const Status: React.FC = () => { }), }; - const renderStatus = (status?: string): JSX.Element => { - return {status}; + const renderStatus = (status?: string, index?: number): JSX.Element => { + const icon = + index === statuses.length - 1 && finalStatus !== FinalStatus.Succeeded ? ( + + ) : ( + + ); + + return ( +
+ {icon} + {status} +
+ ); }; return ( @@ -26,28 +39,37 @@ export const Status: React.FC = () => { {intlText.EXPORT_STATUS_TITLE} - + ); }; -const FinalStatusGadget: React.FC = () => { +interface FinalStatusGadgetProps { + finalStatus: string | undefined; +} + +const FinalStatusGadget: React.FC = ({ finalStatus }) => { const intl = useIntl(); const workflowState = useSelector((state: RootState) => state.workflow); - const status = workflowState.finalStatus; const { targetDirectory } = workflowState.exportData; - const message = intl.formatMessage({ - defaultMessage: 'The selected workflows exported successfully. For next steps, review the ', - description: 'The success message.', - }); + const exportNextStepsPath = intl.formatMessage( + { + defaultMessage: 'For next steps, review the {path} file.', + description: 'Message for next steps after export', + }, + { + path: `${targetDirectory.path}/.logs/export/README.md`, + } + ); - switch (status) { + switch (finalStatus) { case FinalStatus.Succeeded: return ( - - {message} {targetDirectory.path}/.logs/export/README.md - +
+ + {exportNextStepsPath} +
); default: return null;