mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Add docs sidebar to credential modal (#9914)
Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
This commit is contained in:
parent
ab2a548856
commit
b2f8ea7918
|
@ -1,396 +1,373 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.container" data-test-id="node-credentials-config-container">
|
<div>
|
||||||
<Banner
|
<div :class="$style.config" data-test-id="node-credentials-config-container">
|
||||||
v-show="showValidationWarning"
|
<Banner
|
||||||
theme="danger"
|
v-show="showValidationWarning"
|
||||||
:message="
|
theme="danger"
|
||||||
$locale.baseText(
|
:message="
|
||||||
`credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow${
|
$locale.baseText(
|
||||||
credentialPermissions.update ? '' : '.sharee'
|
`credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow${
|
||||||
}`,
|
credentialPermissions.update ? '' : '.sharee'
|
||||||
{ interpolate: { owner: credentialOwnerName } },
|
}`,
|
||||||
)
|
{ interpolate: { owner: credentialOwnerName } },
|
||||||
"
|
)
|
||||||
/>
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
<Banner
|
<Banner
|
||||||
v-if="authError && !showValidationWarning"
|
v-if="authError && !showValidationWarning"
|
||||||
theme="danger"
|
theme="danger"
|
||||||
:message="
|
:message="
|
||||||
$locale.baseText(
|
$locale.baseText(
|
||||||
`credentialEdit.credentialConfig.couldntConnectWithTheseSettings${
|
`credentialEdit.credentialConfig.couldntConnectWithTheseSettings${
|
||||||
credentialPermissions.update ? '' : '.sharee'
|
credentialPermissions.update ? '' : '.sharee'
|
||||||
}`,
|
}`,
|
||||||
{ interpolate: { owner: credentialOwnerName } },
|
{ interpolate: { owner: credentialOwnerName } },
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
:details="authError"
|
:details="authError"
|
||||||
:button-label="$locale.baseText('credentialEdit.credentialConfig.retry')"
|
:button-label="$locale.baseText('credentialEdit.credentialConfig.retry')"
|
||||||
button-loading-label="Retrying"
|
button-loading-label="Retrying"
|
||||||
:button-title="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
|
:button-title="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
|
||||||
:button-loading="isRetesting"
|
:button-loading="isRetesting"
|
||||||
@click="$emit('retest')"
|
@click="$emit('retest')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Banner
|
<Banner
|
||||||
v-show="showOAuthSuccessBanner && !showValidationWarning"
|
v-show="showOAuthSuccessBanner && !showValidationWarning"
|
||||||
theme="success"
|
theme="success"
|
||||||
:message="$locale.baseText('credentialEdit.credentialConfig.accountConnected')"
|
:message="$locale.baseText('credentialEdit.credentialConfig.accountConnected')"
|
||||||
:button-label="$locale.baseText('credentialEdit.credentialConfig.reconnect')"
|
:button-label="$locale.baseText('credentialEdit.credentialConfig.reconnect')"
|
||||||
:button-title="$locale.baseText('credentialEdit.credentialConfig.reconnectOAuth2Credential')"
|
:button-title="
|
||||||
data-test-id="oauth-connect-success-banner"
|
$locale.baseText('credentialEdit.credentialConfig.reconnectOAuth2Credential')
|
||||||
@click="$emit('oauth')"
|
"
|
||||||
>
|
data-test-id="oauth-connect-success-banner"
|
||||||
<template v-if="isGoogleOAuthType" #button>
|
@click="$emit('oauth')"
|
||||||
<p
|
>
|
||||||
:class="$style.googleReconnectLabel"
|
<template v-if="isGoogleOAuthType" #button>
|
||||||
v-text="`${$locale.baseText('credentialEdit.credentialConfig.reconnect')}:`"
|
<p
|
||||||
|
:class="$style.googleReconnectLabel"
|
||||||
|
v-text="`${$locale.baseText('credentialEdit.credentialConfig.reconnect')}:`"
|
||||||
|
/>
|
||||||
|
<GoogleAuthButton @click="$emit('oauth')" />
|
||||||
|
</template>
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-show="testedSuccessfully && !showValidationWarning"
|
||||||
|
theme="success"
|
||||||
|
:message="$locale.baseText('credentialEdit.credentialConfig.connectionTestedSuccessfully')"
|
||||||
|
:button-label="$locale.baseText('credentialEdit.credentialConfig.retry')"
|
||||||
|
:button-loading-label="$locale.baseText('credentialEdit.credentialConfig.retrying')"
|
||||||
|
:button-title="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
|
||||||
|
:button-loading="isRetesting"
|
||||||
|
data-test-id="credentials-config-container-test-success"
|
||||||
|
@click="$emit('retest')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-if="credentialPermissions.update">
|
||||||
|
<n8n-notice v-if="documentationUrl && credentialProperties.length && !docs" theme="warning">
|
||||||
|
{{ $locale.baseText('credentialEdit.credentialConfig.needHelpFillingOutTheseFields') }}
|
||||||
|
<span class="ml-4xs">
|
||||||
|
<n8n-link :to="documentationUrl" size="small" bold @click="onDocumentationUrlClick">
|
||||||
|
{{ $locale.baseText('credentialEdit.credentialConfig.openDocs') }}
|
||||||
|
</n8n-link>
|
||||||
|
</span>
|
||||||
|
</n8n-notice>
|
||||||
|
|
||||||
|
<AuthTypeSelector
|
||||||
|
v-if="showAuthTypeSelector && isNewCredential"
|
||||||
|
:credential-type="credentialType"
|
||||||
|
@auth-type-changed="onAuthTypeChange"
|
||||||
/>
|
/>
|
||||||
<GoogleAuthButton @click="$emit('oauth')" />
|
|
||||||
</template>
|
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<Banner
|
<CopyInput
|
||||||
v-show="testedSuccessfully && !showValidationWarning"
|
v-if="isOAuthType && !allOAuth2BasePropertiesOverridden"
|
||||||
theme="success"
|
:label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')"
|
||||||
:message="$locale.baseText('credentialEdit.credentialConfig.connectionTestedSuccessfully')"
|
:value="oAuthCallbackUrl"
|
||||||
:button-label="$locale.baseText('credentialEdit.credentialConfig.retry')"
|
:copy-button-text="$locale.baseText('credentialEdit.credentialConfig.clickToCopy')"
|
||||||
:button-loading-label="$locale.baseText('credentialEdit.credentialConfig.retrying')"
|
:hint="
|
||||||
:button-title="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
|
$locale.baseText('credentialEdit.credentialConfig.subtitle', {
|
||||||
:button-loading="isRetesting"
|
interpolate: { appName },
|
||||||
data-test-id="credentials-config-container-test-success"
|
|
||||||
@click="$emit('retest')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template v-if="credentialPermissions.update">
|
|
||||||
<n8n-notice v-if="documentationUrl && credentialProperties.length" theme="warning">
|
|
||||||
{{ $locale.baseText('credentialEdit.credentialConfig.needHelpFillingOutTheseFields') }}
|
|
||||||
<span class="ml-4xs">
|
|
||||||
<n8n-link :to="documentationUrl" size="small" bold @click="onDocumentationUrlClick">
|
|
||||||
{{ $locale.baseText('credentialEdit.credentialConfig.openDocs') }}
|
|
||||||
</n8n-link>
|
|
||||||
</span>
|
|
||||||
</n8n-notice>
|
|
||||||
|
|
||||||
<AuthTypeSelector
|
|
||||||
v-if="showAuthTypeSelector && isNewCredential"
|
|
||||||
:credential-type="credentialType"
|
|
||||||
@auth-type-changed="onAuthTypeChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CopyInput
|
|
||||||
v-if="isOAuthType && !allOAuth2BasePropertiesOverridden"
|
|
||||||
:label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')"
|
|
||||||
:value="oAuthCallbackUrl"
|
|
||||||
:copy-button-text="$locale.baseText('credentialEdit.credentialConfig.clickToCopy')"
|
|
||||||
:hint="
|
|
||||||
$locale.baseText('credentialEdit.credentialConfig.subtitle', { interpolate: { appName } })
|
|
||||||
"
|
|
||||||
:toast-title="
|
|
||||||
$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')
|
|
||||||
"
|
|
||||||
:redact-value="true"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<EnterpriseEdition v-else :features="[EnterpriseEditionFeature.Sharing]">
|
|
||||||
<div>
|
|
||||||
<n8n-info-tip :bold="false">
|
|
||||||
{{
|
|
||||||
$locale.baseText('credentialEdit.credentialEdit.info.sharee', {
|
|
||||||
interpolate: { credentialOwnerName },
|
|
||||||
})
|
})
|
||||||
}}
|
"
|
||||||
</n8n-info-tip>
|
:toast-title="
|
||||||
</div>
|
$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')
|
||||||
</EnterpriseEdition>
|
"
|
||||||
|
:redact-value="true"
|
||||||
<CredentialInputs
|
/>
|
||||||
v-if="credentialType && credentialPermissions.update"
|
|
||||||
:credential-data="credentialData"
|
|
||||||
:credential-properties="credentialProperties"
|
|
||||||
:documentation-url="documentationUrl"
|
|
||||||
:show-validation-warnings="showValidationWarning"
|
|
||||||
@update="onDataChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<OauthButton
|
|
||||||
v-if="
|
|
||||||
isOAuthType && requiredPropertiesFilled && !isOAuthConnected && credentialPermissions.update
|
|
||||||
"
|
|
||||||
:is-google-o-auth-type="isGoogleOAuthType"
|
|
||||||
data-test-id="oauth-connect-button"
|
|
||||||
@click="$emit('oauth')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<n8n-text v-if="isMissingCredentials" color="text-base" size="medium">
|
|
||||||
{{ $locale.baseText('credentialEdit.credentialConfig.missingCredentialType') }}
|
|
||||||
</n8n-text>
|
|
||||||
|
|
||||||
<EnterpriseEdition :features="[EnterpriseEditionFeature.ExternalSecrets]">
|
|
||||||
<template #fallback>
|
|
||||||
<n8n-info-tip class="mt-s">
|
|
||||||
{{ $locale.baseText('credentialEdit.credentialConfig.externalSecrets') }}
|
|
||||||
<n8n-link bold :to="$locale.baseText('settings.externalSecrets.docs')" size="small">
|
|
||||||
{{ $locale.baseText('credentialEdit.credentialConfig.externalSecrets.moreInfo') }}
|
|
||||||
</n8n-link>
|
|
||||||
</n8n-info-tip>
|
|
||||||
</template>
|
</template>
|
||||||
</EnterpriseEdition>
|
<EnterpriseEdition v-else :features="[EnterpriseEditionFeature.Sharing]">
|
||||||
|
<div>
|
||||||
|
<n8n-info-tip :bold="false">
|
||||||
|
{{
|
||||||
|
$locale.baseText('credentialEdit.credentialEdit.info.sharee', {
|
||||||
|
interpolate: { credentialOwnerName },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</n8n-info-tip>
|
||||||
|
</div>
|
||||||
|
</EnterpriseEdition>
|
||||||
|
|
||||||
|
<CredentialInputs
|
||||||
|
v-if="credentialType && credentialPermissions.update"
|
||||||
|
:credential-data="credentialData"
|
||||||
|
:credential-properties="credentialProperties"
|
||||||
|
:documentation-url="documentationUrl"
|
||||||
|
:show-validation-warnings="showValidationWarning"
|
||||||
|
@update="onDataChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OauthButton
|
||||||
|
v-if="
|
||||||
|
isOAuthType &&
|
||||||
|
requiredPropertiesFilled &&
|
||||||
|
!isOAuthConnected &&
|
||||||
|
credentialPermissions.update
|
||||||
|
"
|
||||||
|
:is-google-o-auth-type="isGoogleOAuthType"
|
||||||
|
data-test-id="oauth-connect-button"
|
||||||
|
@click="$emit('oauth')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<n8n-text v-if="isMissingCredentials" color="text-base" size="medium">
|
||||||
|
{{ $locale.baseText('credentialEdit.credentialConfig.missingCredentialType') }}
|
||||||
|
</n8n-text>
|
||||||
|
|
||||||
|
<EnterpriseEdition :features="[EnterpriseEditionFeature.ExternalSecrets]">
|
||||||
|
<template #fallback>
|
||||||
|
<n8n-info-tip class="mt-s">
|
||||||
|
{{ $locale.baseText('credentialEdit.credentialConfig.externalSecrets') }}
|
||||||
|
<n8n-link bold :to="$locale.baseText('settings.externalSecrets.docs')" size="small">
|
||||||
|
{{ $locale.baseText('credentialEdit.credentialConfig.externalSecrets.moreInfo') }}
|
||||||
|
</n8n-link>
|
||||||
|
</n8n-info-tip>
|
||||||
|
</template>
|
||||||
|
</EnterpriseEdition>
|
||||||
|
</div>
|
||||||
|
<CredentialDocs
|
||||||
|
v-if="docs"
|
||||||
|
:credential-type="credentialType"
|
||||||
|
:documentation-url="documentationUrl"
|
||||||
|
:docs="docs"
|
||||||
|
:class="$style.docs"
|
||||||
|
>
|
||||||
|
</CredentialDocs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { computed, onBeforeMount, watch } from 'vue';
|
||||||
import type { PropType } from 'vue';
|
|
||||||
import { mapStores } from 'pinia';
|
|
||||||
|
|
||||||
|
import { getAppNameFromCredType, isCommunityPackageName } from '@/utils/nodeTypesUtils';
|
||||||
import type {
|
import type {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
ICredentialType,
|
ICredentialType,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
INodeTypeDescription,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { getAppNameFromCredType, isCommunityPackageName } from '@/utils/nodeTypesUtils';
|
|
||||||
|
|
||||||
|
import type { IUpdateInformation } from '@/Interface';
|
||||||
|
import AuthTypeSelector from '@/components/CredentialEdit/AuthTypeSelector.vue';
|
||||||
|
import EnterpriseEdition from '@/components/EnterpriseEdition.ee.vue';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { BUILTIN_CREDENTIALS_DOCS_URL, DOCS_DOMAIN, EnterpriseEditionFeature } from '@/constants';
|
||||||
|
import type { PermissionsMap } from '@/permissions';
|
||||||
|
import { addCredentialTranslation } from '@/plugins/i18n';
|
||||||
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { useRootStore } from '@/stores/root.store';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import type { CredentialScope } from '@n8n/permissions';
|
||||||
import Banner from '../Banner.vue';
|
import Banner from '../Banner.vue';
|
||||||
import CopyInput from '../CopyInput.vue';
|
import CopyInput from '../CopyInput.vue';
|
||||||
import CredentialInputs from './CredentialInputs.vue';
|
import CredentialInputs from './CredentialInputs.vue';
|
||||||
import OauthButton from './OauthButton.vue';
|
|
||||||
import { addCredentialTranslation } from '@/plugins/i18n';
|
|
||||||
import { BUILTIN_CREDENTIALS_DOCS_URL, DOCS_DOMAIN, EnterpriseEditionFeature } from '@/constants';
|
|
||||||
import type { PermissionsMap } from '@/permissions';
|
|
||||||
import type { CredentialScope } from '@n8n/permissions';
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
import { useRootStore } from '@/stores/root.store';
|
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
||||||
import type { ICredentialsResponse, IUpdateInformation } from '@/Interface';
|
|
||||||
import AuthTypeSelector from '@/components/CredentialEdit/AuthTypeSelector.vue';
|
|
||||||
import GoogleAuthButton from './GoogleAuthButton.vue';
|
import GoogleAuthButton from './GoogleAuthButton.vue';
|
||||||
import EnterpriseEdition from '@/components/EnterpriseEdition.ee.vue';
|
import OauthButton from './OauthButton.vue';
|
||||||
|
import CredentialDocs from './CredentialDocs.vue';
|
||||||
|
import { CREDENTIAL_MARKDOWN_DOCS } from './docs';
|
||||||
|
|
||||||
export default defineComponent({
|
type Props = {
|
||||||
name: 'CredentialConfig',
|
mode: string;
|
||||||
components: {
|
credentialType: ICredentialType;
|
||||||
EnterpriseEdition,
|
credentialProperties: INodeProperties[];
|
||||||
AuthTypeSelector,
|
credentialData: ICredentialDataDecryptedObject;
|
||||||
Banner,
|
credentialId?: string;
|
||||||
CopyInput,
|
credentialPermissions?: PermissionsMap<CredentialScope>;
|
||||||
CredentialInputs,
|
parentTypes?: string[];
|
||||||
OauthButton,
|
showValidationWarning?: boolean;
|
||||||
GoogleAuthButton,
|
authError?: string;
|
||||||
},
|
testedSuccessfully?: boolean;
|
||||||
props: {
|
isOAuthType?: boolean;
|
||||||
credentialType: {
|
allOAuth2BasePropertiesOverridden?: boolean;
|
||||||
type: Object as PropType<ICredentialType>,
|
isOAuthConnected?: boolean;
|
||||||
required: true,
|
isRetesting?: boolean;
|
||||||
},
|
requiredPropertiesFilled?: boolean;
|
||||||
credentialProperties: {
|
showAuthTypeSelector?: boolean;
|
||||||
type: Array as PropType<INodeProperties[]>,
|
};
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
parentTypes: {
|
|
||||||
type: Array as PropType<string[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
credentialData: {
|
|
||||||
type: Object as PropType<ICredentialDataDecryptedObject>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
credentialId: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
showValidationWarning: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
authError: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
testedSuccessfully: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
isOAuthType: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
allOAuth2BasePropertiesOverridden: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
isOAuthConnected: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
isRetesting: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
credentialPermissions: {
|
|
||||||
type: Object as PropType<PermissionsMap<CredentialScope>>,
|
|
||||||
default: () => ({}) as PermissionsMap<CredentialScope>,
|
|
||||||
},
|
|
||||||
requiredPropertiesFilled: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
showAuthTypeSelector: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
EnterpriseEditionFeature,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async beforeMount() {
|
|
||||||
if (this.rootStore.defaultLocale === 'en') return;
|
|
||||||
|
|
||||||
this.uiStore.activeCredentialType = this.credentialType.name;
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
parentTypes: () => [],
|
||||||
|
credentialId: '',
|
||||||
|
authError: '',
|
||||||
|
showValidationWarning: false,
|
||||||
|
credentialPermissions: () => ({}) as PermissionsMap<CredentialScope>,
|
||||||
|
});
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update', value: IUpdateInformation): void;
|
||||||
|
(event: 'authTypeChanged', value: string): void;
|
||||||
|
(event: 'scrollToTop'): void;
|
||||||
|
(event: 'retest'): void;
|
||||||
|
(event: 'oauth'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
const key = `n8n-nodes-base.credentials.${this.credentialType.name}`;
|
const credentialsStore = useCredentialsStore();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
if (this.$locale.exists(key)) return;
|
const i18n = useI18n();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const credTranslation = await this.credentialsStore.getCredentialTranslation(
|
onBeforeMount(async () => {
|
||||||
this.credentialType.name,
|
if (rootStore.defaultLocale === 'en') return;
|
||||||
);
|
|
||||||
|
|
||||||
addCredentialTranslation(
|
uiStore.activeCredentialType = props.credentialType.name;
|
||||||
{ [this.credentialType.name]: credTranslation },
|
|
||||||
this.rootStore.defaultLocale,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(
|
|
||||||
useCredentialsStore,
|
|
||||||
useNDVStore,
|
|
||||||
useNodeTypesStore,
|
|
||||||
useRootStore,
|
|
||||||
useUIStore,
|
|
||||||
useWorkflowsStore,
|
|
||||||
),
|
|
||||||
activeNodeType(): INodeTypeDescription | null {
|
|
||||||
const activeNode = this.ndvStore.activeNode;
|
|
||||||
|
|
||||||
if (activeNode) {
|
const key = `n8n-nodes-base.credentials.${props.credentialType.name}`;
|
||||||
return this.nodeTypesStore.getNodeType(activeNode.type, activeNode.typeVersion);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
appName(): string {
|
|
||||||
if (!this.credentialType) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const appName = getAppNameFromCredType(this.credentialType.displayName);
|
if (i18n.exists(key)) return;
|
||||||
|
|
||||||
return (
|
const credTranslation = await credentialsStore.getCredentialTranslation(
|
||||||
appName ||
|
props.credentialType.name,
|
||||||
this.$locale.baseText('credentialEdit.credentialConfig.theServiceYouReConnectingTo')
|
);
|
||||||
);
|
|
||||||
},
|
|
||||||
credentialTypeName(): string {
|
|
||||||
return this.credentialType?.name;
|
|
||||||
},
|
|
||||||
credentialOwnerName(): string {
|
|
||||||
return this.credentialsStore.getCredentialOwnerNameById(`${this.credentialId}`);
|
|
||||||
},
|
|
||||||
documentationUrl(): string {
|
|
||||||
const type = this.credentialType;
|
|
||||||
const activeNode = this.ndvStore.activeNode;
|
|
||||||
const isCommunityNode = activeNode ? isCommunityPackageName(activeNode.type) : false;
|
|
||||||
|
|
||||||
const documentationUrl = type?.documentationUrl;
|
addCredentialTranslation(
|
||||||
|
{ [props.credentialType.name]: credTranslation },
|
||||||
|
rootStore.defaultLocale,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
if (!documentationUrl) {
|
const appName = computed(() => {
|
||||||
return '';
|
if (!props.credentialType) {
|
||||||
}
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
let url: URL;
|
return (
|
||||||
if (documentationUrl.startsWith('https://') || documentationUrl.startsWith('http://')) {
|
getAppNameFromCredType(props.credentialType.displayName) ||
|
||||||
url = new URL(documentationUrl);
|
i18n.baseText('credentialEdit.credentialConfig.theServiceYouReConnectingTo')
|
||||||
if (url.hostname !== DOCS_DOMAIN) return documentationUrl;
|
);
|
||||||
} else {
|
});
|
||||||
// Don't show documentation link for community nodes if the URL is not an absolute path
|
const credentialTypeName = computed(() => props.credentialType?.name);
|
||||||
if (isCommunityNode) return '';
|
const credentialOwnerName = computed(() =>
|
||||||
else url = new URL(`${BUILTIN_CREDENTIALS_DOCS_URL}${documentationUrl}/`);
|
credentialsStore.getCredentialOwnerNameById(`${props.credentialId}`),
|
||||||
}
|
);
|
||||||
|
const documentationUrl = computed(() => {
|
||||||
|
const type = props.credentialType;
|
||||||
|
const activeNode = ndvStore.activeNode;
|
||||||
|
const isCommunityNode = activeNode ? isCommunityPackageName(activeNode.type) : false;
|
||||||
|
|
||||||
if (url.hostname === DOCS_DOMAIN) {
|
const docUrl = type?.documentationUrl;
|
||||||
url.searchParams.set('utm_source', 'n8n_app');
|
|
||||||
url.searchParams.set('utm_medium', 'credential_settings');
|
|
||||||
url.searchParams.set('utm_campaign', 'create_new_credentials_modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.href;
|
if (!docUrl) {
|
||||||
},
|
return '';
|
||||||
isGoogleOAuthType(): boolean {
|
}
|
||||||
return (
|
|
||||||
this.credentialTypeName === 'googleOAuth2Api' ||
|
let url: URL;
|
||||||
this.parentTypes.includes('googleOAuth2Api')
|
if (docUrl.startsWith('https://') || docUrl.startsWith('http://')) {
|
||||||
);
|
url = new URL(docUrl);
|
||||||
},
|
if (url.hostname !== DOCS_DOMAIN) return docUrl;
|
||||||
oAuthCallbackUrl(): string {
|
} else {
|
||||||
const oauthType =
|
// Don't show documentation link for community nodes if the URL is not an absolute path
|
||||||
this.credentialTypeName === 'oAuth2Api' || this.parentTypes.includes('oAuth2Api')
|
if (isCommunityNode) return '';
|
||||||
? 'oauth2'
|
else url = new URL(`${BUILTIN_CREDENTIALS_DOCS_URL}${docUrl}/`);
|
||||||
: 'oauth1';
|
}
|
||||||
return this.rootStore.OAuthCallbackUrls[oauthType as keyof {}];
|
|
||||||
},
|
if (url.hostname === DOCS_DOMAIN) {
|
||||||
showOAuthSuccessBanner(): boolean {
|
url.searchParams.set('utm_source', 'n8n_app');
|
||||||
return (
|
url.searchParams.set('utm_medium', 'credential_settings');
|
||||||
this.isOAuthType &&
|
url.searchParams.set('utm_campaign', 'create_new_credentials_modal');
|
||||||
this.requiredPropertiesFilled &&
|
}
|
||||||
this.isOAuthConnected &&
|
|
||||||
!this.authError
|
return url.href;
|
||||||
);
|
});
|
||||||
},
|
|
||||||
isMissingCredentials(): boolean {
|
const isGoogleOAuthType = computed(
|
||||||
return this.credentialType === null;
|
() =>
|
||||||
},
|
credentialTypeName.value === 'googleOAuth2Api' || props.parentTypes.includes('googleOAuth2Api'),
|
||||||
isNewCredential(): boolean {
|
);
|
||||||
return this.mode === 'new' && !this.credentialId;
|
|
||||||
},
|
const oAuthCallbackUrl = computed(() => {
|
||||||
},
|
const oauthType =
|
||||||
methods: {
|
credentialTypeName.value === 'oAuth2Api' || props.parentTypes.includes('oAuth2Api')
|
||||||
getCredentialOptions(type: string): ICredentialsResponse[] {
|
? 'oauth2'
|
||||||
return this.credentialsStore.allUsableCredentialsByType[type];
|
: 'oauth1';
|
||||||
},
|
return rootStore.OAuthCallbackUrls[oauthType as keyof {}];
|
||||||
onDataChange(event: IUpdateInformation): void {
|
});
|
||||||
this.$emit('update', event);
|
|
||||||
},
|
const showOAuthSuccessBanner = computed(() => {
|
||||||
onDocumentationUrlClick(): void {
|
return (
|
||||||
this.$telemetry.track('User clicked credential modal docs link', {
|
props.isOAuthType &&
|
||||||
docs_link: this.documentationUrl,
|
props.requiredPropertiesFilled &&
|
||||||
credential_type: this.credentialTypeName,
|
props.isOAuthConnected &&
|
||||||
source: 'modal',
|
!props.authError
|
||||||
workflow_id: this.workflowsStore.workflowId,
|
);
|
||||||
});
|
});
|
||||||
},
|
|
||||||
onAuthTypeChange(newType: string): void {
|
const isMissingCredentials = computed(() => props.credentialType === null);
|
||||||
this.$emit('authTypeChanged', newType);
|
|
||||||
},
|
const isNewCredential = computed(() => props.mode === 'new' && !props.credentialId);
|
||||||
},
|
|
||||||
watch: {
|
const docs = computed(() => CREDENTIAL_MARKDOWN_DOCS[props.credentialType.name]);
|
||||||
showOAuthSuccessBanner(newValue, oldValue) {
|
|
||||||
if (newValue && !oldValue) {
|
function onDataChange(event: IUpdateInformation): void {
|
||||||
this.$emit('scrollToTop');
|
emit('update', event);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
function onDocumentationUrlClick(): void {
|
||||||
|
telemetry.track('User clicked credential modal docs link', {
|
||||||
|
docs_link: documentationUrl.value,
|
||||||
|
credential_type: credentialTypeName.value,
|
||||||
|
source: 'modal',
|
||||||
|
workflow_id: workflowsStore.workflowId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAuthTypeChange(newType: string): void {
|
||||||
|
emit('authTypeChanged', newType);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(showOAuthSuccessBanner, (newValue, oldValue) => {
|
||||||
|
if (newValue && !oldValue) {
|
||||||
|
emit('scrollToTop');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.container {
|
.config {
|
||||||
--notice-margin: 0;
|
--notice-margin: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
margin-bottom: var(--spacing-l);
|
margin-bottom: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:has(+ .docs) {
|
||||||
|
padding-right: 320px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
top: 0;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
.googleReconnectLabel {
|
.googleReconnectLabel {
|
||||||
margin-right: var(--spacing-3xs);
|
margin-right: var(--spacing-3xs);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
<template>
|
||||||
|
<div :class="$style.docs">
|
||||||
|
<div :class="$style.header">
|
||||||
|
<p :class="$style.title">{{ i18n.baseText('credentialEdit.credentialEdit.setupGuide') }}</p>
|
||||||
|
<n8n-link
|
||||||
|
:class="$style.docsLink"
|
||||||
|
theme="text"
|
||||||
|
new-window
|
||||||
|
:to="documentationUrl"
|
||||||
|
@click="onDocumentationUrlClick"
|
||||||
|
>
|
||||||
|
{{ i18n.baseText('credentialEdit.credentialEdit.docs') }}
|
||||||
|
<n8n-icon icon="external-link-alt" size="small" :class="$style.externalIcon" />
|
||||||
|
</n8n-link>
|
||||||
|
</div>
|
||||||
|
<VueMarkdown :source="docs" :options="{ html: true }" :class="$style.markdown" />
|
||||||
|
<Feedback
|
||||||
|
:class="$style.feedback"
|
||||||
|
:model-value="submittedFeedback"
|
||||||
|
@update:model-value="onFeedback"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Feedback from '@/components/Feedback.vue';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import type { ICredentialType } from 'n8n-workflow';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import VueMarkdown from 'vue-markdown-render';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
credentialType: ICredentialType;
|
||||||
|
docs: string;
|
||||||
|
documentationUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
|
const submittedFeedback = ref<'positive' | 'negative'>();
|
||||||
|
|
||||||
|
function onFeedback(feedback: 'positive' | 'negative') {
|
||||||
|
submittedFeedback.value = feedback;
|
||||||
|
telemetry.track('User gave feedback on credential docs', {
|
||||||
|
feedback,
|
||||||
|
docs_link: props.documentationUrl,
|
||||||
|
credential_type: props.credentialType.name,
|
||||||
|
workflow_id: workflowsStore.workflowId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDocumentationUrlClick(): void {
|
||||||
|
telemetry.track('User clicked credential modal docs link', {
|
||||||
|
docs_link: props.documentationUrl,
|
||||||
|
credential_type: props.credentialType.name,
|
||||||
|
source: 'modal-docs-sidebar',
|
||||||
|
workflow_id: workflowsStore.workflowId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.docs {
|
||||||
|
background-color: var(--color-background-light);
|
||||||
|
border-left: var(--border-base);
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: var(--border-base);
|
||||||
|
padding-bottom: var(--spacing-s);
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docsLink {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
|
||||||
|
&:hover .externalIcon {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.externalIcon {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
padding-left: var(--spacing-4xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
border-top: var(--border-base);
|
||||||
|
padding-top: var(--spacing-s);
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
color: var(--color-text-base);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
line-height: var(--font-line-height-xloose);
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
color: var(--color-text-base);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin: var(--spacing-2xs) 0;
|
||||||
|
margin-left: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
ol ol {
|
||||||
|
list-style-type: lower-alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
li > ul,
|
||||||
|
li > ol {
|
||||||
|
margin: var(--spacing-4xs) 0;
|
||||||
|
margin-left: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
li + li {
|
||||||
|
margin-top: var(--spacing-4xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-text-base);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: var(--font-line-height-xloose);
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-4xs) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
File diff suppressed because it is too large
Load diff
7
packages/editor-ui/src/components/CredentialEdit/docs.ts
Normal file
7
packages/editor-ui/src/components/CredentialEdit/docs.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { i18n } from '../../plugins/i18n';
|
||||||
|
|
||||||
|
export const CREDENTIAL_MARKDOWN_DOCS: Record<string, string> = {
|
||||||
|
aws: i18n.baseText('credentialEdit.docs.aws'),
|
||||||
|
gmailOAuth2: i18n.baseText('credentialEdit.docs.gmailOAuth2'),
|
||||||
|
openAiApi: i18n.baseText('credentialEdit.docs.openAiApi'),
|
||||||
|
};
|
|
@ -52,11 +52,13 @@ function onFeedback(feedback: 'positive' | 'negative') {
|
||||||
.feedback {
|
.feedback {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--spacing-4xs);
|
||||||
|
|
||||||
.feedback-button {
|
.feedback-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: var(--spacing-2xl);
|
width: var(--spacing-l);
|
||||||
height: var(--spacing-2xl);
|
height: var(--spacing-l);
|
||||||
|
color: var(--color-text-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -543,6 +543,8 @@
|
||||||
"credentialEdit.credentialInfo.created": "Created",
|
"credentialEdit.credentialInfo.created": "Created",
|
||||||
"credentialEdit.credentialInfo.id": "ID",
|
"credentialEdit.credentialInfo.id": "ID",
|
||||||
"credentialEdit.credentialInfo.lastModified": "Last modified",
|
"credentialEdit.credentialInfo.lastModified": "Last modified",
|
||||||
|
"credentialEdit.credentialEdit.setupGuide": "Setup guide",
|
||||||
|
"credentialEdit.credentialEdit.docs": "Docs",
|
||||||
"credentialEdit.oAuthButton.connectMyAccount": "Connect my account",
|
"credentialEdit.oAuthButton.connectMyAccount": "Connect my account",
|
||||||
"credentialEdit.oAuthButton.signInWithGoogle": "Sign in with Google",
|
"credentialEdit.oAuthButton.signInWithGoogle": "Sign in with Google",
|
||||||
"credentialEdit.credentialSharing.info.owner": "Sharing a credential allows people to use it in their workflows. They cannot access credential details.",
|
"credentialEdit.credentialSharing.info.owner": "Sharing a credential allows people to use it in their workflows. They cannot access credential details.",
|
||||||
|
@ -554,6 +556,9 @@
|
||||||
"credentialEdit.credentialSharing.list.delete.confirm.confirmButtonText": "Remove",
|
"credentialEdit.credentialSharing.list.delete.confirm.confirmButtonText": "Remove",
|
||||||
"credentialEdit.credentialSharing.list.delete.confirm.cancelButtonText": "Cancel",
|
"credentialEdit.credentialSharing.list.delete.confirm.cancelButtonText": "Cancel",
|
||||||
"credentialEdit.credentialSharing.role.user": "User",
|
"credentialEdit.credentialSharing.role.user": "User",
|
||||||
|
"credentialEdit.docs.aws": "Configure this credential:\n\n- Select your AWS **Region**.\n- Log in to your <a href=\"https://aws.amazon.com/\" target=\"_blank\">AWS</a> account.\n- Generate your access key pair:\n - Open the AWS <a href=\"https://console.aws.amazon.com/iam\" target=\"_blank\">IAM console</a> and open your user menu.\n - Select **Security credentials**.\n - Create a new access key pair in the **Access Keys** section.\n - Reveal the **Access Key ID** and **Secret Access Key** and enter them in n8n.\n- To use a **temporary security credential**, turn this option on and add a **Session token**.\n- If you use Amazon Virtual Private Cloud <a href=\"https://aws.amazon.com/vpc/\" target=\"_blank\">VPC</a> to host n8n, you can establish a connection between your VPC and some apps. Use **Custom Endpoints** to enter relevant custom endpoint(s) for this connection.\n\nClick the docs link above for more detailed instructions.",
|
||||||
|
"credentialEdit.docs.gmailOAuth2": "Configure this credential:\n\n- Log in to your <a href=\"https://cloud.google.com/\" target=\"_blank\">Google Cloud</a> account.\n- Go to <a href=\"https://console.cloud.google.com/apis/credentials\" target=\"_blank\">Google Cloud Console / APIs and services</a> and choose the project you want to use from the select at the top left (or create a new one and select it).\n- If you haven't used OAuth in this Google Cloud project before, <a href=\"https://developers.google.com/workspace/guides/configure-oauth-consent\" target=\"_blank\">configure the OAuth consent screen</a>.\n- In Credentials, select **+ CREATE CREDENTIALS > OAuth client ID**.\n- In the **Application type** dropdown, select **Web application**.\n- Under **Authorized redirect URIs**, select **+ ADD URI**. Paste in the OAuth redirect URL from n8n.\n- Select **Create**.\n- In Enabled APIs and services, select **+ ENABLE APIS AND SERVICES**.\n- Select and enable the Gmail API.\n- Back to Credentials, click on the credential in OAuth 2.0 Client IDs, and on the credential page, you will find the Client ID and Client Secret.\n\nClick the docs link above for more detailed instructions.",
|
||||||
|
"credentialEdit.docs.openAiApi": "Configure this credential:\n\n- Log in to your <a href=\"https://openai.com/\" target=\"_blank\">OpenAI</a> account.\n- Open your OpenAI <a href=\"https://platform.openai.com/api-keys\" target=\"_blank\">API keys</a> page to create an **API key**.\n- Enter an **Organization ID** if you belong to multiple organizations; otherwise, leave blank. Open your OpenAI <a href=\"https://platform.openai.com/account/organization\" target=\"_blank\">Organization Settings</a> page to get your Organization ID.\n\nClick the docs link above for more detailed instructions.",
|
||||||
"credentialSelectModal.addNewCredential": "Add new credential",
|
"credentialSelectModal.addNewCredential": "Add new credential",
|
||||||
"credentialSelectModal.continue": "Continue",
|
"credentialSelectModal.continue": "Continue",
|
||||||
"credentialSelectModal.searchForApp": "Search for app...",
|
"credentialSelectModal.searchForApp": "Search for app...",
|
||||||
|
|
Loading…
Reference in a new issue