diff --git a/cypress/e2e/32-worker-view.cy.ts b/cypress/e2e/32-worker-view.cy.ts index 2522fb881..ba3edbe4c 100644 --- a/cypress/e2e/32-worker-view.cy.ts +++ b/cypress/e2e/32-worker-view.cy.ts @@ -1,4 +1,4 @@ -import { INSTANCE_MEMBERS } from '../constants'; +import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants'; import { WorkerViewPage } from '../pages'; const workerViewPage = new WorkerViewPage(); @@ -29,7 +29,7 @@ describe('Worker View (licensed)', () => { }); it('should show up in the menu sidebar', () => { - cy.signin(INSTANCE_MEMBERS[0]); + cy.signin(INSTANCE_OWNER); cy.enableQueueMode(); cy.visit(workerViewPage.url); workerViewPage.getters.menuItem().should('exist'); diff --git a/packages/@n8n/permissions/src/types.ts b/packages/@n8n/permissions/src/types.ts index bbca21c95..6dce8947d 100644 --- a/packages/@n8n/permissions/src/types.ts +++ b/packages/@n8n/permissions/src/types.ts @@ -16,6 +16,7 @@ export type Resource = | 'tag' | 'user' | 'variable' + | 'workersView' | 'workflow'; export type ResourceScope< @@ -50,6 +51,7 @@ export type SourceControlScope = ResourceScope<'sourceControl', 'pull' | 'push' export type TagScope = ResourceScope<'tag'>; export type UserScope = ResourceScope<'user', DefaultOperations | 'resetPassword' | 'changeRole'>; export type VariableScope = ResourceScope<'variable'>; +export type WorkersViewScope = ResourceScope<'workersView', 'manage'>; export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share' | 'execute'>; export type Scope = @@ -69,6 +71,7 @@ export type Scope = | TagScope | UserScope | VariableScope + | WorkersViewScope | WorkflowScope; export type ScopeLevel = 'global' | 'project' | 'resource'; diff --git a/packages/cli/src/permissions/roles.ts b/packages/cli/src/permissions/roles.ts index 44977e0aa..f8619cb88 100644 --- a/packages/cli/src/permissions/roles.ts +++ b/packages/cli/src/permissions/roles.ts @@ -66,6 +66,7 @@ export const ownerPermissions: Scope[] = [ 'workflow:list', 'workflow:share', 'workflow:execute', + 'workersView:manage', ]; export const adminPermissions: Scope[] = ownerPermissions.concat(); export const memberPermissions: Scope[] = [ diff --git a/packages/editor-ui/src/components/SettingsSidebar.vue b/packages/editor-ui/src/components/SettingsSidebar.vue index 0e7412a61..22b003d03 100644 --- a/packages/editor-ui/src/components/SettingsSidebar.vue +++ b/packages/editor-ui/src/components/SettingsSidebar.vue @@ -31,6 +31,7 @@ import type { BaseTextKey } from '@/plugins/i18n'; import { useUIStore } from '@/stores/ui.store'; import { useSettingsStore } from '@/stores/settings.store'; import { useRootStore } from '@/stores/n8nRoot.store'; +import { hasPermission } from '@/rbac/permissions'; export default defineComponent({ name: 'SettingsSidebar', @@ -123,7 +124,8 @@ export default defineComponent({ label: this.$locale.baseText('mainSidebar.workersView'), position: 'top', available: - this.settingsStore.isQueueModeEnabled && this.settingsStore.isWorkerViewAvailable, + this.settingsStore.isQueueModeEnabled && + hasPermission(['rbac'], { rbac: { scope: 'workersView:manage' } }), activateOnRouteNames: [VIEWS.WORKER_VIEW], }, ]; diff --git a/packages/editor-ui/src/components/WorkerList.ee.vue b/packages/editor-ui/src/components/WorkerList.ee.vue index 9d06ecca5..0b496f019 100644 --- a/packages/editor-ui/src/components/WorkerList.ee.vue +++ b/packages/editor-ui/src/components/WorkerList.ee.vue @@ -4,8 +4,8 @@
{{ pageTitle }}
-
- +
+
{{ $locale.baseText('workerList.empty') }}
@@ -55,14 +55,8 @@ export default defineComponent({ ...pushConnection.setup?.(props, ctx), }; }, - data() { - return { - isMounting: true, - }; - }, mounted() { setPageTitle(`n8n - ${this.pageTitle}`); - this.isMounting = false; this.$telemetry.track('User viewed worker view', { instance_id: this.rootStore.instanceId, @@ -91,6 +85,9 @@ export default defineComponent({ } return returnData; }, + initialStatusReceived(): boolean { + return this.orchestrationManagerStore.initialStatusReceived; + }, workerIds(): string[] { return Object.keys(this.orchestrationManagerStore.workers); }, diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index c27c71092..090f68d56 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -628,7 +628,7 @@ "workerList.actionBox.description": "View the current state of workers connected to your instance.", "workerList.actionBox.description.link": "More info", "workerList.actionBox.buttonText": "See plans", - "workerList.docs.url": "https://docs.n8n.io", + "workerList.docs.url": "https://docs.n8n.io/hosting/scaling/queue-mode/#view-running-workers", "executionSidebar.executionName": "Execution {id}", "executionSidebar.searchPlaceholder": "Search executions...", "executionView.onPaste.title": "Cannot paste here", diff --git a/packages/editor-ui/src/stores/orchestration.store.ts b/packages/editor-ui/src/stores/orchestration.store.ts index 618cbc11c..ae4abe011 100644 --- a/packages/editor-ui/src/stores/orchestration.store.ts +++ b/packages/editor-ui/src/stores/orchestration.store.ts @@ -7,6 +7,7 @@ export const WORKER_HISTORY_LENGTH = 100; const STALE_SECONDS = 120 * 1000; export interface IOrchestrationStoreState { + initialStatusReceived: boolean; workers: { [id: string]: IPushDataWorkerStatusPayload }; workersHistory: { [id: string]: IWorkerHistoryItem[]; @@ -22,6 +23,7 @@ export interface IWorkerHistoryItem { export const useOrchestrationStore = defineStore('orchestrationManager', { state: (): IOrchestrationStoreState => ({ + initialStatusReceived: false, workers: {}, workersHistory: {}, workersLastUpdated: {}, @@ -38,6 +40,8 @@ export const useOrchestrationStore = defineStore('orchestrationManager', { this.workersHistory[data.workerId].shift(); } this.workersLastUpdated[data.workerId] = Date.now(); + + this.initialStatusReceived = true; }, removeStaleWorkers() { for (const id in this.workersLastUpdated) {