From 898c25fd7ef8a9eb983e3e6c66093510667ba010 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 15 Nov 2022 14:25:04 +0200 Subject: [PATCH] feat: Add workflow sharing functionality and permissions (#4370) * feat(editor): extract credentials view into reusable layout components for workflows view * feat(editor): add workflow card and start work on empty state * feat: add hoverable card and finish workflows empty state * fix: undo workflows response interface changes * chore: fix linting issues. * fix: remove enterprise sharing env schema * fix(editor): fix workflows resource view when sharing is enabled * fix: change owner tag design and order * feat: add personalization survey on workflows page * fix: update component snapshots * feat: refactored workflow card to use workflow-activator properly * fix: fix workflow activator and proptypes * fix: hide owner tag for workflow card until sharing is available * fix: fixed ownedBy and sharedWith appearing for workflows list * feat: update tags component design * refactor: change resource filter select to n8n-user-select * fix: made telemetry messages reusable * chore: remove unused import * refactor: fix component name casing * refactor: use Vue.set to make workflow property reactive * feat: add support for clicking on tags for filtering * chore: fix tags linting issues * fix: fix resources list layout when title words are very long * refactor: add active and inactive status text to workflow activator * fix: fix credentials and workflows sorting when name contains leading whitespace * fix: remove wrongfully added style tag * feat: add translations and storybook examples for truncated tags * fix: remove enterprise sharing env from schema * refactor: fix workflows module and workflows field store naming conflict * feat: add workflow share button and open dummy modal * feat: add workflow sharing modal (in progress) * feat: add message when sharing disabled * feat: add sharing messages based on flags * feat: add workflow sharing api integration and readonly state handling * fix: change how foreign credentials are handled * refactor: migrate newly added workflow sharing store methods to pinia * fix: update foreign credentials handler and add executable prop to node-settings * fix: fix credentials display issue caused by addCredentials override * fix: fix various issues when sharing from empty state * fix: update node duplication credentials * fix: revert defautl values for sharing env * feat: hide share button behind feature flag * chore: add env variable for sharing feature (testing only) * fix: change enterprise-edition component casing --- packages/cli/src/config/schema.ts | 2 + .../src/components/N8nUsersList/UsersList.vue | 32 ++- packages/editor-ui/src/Interface.ts | 18 +- packages/editor-ui/src/api/credentials.ts | 6 - packages/editor-ui/src/api/workflows.ee.ts | 13 + .../CredentialEdit/CredentialSharing.ee.vue | 2 +- .../components/MainHeader/WorkflowDetails.vue | 15 + packages/editor-ui/src/components/Modals.vue | 8 + .../src/components/NodeCredentials.vue | 3 +- .../src/components/NodeDetailsView.vue | 28 +- .../editor-ui/src/components/NodeSettings.vue | 8 +- .../editor-ui/src/components/WorkflowCard.vue | 8 +- .../src/components/WorkflowShareModal.ee.vue | 265 ++++++++++++++++++ .../src/components/mixins/nodeHelpers.ts | 14 +- .../src/components/mixins/pushConnection.ts | 5 +- .../src/components/mixins/showMessage.ts | 6 +- packages/editor-ui/src/constants.ts | 6 +- .../src/event-bus/node-view-event-bus.ts | 3 + packages/editor-ui/src/permissions.ts | 23 +- .../src/plugins/i18n/locales/en.json | 18 ++ packages/editor-ui/src/stores/credentials.ts | 24 +- packages/editor-ui/src/stores/ui.ts | 16 +- packages/editor-ui/src/stores/users.ts | 4 +- packages/editor-ui/src/stores/workflows.ee.ts | 67 +++++ packages/editor-ui/src/stores/workflows.ts | 2 +- packages/editor-ui/src/views/NodeView.vue | 58 +++- .../editor-ui/src/views/WorkflowsView.vue | 10 +- 27 files changed, 567 insertions(+), 97 deletions(-) create mode 100644 packages/editor-ui/src/api/workflows.ee.ts create mode 100644 packages/editor-ui/src/components/WorkflowShareModal.ee.vue create mode 100644 packages/editor-ui/src/event-bus/node-view-event-bus.ts create mode 100644 packages/editor-ui/src/stores/workflows.ee.ts diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index fbca7d0983..7dfcb76742 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -892,6 +892,7 @@ export const schema = { sharing: { format: Boolean, default: false, + env: 'N8N_SHARING_ENABLED', }, }, // This is a temporary flag (acting as feature toggle) @@ -899,6 +900,7 @@ export const schema = { workflowSharingEnabled: { format: Boolean, default: false, + env: 'N8N_WORKFLOW_SHARING_ENABLED', }, }, diff --git a/packages/design-system/src/components/N8nUsersList/UsersList.vue b/packages/design-system/src/components/N8nUsersList/UsersList.vue index 5cecb62200..605e3e3382 100644 --- a/packages/design-system/src/components/N8nUsersList/UsersList.vue +++ b/packages/design-system/src/components/N8nUsersList/UsersList.vue @@ -15,8 +15,13 @@ > {{ t('nds.auth.roles.owner') }} + t('nds.usersList.reinviteUser'), }, + actions: { + type: Array as PropType, + default: () => ['delete', 'reinvite'], + }, }, computed: { sortedUsers(): IUser[] { @@ -113,6 +123,7 @@ export default mixins(Locale).extend({ }, methods: { getActions(user: IUser): IUserListAction[] { + const actions = []; const DELETE: IUserListAction = { label: this.deleteLabel as string, value: 'delete', @@ -127,16 +138,17 @@ export default mixins(Locale).extend({ return []; } - if (user.firstName) { - return [ - DELETE, - ]; - } else { - return [ - REINVITE, - DELETE, - ]; + if (!user.firstName) { + if (this.actions.includes('reinvite')) { + actions.push(REINVITE); + } } + + if (this.actions.includes('delete')) { + actions.push(DELETE); + } + + return actions; }, onUserAction(user: IUser, action: string): void { if (action === 'delete' || action === 'reinvite') { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 32881943f6..989e1e7586 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -37,6 +37,7 @@ import { NodeParameterValueType, } from 'n8n-workflow'; import { FAKE_DOOR_FEATURES } from './constants'; +import {ICredentialsDb} from "n8n"; export * from 'n8n-design-system/src/types'; @@ -320,6 +321,7 @@ export interface IWorkflowDb { sharedWith?: Array>; ownedBy?: Partial; hash: string; + usedCredentials?: Array>; } // Identical to cli.Interfaces.ts @@ -332,6 +334,14 @@ export interface IWorkflowShortResponse { tags: ITag[]; } +export interface IWorkflowsShareResponse { + id: string; + createdAt: number | string; + updatedAt: number | string; + sharedWith?: Array>; + ownedBy?: Partial; +} + // Identical or almost identical to cli.Interfaces.ts @@ -346,12 +356,17 @@ export interface IShareCredentialsPayload { shareWithIds: string[]; } +export interface IShareWorkflowsPayload { + shareWithIds: string[]; +} + export interface ICredentialsResponse extends ICredentialsEncrypted { id: string; createdAt: number | string; updatedAt: number | string; sharedWith?: Array>; ownedBy?: Partial; + currentUserHasAccess?: boolean; } export interface ICredentialsBase { @@ -987,7 +1002,6 @@ export interface ICredentialMap { export interface ICredentialsState { credentialTypes: ICredentialTypeMap; credentials: ICredentialMap; - foreignCredentials?: ICredentialMap; } export interface ITagsState { @@ -1112,7 +1126,7 @@ export type IFakeDoor = { uiLocations: IFakeDoorLocation[], }; -export type IFakeDoorLocation = 'settings' | 'credentialsModal'; +export type IFakeDoorLocation = 'settings' | 'credentialsModal' | 'workflowShareModal'; export type INodeFilterType = "Regular" | "Trigger" | "All"; diff --git a/packages/editor-ui/src/api/credentials.ts b/packages/editor-ui/src/api/credentials.ts index 9ff6df1c64..60a81e061e 100644 --- a/packages/editor-ui/src/api/credentials.ts +++ b/packages/editor-ui/src/api/credentials.ts @@ -51,9 +51,3 @@ export async function oAuth2CredentialAuthorize(context: IRestApiContext, data: export async function testCredential(context: IRestApiContext, data: INodeCredentialTestRequest): Promise { return makeRestApiRequest(context, 'POST', '/credentials/test', data as unknown as IDataObject); } - -export async function getForeignCredentials(context: IRestApiContext): Promise { - // TODO: Get foreign credentials - //return await makeRestApiRequest(context, 'GET', '/foreign-credentials'); - return []; -} diff --git a/packages/editor-ui/src/api/workflows.ee.ts b/packages/editor-ui/src/api/workflows.ee.ts new file mode 100644 index 0000000000..34ef341b61 --- /dev/null +++ b/packages/editor-ui/src/api/workflows.ee.ts @@ -0,0 +1,13 @@ +import { + IRestApiContext, + IShareWorkflowsPayload, + IWorkflowsShareResponse, +} from '@/Interface'; +import { makeRestApiRequest } from './helpers'; +import { + IDataObject, +} from 'n8n-workflow'; + +export async function setWorkflowSharedWith(context: IRestApiContext, id: string, data: IShareWorkflowsPayload): Promise { + return makeRestApiRequest(context, 'PUT', `/workflows/${id}/share`, data as unknown as IDataObject); +} diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue index 1cd94be649..123c67a00b 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue @@ -73,7 +73,7 @@ export default mixins( }, methods: { async onAddSharee(userId: string) { - const sharee = this.usersStore.getUserById(userId); + const sharee = { ...this.usersStore.getUserById(userId), isOwner: false }; this.$emit('change', (this.credentialData.sharedWith || []).concat(sharee)); }, async onRemoveSharee(userId: string) { diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 38b89bc6b9..7979532acf 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -67,6 +67,15 @@ + + + {{ $locale.baseText('workflowDetails.share') }} + + + + + + @@ -128,6 +132,7 @@ import { VERSIONS_MODAL_KEY, WORKFLOW_ACTIVE_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, + WORKFLOW_SHARE_MODAL_KEY, IMPORT_CURL_MODAL_KEY, } from '@/constants'; @@ -151,6 +156,7 @@ import DeleteUserModal from "./DeleteUserModal.vue"; import ExecutionsList from "./ExecutionsList.vue"; import ActivationModal from "./ActivationModal.vue"; import ImportCurlModal from './ImportCurlModal.vue'; +import WorkflowShareModal from './WorkflowShareModal.ee.vue'; export default Vue.extend({ name: "Modals", @@ -174,6 +180,7 @@ export default Vue.extend({ UpdatesPanel, ValueSurvey, WorkflowSettings, + WorkflowShareModal, ImportCurlModal, }, data: () => ({ @@ -192,6 +199,7 @@ export default Vue.extend({ TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, + WORKFLOW_SHARE_MODAL_KEY, VALUE_SURVEY_MODAL_KEY, EXECUTIONS_MODAL_KEY, WORKFLOW_ACTIVE_MODAL_KEY, diff --git a/packages/editor-ui/src/components/NodeCredentials.vue b/packages/editor-ui/src/components/NodeCredentials.vue index ef4d9bc3c5..b188ef3b56 100644 --- a/packages/editor-ui/src/components/NodeCredentials.vue +++ b/packages/editor-ui/src/components/NodeCredentials.vue @@ -15,7 +15,7 @@ size="small" color="text-dark" > -
+
{ + if (credential.id && foreignCredentials[credential.id] && !foreignCredentials[credential.id].currentUserHasAccess) { + hasForeignCredential = true; + } + }); + } + + return hasForeignCredential; + }, }, watch: { activeNode(node: INodeUi | null) { @@ -384,8 +402,6 @@ export default mixins( nodeSubtitle: this.getNodeSubtitle(node, this.activeNodeType, this.getCurrentWorkflow()), }); - this.checkForeignCredentials(); - setTimeout(() => { if (this.activeNode) { const outgoingConnections = this.workflowsStore.outgoingConnectionsByNodeName( @@ -631,12 +647,6 @@ export default mixins( input_node_type: this.inputNode ? this.inputNode.type : '', }); }, - checkForeignCredentials() { - if(this.activeNode){ - const issues = this.getNodeCredentialIssues(this.activeNode); - this.hasForeignCredential = !!issues?.credentials?.foreign; - } - }, onStopExecution(){ this.$emit('stopExecution'); }, diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index 4d0fa649e9..ae762d2794 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -10,7 +10,7 @@ :isReadOnly="isReadOnly" @input="nameChanged" > -
+
- +
@@ -264,6 +264,10 @@ export default mixins(externalHooks, nodeHelpers).extend({ type: Boolean, default: false, }, + executable: { + type: Boolean, + default: true, + }, }, data() { return { diff --git a/packages/editor-ui/src/components/WorkflowCard.vue b/packages/editor-ui/src/components/WorkflowCard.vue index 82de96c28a..8b1e185efa 100644 --- a/packages/editor-ui/src/components/WorkflowCard.vue +++ b/packages/editor-ui/src/components/WorkflowCard.vue @@ -26,9 +26,9 @@