mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Add free AI credits CTA (#12365)
This commit is contained in:
parent
e26b406665
commit
f8731963f6
|
@ -364,6 +364,7 @@ export interface ICredentialsResponse extends ICredentialsEncrypted {
|
||||||
currentUserHasAccess?: boolean;
|
currentUserHasAccess?: boolean;
|
||||||
scopes?: Scope[];
|
scopes?: Scope[];
|
||||||
ownedBy?: Pick<IUserResponse, 'id' | 'firstName' | 'lastName' | 'email'>;
|
ownedBy?: Pick<IUserResponse, 'id' | 'firstName' | 'lastName' | 'email'>;
|
||||||
|
isManaged: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICredentialsBase {
|
export interface ICredentialsBase {
|
||||||
|
|
|
@ -18,4 +18,7 @@ export const credentialFactory = Factory.extend<ICredentialsResponse>({
|
||||||
updatedAt() {
|
updatedAt() {
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
isManaged() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ const createCredential = (overrides = {}): ICredentialsResponse => ({
|
||||||
type: '',
|
type: '',
|
||||||
name: '',
|
name: '',
|
||||||
sharedWithProjects: [],
|
sharedWithProjects: [],
|
||||||
|
isManaged: false,
|
||||||
homeProject: {} as ProjectSharingData,
|
homeProject: {} as ProjectSharingData,
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,6 +39,7 @@ const props = withDefaults(
|
||||||
name: '',
|
name: '',
|
||||||
sharedWithProjects: [],
|
sharedWithProjects: [],
|
||||||
homeProject: {} as ProjectSharingData,
|
homeProject: {} as ProjectSharingData,
|
||||||
|
isManaged: false,
|
||||||
}),
|
}),
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
},
|
},
|
||||||
|
|
56
packages/editor-ui/src/components/CredentialConfig.test.ts
Normal file
56
packages/editor-ui/src/components/CredentialConfig.test.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import CredentialConfig from './CredentialEdit/CredentialConfig.vue';
|
||||||
|
import { screen } from '@testing-library/vue';
|
||||||
|
import type { ICredentialDataDecryptedObject, ICredentialType } from 'n8n-workflow';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import type { RenderOptions } from '@/__tests__/render';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { STORES } from '@/constants';
|
||||||
|
|
||||||
|
const defaultRenderOptions: RenderOptions = {
|
||||||
|
pinia: createTestingPinia({
|
||||||
|
initialState: {
|
||||||
|
[STORES.SETTINGS]: {
|
||||||
|
settings: {
|
||||||
|
enterprise: {
|
||||||
|
sharing: false,
|
||||||
|
externalSecrets: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
props: {
|
||||||
|
isManaged: true,
|
||||||
|
mode: 'edit',
|
||||||
|
credentialType: {} as ICredentialType,
|
||||||
|
credentialProperties: [],
|
||||||
|
credentialData: {} as ICredentialDataDecryptedObject,
|
||||||
|
credentialPermissions: {
|
||||||
|
share: false,
|
||||||
|
move: false,
|
||||||
|
create: false,
|
||||||
|
read: false,
|
||||||
|
update: false,
|
||||||
|
delete: false,
|
||||||
|
list: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(CredentialConfig, defaultRenderOptions);
|
||||||
|
|
||||||
|
describe('CredentialConfig', () => {
|
||||||
|
it('should display a warning when isManaged is true', async () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(
|
||||||
|
screen.queryByText('This is a managed credential and cannot be edited.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display a warning when isManaged is false', async () => {
|
||||||
|
renderComponent({ props: { isManaged: false } }, { merge: true });
|
||||||
|
expect(
|
||||||
|
screen.queryByText('This is a managed credential and cannot be edited.'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
|
@ -55,6 +55,7 @@ type Props = {
|
||||||
isRetesting?: boolean;
|
isRetesting?: boolean;
|
||||||
requiredPropertiesFilled?: boolean;
|
requiredPropertiesFilled?: boolean;
|
||||||
showAuthTypeSelector?: boolean;
|
showAuthTypeSelector?: boolean;
|
||||||
|
isManaged?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
@ -235,7 +236,10 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<n8n-callout v-if="isManaged" theme="warning" icon="exclamation-triangle">
|
||||||
|
{{ i18n.baseText('freeAi.credits.credentials.edit') }}
|
||||||
|
</n8n-callout>
|
||||||
|
<div v-else>
|
||||||
<div :class="$style.config" data-test-id="node-credentials-config-container">
|
<div :class="$style.config" data-test-id="node-credentials-config-container">
|
||||||
<Banner
|
<Banner
|
||||||
v-show="showValidationWarning"
|
v-show="showValidationWarning"
|
||||||
|
|
|
@ -160,6 +160,11 @@ const credentialTypeName = computed(() => {
|
||||||
return `${props.activeId}`;
|
return `${props.activeId}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isEditingManagedCredential = computed(() => {
|
||||||
|
if (!props.activeId) return false;
|
||||||
|
return credentialsStore.getCredentialById(props.activeId)?.isManaged ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
const isCredentialTestable = computed(() => {
|
const isCredentialTestable = computed(() => {
|
||||||
if (isOAuthType.value || !requiredPropertiesFilled.value) {
|
if (isOAuthType.value || !requiredPropertiesFilled.value) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -597,6 +602,10 @@ function scrollToBottom() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function retestCredential() {
|
async function retestCredential() {
|
||||||
|
if (isEditingManagedCredential.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isCredentialTestable.value || !credentialTypeName.value) {
|
if (!isCredentialTestable.value || !credentialTypeName.value) {
|
||||||
authError.value = '';
|
authError.value = '';
|
||||||
testedSuccessfully.value = false;
|
testedSuccessfully.value = false;
|
||||||
|
@ -1061,7 +1070,9 @@ function resetCredentialData(): void {
|
||||||
<InlineNameEdit
|
<InlineNameEdit
|
||||||
:model-value="credentialName"
|
:model-value="credentialName"
|
||||||
:subtitle="credentialType ? credentialType.displayName : ''"
|
:subtitle="credentialType ? credentialType.displayName : ''"
|
||||||
:readonly="!credentialPermissions.update || !credentialType"
|
:readonly="
|
||||||
|
!credentialPermissions.update || !credentialType || isEditingManagedCredential
|
||||||
|
"
|
||||||
type="Credential"
|
type="Credential"
|
||||||
data-test-id="credential-name"
|
data-test-id="credential-name"
|
||||||
@update:model-value="onNameEdit"
|
@update:model-value="onNameEdit"
|
||||||
|
@ -1113,6 +1124,7 @@ function resetCredentialData(): void {
|
||||||
:credential-properties="credentialProperties"
|
:credential-properties="credentialProperties"
|
||||||
:credential-data="credentialData"
|
:credential-data="credentialData"
|
||||||
:credential-id="credentialId"
|
:credential-id="credentialId"
|
||||||
|
:is-managed="isEditingManagedCredential"
|
||||||
:show-validation-warning="showValidationWarning"
|
:show-validation-warning="showValidationWarning"
|
||||||
:auth-error="authError"
|
:auth-error="authError"
|
||||||
:tested-successfully="testedSuccessfully"
|
:tested-successfully="testedSuccessfully"
|
||||||
|
|
185
packages/editor-ui/src/components/FreeAiCreditsCallout.test.ts
Normal file
185
packages/editor-ui/src/components/FreeAiCreditsCallout.test.ts
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { fireEvent, screen } from '@testing-library/vue';
|
||||||
|
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
||||||
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
|
import { useRootStore } from '@/stores/root.store';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { renderComponent } from '@/__tests__/render';
|
||||||
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
|
|
||||||
|
vi.mock('@/composables/useToast', () => ({
|
||||||
|
useToast: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/settings.store', () => ({
|
||||||
|
useSettingsStore: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/credentials.store', () => ({
|
||||||
|
useCredentialsStore: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/users.store', () => ({
|
||||||
|
useUsersStore: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/ndv.store', () => ({
|
||||||
|
useNDVStore: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/posthog.store', () => ({
|
||||||
|
usePostHog: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/projects.store', () => ({
|
||||||
|
useProjectsStore: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/root.store', () => ({
|
||||||
|
useRootStore: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const assertUserCannotClaimCredits = () => {
|
||||||
|
expect(screen.queryByText('Get 100 free OpenAI API credits')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('button', { name: 'Claim credits' })).not.toBeInTheDocument();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertUserCanClaimCredits = () => {
|
||||||
|
expect(screen.getByText('Get 100 free OpenAI API credits')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('button', { name: 'Claim credits' })).toBeInTheDocument();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertUserClaimedCredits = () => {
|
||||||
|
expect(screen.getByText('Claimed 100 free OpenAI API credits')).toBeInTheDocument();
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('FreeAiCreditsCallout', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
(useSettingsStore as any).mockReturnValue({
|
||||||
|
isAiCreditsEnabled: true,
|
||||||
|
aiCreditsQuota: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
(useCredentialsStore as any).mockReturnValue({
|
||||||
|
allCredentials: [],
|
||||||
|
upsertCredential: vi.fn(),
|
||||||
|
claimFreeAiCredits: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
(useUsersStore as any).mockReturnValue({
|
||||||
|
currentUser: {
|
||||||
|
settings: {
|
||||||
|
userClaimedAiCredits: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(useNDVStore as any).mockReturnValue({
|
||||||
|
activeNode: { type: '@n8n/n8n-nodes-langchain.openAi' },
|
||||||
|
});
|
||||||
|
|
||||||
|
(usePostHog as any).mockReturnValue({
|
||||||
|
isFeatureEnabled: vi.fn().mockReturnValue(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
(useProjectsStore as any).mockReturnValue({
|
||||||
|
currentProject: { id: 'test-project-id' },
|
||||||
|
});
|
||||||
|
|
||||||
|
(useRootStore as any).mockReturnValue({
|
||||||
|
restApiContext: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
(useToast as any).mockReturnValue({
|
||||||
|
showError: vi.fn(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should shows the claim callout when the user can claim credits', () => {
|
||||||
|
renderComponent(FreeAiCreditsCallout);
|
||||||
|
|
||||||
|
assertUserCanClaimCredits();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show success callout when credit are claimed', async () => {
|
||||||
|
const credentialsStore = mockedStore(useCredentialsStore);
|
||||||
|
|
||||||
|
renderComponent(FreeAiCreditsCallout);
|
||||||
|
|
||||||
|
const claimButton = screen.getByRole('button', {
|
||||||
|
name: 'Claim credits',
|
||||||
|
});
|
||||||
|
|
||||||
|
await fireEvent.click(claimButton);
|
||||||
|
|
||||||
|
expect(credentialsStore.claimFreeAiCredits).toHaveBeenCalledWith('test-project-id');
|
||||||
|
assertUserClaimedCredits();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be able to claim credits is user already claimed credits', async () => {
|
||||||
|
(useUsersStore as any).mockReturnValue({
|
||||||
|
currentUser: {
|
||||||
|
settings: {
|
||||||
|
userClaimedAiCredits: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent(FreeAiCreditsCallout);
|
||||||
|
|
||||||
|
assertUserCannotClaimCredits();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be able to claim credits is user does not have ai credits enabled', async () => {
|
||||||
|
(useSettingsStore as any).mockReturnValue({
|
||||||
|
isAiCreditsEnabled: false,
|
||||||
|
aiCreditsQuota: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent(FreeAiCreditsCallout);
|
||||||
|
|
||||||
|
assertUserCannotClaimCredits();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be able to claim credits if user it is not in experiment', async () => {
|
||||||
|
(usePostHog as any).mockReturnValue({
|
||||||
|
isFeatureEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent(FreeAiCreditsCallout);
|
||||||
|
|
||||||
|
assertUserCannotClaimCredits();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be able to claim credits if user already has OpenAiApi credential', async () => {
|
||||||
|
(useCredentialsStore as any).mockReturnValue({
|
||||||
|
allCredentials: [
|
||||||
|
{
|
||||||
|
type: 'openAiApi',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
upsertCredential: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent(FreeAiCreditsCallout);
|
||||||
|
|
||||||
|
assertUserCannotClaimCredits();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be able to claim credits if active node it is not a valid node', async () => {
|
||||||
|
(useNDVStore as any).mockReturnValue({
|
||||||
|
activeNode: { type: '@n8n/n8n-nodes.jira' },
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent(FreeAiCreditsCallout);
|
||||||
|
|
||||||
|
assertUserCannotClaimCredits();
|
||||||
|
});
|
||||||
|
});
|
118
packages/editor-ui/src/components/FreeAiCreditsCallout.vue
Normal file
118
packages/editor-ui/src/components/FreeAiCreditsCallout.vue
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { AI_CREDITS_EXPERIMENT } from '@/constants';
|
||||||
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
const OPEN_AI_API_CREDENTIAL_TYPE = 'openAiApi';
|
||||||
|
|
||||||
|
const LANGCHAIN_NODES_PREFIX = '@n8n/n8n-nodes-langchain.';
|
||||||
|
|
||||||
|
const N8N_NODES_PREFIX = '@n8n/n8n-nodes.';
|
||||||
|
|
||||||
|
const NODES_WITH_OPEN_AI_API_CREDENTIAL = [
|
||||||
|
`${LANGCHAIN_NODES_PREFIX}openAi`,
|
||||||
|
`${LANGCHAIN_NODES_PREFIX}embeddingsOpenAi`,
|
||||||
|
`${LANGCHAIN_NODES_PREFIX}lmChatOpenAi`,
|
||||||
|
`${N8N_NODES_PREFIX}openAi`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const showSuccessCallout = ref(false);
|
||||||
|
const claimingCredits = ref(false);
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const postHogStore = usePostHog();
|
||||||
|
const credentialsStore = useCredentialsStore();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
const projectsStore = useProjectsStore();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const userHasOpenAiCredentialAlready = computed(
|
||||||
|
() =>
|
||||||
|
!!credentialsStore.allCredentials.filter(
|
||||||
|
(credential) => credential.type === OPEN_AI_API_CREDENTIAL_TYPE,
|
||||||
|
).length,
|
||||||
|
);
|
||||||
|
|
||||||
|
const userHasClaimedAiCreditsAlready = computed(
|
||||||
|
() => !!usersStore.currentUser?.settings?.userClaimedAiCredits,
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeNodeHasOpenAiApiCredential = computed(
|
||||||
|
() =>
|
||||||
|
ndvStore.activeNode?.type &&
|
||||||
|
NODES_WITH_OPEN_AI_API_CREDENTIAL.includes(ndvStore.activeNode.type),
|
||||||
|
);
|
||||||
|
|
||||||
|
const userCanClaimOpenAiCredits = computed(() => {
|
||||||
|
return (
|
||||||
|
settingsStore.isAiCreditsEnabled &&
|
||||||
|
activeNodeHasOpenAiApiCredential.value &&
|
||||||
|
postHogStore.isFeatureEnabled(AI_CREDITS_EXPERIMENT.name) &&
|
||||||
|
!userHasOpenAiCredentialAlready.value &&
|
||||||
|
!userHasClaimedAiCreditsAlready.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClaimCreditsClicked = async () => {
|
||||||
|
claimingCredits.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await credentialsStore.claimFreeAiCredits(projectsStore.currentProject?.id);
|
||||||
|
|
||||||
|
if (usersStore?.currentUser?.settings) {
|
||||||
|
usersStore.currentUser.settings.userClaimedAiCredits = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessCallout.value = true;
|
||||||
|
} catch (e) {
|
||||||
|
toast.showError(
|
||||||
|
e,
|
||||||
|
i18n.baseText('freeAi.credits.showError.claim.title'),
|
||||||
|
i18n.baseText('freeAi.credits.showError.claim.message'),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
claimingCredits.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="mt-xs">
|
||||||
|
<n8n-callout
|
||||||
|
v-if="userCanClaimOpenAiCredits && !showSuccessCallout"
|
||||||
|
theme="secondary"
|
||||||
|
icon="exclamation-circle"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
i18n.baseText('freeAi.credits.callout.claim.title', {
|
||||||
|
interpolate: { credits: settingsStore.aiCreditsQuota },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<template #trailingContent>
|
||||||
|
<n8n-button
|
||||||
|
type="tertiary"
|
||||||
|
size="small"
|
||||||
|
:label="i18n.baseText('freeAi.credits.callout.claim.button.label')"
|
||||||
|
:loading="claimingCredits"
|
||||||
|
@click="onClaimCreditsClicked"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</n8n-callout>
|
||||||
|
<n8n-callout v-else-if="showSuccessCallout" theme="success" icon="check-circle">
|
||||||
|
{{
|
||||||
|
i18n.baseText('freeAi.credits.callout.success.title', {
|
||||||
|
interpolate: { credits: settingsStore.aiCreditsQuota },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</n8n-callout>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -48,6 +48,7 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
import { updateDynamicConnections } from '@/utils/nodeSettingsUtils';
|
import { updateDynamicConnections } from '@/utils/nodeSettingsUtils';
|
||||||
|
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -1026,6 +1027,7 @@ onBeforeUnmount(() => {
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<FreeAiCreditsCallout />
|
||||||
<div v-show="openPanel === 'params'">
|
<div v-show="openPanel === 'params'">
|
||||||
<NodeWebhooks :node="node" :node-type-description="nodeType" />
|
<NodeWebhooks :node="node" :node-type-description="nodeType" />
|
||||||
|
|
||||||
|
|
|
@ -2805,5 +2805,11 @@
|
||||||
"testDefinition.notImplemented": "This feature is not implemented yet!",
|
"testDefinition.notImplemented": "This feature is not implemented yet!",
|
||||||
"testDefinition.viewDetails": "View Details",
|
"testDefinition.viewDetails": "View Details",
|
||||||
"testDefinition.editTest": "Edit Test",
|
"testDefinition.editTest": "Edit Test",
|
||||||
"testDefinition.deleteTest": "Delete Test"
|
"testDefinition.deleteTest": "Delete Test",
|
||||||
|
"freeAi.credits.callout.claim.title": "Get {credits} free OpenAI API credits",
|
||||||
|
"freeAi.credits.callout.claim.button.label": "Claim credits",
|
||||||
|
"freeAi.credits.callout.success.title": "Claimed {credits} free OpenAI API credits",
|
||||||
|
"freeAi.credits.credentials.edit": "This is a managed credential and cannot be edited.",
|
||||||
|
"freeAi.credits.showError.claim.title": "Free AI credits",
|
||||||
|
"freeAi.credits.showError.claim.message": "Enable to claim credits"
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,6 +163,7 @@ import {
|
||||||
faStream,
|
faStream,
|
||||||
faPowerOff,
|
faPowerOff,
|
||||||
faPaperPlane,
|
faPaperPlane,
|
||||||
|
faExclamationCircle,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faVariable, faXmark, faVault, faRefresh } from './custom';
|
import { faVariable, faXmark, faVault, faRefresh } from './custom';
|
||||||
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
@ -228,6 +229,7 @@ export const FontAwesomePlugin: Plugin = {
|
||||||
addIcon(faEquals);
|
addIcon(faEquals);
|
||||||
addIcon(faEye);
|
addIcon(faEye);
|
||||||
addIcon(faExclamationTriangle);
|
addIcon(faExclamationTriangle);
|
||||||
|
addIcon(faExclamationCircle);
|
||||||
addIcon(faExpand);
|
addIcon(faExpand);
|
||||||
addIcon(faExpandAlt);
|
addIcon(faExpandAlt);
|
||||||
addIcon(faExternalLinkAlt);
|
addIcon(faExternalLinkAlt);
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { computed, ref } from 'vue';
|
||||||
import { useNodeTypesStore } from './nodeTypes.store';
|
import { useNodeTypesStore } from './nodeTypes.store';
|
||||||
import { useRootStore } from './root.store';
|
import { useRootStore } from './root.store';
|
||||||
import { useSettingsStore } from './settings.store';
|
import { useSettingsStore } from './settings.store';
|
||||||
|
import * as aiApi from '@/api/ai';
|
||||||
|
|
||||||
const DEFAULT_CREDENTIAL_NAME = 'Unnamed credential';
|
const DEFAULT_CREDENTIAL_NAME = 'Unnamed credential';
|
||||||
const DEFAULT_CREDENTIAL_POSTFIX = 'account';
|
const DEFAULT_CREDENTIAL_POSTFIX = 'account';
|
||||||
|
@ -36,6 +37,8 @@ export type CredentialsStore = ReturnType<typeof useCredentialsStore>;
|
||||||
export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
const state = ref<ICredentialsState>({ credentialTypes: {}, credentials: {} });
|
const state = ref<ICredentialsState>({ credentialTypes: {}, credentials: {} });
|
||||||
|
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// #region Computed
|
// #region Computed
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -252,7 +255,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
if (allCredentialTypes.value.length > 0 && !forceFetch) {
|
if (allCredentialTypes.value.length > 0 && !forceFetch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rootStore = useRootStore();
|
|
||||||
const credentialTypes = await credentialsApi.getCredentialTypes(rootStore.baseUrl);
|
const credentialTypes = await credentialsApi.getCredentialTypes(rootStore.baseUrl);
|
||||||
setCredentialTypes(credentialTypes);
|
setCredentialTypes(credentialTypes);
|
||||||
};
|
};
|
||||||
|
@ -261,8 +263,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
projectId?: string,
|
projectId?: string,
|
||||||
includeScopes = true,
|
includeScopes = true,
|
||||||
): Promise<ICredentialsResponse[]> => {
|
): Promise<ICredentialsResponse[]> => {
|
||||||
const rootStore = useRootStore();
|
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
projectId,
|
projectId,
|
||||||
};
|
};
|
||||||
|
@ -279,8 +279,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
const fetchAllCredentialsForWorkflow = async (
|
const fetchAllCredentialsForWorkflow = async (
|
||||||
options: { workflowId: string } | { projectId: string },
|
options: { workflowId: string } | { projectId: string },
|
||||||
): Promise<ICredentialsResponse[]> => {
|
): Promise<ICredentialsResponse[]> => {
|
||||||
const rootStore = useRootStore();
|
|
||||||
|
|
||||||
const credentials = await credentialsApi.getAllCredentialsForWorkflow(
|
const credentials = await credentialsApi.getAllCredentialsForWorkflow(
|
||||||
rootStore.restApiContext,
|
rootStore.restApiContext,
|
||||||
options,
|
options,
|
||||||
|
@ -294,7 +292,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
}): Promise<ICredentialsResponse | ICredentialsDecryptedResponse | undefined> => {
|
}): Promise<ICredentialsResponse | ICredentialsDecryptedResponse | undefined> => {
|
||||||
const rootStore = useRootStore();
|
|
||||||
return await credentialsApi.getCredentialData(rootStore.restApiContext, id);
|
return await credentialsApi.getCredentialData(rootStore.restApiContext, id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -302,7 +299,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
data: ICredentialsDecrypted,
|
data: ICredentialsDecrypted,
|
||||||
projectId?: string,
|
projectId?: string,
|
||||||
): Promise<ICredentialsResponse> => {
|
): Promise<ICredentialsResponse> => {
|
||||||
const rootStore = useRootStore();
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const credential = await credentialsApi.createNewCredential(
|
const credential = await credentialsApi.createNewCredential(
|
||||||
rootStore.restApiContext,
|
rootStore.restApiContext,
|
||||||
|
@ -333,7 +329,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
id: string;
|
id: string;
|
||||||
}): Promise<ICredentialsResponse> => {
|
}): Promise<ICredentialsResponse> => {
|
||||||
const { id, data } = params;
|
const { id, data } = params;
|
||||||
const rootStore = useRootStore();
|
|
||||||
const credential = await credentialsApi.updateCredential(rootStore.restApiContext, id, data);
|
const credential = await credentialsApi.updateCredential(rootStore.restApiContext, id, data);
|
||||||
|
|
||||||
upsertCredential(credential);
|
upsertCredential(credential);
|
||||||
|
@ -342,7 +337,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCredential = async ({ id }: { id: string }) => {
|
const deleteCredential = async ({ id }: { id: string }) => {
|
||||||
const rootStore = useRootStore();
|
|
||||||
const deleted = await credentialsApi.deleteCredential(rootStore.restApiContext, id);
|
const deleted = await credentialsApi.deleteCredential(rootStore.restApiContext, id);
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
const { [id]: deletedCredential, ...rest } = state.value.credentials;
|
const { [id]: deletedCredential, ...rest } = state.value.credentials;
|
||||||
|
@ -351,19 +345,16 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const oAuth2Authorize = async (data: ICredentialsResponse): Promise<string> => {
|
const oAuth2Authorize = async (data: ICredentialsResponse): Promise<string> => {
|
||||||
const rootStore = useRootStore();
|
|
||||||
return await credentialsApi.oAuth2CredentialAuthorize(rootStore.restApiContext, data);
|
return await credentialsApi.oAuth2CredentialAuthorize(rootStore.restApiContext, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const oAuth1Authorize = async (data: ICredentialsResponse): Promise<string> => {
|
const oAuth1Authorize = async (data: ICredentialsResponse): Promise<string> => {
|
||||||
const rootStore = useRootStore();
|
|
||||||
return await credentialsApi.oAuth1CredentialAuthorize(rootStore.restApiContext, data);
|
return await credentialsApi.oAuth1CredentialAuthorize(rootStore.restApiContext, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const testCredential = async (
|
const testCredential = async (
|
||||||
data: ICredentialsDecrypted,
|
data: ICredentialsDecrypted,
|
||||||
): Promise<INodeCredentialTestResult> => {
|
): Promise<INodeCredentialTestResult> => {
|
||||||
const rootStore = useRootStore();
|
|
||||||
return await credentialsApi.testCredential(rootStore.restApiContext, { credentials: data });
|
return await credentialsApi.testCredential(rootStore.restApiContext, { credentials: data });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -377,7 +368,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
newName =
|
newName =
|
||||||
newName.length > 0 ? `${newName} ${DEFAULT_CREDENTIAL_POSTFIX}` : DEFAULT_CREDENTIAL_NAME;
|
newName.length > 0 ? `${newName} ${DEFAULT_CREDENTIAL_POSTFIX}` : DEFAULT_CREDENTIAL_NAME;
|
||||||
}
|
}
|
||||||
const rootStore = useRootStore();
|
|
||||||
const res = await credentialsApi.getCredentialsNewName(rootStore.restApiContext, newName);
|
const res = await credentialsApi.getCredentialsNewName(rootStore.restApiContext, newName);
|
||||||
return res.name;
|
return res.name;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -407,12 +397,19 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCredentialTranslation = async (credentialType: string): Promise<object> => {
|
const getCredentialTranslation = async (credentialType: string): Promise<object> => {
|
||||||
const rootStore = useRootStore();
|
|
||||||
return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/credential-translation', {
|
return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/credential-translation', {
|
||||||
credentialType,
|
credentialType,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const claimFreeAiCredits = async (projectId?: string): Promise<ICredentialsResponse> => {
|
||||||
|
const credential = await aiApi.claimFreeAiCredits(rootStore.restApiContext, {
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
upsertCredential(credential);
|
||||||
|
return credential;
|
||||||
|
};
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -449,6 +446,7 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
testCredential,
|
testCredential,
|
||||||
getCredentialTranslation,
|
getCredentialTranslation,
|
||||||
setCredentialSharedWith,
|
setCredentialSharedWith,
|
||||||
|
claimFreeAiCredits,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,7 @@ describe('CredentialsView', () => {
|
||||||
type: 'test',
|
type: 'test',
|
||||||
createdAt: '2021-05-05T00:00:00Z',
|
createdAt: '2021-05-05T00:00:00Z',
|
||||||
updatedAt: '2021-05-05T00:00:00Z',
|
updatedAt: '2021-05-05T00:00:00Z',
|
||||||
|
isManaged: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const projectsStore = mockedStore(useProjectsStore);
|
const projectsStore = mockedStore(useProjectsStore);
|
||||||
|
@ -77,6 +78,7 @@ describe('CredentialsView', () => {
|
||||||
createdAt: '2021-05-05T00:00:00Z',
|
createdAt: '2021-05-05T00:00:00Z',
|
||||||
updatedAt: '2021-05-05T00:00:00Z',
|
updatedAt: '2021-05-05T00:00:00Z',
|
||||||
scopes: ['credential:update'],
|
scopes: ['credential:update'],
|
||||||
|
isManaged: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
|
@ -84,6 +86,7 @@ describe('CredentialsView', () => {
|
||||||
type: 'test2',
|
type: 'test2',
|
||||||
createdAt: '2021-05-05T00:00:00Z',
|
createdAt: '2021-05-05T00:00:00Z',
|
||||||
updatedAt: '2021-05-05T00:00:00Z',
|
updatedAt: '2021-05-05T00:00:00Z',
|
||||||
|
isManaged: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const projectsStore = mockedStore(useProjectsStore);
|
const projectsStore = mockedStore(useProjectsStore);
|
||||||
|
@ -124,6 +127,7 @@ describe('CredentialsView', () => {
|
||||||
createdAt: '2021-05-05T00:00:00Z',
|
createdAt: '2021-05-05T00:00:00Z',
|
||||||
updatedAt: '2021-05-05T00:00:00Z',
|
updatedAt: '2021-05-05T00:00:00Z',
|
||||||
scopes: ['credential:update'],
|
scopes: ['credential:update'],
|
||||||
|
isManaged: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const { getByTestId } = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
|
|
|
@ -51,5 +51,6 @@ export const newCredential = (
|
||||||
updatedAt: faker.date.past().toISOString(),
|
updatedAt: faker.date.past().toISOString(),
|
||||||
id: faker.string.alphanumeric({ length: 16 }),
|
id: faker.string.alphanumeric({ length: 16 }),
|
||||||
name: faker.commerce.productName(),
|
name: faker.commerce.productName(),
|
||||||
|
isManaged: false,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue