diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index ff23feb45d..1abeb6d1fb 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -953,6 +953,8 @@ export interface IUsedCredential { name: string; credentialType: string; currentUserHasAccess: boolean; + ownedBy: Partial; + sharedWith: Array>; } export interface WorkflowsState { diff --git a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue index 1c824bb236..c76a2f57fa 100644 --- a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue +++ b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue @@ -62,6 +62,8 @@ import { mapStores } from 'pinia'; import { useSettingsStore } from '@/stores/settings'; import { useWorkflowsStore } from '@/stores/workflows'; import { IWorkflowDataUpdate } from '@/Interface'; +import { getWorkflowPermissions, IPermissions } from '@/permissions'; +import { useUsersStore } from '@/stores/users'; export default mixins(showMessage, workflowHelpers, restApi).extend({ components: { TagsDropdown, Modal }, @@ -85,7 +87,13 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({ this.$nextTick(() => this.focusOnNameInput()); }, computed: { - ...mapStores(useSettingsStore, useWorkflowsStore), + ...mapStores(useUsersStore, useSettingsStore, useWorkflowsStore), + workflowPermissions(): IPermissions { + return getWorkflowPermissions( + this.usersStore.currentUser, + this.workflowsStore.getWorkflowById(this.data.id), + ); + }, }, watch: { isActive(active) { @@ -157,6 +165,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({ this.$telemetry.track('User duplicated workflow', { old_workflow_id: currentWorkflowId, workflow_id: this.data.id, + sharing_role: this.workflowPermissions.isOwner ? 'owner' : 'sharee', }); } } catch (error) { diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 2f927b133d..de0568c139 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -139,7 +139,13 @@ import SaveButton from '@/components/SaveButton.vue'; import TagsDropdown from '@/components/TagsDropdown.vue'; import InlineTextEdit from '@/components/InlineTextEdit.vue'; import BreakpointsObserver from '@/components/BreakpointsObserver.vue'; -import { IWorkflowDataUpdate, IWorkflowDb, IWorkflowToShare, NestedRecord } from '@/Interface'; +import { + IUser, + IWorkflowDataUpdate, + IWorkflowDb, + IWorkflowToShare, + NestedRecord, +} from '@/Interface'; import { saveAs } from 'file-saver'; import { titleChange } from '@/mixins/titleChange'; @@ -194,6 +200,9 @@ export default mixins(workflowHelpers, titleChange).extend({ useWorkflowsStore, useUsersStore, ), + currentUser(): IUser | null { + return this.usersStore.currentUser; + }, dynamicTranslations(): NestedRecord { return this.uiStore.dynamicTranslations; }, @@ -302,6 +311,12 @@ export default mixins(workflowHelpers, titleChange).extend({ name: WORKFLOW_SHARE_MODAL_KEY, data: { id: this.currentWorkflowId }, }); + + this.$telemetry.track('User opened sharing modal', { + workflow_id: this.currentWorkflowId, + user_id_sharer: this.currentUser?.id, + sub_view: this.$route.name === VIEWS.WORKFLOWS ? 'Workflows listing' : 'Workflow editor', + }); }, onTagsEditEnable() { this.$data.appliedTagIds = this.currentWorkflowTagIds; diff --git a/packages/editor-ui/src/components/NodeDetailsView.vue b/packages/editor-ui/src/components/NodeDetailsView.vue index 897eb69509..3c642c61ce 100644 --- a/packages/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/editor-ui/src/components/NodeDetailsView.vue @@ -421,6 +421,7 @@ export default mixins( node_type: this.activeNodeType ? this.activeNodeType.name : '', workflow_id: this.workflowsStore.workflowId, session_id: this.sessionId, + is_editable: !this.hasForeignCredential, parameters_pane_position: this.mainPanelPosition, input_first_connector_runs: this.maxInputRun, output_first_connector_runs: this.maxOutputRun, diff --git a/packages/editor-ui/src/components/WorkflowCard.vue b/packages/editor-ui/src/components/WorkflowCard.vue index 5445a30c8a..20737a4f49 100644 --- a/packages/editor-ui/src/components/WorkflowCard.vue +++ b/packages/editor-ui/src/components/WorkflowCard.vue @@ -206,6 +206,12 @@ export default mixins(showMessage, restApi).extend({ name: WORKFLOW_SHARE_MODAL_KEY, data: { id: this.data.id }, }); + + this.$telemetry.track('User opened sharing modal', { + workflow_id: this.data.id, + user_id_sharer: this.currentUser.id, + sub_view: this.$route.name === VIEWS.WORKFLOWS ? 'Workflows listing' : 'Workflow editor', + }); } else if (action === WORKFLOW_LIST_ITEM_ACTIONS.DELETE) { const deleteConfirmed = await this.confirmMessage( this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.message', { diff --git a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue index 8a3ae6b273..f6987f1729 100644 --- a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue +++ b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue @@ -132,7 +132,8 @@ import { useSettingsStore } from '@/stores/settings'; import { useUIStore } from '@/stores/ui'; import { useUsersStore } from '@/stores/users'; import { useWorkflowsStore } from '@/stores/workflows'; -import useWorkflowsEEStore from '@/stores/workflows.ee'; +import { useWorkflowsEEStore } from '@/stores/workflows.ee'; +import { ITelemetryTrackProperties } from 'n8n-workflow'; export default mixins(showMessage).extend({ name: 'workflow-share-modal', @@ -248,12 +249,26 @@ export default mixins(showMessage).extend({ }; try { + const shareesAdded = this.sharedWith.filter( + (sharee) => + !this.workflow.sharedWith?.find((previousSharee) => sharee.id === previousSharee.id), + ); + const shareesRemoved = + this.workflow.sharedWith?.filter( + (previousSharee) => !this.sharedWith.find((sharee) => sharee.id === previousSharee.id), + ) || []; + const workflowId = await saveWorkflowPromise(); await this.workflowsEEStore.saveWorkflowSharedWith({ workflowId, sharedWith: this.sharedWith, }); + this.trackTelemetry({ + user_ids_sharees_added: shareesAdded.map((sharee) => sharee.id), + sharees_removed: shareesRemoved.length, + }); + this.$showMessage({ title: this.$locale.baseText('workflows.shareModal.onSave.success.title'), type: 'success', @@ -270,6 +285,10 @@ export default mixins(showMessage).extend({ const sharee = { id, firstName, lastName, email }; this.sharedWith = this.sharedWith.concat(sharee); + + this.trackTelemetry({ + user_id_sharee: userId, + }); }, async onRemoveSharee(userId: string) { const user = this.usersStore.getUserById(userId)!; @@ -348,6 +367,11 @@ export default mixins(showMessage).extend({ this.sharedWith = this.sharedWith.filter((sharee: Partial) => { return sharee.id !== user.id; }); + + this.trackTelemetry({ + user_id_sharee: userId, + warning_orphan_credentials: isLastUserWithAccessToCredentials, + }); } }, onRoleAction(user: IUser, action: string) { @@ -379,6 +403,14 @@ export default mixins(showMessage).extend({ this.$router.push({ name: VIEWS.USERS_SETTINGS }); this.modalBus.$emit('close'); }, + trackTelemetry(data: ITelemetryTrackProperties) { + this.$telemetry.track('User selected sharee to remove', { + workflow_id: this.workflow.id, + user_id_sharer: this.currentUser?.id, + sub_view: this.$route.name === VIEWS.WORKFLOWS ? 'Workflows listing' : 'Workflow editor', + ...data, + }); + }, }, mounted() { if (this.isSharingAvailable) { diff --git a/packages/editor-ui/src/mixins/workflowHelpers.ts b/packages/editor-ui/src/mixins/workflowHelpers.ts index c77af046af..ddc41848cf 100644 --- a/packages/editor-ui/src/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/mixins/workflowHelpers.ts @@ -66,8 +66,10 @@ import { IWorkflowSettings } from 'n8n-workflow'; import { useNDVStore } from '@/stores/ndv'; import { useTemplatesStore } from '@/stores/templates'; import { useNodeTypesStore } from '@/stores/nodeTypes'; -import { useWorkflowsEEStore } from "@/stores/workflows.ee"; -import { useUsersStore } from "@/stores/users"; +import { useUsersStore } from '@/stores/users'; +import { useWorkflowsEEStore } from '@/stores/workflows.ee'; +import { ICredentialMap, ICredentialsResponse, IUsedCredential } from '@/Interface'; +import { getWorkflowPermissions, IPermissions } from '@/permissions'; import { ICredentialsResponse } from '@/Interface'; let cachedWorkflowKey: string | null = ''; @@ -85,6 +87,9 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM useUsersStore, useUIStore, ), + workflowPermissions(): IPermissions { + return getWorkflowPermissions(this.usersStore.currentUser, this.workflowsStore.workflow); + }, }, methods: { executeData( @@ -827,7 +832,15 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM this.uiStore.removeActiveAction('workflowSaving'); if (error.errorCode === 100) { - const url = this.$router.resolve({ name: VIEWS.WORKFLOW, params: { name: currentWorkflow }}).href; + this.$telemetry.track('User attempted to save locked workflow', { + workflowId: currentWorkflow, + sharing_role: this.workflowPermissions.isOwner ? 'owner' : 'sharee', + }); + + const url = this.$router.resolve({ + name: VIEWS.WORKFLOW, + params: { name: currentWorkflow }, + }).href; const overwrite = await this.confirmMessage( this.$locale.baseText('workflows.concurrentChanges.confirmMessage.message', {