fix(editor): Hide Evaluations setup wizard if protected instance (#18055)

This commit is contained in:
Mutasem Aldmour
2025-08-07 12:26:00 +02:00
committed by GitHub
parent 8b73b1ce82
commit 99c2f3715e
3 changed files with 38 additions and 2 deletions

View File

@@ -3295,6 +3295,7 @@
"evaluation.executions.toast.addedTo.title": "Execution added to test ", "evaluation.executions.toast.addedTo.title": "Execution added to test ",
"evaluation.executions.toast.closeTab": "Close this tab", "evaluation.executions.toast.closeTab": "Close this tab",
"evaluation.executions.toast.removedFrom.title": "Execution removed from test ", "evaluation.executions.toast.removedFrom.title": "Execution removed from test ",
"evaluations.readOnlyNotice": "Evaluations can't be built in read-only mode. Build your evaluation on your development environment.",
"evaluations.paywall.title": "Register to enable evaluation", "evaluations.paywall.title": "Register to enable evaluation",
"evaluations.paywall.description": "Register your Community instance to unlock the evaluation feature", "evaluations.paywall.description": "Register your Community instance to unlock the evaluation feature",
"evaluations.paywall.cta": "Register instance", "evaluations.paywall.cta": "Register instance",

View File

@@ -9,9 +9,10 @@ import { useToast } from '@/composables/useToast';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { useEvaluationStore } from '@/stores/evaluation.store.ee'; import { useEvaluationStore } from '@/stores/evaluation.store.ee';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { computed, watch } from 'vue'; import { computed, watch } from 'vue';
import { N8nLink, N8nText } from '@n8n/design-system'; import { N8nCallout, N8nLink, N8nText } from '@n8n/design-system';
import EvaluationsPaywall from '@/components/Evaluations.ee/Paywall/EvaluationsPaywall.vue'; import EvaluationsPaywall from '@/components/Evaluations.ee/Paywall/EvaluationsPaywall.vue';
import SetupWizard from '@/components/Evaluations.ee/SetupWizard/SetupWizard.vue'; import SetupWizard from '@/components/Evaluations.ee/SetupWizard/SetupWizard.vue';
@@ -26,6 +27,7 @@ const nodeTypesStore = useNodeTypesStore();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const toast = useToast(); const toast = useToast();
const locale = useI18n(); const locale = useI18n();
const sourceControlStore = useSourceControlStore();
const { initializeWorkspace } = useCanvasOperations(); const { initializeWorkspace } = useCanvasOperations();
@@ -33,6 +35,10 @@ const evaluationsLicensed = computed(() => {
return usageStore.workflowsWithEvaluationsLimit !== 0; return usageStore.workflowsWithEvaluationsLimit !== 0;
}); });
const isProtectedEnvironment = computed(() => {
return sourceControlStore.preferences.branchReadOnly;
});
const runs = computed(() => { const runs = computed(() => {
return Object.values(evaluationStore.testRunsById ?? {}).filter( return Object.values(evaluationStore.testRunsById ?? {}).filter(
({ workflowId }) => workflowId === props.name, ({ workflowId }) => workflowId === props.name,
@@ -142,7 +148,10 @@ watch(
</N8nText> </N8nText>
</div> </div>
<div :class="$style.config"> <N8nCallout v-if="isProtectedEnvironment" theme="info" icon="info">
{{ locale.baseText('evaluations.readOnlyNotice') }}
</N8nCallout>
<div v-else :class="$style.config">
<iframe <iframe
style="min-width: 500px" style="min-width: 500px"
width="500" width="500"

View File

@@ -7,6 +7,7 @@ import EvaluationRootView from '../EvaluationsRootView.vue';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useEvaluationStore } from '@/stores/evaluation.store.ee'; import { useEvaluationStore } from '@/stores/evaluation.store.ee';
import { useUsageStore } from '@/stores/usage.store'; import { useUsageStore } from '@/stores/usage.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { mockedStore } from '@/__tests__/utils'; import { mockedStore } from '@/__tests__/utils';
import type { IWorkflowDb } from '@/Interface'; import type { IWorkflowDb } from '@/Interface';
import { waitFor } from '@testing-library/vue'; import { waitFor } from '@testing-library/vue';
@@ -15,6 +16,7 @@ import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { EVALUATION_NODE_TYPE, EVALUATION_TRIGGER_NODE_TYPE, NodeHelpers } from 'n8n-workflow'; import { EVALUATION_NODE_TYPE, EVALUATION_TRIGGER_NODE_TYPE, NodeHelpers } from 'n8n-workflow';
import { mockNodeTypeDescription } from '@/__tests__/mocks'; import { mockNodeTypeDescription } from '@/__tests__/mocks';
import type { SourceControlPreferences } from '@/types/sourceControl.types';
vi.mock('@/composables/useTelemetry', () => { vi.mock('@/composables/useTelemetry', () => {
const track = vi.fn(); const track = vi.fn();
@@ -32,6 +34,15 @@ vi.mock('@/stores/nodeTypes.store', () => ({
})), })),
})); }));
vi.mock('@n8n/i18n', async (importOriginal) => {
return {
...(await importOriginal()),
useI18n: () => ({
baseText: vi.fn((key: string) => `mocked-${key}`),
}),
};
});
describe('EvaluationsRootView', () => { describe('EvaluationsRootView', () => {
const renderComponent = createComponentRenderer(EvaluationRootView); const renderComponent = createComponentRenderer(EvaluationRootView);
@@ -127,6 +138,21 @@ describe('EvaluationsRootView', () => {
await waitFor(() => expect(container.querySelector('.setupContent')).toBeTruthy()); await waitFor(() => expect(container.querySelector('.setupContent')).toBeTruthy());
}); });
it('should render read-only callout when in protected environment', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const sourceControlStore = mockedStore(useSourceControlStore);
workflowsStore.fetchWorkflow.mockResolvedValue(mockWorkflow);
sourceControlStore.preferences = mock<SourceControlPreferences>({ branchReadOnly: true });
const { container } = renderComponent({ props: { name: mockWorkflow.id } });
await waitFor(() => {
const callout = container.querySelector('[role="alert"]');
expect(callout).toBeTruthy();
expect(callout?.textContent).toContain('mocked-evaluations.readOnlyNotice');
});
});
describe('telemetry', () => { describe('telemetry', () => {
it('should send telemetry event on mount with setup view when no test runs exist', async () => { it('should send telemetry event on mount with setup view when no test runs exist', async () => {
const workflowsStore = mockedStore(useWorkflowsStore); const workflowsStore = mockedStore(useWorkflowsStore);