From afe3cd3a8825e92ae3f182e50dbe5ceca394a3fb Mon Sep 17 00:00:00 2001 From: preetriti1 <48265693+preetriti1@users.noreply.github.com> Date: Mon, 24 Jun 2024 00:40:25 -0700 Subject: [PATCH] feat(templates): Adding connections list in create panel and component for create connection (#5013) * feat(templates): Adding Connections panel with create connection component * Fixing tests --------- Co-authored-by: Priti Sambandam --- Localize/lang/strings.json | 8 + .../app/TemplatesStandaloneDesigner.tsx | 10 +- .../src/lib/templates/templates.less | 45 +++ .../templates/templatesParametersField.less | 32 -- .../core/actions/bjsworkflow/connections.ts | 21 ++ .../src/lib/core/queries/connections.ts | 4 +- .../core/state/connection/connectionSlice.ts | 13 +- .../lib/core/state/templates/templateSlice.ts | 5 +- .../lib/core/state/templates/workflowSlice.ts | 46 ++- .../core/templates/TemplatesDataProvider.tsx | 13 +- .../templates/TemplatesDesignerContext.tsx | 2 + .../__tests__/TemplatesDataProvider.spec.tsx | 5 + .../src/lib/core/templates/utils/helper.ts | 15 + .../lib/core/utils/connectors/connections.ts | 17 +- .../createConnection/createConnection.tsx | 4 +- .../createConnectionWrapper.tsx | 175 ++++++---- .../panel/templatePanel/createConnection.tsx | 49 +++ .../__tests__/displayConnections.spec.tsx | 16 +- .../lib/ui/templates/cards/templateCard.less | 2 + .../lib/ui/templates/cards/templateCard.tsx | 13 +- .../ui/templates/connections/connector.tsx | 75 +++-- .../connections/displayConnections.tsx | 307 ++++++++++++++++-- .../src/utils/src/lib/models/template.ts | 2 +- 23 files changed, 706 insertions(+), 173 deletions(-) delete mode 100644 libs/designer-ui/src/lib/templates/templatesParametersField.less create mode 100644 libs/designer/src/lib/ui/panel/templatePanel/createConnection.tsx diff --git a/Localize/lang/strings.json b/Localize/lang/strings.json index 68c62d77800..bddec53f669 100644 --- a/Localize/lang/strings.json +++ b/Localize/lang/strings.json @@ -879,6 +879,7 @@ "Xj4xwI": "The managed identity used with this operation no longer exists. To continue, select an available identity or change the connection.", "XkBxv5": "Select a target schema", "Xkt2vD": "Select a function app function", + "Xld9qI": "Configure connections to authenticate the following services and link your workflows with various services and applications, enabling seamless data integration and automation. Connections are required.", "Xnn0uj": "Request", "XqamWZ": "Delete", "Xs7Uvt": "By Microsoft", @@ -1823,6 +1824,7 @@ "_Xj4xwI.comment": "Erorr mesade when managed identity is not present in logic apps", "_XkBxv5.comment": "Target schema dropdown placeholder", "_Xkt2vD.comment": "Label for function app selection", + "_Xld9qI.comment": "Message to describe the connections tab", "_Xnn0uj.comment": "Header text for request", "_XqamWZ.comment": "Label of Delete Token Button", "_Xs7Uvt.comment": "Panel description for stating it was created by Microsoft", @@ -2057,6 +2059,7 @@ "_hZqQdt.comment": "Time zone value ", "_hbOvB4.comment": "Dislike button text for suggested flow", "_hihfHd.comment": "Error validation message integers", + "_hlrKDC.comment": "Column name for connection display name", "_hq1mk6.comment": "Error while parsing expression for path value", "_hrbDu6.comment": "Label text for retry duration", "_hsZ7em.comment": "An accessability label that describes the connections tab", @@ -2358,6 +2361,7 @@ "_t+XCkg.comment": "Required string parameter for destination time zone", "_t/RPwA.comment": "shows how many results are returned after search", "_t2Xi1/.comment": "tree showing schema nodes", + "_t7ytOJ.comment": "Column name for connection status", "_t9RwOi.comment": "Invalid expression alert", "_tAbbH8.comment": "Title for a too many inputs card", "_tAeKNh.comment": "Function display radio group option for simple", @@ -2369,6 +2373,7 @@ "_tMRPnG.comment": "Label for description of custom workflow Function", "_tMdcE1.comment": "Error message to show on connection error during deserialization", "_tNoZx2.comment": "Parameter Field Type Title", + "_tRe2Ct.comment": "Column name for connector name", "_tTIsTX.comment": "Chatbot suggestion button to test this workflow", "_tUCptx.comment": "label to insert link", "_tUU4ak.comment": "Time zone value ", @@ -2714,6 +2719,7 @@ "hZqQdt": "(UTC+02:00) Gaza, Hebron", "hbOvB4": "This isn't what I'm looking for", "hihfHd": "The value is too large.", + "hlrKDC": "Connection", "hq1mk6": "Operation path value does not match the template for segment. Path {pathValue}, Template {pathTemplate}", "hrbDu6": "Duration", "hsZ7em": "Connections Tab", @@ -3015,6 +3021,7 @@ "t+XCkg": "Required. A string that contains the time zone name of the destination time zone. See https://msdn.microsoft.com/en-us/library/gg154758.aspx for details.", "t/RPwA": "results found", "t2Xi1/": "Schema tree", + "t7ytOJ": "Status", "t9RwOi": "The expression is invalid.", "tAbbH8": "Too many inputs assigned", "tAeKNh": "Simple", @@ -3026,6 +3033,7 @@ "tMRPnG": "This function provides you details for the workflow itself at runtime", "tMdcE1": "Invalid connection, please update your connection to load complete details", "tNoZx2": "Type", + "tRe2Ct": "Name", "tTIsTX": "Test this workflow", "tUCptx": "Insert Link", "tUU4ak": "(UTC+11:00) Chokurdakh", diff --git a/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx b/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx index 789ee76617f..319025559d8 100644 --- a/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx +++ b/apps/Standalone/src/templates/app/TemplatesStandaloneDesigner.tsx @@ -25,7 +25,7 @@ import { HttpClient } from '../../designer/app/AzureLogicAppsDesigner/Services/H // 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 type { Template, LogicAppsV2, IWorkflowService } from '@microsoft/logic-apps-shared'; import { saveWorkflowStandard } from '../../designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts'; import type { ParametersData } from '../../designer/app/AzureLogicAppsDesigner/Models/Workflow'; import axios from 'axios'; @@ -46,7 +46,7 @@ export const TemplatesStandaloneDesigner = () => { const navigate = useNavigate(); // const navigate = useNavigate(); - + const connectionReferences = WorkflowUtility.convertConnectionsDataToReferences(connectionsData); const sanitizeParameterName = (parameterName: string, workflowName: string) => parameterName.replace('_#workflowname#', `_${workflowName}`); @@ -171,6 +171,7 @@ export const TemplatesStandaloneDesigner = () => { resourceGroup: resourceDetails.resourceGroup, location: canonicalLocation, }} + connectionReferences={connectionReferences} services={services} isConsumption={isConsumption} existingWorkflowName={existingWorkflowName} @@ -237,11 +238,16 @@ const getServices = ( tenantId, objectId, }); + const workflowService: IWorkflowService = { + getCallbackUrl: () => Promise.resolve({} as any), + getAppIdentity: () => workflowApp?.identity as any, + }; return { connectionService, gatewayService, tenantService, oAuthService, + workflowService, }; }; diff --git a/libs/designer-ui/src/lib/templates/templates.less b/libs/designer-ui/src/lib/templates/templates.less index 600749c8417..0624979757d 100644 --- a/libs/designer-ui/src/lib/templates/templates.less +++ b/libs/designer-ui/src/lib/templates/templates.less @@ -109,4 +109,49 @@ min-height: 24px; } } + +.msla-template-create-tabs { + padding: 30px 0 30px 0; + .msla-template-create-tabs-description { + padding: 20px 10px 10px 0; + } + + .msla-template-create-connector { + + .msla-template-create-connector-icon { + position: relative; + top: 2px; + width: 20px; + height: 20px; + } + + .msla-template-create-connector-text { + position: relative; + top: -3px; + padding-left: 10px; + } + } + + .msla-template-connection-status { + .msla-template-connection-status-badge { + margin: 6px 8px 0 0; + font-size: 14px; + } + + .msla-template-connection-status-text { + position: relative; + top: -3px; + } + } + + .msla-template-connection-text { + margin-top: 3px; + } + + .msla-template-create-progress-connector { + display: flex; + position: relative; + top: 5px; + } +} \ No newline at end of file diff --git a/libs/designer-ui/src/lib/templates/templatesParametersField.less b/libs/designer-ui/src/lib/templates/templatesParametersField.less deleted file mode 100644 index 5dfe9b005e4..00000000000 --- a/libs/designer-ui/src/lib/templates/templatesParametersField.less +++ /dev/null @@ -1,32 +0,0 @@ -.msla-templates-parameters { - display: flex; - flex-direction: column; - gap: 16px; - - .msla-templates-parameter-heading { - margin-bottom: 8px; - } - - .msla-templates-parameter-heading-text { - font-weight: 600; - font-size: 14px; - } - - .msla-templates-parameter-description { - margin-bottom: 16px; - } - - .msla-templates-parameter-description-text { - font-size: 13px; - color: @ms-color-secondary; - } - - .ms-List-cell { - padding-bottom: 16px; - } - - .msla-templates-parameter-field { - display: flex; - min-height: 24px; - } -} diff --git a/libs/designer/src/lib/core/actions/bjsworkflow/connections.ts b/libs/designer/src/lib/core/actions/bjsworkflow/connections.ts index 94ee0751d5d..28468582603 100644 --- a/libs/designer/src/lib/core/actions/bjsworkflow/connections.ts +++ b/libs/designer/src/lib/core/actions/bjsworkflow/connections.ts @@ -5,7 +5,9 @@ import type { DeserializedWorkflow } from '../../parsers/BJSWorkflow/BJSDeserial import { getConnection } from '../../queries/connections'; import { getConnector, getOperationInfo, getOperationManifest } from '../../queries/operation'; import { changeConnectionMapping, initializeConnectionsMappings } from '../../state/connection/connectionSlice'; +import { changeConnectionMapping as changeTemplateConnectionMapping } from '../../state/templates/workflowSlice'; import { updateErrorDetails } from '../../state/operation/operationMetadataSlice'; +import type { RootState as TemplateRootState } from '../../state/templates/store'; import type { RootState } from '../../store'; import { getConnectionReference, @@ -54,6 +56,25 @@ export interface UpdateConnectionPayload { connectionRuntimeUrl?: string; } +export const updateTemplateConnection = createAsyncThunk( + 'updateTemplateConnection', + async (payload: ConnectionPayload, { dispatch, getState }): Promise => { + const { nodeId, connector, connection, connectionProperties, authentication } = payload; + dispatch( + changeTemplateConnectionMapping({ + nodeId, + connectorId: connector.id, + connectionId: connection.id, + authentication: authentication ?? getApiHubAuthenticationIfRequired(), + connectionProperties: connectionProperties ?? getConnectionPropertiesIfRequired(connection, connector), + connectionRuntimeUrl: isOpenApiSchemaVersion((getState() as TemplateRootState).template.workflowDefinition) + ? connection.properties.connectionRuntimeUrl + : undefined, + }) + ); + } +); + export const updateNodeConnection = createAsyncThunk( 'updateNodeConnection', async (payload: ConnectionPayload, { dispatch, getState }): Promise => { diff --git a/libs/designer/src/lib/core/queries/connections.ts b/libs/designer/src/lib/core/queries/connections.ts index 6bcd614bf01..fee00dfaea0 100644 --- a/libs/designer/src/lib/core/queries/connections.ts +++ b/libs/designer/src/lib/core/queries/connections.ts @@ -68,10 +68,10 @@ export const useAllConnections = (): UseQueryResult => { }); }; -export const useConnectionsForConnector = (connectorId: string) => { +export const useConnectionsForConnector = (connectorId: string, shouldNotRefetch?: boolean) => { return useQuery([connectionKey, connectorId?.toLowerCase()], () => ConnectionService().getConnections(connectorId), { enabled: !!connectorId, - refetchOnMount: true, + refetchOnMount: !shouldNotRefetch && true, cacheTime: 0, staleTime: 0, }); diff --git a/libs/designer/src/lib/core/state/connection/connectionSlice.ts b/libs/designer/src/lib/core/state/connection/connectionSlice.ts index fc8ef62b77e..d9d6c468aa0 100644 --- a/libs/designer/src/lib/core/state/connection/connectionSlice.ts +++ b/libs/designer/src/lib/core/state/connection/connectionSlice.ts @@ -1,7 +1,8 @@ +import { getExistingReferenceKey } from '../../utils/connectors/connections'; import type { ConnectionMapping, ConnectionReference, ConnectionReferences, NodeId, ReferenceKey } from '../../../common/models/workflow'; import type { UpdateConnectionPayload } from '../../actions/bjsworkflow/connections'; import { resetWorkflowState } from '../global'; -import { LogEntryLevel, LoggerService, deepCompareObjects, equals, getUniqueName } from '@microsoft/logic-apps-shared'; +import { LogEntryLevel, LoggerService, getUniqueName } from '@microsoft/logic-apps-shared'; import { createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; @@ -29,15 +30,7 @@ export const connectionSlice = createSlice({ }, changeConnectionMapping: (state, action: PayloadAction) => { const { nodeId, connectionId, connectorId, connectionProperties, connectionRuntimeUrl, authentication } = action.payload; - const existingReferenceKey = Object.keys(state.connectionReferences).find((referenceKey) => { - const reference = state.connectionReferences[referenceKey]; - return ( - equals(reference.api.id, connectorId) && - equals(reference.connection.id, connectionId) && - equals(reference.connectionRuntimeUrl ?? '', connectionRuntimeUrl ?? '') && - deepCompareObjects(reference.connectionProperties, connectionProperties) - ); - }); + const existingReferenceKey = getExistingReferenceKey(state.connectionReferences, action.payload); if (existingReferenceKey) { state.connectionsMapping[nodeId] = existingReferenceKey; diff --git a/libs/designer/src/lib/core/state/templates/templateSlice.ts b/libs/designer/src/lib/core/state/templates/templateSlice.ts index 585246d2cc7..89520154707 100644 --- a/libs/designer/src/lib/core/state/templates/templateSlice.ts +++ b/libs/designer/src/lib/core/state/templates/templateSlice.ts @@ -11,6 +11,7 @@ import { getRecordEntry, type LogicAppsV2, type Template, + InitWorkflowService, } from '@microsoft/logic-apps-shared'; import type { PayloadAction } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; @@ -55,6 +56,7 @@ export const initializeTemplateServices = createAsyncThunk( 'initializeTemplateServices', async ({ connectionService, + workflowService, oAuthService, gatewayService, tenantService, @@ -65,6 +67,7 @@ export const initializeTemplateServices = createAsyncThunk( }: TemplateServiceOptions) => { InitConnectionService(connectionService); InitOAuthService(oAuthService); + InitWorkflowService(workflowService); if (gatewayService) { InitGatewayService(gatewayService); @@ -233,7 +236,7 @@ const loadTemplateFromGithub = async (templateName: string, manifest: Template.M workflowDefinition: (templateWorkflowDefinition as any)?.default ?? templateWorkflowDefinition, manifest: templateManifest, workflowName: templateManifest.title, - kind: templateManifest.kinds.length === 1 ? templateManifest.kinds[0] : undefined, + kind: templateManifest.kinds?.length === 1 ? templateManifest.kinds[0] : undefined, parameters: { definitions: parametersDefinitions, validationErrors: {}, diff --git a/libs/designer/src/lib/core/state/templates/workflowSlice.ts b/libs/designer/src/lib/core/state/templates/workflowSlice.ts index c2285afe00a..5a98339a2de 100644 --- a/libs/designer/src/lib/core/state/templates/workflowSlice.ts +++ b/libs/designer/src/lib/core/state/templates/workflowSlice.ts @@ -1,5 +1,8 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; +import type { ConnectionReferences } from '../../../common/models/workflow'; +import type { UpdateConnectionPayload } from '../../actions/bjsworkflow/connections'; +import { getExistingReferenceKey } from '../../utils/connectors/connections'; export interface ResourceDetails { subscriptionId: string; @@ -13,6 +16,10 @@ export interface WorkflowState { subscriptionId: string; resourceGroup: string; location: string; + connections: { + references: ConnectionReferences; + mapping: Record; + }; } const initialState: WorkflowState = { @@ -20,6 +27,10 @@ const initialState: WorkflowState = { subscriptionId: '', resourceGroup: '', location: '', + connections: { + references: {}, + mapping: {}, + }, }; export const workflowSlice = createSlice({ @@ -41,8 +52,41 @@ export const workflowSlice = createSlice({ state.isConsumption = action.payload; state.existingWorkflowName = undefined; }, + initializeConnectionReferences: (state, action: PayloadAction) => { + const references = action.payload; + state.connections.references = references; + state.connections.mapping = Object.keys(references).reduce((result: Record, key: string) => { + result[key] = key; + return result; + }, {}); + }, + changeConnectionMapping: (state, action: PayloadAction) => { + const { nodeId: key, connectionId, connectorId, connectionProperties, connectionRuntimeUrl, authentication } = action.payload; + const existingReferenceKey = getExistingReferenceKey(state.connections.references, action.payload); + + if (existingReferenceKey) { + state.connections.mapping[key] = existingReferenceKey; + } else { + state.connections.references[key] = { + api: { id: connectorId }, + connection: { id: connectionId }, + connectionName: connectionId.split('/').at(-1) as string, + connectionProperties, + connectionRuntimeUrl, + authentication, + }; + state.connections.mapping[key] = key; + } + }, }, }); -export const { setExistingWorkflowName, setResourceDetails, clearWorkflowDetails, setConsumption } = workflowSlice.actions; +export const { + setExistingWorkflowName, + setResourceDetails, + clearWorkflowDetails, + setConsumption, + initializeConnectionReferences, + changeConnectionMapping, +} = workflowSlice.actions; export default workflowSlice.reducer; diff --git a/libs/designer/src/lib/core/templates/TemplatesDataProvider.tsx b/libs/designer/src/lib/core/templates/TemplatesDataProvider.tsx index 8163c180396..0d2db408e82 100644 --- a/libs/designer/src/lib/core/templates/TemplatesDataProvider.tsx +++ b/libs/designer/src/lib/core/templates/TemplatesDataProvider.tsx @@ -4,15 +4,23 @@ import { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import type { AppDispatch, RootState } from '../state/templates/store'; import { loadManifestNames, loadManifests } from '../state/templates/manifestSlice'; -import { type ResourceDetails, setConsumption, setExistingWorkflowName, setResourceDetails } from '../state/templates/workflowSlice'; +import { + type ResourceDetails, + setConsumption, + setExistingWorkflowName, + setResourceDetails, + initializeConnectionReferences, +} from '../state/templates/workflowSlice'; import { initializeTemplateServices } from '../state/templates/templateSlice'; import { useAreServicesInitialized } from '../state/templates/templateselectors'; +import type { ConnectionReferences } from '../../common/models/workflow'; export interface TemplatesDataProviderProps { isConsumption: boolean | undefined; existingWorkflowName: string | undefined; resourceDetails: ResourceDetails; services: TemplateServiceOptions; + connectionReferences: ConnectionReferences; children?: React.ReactNode; } @@ -58,7 +66,8 @@ export const TemplatesDataProvider = (props: TemplatesDataProviderProps) => { } dispatch(setResourceDetails(props.resourceDetails)); - }, [dispatch, servicesInitialized, props.services, props.resourceDetails]); + dispatch(initializeConnectionReferences(props.connectionReferences)); + }, [dispatch, servicesInitialized, props.services, props.resourceDetails, props.connectionReferences]); if (!servicesInitialized) { return null; diff --git a/libs/designer/src/lib/core/templates/TemplatesDesignerContext.tsx b/libs/designer/src/lib/core/templates/TemplatesDesignerContext.tsx index a311a87460c..a74d31fcfcf 100644 --- a/libs/designer/src/lib/core/templates/TemplatesDesignerContext.tsx +++ b/libs/designer/src/lib/core/templates/TemplatesDesignerContext.tsx @@ -8,6 +8,7 @@ import type { ITenantService, ILoggerService, IOAuthService, + IWorkflowService, } from '@microsoft/logic-apps-shared'; import { createContext } from 'react'; @@ -17,6 +18,7 @@ export interface TemplatesDesignerContext { export interface TemplateServiceOptions { connectionService: IConnectionService; + workflowService: IWorkflowService; gatewayService?: IGatewayService; tenantService?: ITenantService; loggerService?: ILoggerService; diff --git a/libs/designer/src/lib/core/templates/__tests__/TemplatesDataProvider.spec.tsx b/libs/designer/src/lib/core/templates/__tests__/TemplatesDataProvider.spec.tsx index 08438ef02c8..2a7f353509b 100644 --- a/libs/designer/src/lib/core/templates/__tests__/TemplatesDataProvider.spec.tsx +++ b/libs/designer/src/lib/core/templates/__tests__/TemplatesDataProvider.spec.tsx @@ -5,6 +5,8 @@ import { type RootState, setupStore, type AppStore } from '../../state/templates import { renderWithProviders } from '../../../__test__/template-test-utils'; import { TemplatesDesignerProvider } from '../TemplatesDesignerProvider'; import { ReactQueryProvider } from '../../ReactQueryProvider'; +// biome-ignore lint/correctness/noUnusedImports: +import React from 'react'; describe('templates/TemplatesDataProvider', () => { let store: AppStore; @@ -16,6 +18,7 @@ describe('templates/TemplatesDataProvider', () => { subscriptionId: '', resourceGroup: '', location: '', + connections: { references: {}, mapping: {}}, }, template: { workflowDefinition: undefined, @@ -46,9 +49,11 @@ describe('templates/TemplatesDataProvider', () => { resourceDetails={{ subscriptionId: 'sub', resourceGroup: 'rg', location: 'us' }} isConsumption={false} existingWorkflowName={'workflowName'} + connectionReferences={{}} services={{ connectionService: {} as any, oAuthService: {} as any, + workflowService: {} as any, }} >
{'Children'}
diff --git a/libs/designer/src/lib/core/templates/utils/helper.ts b/libs/designer/src/lib/core/templates/utils/helper.ts index a20883bb379..80d9a112d44 100644 --- a/libs/designer/src/lib/core/templates/utils/helper.ts +++ b/libs/designer/src/lib/core/templates/utils/helper.ts @@ -36,3 +36,18 @@ export const normalizeConnectorId = (connectorId: string, subscriptionId: string const result = connectorId.replaceAll('#subscription#', subscriptionId); return result.replaceAll('#location#', location); }; + +export const getConnectorResources = (intl: IntlShape) => { + return { + connected: intl.formatMessage({ + defaultMessage: 'Connected', + id: 'oOGTSo', + description: 'Connected text', + }), + notConnected: intl.formatMessage({ + defaultMessage: 'Not Connected', + id: '3HrFPS', + description: 'Not Connected text', + }), + }; +}; diff --git a/libs/designer/src/lib/core/utils/connectors/connections.ts b/libs/designer/src/lib/core/utils/connectors/connections.ts index a7b6010ff24..f7d684ae258 100644 --- a/libs/designer/src/lib/core/utils/connectors/connections.ts +++ b/libs/designer/src/lib/core/utils/connectors/connections.ts @@ -1,5 +1,5 @@ import constants from '../../../common/constants'; -import type { ConnectionReference } from '../../../common/models/workflow'; +import type { ConnectionReference, ConnectionReferences } from '../../../common/models/workflow'; import { getConnection } from '../../queries/connections'; import { getOperationManifest } from '../../queries/operation'; import type { ConnectionsStoreState } from '../../state/connection/connectionSlice'; @@ -18,6 +18,7 @@ import { getResourceName, getRecordEntry, getPropertyValue, + deepCompareObjects, } from '@microsoft/logic-apps-shared'; import type { AssistedConnectionProps } from '@microsoft/designer-ui'; import type { @@ -28,6 +29,7 @@ import type { ManagedIdentity, OperationManifest, } from '@microsoft/logic-apps-shared'; +import type { UpdateConnectionPayload } from '../../../core/actions/bjsworkflow/connections'; export function getConnectionId(state: ConnectionsStoreState, nodeId: string): string { return getConnectionReference(state, nodeId)?.connection?.id ?? ''; @@ -66,6 +68,19 @@ export async function isConnectionReferenceValid( } } +export function getExistingReferenceKey(allReferences: ConnectionReferences, connectionData: UpdateConnectionPayload): string | undefined { + const { connectionId, connectorId, connectionProperties, connectionRuntimeUrl } = connectionData; + return Object.keys(allReferences).find((referenceKey) => { + const reference = allReferences[referenceKey]; + return ( + equals(reference.api.id, connectorId) && + equals(reference.connection.id, connectionId) && + equals(reference.connectionRuntimeUrl ?? '', connectionRuntimeUrl ?? '') && + deepCompareObjects(reference.connectionProperties, connectionProperties) + ); + }); +} + export function getAssistedConnectionProps(connector: Connector, manifest?: OperationManifest): AssistedConnectionProps | undefined { const hasAzureConnection = connector.properties.capabilities?.includes('azureConnection') ?? false; diff --git a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx index 131a2e0fd3e..72efd3dc68b 100644 --- a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx +++ b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx @@ -64,6 +64,7 @@ export interface CreateConnectionProps { ) => void; cancelCallback?: () => void; hideCancelButton?: boolean; + showActionBar?: boolean; errorMessage?: string; clearErrorCallback?: () => void; selectSubscriptionCallback?: (subscriptionId: string) => void; @@ -78,6 +79,7 @@ export interface CreateConnectionProps { export const CreateConnection = (props: CreateConnectionProps) => { const { nodeIds = [], + showActionBar = true, iconUri = '', connector, connectionParameterSets: _connectionParameterSets, @@ -572,7 +574,7 @@ export const CreateConnection = (props: CreateConnectionProps) => { return (
- + {showActionBar ? : null} {componentDescription} diff --git a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnectionWrapper.tsx b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnectionWrapper.tsx index 6cae25dfc8b..30c4f25dd54 100644 --- a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnectionWrapper.tsx +++ b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnectionWrapper.tsx @@ -2,7 +2,6 @@ import { useQueryClient } from '@tanstack/react-query'; import constants from '../../../../common/constants'; import type { AppDispatch, RootState } from '../../../../core'; import { useOperationInfo, useSelectedNodeId, useSelectedNodeIds } from '../../../../core'; -import type { ConnectionPayload } from '../../../../core/actions/bjsworkflow/connections'; import { getApiHubAuthentication, getConnectionMetadata, @@ -13,6 +12,7 @@ import { import { getUniqueConnectionName } from '../../../../core/queries/connections'; import { useConnectorByNodeId, + useConnectorOnly, useGatewayServiceConfig, useGateways, useSubscriptions, @@ -34,6 +34,7 @@ import { WorkflowService, getIconUriFromConnector, getRecordEntry, + type ConnectionMetadata, type ConnectionCreationInfo, type ConnectionParametersMetadata, type Connection, @@ -45,59 +46,106 @@ import { import { useCallback, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; +import type { AssistedConnectionProps } from '@microsoft/designer-ui'; +import type { ApiHubAuthentication } from 'lib/common/models/workflow'; export const CreateConnectionWrapper = () => { const dispatch = useDispatch(); - const intl = useIntl(); const nodeId: string = useSelectedNodeId(); const nodeIds = useSelectedNodeIds(); const connector = useConnectorByNodeId(nodeId); - const iconUri = useMemo(() => getIconUriFromConnector(connector), [connector]); const operationInfo = useOperationInfo(nodeId); const { data: operationManifest } = useOperationManifest(operationInfo); const connectionMetadata = getConnectionMetadata(operationManifest); const hasExistingConnection = useSelector((state: RootState) => !!getRecordEntry(state.connections.connectionsMapping, nodeId)); - const subscriptionsQuery = useSubscriptions(); - const subscriptions = useMemo(() => subscriptionsQuery.data, [subscriptionsQuery.data]); - const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(''); - const gatewaysQuery = useGateways(selectedSubscriptionId, connector?.id ?? ''); - const availableGateways = useMemo(() => gatewaysQuery.data, [gatewaysQuery]); - const gatewayServiceConfig = useGatewayServiceConfig(); - const existingReferences = useSelector((state: RootState) => Object.keys(state.connections.connectionReferences)); - const identity = WorkflowService().getAppIdentity?.() as ManagedIdentity; - - const [isLoading, setIsLoading] = useState(false); - - const [errorMessage, setErrorMessage] = useState(undefined); + const assistedConnectionProps = useMemo( + () => (connector ? getAssistedConnectionProps(connector, operationManifest) : undefined), + [connector, operationManifest] + ); - const applyNewConnection = useCallback( - (_nodeId: string, newConnection: Connection, selectedIdentity?: string) => { - const payload: ConnectionPayload = { - nodeId: _nodeId, - connection: newConnection, - connector: connector as Connector, - }; + const referencePanelMode = useReferencePanelMode(); + const closeConnectionsFlow = useCallback(() => { + const panelMode = referencePanelMode ?? 'Operation'; + const nodeId = panelMode === 'Operation' ? nodeIds?.[0] : undefined; + dispatch(setIsCreatingConnection(false)); + dispatch(openPanel({ nodeId, panelMode })); + }, [dispatch, referencePanelMode, nodeIds]); - if (selectedIdentity) { - const userAssignedIdentity = selectedIdentity !== constants.SYSTEM_ASSIGNED_MANAGED_IDENTITY ? selectedIdentity : undefined; - payload.connectionProperties = getConnectionProperties(connector as Connector, userAssignedIdentity); - payload.authentication = getApiHubAuthentication(userAssignedIdentity); + const updateConnectionInState = useCallback( + (payload: CreatedConnectionPayload) => { + for (const nodeId of nodeIds) { + dispatch(updateNodeConnection({ ...payload, nodeId })); } - - dispatch(updateNodeConnection(payload)); }, - [connector, dispatch] + [dispatch, nodeIds] ); - const assistedConnectionProps = useMemo( - () => (connector ? getAssistedConnectionProps(connector, operationManifest) : undefined), - [connector, operationManifest] + return ( + closeConnectionsFlow()} + /> ); +}; +export interface CreatedConnectionPayload { + connector: Connector; + connection: Connection; + connectionProperties?: Record; + authentication?: ApiHubAuthentication; +} + +export const CreateConnectionInternal = (props: { + connectorId: string; + operationType: string; + existingReferences: string[]; + hideCancelButton: boolean; + showActionBar: boolean; + updateConnectionInState: (payload: CreatedConnectionPayload) => void; + onConnectionCreated: (connection: Connection) => void; + nodeIds?: string[]; + assistedConnectionProps?: AssistedConnectionProps; + connectionMetadata?: ConnectionMetadata; +}) => { + const { + connectorId, + operationType, + assistedConnectionProps, + existingReferences, + connectionMetadata, + nodeIds = [], + hideCancelButton, + showActionBar, + updateConnectionInState, + onConnectionCreated, + } = props; + const dispatch = useDispatch(); + + const intl = useIntl(); + const { data: connector } = useConnectorOnly(connectorId); + const iconUri = useMemo(() => getIconUriFromConnector(connector), [connector]); + const subscriptionsQuery = useSubscriptions(); + const subscriptions = useMemo(() => subscriptionsQuery.data, [subscriptionsQuery.data]); + const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(''); + const gatewaysQuery = useGateways(selectedSubscriptionId, connector?.id ?? ''); + const availableGateways = useMemo(() => gatewaysQuery.data, [gatewaysQuery]); + const gatewayServiceConfig = useGatewayServiceConfig(); + const identity = WorkflowService().getAppIdentity?.() as ManagedIdentity; + + const [isCreating, setIsCreating] = useState(false); + const [errorMessage, setErrorMessage] = useState(undefined); const [selectedResourceId, setSelectedResourceId] = useState(''); const [selectedSubResource, setSelectedSubResource] = useState(); @@ -120,16 +168,8 @@ export const CreateConnectionWrapper = () => { } : undefined; - const referencePanelMode = useReferencePanelMode(); - const closeConnectionsFlow = useCallback(() => { - const panelMode = referencePanelMode ?? 'Operation'; - const nodeId = panelMode === 'Operation' ? nodeIds?.[0] : undefined; - dispatch(setIsCreatingConnection(false)); - dispatch(openPanel({ nodeId, panelMode })); - }, [dispatch, referencePanelMode, nodeIds]); - const queryClient = useQueryClient(); - const updateNewConnection = useCallback( + const updateNewConnectionInCache = useCallback( async (newConnection: Connection) => { return queryClient.setQueryData( ['connections', connector?.id?.toLowerCase()], @@ -138,6 +178,26 @@ export const CreateConnectionWrapper = () => { }, [connector?.id, queryClient] ); + + const applyNewConnection = useCallback( + (newConnection: Connection, selectedIdentity?: string) => { + const payload: CreatedConnectionPayload = { + connection: newConnection, + connector: connector as Connector, + }; + + if (selectedIdentity) { + const userAssignedIdentity = selectedIdentity !== constants.SYSTEM_ASSIGNED_MANAGED_IDENTITY ? selectedIdentity : undefined; + payload.connectionProperties = getConnectionProperties(connector as Connector, userAssignedIdentity); + payload.authentication = getApiHubAuthentication(userAssignedIdentity); + } + + updateConnectionInState(payload); + onConnectionCreated(newConnection); + }, + [connector, onConnectionCreated, updateConnectionInState] + ); + const createConnectionCallback = useCallback( async ( displayName?: string, @@ -152,7 +212,7 @@ export const CreateConnectionWrapper = () => { return; } - setIsLoading(true); + setIsCreating(true); setErrorMessage(undefined); let outputParameterValues = parameterValues; @@ -185,7 +245,7 @@ export const CreateConnectionWrapper = () => { // Assign connection parameters from resource selector experience if (assistedConnectionProps) { outputParameterValues = await getConnectionParametersForAzureConnection( - operationManifest?.properties.connection?.type, + connectionMetadata?.type, selectedSubResource, outputParameterValues, !!selectedParameterSet // TODO: Should remove this when backend updates all connection parameters for functions and apim @@ -203,7 +263,7 @@ export const CreateConnectionWrapper = () => { }; const parametersMetadata: ConnectionParametersMetadata = { - connectionMetadata: connectionMetadata, + connectionMetadata, connectionParameterSet: selectedParameterSet, connectionParameters: selectedParameterSet?.parameters ?? connector?.properties.connectionParameters, }; @@ -228,11 +288,8 @@ export const CreateConnectionWrapper = () => { } if (connection) { - updateNewConnection(connection); - for (const nodeId of nodeIds) { - applyNewConnection(nodeId, connection, identitySelected); - } - closeConnectionsFlow(); + updateNewConnectionInCache(connection); + applyNewConnection(connection, identitySelected); } else if (err) { setErrorMessage(String(err)); } @@ -246,19 +303,16 @@ export const CreateConnectionWrapper = () => { error: error instanceof Error ? error : undefined, }); } - setIsLoading(false); + setIsCreating(false); }, [ - connector, + applyNewConnection, assistedConnectionProps, connectionMetadata, - operationManifest?.properties.connection?.type, - selectedSubResource, - closeConnectionsFlow, - nodeIds, - applyNewConnection, + connector, existingReferences, - updateNewConnection, + selectedSubResource, + updateNewConnectionInCache, ] ); @@ -284,17 +338,18 @@ export const CreateConnectionWrapper = () => { setErrorMessage(undefined)} selectSubscriptionCallback={(subscriptionId: string) => setSelectedSubscriptionId(subscriptionId)} diff --git a/libs/designer/src/lib/ui/panel/templatePanel/createConnection.tsx b/libs/designer/src/lib/ui/panel/templatePanel/createConnection.tsx new file mode 100644 index 00000000000..1307a4adfe0 --- /dev/null +++ b/libs/designer/src/lib/ui/panel/templatePanel/createConnection.tsx @@ -0,0 +1,49 @@ +import { useCallback, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import type { Connection } from '@microsoft/logic-apps-shared'; +import { equals } from '@microsoft/logic-apps-shared'; +import type { CreatedConnectionPayload } from '../connectionsPanel/createConnection/createConnectionWrapper'; +import { CreateConnectionInternal } from '../connectionsPanel/createConnection/createConnectionWrapper'; +import { useConnectorOnly } from '../../../core/state/connection/connectionSelector'; +import type { AppDispatch, RootState } from '../../../core/state/templates/store'; +import { getAssistedConnectionProps } from '../../../core/utils/connectors/connections'; +import { updateTemplateConnection } from '../../../core/actions/bjsworkflow/connections'; + +export const CreateConnectionInTemplate = (props: { + connectorId: string; + connectionKey: string; + onConnectionCreated: (connection: Connection) => void; +}) => { + const { connectorId, connectionKey, onConnectionCreated } = props; + const dispatch = useDispatch(); + const { data: connector } = useConnectorOnly(connectorId); + + const { references, connections } = useSelector((state: RootState) => ({ + references: Object.keys(state.workflow.connections.references), + connections: state.template.manifest?.connections, + })); + const isInAppConnector = equals(connections?.[connectionKey]?.kind, 'inapp'); + + // TODO - Needs update to support azure resource selection for connections. Operation Manifest is required. + const assistedConnectionProps = useMemo(() => (connector ? getAssistedConnectionProps(connector) : undefined), [connector]); + + const updateConnectionInState = useCallback( + (payload: CreatedConnectionPayload) => { + dispatch(updateTemplateConnection({ ...payload, nodeId: connectionKey })); + }, + [connectionKey, dispatch] + ); + + return ( + + ); +}; diff --git a/libs/designer/src/lib/ui/templates/__tests__/displayConnections.spec.tsx b/libs/designer/src/lib/ui/templates/__tests__/displayConnections.spec.tsx index ea776ff6595..ccc036ce42b 100644 --- a/libs/designer/src/lib/ui/templates/__tests__/displayConnections.spec.tsx +++ b/libs/designer/src/lib/ui/templates/__tests__/displayConnections.spec.tsx @@ -1,11 +1,14 @@ import { describe, beforeAll, expect, it, beforeEach } from 'vitest'; import type { AppStore } from '../../../core/state/templates/store'; import { setupStore } from '../../../core/state/templates/store'; -import type { Template } from '@microsoft/logic-apps-shared'; +import { InitConnectionService, type Template } from '@microsoft/logic-apps-shared'; import { renderWithProviders } from '../../../__test__/template-test-utils'; import { screen } from '@testing-library/react'; import type { TemplateState } from '../../../core/state/templates/templateSlice'; import { DisplayConnections } from '../connections/displayConnections'; +import { ReactQueryProvider } from '../../../core/ReactQueryProvider'; +// biome-ignore lint/correctness/noUnusedImports: +import React from 'react'; describe('ui/templates/displayConnections', () => { let store: AppStore; @@ -57,14 +60,21 @@ describe('ui/templates/displayConnections', () => { template: templateSliceData, }; store = setupStore(minimalStoreData); + + InitConnectionService({ + getConnector: async () => Promise.resolve({ id: '/serviceProviders/abc', properties: { iconUrl: 'iconUrl', displayName: 'AB C' }}) + } as any); }); beforeEach(() => { - renderWithProviders(, { store }); + renderWithProviders( + + + , { store }); }); it('should render the connection ids for connections', async () => { const conn = template1Manifest?.connections['conn1']; - expect(screen.getByText(`1: ${conn.connectorId}`)).toBeDefined(); + expect(screen.getByText('Name')).toBeDefined(); }); }); diff --git a/libs/designer/src/lib/ui/templates/cards/templateCard.less b/libs/designer/src/lib/ui/templates/cards/templateCard.less index 2a8e38cc173..1669c8f9013 100644 --- a/libs/designer/src/lib/ui/templates/cards/templateCard.less +++ b/libs/designer/src/lib/ui/templates/cards/templateCard.less @@ -78,6 +78,8 @@ padding: 5px; position: relative; top: 10px; + width: 20px; + height: 20px; } } } diff --git a/libs/designer/src/lib/ui/templates/cards/templateCard.tsx b/libs/designer/src/lib/ui/templates/cards/templateCard.tsx index 570567f9f19..d80b82005b9 100644 --- a/libs/designer/src/lib/ui/templates/cards/templateCard.tsx +++ b/libs/designer/src/lib/ui/templates/cards/templateCard.tsx @@ -12,7 +12,7 @@ interface TemplateCardProps { templateName: string; } -const maxConnectorsToShow = 5; +const maxConnectorsToShow = 1; export const TemplateCard = ({ templateName }: TemplateCardProps) => { const dispatch = useDispatch(); @@ -38,7 +38,16 @@ export const TemplateCard = ({ templateName }: TemplateCardProps) => { const showOverflow = connectors.length > maxConnectorsToShow; const connectorsToShow = showOverflow ? connectors.slice(0, maxConnectorsToShow) : connectors; const overflowList = showOverflow ? connectors.slice(maxConnectorsToShow) : []; - const onRenderMenuItem = (item: IContextualMenuItem) => ; + const onRenderMenuItem = (item: IContextualMenuItem) => ( + + ); const onRenderMenuIcon = () =>
{`+${overflowList.length}`}
; const menuProps: IContextualMenuProps = { items: overflowList.map((connector) => ({ key: connector.connectorId, text: connector.connectorId, onRender: onRenderMenuItem })), diff --git a/libs/designer/src/lib/ui/templates/connections/connector.tsx b/libs/designer/src/lib/ui/templates/connections/connector.tsx index c964dde897c..778822a9772 100644 --- a/libs/designer/src/lib/ui/templates/connections/connector.tsx +++ b/libs/designer/src/lib/ui/templates/connections/connector.tsx @@ -1,10 +1,11 @@ import type { IImageStyles, IImageStyleProps, IStyleFunctionOrObject } from '@fluentui/react'; -import { Icon, ImageFit, Shimmer, ShimmerElementType, Spinner, SpinnerSize, Text } from '@fluentui/react'; +import { Icon, ImageFit, Shimmer, ShimmerElementType, Spinner, SpinnerSize, Text, css } from '@fluentui/react'; import { useConnectorOnly } from '../../../core/state/connection/connectionSelector'; -import type { Template } from '@microsoft/logic-apps-shared'; +import type { Connector, Template } from '@microsoft/logic-apps-shared'; import { useIntl } from 'react-intl'; import { getConnectorAllCategories } from '@microsoft/designer-ui'; import { useConnectionsForConnector } from '../../../core/queries/connections'; +import { getConnectorResources } from '../../../core/templates/utils/helper'; const iconStyles = { root: { @@ -37,25 +38,46 @@ export const ConnectorIcon = ({ ); }; -export const ConnectorIconWithName = ({ connectorId }: { connectorId: string }) => { - const { data: connector, isLoading, isError } = useConnectorOnly(connectorId); - const icon = isLoading ? : isError ? : ; +export const ConnectorIconWithName = ({ + connectorId, + classes, + showProgress, + onConnectorLoaded, +}: { + connectorId: string; + classes: Record; + showProgress?: boolean; + onConnectorLoaded?: (connector: Connector) => void; +}) => { + const { data: connector, isLoading } = useConnectorOnly(connectorId); - return ( -
- {connector ? ( - + + - ) : ( - icon - )} - {connector?.properties.displayName} +
+ ); + } + + if (onConnectorLoaded && connector) { + onConnectorLoaded(connector); + } + + return ( +
+ + {connector?.properties.displayName}
); }; @@ -70,7 +92,7 @@ const textStyles = { export const ConnectorWithDetails = ({ connectorId, kind }: Template.Connection) => { const { data: connector, isLoading, isError } = useConnectorOnly(connectorId); - const { data: connections, isLoading: isConnectionsLoading } = useConnectionsForConnector(connectorId); + const { data: connections, isLoading: isConnectionsLoading } = useConnectionsForConnector(connectorId, /* shouldNotRefetch */ true); const intl = useIntl(); if (!connector) { @@ -78,18 +100,7 @@ export const ConnectorWithDetails = ({ connectorId, kind }: Template.Connection) } const allCategories = getConnectorAllCategories(); - const text = { - connected: intl.formatMessage({ - defaultMessage: 'Connected', - id: 'oOGTSo', - description: 'Connected text', - }), - notConnected: intl.formatMessage({ - defaultMessage: 'Not Connected', - id: '3HrFPS', - description: 'Not Connected text', - }), - }; + const text = getConnectorResources(intl); return (
= { + true: { + iconName: 'SkypeCircleCheck', + color: '#57a300', + }, + false: { + iconName: 'Blocked2Solid', + color: '#e00b1ccf', + }, +}; export interface DisplayConnectionsProps { connections: Record; } export const DisplayConnections = ({ connections }: DisplayConnectionsProps) => { - return ( - <> -
Template Connections
- {Object.keys(connections).map((connectionKey, index) => { - const connection = connections[connectionKey]; + const intl = useIntl(); + const { subscriptionId, location } = useSelector((state: RootState) => state.workflow); + const columnsNames = { + name: intl.formatMessage({ defaultMessage: 'Name', id: 'tRe2Ct', description: 'Column name for connector name' }), + status: intl.formatMessage({ defaultMessage: 'Status', id: 't7ytOJ', description: 'Column name for connection status' }), + connection: intl.formatMessage({ defaultMessage: 'Connection', id: 'hlrKDC', description: 'Column name for connection display name' }), + }; + + const [connectionsList, setConnectionsList] = useFunctionalState( + Object.keys(connections).map((key) => ({ + key, + connectorId: normalizeConnectorId(connections[key].connectorId, subscriptionId, location), + })) + ); + + const updateItemInConnectionsList = (key: string, item: ConnectionItem) => { + const newList = connectionsList().map((connection: ConnectionItem) => (connection.key === key ? item : connection)); + setConnectionsList(newList); + }; + + const _onColumnClick = (_event: React.MouseEvent, column: IColumn): void => { + let isSortedDescending = column.isSortedDescending; + + // If we've sorted this column, flip it. + if (column.isSorted) { + isSortedDescending = !isSortedDescending; + } + + // Sort the items. + const sortedItems = _copyAndSort(connectionsList(), column.fieldName as string, isSortedDescending); + setConnectionsList(sortedItems); + setColumns( + columns().map((col) => { + col.isSorted = col.key === column.key; + + if (col.isSorted) { + col.isSortedDescending = !!isSortedDescending; + } + + return col; + }) + ); + }; + + const [columns, setColumns] = useFunctionalState([ + { + ariaLabel: columnsNames.name, + fieldName: '$name', + key: '$name', + isResizable: true, + minWidth: 1, + name: columnsNames.name, + maxWidth: 200, + showSortIconWhenUnsorted: true, + onColumnClick: _onColumnClick, + }, + { + ariaLabel: columnsNames.status, + fieldName: '$status', + flexGrow: 1, + key: '$status', + isResizable: true, + minWidth: 1, + maxWidth: 150, + name: columnsNames.status, + showSortIconWhenUnsorted: true, + onColumnClick: _onColumnClick, + }, + { + ariaLabel: columnsNames.connection, + fieldName: '$connection', + flexGrow: 1, + key: '$connection', + isMultiline: true, + isResizable: true, + minWidth: 0, + name: columnsNames.connection, + showSortIconWhenUnsorted: true, + targetWidthProportion: 6, + onColumnClick: _onColumnClick, + }, + ]); + + const onRenderItemColumn = (item: ConnectionItem, _index: number | undefined, column: IColumn | undefined) => { + switch (column?.key) { + case '$name': + return ( + + updateItemInConnectionsList(item.key, { ...item, connectorDisplayName: connector.properties.displayName }) + } + /> + ); + + case '$status': { + return item.allConnections === undefined ? ( + { + const hasConnection = connections.length > 0; + updateItemInConnectionsList(item.key, { + ...item, + allConnections: connections, + hasConnection, + connection: hasConnection ? getConnectionDetails(connections[0]) : undefined, + }); + }} + /> + ) : ( + + ); + } + + case '$connection': return ( -
- - {index + 1}: {connection?.connectorId} - - -
- ID: {connection?.connectorId}
-
+ { + updateItemInConnectionsList(item.key, { + ...item, + connection: { id: connection.id, displayName: connection.properties.displayName }, + }); + }} + /> ); - })} - + + default: + return null; + } + }; + + return ( +
+ + {intl.formatMessage({ + defaultMessage: + 'Configure connections to authenticate the following services and link your workflows with various services and applications, enabling seamless data integration and automation. Connections are required.', + id: 'Xld9qI', + description: 'Message to describe the connections tab', + })} + + +
); }; + +const ConnectionStatusWithProgress = ({ + item, + intl, + onConnectionLoaded, +}: { item: ConnectionItem; intl: IntlShape; onConnectionLoaded?: (connections: Connection[]) => void }): JSX.Element => { + const { data } = useConnectionsForConnector(item.connectorId, /* shouldNotRefetch */ true); + + if (data && onConnectionLoaded) { + onConnectionLoaded(data); + } + + return data ? ( + 0} intl={intl} /> + ) : ( + + ); +}; + +const ConnectionStatus = ({ hasConnection, intl }: { hasConnection: boolean; intl: IntlShape }): JSX.Element => { + const resources = getConnectorResources(intl); + const statusText: Record = { + true: resources.connected, + false: resources.notConnected, + }; + const key = (!!hasConnection).toString(); + const details = connectionStatus[key]; + return ( +
+ + {hasConnection ? statusText[key] : statusText[key]} +
+ ); +}; + +const ConnectionName = ({ + item, + onConnectionCreated, +}: { item: ConnectionItem; onConnectionCreated: (connection: Connection) => void }): JSX.Element => { + const { connectorId, key, connection } = item; + const [showCreate, setShowCreate] = useState(false); + //const { data, isLoading } = useConnectionsForConnector(connectorId); + if (connection?.id) { + return {connection.displayName}; + } + + const handleConnectionCreate = (connection: Connection) => { + onConnectionCreated(connection); + setShowCreate(false); + }; + + const onCreateConnection = () => { + setShowCreate(true); + }; + return showCreate ? ( + + ) : ( + + Connect + + ); +}; + +interface ConnectionItem { + key: string; + connectorId: string; + connectorDisplayName?: string; + connection?: { + id: string; + displayName: string; + }; + hasConnection?: boolean; + allConnections?: Connection[]; +} + +// TODO: Update the connection in store or the reference. +function getConnectionDetails(connection: Connection): { id: string; referenceKey?: string; displayName: string } { + return { + id: connection.id, + displayName: connection.properties.displayName ?? connection.name, + }; +} + +function _copyAndSort(items: ConnectionItem[], columnKey: string, isSortedDescending?: boolean): ConnectionItem[] { + const keyPath = + columnKey === '$name' ? ['connectorDisplayName'] : columnKey === '$status' ? ['hasConnection'] : ['connection', 'displayName']; + return items.slice(0).sort((a: ConnectionItem, b: ConnectionItem) => { + return ( + isSortedDescending + ? getObjectPropertyValue(a, keyPath) < getObjectPropertyValue(b, keyPath) + : getObjectPropertyValue(a, keyPath) > getObjectPropertyValue(b, keyPath) + ) + ? 1 + : -1; + }); +} diff --git a/libs/logic-apps-shared/src/utils/src/lib/models/template.ts b/libs/logic-apps-shared/src/utils/src/lib/models/template.ts index 3e2696ecb7c..c84b1a97388 100644 --- a/libs/logic-apps-shared/src/utils/src/lib/models/template.ts +++ b/libs/logic-apps-shared/src/utils/src/lib/models/template.ts @@ -6,7 +6,7 @@ export interface Manifest { title: string; description: string; skus: SkuType[]; - kinds: WorkflowKindType[]; + kinds?: WorkflowKindType[]; details: Record; tags?: string[]; artifacts: Artifact[];