From e5afa227818d80a28b7bd3e423136c69a1abd258 Mon Sep 17 00:00:00 2001 From: Elaina Lee <144840522+Elaina-Lee@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:16:33 -0800 Subject: [PATCH] feat(Templates): Support Consumption Template Creation + UI (#6132) * added consumption different check * added basic connection logic * hide tab&description on single tab, add no config on no config case * added connections to work * modified tab footers * removed unused code * removed console log --- Localize/lang/strings.json | 2 + .../Services/WorkflowAndArtifacts.tsx | 12 +- .../Utilities/resourceUtilities.ts | 2 +- .../laDesignerConsumption.tsx | 2 +- .../app/TemplatesStandaloneDesigner.tsx | 104 ++++++++++++++++-- .../lib/templates/templatesPanelContent.tsx | 30 ++--- .../createWorkflowPanel.tsx | 3 +- .../createWorkflowPanel/tabs/basicsTab.tsx | 3 +- .../tabs/connectionsTab.tsx | 31 ++++-- .../tabs/parametersTab.tsx | 31 ++++-- .../tabs/reviewCreateTab.tsx | 48 ++++++-- .../createWorkflowPanel/usePanelTabs.tsx | 40 +++++-- .../src/lib/ui/panel/templatePanel/panel.less | 5 + 13 files changed, 250 insertions(+), 63 deletions(-) diff --git a/Localize/lang/strings.json b/Localize/lang/strings.json index b382ee269c3..b504be2c18c 100644 --- a/Localize/lang/strings.json +++ b/Localize/lang/strings.json @@ -108,6 +108,7 @@ "1pjO9s": "Hide source schema", "1r9ljA": "Enter a valid URI.", "1uGBLP": "5", + "1vqDeQ": "Select Create to start a new workflow based on this template, no configuration required.", "20oqsp": "Add children (recursive)", "23fENy": "Returns a binary representation of a base 64 encoded string", "23szE+": "Required. The value to convert to data URI.", @@ -1148,6 +1149,7 @@ "_1pjO9s.comment": "Label to close source schema toolbox", "_1r9ljA.comment": "Error validation message for URIs", "_1uGBLP.comment": "Hour of the day", + "_1vqDeQ.comment": "Accessibility label for no configuration required", "_20oqsp.comment": "Add the current node and its children to the map", "_23fENy.comment": "Label for description of custom base64ToBinary Function", "_23szE+.comment": "Required string parameter to be converted using dataUri function", diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx index e1cbb8b473d..559e5870033 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx @@ -172,7 +172,7 @@ export const useWorkflowAndArtifactsConsumption = (workflowId: string) => { }); }; -const getWorkflowAndArtifactsConsumption = async (workflowId: string): Promise => { +export const getWorkflowAndArtifactsConsumption = async (workflowId: string): Promise => { const uri = `${baseUrl}${workflowId}?api-version=${consumptionApiVersion}`; const response = await axios.get(uri, { headers: { @@ -543,8 +543,13 @@ export const saveWorkflowConsumption = async ( outdatedWorkflow: Workflow, workflow: any, clearDirtyState: () => void, - shouldConvertToConsumption = true /* false when saving from code view*/ + options?: { + shouldConvertToConsumption?: boolean /* false when saving from code view*/; + throwError?: boolean; + } ): Promise => { + const shouldConvertToConsumption = options?.shouldConvertToConsumption ?? true; + const workflowToSave = shouldConvertToConsumption ? await convertDesignerWorkflowToConsumptionWorkflow(workflow) : workflow; const outputWorkflow: Workflow = { @@ -566,6 +571,9 @@ export const saveWorkflowConsumption = async ( clearDirtyState(); } catch (error) { console.log(error); + if (options?.throwError) { + throw error; + } } }; diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Utilities/resourceUtilities.ts b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Utilities/resourceUtilities.ts index c557352a1d9..1581232523e 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Utilities/resourceUtilities.ts +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Utilities/resourceUtilities.ts @@ -13,7 +13,7 @@ export const validateResourceId = (resourceId: string): string => { export const fetchAppsByQuery = async (query: string): Promise => { const requestPage = async (value: any[] = [], pageNum = 0, currentSkipToken = ''): Promise => { try { - const pageSize = 1000; + const pageSize = 500; const { data } = await axios.post( 'https://edge.management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01', { diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx index 79e6bd652c0..770e83a66fa 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx @@ -206,7 +206,7 @@ const DesignerEditorConsumption = () => { try { const codeToConvert = JSON.parse(codeEditorRef.current?.getValue() ?? ''); await validateWorkflowConsumption(workflowId, canonicalLocation, workflowAndArtifactsData, codeToConvert); - saveWorkflowConsumption(workflowAndArtifactsData, codeToConvert, clearDirtyState, /*shouldConvertToConsumption*/ false); + saveWorkflowConsumption(workflowAndArtifactsData, codeToConvert, clearDirtyState, { shouldConvertToConsumption: false }); } catch (error: any) { if (error.status !== 404) { alert(`Error converting code to workflow ${error}`); diff --git a/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx b/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx index 7fd40e4fc19..a751ded73d7 100644 --- a/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx +++ b/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, type ReactNode } from 'react'; -import { TemplatesDataProvider, templateStore } from '@microsoft/logic-apps-designer'; +import { isOpenApiSchemaVersion, TemplatesDataProvider, templateStore, type WorkflowParameter } from '@microsoft/logic-apps-designer'; import { environment, loadToken } from '../../environments/environment'; import { DevToolbox } from '../components/DevToolbox'; import type { RootState } from '../state/Store'; @@ -22,6 +22,8 @@ import { } from '@microsoft/logic-apps-shared'; import { getConnectionStandard, + getWorkflowAndArtifactsConsumption, + saveWorkflowConsumption, useAppSettings, useConnectionsData, useCurrentObjectId, @@ -56,7 +58,12 @@ const LoadWhenArmTokenIsLoaded = ({ children }: { children: ReactNode }) => { }; export const TemplatesStandaloneDesigner = () => { const theme = useSelector((state: RootState) => state.workflowLoader.theme); - const { appId, hostingPlan, workflowName: existingWorkflowName } = useSelector((state: RootState) => state.workflowLoader); + const { + appId, + hostingPlan, + workflowName: existingWorkflowName, + resourcePath: workflowId, + } = useSelector((state: RootState) => state.workflowLoader); const { data: workflowAppData } = useWorkflowApp(appId as string, hostingPlan); const canonicalLocation = WorkflowUtility.convertToCanonicalFormat(workflowAppData?.location ?? ''); const { data: tenantId } = useCurrentTenantId(); @@ -88,9 +95,7 @@ export const TemplatesStandaloneDesigner = () => { parametersData: Record ) => { if (appId) { - if (hostingPlan !== 'standard') { - console.log('Hosting plan is not ready yet!'); - } else { + if (hostingPlan === 'standard') { let sanitizedWorkflowDefinitions = workflows.map((workflow) => ({ name: workflow.name as string, kind: workflow.kind as string, @@ -105,7 +110,6 @@ export const TemplatesStandaloneDesigner = () => { const sanitizedParameterName = replaceWithWorkflowName(parameter.name, uniqueIdentifier); sanitizedParameterData[sanitizedParameterName] = { type: parameter.type, - description: parameter?.description, value: parseWorkflowParameterValue(parameter.type, parameter?.value ?? parameter?.default), }; sanitizedWorkflowDefinitions = replaceAllStringInAllWorkflows( @@ -178,6 +182,82 @@ export const TemplatesStandaloneDesigner = () => { () => {}, { skipValidation: true, throwError: true } ); + } else if (hostingPlan === 'consumption') { + const uniqueIdentifier = ''; + + let sanitizedWorkflowDefinition = JSON.stringify(workflows[0].definition); + const sanitizedParameterData: Record = {}; + // Sanitizing parameter name & body + Object.keys(parametersData).forEach((key) => { + const parameter = parametersData[key]; + const sanitizedParameterName = replaceWithWorkflowName(parameter.name, uniqueIdentifier); + sanitizedParameterData[sanitizedParameterName] = { + // name: parameter.name, + type: parameter.type, + defaultValue: parseWorkflowParameterValue(parameter.type, parameter?.value ?? parameter?.default), + // allowedValues: parameter.allowedValues, + }; + sanitizedWorkflowDefinition = replaceAllStringInWorkflowDefinition( + sanitizedWorkflowDefinition, + `parameters('${parameter.name}')`, + `parameters('${sanitizedParameterName}')` + ); + }); + + const { connectionsData: updatedConnectionsData, workflowsJsonString: updatedWorkflowsJsonString } = + await updateConnectionsDataWithNewConnections( + connectionsData(), + settingsData()?.properties, + connectionsMapping, + [ + { + name: '', + kind: '', + definition: sanitizedWorkflowDefinition, + }, + ], + uniqueIdentifier + ); + + sanitizedWorkflowDefinition = updatedWorkflowsJsonString[0].definition; + + const updatedConnectionReferences = Object.values(updatedConnectionsData).reduce((acc, group) => { + for (const key in group) { + acc[key] = group[key]; + } + return acc; + }, {}); + + const workflowDefinition = JSON.parse(sanitizedWorkflowDefinition); + const workflowToSave: any = { + definition: workflowDefinition, + parameters: sanitizedParameterData, + connectionReferences: updatedConnectionReferences, + }; + + const newConnectionsObj: Record = {}; + if (Object.keys(updatedConnectionReferences ?? {}).length) { + await Promise.all( + Object.keys(updatedConnectionReferences).map(async (referenceKey) => { + const reference = updatedConnectionReferences[referenceKey]; + const { api, connection, connectionProperties, connectionRuntimeUrl } = reference; + newConnectionsObj[referenceKey] = { + api, + connection, + connectionId: isOpenApiSchemaVersion(workflowDefinition) ? undefined : connection.id, + connectionProperties, + connectionRuntimeUrl, + }; + }) + ); + } + workflowToSave.connections = newConnectionsObj; + + const workflowArtifacts = await getWorkflowAndArtifactsConsumption(workflowId!); + await saveWorkflowConsumption(workflowArtifacts, workflowToSave, () => {}, { throwError: true }); + alert('Workflow saved successfully!'); + } else { + console.log('Hosting plan is not ready yet!'); } } else { console.log('Select App Id first!'); @@ -306,7 +386,7 @@ const getServices = ( const connectionService = isConsumption ? new ConsumptionConnectionService({ apiVersion: '2018-07-01-preview', - baseUrl, + baseUrl: armUrl, subscriptionId, resourceGroup, location, @@ -358,8 +438,8 @@ const getServices = ( const templateService = isConsumption ? new BaseTemplateService({ - openBladeAfterCreate: (workflowName: string | undefined) => { - window.alert(`Open blade after create, workflowName is: ${workflowName}`); + openBladeAfterCreate: (_workflowName: string | undefined) => { + window.alert('Open blade after create, consumption creation is complete'); }, onAddBlankWorkflow: () => { console.log('On add blank workflow click'); @@ -400,11 +480,15 @@ const replaceAllStringInAllWorkflows = (workflows: StringifiedWorkflow[], oldStr return workflows.map((workflow) => { return { ...workflow, - definition: workflow.definition.replaceAll(oldString, newString), + definition: replaceAllStringInWorkflowDefinition(workflow.definition, oldString, newString), }; }); }; +const replaceAllStringInWorkflowDefinition = (workflowDefinition: string, oldString: string, newString: string) => { + return workflowDefinition.replaceAll(oldString, newString); +}; + const removeUnusedConnections = ( connectionsData: ConnectionsData, connections: ConnectionMapping diff --git a/libs/designer-ui/src/lib/templates/templatesPanelContent.tsx b/libs/designer-ui/src/lib/templates/templatesPanelContent.tsx index 67072ef0d4f..199da885895 100644 --- a/libs/designer-ui/src/lib/templates/templatesPanelContent.tsx +++ b/libs/designer-ui/src/lib/templates/templatesPanelContent.tsx @@ -24,19 +24,23 @@ export const TemplatesPanelContent = ({ tabs = [], selectedTab, selectTab, class const tabClass = className ?? 'msla-templates-panel-tabs'; return (
- - {tabs.map(({ id, title, hasError = false }) => ( - - {hasError && ( - - - - )} - {title} - - ))} - - {selectedTabProps?.description &&
{selectedTabProps?.description}
} + {tabs.length > 1 && ( + <> + + {tabs.map(({ id, title, hasError = false }) => ( + + {hasError && ( + + + + )} + {title} + + ))} + + {selectedTabProps?.description &&
{selectedTabProps?.description}
} + + )}
{selectedTabProps?.content}
); diff --git a/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/createWorkflowPanel.tsx b/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/createWorkflowPanel.tsx index b0ade00d037..cd48694d47c 100644 --- a/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/createWorkflowPanel.tsx +++ b/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/createWorkflowPanel.tsx @@ -19,6 +19,7 @@ export interface CreateWorkflowTabProps { previousTabId?: string; nextTabId?: string; hasError: boolean; + shouldClearDetails: boolean; } export interface CreateWorkflowPanelProps { @@ -85,7 +86,7 @@ export const CreateWorkflowPanel = ({ createWorkflow, onClose, clearDetailsOnClo description={manifest?.description ?? ''} /> ), - [isMultiWorkflowTemplate, resources.multiWorkflowCreateTitle, manifest?.details] + [resources.multiWorkflowCreateTitle, isMultiWorkflow, manifest] ); const selectedTabProps = selectedTabId ? panelTabs?.find((tab) => tab.id === selectedTabId) : panelTabs[0]; diff --git a/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/basicsTab.tsx b/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/basicsTab.tsx index 156f4dd29e1..506142077d0 100644 --- a/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/basicsTab.tsx +++ b/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/basicsTab.tsx @@ -17,8 +17,7 @@ export const WorkflowBasics = () => { export const basicsTab = ( intl: IntlShape, dispatch: AppDispatch, - shouldClearDetails: boolean, - { isCreating, nextTabId, hasError }: CreateWorkflowTabProps + { shouldClearDetails, isCreating, nextTabId, hasError }: CreateWorkflowTabProps ): TemplatePanelTab => ({ id: constants.TEMPLATE_PANEL_TAB_NAMES.BASIC, title: intl.formatMessage({ diff --git a/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/connectionsTab.tsx b/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/connectionsTab.tsx index b5646b8c5b1..99e285836ea 100644 --- a/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/connectionsTab.tsx +++ b/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/connectionsTab.tsx @@ -3,9 +3,10 @@ import { useSelector } from 'react-redux'; import type { IntlShape } from 'react-intl'; import constants from '../../../../../common/constants'; import type { TemplatePanelTab } from '@microsoft/designer-ui'; -import { selectPanelTab } from '../../../../../core/state/templates/panelSlice'; +import { closePanel, selectPanelTab } from '../../../../../core/state/templates/panelSlice'; import type { CreateWorkflowTabProps } from '../createWorkflowPanel'; import { WorkflowConnections } from '../../../../templates/connections/workflowconnections'; +import { clearTemplateDetails } from '../../../../../core/state/templates/templateSlice'; export const ConnectionsPanel: React.FC = () => { const { connections } = useSelector((state: RootState) => state.template); @@ -16,7 +17,7 @@ export const ConnectionsPanel: React.FC = () => { export const connectionsTab = ( intl: IntlShape, dispatch: AppDispatch, - { isCreating, nextTabId, hasError }: CreateWorkflowTabProps + { shouldClearDetails, previousTabId, isCreating, nextTabId, hasError }: CreateWorkflowTabProps ): TemplatePanelTab => ({ id: constants.TEMPLATE_PANEL_TAB_NAMES.CONNECTIONS, title: intl.formatMessage({ @@ -40,13 +41,27 @@ export const connectionsTab = ( primaryButtonOnClick: () => { dispatch(selectPanelTab(nextTabId)); }, - secondaryButtonText: intl.formatMessage({ - defaultMessage: 'Previous', - id: 'Yua/4o', - description: 'Button text for moving to the previous tab in the create workflow panel', - }), + secondaryButtonText: previousTabId + ? intl.formatMessage({ + defaultMessage: 'Previous', + id: 'Yua/4o', + description: 'Button text for moving to the previous tab in the create workflow panel', + }) + : intl.formatMessage({ + defaultMessage: 'Close', + id: 'FTrMxN', + description: 'Button text for closing the panel', + }), secondaryButtonOnClick: () => { - dispatch(selectPanelTab(constants.TEMPLATE_PANEL_TAB_NAMES.BASIC)); + if (previousTabId) { + dispatch(selectPanelTab(previousTabId)); + } else { + dispatch(closePanel()); + + if (shouldClearDetails) { + dispatch(clearTemplateDetails()); + } + } }, secondaryButtonDisabled: isCreating, }, diff --git a/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/parametersTab.tsx b/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/parametersTab.tsx index 8b7b979ee91..57555b56287 100644 --- a/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/parametersTab.tsx +++ b/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/parametersTab.tsx @@ -3,8 +3,9 @@ import type { IntlShape } from 'react-intl'; import constants from '../../../../../common/constants'; import { DisplayParameters } from '../../../../templates/parameters/displayParameters'; import type { TemplatePanelTab } from '@microsoft/designer-ui'; -import { selectPanelTab } from '../../../../../core/state/templates/panelSlice'; +import { closePanel, selectPanelTab } from '../../../../../core/state/templates/panelSlice'; import type { CreateWorkflowTabProps } from '../createWorkflowPanel'; +import { clearTemplateDetails } from '../../../../../core/state/templates/templateSlice'; export const ParametersPanel: React.FC = () => { return ; @@ -13,7 +14,7 @@ export const ParametersPanel: React.FC = () => { export const parametersTab = ( intl: IntlShape, dispatch: AppDispatch, - { isCreating, previousTabId, hasError }: CreateWorkflowTabProps + { isCreating, shouldClearDetails, previousTabId, hasError }: CreateWorkflowTabProps ): TemplatePanelTab => ({ id: constants.TEMPLATE_PANEL_TAB_NAMES.PARAMETERS, title: intl.formatMessage({ @@ -37,13 +38,27 @@ export const parametersTab = ( primaryButtonOnClick: () => { dispatch(selectPanelTab(constants.TEMPLATE_PANEL_TAB_NAMES.REVIEW_AND_CREATE)); }, - secondaryButtonText: intl.formatMessage({ - defaultMessage: 'Previous', - id: 'Yua/4o', - description: 'Button text for moving to the previous tab in the create workflow panel', - }), + secondaryButtonText: previousTabId + ? intl.formatMessage({ + defaultMessage: 'Previous', + id: 'Yua/4o', + description: 'Button text for moving to the previous tab in the create workflow panel', + }) + : intl.formatMessage({ + defaultMessage: 'Close', + id: 'FTrMxN', + description: 'Button text for closing the panel', + }), secondaryButtonOnClick: () => { - dispatch(selectPanelTab(previousTabId)); + if (previousTabId) { + dispatch(selectPanelTab(previousTabId)); + } else { + dispatch(closePanel()); + + if (shouldClearDetails) { + dispatch(clearTemplateDetails()); + } + } }, secondaryButtonDisabled: isCreating, }, diff --git a/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/reviewCreateTab.tsx b/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/reviewCreateTab.tsx index 5e29a5f3d2a..354548c8526 100644 --- a/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/reviewCreateTab.tsx +++ b/libs/designer/src/lib/ui/panel/templatePanel/createWorkflowPanel/tabs/reviewCreateTab.tsx @@ -5,10 +5,12 @@ import constants from '../../../../../common/constants'; import type { TemplatePanelTab } from '@microsoft/designer-ui'; import { useSelector } from 'react-redux'; import { MessageBar, MessageBarType, Spinner, SpinnerSize } from '@fluentui/react'; -import { selectPanelTab } from '../../../../../core/state/templates/panelSlice'; +import { closePanel, selectPanelTab } from '../../../../../core/state/templates/panelSlice'; import { equals, isUndefinedOrEmptyString, normalizeConnectorId } from '@microsoft/logic-apps-shared'; import { ConnectorConnectionStatus } from '../../../../templates/connections/connector'; import { WorkflowKind } from '../../../../../core/state/workflow/workflowInterfaces'; +import type { CreateWorkflowTabProps } from '../createWorkflowPanel'; +import { clearTemplateDetails } from '../../../../../core/state/templates/templateSlice'; export const ReviewCreatePanel = () => { const intl = useIntl(); @@ -62,6 +64,11 @@ export const ReviewCreatePanel = () => { id: 'cNXS5n', description: 'Dropdown option for stateless type', }), + NO_CONFIG: intl.formatMessage({ + defaultMessage: 'Select Create to start a new workflow based on this template, no configuration required.', + id: '1vqDeQ', + description: 'Accessibility label for no configuration required', + }), }; return ( @@ -97,6 +104,12 @@ export const ReviewCreatePanel = () => { )} + {isConsumption && !Object.keys(connections).length && !Object.keys(parameterDefinitions).length ? ( +
+ {intlText.NO_CONFIG} +
+ ) : null} + {Object.keys(connections).length > 0 && ( <>