mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-02 07:01:30 -08:00
refactor(editor): Migrate components to composition API (#11497)
This commit is contained in:
parent
3eb05e6df9
commit
611967decc
|
@ -1,65 +1,59 @@
|
|||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||
import { mapStores } from 'pinia';
|
||||
import { defineComponent } from 'vue';
|
||||
import { NPM_PACKAGE_DOCS_BASE_URL, COMMUNITY_PACKAGE_MANAGE_ACTIONS } from '@/constants';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CommunityPackageCard',
|
||||
props: {
|
||||
communityPackage: {
|
||||
type: Object as () => PublicInstalledPackage | null,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
packageActions: [
|
||||
{
|
||||
label: this.$locale.baseText('settings.communityNodes.viewDocsAction.label'),
|
||||
value: COMMUNITY_PACKAGE_MANAGE_ACTIONS.VIEW_DOCS,
|
||||
type: 'external-link',
|
||||
},
|
||||
{
|
||||
label: this.$locale.baseText('settings.communityNodes.uninstallAction.label'),
|
||||
value: COMMUNITY_PACKAGE_MANAGE_ACTIONS.UNINSTALL,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useUIStore),
|
||||
},
|
||||
methods: {
|
||||
async onAction(value: string) {
|
||||
if (!this.communityPackage) return;
|
||||
switch (value) {
|
||||
case COMMUNITY_PACKAGE_MANAGE_ACTIONS.VIEW_DOCS:
|
||||
this.$telemetry.track('user clicked to browse the cnr package documentation', {
|
||||
package_name: this.communityPackage.packageName,
|
||||
package_version: this.communityPackage.installedVersion,
|
||||
});
|
||||
window.open(`${NPM_PACKAGE_DOCS_BASE_URL}${this.communityPackage.packageName}`, '_blank');
|
||||
break;
|
||||
case COMMUNITY_PACKAGE_MANAGE_ACTIONS.UNINSTALL:
|
||||
this.uiStore.openCommunityPackageUninstallConfirmModal(this.communityPackage.packageName);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
onUpdateClick() {
|
||||
if (!this.communityPackage) return;
|
||||
this.uiStore.openCommunityPackageUpdateConfirmModal(this.communityPackage.packageName);
|
||||
},
|
||||
},
|
||||
interface Props {
|
||||
communityPackage?: PublicInstalledPackage | null;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
communityPackage: null,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const { openCommunityPackageUpdateConfirmModal, openCommunityPackageUninstallConfirmModal } =
|
||||
useUIStore();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const packageActions = [
|
||||
{
|
||||
label: i18n.baseText('settings.communityNodes.viewDocsAction.label'),
|
||||
value: COMMUNITY_PACKAGE_MANAGE_ACTIONS.VIEW_DOCS,
|
||||
type: 'external-link',
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('settings.communityNodes.uninstallAction.label'),
|
||||
value: COMMUNITY_PACKAGE_MANAGE_ACTIONS.UNINSTALL,
|
||||
},
|
||||
];
|
||||
|
||||
async function onAction(value: string) {
|
||||
if (!props.communityPackage) return;
|
||||
switch (value) {
|
||||
case COMMUNITY_PACKAGE_MANAGE_ACTIONS.VIEW_DOCS:
|
||||
telemetry.track('user clicked to browse the cnr package documentation', {
|
||||
package_name: props.communityPackage.packageName,
|
||||
package_version: props.communityPackage.installedVersion,
|
||||
});
|
||||
window.open(`${NPM_PACKAGE_DOCS_BASE_URL}${props.communityPackage.packageName}`, '_blank');
|
||||
break;
|
||||
case COMMUNITY_PACKAGE_MANAGE_ACTIONS.UNINSTALL:
|
||||
openCommunityPackageUninstallConfirmModal(props.communityPackage.packageName);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function onUpdateClick() {
|
||||
if (!props.communityPackage) return;
|
||||
openCommunityPackageUpdateConfirmModal(props.communityPackage.packageName);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -76,7 +70,7 @@ export default defineComponent({
|
|||
<div :class="$style.cardSubtitle">
|
||||
<n8n-text :bold="true" size="small" color="text-light">
|
||||
{{
|
||||
$locale.baseText('settings.communityNodes.packageNodes.label', {
|
||||
i18n.baseText('settings.communityNodes.packageNodes.label', {
|
||||
adjustToNumber: communityPackage.installedNodes.length,
|
||||
})
|
||||
}}:
|
||||
|
@ -96,7 +90,7 @@ export default defineComponent({
|
|||
<n8n-tooltip v-if="communityPackage.failedLoading === true" placement="top">
|
||||
<template #content>
|
||||
<div>
|
||||
{{ $locale.baseText('settings.communityNodes.failedToLoad.tooltip') }}
|
||||
{{ i18n.baseText('settings.communityNodes.failedToLoad.tooltip') }}
|
||||
</div>
|
||||
</template>
|
||||
<n8n-icon icon="exclamation-triangle" color="danger" size="large" />
|
||||
|
@ -104,7 +98,7 @@ export default defineComponent({
|
|||
<n8n-tooltip v-else-if="communityPackage.updateAvailable" placement="top">
|
||||
<template #content>
|
||||
<div>
|
||||
{{ $locale.baseText('settings.communityNodes.updateAvailable.tooltip') }}
|
||||
{{ i18n.baseText('settings.communityNodes.updateAvailable.tooltip') }}
|
||||
</div>
|
||||
</template>
|
||||
<n8n-button outline label="Update" @click="onUpdateClick" />
|
||||
|
@ -112,7 +106,7 @@ export default defineComponent({
|
|||
<n8n-tooltip v-else placement="top">
|
||||
<template #content>
|
||||
<div>
|
||||
{{ $locale.baseText('settings.communityNodes.upToDate.tooltip') }}
|
||||
{{ i18n.baseText('settings.communityNodes.upToDate.tooltip') }}
|
||||
</div>
|
||||
</template>
|
||||
<n8n-icon icon="check-circle" color="text-light" size="large" />
|
||||
|
|
|
@ -51,6 +51,7 @@ export default defineComponent({
|
|||
},
|
||||
pushRef: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import Modal from './Modal.vue';
|
||||
import type { IFormInputs, IInviteResponse, IUser, InvitableRoleName } from '@/Interface';
|
||||
|
@ -12,13 +11,256 @@ import {
|
|||
} from '@/constants';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { createFormEventBus, createEventBus } from 'n8n-design-system/utils';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||
|
||||
const NAME_EMAIL_FORMAT_REGEX = /^.* <(.*)>$/;
|
||||
|
||||
const usersStore = useUsersStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const clipboard = useClipboard();
|
||||
const { showMessage, showError } = useToast();
|
||||
const i18n = useI18n();
|
||||
const { goToUpgrade } = usePageRedirectionHelper();
|
||||
|
||||
const formBus = createFormEventBus();
|
||||
const modalBus = createEventBus();
|
||||
const config = ref<IFormInputs | null>();
|
||||
const emails = ref('');
|
||||
const role = ref<InvitableRoleName>(ROLE.Member);
|
||||
const showInviteUrls = ref<IInviteResponse[] | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
config.value = [
|
||||
{
|
||||
name: 'emails',
|
||||
properties: {
|
||||
label: i18n.baseText('settings.users.newEmailsToInvite'),
|
||||
required: true,
|
||||
validationRules: [{ name: 'VALID_EMAILS' }],
|
||||
validators: {
|
||||
VALID_EMAILS: {
|
||||
validate: validateEmails,
|
||||
},
|
||||
},
|
||||
placeholder: 'name1@email.com, name2@email.com, ...',
|
||||
capitalize: true,
|
||||
focusInitially: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
initialValue: ROLE.Member,
|
||||
properties: {
|
||||
label: i18n.baseText('auth.role'),
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
value: ROLE.Member,
|
||||
label: i18n.baseText('auth.roles.member'),
|
||||
},
|
||||
{
|
||||
value: ROLE.Admin,
|
||||
label: i18n.baseText('auth.roles.admin'),
|
||||
disabled: !isAdvancedPermissionsEnabled.value,
|
||||
},
|
||||
],
|
||||
capitalize: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const emailsCount = computed((): number => {
|
||||
return emails.value.split(',').filter((email: string) => !!email.trim()).length;
|
||||
});
|
||||
|
||||
const buttonLabel = computed((): string => {
|
||||
if (emailsCount.value > 1) {
|
||||
return i18n.baseText(
|
||||
`settings.users.inviteXUser${settingsStore.isSmtpSetup ? '' : '.inviteUrl'}`,
|
||||
{
|
||||
interpolate: { count: emailsCount.value.toString() },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return i18n.baseText(`settings.users.inviteUser${settingsStore.isSmtpSetup ? '' : '.inviteUrl'}`);
|
||||
});
|
||||
|
||||
const enabledButton = computed((): boolean => {
|
||||
return emailsCount.value >= 1;
|
||||
});
|
||||
|
||||
const invitedUsers = computed((): IUser[] => {
|
||||
return showInviteUrls.value
|
||||
? usersStore.allUsers.filter((user) =>
|
||||
showInviteUrls.value?.find((invite) => invite.user.id === user.id),
|
||||
)
|
||||
: [];
|
||||
});
|
||||
|
||||
const isAdvancedPermissionsEnabled = computed((): boolean => {
|
||||
return settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.AdvancedPermissions];
|
||||
});
|
||||
|
||||
const validateEmails = (value: string | number | boolean | null | undefined) => {
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const emails = value.split(',');
|
||||
for (let i = 0; i < emails.length; i++) {
|
||||
const email = emails[i];
|
||||
const parsed = getEmail(email);
|
||||
|
||||
if (!!parsed.trim() && !VALID_EMAIL_REGEX.test(String(parsed).trim().toLowerCase())) {
|
||||
return {
|
||||
messageKey: 'settings.users.invalidEmailError',
|
||||
options: { interpolate: { email: parsed } },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
function onInput(e: { name: string; value: InvitableRoleName }) {
|
||||
if (e.name === 'emails') {
|
||||
emails.value = e.value;
|
||||
}
|
||||
if (e.name === 'role') {
|
||||
role.value = e.value;
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const emailList = emails.value
|
||||
.split(',')
|
||||
.map((email) => ({ email: getEmail(email), role: role.value }))
|
||||
.filter((invite) => !!invite.email);
|
||||
|
||||
if (emailList.length === 0) {
|
||||
throw new Error(i18n.baseText('settings.users.noUsersToInvite'));
|
||||
}
|
||||
|
||||
const invited = await usersStore.inviteUsers(emailList);
|
||||
const erroredInvites = invited.filter((invite) => invite.error);
|
||||
const successfulEmailInvites = invited.filter(
|
||||
(invite) => !invite.error && invite.user.emailSent,
|
||||
);
|
||||
const successfulUrlInvites = invited.filter(
|
||||
(invite) => !invite.error && !invite.user.emailSent,
|
||||
);
|
||||
|
||||
if (successfulEmailInvites.length) {
|
||||
showMessage({
|
||||
type: 'success',
|
||||
title: i18n.baseText(
|
||||
successfulEmailInvites.length > 1
|
||||
? 'settings.users.usersInvited'
|
||||
: 'settings.users.userInvited',
|
||||
),
|
||||
message: i18n.baseText('settings.users.emailInvitesSent', {
|
||||
interpolate: {
|
||||
emails: successfulEmailInvites.map(({ user }) => user.email).join(', '),
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (successfulUrlInvites.length) {
|
||||
if (successfulUrlInvites.length === 1) {
|
||||
void clipboard.copy(successfulUrlInvites[0].user.inviteAcceptUrl);
|
||||
}
|
||||
|
||||
showMessage({
|
||||
type: 'success',
|
||||
title: i18n.baseText(
|
||||
successfulUrlInvites.length > 1
|
||||
? 'settings.users.multipleInviteUrlsCreated'
|
||||
: 'settings.users.inviteUrlCreated',
|
||||
),
|
||||
message: i18n.baseText(
|
||||
successfulUrlInvites.length > 1
|
||||
? 'settings.users.multipleInviteUrlsCreated.message'
|
||||
: 'settings.users.inviteUrlCreated.message',
|
||||
{
|
||||
interpolate: {
|
||||
emails: successfulUrlInvites.map(({ user }) => user.email).join(', '),
|
||||
},
|
||||
},
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (erroredInvites.length) {
|
||||
setTimeout(() => {
|
||||
showMessage({
|
||||
type: 'error',
|
||||
title: i18n.baseText('settings.users.usersEmailedError'),
|
||||
message: i18n.baseText('settings.users.emailInvitesSentError', {
|
||||
interpolate: { emails: erroredInvites.map(({ error }) => error).join(', ') },
|
||||
}),
|
||||
});
|
||||
}, 0); // notifications stack on top of each other otherwise
|
||||
}
|
||||
|
||||
if (successfulUrlInvites.length > 1) {
|
||||
showInviteUrls.value = successfulUrlInvites;
|
||||
} else {
|
||||
modalBus.emit('close');
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, i18n.baseText('settings.users.usersInvitedError'));
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function showCopyInviteLinkToast(successfulUrlInvites: IInviteResponse[]) {
|
||||
showMessage({
|
||||
type: 'success',
|
||||
title: i18n.baseText(
|
||||
successfulUrlInvites.length > 1
|
||||
? 'settings.users.multipleInviteUrlsCreated'
|
||||
: 'settings.users.inviteUrlCreated',
|
||||
),
|
||||
message: i18n.baseText(
|
||||
successfulUrlInvites.length > 1
|
||||
? 'settings.users.multipleInviteUrlsCreated.message'
|
||||
: 'settings.users.inviteUrlCreated.message',
|
||||
{
|
||||
interpolate: {
|
||||
emails: successfulUrlInvites.map(({ user }) => user.email).join(', '),
|
||||
},
|
||||
},
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmitClick() {
|
||||
formBus.emit('submit');
|
||||
}
|
||||
|
||||
function onCopyInviteLink(user: IUser) {
|
||||
if (user.inviteAcceptUrl && showInviteUrls.value) {
|
||||
void clipboard.copy(user.inviteAcceptUrl);
|
||||
showCopyInviteLinkToast([]);
|
||||
}
|
||||
}
|
||||
|
||||
function goToUpgradeAdvancedPermissions() {
|
||||
void goToUpgrade('advanced-permissions', 'upgrade-advanced-permissions');
|
||||
}
|
||||
|
||||
function getEmail(email: string): string {
|
||||
let parsed = email.trim();
|
||||
if (NAME_EMAIL_FORMAT_REGEX.test(parsed)) {
|
||||
|
@ -29,267 +271,13 @@ function getEmail(email: string): string {
|
|||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'InviteUsersModal',
|
||||
components: { Modal },
|
||||
props: {
|
||||
modalName: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return {
|
||||
clipboard,
|
||||
...useToast(),
|
||||
...usePageRedirectionHelper(),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
config: null as IFormInputs | null,
|
||||
formBus: createFormEventBus(),
|
||||
modalBus: createEventBus(),
|
||||
emails: '',
|
||||
role: ROLE.Member as InvitableRoleName,
|
||||
showInviteUrls: null as IInviteResponse[] | null,
|
||||
loading: false,
|
||||
INVITE_USER_MODAL_KEY,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.config = [
|
||||
{
|
||||
name: 'emails',
|
||||
properties: {
|
||||
label: this.$locale.baseText('settings.users.newEmailsToInvite'),
|
||||
required: true,
|
||||
validationRules: [{ name: 'VALID_EMAILS' }],
|
||||
validators: {
|
||||
VALID_EMAILS: {
|
||||
validate: this.validateEmails,
|
||||
},
|
||||
},
|
||||
placeholder: 'name1@email.com, name2@email.com, ...',
|
||||
capitalize: true,
|
||||
focusInitially: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
initialValue: ROLE.Member,
|
||||
properties: {
|
||||
label: this.$locale.baseText('auth.role'),
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
value: ROLE.Member,
|
||||
label: this.$locale.baseText('auth.roles.member'),
|
||||
},
|
||||
{
|
||||
value: ROLE.Admin,
|
||||
label: this.$locale.baseText('auth.roles.admin'),
|
||||
disabled: !this.isAdvancedPermissionsEnabled,
|
||||
},
|
||||
],
|
||||
capitalize: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useUsersStore, useSettingsStore, useUIStore),
|
||||
emailsCount(): number {
|
||||
return this.emails.split(',').filter((email: string) => !!email.trim()).length;
|
||||
},
|
||||
buttonLabel(): string {
|
||||
if (this.emailsCount > 1) {
|
||||
return this.$locale.baseText(
|
||||
`settings.users.inviteXUser${this.settingsStore.isSmtpSetup ? '' : '.inviteUrl'}`,
|
||||
{
|
||||
interpolate: { count: this.emailsCount.toString() },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.$locale.baseText(
|
||||
`settings.users.inviteUser${this.settingsStore.isSmtpSetup ? '' : '.inviteUrl'}`,
|
||||
);
|
||||
},
|
||||
enabledButton(): boolean {
|
||||
return this.emailsCount >= 1;
|
||||
},
|
||||
invitedUsers(): IUser[] {
|
||||
return this.showInviteUrls
|
||||
? this.usersStore.allUsers.filter((user) =>
|
||||
this.showInviteUrls!.find((invite) => invite.user.id === user.id),
|
||||
)
|
||||
: [];
|
||||
},
|
||||
isAdvancedPermissionsEnabled(): boolean {
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled[
|
||||
EnterpriseEditionFeature.AdvancedPermissions
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
validateEmails(value: string | number | boolean | null | undefined) {
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const emails = value.split(',');
|
||||
for (let i = 0; i < emails.length; i++) {
|
||||
const email = emails[i];
|
||||
const parsed = getEmail(email);
|
||||
|
||||
if (!!parsed.trim() && !VALID_EMAIL_REGEX.test(String(parsed).trim().toLowerCase())) {
|
||||
return {
|
||||
messageKey: 'settings.users.invalidEmailError',
|
||||
options: { interpolate: { email: parsed } },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
onInput(e: { name: string; value: InvitableRoleName }) {
|
||||
if (e.name === 'emails') {
|
||||
this.emails = e.value;
|
||||
}
|
||||
if (e.name === 'role') {
|
||||
this.role = e.value;
|
||||
}
|
||||
},
|
||||
async onSubmit() {
|
||||
try {
|
||||
this.loading = true;
|
||||
|
||||
const emails = this.emails
|
||||
.split(',')
|
||||
.map((email) => ({ email: getEmail(email), role: this.role }))
|
||||
.filter((invite) => !!invite.email);
|
||||
|
||||
if (emails.length === 0) {
|
||||
throw new Error(this.$locale.baseText('settings.users.noUsersToInvite'));
|
||||
}
|
||||
|
||||
const invited = await this.usersStore.inviteUsers(emails);
|
||||
const erroredInvites = invited.filter((invite) => invite.error);
|
||||
const successfulEmailInvites = invited.filter(
|
||||
(invite) => !invite.error && invite.user.emailSent,
|
||||
);
|
||||
const successfulUrlInvites = invited.filter(
|
||||
(invite) => !invite.error && !invite.user.emailSent,
|
||||
);
|
||||
|
||||
if (successfulEmailInvites.length) {
|
||||
this.showMessage({
|
||||
type: 'success',
|
||||
title: this.$locale.baseText(
|
||||
successfulEmailInvites.length > 1
|
||||
? 'settings.users.usersInvited'
|
||||
: 'settings.users.userInvited',
|
||||
),
|
||||
message: this.$locale.baseText('settings.users.emailInvitesSent', {
|
||||
interpolate: {
|
||||
emails: successfulEmailInvites.map(({ user }) => user.email).join(', '),
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (successfulUrlInvites.length) {
|
||||
if (successfulUrlInvites.length === 1) {
|
||||
void this.clipboard.copy(successfulUrlInvites[0].user.inviteAcceptUrl);
|
||||
}
|
||||
|
||||
this.showMessage({
|
||||
type: 'success',
|
||||
title: this.$locale.baseText(
|
||||
successfulUrlInvites.length > 1
|
||||
? 'settings.users.multipleInviteUrlsCreated'
|
||||
: 'settings.users.inviteUrlCreated',
|
||||
),
|
||||
message: this.$locale.baseText(
|
||||
successfulUrlInvites.length > 1
|
||||
? 'settings.users.multipleInviteUrlsCreated.message'
|
||||
: 'settings.users.inviteUrlCreated.message',
|
||||
{
|
||||
interpolate: {
|
||||
emails: successfulUrlInvites.map(({ user }) => user.email).join(', '),
|
||||
},
|
||||
},
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (erroredInvites.length) {
|
||||
setTimeout(() => {
|
||||
this.showMessage({
|
||||
type: 'error',
|
||||
title: this.$locale.baseText('settings.users.usersEmailedError'),
|
||||
message: this.$locale.baseText('settings.users.emailInvitesSentError', {
|
||||
interpolate: { emails: erroredInvites.map(({ error }) => error).join(', ') },
|
||||
}),
|
||||
});
|
||||
}, 0); // notifications stack on top of each other otherwise
|
||||
}
|
||||
|
||||
if (successfulUrlInvites.length > 1) {
|
||||
this.showInviteUrls = successfulUrlInvites;
|
||||
} else {
|
||||
this.modalBus.emit('close');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error, this.$locale.baseText('settings.users.usersInvitedError'));
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
showCopyInviteLinkToast(successfulUrlInvites: IInviteResponse[]) {
|
||||
this.showMessage({
|
||||
type: 'success',
|
||||
title: this.$locale.baseText(
|
||||
successfulUrlInvites.length > 1
|
||||
? 'settings.users.multipleInviteUrlsCreated'
|
||||
: 'settings.users.inviteUrlCreated',
|
||||
),
|
||||
message: this.$locale.baseText(
|
||||
successfulUrlInvites.length > 1
|
||||
? 'settings.users.multipleInviteUrlsCreated.message'
|
||||
: 'settings.users.inviteUrlCreated.message',
|
||||
{
|
||||
interpolate: {
|
||||
emails: successfulUrlInvites.map(({ user }) => user.email).join(', '),
|
||||
},
|
||||
},
|
||||
),
|
||||
});
|
||||
},
|
||||
onSubmitClick() {
|
||||
this.formBus.emit('submit');
|
||||
},
|
||||
onCopyInviteLink(user: IUser) {
|
||||
if (user.inviteAcceptUrl && this.showInviteUrls) {
|
||||
void this.clipboard.copy(user.inviteAcceptUrl);
|
||||
this.showCopyInviteLinkToast([]);
|
||||
}
|
||||
},
|
||||
goToUpgradeAdvancedPermissions() {
|
||||
void this.goToUpgrade('advanced-permissions', 'upgrade-advanced-permissions');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:name="INVITE_USER_MODAL_KEY"
|
||||
:title="
|
||||
$locale.baseText(
|
||||
i18n.baseText(
|
||||
showInviteUrls ? 'settings.users.copyInviteUrls' : 'settings.users.inviteNewUsers',
|
||||
)
|
||||
"
|
||||
|
@ -303,7 +291,7 @@ export default defineComponent({
|
|||
<i18n-t keypath="settings.users.advancedPermissions.warning">
|
||||
<template #link>
|
||||
<n8n-link size="small" @click="goToUpgradeAdvancedPermissions">
|
||||
{{ $locale.baseText('settings.users.advancedPermissions.warning.link') }}
|
||||
{{ i18n.baseText('settings.users.advancedPermissions.warning.link') }}
|
||||
</n8n-link>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
@ -313,7 +301,7 @@ export default defineComponent({
|
|||
<template #actions="{ user }">
|
||||
<n8n-tooltip>
|
||||
<template #content>
|
||||
{{ $locale.baseText('settings.users.inviteLink.copy') }}
|
||||
{{ i18n.baseText('settings.users.inviteLink.copy') }}
|
||||
</template>
|
||||
<n8n-icon-button
|
||||
icon="link"
|
||||
|
|
|
@ -122,7 +122,7 @@ const badge = computed(() => {
|
|||
:disabled="disabled"
|
||||
:size="size"
|
||||
:circle="circle"
|
||||
:node-type-name="nodeName ?? nodeType?.displayName ?? ''"
|
||||
:node-type-name="nodeName ? nodeName : nodeType?.displayName"
|
||||
:show-tooltip="showTooltip"
|
||||
:tooltip-position="tooltipPosition"
|
||||
:badge="badge"
|
||||
|
|
|
@ -35,7 +35,7 @@ type Props = {
|
|||
isReadOnly?: boolean;
|
||||
linkedRuns?: boolean;
|
||||
canLinkRuns?: boolean;
|
||||
pushRef?: string;
|
||||
pushRef: string;
|
||||
blockUI?: boolean;
|
||||
isProductionExecutionPreview?: boolean;
|
||||
isPaneActive?: boolean;
|
||||
|
|
|
@ -135,6 +135,7 @@ export default defineComponent({
|
|||
},
|
||||
pushRef: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
paneType: {
|
||||
type: String as PropType<NodePanelType>,
|
||||
|
|
|
@ -23,15 +23,15 @@ const LazyRunDataJsonActions = defineAsyncComponent(
|
|||
const props = withDefaults(
|
||||
defineProps<{
|
||||
editMode: { enabled?: boolean; value?: string };
|
||||
pushRef?: string;
|
||||
paneType?: string;
|
||||
pushRef: string;
|
||||
paneType: string;
|
||||
node: INodeUi;
|
||||
inputData: INodeExecutionData[];
|
||||
mappingEnabled?: boolean;
|
||||
distanceFromActive: number;
|
||||
runIndex?: number;
|
||||
totalRuns?: number;
|
||||
search?: string;
|
||||
runIndex: number | undefined;
|
||||
totalRuns: number | undefined;
|
||||
search: string | undefined;
|
||||
}>(),
|
||||
{
|
||||
editMode: () => ({}),
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores, storeToRefs } from 'pinia';
|
||||
<script lang="ts" setup>
|
||||
import jp from 'jsonpath';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
|
@ -14,192 +11,168 @@ import { useToast } from '@/composables/useToast';
|
|||
import { useI18n } from '@/composables/useI18n';
|
||||
import { nonExistingJsonPath } from '@/constants';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { usePinnedData } from '@/composables/usePinnedData';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
|
||||
type JsonPathData = {
|
||||
path: string;
|
||||
startPath: string;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RunDataJsonActions',
|
||||
props: {
|
||||
node: {
|
||||
type: Object as PropType<INodeUi>,
|
||||
required: true,
|
||||
},
|
||||
paneType: {
|
||||
type: String,
|
||||
},
|
||||
pushRef: {
|
||||
type: String,
|
||||
},
|
||||
currentOutputIndex: {
|
||||
type: Number,
|
||||
},
|
||||
runIndex: {
|
||||
type: Number,
|
||||
},
|
||||
displayMode: {
|
||||
type: String,
|
||||
},
|
||||
distanceFromActive: {
|
||||
type: Number,
|
||||
},
|
||||
selectedJsonPath: {
|
||||
type: String,
|
||||
default: nonExistingJsonPath,
|
||||
},
|
||||
jsonData: {
|
||||
type: Array as PropType<IDataObject[]>,
|
||||
required: true,
|
||||
},
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
node: INodeUi;
|
||||
paneType: string;
|
||||
pushRef: string;
|
||||
displayMode: string;
|
||||
distanceFromActive: number;
|
||||
selectedJsonPath: string;
|
||||
jsonData: IDataObject[];
|
||||
currentOutputIndex?: number;
|
||||
runIndex?: number;
|
||||
}>(),
|
||||
{
|
||||
selectedJsonPath: nonExistingJsonPath,
|
||||
},
|
||||
setup() {
|
||||
const ndvStore = useNDVStore();
|
||||
const i18n = useI18n();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const clipboard = useClipboard();
|
||||
const { activeNode } = storeToRefs(ndvStore);
|
||||
const pinnedData = usePinnedData(activeNode);
|
||||
);
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
return {
|
||||
i18n,
|
||||
nodeHelpers,
|
||||
clipboard,
|
||||
pinnedData,
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNodeTypesStore, useNDVStore, useWorkflowsStore, useSourceControlStore),
|
||||
isReadOnlyRoute() {
|
||||
return this.$route?.meta?.readOnlyCanvas === true;
|
||||
},
|
||||
activeNode(): INodeUi | null {
|
||||
return this.ndvStore.activeNode;
|
||||
},
|
||||
noSelection() {
|
||||
return this.selectedJsonPath === nonExistingJsonPath;
|
||||
},
|
||||
normalisedJsonPath(): string {
|
||||
return this.noSelection ? '[""]' : this.selectedJsonPath;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getJsonValue(): string {
|
||||
let selectedValue = jp.query(this.jsonData, `$${this.normalisedJsonPath}`)[0];
|
||||
if (this.noSelection) {
|
||||
const inExecutionsFrame =
|
||||
window !== window.parent && window.parent.location.pathname.includes('/executions');
|
||||
const i18n = useI18n();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const clipboard = useClipboard();
|
||||
const { activeNode } = ndvStore;
|
||||
const pinnedData = usePinnedData(activeNode);
|
||||
const { showToast } = useToast();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
if (this.pinnedData.hasData.value && !inExecutionsFrame) {
|
||||
selectedValue = clearJsonKey(this.pinnedData.data.value as object);
|
||||
} else {
|
||||
selectedValue = executionDataToJson(
|
||||
this.nodeHelpers.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
const route = useRoute();
|
||||
|
||||
let value = '';
|
||||
if (typeof selectedValue === 'object') {
|
||||
value = JSON.stringify(selectedValue, null, 2);
|
||||
} else {
|
||||
value = selectedValue.toString();
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
getJsonItemPath(): JsonPathData {
|
||||
const newPath = convertPath(this.normalisedJsonPath);
|
||||
let startPath = '';
|
||||
let path = '';
|
||||
|
||||
const pathParts = newPath.split(']');
|
||||
const index = pathParts[0].slice(1);
|
||||
path = pathParts.slice(1).join(']');
|
||||
startPath = `$item(${index}).$node["${this.node.name}"].json`;
|
||||
|
||||
return { path, startPath };
|
||||
},
|
||||
getJsonParameterPath(): JsonPathData {
|
||||
const newPath = convertPath(this.normalisedJsonPath);
|
||||
const path = newPath.split(']').slice(1).join(']');
|
||||
let startPath = `$node["${this.node.name}"].json`;
|
||||
|
||||
if (this.distanceFromActive === 1) {
|
||||
startPath = '$json';
|
||||
}
|
||||
|
||||
return { path, startPath };
|
||||
},
|
||||
handleCopyClick(commandData: { command: string }) {
|
||||
let value: string;
|
||||
if (commandData.command === 'value') {
|
||||
value = this.getJsonValue();
|
||||
|
||||
this.showToast({
|
||||
title: this.i18n.baseText('runData.copyValue.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
} else {
|
||||
let startPath = '';
|
||||
let path = '';
|
||||
if (commandData.command === 'itemPath') {
|
||||
const jsonItemPath = this.getJsonItemPath();
|
||||
startPath = jsonItemPath.startPath;
|
||||
path = jsonItemPath.path;
|
||||
|
||||
this.showToast({
|
||||
title: this.i18n.baseText('runData.copyItemPath.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
} else if (commandData.command === 'parameterPath') {
|
||||
const jsonParameterPath = this.getJsonParameterPath();
|
||||
startPath = jsonParameterPath.startPath;
|
||||
path = jsonParameterPath.path;
|
||||
|
||||
this.showToast({
|
||||
title: this.i18n.baseText('runData.copyParameterPath.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
if (!path.startsWith('[') && !path.startsWith('.') && path) {
|
||||
path += '.';
|
||||
}
|
||||
value = `{{ ${startPath + path} }}`;
|
||||
}
|
||||
|
||||
const copyType = {
|
||||
value: 'selection',
|
||||
itemPath: 'item_path',
|
||||
parameterPath: 'parameter_path',
|
||||
}[commandData.command];
|
||||
|
||||
this.$telemetry.track('User copied ndv data', {
|
||||
node_type: this.activeNode?.type,
|
||||
push_ref: this.pushRef,
|
||||
run_index: this.runIndex,
|
||||
view: 'json',
|
||||
copy_type: copyType,
|
||||
workflow_id: this.workflowsStore.workflowId,
|
||||
pane: this.paneType,
|
||||
in_execution_log: this.isReadOnlyRoute,
|
||||
});
|
||||
|
||||
void this.clipboard.copy(value);
|
||||
},
|
||||
},
|
||||
const isReadOnlyRoute = computed(() => {
|
||||
return route?.meta?.readOnlyCanvas === true;
|
||||
});
|
||||
|
||||
const noSelection = computed(() => {
|
||||
return props.selectedJsonPath === nonExistingJsonPath;
|
||||
});
|
||||
const normalisedJsonPath = computed((): string => {
|
||||
return noSelection.value ? '[""]' : props.selectedJsonPath;
|
||||
});
|
||||
|
||||
function getJsonValue(): string {
|
||||
let selectedValue = jp.query(props.jsonData, `$${normalisedJsonPath.value}`)[0];
|
||||
if (noSelection.value) {
|
||||
const inExecutionsFrame =
|
||||
window !== window.parent && window.parent.location.pathname.includes('/executions');
|
||||
|
||||
if (pinnedData.hasData.value && !inExecutionsFrame) {
|
||||
selectedValue = clearJsonKey(pinnedData.data.value as object);
|
||||
} else {
|
||||
selectedValue = executionDataToJson(
|
||||
nodeHelpers.getNodeInputData(props.node, props.runIndex, props.currentOutputIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let value = '';
|
||||
if (typeof selectedValue === 'object') {
|
||||
value = JSON.stringify(selectedValue, null, 2);
|
||||
} else {
|
||||
value = selectedValue.toString();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getJsonItemPath(): JsonPathData {
|
||||
const newPath = convertPath(normalisedJsonPath.value);
|
||||
let startPath = '';
|
||||
let path = '';
|
||||
|
||||
const pathParts = newPath.split(']');
|
||||
const index = pathParts[0].slice(1);
|
||||
path = pathParts.slice(1).join(']');
|
||||
startPath = `$item(${index}).$node["${props.node.name}"].json`;
|
||||
|
||||
return { path, startPath };
|
||||
}
|
||||
|
||||
function getJsonParameterPath(): JsonPathData {
|
||||
const newPath = convertPath(normalisedJsonPath.value);
|
||||
const path = newPath.split(']').slice(1).join(']');
|
||||
let startPath = `$node["${props.node.name}"].json`;
|
||||
|
||||
if (props.distanceFromActive === 1) {
|
||||
startPath = '$json';
|
||||
}
|
||||
|
||||
return { path, startPath };
|
||||
}
|
||||
|
||||
function handleCopyClick(commandData: { command: string }) {
|
||||
let value: string;
|
||||
if (commandData.command === 'value') {
|
||||
value = getJsonValue();
|
||||
|
||||
showToast({
|
||||
title: i18n.baseText('runData.copyValue.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
} else {
|
||||
let startPath = '';
|
||||
let path = '';
|
||||
if (commandData.command === 'itemPath') {
|
||||
const jsonItemPath = getJsonItemPath();
|
||||
startPath = jsonItemPath.startPath;
|
||||
path = jsonItemPath.path;
|
||||
|
||||
showToast({
|
||||
title: i18n.baseText('runData.copyItemPath.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
} else if (commandData.command === 'parameterPath') {
|
||||
const jsonParameterPath = getJsonParameterPath();
|
||||
startPath = jsonParameterPath.startPath;
|
||||
path = jsonParameterPath.path;
|
||||
|
||||
showToast({
|
||||
title: i18n.baseText('runData.copyParameterPath.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
if (!path.startsWith('[') && !path.startsWith('.') && path) {
|
||||
path += '.';
|
||||
}
|
||||
value = `{{ ${startPath + path} }}`;
|
||||
}
|
||||
|
||||
const copyType = {
|
||||
value: 'selection',
|
||||
itemPath: 'item_path',
|
||||
parameterPath: 'parameter_path',
|
||||
}[commandData.command];
|
||||
|
||||
telemetry.track('User copied ndv data', {
|
||||
node_type: activeNode?.type,
|
||||
push_ref: props.pushRef,
|
||||
run_index: props.runIndex,
|
||||
view: 'json',
|
||||
copy_type: copyType,
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
pane: props.paneType,
|
||||
in_execution_log: isReadOnlyRoute.value,
|
||||
});
|
||||
|
||||
void clipboard.copy(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,134 +1,135 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { EnterpriseEditionFeature, MODAL_CONFIRM } from '@/constants';
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeMount, onMounted, ref } from 'vue';
|
||||
import { MODAL_CONFIRM } from '@/constants';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { useLogStreamingStore } from '@/stores/logStreaming.store';
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import type { MessageEventBusDestinationOptions } from 'n8n-workflow';
|
||||
import { deepCopy, defaultMessageEventBusDestinationOptions } from 'n8n-workflow';
|
||||
import type { BaseTextKey } from '@/plugins/i18n';
|
||||
import type { EventBus } from 'n8n-design-system';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { assert } from '@/utils/assert';
|
||||
|
||||
export const DESTINATION_LIST_ITEM_ACTIONS = {
|
||||
const DESTINATION_LIST_ITEM_ACTIONS = {
|
||||
OPEN: 'open',
|
||||
DELETE: 'delete',
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
setup() {
|
||||
return {
|
||||
...useMessage(),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
EnterpriseEditionFeature,
|
||||
nodeParameters: {} as MessageEventBusDestinationOptions,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
eventBus: {
|
||||
type: Object as PropType<EventBus>,
|
||||
},
|
||||
destination: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: deepCopy(defaultMessageEventBusDestinationOptions),
|
||||
},
|
||||
readonly: Boolean,
|
||||
},
|
||||
mounted() {
|
||||
this.nodeParameters = Object.assign(
|
||||
deepCopy(defaultMessageEventBusDestinationOptions),
|
||||
this.destination,
|
||||
);
|
||||
this.eventBus?.on('destinationWasSaved', this.onDestinationWasSaved);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.eventBus?.off('destinationWasSaved', this.onDestinationWasSaved);
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useLogStreamingStore),
|
||||
actions(): Array<{ label: string; value: string }> {
|
||||
const actions = [
|
||||
{
|
||||
label: this.$locale.baseText('workflows.item.open'),
|
||||
value: DESTINATION_LIST_ITEM_ACTIONS.OPEN,
|
||||
},
|
||||
];
|
||||
if (!this.readonly) {
|
||||
actions.push({
|
||||
label: this.$locale.baseText('workflows.item.delete'),
|
||||
value: DESTINATION_LIST_ITEM_ACTIONS.DELETE,
|
||||
});
|
||||
}
|
||||
return actions;
|
||||
},
|
||||
typeLabelName(): BaseTextKey {
|
||||
return `settings.log-streaming.${this.destination.__type}` as BaseTextKey;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onDestinationWasSaved() {
|
||||
const updatedDestination = this.logStreamingStore.getDestination(this.destination.id);
|
||||
if (updatedDestination) {
|
||||
this.nodeParameters = Object.assign(
|
||||
deepCopy(defaultMessageEventBusDestinationOptions),
|
||||
this.destination,
|
||||
);
|
||||
}
|
||||
},
|
||||
async onClick(event: Event) {
|
||||
const cardActions = this.$refs.cardActions as HTMLDivElement | null;
|
||||
const target = event.target as HTMLDivElement | null;
|
||||
if (
|
||||
cardActions === target ||
|
||||
cardActions?.contains(target) ||
|
||||
target?.contains(cardActions)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const { confirm } = useMessage();
|
||||
const i18n = useI18n();
|
||||
const logStreamingStore = useLogStreamingStore();
|
||||
|
||||
this.$emit('edit', this.destination.id);
|
||||
},
|
||||
onEnabledSwitched(state: boolean) {
|
||||
this.nodeParameters.enabled = state;
|
||||
void this.saveDestination();
|
||||
},
|
||||
async saveDestination() {
|
||||
await this.logStreamingStore.saveDestination(this.nodeParameters);
|
||||
},
|
||||
async onAction(action: string) {
|
||||
if (action === DESTINATION_LIST_ITEM_ACTIONS.OPEN) {
|
||||
this.$emit('edit', this.destination.id);
|
||||
} else if (action === DESTINATION_LIST_ITEM_ACTIONS.DELETE) {
|
||||
const deleteConfirmed = await this.confirm(
|
||||
this.$locale.baseText('settings.log-streaming.destinationDelete.message', {
|
||||
interpolate: { destinationName: this.destination.label },
|
||||
}),
|
||||
this.$locale.baseText('settings.log-streaming.destinationDelete.headline'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: this.$locale.baseText(
|
||||
'settings.log-streaming.destinationDelete.confirmButtonText',
|
||||
),
|
||||
cancelButtonText: this.$locale.baseText(
|
||||
'settings.log-streaming.destinationDelete.cancelButtonText',
|
||||
),
|
||||
},
|
||||
);
|
||||
const nodeParameters = ref<MessageEventBusDestinationOptions>({});
|
||||
const cardActions = ref<HTMLDivElement | null>(null);
|
||||
|
||||
if (deleteConfirmed !== MODAL_CONFIRM) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('remove', this.destination.id);
|
||||
}
|
||||
},
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
eventBus: EventBus;
|
||||
destination: MessageEventBusDestinationOptions;
|
||||
readonly: boolean;
|
||||
}>(),
|
||||
{
|
||||
destination: () => deepCopy(defaultMessageEventBusDestinationOptions),
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
edit: [id: string | undefined];
|
||||
remove: [id: string | undefined];
|
||||
}>();
|
||||
|
||||
onMounted(() => {
|
||||
nodeParameters.value = Object.assign(
|
||||
deepCopy(defaultMessageEventBusDestinationOptions),
|
||||
props.destination,
|
||||
);
|
||||
props.eventBus?.on('destinationWasSaved', onDestinationWasSaved);
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
props.eventBus?.off('destinationWasSaved', onDestinationWasSaved);
|
||||
});
|
||||
|
||||
const actions = computed((): Array<{ label: string; value: string }> => {
|
||||
const actionList = [
|
||||
{
|
||||
label: i18n.baseText('workflows.item.open'),
|
||||
value: DESTINATION_LIST_ITEM_ACTIONS.OPEN,
|
||||
},
|
||||
];
|
||||
if (!props.readonly) {
|
||||
actionList.push({
|
||||
label: i18n.baseText('workflows.item.delete'),
|
||||
value: DESTINATION_LIST_ITEM_ACTIONS.DELETE,
|
||||
});
|
||||
}
|
||||
return actionList;
|
||||
});
|
||||
|
||||
const typeLabelName = computed((): BaseTextKey => {
|
||||
return `settings.log-streaming.${props.destination.__type}` as BaseTextKey;
|
||||
});
|
||||
|
||||
function onDestinationWasSaved() {
|
||||
assert(props.destination.id);
|
||||
const updatedDestination = logStreamingStore.getDestination(props.destination.id);
|
||||
if (updatedDestination) {
|
||||
nodeParameters.value = Object.assign(
|
||||
deepCopy(defaultMessageEventBusDestinationOptions),
|
||||
props.destination,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function onClick(event: Event) {
|
||||
const target = event.target as HTMLDivElement | null;
|
||||
if (
|
||||
cardActions.value === target ||
|
||||
cardActions.value?.contains(target) ||
|
||||
target?.contains(cardActions.value)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('edit', props.destination.id);
|
||||
}
|
||||
|
||||
function onEnabledSwitched(state: boolean) {
|
||||
nodeParameters.value.enabled = state;
|
||||
void saveDestination();
|
||||
}
|
||||
|
||||
async function saveDestination() {
|
||||
await logStreamingStore.saveDestination(nodeParameters.value);
|
||||
}
|
||||
|
||||
async function onAction(action: string) {
|
||||
if (action === DESTINATION_LIST_ITEM_ACTIONS.OPEN) {
|
||||
emit('edit', props.destination.id);
|
||||
} else if (action === DESTINATION_LIST_ITEM_ACTIONS.DELETE) {
|
||||
const deleteConfirmed = await confirm(
|
||||
i18n.baseText('settings.log-streaming.destinationDelete.message', {
|
||||
interpolate: { destinationName: props.destination.label ?? '' },
|
||||
}),
|
||||
i18n.baseText('settings.log-streaming.destinationDelete.headline'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: i18n.baseText(
|
||||
'settings.log-streaming.destinationDelete.confirmButtonText',
|
||||
),
|
||||
cancelButtonText: i18n.baseText(
|
||||
'settings.log-streaming.destinationDelete.cancelButtonText',
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
if (deleteConfirmed !== MODAL_CONFIRM) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('remove', props.destination.id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -140,7 +141,7 @@ export default defineComponent({
|
|||
</n8n-heading>
|
||||
<div :class="$style.cardDescription">
|
||||
<n8n-text color="text-light" size="small">
|
||||
<span>{{ $locale.baseText(typeLabelName) }}</span>
|
||||
<span>{{ i18n.baseText(typeLabelName) }}</span>
|
||||
</n8n-text>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -149,10 +150,10 @@ export default defineComponent({
|
|||
<div ref="cardActions" :class="$style.cardActions">
|
||||
<div :class="$style.activeStatusText" data-test-id="destination-activator-status">
|
||||
<n8n-text v-if="nodeParameters.enabled" :color="'success'" size="small" bold>
|
||||
{{ $locale.baseText('workflowActivator.active') }}
|
||||
{{ i18n.baseText('workflowActivator.active') }}
|
||||
</n8n-text>
|
||||
<n8n-text v-else color="text-base" size="small" bold>
|
||||
{{ $locale.baseText('workflowActivator.inactive') }}
|
||||
{{ i18n.baseText('workflowActivator.inactive') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
||||
|
@ -162,8 +163,8 @@ export default defineComponent({
|
|||
:model-value="nodeParameters.enabled"
|
||||
:title="
|
||||
nodeParameters.enabled
|
||||
? $locale.baseText('workflowActivator.deactivateWorkflow')
|
||||
: $locale.baseText('workflowActivator.activateWorkflow')
|
||||
? i18n.baseText('workflowActivator.deactivateWorkflow')
|
||||
: i18n.baseText('workflowActivator.activateWorkflow')
|
||||
"
|
||||
active-color="#13ce66"
|
||||
inactive-color="#8899AA"
|
||||
|
|
|
@ -1,63 +1,45 @@
|
|||
<script lang="ts">
|
||||
import { type PropType, defineComponent } from 'vue';
|
||||
import { filterTemplateNodes } from '@/utils/nodeTypesUtils';
|
||||
<script lang="ts" setup>
|
||||
import { abbreviateNumber } from '@/utils/typesUtils';
|
||||
import NodeList from './NodeList.vue';
|
||||
import TimeAgo from '@/components/TimeAgo.vue';
|
||||
import type { ITemplatesWorkflow } from '@/Interface';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { BaseTextKey } from '@/plugins/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TemplateCard',
|
||||
components: {
|
||||
TimeAgo,
|
||||
NodeList,
|
||||
const i18n = useI18n();
|
||||
|
||||
const nodesToBeShown = 5;
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
workflow?: ITemplatesWorkflow;
|
||||
lastItem?: boolean;
|
||||
firstItem?: boolean;
|
||||
useWorkflowButton?: boolean;
|
||||
loading?: boolean;
|
||||
simpleView?: boolean;
|
||||
}>(),
|
||||
{
|
||||
lastItem: false,
|
||||
firstItem: false,
|
||||
useWorkflowButton: false,
|
||||
loading: false,
|
||||
simpleView: false,
|
||||
},
|
||||
props: {
|
||||
workflow: {
|
||||
type: Object as PropType<ITemplatesWorkflow>,
|
||||
},
|
||||
lastItem: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
firstItem: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
useWorkflowButton: {
|
||||
type: Boolean,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
simpleView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nodesToBeShown: 5,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
filterTemplateNodes,
|
||||
abbreviateNumber,
|
||||
countNodesToBeSliced(nodes: []): number {
|
||||
if (nodes.length > this.nodesToBeShown) {
|
||||
return this.nodesToBeShown - 1;
|
||||
} else {
|
||||
return this.nodesToBeShown;
|
||||
}
|
||||
},
|
||||
onUseWorkflowClick(e: MouseEvent) {
|
||||
this.$emit('useWorkflow', e);
|
||||
},
|
||||
onCardClick(e: MouseEvent) {
|
||||
this.$emit('click', e);
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
useWorkflow: [e: MouseEvent];
|
||||
click: [e: MouseEvent];
|
||||
}>();
|
||||
|
||||
function onUseWorkflowClick(e: MouseEvent) {
|
||||
emit('useWorkflow', e);
|
||||
}
|
||||
|
||||
function onCardClick(e: MouseEvent) {
|
||||
emit('click', e);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -88,8 +70,12 @@ export default defineComponent({
|
|||
<TimeAgo :date="workflow.createdAt" />
|
||||
</n8n-text>
|
||||
<div v-if="workflow.user" :class="$style.line" v-text="'|'" />
|
||||
<n8n-text v-if="workflow.user" size="small" color="text-light"
|
||||
>By {{ workflow.user.username }}</n8n-text
|
||||
<n8n-text v-if="workflow.user" size="small" color="text-light">
|
||||
{{
|
||||
i18n.baseText('template.byAuthor' as BaseTextKey, {
|
||||
interpolate: { name: workflow.user.username },
|
||||
})
|
||||
}}</n8n-text
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,101 +1,104 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { ITemplatesCategory } from '@/Interface';
|
||||
import type { PropType } from 'vue';
|
||||
import { useTemplatesStore } from '@/stores/templates.store';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TemplateFilters',
|
||||
props: {
|
||||
categories: {
|
||||
type: Array as PropType<ITemplatesCategory[]>,
|
||||
default: () => [],
|
||||
},
|
||||
sortOnPopulate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
expandLimit: {
|
||||
type: Number,
|
||||
default: 12,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
selected: {
|
||||
type: Array as PropType<ITemplatesCategory[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['clearAll', 'select', 'clear'],
|
||||
data() {
|
||||
return {
|
||||
collapsed: true,
|
||||
sortedCategories: [] as ITemplatesCategory[],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useTemplatesStore),
|
||||
allSelected(): boolean {
|
||||
return this.selected.length === 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
sortOnPopulate: {
|
||||
handler(value: boolean) {
|
||||
if (value) {
|
||||
this.sortCategories();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
categories: {
|
||||
handler(categories: ITemplatesCategory[]) {
|
||||
if (categories.length > 0) {
|
||||
this.sortCategories();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sortCategories() {
|
||||
if (!this.sortOnPopulate) {
|
||||
this.sortedCategories = this.categories;
|
||||
} else {
|
||||
const selected = this.selected || [];
|
||||
const selectedCategories = this.categories.filter((cat) => selected.includes(cat));
|
||||
const notSelectedCategories = this.categories.filter((cat) => !selected.includes(cat));
|
||||
this.sortedCategories = selectedCategories.concat(notSelectedCategories);
|
||||
}
|
||||
},
|
||||
collapseAction() {
|
||||
this.collapsed = false;
|
||||
},
|
||||
handleCheckboxChanged(value: boolean, selectedCategory: ITemplatesCategory) {
|
||||
this.$emit(value ? 'select' : 'clear', selectedCategory);
|
||||
},
|
||||
isSelected(category: ITemplatesCategory) {
|
||||
return this.selected.includes(category);
|
||||
},
|
||||
resetCategories() {
|
||||
this.$emit('clearAll');
|
||||
},
|
||||
},
|
||||
interface Props {
|
||||
categories?: ITemplatesCategory[];
|
||||
sortOnPopulate?: boolean;
|
||||
expandLimit?: number;
|
||||
loading?: boolean;
|
||||
selected?: ITemplatesCategory[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
categories: () => [],
|
||||
sortOnPopulate: false,
|
||||
expandLimit: 12,
|
||||
loading: false,
|
||||
selected: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
clearAll: [];
|
||||
select: [category: ITemplatesCategory];
|
||||
clear: [category: ITemplatesCategory];
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const collapsed = ref(true);
|
||||
const sortedCategories = ref<ITemplatesCategory[]>([]);
|
||||
|
||||
const allSelected = computed((): boolean => {
|
||||
return props.selected.length === 0;
|
||||
});
|
||||
|
||||
function sortCategories() {
|
||||
if (!props.sortOnPopulate) {
|
||||
sortedCategories.value = props.categories;
|
||||
} else {
|
||||
const selected = props.selected || [];
|
||||
const selectedCategories = props.categories.filter((cat) => selected.includes(cat));
|
||||
const notSelectedCategories = props.categories.filter((cat) => !selected.includes(cat));
|
||||
sortedCategories.value = selectedCategories.concat(notSelectedCategories);
|
||||
}
|
||||
}
|
||||
function collapseAction() {
|
||||
collapsed.value = false;
|
||||
}
|
||||
|
||||
function handleCheckboxChanged(value: boolean, selectedCategory: ITemplatesCategory) {
|
||||
if (value) {
|
||||
emit('select', selectedCategory);
|
||||
} else {
|
||||
emit('clear', selectedCategory);
|
||||
}
|
||||
}
|
||||
|
||||
function isSelected(category: ITemplatesCategory) {
|
||||
return props.selected.includes(category);
|
||||
}
|
||||
|
||||
function resetCategories() {
|
||||
emit('clearAll');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.sortOnPopulate,
|
||||
(value: boolean) => {
|
||||
if (value) {
|
||||
sortCategories();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.categories,
|
||||
(categories: ITemplatesCategory[]) => {
|
||||
if (categories.length > 0) {
|
||||
sortCategories();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.filters" class="template-filters" data-test-id="templates-filter-container">
|
||||
<div :class="$style.title" v-text="$locale.baseText('templates.categoriesHeading')" />
|
||||
<div :class="$style.title" v-text="i18n.baseText('templates.categoriesHeading')" />
|
||||
<div v-if="loading" :class="$style.list">
|
||||
<n8n-loading :loading="loading" :rows="expandLimit" />
|
||||
</div>
|
||||
<ul v-if="!loading" :class="$style.categories">
|
||||
<li :class="$style.item" data-test-id="template-filter-all-categories">
|
||||
<el-checkbox :model-value="allSelected" @update:model-value="() => resetCategories()">
|
||||
{{ $locale.baseText('templates.allCategories') }}
|
||||
{{ i18n.baseText('templates.allCategories') }}
|
||||
</el-checkbox>
|
||||
</li>
|
||||
<li
|
||||
|
|
|
@ -1,33 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { type PropType, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import Card from '@/components/CollectionWorkflowCard.vue';
|
||||
import NodeList from '@/components/NodeList.vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { ITemplatesCollection } from '@/Interface';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TemplatesInfoCard',
|
||||
components: {
|
||||
Card,
|
||||
NodeList,
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
collection: ITemplatesCollection;
|
||||
loading?: boolean;
|
||||
showItemCount?: boolean;
|
||||
width: string;
|
||||
}>(),
|
||||
{
|
||||
loading: false,
|
||||
showItemCount: true,
|
||||
},
|
||||
props: {
|
||||
collection: {
|
||||
type: Object as PropType<ITemplatesCollection>,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
showItemCount: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const i18n = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -36,7 +26,7 @@ export default defineComponent({
|
|||
<span>
|
||||
<n8n-text v-show="showItemCount" size="small" color="text-light">
|
||||
{{ collection.workflows.length }}
|
||||
{{ $locale.baseText('templates.workflows') }}
|
||||
{{ i18n.baseText('templates.workflows') }}
|
||||
</n8n-text>
|
||||
</span>
|
||||
<NodeList :nodes="collection.nodes" :show-more="false" />
|
||||
|
|
|
@ -1991,6 +1991,7 @@
|
|||
"template.details.details": "Details",
|
||||
"template.details.times": "times",
|
||||
"template.details.viewed": "Viewed",
|
||||
"template.byAuthor": "By {name}",
|
||||
"templates.allCategories": "All Categories",
|
||||
"templates.categoriesHeading": "Categories",
|
||||
"templates.collection": "Collection",
|
||||
|
|
|
@ -1,142 +1,122 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { ApiKey, IUser } from '@/Interface';
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import type { ApiKey } from '@/Interface';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
|
||||
import CopyInput from '@/components/CopyInput.vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useRootStore } from '@/stores/root.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||
import { DOCS_DOMAIN, MODAL_CONFIRM } from '@/constants';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SettingsApiView',
|
||||
components: {
|
||||
CopyInput,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
...useToast(),
|
||||
...useMessage(),
|
||||
...useUIStore(),
|
||||
pageRedirectionHelper: usePageRedirectionHelper(),
|
||||
documentTitle: useDocumentTitle(),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
mounted: false,
|
||||
apiKeys: [] as ApiKey[],
|
||||
swaggerUIEnabled: false,
|
||||
apiDocsURL: '',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.documentTitle.set(this.$locale.baseText('settings.api'));
|
||||
if (!this.isPublicApiEnabled) return;
|
||||
const settingsStore = useSettingsStore();
|
||||
const cloudPlanStore = useCloudPlanStore();
|
||||
const { baseUrl } = useRootStore();
|
||||
|
||||
void this.getApiKeys();
|
||||
const baseUrl = this.rootStore.baseUrl;
|
||||
const apiPath = this.settingsStore.publicApiPath;
|
||||
const latestVersion = this.settingsStore.publicApiLatestVersion;
|
||||
this.swaggerUIEnabled = this.settingsStore.isSwaggerUIEnabled;
|
||||
this.apiDocsURL = this.swaggerUIEnabled
|
||||
? `${baseUrl}${apiPath}/v${latestVersion}/docs`
|
||||
: `https://${DOCS_DOMAIN}/api/api-reference/`;
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore, useSettingsStore, useUsersStore, useCloudPlanStore, useUIStore),
|
||||
currentUser(): IUser | null {
|
||||
return this.usersStore.currentUser;
|
||||
},
|
||||
isTrialing(): boolean {
|
||||
return this.cloudPlanStore.userIsTrialing;
|
||||
},
|
||||
isLoadingCloudPlans(): boolean {
|
||||
return this.cloudPlanStore.state.loadingPlan;
|
||||
},
|
||||
isPublicApiEnabled(): boolean {
|
||||
return this.settingsStore.isPublicApiEnabled;
|
||||
},
|
||||
isRedactedApiKey(): boolean {
|
||||
if (!this.apiKeys) return false;
|
||||
return this.apiKeys[0].apiKey.includes('*');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onUpgrade() {
|
||||
void this.pageRedirectionHelper.goToUpgrade('settings-n8n-api', 'upgrade-api', 'redirect');
|
||||
},
|
||||
async showDeleteModal() {
|
||||
const confirmed = await this.confirm(
|
||||
this.$locale.baseText('settings.api.delete.description'),
|
||||
this.$locale.baseText('settings.api.delete.title'),
|
||||
{
|
||||
confirmButtonText: this.$locale.baseText('settings.api.delete.button'),
|
||||
cancelButtonText: this.$locale.baseText('generic.cancel'),
|
||||
},
|
||||
);
|
||||
if (confirmed === MODAL_CONFIRM) {
|
||||
await this.deleteApiKey();
|
||||
}
|
||||
},
|
||||
async getApiKeys() {
|
||||
try {
|
||||
this.apiKeys = await this.settingsStore.getApiKeys();
|
||||
} catch (error) {
|
||||
this.showError(error, this.$locale.baseText('settings.api.view.error'));
|
||||
} finally {
|
||||
this.mounted = true;
|
||||
}
|
||||
},
|
||||
async createApiKey() {
|
||||
this.loading = true;
|
||||
const { showError, showMessage } = useToast();
|
||||
const { confirm } = useMessage();
|
||||
const documentTitle = useDocumentTitle();
|
||||
const i18n = useI18n();
|
||||
const { goToUpgrade } = usePageRedirectionHelper();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
try {
|
||||
const newApiKey = await this.settingsStore.createApiKey();
|
||||
this.apiKeys.push(newApiKey);
|
||||
} catch (error) {
|
||||
this.showError(error, this.$locale.baseText('settings.api.create.error'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.$telemetry.track('User clicked create API key button');
|
||||
}
|
||||
},
|
||||
async deleteApiKey() {
|
||||
try {
|
||||
await this.settingsStore.deleteApiKey(this.apiKeys[0].id);
|
||||
this.showMessage({
|
||||
title: this.$locale.baseText('settings.api.delete.toast'),
|
||||
type: 'success',
|
||||
});
|
||||
this.apiKeys = [];
|
||||
} catch (error) {
|
||||
this.showError(error, this.$locale.baseText('settings.api.delete.error'));
|
||||
} finally {
|
||||
this.$telemetry.track('User clicked delete API key button');
|
||||
}
|
||||
},
|
||||
onCopy() {
|
||||
this.$telemetry.track('User clicked copy API key button');
|
||||
},
|
||||
},
|
||||
const loading = ref(false);
|
||||
const mounted = ref(false);
|
||||
const apiKeys = ref<ApiKey[]>([]);
|
||||
const apiDocsURL = ref('');
|
||||
|
||||
const { isPublicApiEnabled, isSwaggerUIEnabled, publicApiPath, publicApiLatestVersion } =
|
||||
settingsStore;
|
||||
|
||||
const isRedactedApiKey = computed((): boolean => {
|
||||
if (!apiKeys.value) return false;
|
||||
return apiKeys.value[0].apiKey.includes('*');
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
documentTitle.set(i18n.baseText('settings.api'));
|
||||
if (!isPublicApiEnabled) return;
|
||||
|
||||
void getApiKeys();
|
||||
apiDocsURL.value = isSwaggerUIEnabled
|
||||
? `${baseUrl}${publicApiPath}/v${publicApiLatestVersion}/docs`
|
||||
: `https://${DOCS_DOMAIN}/api/api-reference/`;
|
||||
});
|
||||
|
||||
function onUpgrade() {
|
||||
void goToUpgrade('settings-n8n-api', 'upgrade-api', 'redirect');
|
||||
}
|
||||
|
||||
async function showDeleteModal() {
|
||||
const confirmed = await confirm(
|
||||
i18n.baseText('settings.api.delete.description'),
|
||||
i18n.baseText('settings.api.delete.title'),
|
||||
{
|
||||
confirmButtonText: i18n.baseText('settings.api.delete.button'),
|
||||
cancelButtonText: i18n.baseText('generic.cancel'),
|
||||
},
|
||||
);
|
||||
if (confirmed === MODAL_CONFIRM) {
|
||||
await deleteApiKey();
|
||||
}
|
||||
}
|
||||
|
||||
async function getApiKeys() {
|
||||
try {
|
||||
apiKeys.value = await settingsStore.getApiKeys();
|
||||
} catch (error) {
|
||||
showError(error, i18n.baseText('settings.api.view.error'));
|
||||
} finally {
|
||||
mounted.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function createApiKey() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const newApiKey = await settingsStore.createApiKey();
|
||||
apiKeys.value.push(newApiKey);
|
||||
} catch (error) {
|
||||
showError(error, i18n.baseText('settings.api.create.error'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
telemetry.track('User clicked create API key button');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteApiKey() {
|
||||
try {
|
||||
await settingsStore.deleteApiKey(apiKeys.value[0].id);
|
||||
showMessage({
|
||||
title: i18n.baseText('settings.api.delete.toast'),
|
||||
type: 'success',
|
||||
});
|
||||
apiKeys.value = [];
|
||||
} catch (error) {
|
||||
showError(error, i18n.baseText('settings.api.delete.error'));
|
||||
} finally {
|
||||
telemetry.track('User clicked delete API key button');
|
||||
}
|
||||
}
|
||||
|
||||
function onCopy() {
|
||||
telemetry.track('User clicked copy API key button');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.header">
|
||||
<n8n-heading size="2xlarge">
|
||||
{{ $locale.baseText('settings.api') }}
|
||||
{{ i18n.baseText('settings.api') }}
|
||||
<span :style="{ fontSize: 'var(--font-size-s)', color: 'var(--color-text-light)' }">
|
||||
({{ $locale.baseText('generic.beta') }})
|
||||
({{ i18n.baseText('generic.beta') }})
|
||||
</span>
|
||||
</n8n-heading>
|
||||
</div>
|
||||
|
@ -149,14 +129,14 @@ export default defineComponent({
|
|||
<a
|
||||
href="https://docs.n8n.io/api"
|
||||
target="_blank"
|
||||
v-text="$locale.baseText('settings.api.view.info.api')"
|
||||
v-text="i18n.baseText('settings.api.view.info.api')"
|
||||
/>
|
||||
</template>
|
||||
<template #webhookAction>
|
||||
<a
|
||||
href="https://docs.n8n.io/integrations/core-nodes/n8n-nodes-base.webhook/"
|
||||
target="_blank"
|
||||
v-text="$locale.baseText('settings.api.view.info.webhook')"
|
||||
v-text="i18n.baseText('settings.api.view.info.webhook')"
|
||||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
@ -165,7 +145,7 @@ export default defineComponent({
|
|||
<n8n-card class="mb-4xs" :class="$style.card">
|
||||
<span :class="$style.delete">
|
||||
<n8n-link :bold="true" @click="showDeleteModal">
|
||||
{{ $locale.baseText('generic.delete') }}
|
||||
{{ i18n.baseText('generic.delete') }}
|
||||
</n8n-link>
|
||||
</span>
|
||||
|
||||
|
@ -173,47 +153,43 @@ export default defineComponent({
|
|||
<CopyInput
|
||||
:label="apiKeys[0].label"
|
||||
:value="apiKeys[0].apiKey"
|
||||
:copy-button-text="$locale.baseText('generic.clickToCopy')"
|
||||
:toast-title="$locale.baseText('settings.api.view.copy.toast')"
|
||||
:copy-button-text="i18n.baseText('generic.clickToCopy')"
|
||||
:toast-title="i18n.baseText('settings.api.view.copy.toast')"
|
||||
:redact-value="true"
|
||||
:disable-copy="isRedactedApiKey"
|
||||
:hint="!isRedactedApiKey ? $locale.baseText('settings.api.view.copy') : ''"
|
||||
:hint="!isRedactedApiKey ? i18n.baseText('settings.api.view.copy') : ''"
|
||||
@copy="onCopy"
|
||||
/>
|
||||
</div>
|
||||
</n8n-card>
|
||||
<div :class="$style.hint">
|
||||
<n8n-text size="small">
|
||||
{{
|
||||
$locale.baseText(`settings.api.view.${swaggerUIEnabled ? 'tryapi' : 'more-details'}`)
|
||||
}}
|
||||
{{ i18n.baseText(`settings.api.view.${isSwaggerUIEnabled ? 'tryapi' : 'more-details'}`) }}
|
||||
</n8n-text>
|
||||
{{ ' ' }}
|
||||
<n8n-link :to="apiDocsURL" :new-window="true" size="small">
|
||||
{{
|
||||
$locale.baseText(
|
||||
`settings.api.view.${swaggerUIEnabled ? 'apiPlayground' : 'external-docs'}`,
|
||||
i18n.baseText(
|
||||
`settings.api.view.${isSwaggerUIEnabled ? 'apiPlayground' : 'external-docs'}`,
|
||||
)
|
||||
}}
|
||||
</n8n-link>
|
||||
</div>
|
||||
</div>
|
||||
<n8n-action-box
|
||||
v-else-if="!isPublicApiEnabled && isTrialing"
|
||||
v-else-if="!isPublicApiEnabled && cloudPlanStore.userIsTrialing"
|
||||
data-test-id="public-api-upgrade-cta"
|
||||
:heading="$locale.baseText('settings.api.trial.upgradePlan.title')"
|
||||
:description="$locale.baseText('settings.api.trial.upgradePlan.description')"
|
||||
:button-text="$locale.baseText('settings.api.trial.upgradePlan.cta')"
|
||||
:heading="i18n.baseText('settings.api.trial.upgradePlan.title')"
|
||||
:description="i18n.baseText('settings.api.trial.upgradePlan.description')"
|
||||
:button-text="i18n.baseText('settings.api.trial.upgradePlan.cta')"
|
||||
@click:button="onUpgrade"
|
||||
/>
|
||||
<n8n-action-box
|
||||
v-else-if="mounted && !isLoadingCloudPlans"
|
||||
v-else-if="mounted && !cloudPlanStore.state.loadingPlan"
|
||||
:button-text="
|
||||
$locale.baseText(
|
||||
loading ? 'settings.api.create.button.loading' : 'settings.api.create.button',
|
||||
)
|
||||
i18n.baseText(loading ? 'settings.api.create.button.loading' : 'settings.api.create.button')
|
||||
"
|
||||
:description="$locale.baseText('settings.api.create.description')"
|
||||
:description="i18n.baseText('settings.api.create.description')"
|
||||
@click:button="createApiKey"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, nextTick } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onBeforeMount, onMounted } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { hasPermission } from '@/utils/rbac/permissions';
|
||||
|
@ -14,154 +13,158 @@ import { deepCopy, defaultMessageEventBusDestinationOptions } from 'n8n-workflow
|
|||
import EventDestinationCard from '@/components/SettingsLogStreaming/EventDestinationCard.ee.vue';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
import { ref, getCurrentInstance } from 'vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SettingsLogStreamingView',
|
||||
components: {
|
||||
EventDestinationCard,
|
||||
},
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
eventBus: createEventBus(),
|
||||
destinations: Array<MessageEventBusDestinationOptions>,
|
||||
disableLicense: false,
|
||||
allDestinations: [] as MessageEventBusDestinationOptions[],
|
||||
documentTitle: useDocumentTitle(),
|
||||
pageRedirectionHelper: usePageRedirectionHelper(),
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.documentTitle.set(this.$locale.baseText('settings.log-streaming.heading'));
|
||||
if (!this.isLicensed) return;
|
||||
const environment = process.env.NODE_ENV;
|
||||
|
||||
// Prepare credentialsStore so modals can pick up credentials
|
||||
await this.credentialsStore.fetchCredentialTypes(false);
|
||||
await this.credentialsStore.fetchAllCredentials();
|
||||
this.uiStore.nodeViewInitialized = false;
|
||||
const settingsStore = useSettingsStore();
|
||||
const logStreamingStore = useLogStreamingStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const uiStore = useUIStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const documentTitle = useDocumentTitle();
|
||||
const i18n = useI18n();
|
||||
|
||||
// fetch Destination data from the backend
|
||||
await this.getDestinationDataFromBackend();
|
||||
const pageRedirectHelper = usePageRedirectionHelper();
|
||||
|
||||
// since we are not really integrated into the hooks, we listen to the store and refresh the destinations
|
||||
this.logStreamingStore.$onAction(({ name, after }) => {
|
||||
if (name === 'removeDestination' || name === 'updateDestination') {
|
||||
after(async () => {
|
||||
this.$forceUpdate();
|
||||
});
|
||||
}
|
||||
});
|
||||
// refresh when a modal closes
|
||||
this.eventBus.on('destinationWasSaved', this.onDestinationWasSaved);
|
||||
// listen to remove emission
|
||||
this.eventBus.on('remove', this.onRemove);
|
||||
// listen to modal closing and remove nodes from store
|
||||
this.eventBus.on('closing', this.onBusClosing);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.eventBus.off('destinationWasSaved', this.onDestinationWasSaved);
|
||||
this.eventBus.off('remove', this.onRemove);
|
||||
this.eventBus.off('closing', this.onBusClosing);
|
||||
},
|
||||
computed: {
|
||||
...mapStores(
|
||||
useSettingsStore,
|
||||
useLogStreamingStore,
|
||||
useWorkflowsStore,
|
||||
useUIStore,
|
||||
useCredentialsStore,
|
||||
),
|
||||
sortedItemKeysByLabel() {
|
||||
const sortedKeys: Array<{ label: string; key: string }> = [];
|
||||
for (const [key, value] of Object.entries(this.logStreamingStore.items)) {
|
||||
sortedKeys.push({ key, label: value.destination?.label ?? 'Destination' });
|
||||
}
|
||||
return sortedKeys.sort((a, b) => a.label.localeCompare(b.label));
|
||||
},
|
||||
environment() {
|
||||
return process.env.NODE_ENV;
|
||||
},
|
||||
isLicensed(): boolean {
|
||||
if (this.disableLicense) return false;
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.LogStreaming];
|
||||
},
|
||||
canManageLogStreaming(): boolean {
|
||||
return hasPermission(['rbac'], { rbac: { scope: 'logStreaming:manage' } });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onDestinationWasSaved() {
|
||||
this.$forceUpdate();
|
||||
},
|
||||
onBusClosing() {
|
||||
this.workflowsStore.removeAllNodes({ setStateDirty: false, removePinData: true });
|
||||
this.uiStore.stateIsDirty = false;
|
||||
},
|
||||
async getDestinationDataFromBackend(): Promise<void> {
|
||||
this.logStreamingStore.clearEventNames();
|
||||
this.logStreamingStore.clearDestinations();
|
||||
this.allDestinations = [];
|
||||
const eventNamesData = await this.logStreamingStore.fetchEventNames();
|
||||
if (eventNamesData) {
|
||||
for (const eventName of eventNamesData) {
|
||||
this.logStreamingStore.addEventName(eventName);
|
||||
}
|
||||
}
|
||||
const destinationData: MessageEventBusDestinationOptions[] =
|
||||
await this.logStreamingStore.fetchDestinations();
|
||||
if (destinationData) {
|
||||
for (const destination of destinationData) {
|
||||
this.logStreamingStore.addDestination(destination);
|
||||
this.allDestinations.push(destination);
|
||||
}
|
||||
}
|
||||
this.$forceUpdate();
|
||||
},
|
||||
goToUpgrade() {
|
||||
void this.pageRedirectionHelper.goToUpgrade('log-streaming', 'upgrade-log-streaming');
|
||||
},
|
||||
storeHasItems(): boolean {
|
||||
return this.logStreamingStore.items && Object.keys(this.logStreamingStore.items).length > 0;
|
||||
},
|
||||
async addDestination() {
|
||||
const newDestination = deepCopy(defaultMessageEventBusDestinationOptions);
|
||||
newDestination.id = uuid();
|
||||
this.logStreamingStore.addDestination(newDestination);
|
||||
await nextTick();
|
||||
this.uiStore.openModalWithData({
|
||||
name: LOG_STREAM_MODAL_KEY,
|
||||
data: {
|
||||
destination: newDestination,
|
||||
isNew: true,
|
||||
eventBus: this.eventBus,
|
||||
},
|
||||
});
|
||||
},
|
||||
async onRemove(destinationId?: string) {
|
||||
if (!destinationId) return;
|
||||
await this.logStreamingStore.deleteDestination(destinationId);
|
||||
const foundNode = this.workflowsStore.getNodeByName(destinationId);
|
||||
if (foundNode) {
|
||||
this.workflowsStore.removeNode(foundNode);
|
||||
}
|
||||
},
|
||||
async onEdit(destinationId?: string) {
|
||||
if (!destinationId) return;
|
||||
const editDestination = this.logStreamingStore.getDestination(destinationId);
|
||||
if (editDestination) {
|
||||
this.uiStore.openModalWithData({
|
||||
name: LOG_STREAM_MODAL_KEY,
|
||||
data: {
|
||||
destination: editDestination,
|
||||
isNew: false,
|
||||
eventBus: this.eventBus,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
const eventBus = createEventBus();
|
||||
const disableLicense = ref(false);
|
||||
const allDestinations = ref<MessageEventBusDestinationOptions[]>([]);
|
||||
|
||||
const sortedItemKeysByLabel = computed(() => {
|
||||
const sortedKeys: Array<{ label: string; key: string }> = [];
|
||||
for (const [key, value] of Object.entries(logStreamingStore.items)) {
|
||||
sortedKeys.push({ key, label: value.destination?.label ?? 'Destination' });
|
||||
}
|
||||
return sortedKeys.sort((a, b) => a.label.localeCompare(b.label));
|
||||
});
|
||||
|
||||
const isLicensed = computed((): boolean => {
|
||||
if (disableLicense.value) return false;
|
||||
return settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.LogStreaming];
|
||||
});
|
||||
|
||||
const canManageLogStreaming = computed((): boolean => {
|
||||
return hasPermission(['rbac'], { rbac: { scope: 'logStreaming:manage' } });
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
documentTitle.set(i18n.baseText('settings.log-streaming.heading'));
|
||||
if (!isLicensed.value) return;
|
||||
|
||||
// Prepare credentialsStore so modals can pick up credentials
|
||||
await credentialsStore.fetchCredentialTypes(false);
|
||||
await credentialsStore.fetchAllCredentials();
|
||||
uiStore.nodeViewInitialized = false;
|
||||
|
||||
// fetch Destination data from the backend
|
||||
await getDestinationDataFromBackend();
|
||||
|
||||
// since we are not really integrated into the hooks, we listen to the store and refresh the destinations
|
||||
logStreamingStore.$onAction(({ name, after }) => {
|
||||
if (name === 'removeDestination' || name === 'updateDestination') {
|
||||
after(async () => {
|
||||
forceUpdateInstance();
|
||||
});
|
||||
}
|
||||
});
|
||||
// refresh when a modal closes
|
||||
eventBus.on('destinationWasSaved', onDestinationWasSaved);
|
||||
// listen to remove emission
|
||||
eventBus.on('remove', onRemove);
|
||||
// listen to modal closing and remove nodes from store
|
||||
eventBus.on('closing', onBusClosing);
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
eventBus.off('destinationWasSaved', onDestinationWasSaved);
|
||||
eventBus.off('remove', onRemove);
|
||||
eventBus.off('closing', onBusClosing);
|
||||
});
|
||||
|
||||
function onDestinationWasSaved() {
|
||||
forceUpdateInstance();
|
||||
}
|
||||
|
||||
function forceUpdateInstance() {
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$forceUpdate();
|
||||
}
|
||||
|
||||
function onBusClosing() {
|
||||
workflowsStore.removeAllNodes({ setStateDirty: false, removePinData: true });
|
||||
uiStore.stateIsDirty = false;
|
||||
}
|
||||
|
||||
async function getDestinationDataFromBackend(): Promise<void> {
|
||||
logStreamingStore.clearEventNames();
|
||||
logStreamingStore.clearDestinations();
|
||||
allDestinations.value = [];
|
||||
const eventNamesData = await logStreamingStore.fetchEventNames();
|
||||
if (eventNamesData) {
|
||||
for (const eventName of eventNamesData) {
|
||||
logStreamingStore.addEventName(eventName);
|
||||
}
|
||||
}
|
||||
const destinationData: MessageEventBusDestinationOptions[] =
|
||||
await logStreamingStore.fetchDestinations();
|
||||
if (destinationData) {
|
||||
for (const destination of destinationData) {
|
||||
logStreamingStore.addDestination(destination);
|
||||
allDestinations.value.push(destination);
|
||||
}
|
||||
}
|
||||
forceUpdateInstance();
|
||||
}
|
||||
|
||||
function goToUpgrade() {
|
||||
void pageRedirectHelper.goToUpgrade('log-streaming', 'upgrade-log-streaming');
|
||||
}
|
||||
|
||||
function storeHasItems(): boolean {
|
||||
return logStreamingStore.items && Object.keys(logStreamingStore.items).length > 0;
|
||||
}
|
||||
|
||||
async function addDestination() {
|
||||
const newDestination = deepCopy(defaultMessageEventBusDestinationOptions);
|
||||
newDestination.id = uuid();
|
||||
logStreamingStore.addDestination(newDestination);
|
||||
await nextTick();
|
||||
uiStore.openModalWithData({
|
||||
name: LOG_STREAM_MODAL_KEY,
|
||||
data: {
|
||||
destination: newDestination,
|
||||
isNew: true,
|
||||
eventBus,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function onRemove(destinationId?: string) {
|
||||
if (!destinationId) return;
|
||||
await logStreamingStore.deleteDestination(destinationId);
|
||||
const foundNode = workflowsStore.getNodeByName(destinationId);
|
||||
if (foundNode) {
|
||||
workflowsStore.removeNode(foundNode);
|
||||
}
|
||||
}
|
||||
|
||||
async function onEdit(destinationId?: string) {
|
||||
if (!destinationId) return;
|
||||
const editDestination = logStreamingStore.getDestination(destinationId);
|
||||
if (editDestination) {
|
||||
uiStore.openModalWithData({
|
||||
name: LOG_STREAM_MODAL_KEY,
|
||||
data: {
|
||||
destination: editDestination,
|
||||
isNew: false,
|
||||
eventBus,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -169,7 +172,7 @@ export default defineComponent({
|
|||
<div :class="$style.header">
|
||||
<div class="mb-2xl">
|
||||
<n8n-heading size="2xlarge">
|
||||
{{ $locale.baseText(`settings.log-streaming.heading`) }}
|
||||
{{ i18n.baseText(`settings.log-streaming.heading`) }}
|
||||
</n8n-heading>
|
||||
<template v-if="environment !== 'production'">
|
||||
<strong class="ml-m">Disable License ({{ environment }}) </strong>
|
||||
|
@ -180,7 +183,7 @@ export default defineComponent({
|
|||
<template v-if="isLicensed">
|
||||
<div class="mb-l">
|
||||
<n8n-info-tip theme="info" type="note">
|
||||
<span v-n8n-html="$locale.baseText('settings.log-streaming.infoText')"></span>
|
||||
<span v-n8n-html="i18n.baseText('settings.log-streaming.infoText')"></span>
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
<template v-if="storeHasItems()">
|
||||
|
@ -202,35 +205,35 @@ export default defineComponent({
|
|||
</el-row>
|
||||
<div class="mt-m text-right">
|
||||
<n8n-button v-if="canManageLogStreaming" size="large" @click="addDestination">
|
||||
{{ $locale.baseText(`settings.log-streaming.add`) }}
|
||||
{{ i18n.baseText(`settings.log-streaming.add`) }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else data-test-id="action-box-licensed">
|
||||
<n8n-action-box
|
||||
:button-text="$locale.baseText(`settings.log-streaming.add`)"
|
||||
:button-text="i18n.baseText(`settings.log-streaming.add`)"
|
||||
@click:button="addDestination"
|
||||
>
|
||||
<template #heading>
|
||||
<span v-n8n-html="$locale.baseText(`settings.log-streaming.addFirstTitle`)" />
|
||||
<span v-n8n-html="i18n.baseText(`settings.log-streaming.addFirstTitle`)" />
|
||||
</template>
|
||||
</n8n-action-box>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="$locale.baseText('settings.log-streaming.infoText')" class="mb-l">
|
||||
<div v-if="i18n.baseText('settings.log-streaming.infoText')" class="mb-l">
|
||||
<n8n-info-tip theme="info" type="note">
|
||||
<span v-n8n-html="$locale.baseText('settings.log-streaming.infoText')"></span>
|
||||
<span v-n8n-html="i18n.baseText('settings.log-streaming.infoText')"></span>
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
<div data-test-id="action-box-unlicensed">
|
||||
<n8n-action-box
|
||||
:description="$locale.baseText('settings.log-streaming.actionBox.description')"
|
||||
:button-text="$locale.baseText('settings.log-streaming.actionBox.button')"
|
||||
:description="i18n.baseText('settings.log-streaming.actionBox.description')"
|
||||
:button-text="i18n.baseText('settings.log-streaming.actionBox.button')"
|
||||
@click:button="goToUpgrade"
|
||||
>
|
||||
<template #heading>
|
||||
<span v-n8n-html="$locale.baseText('settings.log-streaming.actionBox.title')" />
|
||||
<span v-n8n-html="i18n.baseText('settings.log-streaming.actionBox.title')" />
|
||||
</template>
|
||||
</n8n-action-box>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue