feat(editor): Add docs sidebar to credential modal (#9914)

Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
This commit is contained in:
Elias Meire 2024-07-03 14:18:47 +02:00 committed by GitHub
parent ab2a548856
commit b2f8ea7918
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1456 additions and 1344 deletions

View file

@ -1,396 +1,373 @@
<template>
<div :class="$style.container" data-test-id="node-credentials-config-container">
<Banner
v-show="showValidationWarning"
theme="danger"
:message="
$locale.baseText(
`credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow${
credentialPermissions.update ? '' : '.sharee'
}`,
{ interpolate: { owner: credentialOwnerName } },
)
"
/>
<div>
<div :class="$style.config" data-test-id="node-credentials-config-container">
<Banner
v-show="showValidationWarning"
theme="danger"
:message="
$locale.baseText(
`credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow${
credentialPermissions.update ? '' : '.sharee'
}`,
{ interpolate: { owner: credentialOwnerName } },
)
"
/>
<Banner
v-if="authError && !showValidationWarning"
theme="danger"
:message="
$locale.baseText(
`credentialEdit.credentialConfig.couldntConnectWithTheseSettings${
credentialPermissions.update ? '' : '.sharee'
}`,
{ interpolate: { owner: credentialOwnerName } },
)
"
:details="authError"
:button-label="$locale.baseText('credentialEdit.credentialConfig.retry')"
button-loading-label="Retrying"
:button-title="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
:button-loading="isRetesting"
@click="$emit('retest')"
/>
<Banner
v-if="authError && !showValidationWarning"
theme="danger"
:message="
$locale.baseText(
`credentialEdit.credentialConfig.couldntConnectWithTheseSettings${
credentialPermissions.update ? '' : '.sharee'
}`,
{ interpolate: { owner: credentialOwnerName } },
)
"
:details="authError"
:button-label="$locale.baseText('credentialEdit.credentialConfig.retry')"
button-loading-label="Retrying"
:button-title="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
:button-loading="isRetesting"
@click="$emit('retest')"
/>
<Banner
v-show="showOAuthSuccessBanner && !showValidationWarning"
theme="success"
:message="$locale.baseText('credentialEdit.credentialConfig.accountConnected')"
:button-label="$locale.baseText('credentialEdit.credentialConfig.reconnect')"
:button-title="$locale.baseText('credentialEdit.credentialConfig.reconnectOAuth2Credential')"
data-test-id="oauth-connect-success-banner"
@click="$emit('oauth')"
>
<template v-if="isGoogleOAuthType" #button>
<p
:class="$style.googleReconnectLabel"
v-text="`${$locale.baseText('credentialEdit.credentialConfig.reconnect')}:`"
<Banner
v-show="showOAuthSuccessBanner && !showValidationWarning"
theme="success"
:message="$locale.baseText('credentialEdit.credentialConfig.accountConnected')"
:button-label="$locale.baseText('credentialEdit.credentialConfig.reconnect')"
:button-title="
$locale.baseText('credentialEdit.credentialConfig.reconnectOAuth2Credential')
"
data-test-id="oauth-connect-success-banner"
@click="$emit('oauth')"
>
<template v-if="isGoogleOAuthType" #button>
<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
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" 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 },
<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 },
})
}}
</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>
"
:toast-title="
$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')
"
:redact-value="true"
/>
</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>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';
<script setup lang="ts">
import { computed, onBeforeMount, watch } from 'vue';
import { getAppNameFromCredType, isCommunityPackageName } from '@/utils/nodeTypesUtils';
import type {
ICredentialDataDecryptedObject,
ICredentialType,
INodeProperties,
INodeTypeDescription,
} 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 CopyInput from '../CopyInput.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 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({
name: 'CredentialConfig',
components: {
EnterpriseEdition,
AuthTypeSelector,
Banner,
CopyInput,
CredentialInputs,
OauthButton,
GoogleAuthButton,
},
props: {
credentialType: {
type: Object as PropType<ICredentialType>,
required: true,
},
credentialProperties: {
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;
type Props = {
mode: string;
credentialType: ICredentialType;
credentialProperties: INodeProperties[];
credentialData: ICredentialDataDecryptedObject;
credentialId?: string;
credentialPermissions?: PermissionsMap<CredentialScope>;
parentTypes?: string[];
showValidationWarning?: boolean;
authError?: string;
testedSuccessfully?: boolean;
isOAuthType?: boolean;
allOAuth2BasePropertiesOverridden?: boolean;
isOAuthConnected?: boolean;
isRetesting?: boolean;
requiredPropertiesFilled?: boolean;
showAuthTypeSelector?: boolean;
};
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(
this.credentialType.name,
);
onBeforeMount(async () => {
if (rootStore.defaultLocale === 'en') return;
addCredentialTranslation(
{ [this.credentialType.name]: credTranslation },
this.rootStore.defaultLocale,
);
},
computed: {
...mapStores(
useCredentialsStore,
useNDVStore,
useNodeTypesStore,
useRootStore,
useUIStore,
useWorkflowsStore,
),
activeNodeType(): INodeTypeDescription | null {
const activeNode = this.ndvStore.activeNode;
uiStore.activeCredentialType = props.credentialType.name;
if (activeNode) {
return this.nodeTypesStore.getNodeType(activeNode.type, activeNode.typeVersion);
}
return null;
},
appName(): string {
if (!this.credentialType) {
return '';
}
const key = `n8n-nodes-base.credentials.${props.credentialType.name}`;
const appName = getAppNameFromCredType(this.credentialType.displayName);
if (i18n.exists(key)) return;
return (
appName ||
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 credTranslation = await credentialsStore.getCredentialTranslation(
props.credentialType.name,
);
const documentationUrl = type?.documentationUrl;
addCredentialTranslation(
{ [props.credentialType.name]: credTranslation },
rootStore.defaultLocale,
);
});
if (!documentationUrl) {
return '';
}
const appName = computed(() => {
if (!props.credentialType) {
return '';
}
let url: URL;
if (documentationUrl.startsWith('https://') || documentationUrl.startsWith('http://')) {
url = new URL(documentationUrl);
if (url.hostname !== DOCS_DOMAIN) return documentationUrl;
} else {
// Don't show documentation link for community nodes if the URL is not an absolute path
if (isCommunityNode) return '';
else url = new URL(`${BUILTIN_CREDENTIALS_DOCS_URL}${documentationUrl}/`);
}
return (
getAppNameFromCredType(props.credentialType.displayName) ||
i18n.baseText('credentialEdit.credentialConfig.theServiceYouReConnectingTo')
);
});
const credentialTypeName = computed(() => props.credentialType?.name);
const credentialOwnerName = computed(() =>
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) {
url.searchParams.set('utm_source', 'n8n_app');
url.searchParams.set('utm_medium', 'credential_settings');
url.searchParams.set('utm_campaign', 'create_new_credentials_modal');
}
const docUrl = type?.documentationUrl;
return url.href;
},
isGoogleOAuthType(): boolean {
return (
this.credentialTypeName === 'googleOAuth2Api' ||
this.parentTypes.includes('googleOAuth2Api')
);
},
oAuthCallbackUrl(): string {
const oauthType =
this.credentialTypeName === 'oAuth2Api' || this.parentTypes.includes('oAuth2Api')
? 'oauth2'
: 'oauth1';
return this.rootStore.OAuthCallbackUrls[oauthType as keyof {}];
},
showOAuthSuccessBanner(): boolean {
return (
this.isOAuthType &&
this.requiredPropertiesFilled &&
this.isOAuthConnected &&
!this.authError
);
},
isMissingCredentials(): boolean {
return this.credentialType === null;
},
isNewCredential(): boolean {
return this.mode === 'new' && !this.credentialId;
},
},
methods: {
getCredentialOptions(type: string): ICredentialsResponse[] {
return this.credentialsStore.allUsableCredentialsByType[type];
},
onDataChange(event: IUpdateInformation): void {
this.$emit('update', event);
},
onDocumentationUrlClick(): void {
this.$telemetry.track('User clicked credential modal docs link', {
docs_link: this.documentationUrl,
credential_type: this.credentialTypeName,
source: 'modal',
workflow_id: this.workflowsStore.workflowId,
});
},
onAuthTypeChange(newType: string): void {
this.$emit('authTypeChanged', newType);
},
},
watch: {
showOAuthSuccessBanner(newValue, oldValue) {
if (newValue && !oldValue) {
this.$emit('scrollToTop');
}
},
},
if (!docUrl) {
return '';
}
let url: URL;
if (docUrl.startsWith('https://') || docUrl.startsWith('http://')) {
url = new URL(docUrl);
if (url.hostname !== DOCS_DOMAIN) return docUrl;
} else {
// Don't show documentation link for community nodes if the URL is not an absolute path
if (isCommunityNode) return '';
else url = new URL(`${BUILTIN_CREDENTIALS_DOCS_URL}${docUrl}/`);
}
if (url.hostname === DOCS_DOMAIN) {
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;
});
const isGoogleOAuthType = computed(
() =>
credentialTypeName.value === 'googleOAuth2Api' || props.parentTypes.includes('googleOAuth2Api'),
);
const oAuthCallbackUrl = computed(() => {
const oauthType =
credentialTypeName.value === 'oAuth2Api' || props.parentTypes.includes('oAuth2Api')
? 'oauth2'
: 'oauth1';
return rootStore.OAuthCallbackUrls[oauthType as keyof {}];
});
const showOAuthSuccessBanner = computed(() => {
return (
props.isOAuthType &&
props.requiredPropertiesFilled &&
props.isOAuthConnected &&
!props.authError
);
});
const isMissingCredentials = computed(() => props.credentialType === null);
const isNewCredential = computed(() => props.mode === 'new' && !props.credentialId);
const docs = computed(() => CREDENTIAL_MARKDOWN_DOCS[props.credentialType.name]);
function onDataChange(event: IUpdateInformation): void {
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>
<style lang="scss" module>
.container {
.config {
--notice-margin: 0;
flex-grow: 1;
> * {
margin-bottom: var(--spacing-l);
}
&:has(+ .docs) {
padding-right: 320px;
}
}
.docs {
position: absolute;
right: 0;
bottom: 0;
top: 0;
max-width: 320px;
}
.googleReconnectLabel {
margin-right: var(--spacing-3xs);
}

View file

@ -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>

View 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'),
};

View file

@ -52,11 +52,13 @@ function onFeedback(feedback: 'positive' | 'negative') {
.feedback {
display: flex;
align-items: center;
gap: var(--spacing-4xs);
.feedback-button {
cursor: pointer;
width: var(--spacing-2xl);
height: var(--spacing-2xl);
width: var(--spacing-l);
height: var(--spacing-l);
color: var(--color-text-light);
display: flex;
justify-content: center;
align-items: center;

View file

@ -543,6 +543,8 @@
"credentialEdit.credentialInfo.created": "Created",
"credentialEdit.credentialInfo.id": "ID",
"credentialEdit.credentialInfo.lastModified": "Last modified",
"credentialEdit.credentialEdit.setupGuide": "Setup guide",
"credentialEdit.credentialEdit.docs": "Docs",
"credentialEdit.oAuthButton.connectMyAccount": "Connect my account",
"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.",
@ -554,6 +556,9 @@
"credentialEdit.credentialSharing.list.delete.confirm.confirmButtonText": "Remove",
"credentialEdit.credentialSharing.list.delete.confirm.cancelButtonText": "Cancel",
"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.continue": "Continue",
"credentialSelectModal.searchForApp": "Search for app...",