mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-28 17:22:01 +08:00
feat(editor): Surface cluster information in debug data (no-changelog) (#29583)
This commit is contained in:
@@ -37,7 +37,7 @@ jobs:
|
||||
uses: ./.github/workflows/test-e2e-reusable.yml
|
||||
with:
|
||||
test-mode: docker-artifact
|
||||
test-command: pnpm --filter=n8n-playwright test:all --project='${{ matrix.profile }}:infrastructure' --workers=1
|
||||
test-command: pnpm --filter=n8n-playwright test:all --project=${{ matrix.profile }}:infrastructure --workers=1
|
||||
runner: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
secrets: inherit
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ClusterCheckSummary, ClusterInfoResponse } from '@n8n/api-types';
|
||||
import { Get, GlobalScope, RestController } from '@n8n/decorators';
|
||||
import { Get, RestController } from '@n8n/decorators';
|
||||
|
||||
import { CheckService } from './checks/check.service';
|
||||
import { InstanceRegistryService } from './instance-registry.service';
|
||||
@@ -12,7 +12,6 @@ export class InstanceRegistryController {
|
||||
) {}
|
||||
|
||||
@Get('/')
|
||||
@GlobalScope('orchestration:read')
|
||||
async getClusterInfo(): Promise<ClusterInfoResponse> {
|
||||
const [instances, { results }] = await Promise.all([
|
||||
this.instanceRegistryService.getAllInstances(),
|
||||
|
||||
@@ -2,10 +2,6 @@ import type { ModuleInterface } from '@n8n/decorators';
|
||||
import { BackendModule, OnShutdown } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
function isFeatureFlagEnabled(): boolean {
|
||||
return process.env.N8N_ENV_FEAT_INSTANCE_REGISTRY === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance Registry Module
|
||||
*
|
||||
@@ -17,10 +13,6 @@ function isFeatureFlagEnabled(): boolean {
|
||||
@BackendModule({ name: 'instance-registry' })
|
||||
export class InstanceRegistryModule implements ModuleInterface {
|
||||
async init() {
|
||||
if (!isFeatureFlagEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await import('./instance-registry.controller');
|
||||
|
||||
const { InstanceRegistryService } = await import('./instance-registry.service');
|
||||
@@ -42,10 +34,6 @@ export class InstanceRegistryModule implements ModuleInterface {
|
||||
|
||||
@OnShutdown()
|
||||
async shutdown() {
|
||||
if (!isFeatureFlagEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { InstanceRegistryService } = await import('./instance-registry.service');
|
||||
await Container.get(InstanceRegistryService).shutdown();
|
||||
}
|
||||
|
||||
@@ -4267,9 +4267,6 @@
|
||||
"dataTable.search.dateSearchInfo": "Date searches use UTC format, while the table displays dates in your local timezone",
|
||||
"dataTable.cell.oversized": "Value too large to display",
|
||||
"dataTable.cell.oversized.tooltip": "The value can be modified using the data table node",
|
||||
"settings.instanceRegistry": "Instance Registry",
|
||||
"settings.instanceRegistry.title": "Instance Registry",
|
||||
"settings.instanceRegistry.error": "Failed to load cluster information. Please try again later.",
|
||||
"settings.ldap": "LDAP",
|
||||
"settings.ldap.note": "LDAP allows users to authenticate with their centralized account. It's compatible with services that provide an LDAP interface like Active Directory, Okta and Jumpcloud.",
|
||||
"settings.ldap.infoTip": "Learn more about <a href='https://docs.n8n.io/user-management/ldap/' target='_blank'>LDAP in the Docs</a>",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import Modal from './Modal.vue';
|
||||
import { ABOUT_MODAL_KEY } from '../constants';
|
||||
@@ -6,6 +7,7 @@ import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import { useClipboard } from '@/app/composables/useClipboard';
|
||||
import { useDebugInfo } from '@/app/composables/useDebugInfo';
|
||||
import { useInstanceRegistryStore } from '@/features/instanceRegistry/stores/instanceRegistry.store';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { getThirdPartyLicenses } from '@n8n/rest-api-client';
|
||||
|
||||
@@ -17,6 +19,11 @@ const i18n = useI18n();
|
||||
const debugInfo = useDebugInfo();
|
||||
const clipboard = useClipboard();
|
||||
const rootStore = useRootStore();
|
||||
const instanceRegistryStore = useInstanceRegistryStore();
|
||||
|
||||
onMounted(async () => {
|
||||
await instanceRegistryStore.fetchClusterInfo();
|
||||
});
|
||||
|
||||
const closeDialog = () => {
|
||||
modalBus.emit('close');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import type { ClusterInfoResponse } from '@n8n/api-types';
|
||||
import { useDebugInfo } from './useDebugInfo';
|
||||
import type { RootStoreState } from '@n8n/stores/useRootStore';
|
||||
import type { useSettingsStore as useSettingsStoreType } from '@/app/stores/settings.store';
|
||||
@@ -57,6 +58,22 @@ vi.mock('@n8n/composables/useDeviceSupport', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const { mockClusterInfo } = vi.hoisted(() => ({
|
||||
mockClusterInfo: { value: null as ClusterInfoResponse | null },
|
||||
}));
|
||||
|
||||
vi.mock('@/features/instanceRegistry/stores/instanceRegistry.store', () => ({
|
||||
useInstanceRegistryStore: () => ({
|
||||
get clusterInfo() {
|
||||
return mockClusterInfo.value;
|
||||
},
|
||||
get isAvailable() {
|
||||
return mockClusterInfo.value !== null;
|
||||
},
|
||||
fetchClusterInfo: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const NOW = 1717602004819;
|
||||
|
||||
vi.useFakeTimers({
|
||||
@@ -66,6 +83,7 @@ vi.useFakeTimers({
|
||||
describe('useDebugInfo', () => {
|
||||
beforeEach(() => {
|
||||
useSettingsStore.mockReturnValue(MOCK_BASE_SETTINGS);
|
||||
mockClusterInfo.value = null;
|
||||
});
|
||||
|
||||
it('should generate debug info', () => {
|
||||
@@ -115,4 +133,96 @@ describe('useDebugInfo', () => {
|
||||
expect(debugInfo).toContain('### core');
|
||||
expect(debugInfo).toContain('## Debug info');
|
||||
});
|
||||
|
||||
it('should not include cluster section when registry has no snapshot', () => {
|
||||
const { generateDebugInfo } = useDebugInfo();
|
||||
const debugInfo = generateDebugInfo();
|
||||
|
||||
expect(debugInfo).not.toContain('## cluster');
|
||||
});
|
||||
|
||||
it('should include cluster section when registry has data', () => {
|
||||
mockClusterInfo.value = {
|
||||
instances: [
|
||||
{
|
||||
schemaVersion: 1,
|
||||
instanceKey: 'main-1',
|
||||
hostId: 'host-a',
|
||||
instanceType: 'main',
|
||||
instanceRole: 'leader',
|
||||
version: '1.110.0',
|
||||
registeredAt: 0,
|
||||
lastSeen: 0,
|
||||
},
|
||||
{
|
||||
schemaVersion: 1,
|
||||
instanceKey: 'worker-1',
|
||||
hostId: 'host-b',
|
||||
instanceType: 'worker',
|
||||
instanceRole: 'follower',
|
||||
version: '1.111.0',
|
||||
registeredAt: 0,
|
||||
lastSeen: 0,
|
||||
},
|
||||
],
|
||||
checks: {
|
||||
'version-mismatch': {
|
||||
check: 'version-mismatch',
|
||||
executedAt: 0,
|
||||
status: 'failed',
|
||||
warnings: [
|
||||
{
|
||||
check: 'version-mismatch',
|
||||
code: 'cluster.version-mismatch',
|
||||
message: 'Detected multiple n8n versions in the cluster: 1.110.0, 1.111.0',
|
||||
severity: 'error',
|
||||
},
|
||||
],
|
||||
},
|
||||
'hostid-clash': {
|
||||
check: 'hostid-clash',
|
||||
executedAt: 0,
|
||||
status: 'succeeded',
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { generateDebugInfo } = useDebugInfo();
|
||||
const debugInfo = generateDebugInfo();
|
||||
|
||||
expect(debugInfo).toContain('## cluster');
|
||||
expect(debugInfo).toContain('instanceCount: 2');
|
||||
expect(debugInfo).toContain('versions: 1.110.0, 1.111.0');
|
||||
expect(debugInfo).toContain('instanceKey: main-1');
|
||||
expect(debugInfo).toContain('instanceKey: worker-1');
|
||||
expect(debugInfo).toContain('check: hostid-clash, status: succeeded, warnings: -');
|
||||
expect(debugInfo).toContain(
|
||||
'check: version-mismatch, status: failed, warnings: cluster.version-mismatch',
|
||||
);
|
||||
});
|
||||
|
||||
it('should include cluster section even when skipSensitive is true', () => {
|
||||
mockClusterInfo.value = {
|
||||
instances: [
|
||||
{
|
||||
schemaVersion: 1,
|
||||
instanceKey: 'main-1',
|
||||
hostId: 'host-a',
|
||||
instanceType: 'main',
|
||||
instanceRole: 'leader',
|
||||
version: '1.110.0',
|
||||
registeredAt: 0,
|
||||
lastSeen: 0,
|
||||
},
|
||||
],
|
||||
checks: {},
|
||||
};
|
||||
|
||||
const { generateDebugInfo } = useDebugInfo();
|
||||
const debugInfo = generateDebugInfo({ skipSensitive: true });
|
||||
|
||||
expect(debugInfo).toContain('## cluster');
|
||||
expect(debugInfo).toContain('instanceCount: 1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { useInstanceRegistryStore } from '@/features/instanceRegistry/stores/instanceRegistry.store';
|
||||
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
|
||||
import type { WorkflowSettings } from 'n8n-workflow';
|
||||
|
||||
type ClusterInstanceSummary = {
|
||||
instanceKey: string;
|
||||
hostId: string;
|
||||
instanceType: 'main' | 'worker' | 'webhook';
|
||||
instanceRole: 'leader' | 'follower' | 'unset';
|
||||
version: string;
|
||||
};
|
||||
|
||||
type ClusterCheckSummary = {
|
||||
check: string;
|
||||
status: 'succeeded' | 'failed';
|
||||
warnings: string;
|
||||
};
|
||||
|
||||
type DebugInfo = {
|
||||
core: {
|
||||
n8nVersion: string;
|
||||
@@ -42,11 +57,22 @@ type DebugInfo = {
|
||||
userAgent: string;
|
||||
isTouchDevice: boolean;
|
||||
};
|
||||
/**
|
||||
* Reported only when the instance registry has a snapshot loaded.
|
||||
* Contains no PII — instance keys, hostIds, versions, and cluster role.
|
||||
*/
|
||||
cluster?: {
|
||||
instanceCount: number;
|
||||
versions: string;
|
||||
instances: ClusterInstanceSummary[];
|
||||
checks: ClusterCheckSummary[];
|
||||
};
|
||||
};
|
||||
|
||||
export function useDebugInfo() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const rootStore = useRootStore();
|
||||
const instanceRegistryStore = useInstanceRegistryStore();
|
||||
const { isTouchDevice, userAgent } = useDeviceSupport();
|
||||
|
||||
const coreInfo = (skipSensitive?: boolean) => {
|
||||
@@ -125,6 +151,36 @@ export function useDebugInfo() {
|
||||
};
|
||||
};
|
||||
|
||||
const clusterInfo = (): DebugInfo['cluster'] => {
|
||||
const snapshot = instanceRegistryStore.clusterInfo;
|
||||
if (!snapshot) return;
|
||||
|
||||
const instances: ClusterInstanceSummary[] = snapshot.instances.map((i) => ({
|
||||
instanceKey: i.instanceKey,
|
||||
hostId: i.hostId,
|
||||
instanceType: i.instanceType,
|
||||
instanceRole: i.instanceRole,
|
||||
version: i.version,
|
||||
}));
|
||||
|
||||
const versions = [...new Set(instances.map((i) => i.version))].sort();
|
||||
|
||||
const checks: ClusterCheckSummary[] = Object.values(snapshot.checks)
|
||||
.map((c) => ({
|
||||
check: c.check,
|
||||
status: c.status,
|
||||
warnings: c.warnings.length > 0 ? c.warnings.map((w) => w.code).join('; ') : '-',
|
||||
}))
|
||||
.sort((a, b) => a.check.localeCompare(b.check));
|
||||
|
||||
return {
|
||||
instanceCount: instances.length,
|
||||
versions: versions.join(', '),
|
||||
instances,
|
||||
checks,
|
||||
};
|
||||
};
|
||||
|
||||
const gatherDebugInfo = (skipSensitive?: boolean) => {
|
||||
const debugInfo: DebugInfo = {
|
||||
core: coreInfo(skipSensitive),
|
||||
@@ -134,9 +190,11 @@ export function useDebugInfo() {
|
||||
};
|
||||
|
||||
const security = securityInfo();
|
||||
|
||||
if (security) debugInfo.security = security;
|
||||
|
||||
const cluster = clusterInfo();
|
||||
if (cluster) debugInfo.cluster = cluster;
|
||||
|
||||
return debugInfo;
|
||||
};
|
||||
|
||||
@@ -154,9 +212,16 @@ export function useDebugInfo() {
|
||||
|
||||
if (!section) continue;
|
||||
|
||||
for (const itemKey in section) {
|
||||
const itemValue = section[itemKey as keyof typeof section];
|
||||
markdown += `- ${itemKey}: ${itemValue}\n`;
|
||||
for (const [itemKey, itemValue] of Object.entries(section as Record<string, unknown>)) {
|
||||
if (Array.isArray(itemValue)) {
|
||||
markdown += `- ${itemKey}:\n`;
|
||||
for (const entry of itemValue as Array<Record<string, unknown>>) {
|
||||
const parts = Object.entries(entry).map(([k, v]) => `${k}: ${v}`);
|
||||
markdown += ` - ${parts.join(', ')}\n`;
|
||||
}
|
||||
} else {
|
||||
markdown += `- ${itemKey}: ${itemValue}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
markdown += '\n';
|
||||
|
||||
@@ -145,16 +145,6 @@ export function useSettingsItems() {
|
||||
available: canUserAccessRouteByName(VIEWS.LDAP_SETTINGS),
|
||||
route: { to: { name: VIEWS.LDAP_SETTINGS } },
|
||||
},
|
||||
{
|
||||
id: 'settings-instance-registry',
|
||||
icon: 'server',
|
||||
label: i18n.baseText('settings.instanceRegistry'),
|
||||
position: 'top',
|
||||
available:
|
||||
envFeatureFlagCheck.value('INSTANCE_REGISTRY') &&
|
||||
canUserAccessRouteByName(VIEWS.INSTANCE_REGISTRY),
|
||||
route: { to: { name: VIEWS.INSTANCE_REGISTRY } },
|
||||
},
|
||||
{
|
||||
id: 'settings-workersview',
|
||||
icon: 'waypoints',
|
||||
|
||||
@@ -71,7 +71,6 @@ export const enum VIEWS {
|
||||
MIGRATION_REPORT = 'MigrationReport',
|
||||
MIGRATION_RULE_REPORT = 'MigrationRuleReport',
|
||||
RESOLVERS = 'Resolvers',
|
||||
INSTANCE_REGISTRY = 'InstanceRegistryView',
|
||||
RESOURCE_CENTER = 'ResourceCenter',
|
||||
}
|
||||
|
||||
|
||||
@@ -93,8 +93,6 @@ const SettingsExternalSecrets = async () => {
|
||||
};
|
||||
const WorkerView = async () =>
|
||||
await import('@/features/settings/orchestration.ee/views/WorkerView.vue');
|
||||
const SettingsInstanceRegistryView = async () =>
|
||||
await import('@/features/settings/instanceRegistry/views/SettingsInstanceRegistryView.vue');
|
||||
const WorkflowHistory = async () =>
|
||||
await import('@/features/workflows/workflowHistory/views/WorkflowHistory.vue');
|
||||
const WorkflowOnboardingView = async () => await import('@/app/views/WorkflowOnboardingView.vue');
|
||||
@@ -969,31 +967,6 @@ export const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'instance-registry',
|
||||
name: VIEWS.INSTANCE_REGISTRY,
|
||||
component: SettingsInstanceRegistryView,
|
||||
meta: {
|
||||
middleware: ['authenticated', 'rbac', 'custom'],
|
||||
middlewareOptions: {
|
||||
rbac: {
|
||||
scope: 'orchestration:read',
|
||||
},
|
||||
custom: () => {
|
||||
const { check } = useEnvFeatureFlag();
|
||||
return check.value('INSTANCE_REGISTRY');
|
||||
},
|
||||
},
|
||||
telemetry: {
|
||||
pageCategory: 'settings',
|
||||
getProperties() {
|
||||
return {
|
||||
feature: 'instance-registry',
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'workers',
|
||||
name: VIEWS.WORKER_VIEW,
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import type { ClusterInfoResponse } from '@n8n/api-types';
|
||||
import { useInstanceRegistryStore } from '../instanceRegistry.store';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getClusterInfo: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/rest-api-client/api/instance-registry', () => ({
|
||||
getClusterInfo: mocks.getClusterInfo,
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/stores/useRootStore', () => ({
|
||||
useRootStore: () => ({
|
||||
restApiContext: { baseUrl: 'http://localhost', sessionId: 'test' },
|
||||
}),
|
||||
}));
|
||||
|
||||
const SAMPLE_RESPONSE: ClusterInfoResponse = {
|
||||
instances: [
|
||||
{
|
||||
schemaVersion: 1,
|
||||
instanceKey: 'main-1',
|
||||
hostId: 'host-a',
|
||||
instanceType: 'main',
|
||||
instanceRole: 'leader',
|
||||
version: '1.110.0',
|
||||
registeredAt: 0,
|
||||
lastSeen: 0,
|
||||
},
|
||||
],
|
||||
checks: {},
|
||||
};
|
||||
|
||||
describe('useInstanceRegistryStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
mocks.getClusterInfo.mockReset();
|
||||
});
|
||||
|
||||
it('fetches and stores cluster info', async () => {
|
||||
mocks.getClusterInfo.mockResolvedValue(SAMPLE_RESPONSE);
|
||||
|
||||
const store = useInstanceRegistryStore();
|
||||
await store.fetchClusterInfo();
|
||||
|
||||
expect(mocks.getClusterInfo).toHaveBeenCalledTimes(1);
|
||||
expect(store.clusterInfo).toEqual(SAMPLE_RESPONSE);
|
||||
expect(store.isAvailable).toBe(true);
|
||||
});
|
||||
|
||||
it('swallows errors and preserves the prior snapshot', async () => {
|
||||
mocks.getClusterInfo.mockResolvedValueOnce(SAMPLE_RESPONSE);
|
||||
mocks.getClusterInfo.mockRejectedValueOnce(new Error('endpoint failure'));
|
||||
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
|
||||
const store = useInstanceRegistryStore();
|
||||
await store.fetchClusterInfo();
|
||||
await store.fetchClusterInfo();
|
||||
|
||||
expect(store.clusterInfo).toEqual(SAMPLE_RESPONSE);
|
||||
expect(debugSpy).toHaveBeenCalled();
|
||||
debugSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import type { ClusterInfoResponse } from '@n8n/api-types';
|
||||
import * as instanceRegistryApi from '@n8n/rest-api-client/api/instance-registry';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
export const useInstanceRegistryStore = defineStore('instanceRegistry', () => {
|
||||
const rootStore = useRootStore();
|
||||
|
||||
const clusterInfo = ref<ClusterInfoResponse | null>(null);
|
||||
|
||||
const isAvailable = computed(() => clusterInfo.value !== null);
|
||||
|
||||
async function fetchClusterInfo(): Promise<void> {
|
||||
try {
|
||||
clusterInfo.value = await instanceRegistryApi.getClusterInfo(rootStore.restApiContext);
|
||||
} catch (error) {
|
||||
// Leave the previous snapshot in place on transient network errors — debug
|
||||
// generation must never fail because cluster info couldn't be fetched.
|
||||
console.debug('Failed to fetch instance registry cluster info', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
clusterInfo,
|
||||
isAvailable,
|
||||
fetchClusterInfo,
|
||||
};
|
||||
});
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { ClusterInfoResponse } from '@n8n/api-types';
|
||||
import * as instanceRegistryApi from '@n8n/rest-api-client/api/instance-registry';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { N8nHeading, N8nLoading } from '@n8n/design-system';
|
||||
|
||||
const i18n = useI18n();
|
||||
const documentTitle = useDocumentTitle();
|
||||
const rootStore = useRootStore();
|
||||
|
||||
const loading = ref(true);
|
||||
const clusterInfo = ref<ClusterInfoResponse | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
documentTitle.set(i18n.baseText('settings.instanceRegistry.title'));
|
||||
|
||||
try {
|
||||
clusterInfo.value = await instanceRegistryApi.getClusterInfo(rootStore.restApiContext);
|
||||
} catch (e) {
|
||||
console.error('Failed to load instance registry', e);
|
||||
error.value = i18n.baseText('settings.instanceRegistry.error');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<N8nHeading size="2xlarge" :class="$style.heading">
|
||||
{{ i18n.baseText('settings.instanceRegistry.title') }}
|
||||
</N8nHeading>
|
||||
|
||||
<N8nLoading v-if="loading" :rows="4" />
|
||||
|
||||
<div v-else-if="error" :class="$style.error">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<pre v-else :class="$style.json">{{ JSON.stringify(clusterInfo, null, 2) }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.container {
|
||||
padding: var(--spacing--lg);
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-bottom: var(--spacing--lg);
|
||||
}
|
||||
|
||||
.json {
|
||||
background-color: var(--color--background--shade-1);
|
||||
border: var(--border);
|
||||
border-radius: var(--radius--lg);
|
||||
padding: var(--spacing--sm);
|
||||
overflow: auto;
|
||||
font-size: var(--font-size--2xs);
|
||||
line-height: var(--line-height--xl);
|
||||
color: var(--color--text);
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--color--text--danger);
|
||||
padding: var(--spacing--sm);
|
||||
}
|
||||
</style>
|
||||
@@ -276,7 +276,6 @@ export class ApiHelpers {
|
||||
|
||||
/**
|
||||
* Fetch cluster info from the instance registry endpoint.
|
||||
* Requires `N8N_ENV_FEAT_INSTANCE_REGISTRY=true` on every container.
|
||||
*/
|
||||
async getClusterInfo(): Promise<ClusterInfoResponse> {
|
||||
const response = await this.request.get('/rest/instance-registry');
|
||||
|
||||
-1
@@ -20,7 +20,6 @@ test.use({
|
||||
capability: {
|
||||
mains: 2,
|
||||
workers: 1,
|
||||
env: { N8N_ENV_FEAT_INSTANCE_REGISTRY: 'true' },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user