mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat: New package version available callout (#17097)
This commit is contained in:
@@ -17,4 +17,5 @@ export type CommunityNodeType = {
|
|||||||
companyName?: string;
|
companyName?: string;
|
||||||
nodeDescription: INodeTypeDescription;
|
nodeDescription: INodeTypeDescription;
|
||||||
isInstalled: boolean;
|
isInstalled: boolean;
|
||||||
|
nodeVersions?: Array<{ npmVersion: string; checksum: string }>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,31 +2,16 @@ import type { CommunityNodeType } from '@n8n/api-types';
|
|||||||
import { Logger, inProduction } from '@n8n/backend-common';
|
import { Logger, inProduction } from '@n8n/backend-common';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { ensureError, type INodeTypeDescription } from 'n8n-workflow';
|
import { ensureError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { CommunityPackagesService } from './community-packages.service';
|
import { CommunityPackagesService } from './community-packages.service';
|
||||||
import { getCommunityNodeTypes } from '../utils/community-node-types-utils';
|
import {
|
||||||
|
getCommunityNodeTypes,
|
||||||
|
StrapiCommunityNodeType,
|
||||||
|
} from '../utils/community-node-types-utils';
|
||||||
|
|
||||||
const UPDATE_INTERVAL = 8 * 60 * 60 * 1000;
|
const UPDATE_INTERVAL = 8 * 60 * 60 * 1000;
|
||||||
|
|
||||||
export type StrapiCommunityNodeType = {
|
|
||||||
authorGithubUrl: string;
|
|
||||||
authorName: string;
|
|
||||||
checksum: string;
|
|
||||||
description: string;
|
|
||||||
displayName: string;
|
|
||||||
name: string;
|
|
||||||
numberOfStars: number;
|
|
||||||
numberOfDownloads: number;
|
|
||||||
packageName: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
npmVersion: string;
|
|
||||||
isOfficialNode: boolean;
|
|
||||||
companyName?: string;
|
|
||||||
nodeDescription: INodeTypeDescription;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CommunityNodeTypesService {
|
export class CommunityNodeTypesService {
|
||||||
private communityNodeTypes: Map<string, StrapiCommunityNodeType> = new Map();
|
private communityNodeTypes: Map<string, StrapiCommunityNodeType> = new Map();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type StrapiCommunityNodeType = {
|
|||||||
isOfficialNode: boolean;
|
isOfficialNode: boolean;
|
||||||
companyName?: string;
|
companyName?: string;
|
||||||
nodeDescription: INodeTypeDescription;
|
nodeDescription: INodeTypeDescription;
|
||||||
|
nodeVersions?: Array<{ npmVersion: string; checksum: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const N8N_VETTED_NODE_TYPES_STAGING_URL = 'https://api-staging.n8n.io/api/community-nodes';
|
const N8N_VETTED_NODE_TYPES_STAGING_URL = 'https://api-staging.n8n.io/api/community-nodes';
|
||||||
|
|||||||
@@ -3279,6 +3279,7 @@
|
|||||||
"communityNodeInfo.downloads": "{downloads} Downloads",
|
"communityNodeInfo.downloads": "{downloads} Downloads",
|
||||||
"communityNodeInfo.publishedBy": "Published by {publisherName}",
|
"communityNodeInfo.publishedBy": "Published by {publisherName}",
|
||||||
"communityNodeInfo.contact.admin": "Please contact an administrator to install this community node:",
|
"communityNodeInfo.contact.admin": "Please contact an administrator to install this community node:",
|
||||||
|
"communityNodeUpdateInfo.available": "A new node package version is available",
|
||||||
"insights.upgradeModal.button.dismiss": "Dismiss",
|
"insights.upgradeModal.button.dismiss": "Dismiss",
|
||||||
"insights.upgradeModal.button.upgrade": "Upgrade",
|
"insights.upgradeModal.button.upgrade": "Upgrade",
|
||||||
"insights.upgradeModal.content": "Viewing this time period requires an enterprise plan. Upgrade to Enterprise to unlock advanced features.",
|
"insights.upgradeModal.content": "Viewing this time period requires an enterprise plan. Upgrade to Enterprise to unlock advanced features.",
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import { setActivePinia } from 'pinia';
|
|||||||
import CommunityNodeInfo from './CommunityNodeInfo.vue';
|
import CommunityNodeInfo from './CommunityNodeInfo.vue';
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
import type { CommunityNodeDetails } from '../composables/useViewStacks';
|
||||||
|
|
||||||
const getCommunityNodeAttributes = vi.fn();
|
const getCommunityNodeAttributes = vi.fn();
|
||||||
const communityNodesStore: { getInstalledPackages: PublicInstalledPackage[] } = {
|
const getInstalledPackage = vi.fn();
|
||||||
getInstalledPackages: [],
|
const communityNodesStore: {
|
||||||
|
getInstalledPackage: (packageName: string) => Promise<PublicInstalledPackage>;
|
||||||
|
} = {
|
||||||
|
getInstalledPackage,
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mock('@/stores/nodeTypes.store', () => ({
|
vi.mock('@/stores/nodeTypes.store', () => ({
|
||||||
@@ -27,8 +31,15 @@ vi.mock('@/stores/communityNodes.store', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../composables/useViewStacks', () => ({
|
vi.mock('../composables/useViewStacks', () => ({
|
||||||
useViewStacks: vi.fn(() => ({
|
useViewStacks: vi.fn(),
|
||||||
activeViewStack: {
|
}));
|
||||||
|
|
||||||
|
describe('CommunityNodeInfo', () => {
|
||||||
|
const renderComponent = createComponentRenderer(CommunityNodeInfo);
|
||||||
|
let pinia: TestingPinia;
|
||||||
|
let originalFetch: typeof global.fetch;
|
||||||
|
|
||||||
|
const defaultViewStack = {
|
||||||
communityNodeDetails: {
|
communityNodeDetails: {
|
||||||
description: 'Other node description',
|
description: 'Other node description',
|
||||||
installed: false,
|
installed: false,
|
||||||
@@ -60,21 +71,19 @@ vi.mock('../composables/useViewStacks', () => ({
|
|||||||
rootView: undefined,
|
rootView: undefined,
|
||||||
subcategory: 'Other Node',
|
subcategory: 'Other Node',
|
||||||
title: 'Node details',
|
title: 'Node details',
|
||||||
},
|
};
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('CommunityNodeInfo', () => {
|
beforeEach(async () => {
|
||||||
const renderComponent = createComponentRenderer(CommunityNodeInfo);
|
|
||||||
let pinia: TestingPinia;
|
|
||||||
let originalFetch: typeof global.fetch;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
pinia = createTestingPinia();
|
pinia = createTestingPinia();
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
|
|
||||||
originalFetch = global.fetch;
|
originalFetch = global.fetch;
|
||||||
global.fetch = vi.fn();
|
global.fetch = vi.fn();
|
||||||
|
|
||||||
|
const { useViewStacks } = await import('../composables/useViewStacks');
|
||||||
|
vi.mocked(useViewStacks).mockReturnValue({
|
||||||
|
activeViewStack: defaultViewStack,
|
||||||
|
} as ReturnType<typeof useViewStacks>);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -87,13 +96,13 @@ describe('CommunityNodeInfo', () => {
|
|||||||
npmVersion: '1.0.0',
|
npmVersion: '1.0.0',
|
||||||
authorName: 'contributor',
|
authorName: 'contributor',
|
||||||
numberOfDownloads: 9999,
|
numberOfDownloads: 9999,
|
||||||
|
nodeVersions: [{ npmVersion: '1.0.0' }],
|
||||||
});
|
});
|
||||||
communityNodesStore.getInstalledPackages = [
|
getInstalledPackage.mockResolvedValue({
|
||||||
{
|
|
||||||
installedVersion: '1.0.0',
|
installedVersion: '1.0.0',
|
||||||
packageName: 'n8n-nodes-test',
|
packageName: 'n8n-nodes-test',
|
||||||
} as PublicInstalledPackage,
|
} as PublicInstalledPackage);
|
||||||
];
|
|
||||||
const wrapper = renderComponent({ pinia });
|
const wrapper = renderComponent({ pinia });
|
||||||
|
|
||||||
await waitFor(() => expect(wrapper.queryByTestId('number-of-downloads')).toBeInTheDocument());
|
await waitFor(() => expect(wrapper.queryByTestId('number-of-downloads')).toBeInTheDocument());
|
||||||
@@ -106,6 +115,45 @@ describe('CommunityNodeInfo', () => {
|
|||||||
expect(wrapper.getByTestId('publisher-name').textContent).toEqual('Published by contributor');
|
expect(wrapper.getByTestId('publisher-name').textContent).toEqual('Published by contributor');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should display update notice, should show verified batch for older versions', async () => {
|
||||||
|
const { useViewStacks } = await import('../composables/useViewStacks');
|
||||||
|
vi.mocked(useViewStacks).mockReturnValue({
|
||||||
|
activeViewStack: {
|
||||||
|
...defaultViewStack,
|
||||||
|
communityNodeDetails: {
|
||||||
|
...defaultViewStack.communityNodeDetails,
|
||||||
|
installed: true,
|
||||||
|
} as CommunityNodeDetails,
|
||||||
|
},
|
||||||
|
} as ReturnType<typeof useViewStacks>);
|
||||||
|
|
||||||
|
getCommunityNodeAttributes.mockResolvedValue({
|
||||||
|
npmVersion: '1.0.0',
|
||||||
|
authorName: 'contributor',
|
||||||
|
numberOfDownloads: 9999,
|
||||||
|
nodeVersions: [{ npmVersion: '1.0.0' }, { npmVersion: '0.0.9' }],
|
||||||
|
});
|
||||||
|
getInstalledPackage.mockResolvedValue({
|
||||||
|
installedVersion: '0.0.9',
|
||||||
|
packageName: 'n8n-nodes-test',
|
||||||
|
updateAvailable: '1.0.1',
|
||||||
|
} as PublicInstalledPackage);
|
||||||
|
|
||||||
|
const wrapper = renderComponent({ pinia });
|
||||||
|
|
||||||
|
await waitFor(() => expect(wrapper.queryByTestId('number-of-downloads')).toBeInTheDocument());
|
||||||
|
|
||||||
|
expect(wrapper.container.querySelector('.description')?.textContent).toEqual(
|
||||||
|
'Other node description',
|
||||||
|
);
|
||||||
|
expect(wrapper.getByTestId('verified-tag').textContent).toEqual('Verified');
|
||||||
|
expect(wrapper.getByTestId('number-of-downloads').textContent).toEqual('9,999 Downloads');
|
||||||
|
expect(wrapper.getByTestId('publisher-name').textContent).toEqual('Published by contributor');
|
||||||
|
expect(wrapper.getByTestId('update-available').textContent).toEqual(
|
||||||
|
'A new node package version is available',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should render correctly with fetched info', async () => {
|
it('should render correctly with fetched info', async () => {
|
||||||
const packageData = {
|
const packageData = {
|
||||||
maintainers: [{ name: 'testAuthor' }],
|
maintainers: [{ name: 'testAuthor' }],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useCommunityNodesStore } from '@/stores/communityNodes.store';
|
|||||||
import { captureException } from '@sentry/vue';
|
import { captureException } from '@sentry/vue';
|
||||||
import { N8nText, N8nTooltip, N8nIcon } from '@n8n/design-system';
|
import { N8nText, N8nTooltip, N8nIcon } from '@n8n/design-system';
|
||||||
import ShieldIcon from 'virtual:icons/fa-solid/shield-alt';
|
import ShieldIcon from 'virtual:icons/fa-solid/shield-alt';
|
||||||
|
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||||
|
|
||||||
const { activeViewStack } = useViewStacks();
|
const { activeViewStack } = useViewStacks();
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ const publisherName = ref<string | undefined>(undefined);
|
|||||||
const downloads = ref<string | null>(null);
|
const downloads = ref<string | null>(null);
|
||||||
const verified = ref(false);
|
const verified = ref(false);
|
||||||
const official = ref(false);
|
const official = ref(false);
|
||||||
|
const installedPackage = ref<PublicInstalledPackage | undefined>(undefined);
|
||||||
|
|
||||||
const communityNodesStore = useCommunityNodesStore();
|
const communityNodesStore = useCommunityNodesStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
|
||||||
@@ -42,17 +45,22 @@ async function fetchPackageInfo(packageName: string) {
|
|||||||
activeViewStack.communityNodeDetails?.key || '',
|
activeViewStack.communityNodeDetails?.key || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (communityNodeDetails?.installed) {
|
||||||
|
installedPackage.value = await communityNodesStore.getInstalledPackage(
|
||||||
|
communityNodeDetails.packageName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (communityNodeAttributes) {
|
if (communityNodeAttributes) {
|
||||||
publisherName.value = communityNodeAttributes.companyName ?? communityNodeAttributes.authorName;
|
publisherName.value = communityNodeAttributes.companyName ?? communityNodeAttributes.authorName;
|
||||||
downloads.value = formatNumber(communityNodeAttributes.numberOfDownloads);
|
downloads.value = formatNumber(communityNodeAttributes.numberOfDownloads);
|
||||||
official.value = communityNodeAttributes.isOfficialNode;
|
official.value = communityNodeAttributes.isOfficialNode;
|
||||||
const packageInfo = communityNodesStore.getInstalledPackages.find(
|
|
||||||
(p) => p.packageName === communityNodeAttributes.packageName,
|
if (!installedPackage.value) {
|
||||||
);
|
|
||||||
if (!packageInfo) {
|
|
||||||
verified.value = true;
|
verified.value = true;
|
||||||
} else {
|
} else {
|
||||||
verified.value = packageInfo.installedVersion === communityNodeAttributes.npmVersion;
|
const verifiedVersions = communityNodeAttributes.nodeVersions?.map((v) => v.npmVersion) ?? [];
|
||||||
|
verified.value = verifiedVersions.includes(installedPackage.value.installedVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -107,7 +115,11 @@ onMounted(async () => {
|
|||||||
<N8nText :class="$style.description" color="text-base" size="medium">
|
<N8nText :class="$style.description" color="text-base" size="medium">
|
||||||
{{ communityNodeDetails?.description }}
|
{{ communityNodeDetails?.description }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<div :class="$style.separator"></div>
|
<CommunityNodeUpdateInfo
|
||||||
|
v-if="isOwner && installedPackage?.updateAvailable"
|
||||||
|
data-test-id="update-available"
|
||||||
|
/>
|
||||||
|
<div v-else :class="$style.separator"></div>
|
||||||
<div :class="$style.info">
|
<div :class="$style.info">
|
||||||
<N8nTooltip v-if="verified" placement="top">
|
<N8nTooltip v-if="verified" placement="top">
|
||||||
<template #content>{{
|
<template #content>{{
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { N8nNotice } from '@n8n/design-system';
|
||||||
|
import { i18n } from '@n8n/i18n';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const noticeStyles = computed(() => {
|
||||||
|
const isDark = useUIStore().appliedTheme === 'dark';
|
||||||
|
if (isDark) {
|
||||||
|
return {
|
||||||
|
borderColor: 'var(--color-callout-secondary-border)',
|
||||||
|
backgroundColor: 'var(--color-callout-secondary-background)',
|
||||||
|
color: 'var(--color-callout-secondary-font)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
borderColor: 'var(--color-secondary)',
|
||||||
|
backgroundColor: 'var(--color-secondary-tint-3)',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<N8nNotice
|
||||||
|
theme="info"
|
||||||
|
:style="{
|
||||||
|
marginTop: '0',
|
||||||
|
...noticeStyles,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ i18n.baseText('communityNodeUpdateInfo.available') }}
|
||||||
|
</N8nNotice>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module></style>
|
||||||
@@ -96,8 +96,17 @@ export const useCommunityNodesStore = defineStore(STORES.COMMUNITY_NODES, () =>
|
|||||||
updatePackageObject(updatedPackage);
|
updatePackageObject(updatedPackage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getInstalledPackage = async (packageName: string) => {
|
||||||
|
if (!getInstalledPackages.value.length) {
|
||||||
|
await fetchInstalledPackages();
|
||||||
|
}
|
||||||
|
|
||||||
|
return getInstalledPackages.value.find((p) => p.packageName === packageName);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
installedPackages,
|
installedPackages,
|
||||||
|
getInstalledPackage,
|
||||||
getInstalledPackages,
|
getInstalledPackages,
|
||||||
availablePackageCount,
|
availablePackageCount,
|
||||||
fetchAvailableCommunityPackageCount,
|
fetchAvailableCommunityPackageCount,
|
||||||
|
|||||||
Reference in New Issue
Block a user