Skip to content

Commit

Permalink
feat(templates): Adding template card UI (#4975)
Browse files Browse the repository at this point in the history
* feat(templates): Adding template card UI

* Updating css and fixing build

* Addressing pr comment

* Fixing test file build break and test data

---------

Co-authored-by: Priti Sambandam <psamband@microsoft.com>
  • Loading branch information
preetriti1 and Priti Sambandam committed Jun 14, 2024
1 parent c8c94ce commit 43070e2
Show file tree
Hide file tree
Showing 23 changed files with 620 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Artifact } from '../Models/Workflow';
import { validateResourceId } from '../Utilities/resourceUtilities';
import { convertDesignerWorkflowToConsumptionWorkflow } from './ConsumptionSerializationHelpers';
import type { AllCustomCodeFiles } from '@microsoft/logic-apps-designer';
import { CustomCodeService, LogEntryLevel, LoggerService, getAppFileForFileExtension } from '@microsoft/logic-apps-shared';
import { CustomCodeService, LogEntryLevel, LoggerService, equals, getAppFileForFileExtension } from '@microsoft/logic-apps-shared';
import type { LogicAppsV2, VFSObject } from '@microsoft/logic-apps-shared';
import axios from 'axios';
import jwt_decode from 'jwt-decode';
Expand All @@ -17,6 +17,36 @@ const baseUrl = 'https://management.azure.com';
const standardApiVersion = '2020-06-01';
const consumptionApiVersion = '2019-05-01';

export const useConnectionsData = (appId?: string) => {
return useQuery(
['getConnectionsData', appId],
async () => {
const uri = `${baseUrl}/${appId}/workflowsconfiguration/connections?api-version=2018-11-01`;
try {
const response = await axios.get(uri, {
headers: {
Authorization: `Bearer ${environment.armToken}`,
},
});
const { files, health } = response.data.properties;
if (equals(health.state, 'healthy')) {
return files['connections.json'];
}
const { error } = health;
throw new Error(error.message);
} catch (error) {
return {};
}
},
{
enabled: !!appId,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
}
);
};

export const useWorkflowAndArtifactsStandard = (workflowId: string) => {
return useQuery(
['workflowArtifactsStandard', workflowId],
Expand Down
115 changes: 108 additions & 7 deletions apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import type { ReactNode } from 'react';
import { useMemo, type ReactNode } from 'react';
import { ReactQueryProvider, TemplatesDataProvider } from '@microsoft/logic-apps-designer';
import { environment, loadToken } from '../../environments/environment';
import { DevToolbox } from '../components/DevToolbox';
import type { RootState } from '../state/Store';
import { TemplatesDesigner, TemplatesDesignerProvider } from '@microsoft/logic-apps-designer';
import { useQuery } from '@tanstack/react-query';
import { useSelector } from 'react-redux';
import { BaseGatewayService, StandardConnectionService } from '@microsoft/logic-apps-shared';
import {
useAppSettings,
useConnectionsData,
useCurrentObjectId,
useCurrentTenantId,
useWorkflowApp,
} from '../../designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts';
import type { ConnectionsData } from '../../designer/app/AzureLogicAppsDesigner/Models/Workflow';
import type { WorkflowApp } from '../../designer/app/AzureLogicAppsDesigner/Models/WorkflowApp';
import { ArmParser } from '../../designer/app/AzureLogicAppsDesigner/Utilities/ArmParser';
import { StandaloneOAuthService } from '../../designer/app/AzureLogicAppsDesigner/Services/OAuthService';
import { WorkflowUtility } from '../../designer/app/AzureLogicAppsDesigner/Utilities/Workflow';
import { HttpClient } from '../../designer/app/AzureLogicAppsDesigner/Services/HttpClient';
// import { useNavigate } from 'react-router-dom';
// import type { Template, LogicAppsV2 } from '@microsoft/logic-apps-shared';
// import { saveWorkflowStandard } from '../../designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts';
// import type { ParametersData } from '../../designer/app/AzureLogicAppsDesigner/Models/Workflow';
import { useNavigate } from 'react-router-dom';
import type { Template, LogicAppsV2 } from '@microsoft/logic-apps-shared';
import { saveWorkflowStandard } from '../../designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts';
Expand All @@ -17,9 +35,18 @@ const LoadWhenArmTokenIsLoaded = ({ children }: { children: ReactNode }) => {
return isLoading ? null : <>{children}</>;
};
export const TemplatesStandaloneDesigner = () => {
const { appId, isConsumption, workflowName: existingWorkflowName, theme } = useSelector((state: RootState) => state.workflowLoader);
const theme = useSelector((state: RootState) => state.workflowLoader.theme);
const { appId, isConsumption, workflowName: existingWorkflowName } = useSelector((state: RootState) => state.workflowLoader);
const { data: workflowAppData } = useWorkflowApp(appId as string);
const canonicalLocation = WorkflowUtility.convertToCanonicalFormat(workflowAppData?.location ?? '');
const { data: tenantId } = useCurrentTenantId();
const { data: objectId } = useCurrentObjectId();
const { data: connectionsData } = useConnectionsData(appId);
const { data: settingsData } = useAppSettings(appId as string);
const navigate = useNavigate();

// const navigate = useNavigate();

const sanitizeParameterName = (parameterName: string, workflowName: string) =>
parameterName.replace('_#workflowname#', `_${workflowName}`);

Expand Down Expand Up @@ -127,16 +154,90 @@ export const TemplatesStandaloneDesigner = () => {
}
};

const services = useMemo(
() => getServices(connectionsData ?? {}, workflowAppData as WorkflowApp, tenantId, objectId, canonicalLocation),
// eslint-disable-next-line react-hooks/exhaustive-deps
[connectionsData, settingsData, workflowAppData, tenantId, canonicalLocation]
);
const resourceDetails = new ArmParser(appId ?? '');
return (
<ReactQueryProvider>
<LoadWhenArmTokenIsLoaded>
<DevToolbox />
<TemplatesDesignerProvider locale="en-US" theme={theme}>
<TemplatesDataProvider isConsumption={isConsumption} existingWorkflowName={existingWorkflowName}>
<TemplatesDesigner createWorkflowCall={createWorkflowCall} />
</TemplatesDataProvider>
</TemplatesDesignerProvider>
{workflowAppData ? (
<TemplatesDesignerProvider locale="en-US" theme={theme}>
<TemplatesDataProvider
resourceDetails={{
subscriptionId: resourceDetails.subscriptionId,
resourceGroup: resourceDetails.resourceGroup,
location: canonicalLocation,
}}
services={services}
isConsumption={isConsumption}
existingWorkflowName={existingWorkflowName}
>
<TemplatesDesigner createWorkflowCall={createWorkflowCall} />
</TemplatesDataProvider>
</TemplatesDesignerProvider>
) : null}
</LoadWhenArmTokenIsLoaded>
</ReactQueryProvider>
);
};

const apiVersion = '2020-06-01';
const httpClient = new HttpClient();

const getServices = (
connectionsData: ConnectionsData,
workflowApp: WorkflowApp | undefined,
tenantId: string | undefined,
objectId: string | undefined,
location: string
): any => {
const siteResourceId = workflowApp?.id;
const armUrl = 'https://management.azure.com';
const baseUrl = `${armUrl}${siteResourceId}/hostruntime/runtime/webhooks/workflow/api/management`;
const appName = workflowApp?.name ?? '';
const { subscriptionId, resourceGroup } = new ArmParser(siteResourceId ?? '');

const defaultServiceParams = { baseUrl, httpClient, apiVersion };

const connectionService = new StandardConnectionService({
...defaultServiceParams,
apiHubServiceDetails: {
apiVersion: '2018-07-01-preview',
baseUrl: armUrl,
subscriptionId,
resourceGroup,
location,
tenantId,
httpClient,
},
workflowAppDetails: { appName, identity: workflowApp?.identity as any },
readConnections: () => Promise.resolve(connectionsData),
});
const gatewayService = new BaseGatewayService({
baseUrl: armUrl,
httpClient,
apiVersions: {
subscription: apiVersion,
gateway: '2016-06-01',
},
});
const oAuthService = new StandaloneOAuthService({
...defaultServiceParams,
apiVersion: '2018-07-01-preview',
subscriptionId,
resourceGroup,
location,
tenantId,
objectId,
});

return {
connectionService,
gatewayService,
oAuthService,
};
};
19 changes: 19 additions & 0 deletions libs/designer/src/lib/core/state/connection/connectionSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@ import { useQuery } from '@tanstack/react-query';
import { useSelector } from 'react-redux';
import type { ConnectionsStoreState } from './connectionSlice';

export const useConnectorOnly = (connectorId: string | undefined, enabled = true): UseQueryResult<Connector | undefined, unknown> => {
return useQuery(
['apiOnly', { connectorId }],
async () => {
if (!connectorId) {
return null;
}
return await ConnectionService().getConnector(connectorId);
},
{
enabled: !!connectorId && enabled,
cacheTime: 1000 * 60 * 60 * 24,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
}
);
};

export const useConnector = (connectorId: string | undefined, enabled = true): UseQueryResult<Connector | undefined, unknown> => {
const { data, ...rest }: any = useConnectorAndSwagger(connectorId, enabled);
return { data: data?.connector, ...rest };
Expand Down
67 changes: 60 additions & 7 deletions libs/designer/src/lib/core/state/templates/templateSlice.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { getIntl, getRecordEntry, type LogicAppsV2, type Template } from '@microsoft/logic-apps-shared';
import {
InitApiManagementService,
InitAppServiceService,
InitConnectionParameterEditorService,
InitConnectionService,
InitFunctionService,
InitGatewayService,
InitOAuthService,
getIntl,
getRecordEntry,
type LogicAppsV2,
type Template,
} from '@microsoft/logic-apps-shared';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { templatesPathFromState, type RootState } from './store';
import type { WorkflowParameterUpdateEvent } from '@microsoft/designer-ui';
import { validateParameterValueWithSwaggerType } from '../../../core/utils/validation';
import type { TemplateServiceOptions } from '../../../core/templates/TemplatesDesignerContext';

export interface TemplateState {
templateName?: string;
interface TemplateData {
workflowDefinition: LogicAppsV2.WorkflowDefinition | undefined;
manifest: Template.Manifest | undefined;
workflowName: string | undefined;
Expand All @@ -18,6 +30,11 @@ export interface TemplateState {
connections: Record<string, Template.Connection>;
}

export interface TemplateState extends TemplateData {
templateName?: string;
servicesInitialized: boolean;
}

const initialState: TemplateState = {
workflowDefinition: undefined,
manifest: undefined,
Expand All @@ -28,8 +45,43 @@ const initialState: TemplateState = {
validationErrors: {},
},
connections: {},
servicesInitialized: false,
};

export const initializeTemplateServices = createAsyncThunk(
'initializeTemplateServices',
async ({
connectionService,
oAuthService,
gatewayService,
apimService,
functionService,
appServiceService,
connectionParameterEditorService,
}: TemplateServiceOptions) => {
InitConnectionService(connectionService);
InitOAuthService(oAuthService);

if (gatewayService) {
InitGatewayService(gatewayService);
}
if (apimService) {
InitApiManagementService(apimService);
}
if (functionService) {
InitFunctionService(functionService);
}
if (appServiceService) {
InitAppServiceService(appServiceService);
}
if (connectionParameterEditorService) {
InitConnectionParameterEditorService(connectionParameterEditorService);
}

return true;
}
);

export const loadTemplate = createAsyncThunk(
'template/loadTemplate',
async (preLoadedManifest: Template.Manifest | undefined, thunkAPI) => {
Expand Down Expand Up @@ -111,16 +163,17 @@ export const templateSlice = createSlice({
};
state.connections = {};
});

builder.addCase(initializeTemplateServices.fulfilled, (state, action) => {
state.servicesInitialized = action.payload;
});
},
});

export const { changeCurrentTemplateName, updateWorkflowName, updateKind, updateTemplateParameterValue } = templateSlice.actions;
export default templateSlice.reducer;

const loadTemplateFromGithub = async (
templateName: string,
manifest: Template.Manifest | undefined
): Promise<TemplateState | undefined> => {
const loadTemplateFromGithub = async (templateName: string, manifest: Template.Manifest | undefined): Promise<TemplateData | undefined> => {
try {
const templateWorkflowDefinition: LogicAppsV2.WorkflowDefinition = await import(
`${templatesPathFromState}/${templateName}/workflow.json`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useSelector } from "react-redux";
import type { RootState } from "./store";

export const useAreServicesInitialized = () => {
return useSelector((state: RootState) => state.template.servicesInitialized ?? false);
};
19 changes: 18 additions & 1 deletion libs/designer/src/lib/core/state/templates/workflowSlice.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';

export interface ResourceDetails {
subscriptionId: string;
resourceGroup: string;
location: string;
}

export interface WorkflowState {
existingWorkflowName?: string;
isConsumption: boolean;
subscriptionId: string;
resourceGroup: string;
location: string;
}

const initialState: WorkflowState = {
isConsumption: false,
subscriptionId: '',
resourceGroup: '',
location: '',
};

export const workflowSlice = createSlice({
Expand All @@ -17,6 +29,11 @@ export const workflowSlice = createSlice({
setExistingWorkflowName: (state, action: PayloadAction<string>) => {
state.existingWorkflowName = action.payload;
},
setResourceDetails: (state, action: PayloadAction<ResourceDetails>) => {
state.subscriptionId = action.payload.subscriptionId;
state.resourceGroup = action.payload.resourceGroup;
state.location = action.payload.location;
},
clearWorkflowDetails: (state) => {
state.existingWorkflowName = undefined;
},
Expand All @@ -27,5 +44,5 @@ export const workflowSlice = createSlice({
},
});

export const { setExistingWorkflowName, clearWorkflowDetails, setConsumption } = workflowSlice.actions;
export const { setExistingWorkflowName, setResourceDetails, clearWorkflowDetails, setConsumption } = workflowSlice.actions;
export default workflowSlice.reducer;
Loading

0 comments on commit 43070e2

Please sign in to comment.