mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(core): Add documentation hints for API keys to SettingApiView.vue
(no-changelog) (#13617)
This commit is contained in:
parent
4067fb0b12
commit
a7f0c66e30
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue