From 3cf6704dbb347cf4d59848cad508db926c54bc4b Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 5 Jan 2024 18:07:57 +0200 Subject: [PATCH] feat: Enable cred setup for workflows created from templates (no-changelog) (#8240) ## Summary Enable users to open credential setup for workflows that have been created from templates if they skip it. Next steps (will be their own PRs): - Add telemetry events - Add e2e test - Hide the button when user sets up all the credentials - Change the feature flag to a new one ## Related tickets and issues https://linear.app/n8n/issue/ADO-1637/feature-support-template-credential-setup-for-http-request-nodes-that --- cypress/e2e/16-form-trigger-node.cy.ts | 5 +- .../CredentialPicker/CredentialPicker.vue | 27 +- .../src/components/MainHeader/MainHeader.vue | 1 - packages/editor-ui/src/components/Modal.vue | 3 +- packages/editor-ui/src/components/Modals.vue | 14 ++ .../SetupWorkflowCredentialsButton.vue | 44 ++++ .../SetupWorkflowCredentialsModal.vue | 89 +++++++ .../useSetupWorkflowCredentialsModalState.ts | 129 ++++++++++ packages/editor-ui/src/constants.ts | 1 + .../src/plugins/i18n/locales/en.json | 4 +- packages/editor-ui/src/stores/ui.store.ts | 156 ++++++------ .../src/utils/nodes/nodeTransforms.ts | 9 + .../src/utils/templates/templateActions.ts | 12 +- .../src/utils/templates/templateTransforms.ts | 34 +-- packages/editor-ui/src/views/NodeView.vue | 16 ++ .../AppsRequiringCredsNotice.vue | 18 +- .../SetupTemplateFormStep.vue | 49 ++-- .../SetupWorkflowFromTemplateView.vue | 17 +- .../__tests__/setupTemplate.store.test.ts | 222 +---------------- .../__tests__/useCredentialSetupState.test.ts | 141 +++++++++++ .../setupTemplate.store.ts | 196 ++------------- .../useCredentialSetupState.ts | 231 ++++++++++++++++++ 22 files changed, 858 insertions(+), 560 deletions(-) create mode 100644 packages/editor-ui/src/components/SetupWorkflowCredentialsButton/SetupWorkflowCredentialsButton.vue create mode 100644 packages/editor-ui/src/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue create mode 100644 packages/editor-ui/src/components/SetupWorkflowCredentialsModal/useSetupWorkflowCredentialsModalState.ts create mode 100644 packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/useCredentialSetupState.test.ts create mode 100644 packages/editor-ui/src/views/SetupWorkflowFromTemplateView/useCredentialSetupState.ts diff --git a/cypress/e2e/16-form-trigger-node.cy.ts b/cypress/e2e/16-form-trigger-node.cy.ts index 1ec2abc640..8226df6b33 100644 --- a/cypress/e2e/16-form-trigger-node.cy.ts +++ b/cypress/e2e/16-form-trigger-node.cy.ts @@ -11,10 +11,7 @@ describe('n8n Form Trigger', () => { it("add node by clicking on 'On form submission'", () => { workflowPage.getters.canvasPlusButton().click(); - cy.get('#node-view-root > div:nth-child(2) > div > div > aside ') - .find('span') - .contains('On form submission') - .click(); + workflowPage.getters.nodeCreatorNodeItems().contains('On form submission').click(); ndv.getters.parameterInput('formTitle').type('Test Form'); ndv.getters.parameterInput('formDescription').type('Test Form Description'); ndv.getters.parameterInput('fieldLabel').type('Test Field 1'); diff --git a/packages/editor-ui/src/components/CredentialPicker/CredentialPicker.vue b/packages/editor-ui/src/components/CredentialPicker/CredentialPicker.vue index 4ffb4d431d..1f3980aca8 100644 --- a/packages/editor-ui/src/components/CredentialPicker/CredentialPicker.vue +++ b/packages/editor-ui/src/components/CredentialPicker/CredentialPicker.vue @@ -1,10 +1,11 @@ + + + + @@ -197,6 +207,7 @@ import { MFA_SETUP_MODAL_KEY, WORKFLOW_HISTORY_VERSION_RESTORE, SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY, + SETUP_CREDENTIALS_MODAL_KEY, } from '@/constants'; import AboutModal from './AboutModal.vue'; @@ -229,6 +240,7 @@ import ExternalSecretsProviderModal from '@/components/ExternalSecretsProviderMo import DebugPaywallModal from '@/components/DebugPaywallModal.vue'; import WorkflowHistoryVersionRestoreModal from '@/components/WorkflowHistory/WorkflowHistoryVersionRestoreModal.vue'; import SuggestedTemplatesPreviewModal from '@/components/SuggestedTemplates/SuggestedTemplatesPreviewModal.vue'; +import SetupWorkflowCredentialsModal from '@/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue'; export default defineComponent({ name: 'Modals', @@ -263,6 +275,7 @@ export default defineComponent({ MfaSetupModal, WorkflowHistoryVersionRestoreModal, SuggestedTemplatesPreviewModal, + SetupWorkflowCredentialsModal, }, data: () => ({ CHAT_EMBED_MODAL_KEY, @@ -294,6 +307,7 @@ export default defineComponent({ MFA_SETUP_MODAL_KEY, WORKFLOW_HISTORY_VERSION_RESTORE, SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY, + SETUP_CREDENTIALS_MODAL_KEY, }), }); diff --git a/packages/editor-ui/src/components/SetupWorkflowCredentialsButton/SetupWorkflowCredentialsButton.vue b/packages/editor-ui/src/components/SetupWorkflowCredentialsButton/SetupWorkflowCredentialsButton.vue new file mode 100644 index 0000000000..8d7bc0e53a --- /dev/null +++ b/packages/editor-ui/src/components/SetupWorkflowCredentialsButton/SetupWorkflowCredentialsButton.vue @@ -0,0 +1,44 @@ + + + diff --git a/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue b/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue new file mode 100644 index 0000000000..2a0bf164ca --- /dev/null +++ b/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/useSetupWorkflowCredentialsModalState.ts b/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/useSetupWorkflowCredentialsModalState.ts new file mode 100644 index 0000000000..cd5657f015 --- /dev/null +++ b/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/useSetupWorkflowCredentialsModalState.ts @@ -0,0 +1,129 @@ +import { computed } from 'vue'; +import type { INodeCredentialsDetails } from 'n8n-workflow'; +import { useNodeHelpers } from '@/composables/useNodeHelpers'; +import { useCredentialsStore } from '@/stores/credentials.store'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; +import { useCredentialSetupState } from '@/views/SetupWorkflowFromTemplateView/useCredentialSetupState'; + +export const useSetupWorkflowCredentialsModalState = () => { + const workflowsStore = useWorkflowsStore(); + const credentialsStore = useCredentialsStore(); + const nodeHelpers = useNodeHelpers(); + + const workflowNodes = computed(() => { + return workflowsStore.allNodes; + }); + + const { + appCredentials, + credentialOverrides, + credentialUsages, + credentialsByKey, + numFilledCredentials, + selectedCredentialIdByKey, + setSelectedCredentialId, + unsetSelectedCredential, + } = useCredentialSetupState(workflowNodes); + + /** + * Selects initial credentials. For existing workflows this means using + * the credentials that are already set on the nodes. + */ + const setInitialCredentialSelection = () => { + selectedCredentialIdByKey.value = {}; + + for (const credUsage of credentialUsages.value) { + const typeCredentials = credentialsStore.getCredentialsByType(credUsage.credentialType); + // Make sure there is a credential for this type with the given name + const credential = typeCredentials.find((cred) => cred.name === credUsage.credentialName); + if (!credential) { + continue; + } + + selectedCredentialIdByKey.value[credUsage.key] = credential.id; + } + }; + + /** + * Sets the given credential to all nodes that use it. + */ + const setCredential = (credentialKey: TemplateCredentialKey, credentialId: string) => { + setSelectedCredentialId(credentialKey, credentialId); + + const usages = credentialsByKey.value.get(credentialKey); + if (!usages) { + return; + } + + const credentialName = credentialsStore.getCredentialById(credentialId)?.name; + const credential: INodeCredentialsDetails = { + id: credentialId, + name: credentialName, + }; + + usages.usedBy.forEach((node) => { + workflowsStore.updateNodeProperties({ + name: node.name, + properties: { + position: node.position, + credentials: { + ...node.credentials, + [usages.credentialType]: credential, + }, + }, + }); + + // We can't use updateNodeCredentialIssues because the previous + // step creates a new instance of the node in the store and + // `node` no longer points to the correct node. + nodeHelpers.updateNodeCredentialIssuesByName(node.name); + }); + + setInitialCredentialSelection(); + }; + + /** + * Removes given credential from all nodes that use it. + */ + const unsetCredential = (credentialKey: TemplateCredentialKey) => { + unsetSelectedCredential(credentialKey); + + const usages = credentialsByKey.value.get(credentialKey); + if (!usages) { + return; + } + + usages.usedBy.forEach((node) => { + const credentials = { ...node.credentials }; + delete credentials[usages.credentialType]; + + workflowsStore.updateNodeProperties({ + name: node.name, + properties: { + position: node.position, + credentials, + }, + }); + + // We can't use updateNodeCredentialIssues because the previous + // step creates a new instance of the node in the store and + // `node` no longer points to the correct node. + nodeHelpers.updateNodeCredentialIssuesByName(node.name); + }); + + setInitialCredentialSelection(); + }; + + return { + appCredentials, + credentialOverrides, + credentialUsages, + credentialsByKey, + numFilledCredentials, + selectedCredentialIdByKey, + setInitialCredentialSelection, + setCredential, + unsetCredential, + }; +}; diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 68693fb573..42371e9946 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -57,6 +57,7 @@ export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall'; export const MFA_SETUP_MODAL_KEY = 'mfaSetup'; export const WORKFLOW_HISTORY_VERSION_RESTORE = 'workflowHistoryVersionRestore'; export const SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY = 'suggestedTemplatePreview'; +export const SETUP_CREDENTIALS_MODAL_KEY = 'setupCredentials'; export const EXTERNAL_SECRETS_PROVIDER_MODAL_KEY = 'externalSecretsProvider'; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 6bd1383745..562eae33bf 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1095,6 +1095,7 @@ "nodeView.zoomOut": "Zoom Out", "nodeView.zoomToFit": "Zoom to Fit", "nodeView.replaceMe": "Replace Me", + "nodeView.setupTemplate": "Set up Template", "contextMenu.node": "node | nodes", "contextMenu.sticky": "sticky note | sticky notes", "contextMenu.selectAll": "Select all", @@ -2359,5 +2360,6 @@ "templateSetup.skip": "Skip", "templateSetup.continue.button": "Continue", "templateSetup.credential.description": "The credential you select will be used in the {0} node of the workflow template. | The credential you select will be used in the {0} nodes of the workflow template.", - "templateSetup.continue.button.fillRemaining": "Fill remaining credentials to continue" + "templateSetup.continue.button.fillRemaining": "Fill remaining credentials to continue", + "setupCredentialsModal.title": "Setup credentials" } diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index 9667f80e5c..e127d36cdb 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -38,6 +38,7 @@ import { N8N_PRICING_PAGE_URL, WORKFLOW_HISTORY_VERSION_RESTORE, SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY, + SETUP_CREDENTIALS_MODAL_KEY, } from '@/constants'; import type { CloudUpdateLinkSourceType, @@ -84,67 +85,47 @@ try { } } catch (e) {} +export type UiStore = ReturnType; + export const useUIStore = defineStore(STORES.UI, { state: (): UIState => ({ activeActions: [], activeCredentialType: null, theme: savedTheme, modals: { - [ABOUT_MODAL_KEY]: { - open: false, - }, - [CHAT_EMBED_MODAL_KEY]: { - open: false, - }, - [CHANGE_PASSWORD_MODAL_KEY]: { - open: false, - }, - [CONTACT_PROMPT_MODAL_KEY]: { - open: false, - }, - [CREDENTIAL_SELECT_MODAL_KEY]: { - open: false, - }, + ...Object.fromEntries( + [ + ABOUT_MODAL_KEY, + CHAT_EMBED_MODAL_KEY, + CHANGE_PASSWORD_MODAL_KEY, + CONTACT_PROMPT_MODAL_KEY, + CREDENTIAL_SELECT_MODAL_KEY, + DUPLICATE_MODAL_KEY, + ONBOARDING_CALL_SIGNUP_MODAL_KEY, + PERSONALIZATION_MODAL_KEY, + INVITE_USER_MODAL_KEY, + TAGS_MANAGER_MODAL_KEY, + VALUE_SURVEY_MODAL_KEY, + VERSIONS_MODAL_KEY, + WORKFLOW_LM_CHAT_MODAL_KEY, + WORKFLOW_SETTINGS_MODAL_KEY, + WORKFLOW_SHARE_MODAL_KEY, + WORKFLOW_ACTIVE_MODAL_KEY, + COMMUNITY_PACKAGE_INSTALL_MODAL_KEY, + MFA_SETUP_MODAL_KEY, + SOURCE_CONTROL_PUSH_MODAL_KEY, + SOURCE_CONTROL_PULL_MODAL_KEY, + EXTERNAL_SECRETS_PROVIDER_MODAL_KEY, + DEBUG_PAYWALL_MODAL_KEY, + WORKFLOW_HISTORY_VERSION_RESTORE, + SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY, + SETUP_CREDENTIALS_MODAL_KEY, + ].map((modalKey) => [modalKey, { open: false }]), + ), [DELETE_USER_MODAL_KEY]: { open: false, activeId: null, }, - [DUPLICATE_MODAL_KEY]: { - open: false, - }, - [ONBOARDING_CALL_SIGNUP_MODAL_KEY]: { - open: false, - }, - [PERSONALIZATION_MODAL_KEY]: { - open: false, - }, - [INVITE_USER_MODAL_KEY]: { - open: false, - }, - [TAGS_MANAGER_MODAL_KEY]: { - open: false, - }, - [VALUE_SURVEY_MODAL_KEY]: { - open: false, - }, - [VERSIONS_MODAL_KEY]: { - open: false, - }, - [WORKFLOW_LM_CHAT_MODAL_KEY]: { - open: false, - }, - [WORKFLOW_SETTINGS_MODAL_KEY]: { - open: false, - }, - [WORKFLOW_SHARE_MODAL_KEY]: { - open: false, - }, - [WORKFLOW_ACTIVE_MODAL_KEY]: { - open: false, - }, - [COMMUNITY_PACKAGE_INSTALL_MODAL_KEY]: { - open: false, - }, [COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY]: { open: false, mode: '', @@ -155,9 +136,6 @@ export const useUIStore = defineStore(STORES.UI, { curlCommand: '', httpNodeParameters: '', }, - [MFA_SETUP_MODAL_KEY]: { - open: false, - }, [LOG_STREAM_MODAL_KEY]: { open: false, data: undefined, @@ -168,24 +146,6 @@ export const useUIStore = defineStore(STORES.UI, { activeId: null, showAuthSelector: false, }, - [SOURCE_CONTROL_PUSH_MODAL_KEY]: { - open: false, - }, - [SOURCE_CONTROL_PULL_MODAL_KEY]: { - open: false, - }, - [EXTERNAL_SECRETS_PROVIDER_MODAL_KEY]: { - open: false, - }, - [DEBUG_PAYWALL_MODAL_KEY]: { - open: false, - }, - [WORKFLOW_HISTORY_VERSION_RESTORE]: { - open: false, - }, - [SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY]: { - open: false, - }, }, modalStack: [], sidebarMenuCollapsed: true, @@ -463,17 +423,6 @@ export const useUIStore = defineStore(STORES.UI, { return name !== openModalName; }); }, - closeAllModals(): void { - Object.keys(this.modals).forEach((name) => { - if (this.modals[name].open) { - this.modals[name] = { - ...this.modals[name], - open: false, - }; - } - }); - this.modalStack = []; - }, draggableStartDragging(type: string, data: string): void { this.draggable = { isDragging: true, @@ -672,3 +621,44 @@ export const useUIStore = defineStore(STORES.UI, { }, }, }); + +/** + * Helper function for listening to credential changes in the store + */ +export const listenForModalChanges = (opts: { + store: UiStore; + onModalOpened?: (name: keyof Modals) => void; + onModalClosed?: (name: keyof Modals) => void; +}): void => { + const { store, onModalClosed, onModalOpened } = opts; + const listeningForActions = ['openModal', 'openModalWithData', 'closeModal']; + + store.$onAction((result) => { + const { name, after, args } = result; + after(async () => { + if (!listeningForActions.includes(name)) { + return; + } + + switch (name) { + case 'openModal': { + const modalName = args[0]; + onModalOpened?.(modalName); + break; + } + + case 'openModalWithData': { + const { name: modalName } = args[0] ?? {}; + onModalOpened?.(modalName); + break; + } + + case 'closeModal': { + const modalName = args[0]; + onModalClosed?.(modalName); + break; + } + } + }); + }); +}; diff --git a/packages/editor-ui/src/utils/nodes/nodeTransforms.ts b/packages/editor-ui/src/utils/nodes/nodeTransforms.ts index 80c4a4a2a6..0cdad985ae 100644 --- a/packages/editor-ui/src/utils/nodes/nodeTransforms.ts +++ b/packages/editor-ui/src/utils/nodes/nodeTransforms.ts @@ -31,3 +31,12 @@ export function getNodeTypeDisplayableCredentials( return displayableCredentials; } + +export function doesNodeHaveCredentialsToFill( + nodeTypeProvider: NodeTypeProvider, + node: Pick, +): boolean { + const requiredCredentials = getNodeTypeDisplayableCredentials(nodeTypeProvider, node); + + return requiredCredentials.length > 0; +} diff --git a/packages/editor-ui/src/utils/templates/templateActions.ts b/packages/editor-ui/src/utils/templates/templateActions.ts index 210ec3f10b..ba85cf3178 100644 --- a/packages/editor-ui/src/utils/templates/templateActions.ts +++ b/packages/editor-ui/src/utils/templates/templateActions.ts @@ -12,10 +12,7 @@ import type { useWorkflowsStore } from '@/stores/workflows.store'; import { getFixedNodesList } from '@/utils/nodeViewUtils'; import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms'; import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; -import { - getNodesRequiringCredentials, - replaceAllTemplateNodeCredentials, -} from '@/utils/templates/templateTransforms'; +import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms'; import type { INodeCredentialsDetails } from 'n8n-workflow'; import type { RouteLocationRaw, Router } from 'vue-router'; import type { TemplatesStore } from '@/stores/templates.store'; @@ -23,6 +20,7 @@ import type { NodeTypesStore } from '@/stores/nodeTypes.store'; import type { Telemetry } from '@/plugins/telemetry'; import type { useExternalHooks } from '@/composables/useExternalHooks'; import { assert } from '@/utils/assert'; +import { doesNodeHaveCredentialsToFill } from '@/utils/nodes/nodeTransforms'; type ExternalHooks = ReturnType; @@ -126,9 +124,9 @@ function hasTemplateCredentials( nodeTypeProvider: NodeTypeProvider, template: ITemplatesWorkflowFull, ) { - const nodesRequiringCreds = getNodesRequiringCredentials(nodeTypeProvider, template); - - return nodesRequiringCreds.length > 0; + return template.workflow.nodes.some((node) => + doesNodeHaveCredentialsToFill(nodeTypeProvider, node), + ); } async function getFullTemplate(templatesStore: TemplatesStore, templateId: string) { diff --git a/packages/editor-ui/src/utils/templates/templateTransforms.ts b/packages/editor-ui/src/utils/templates/templateTransforms.ts index e92b7a2347..a1eba68c67 100644 --- a/packages/editor-ui/src/utils/templates/templateTransforms.ts +++ b/packages/editor-ui/src/utils/templates/templateTransforms.ts @@ -1,8 +1,4 @@ -import type { - ITemplatesWorkflowFull, - IWorkflowTemplateNode, - IWorkflowTemplateNodeCredentials, -} from '@/Interface'; +import type { IWorkflowTemplateNode, IWorkflowTemplateNodeCredentials } from '@/Interface'; import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms'; import { getNodeTypeDisplayableCredentials } from '@/utils/nodes/nodeTransforms'; import type { NormalizedTemplateNodeCredentials } from '@/utils/templates/templateTypes'; @@ -43,8 +39,12 @@ export const keyFromCredentialTypeAndName = ( * different versions of n8n may have different credential formats. */ export const normalizeTemplateNodeCredentials = ( - credentials: IWorkflowTemplateNodeCredentials, + credentials?: IWorkflowTemplateNodeCredentials, ): NormalizedTemplateNodeCredentials => { + if (!credentials) { + return {}; + } + return Object.fromEntries( Object.entries(credentials).map(([key, value]) => { return typeof value === 'string' ? [key, value] : [key, value.name]; @@ -133,25 +133,3 @@ export const replaceAllTemplateNodeCredentials = ( }; }); }; - -/** - * Returns the nodes in the template that require credentials - * and the required credentials for each node. - */ -export const getNodesRequiringCredentials = ( - nodeTypeProvider: NodeTypeProvider, - template: ITemplatesWorkflowFull, -): TemplateNodeWithRequiredCredential[] => { - if (!template) { - return []; - } - - const nodesWithCredentials: TemplateNodeWithRequiredCredential[] = template.workflow.nodes - .map((node) => ({ - node, - requiredCredentials: getNodeTypeDisplayableCredentials(nodeTypeProvider, node), - })) - .filter(({ requiredCredentials }) => requiredCredentials.length > 0); - - return nodesWithCredentials; -}; diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index f163879ef0..86806ca93c 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -96,6 +96,11 @@ @stopExecution="stopExecution" @saveKeyboardShortcut="onSaveKeyboardShortcut" /> + +
+ +
+
import('@/components/Node/NodeCreation.vue')); const CanvasControls = defineAsyncComponent(async () => import('@/components/CanvasControls.vue')); +const SetupWorkflowCredentialsButton = defineAsyncComponent( + async () => + import('@/components/SetupWorkflowCredentialsButton/SetupWorkflowCredentialsButton.vue'), +); export default defineComponent({ name: 'NodeView', @@ -393,6 +402,7 @@ export default defineComponent({ NodeCreation, CanvasControls, ContextMenu, + SetupWorkflowCredentialsButton, }, mixins: [moveNodeWorkflow, workflowHelpers, workflowRun, debounceHelper], async beforeRouteLeave(to, from, next) { @@ -5180,4 +5190,10 @@ export default defineComponent({ transform: translate3d(4px, 0, 0); } } + +.setupCredentialsButtonWrapper { + position: absolute; + left: 35px; + top: var(--spacing-s); +} diff --git a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/AppsRequiringCredsNotice.vue b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/AppsRequiringCredsNotice.vue index 401a122ae2..a640b49f9d 100644 --- a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/AppsRequiringCredsNotice.vue +++ b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/AppsRequiringCredsNotice.vue @@ -1,20 +1,24 @@