feat(editor): Finalize workers view (#8052)

https://linear.app/n8n/issue/PAY-1065
This commit is contained in:
Iván Ovejero 2023-12-20 17:49:14 +01:00 committed by GitHub
parent d917dfe9f8
commit edfa78414d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 19 additions and 12 deletions

View file

@ -1,4 +1,4 @@
import { INSTANCE_MEMBERS } from '../constants'; import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants';
import { WorkerViewPage } from '../pages'; import { WorkerViewPage } from '../pages';
const workerViewPage = new WorkerViewPage(); const workerViewPage = new WorkerViewPage();
@ -29,7 +29,7 @@ describe('Worker View (licensed)', () => {
}); });
it('should show up in the menu sidebar', () => { it('should show up in the menu sidebar', () => {
cy.signin(INSTANCE_MEMBERS[0]); cy.signin(INSTANCE_OWNER);
cy.enableQueueMode(); cy.enableQueueMode();
cy.visit(workerViewPage.url); cy.visit(workerViewPage.url);
workerViewPage.getters.menuItem().should('exist'); workerViewPage.getters.menuItem().should('exist');

View file

@ -16,6 +16,7 @@ export type Resource =
| 'tag' | 'tag'
| 'user' | 'user'
| 'variable' | 'variable'
| 'workersView'
| 'workflow'; | 'workflow';
export type ResourceScope< export type ResourceScope<
@ -50,6 +51,7 @@ export type SourceControlScope = ResourceScope<'sourceControl', 'pull' | 'push'
export type TagScope = ResourceScope<'tag'>; export type TagScope = ResourceScope<'tag'>;
export type UserScope = ResourceScope<'user', DefaultOperations | 'resetPassword' | 'changeRole'>; export type UserScope = ResourceScope<'user', DefaultOperations | 'resetPassword' | 'changeRole'>;
export type VariableScope = ResourceScope<'variable'>; export type VariableScope = ResourceScope<'variable'>;
export type WorkersViewScope = ResourceScope<'workersView', 'manage'>;
export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share' | 'execute'>; export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share' | 'execute'>;
export type Scope = export type Scope =
@ -69,6 +71,7 @@ export type Scope =
| TagScope | TagScope
| UserScope | UserScope
| VariableScope | VariableScope
| WorkersViewScope
| WorkflowScope; | WorkflowScope;
export type ScopeLevel = 'global' | 'project' | 'resource'; export type ScopeLevel = 'global' | 'project' | 'resource';

View file

@ -66,6 +66,7 @@ export const ownerPermissions: Scope[] = [
'workflow:list', 'workflow:list',
'workflow:share', 'workflow:share',
'workflow:execute', 'workflow:execute',
'workersView:manage',
]; ];
export const adminPermissions: Scope[] = ownerPermissions.concat(); export const adminPermissions: Scope[] = ownerPermissions.concat();
export const memberPermissions: Scope[] = [ export const memberPermissions: Scope[] = [

View file

@ -31,6 +31,7 @@ import type { BaseTextKey } from '@/plugins/i18n';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@/stores/n8nRoot.store'; import { useRootStore } from '@/stores/n8nRoot.store';
import { hasPermission } from '@/rbac/permissions';
export default defineComponent({ export default defineComponent({
name: 'SettingsSidebar', name: 'SettingsSidebar',
@ -123,7 +124,8 @@ export default defineComponent({
label: this.$locale.baseText('mainSidebar.workersView'), label: this.$locale.baseText('mainSidebar.workersView'),
position: 'top', position: 'top',
available: available:
this.settingsStore.isQueueModeEnabled && this.settingsStore.isWorkerViewAvailable, this.settingsStore.isQueueModeEnabled &&
hasPermission(['rbac'], { rbac: { scope: 'workersView:manage' } }),
activateOnRouteNames: [VIEWS.WORKER_VIEW], activateOnRouteNames: [VIEWS.WORKER_VIEW],
}, },
]; ];

View file

@ -4,8 +4,8 @@
<div :class="$style.workerListHeader"> <div :class="$style.workerListHeader">
<n8n-heading tag="h1" size="2xlarge">{{ pageTitle }}</n8n-heading> <n8n-heading tag="h1" size="2xlarge">{{ pageTitle }}</n8n-heading>
</div> </div>
<div v-if="isMounting"> <div v-if="!initialStatusReceived">
<n8n-loading :class="$style.tableLoader" variant="custom" /> <n8n-spinner />
</div> </div>
<div v-else> <div v-else>
<div v-if="workerIds.length === 0">{{ $locale.baseText('workerList.empty') }}</div> <div v-if="workerIds.length === 0">{{ $locale.baseText('workerList.empty') }}</div>
@ -55,14 +55,8 @@ export default defineComponent({
...pushConnection.setup?.(props, ctx), ...pushConnection.setup?.(props, ctx),
}; };
}, },
data() {
return {
isMounting: true,
};
},
mounted() { mounted() {
setPageTitle(`n8n - ${this.pageTitle}`); setPageTitle(`n8n - ${this.pageTitle}`);
this.isMounting = false;
this.$telemetry.track('User viewed worker view', { this.$telemetry.track('User viewed worker view', {
instance_id: this.rootStore.instanceId, instance_id: this.rootStore.instanceId,
@ -91,6 +85,9 @@ export default defineComponent({
} }
return returnData; return returnData;
}, },
initialStatusReceived(): boolean {
return this.orchestrationManagerStore.initialStatusReceived;
},
workerIds(): string[] { workerIds(): string[] {
return Object.keys(this.orchestrationManagerStore.workers); return Object.keys(this.orchestrationManagerStore.workers);
}, },

View file

@ -628,7 +628,7 @@
"workerList.actionBox.description": "View the current state of workers connected to your instance.", "workerList.actionBox.description": "View the current state of workers connected to your instance.",
"workerList.actionBox.description.link": "More info", "workerList.actionBox.description.link": "More info",
"workerList.actionBox.buttonText": "See plans", "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.executionName": "Execution {id}",
"executionSidebar.searchPlaceholder": "Search executions...", "executionSidebar.searchPlaceholder": "Search executions...",
"executionView.onPaste.title": "Cannot paste here", "executionView.onPaste.title": "Cannot paste here",

View file

@ -7,6 +7,7 @@ export const WORKER_HISTORY_LENGTH = 100;
const STALE_SECONDS = 120 * 1000; const STALE_SECONDS = 120 * 1000;
export interface IOrchestrationStoreState { export interface IOrchestrationStoreState {
initialStatusReceived: boolean;
workers: { [id: string]: IPushDataWorkerStatusPayload }; workers: { [id: string]: IPushDataWorkerStatusPayload };
workersHistory: { workersHistory: {
[id: string]: IWorkerHistoryItem[]; [id: string]: IWorkerHistoryItem[];
@ -22,6 +23,7 @@ export interface IWorkerHistoryItem {
export const useOrchestrationStore = defineStore('orchestrationManager', { export const useOrchestrationStore = defineStore('orchestrationManager', {
state: (): IOrchestrationStoreState => ({ state: (): IOrchestrationStoreState => ({
initialStatusReceived: false,
workers: {}, workers: {},
workersHistory: {}, workersHistory: {},
workersLastUpdated: {}, workersLastUpdated: {},
@ -38,6 +40,8 @@ export const useOrchestrationStore = defineStore('orchestrationManager', {
this.workersHistory[data.workerId].shift(); this.workersHistory[data.workerId].shift();
} }
this.workersLastUpdated[data.workerId] = Date.now(); this.workersLastUpdated[data.workerId] = Date.now();
this.initialStatusReceived = true;
}, },
removeStaleWorkers() { removeStaleWorkers() {
for (const id in this.workersLastUpdated) { for (const id in this.workersLastUpdated) {