feat(editor): Show avatars for users currently working on the same workflow (#7763)

This PR introduces the following changes:
- New Vue stores: `collaborationStore` and `pushConnectionStore`
- Front-end push connection handling overhaul: Keep only a singe
connection open and handle it from the new store
- Add user avatars in the editor header when there are multiple users
working on the same workflow
- Sending a heartbeat event to back-end service periodically to confirm
user is still active

- Back-end overhauls (authored by @tomi):
  - Implementing a cleanup procedure that removes inactive users
  - Refactoring collaboration service current implementation

---------

Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
Milorad FIlipović
2023-11-23 10:14:34 +01:00
committed by GitHub
parent 99a9ea497a
commit 77bc8ecd4b
18 changed files with 654 additions and 148 deletions

View File

@@ -1,5 +1,6 @@
import type { Workflow } from 'n8n-workflow';
import { Service } from 'typedi';
import config from '@/config';
import { Push } from '../push';
import { Logger } from '@/Logger';
import type { WorkflowClosedMessage, WorkflowOpenedMessage } from './collaboration.message';
@@ -8,6 +9,13 @@ import { UserService } from '../services/user.service';
import type { IActiveWorkflowUsersChanged } from '../Interfaces';
import type { OnPushMessageEvent } from '@/push/types';
import { CollaborationState } from '@/collaboration/collaboration.state';
import { TIME } from '@/constants';
/**
* After how many minutes of inactivity a user should be removed
* as being an active user of a workflow.
*/
const INACTIVITY_CLEAN_UP_TIME_IN_MS = 15 * TIME.MINUTE;
/**
* Service for managing collaboration feature between users. E.g. keeping
@@ -28,6 +36,14 @@ export class CollaborationService {
return;
}
const isMultiMainSetup = config.get('multiMainSetup.enabled');
if (isMultiMainSetup) {
// TODO: We should support collaboration in multi-main setup as well
// This requires using redis as the state store instead of in-memory
logger.warn('Collaboration features are disabled because multi-main setup is enabled.');
return;
}
this.push.on('message', async (event: OnPushMessageEvent) => {
try {
await this.handleUserMessage(event.userId, event.msg);
@@ -53,6 +69,7 @@ export class CollaborationService {
const { workflowId } = msg;
this.state.addActiveWorkflowUser(workflowId, userId);
this.state.cleanInactiveUsers(workflowId, INACTIVITY_CLEAN_UP_TIME_IN_MS);
await this.sendWorkflowUsersChangedMessage(workflowId);
}

View File

@@ -59,4 +59,21 @@ export class CollaborationState {
return [...workflowState.values()];
}
/**
* Removes all users that have not been seen in a given time
*/
cleanInactiveUsers(workflowId: Workflow['id'], inactivityCleanUpTimeInMs: number) {
const activeUsers = this.state.activeUsersByWorkflowId.get(workflowId);
if (!activeUsers) {
return;
}
const now = Date.now();
for (const user of activeUsers.values()) {
if (now - user.lastSeen.getTime() > inactivityCleanUpTimeInMs) {
activeUsers.delete(user.userId);
}
}
}
}