Files
n8n-enterprise-unlocked/packages/frontend/editor-ui/src/components/WorkflowActivator.vue

294 lines
8.4 KiB
Vue

<script lang="ts" setup>
import { useToast } from '@/composables/useToast';
import { useWorkflowActivate } from '@/composables/useWorkflowActivate';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { getActivatableTriggerNodes } from '@/utils/nodeTypesUtils';
import type { VNode } from 'vue';
import { computed, h, watch } from 'vue';
import { useI18n } from '@n8n/i18n';
import type { PermissionsRecord } from '@/permissions';
import {
WORKFLOW_ACTIVATION_CONFLICTING_WEBHOOK_MODAL_KEY,
EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
} from '@/constants';
import WorkflowActivationErrorMessage from './WorkflowActivationErrorMessage.vue';
import { useCredentialsStore } from '@/stores/credentials.store';
import type { INodeUi, IUsedCredential } from '@/Interface';
import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
const props = defineProps<{
isArchived: boolean;
workflowActive: boolean;
workflowId: string;
workflowPermissions: PermissionsRecord['workflow'];
}>();
const emit = defineEmits<{
'update:workflowActive': [value: { id: string; active: boolean }];
}>();
const { showMessage } = useToast();
const workflowActivate = useWorkflowActivate();
const uiStore = useUIStore();
const workflowHelpers = useWorkflowHelpers();
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const credentialsStore = useCredentialsStore();
const isWorkflowActive = computed((): boolean => {
const activeWorkflows = workflowsStore.activeWorkflows;
return activeWorkflows.includes(props.workflowId);
});
const couldNotBeStarted = computed((): boolean => {
return props.workflowActive && isWorkflowActive.value !== props.workflowActive;
});
const getActiveColor = computed((): string => {
if (couldNotBeStarted.value) {
return '#ff4949';
}
return '#13ce66';
});
const isCurrentWorkflow = computed((): boolean => {
return workflowsStore.workflowId === props.workflowId;
});
const foundTriggers = computed(() =>
getActivatableTriggerNodes(workflowsStore.workflowTriggerNodes),
);
const containsTrigger = computed((): boolean => {
return foundTriggers.value.length > 0;
});
const containsOnlyExecuteWorkflowTrigger = computed((): boolean => {
const foundActiveTriggers = workflowsStore.workflowTriggerNodes.filter(
(trigger) => !trigger.disabled,
);
const foundTriggers = foundActiveTriggers.filter(
(trigger) => trigger.type === EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
);
return foundTriggers.length > 0 && foundTriggers.length === foundActiveTriggers.length;
});
const isNewWorkflow = computed(
() =>
!props.workflowId ||
props.workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID ||
props.workflowId === 'new',
);
const disabled = computed((): boolean => {
if (props.isArchived) {
return true;
}
if (isNewWorkflow.value || isCurrentWorkflow.value) {
return !props.workflowActive && !containsTrigger.value;
}
return false;
});
function findManagedOpenAiCredentialId(
usedCredentials: Record<string, IUsedCredential>,
): string | undefined {
return Object.keys(usedCredentials).find((credentialId) => {
const credential = credentialsStore.state.credentials[credentialId];
return credential.isManaged && credential.type === OPEN_AI_API_CREDENTIAL_TYPE;
});
}
function hasActiveNodeUsingCredential(nodes: INodeUi[], credentialId: string): boolean {
return nodes.some(
(node) =>
node?.credentials?.[OPEN_AI_API_CREDENTIAL_TYPE]?.id === credentialId && !node.disabled,
);
}
/**
* Determines if the warning for free AI credits should be shown in the workflow.
*
* This computed property evaluates whether to display a warning about free AI credits
* in the workflow. The warning is shown when both conditions are met:
* 1. The workflow uses managed OpenAI API credentials
* 2. Those credentials are associated with at least one enabled node
*
*/
const shouldShowFreeAiCreditsWarning = computed((): boolean => {
const usedCredentials = workflowsStore?.usedCredentials;
if (!usedCredentials) return false;
const managedOpenAiCredentialId = findManagedOpenAiCredentialId(usedCredentials);
if (!managedOpenAiCredentialId) return false;
return hasActiveNodeUsingCredential(workflowsStore.allNodes, managedOpenAiCredentialId);
});
async function activeChanged(newActiveState: boolean) {
if (!isWorkflowActive.value) {
const conflictData = await workflowHelpers.checkConflictingWebhooks(props.workflowId);
if (conflictData) {
const { trigger, conflict } = conflictData;
const conflictingWorkflow = await workflowsStore.fetchWorkflow(conflict.workflowId);
uiStore.openModalWithData({
name: WORKFLOW_ACTIVATION_CONFLICTING_WEBHOOK_MODAL_KEY,
data: {
triggerName: trigger.name,
workflowName: conflictingWorkflow.name,
...conflict,
},
});
return;
}
}
const newState = await workflowActivate.updateWorkflowActivation(
props.workflowId,
newActiveState,
);
emit('update:workflowActive', { id: props.workflowId, active: newState });
}
async function displayActivationError() {
let errorMessage: string | VNode;
try {
const errorData = await workflowsStore.getActivationError(props.workflowId);
if (errorData === undefined) {
errorMessage = i18n.baseText(
'workflowActivator.showMessage.displayActivationError.message.errorDataUndefined',
);
} else {
errorMessage = h(WorkflowActivationErrorMessage, {
message: errorData,
});
}
} catch (error) {
errorMessage = i18n.baseText(
'workflowActivator.showMessage.displayActivationError.message.catchBlock',
);
}
showMessage({
title: i18n.baseText('workflowActivator.showMessage.displayActivationError.title'),
message: errorMessage,
type: 'warning',
duration: 0,
});
}
watch(
() => props.workflowActive,
(workflowActive) => {
if (workflowActive && shouldShowFreeAiCreditsWarning.value) {
showMessage({
title: i18n.baseText('freeAi.credits.showWarning.workflow.activation.title'),
message: i18n.baseText('freeAi.credits.showWarning.workflow.activation.description'),
type: 'warning',
duration: 0,
});
}
},
);
</script>
<template>
<div class="workflow-activator">
<div :class="$style.activeStatusText" data-test-id="workflow-activator-status">
<n8n-text
v-if="workflowActive"
:color="couldNotBeStarted ? 'danger' : 'success'"
size="small"
bold
>
{{ i18n.baseText('workflowActivator.active') }}
</n8n-text>
<n8n-text v-else color="text-base" size="small" bold>
{{ i18n.baseText('workflowActivator.inactive') }}
</n8n-text>
</div>
<n8n-tooltip :disabled="!disabled" placement="bottom">
<template #content>
<div>
{{
i18n.baseText(
isArchived
? 'workflowActivator.thisWorkflowIsArchived'
: containsOnlyExecuteWorkflowTrigger
? 'workflowActivator.thisWorkflowHasOnlyOneExecuteWorkflowTriggerNode'
: 'workflowActivator.thisWorkflowHasNoTriggerNodes',
)
}}
</div>
</template>
<el-switch
v-loading="workflowActivate.updatingWorkflowActivation.value"
:model-value="workflowActive"
:title="
workflowActive
? i18n.baseText('workflowActivator.deactivateWorkflow')
: i18n.baseText('workflowActivator.activateWorkflow')
"
:disabled="
disabled ||
workflowActivate.updatingWorkflowActivation.value ||
(!isNewWorkflow && !workflowPermissions.update)
"
:active-color="getActiveColor"
inactive-color="#8899AA"
data-test-id="workflow-activate-switch"
@update:model-value="activeChanged"
>
</el-switch>
</n8n-tooltip>
<div v-if="couldNotBeStarted" class="could-not-be-started">
<n8n-tooltip placement="top">
<template #content>
<div
v-n8n-html="i18n.baseText('workflowActivator.theWorkflowIsSetToBeActiveBut')"
@click="displayActivationError"
></div>
</template>
<font-awesome-icon icon="exclamation-triangle" @click="displayActivationError" />
</n8n-tooltip>
</div>
</div>
</template>
<style lang="scss" module>
.activeStatusText {
width: 64px; // Required to avoid jumping when changing active state
padding-right: var(--spacing-2xs);
box-sizing: border-box;
display: inline-block;
text-align: right;
}
</style>
<style lang="scss" scoped>
.workflow-activator {
display: inline-flex;
flex-wrap: nowrap;
align-items: center;
}
.could-not-be-started {
display: inline-block;
color: var(--color-text-danger);
margin-left: 0.5em;
}
</style>