fix(core): Add documentation hints for API keys to SettingApiView.vue (no-changelog) (#13617)

This commit is contained in:
Ricardo Espinoza 2025-03-03 10:49:48 +01:00 committed by GitHub
parent 4067fb0b12
commit a7f0c66e30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 178 additions and 68 deletions

View file

@ -73,10 +73,6 @@ describe('ApiKeyCreateOrEditModal', () => {
getByText('Make sure to copy your API key now as you will not be able to see this again.'), getByText('Make sure to copy your API key now as you will not be able to see this again.'),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(getByText('You can find more details in')).toBeInTheDocument();
expect(getByText('the API documentation')).toBeInTheDocument();
expect(getByText('Click to copy')).toBeInTheDocument(); expect(getByText('Click to copy')).toBeInTheDocument();
expect(getByText('new api key')).toBeInTheDocument(); expect(getByText('new api key')).toBeInTheDocument();
@ -138,10 +134,6 @@ describe('ApiKeyCreateOrEditModal', () => {
getByText('Make sure to copy your API key now as you will not be able to see this again.'), getByText('Make sure to copy your API key now as you will not be able to see this again.'),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(getByText('You can find more details in')).toBeInTheDocument();
expect(getByText('the API documentation')).toBeInTheDocument();
expect(getByText('Click to copy')).toBeInTheDocument(); expect(getByText('Click to copy')).toBeInTheDocument();
expect(getByText('new api key')).toBeInTheDocument(); expect(getByText('new api key')).toBeInTheDocument();
@ -187,10 +179,6 @@ describe('ApiKeyCreateOrEditModal', () => {
getByText('Make sure to copy your API key now as you will not be able to see this again.'), getByText('Make sure to copy your API key now as you will not be able to see this again.'),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(getByText('You can find more details in')).toBeInTheDocument();
expect(getByText('the API documentation')).toBeInTheDocument();
expect(getByText('Click to copy')).toBeInTheDocument(); expect(getByText('Click to copy')).toBeInTheDocument();
expect(getByText('new api key')).toBeInTheDocument(); expect(getByText('new api key')).toBeInTheDocument();

View file

@ -1,11 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import Modal from '@/components/Modal.vue'; import Modal from '@/components/Modal.vue';
import { API_KEY_CREATE_OR_EDIT_MODAL_KEY, DOCS_DOMAIN } from '@/constants'; import { API_KEY_CREATE_OR_EDIT_MODAL_KEY } from '@/constants';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { createEventBus } from '@n8n/utils/event-bus'; import { createEventBus } from '@n8n/utils/event-bus';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
import { useDocumentTitle } from '@/composables/useDocumentTitle'; import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useApiKeysStore } from '@/stores/apiKeys.store'; import { useApiKeysStore } from '@/stores/apiKeys.store';
@ -29,17 +28,13 @@ const { showError, showMessage } = useToast();
const uiStore = useUIStore(); const uiStore = useUIStore();
const rootStore = useRootStore(); const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const { isSwaggerUIEnabled, publicApiPath, publicApiLatestVersion } = settingsStore;
const { createApiKey, updateApiKey, apiKeysById } = useApiKeysStore(); const { createApiKey, updateApiKey, apiKeysById } = useApiKeysStore();
const { baseUrl } = useRootStore();
const documentTitle = useDocumentTitle(); const documentTitle = useDocumentTitle();
const label = ref(''); const label = ref('');
const expirationDaysFromNow = ref(EXPIRATION_OPTIONS['30_DAYS']); const expirationDaysFromNow = ref(EXPIRATION_OPTIONS['30_DAYS']);
const modalBus = createEventBus(); const modalBus = createEventBus();
const newApiKey = ref<ApiKeyWithRawValue | null>(null); const newApiKey = ref<ApiKeyWithRawValue | null>(null);
const apiDocsURL = ref('');
const loading = ref(false); const loading = ref(false);
const rawApiKey = ref(''); const rawApiKey = ref('');
const customExpirationDate = ref(''); const customExpirationDate = ref('');
@ -110,10 +105,6 @@ onMounted(() => {
label.value = apiKey.label ?? ''; label.value = apiKey.label ?? '';
apiKeyCreationDate.value = getApiKeyCreationTime(apiKey); apiKeyCreationDate.value = getApiKeyCreationTime(apiKey);
} }
apiDocsURL.value = isSwaggerUIEnabled
? `${baseUrl}${publicApiPath}/v${publicApiLatestVersion}/docs`
: `https://${DOCS_DOMAIN}/api/api-reference/`;
}); });
function onInput(value: string): void { function onInput(value: string): void {
@ -222,26 +213,6 @@ const onSelect = (value: number) => {
> >
<template #content> <template #content>
<div> <div>
<p v-if="newApiKey" class="mb-s">
<n8n-info-tip :bold="false">
<i18n-t keypath="settings.api.view.info" tag="span">
<template #apiAction>
<a
href="https://docs.n8n.io/api"
target="_blank"
v-text="i18n.baseText('settings.api.view.info.api')"
/>
</template>
<template #webhookAction>
<a
href="https://docs.n8n.io/integrations/core-nodes/n8n-nodes-base.webhook/"
target="_blank"
v-text="i18n.baseText('settings.api.view.info.webhook')"
/>
</template>
</i18n-t>
</n8n-info-tip>
</p>
<n8n-card v-if="newApiKey" class="mb-4xs"> <n8n-card v-if="newApiKey" class="mb-4xs">
<CopyInput <CopyInput
:label="newApiKey.label" :label="newApiKey.label"
@ -253,23 +224,6 @@ const onSelect = (value: number) => {
/> />
</n8n-card> </n8n-card>
<div v-if="newApiKey" :class="$style.hint">
<N8nText size="small">
{{
i18n.baseText(
`settings.api.view.${settingsStore.isSwaggerUIEnabled ? 'tryapi' : 'more-details'}`,
)
}}
</N8nText>
{{ ' ' }}
<n8n-link :to="apiDocsURL" :new-window="true" size="small">
{{
i18n.baseText(
`settings.api.view.${isSwaggerUIEnabled ? 'apiPlayground' : 'external-docs'}`,
)
}}
</n8n-link>
</div>
<div v-else :class="$style.form"> <div v-else :class="$style.form">
<N8nInputLabel <N8nInputLabel
:label="i18n.baseText('settings.api.view.modal.form.label')" :label="i18n.baseText('settings.api.view.modal.form.label')"
@ -362,11 +316,6 @@ const onSelect = (value: number) => {
margin: 0; margin: 0;
} }
.hint {
color: var(--color-text-light);
margin-bottom: var(--spacing-s);
}
.form { .form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -9,12 +9,51 @@ import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import { useApiKeysStore } from '@/stores/apiKeys.store'; import { useApiKeysStore } from '@/stores/apiKeys.store';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useRootStore } from '@/stores/root.store';
setActivePinia(createTestingPinia()); setActivePinia(createTestingPinia());
const settingsStore = mockedStore(useSettingsStore); const settingsStore = mockedStore(useSettingsStore);
const cloudStore = mockedStore(useCloudPlanStore); const cloudStore = mockedStore(useCloudPlanStore);
const apiKeysStore = mockedStore(useApiKeysStore); const apiKeysStore = mockedStore(useApiKeysStore);
const rootStore = mockedStore(useRootStore);
const assertHintsAreShown = ({ isSwaggerUIEnabled }: { isSwaggerUIEnabled: boolean }) => {
const apiDocsLink = screen.getByTestId('api-docs-link');
expect(apiDocsLink).toBeInTheDocument();
expect(apiDocsLink).toHaveAttribute('href', 'https://docs.n8n.io/api');
expect(apiDocsLink).toHaveAttribute('target', '_blank');
const webhookDocsLink = screen.getByTestId('webhook-docs-link');
expect(webhookDocsLink).toBeInTheDocument();
expect(webhookDocsLink).toHaveAttribute(
'href',
'https://docs.n8n.io/integrations/core-nodes/n8n-nodes-base.webhook/',
);
expect(webhookDocsLink).toHaveAttribute('target', '_blank');
expect(
screen.getByText('Use your API Key to control n8n programmatically using the', {
exact: false,
}),
).toBeInTheDocument();
expect(
screen.getByText('. But if you only want to trigger workflows, consider using the', {
exact: false,
}),
).toBeInTheDocument();
expect(screen.getByText('instead.', { exact: false })).toBeInTheDocument();
if (isSwaggerUIEnabled) {
expect(screen.getByText('Try it out using the')).toBeInTheDocument();
expect(screen.getByText('API Playground')).toBeInTheDocument();
} else {
expect(screen.getByText('You can find more details in')).toBeInTheDocument();
expect(screen.getByText('the API documentation')).toBeInTheDocument();
}
};
describe('SettingsApiView', () => { describe('SettingsApiView', () => {
beforeEach(() => { beforeEach(() => {
@ -50,11 +89,15 @@ describe('SettingsApiView', () => {
expect(screen.getByText('n8n API')).toBeInTheDocument(); expect(screen.getByText('n8n API')).toBeInTheDocument();
}); });
it('if user public api enabled and there are API Keys in account, they should be rendered', async () => { it('if user public api enabled, swagger enabled, and there are API Keys in account, they should be rendered', async () => {
const dateInTheFuture = DateTime.now().plus({ days: 1 }); const dateInTheFuture = DateTime.now().plus({ days: 1 });
const dateInThePast = DateTime.now().minus({ days: 1 }); const dateInThePast = DateTime.now().minus({ days: 1 });
rootStore.baseUrl = 'http://localhost:5678';
settingsStore.publicApiPath = '/api';
settingsStore.publicApiLatestVersion = 1;
settingsStore.isPublicApiEnabled = true; settingsStore.isPublicApiEnabled = true;
settingsStore.isSwaggerUIEnabled = true;
cloudStore.userIsTrialing = false; cloudStore.userIsTrialing = false;
apiKeysStore.apiKeys = [ apiKeysStore.apiKeys = [
{ {
@ -98,6 +141,64 @@ describe('SettingsApiView', () => {
expect(screen.getByText('This API key has expired')).toBeInTheDocument(); expect(screen.getByText('This API key has expired')).toBeInTheDocument();
expect(screen.getByText('****Wtcr')).toBeInTheDocument(); expect(screen.getByText('****Wtcr')).toBeInTheDocument();
expect(screen.getByText('test-key-3')).toBeInTheDocument(); expect(screen.getByText('test-key-3')).toBeInTheDocument();
assertHintsAreShown({ isSwaggerUIEnabled: true });
});
it('if user public api enabled, swagger disabled and there are API Keys in account, they should be rendered', async () => {
const dateInTheFuture = DateTime.now().plus({ days: 1 });
const dateInThePast = DateTime.now().minus({ days: 1 });
rootStore.baseUrl = 'http://localhost:5678';
settingsStore.publicApiPath = '/api';
settingsStore.publicApiLatestVersion = 1;
settingsStore.isPublicApiEnabled = true;
settingsStore.isSwaggerUIEnabled = false;
cloudStore.userIsTrialing = false;
apiKeysStore.apiKeys = [
{
id: '1',
label: 'test-key-1',
createdAt: new Date().toString(),
updatedAt: new Date().toString(),
apiKey: '****Atcr',
expiresAt: null,
},
{
id: '2',
label: 'test-key-2',
createdAt: new Date().toString(),
updatedAt: new Date().toString(),
apiKey: '****Bdcr',
expiresAt: dateInTheFuture.toSeconds(),
},
{
id: '3',
label: 'test-key-3',
createdAt: new Date().toString(),
updatedAt: new Date().toString(),
apiKey: '****Wtcr',
expiresAt: dateInThePast.toSeconds(),
},
];
renderComponent(SettingsApiView);
expect(screen.getByText('Never expires')).toBeInTheDocument();
expect(screen.getByText('****Atcr')).toBeInTheDocument();
expect(screen.getByText('test-key-1')).toBeInTheDocument();
expect(
screen.getByText(`Expires on ${dateInTheFuture.toFormat('ccc, MMM d yyyy')}`),
).toBeInTheDocument();
expect(screen.getByText('****Bdcr')).toBeInTheDocument();
expect(screen.getByText('test-key-2')).toBeInTheDocument();
expect(screen.getByText('This API key has expired')).toBeInTheDocument();
expect(screen.getByText('****Wtcr')).toBeInTheDocument();
expect(screen.getByText('test-key-3')).toBeInTheDocument();
assertHintsAreShown({ isSwaggerUIEnabled: false });
}); });
it('should show delete warning when trying to delete an API key', async () => { it('should show delete warning when trying to delete an API key', async () => {

View file

@ -6,13 +6,14 @@ import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useCloudPlanStore } from '@/stores/cloudPlan.store'; import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { API_KEY_CREATE_OR_EDIT_MODAL_KEY, MODAL_CONFIRM } from '@/constants'; import { API_KEY_CREATE_OR_EDIT_MODAL_KEY, DOCS_DOMAIN, MODAL_CONFIRM } from '@/constants';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper'; import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useApiKeysStore } from '@/stores/apiKeys.store'; import { useApiKeysStore } from '@/stores/apiKeys.store';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useRootStore } from '@/stores/root.store';
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
@ -29,9 +30,13 @@ const loading = ref(false);
const apiKeysStore = useApiKeysStore(); const apiKeysStore = useApiKeysStore();
const { getAndCacheApiKeys, deleteApiKey } = apiKeysStore; const { getAndCacheApiKeys, deleteApiKey } = apiKeysStore;
const { apiKeysSortByCreationDate } = storeToRefs(apiKeysStore); const { apiKeysSortByCreationDate } = storeToRefs(apiKeysStore);
const { isSwaggerUIEnabled, publicApiPath, publicApiLatestVersion } = settingsStore;
const { baseUrl } = useRootStore();
const { isPublicApiEnabled } = settingsStore; const { isPublicApiEnabled } = settingsStore;
const apiDocsURL = ref('');
const onCreateApiKey = async () => { const onCreateApiKey = async () => {
telemetry.track('User clicked create API key button'); telemetry.track('User clicked create API key button');
@ -44,6 +49,10 @@ const onCreateApiKey = async () => {
onMounted(async () => { onMounted(async () => {
documentTitle.set(i18n.baseText('settings.api')); documentTitle.set(i18n.baseText('settings.api'));
apiDocsURL.value = isSwaggerUIEnabled
? `${baseUrl}${publicApiPath}/v${publicApiLatestVersion}/docs`
: `https://${DOCS_DOMAIN}/api/api-reference/`;
if (!isPublicApiEnabled) return; if (!isPublicApiEnabled) return;
await getApiKeys(); await getApiKeys();
@ -107,6 +116,28 @@ function onEdit(id: string) {
</span> </span>
</n8n-heading> </n8n-heading>
</div> </div>
<p v-if="isPublicApiEnabled && apiKeysSortByCreationDate.length" :class="$style.topHint">
<n8n-text>
<i18n-t keypath="settings.api.view.info" tag="span">
<template #apiAction>
<a
data-test-id="api-docs-link"
href="https://docs.n8n.io/api"
target="_blank"
v-text="i18n.baseText('settings.api.view.info.api')"
/>
</template>
<template #webhookAction>
<a
data-test-id="webhook-docs-link"
href="https://docs.n8n.io/integrations/core-nodes/n8n-nodes-base.webhook/"
target="_blank"
v-text="i18n.baseText('settings.api.view.info.webhook')"
/>
</template>
</i18n-t>
</n8n-text>
</p>
<template v-if="apiKeysSortByCreationDate.length"> <template v-if="apiKeysSortByCreationDate.length">
<el-row <el-row
v-for="apiKey in apiKeysSortByCreationDate" v-for="apiKey in apiKeysSortByCreationDate"
@ -119,6 +150,34 @@ function onEdit(id: string) {
</el-col> </el-col>
</el-row> </el-row>
<div v-if="isPublicApiEnabled && apiKeysSortByCreationDate.length" :class="$style.BottomHint">
<N8nText size="small" color="text-light">
{{
i18n.baseText(
`settings.api.view.${settingsStore.isSwaggerUIEnabled ? 'tryapi' : 'more-details'}`,
)
}}
</N8nText>
{{ ' ' }}
<n8n-link
v-if="isSwaggerUIEnabled"
data-test-id="api-playground-link"
:to="apiDocsURL"
:new-window="true"
size="small"
>
{{ i18n.baseText('settings.api.view.apiPlayground') }}
</n8n-link>
<n8n-link
v-else
data-test-id="api-endpoint-docs-link"
:to="apiDocsURL"
:new-window="true"
size="small"
>
{{ i18n.baseText(`settings.api.view.external-docs`) }}
</n8n-link>
</div>
<div class="mt-m text-right"> <div class="mt-m text-right">
<n8n-button <n8n-button
size="large" size="large"
@ -138,6 +197,7 @@ function onEdit(id: string) {
:button-text="i18n.baseText('settings.api.trial.upgradePlan.cta')" :button-text="i18n.baseText('settings.api.trial.upgradePlan.cta')"
@click:button="onUpgrade" @click:button="onUpgrade"
/> />
<n8n-action-box <n8n-action-box
v-if="isPublicApiEnabled && !apiKeysSortByCreationDate.length" v-if="isPublicApiEnabled && !apiKeysSortByCreationDate.length"
:button-text=" :button-text="
@ -154,7 +214,7 @@ function onEdit(id: string) {
display: flex; display: flex;
align-items: center; align-items: center;
white-space: nowrap; white-space: nowrap;
margin-bottom: var(--spacing-2xl); margin-bottom: var(--spacing-xl);
*:first-child { *:first-child {
flex-grow: 1; flex-grow: 1;
@ -176,7 +236,19 @@ function onEdit(id: string) {
right: var(--spacing-s); right: var(--spacing-s);
} }
.hint { .topHint {
margin-top: none;
margin-bottom: var(--spacing-s);
color: var(--color-text-light); color: var(--color-text-light);
span {
font-size: var(--font-size-s);
line-height: var(--font-line-height-loose);
font-weight: var(--font-weight-regular);
}
}
.BottomHint {
margin-bottom: var(--spacing-s);
} }
</style> </style>