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.'),
).toBeInTheDocument();
expect(getByText('You can find more details in')).toBeInTheDocument();
expect(getByText('the API documentation')).toBeInTheDocument();
expect(getByText('Click to copy')).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.'),
).toBeInTheDocument();
expect(getByText('You can find more details in')).toBeInTheDocument();
expect(getByText('the API documentation')).toBeInTheDocument();
expect(getByText('Click to copy')).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.'),
).toBeInTheDocument();
expect(getByText('You can find more details in')).toBeInTheDocument();
expect(getByText('the API documentation')).toBeInTheDocument();
expect(getByText('Click to copy')).toBeInTheDocument();
expect(getByText('new api key')).toBeInTheDocument();

View file

@ -1,11 +1,10 @@
<script lang="ts" setup>
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 { useUIStore } from '@/stores/ui.store';
import { createEventBus } from '@n8n/utils/event-bus';
import { useI18n } from '@/composables/useI18n';
import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/root.store';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useApiKeysStore } from '@/stores/apiKeys.store';
@ -29,17 +28,13 @@ const { showError, showMessage } = useToast();
const uiStore = useUIStore();
const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const { isSwaggerUIEnabled, publicApiPath, publicApiLatestVersion } = settingsStore;
const { createApiKey, updateApiKey, apiKeysById } = useApiKeysStore();
const { baseUrl } = useRootStore();
const documentTitle = useDocumentTitle();
const label = ref('');
const expirationDaysFromNow = ref(EXPIRATION_OPTIONS['30_DAYS']);
const modalBus = createEventBus();
const newApiKey = ref<ApiKeyWithRawValue | null>(null);
const apiDocsURL = ref('');
const loading = ref(false);
const rawApiKey = ref('');
const customExpirationDate = ref('');
@ -110,10 +105,6 @@ onMounted(() => {
label.value = apiKey.label ?? '';
apiKeyCreationDate.value = getApiKeyCreationTime(apiKey);
}
apiDocsURL.value = isSwaggerUIEnabled
? `${baseUrl}${publicApiPath}/v${publicApiLatestVersion}/docs`
: `https://${DOCS_DOMAIN}/api/api-reference/`;
});
function onInput(value: string): void {
@ -222,26 +213,6 @@ const onSelect = (value: number) => {
>
<template #content>
<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">
<CopyInput
:label="newApiKey.label"
@ -253,23 +224,6 @@ const onSelect = (value: number) => {
/>
</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">
<N8nInputLabel
:label="i18n.baseText('settings.api.view.modal.form.label')"
@ -362,11 +316,6 @@ const onSelect = (value: number) => {
margin: 0;
}
.hint {
color: var(--color-text-light);
margin-bottom: var(--spacing-s);
}
.form {
display: flex;
flex-direction: column;

View file

@ -9,12 +9,51 @@ import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { useApiKeysStore } from '@/stores/apiKeys.store';
import { DateTime } from 'luxon';
import { useRootStore } from '@/stores/root.store';
setActivePinia(createTestingPinia());
const settingsStore = mockedStore(useSettingsStore);
const cloudStore = mockedStore(useCloudPlanStore);
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', () => {
beforeEach(() => {
@ -50,11 +89,15 @@ describe('SettingsApiView', () => {
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 dateInThePast = DateTime.now().minus({ days: 1 });
rootStore.baseUrl = 'http://localhost:5678';
settingsStore.publicApiPath = '/api';
settingsStore.publicApiLatestVersion = 1;
settingsStore.isPublicApiEnabled = true;
settingsStore.isSwaggerUIEnabled = true;
cloudStore.userIsTrialing = false;
apiKeysStore.apiKeys = [
{
@ -98,6 +141,64 @@ describe('SettingsApiView', () => {
expect(screen.getByText('This API key has expired')).toBeInTheDocument();
expect(screen.getByText('****Wtcr')).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 () => {

View file

@ -6,13 +6,14 @@ import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useSettingsStore } from '@/stores/settings.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 { useTelemetry } from '@/composables/useTelemetry';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
import { useUIStore } from '@/stores/ui.store';
import { useApiKeysStore } from '@/stores/apiKeys.store';
import { storeToRefs } from 'pinia';
import { useRootStore } from '@/stores/root.store';
const settingsStore = useSettingsStore();
const uiStore = useUIStore();
@ -29,9 +30,13 @@ const loading = ref(false);
const apiKeysStore = useApiKeysStore();
const { getAndCacheApiKeys, deleteApiKey } = apiKeysStore;
const { apiKeysSortByCreationDate } = storeToRefs(apiKeysStore);
const { isSwaggerUIEnabled, publicApiPath, publicApiLatestVersion } = settingsStore;
const { baseUrl } = useRootStore();
const { isPublicApiEnabled } = settingsStore;
const apiDocsURL = ref('');
const onCreateApiKey = async () => {
telemetry.track('User clicked create API key button');
@ -44,6 +49,10 @@ const onCreateApiKey = async () => {
onMounted(async () => {
documentTitle.set(i18n.baseText('settings.api'));
apiDocsURL.value = isSwaggerUIEnabled
? `${baseUrl}${publicApiPath}/v${publicApiLatestVersion}/docs`
: `https://${DOCS_DOMAIN}/api/api-reference/`;
if (!isPublicApiEnabled) return;
await getApiKeys();
@ -107,6 +116,28 @@ function onEdit(id: string) {
</span>
</n8n-heading>
</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">
<el-row
v-for="apiKey in apiKeysSortByCreationDate"
@ -119,6 +150,34 @@ function onEdit(id: string) {
</el-col>
</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">
<n8n-button
size="large"
@ -138,6 +197,7 @@ function onEdit(id: string) {
:button-text="i18n.baseText('settings.api.trial.upgradePlan.cta')"
@click:button="onUpgrade"
/>
<n8n-action-box
v-if="isPublicApiEnabled && !apiKeysSortByCreationDate.length"
:button-text="
@ -154,7 +214,7 @@ function onEdit(id: string) {
display: flex;
align-items: center;
white-space: nowrap;
margin-bottom: var(--spacing-2xl);
margin-bottom: var(--spacing-xl);
*:first-child {
flex-grow: 1;
@ -176,7 +236,19 @@ function onEdit(id: string) {
right: var(--spacing-s);
}
.hint {
.topHint {
margin-top: none;
margin-bottom: var(--spacing-s);
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>