Skip to content

Commit

Permalink
feat(Templates): Support Consumption Template Creation + UI (#6132)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Elaina-Lee authored Nov 18, 2024
1 parent 7bc895f commit e5afa22
Show file tree
Hide file tree
Showing 13 changed files with 250 additions and 63 deletions.
2 changes: 2 additions & 0 deletions Localize/lang/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export const useWorkflowAndArtifactsConsumption = (workflowId: string) => {
});
};

const getWorkflowAndArtifactsConsumption = async (workflowId: string): Promise<Workflow> => {
export const getWorkflowAndArtifactsConsumption = async (workflowId: string): Promise<Workflow> => {
const uri = `${baseUrl}${workflowId}?api-version=${consumptionApiVersion}`;
const response = await axios.get(uri, {
headers: {
Expand Down Expand Up @@ -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<any> => {
const shouldConvertToConsumption = options?.shouldConvertToConsumption ?? true;

const workflowToSave = shouldConvertToConsumption ? await convertDesignerWorkflowToConsumptionWorkflow(workflow) : workflow;

const outputWorkflow: Workflow = {
Expand All @@ -566,6 +571,9 @@ export const saveWorkflowConsumption = async (
clearDirtyState();
} catch (error) {
console.log(error);
if (options?.throwError) {
throw error;
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const validateResourceId = (resourceId: string): string => {
export const fetchAppsByQuery = async (query: string): Promise<any[]> => {
const requestPage = async (value: any[] = [], pageNum = 0, currentSkipToken = ''): Promise<any> => {
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',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
104 changes: 94 additions & 10 deletions apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -22,6 +22,8 @@ import {
} from '@microsoft/logic-apps-shared';
import {
getConnectionStandard,
getWorkflowAndArtifactsConsumption,
saveWorkflowConsumption,
useAppSettings,
useConnectionsData,
useCurrentObjectId,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -88,9 +95,7 @@ export const TemplatesStandaloneDesigner = () => {
parametersData: Record<string, Template.ParameterDefinition>
) => {
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,
Expand All @@ -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(
Expand Down Expand Up @@ -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<string, WorkflowParameter> = {};
// 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<string, any> = {};
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!');
Expand Down Expand Up @@ -306,7 +386,7 @@ const getServices = (
const connectionService = isConsumption
? new ConsumptionConnectionService({
apiVersion: '2018-07-01-preview',
baseUrl,
baseUrl: armUrl,
subscriptionId,
resourceGroup,
location,
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down
30 changes: 17 additions & 13 deletions libs/designer-ui/src/lib/templates/templatesPanelContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,23 @@ export const TemplatesPanelContent = ({ tabs = [], selectedTab, selectTab, class
const tabClass = className ?? 'msla-templates-panel-tabs';
return (
<div className="msla-templates-panel">
<TabList selectedValue={selectedTabId} onTabSelect={onTabSelected} className={tabClass}>
{tabs.map(({ id, title, hasError = false }) => (
<Tab key={id} id={id} data-testid={id} className="msla-templates-panel-tabName" value={id} role={'tab'}>
{hasError && (
<span className="msla-templates-panel-error-icon">
<Dismiss12Filled />
</span>
)}
{title}
</Tab>
))}
</TabList>
{selectedTabProps?.description && <div className="msla-panel-content-description">{selectedTabProps?.description}</div>}
{tabs.length > 1 && (
<>
<TabList selectedValue={selectedTabId} onTabSelect={onTabSelected} className={tabClass}>
{tabs.map(({ id, title, hasError = false }) => (
<Tab key={id} id={id} data-testid={id} className="msla-templates-panel-tabName" value={id} role={'tab'}>
{hasError && (
<span className="msla-templates-panel-error-icon">
<Dismiss12Filled />
</span>
)}
{title}
</Tab>
))}
</TabList>
{selectedTabProps?.description && <div className="msla-panel-content-description">{selectedTabProps?.description}</div>}
</>
)}
<div className="msla-panel-content-container">{selectedTabProps?.content}</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface CreateWorkflowTabProps {
previousTabId?: string;
nextTabId?: string;
hasError: boolean;
shouldClearDetails: boolean;
}

export interface CreateWorkflowPanelProps {
Expand Down Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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({
Expand All @@ -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,
},
Expand Down
Loading

0 comments on commit e5afa22

Please sign in to comment.