mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-22 12:19:09 +00:00
fix(editor): Show additional data on Users list page (#17339)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -15,8 +15,8 @@ export const USERS_LIST_SORT_OPTIONS = [
|
|||||||
'role:desc',
|
'role:desc',
|
||||||
'mfaEnabled:asc',
|
'mfaEnabled:asc',
|
||||||
'mfaEnabled:desc',
|
'mfaEnabled:desc',
|
||||||
// 'lastActive:asc',
|
'lastActiveAt:asc',
|
||||||
// 'lastActive:desc',
|
'lastActiveAt:desc',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type UsersListSortOptions = (typeof USERS_LIST_SORT_OPTIONS)[number];
|
export type UsersListSortOptions = (typeof USERS_LIST_SORT_OPTIONS)[number];
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export const userListItemSchema = z.object({
|
|||||||
signInType: z.string().optional(),
|
signInType: z.string().optional(),
|
||||||
settings: userSettingsSchema.nullable().optional(),
|
settings: userSettingsSchema.nullable().optional(),
|
||||||
personalizationAnswers: z.object({}).passthrough().nullable().optional(),
|
personalizationAnswers: z.object({}).passthrough().nullable().optional(),
|
||||||
lastActive: z.string().optional(),
|
|
||||||
projectRelations: z.array(userProjectSchema).nullable().optional(),
|
projectRelations: z.array(userProjectSchema).nullable().optional(),
|
||||||
mfaEnabled: z.boolean().optional(),
|
mfaEnabled: z.boolean().optional(),
|
||||||
lastActiveAt: z.string().nullable().optional(),
|
lastActiveAt: z.string().nullable().optional(),
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"generic.missing.permissions": "Missing permissions to perform this action",
|
"generic.missing.permissions": "Missing permissions to perform this action",
|
||||||
"generic.shortcutHint": "Or press",
|
"generic.shortcutHint": "Or press",
|
||||||
"generic.upgradeToEnterprise": "Upgrade to Enterprise",
|
"generic.upgradeToEnterprise": "Upgrade to Enterprise",
|
||||||
|
"generic.never": "Never",
|
||||||
"about.aboutN8n": "About n8n",
|
"about.aboutN8n": "About n8n",
|
||||||
"about.close": "Close",
|
"about.close": "Close",
|
||||||
"about.license": "License",
|
"about.license": "License",
|
||||||
@@ -1953,6 +1954,7 @@
|
|||||||
"settings.personal.security": "Security",
|
"settings.personal.security": "Security",
|
||||||
"settings.signup.signUpInviterInfo": "{firstName} {lastName} has invited you to n8n",
|
"settings.signup.signUpInviterInfo": "{firstName} {lastName} has invited you to n8n",
|
||||||
"settings.users": "Users",
|
"settings.users": "Users",
|
||||||
|
"settings.users.count": "{count} user | {count} users",
|
||||||
"settings.users.search.placeholder": "Search by name or email",
|
"settings.users.search.placeholder": "Search by name or email",
|
||||||
"settings.users.confirmDataHandlingAfterDeletion": "What should we do with their data?",
|
"settings.users.confirmDataHandlingAfterDeletion": "What should we do with their data?",
|
||||||
"settings.users.confirmUserDeletion": "Are you sure you want to delete this invited user?",
|
"settings.users.confirmUserDeletion": "Are you sure you want to delete this invited user?",
|
||||||
@@ -2014,7 +2016,6 @@
|
|||||||
"settings.users.usersInvited": "Users invited",
|
"settings.users.usersInvited": "Users invited",
|
||||||
"settings.users.usersInvitedError": "Could not invite users",
|
"settings.users.usersInvitedError": "Could not invite users",
|
||||||
"settings.users.advancedPermissions.warning": "{link} to unlock the ability to create additional admin users",
|
"settings.users.advancedPermissions.warning": "{link} to unlock the ability to create additional admin users",
|
||||||
"settings.users.advancedPermissions.warning.link": "Upgrade",
|
|
||||||
"settings.users.userRoleUpdated": "Changes saved",
|
"settings.users.userRoleUpdated": "Changes saved",
|
||||||
"settings.users.userRoleUpdated.message": "{user} has been successfully updated to a {role}",
|
"settings.users.userRoleUpdated.message": "{user} has been successfully updated to a {role}",
|
||||||
"settings.users.userRoleUpdatedError": "Unable to updated role",
|
"settings.users.userRoleUpdatedError": "Unable to updated role",
|
||||||
@@ -2032,7 +2033,6 @@
|
|||||||
"settings.users.table.row.2fa.disabled": "@:_reusableBaseText.disabled",
|
"settings.users.table.row.2fa.disabled": "@:_reusableBaseText.disabled",
|
||||||
"settings.api": "API",
|
"settings.api": "API",
|
||||||
"settings.api.scopes.upgrade": "{link} to unlock the ability to modify API key scopes",
|
"settings.api.scopes.upgrade": "{link} to unlock the ability to modify API key scopes",
|
||||||
"settings.api.scopes.upgrade.link": "Upgrade",
|
|
||||||
"settings.n8napi": "n8n API",
|
"settings.n8napi": "n8n API",
|
||||||
"settings.log-streaming": "Log Streaming",
|
"settings.log-streaming": "Log Streaming",
|
||||||
"settings.log-streaming.heading": "Log Streaming",
|
"settings.log-streaming.heading": "Log Streaming",
|
||||||
@@ -2352,6 +2352,10 @@
|
|||||||
"templates.workflows": "Workflows",
|
"templates.workflows": "Workflows",
|
||||||
"templates.workflowsNotFound": "Workflow could not be found",
|
"templates.workflowsNotFound": "Workflow could not be found",
|
||||||
"textEdit.edit": "Edit",
|
"textEdit.edit": "Edit",
|
||||||
|
"userActivity.daysAgo": "{count} days ago",
|
||||||
|
"userActivity.lastTime": "Last {time}",
|
||||||
|
"userActivity.today": "Today",
|
||||||
|
"userActivity.yesterday": "Yesterday",
|
||||||
"timeAgo.daysAgo": "%s days ago",
|
"timeAgo.daysAgo": "%s days ago",
|
||||||
"timeAgo.hoursAgo": "%s hours ago",
|
"timeAgo.hoursAgo": "%s hours ago",
|
||||||
"timeAgo.inDays": "in %s days",
|
"timeAgo.inDays": "in %s days",
|
||||||
@@ -3256,7 +3260,6 @@
|
|||||||
"insights.dashboard.table.title": "Breakdown by workflow",
|
"insights.dashboard.table.title": "Breakdown by workflow",
|
||||||
"insights.dashboard.table.estimate": "Estimate",
|
"insights.dashboard.table.estimate": "Estimate",
|
||||||
"insights.dashboard.title": "Insights",
|
"insights.dashboard.title": "Insights",
|
||||||
"insights.dashboard.paywall.cta": "Upgrade",
|
|
||||||
"insights.dashboard.paywall.title": "Upgrade to access more detailed insights",
|
"insights.dashboard.paywall.title": "Upgrade to access more detailed insights",
|
||||||
"insights.dashboard.paywall.description": "Gain access to more granular, per-workflow insights and visual breakdown of production executions over different time periods.",
|
"insights.dashboard.paywall.description": "Gain access to more granular, per-workflow insights and visual breakdown of production executions over different time periods.",
|
||||||
"insights.banner.title.timeSaved.tooltip": "Total time saved calculated from your estimated time savings per execution across all workflows",
|
"insights.banner.title.timeSaved.tooltip": "Total time saved calculated from your estimated time savings per execution across all workflows",
|
||||||
@@ -3281,7 +3284,6 @@
|
|||||||
"communityNodeInfo.contact.admin": "Please contact an administrator to install this community node:",
|
"communityNodeInfo.contact.admin": "Please contact an administrator to install this community node:",
|
||||||
"communityNodeUpdateInfo.available": "A new node package version is available",
|
"communityNodeUpdateInfo.available": "A new node package version is available",
|
||||||
"insights.upgradeModal.button.dismiss": "Dismiss",
|
"insights.upgradeModal.button.dismiss": "Dismiss",
|
||||||
"insights.upgradeModal.button.upgrade": "Upgrade",
|
|
||||||
"insights.upgradeModal.content": "Viewing this time period requires an enterprise plan. Upgrade to Enterprise to unlock advanced features.",
|
"insights.upgradeModal.content": "Viewing this time period requires an enterprise plan. Upgrade to Enterprise to unlock advanced features.",
|
||||||
"insights.upgradeModal.perks.0": "View up to one year of insights history",
|
"insights.upgradeModal.perks.0": "View up to one year of insights history",
|
||||||
"insights.upgradeModal.perks.1": "Zoom into last 24 hours with hourly granularity",
|
"insights.upgradeModal.perks.1": "Zoom into last 24 hours with hourly granularity",
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ function goToUpgradeApiKeyScopes() {
|
|||||||
<I18nT keypath="settings.api.scopes.upgrade" scope="global">
|
<I18nT keypath="settings.api.scopes.upgrade" scope="global">
|
||||||
<template #link>
|
<template #link>
|
||||||
<n8n-link size="small" @click="goToUpgradeApiKeyScopes">
|
<n8n-link size="small" @click="goToUpgradeApiKeyScopes">
|
||||||
{{ i18n.baseText('settings.api.scopes.upgrade.link') }}
|
{{ i18n.baseText('generic.upgrade') }}
|
||||||
</n8n-link>
|
</n8n-link>
|
||||||
</template>
|
</template>
|
||||||
</I18nT>
|
</I18nT>
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ function getEmail(email: string): string {
|
|||||||
<I18nT keypath="settings.users.advancedPermissions.warning" scope="global">
|
<I18nT keypath="settings.users.advancedPermissions.warning" scope="global">
|
||||||
<template #link>
|
<template #link>
|
||||||
<n8n-link size="small" @click="goToUpgradeAdvancedPermissions">
|
<n8n-link size="small" @click="goToUpgradeAdvancedPermissions">
|
||||||
{{ i18n.baseText('settings.users.advancedPermissions.warning.link') }}
|
{{ i18n.baseText('generic.upgrade') }}
|
||||||
</n8n-link>
|
</n8n-link>
|
||||||
</template>
|
</template>
|
||||||
</I18nT>
|
</I18nT>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { type UsersList } from '@n8n/api-types';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import { formatTimeAgo } from '@/utils/formatters/dateFormatter';
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{ data: UsersList['items'][number] }>();
|
||||||
|
const formattedDate = computed(() =>
|
||||||
|
props.data.lastActiveAt ? formatTimeAgo(props.data.lastActiveAt) : i18n.baseText('generic.never'),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>{{ formattedDate }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module></style>
|
||||||
@@ -13,6 +13,7 @@ import type { IUser } from '@/Interface';
|
|||||||
import SettingsUsersRoleCell from '@/components/SettingsUsers/SettingsUsersRoleCell.vue';
|
import SettingsUsersRoleCell from '@/components/SettingsUsers/SettingsUsersRoleCell.vue';
|
||||||
import SettingsUsersProjectsCell from '@/components/SettingsUsers/SettingsUsersProjectsCell.vue';
|
import SettingsUsersProjectsCell from '@/components/SettingsUsers/SettingsUsersProjectsCell.vue';
|
||||||
import SettingsUsersActionsCell from '@/components/SettingsUsers/SettingsUsersActionsCell.vue';
|
import SettingsUsersActionsCell from '@/components/SettingsUsers/SettingsUsersActionsCell.vue';
|
||||||
|
import SettingsUsersLastActiveCell from '@/components/SettingsUsers/SettingsUsersLastActiveCell.vue';
|
||||||
import { hasPermission } from '@/utils/rbac/permissions';
|
import { hasPermission } from '@/utils/rbac/permissions';
|
||||||
import type { UsersInfoProps } from '@n8n/design-system/components/N8nUserInfo/UserInfo.vue';
|
import type { UsersInfoProps } from '@n8n/design-system/components/N8nUserInfo/UserInfo.vue';
|
||||||
|
|
||||||
@@ -54,6 +55,17 @@ const headers = ref<Array<TableHeader<Item>>>([
|
|||||||
title: i18n.baseText('settings.users.table.header.accountType'),
|
title: i18n.baseText('settings.users.table.header.accountType'),
|
||||||
key: 'role',
|
key: 'role',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18n.baseText('settings.users.table.header.lastActive'),
|
||||||
|
key: 'lastActiveAt',
|
||||||
|
value(row) {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
// TODO: Fix N8nDataTableServer so it doesn't break sorting when `value` is of mixed type (for example, string or null)
|
||||||
|
lastActiveAt: row.lastActiveAt ?? '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: i18n.baseText('settings.users.table.header.2fa'),
|
title: i18n.baseText('settings.users.table.header.2fa'),
|
||||||
key: 'mfaEnabled',
|
key: 'mfaEnabled',
|
||||||
@@ -160,6 +172,9 @@ const onRoleChange = ({ role, userId }: { role: string; userId: string }) => {
|
|||||||
/>
|
/>
|
||||||
<N8nText v-else color="text-dark">{{ roles[item.role ?? ROLE.Default].label }}</N8nText>
|
<N8nText v-else color="text-dark">{{ roles[item.role ?? ROLE.Default].label }}</N8nText>
|
||||||
</template>
|
</template>
|
||||||
|
<template #[`item.lastActiveAt`]="{ item }">
|
||||||
|
<SettingsUsersLastActiveCell :data="item" />
|
||||||
|
</template>
|
||||||
<template #[`item.projects`]="{ item }">
|
<template #[`item.projects`]="{ item }">
|
||||||
<SettingsUsersProjectsCell :data="item" />
|
<SettingsUsersProjectsCell :data="item" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const goToUpgrade = async () => {
|
|||||||
{{ i18n.baseText('insights.dashboard.paywall.description') }}
|
{{ i18n.baseText('insights.dashboard.paywall.description') }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<N8nButton type="primary" native-type="button" size="large" @click="goToUpgrade">
|
<N8nButton type="primary" native-type="button" size="large" @click="goToUpgrade">
|
||||||
{{ i18n.baseText('insights.dashboard.paywall.cta') }}
|
{{ i18n.baseText('generic.upgrade') }}
|
||||||
</N8nButton>
|
</N8nButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const perks = computed(() =>
|
|||||||
{{ i18n.baseText('insights.upgradeModal.button.dismiss') }}
|
{{ i18n.baseText('insights.upgradeModal.button.dismiss') }}
|
||||||
</N8nButton>
|
</N8nButton>
|
||||||
<N8nButton type="primary" @click="goToUpgrade">
|
<N8nButton type="primary" @click="goToUpgrade">
|
||||||
{{ i18n.baseText('insights.upgradeModal.button.upgrade') }}
|
{{ i18n.baseText('generic.upgrade') }}
|
||||||
</N8nButton>
|
</N8nButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { formatTimeAgo } from './dateFormatter';
|
||||||
|
|
||||||
|
describe('formatTimeAgo', () => {
|
||||||
|
const now = new Date('2023-07-15T12:00:00Z');
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(now);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return "Today" for today', () => {
|
||||||
|
const today = new Date('2023-07-15T08:00:00Z');
|
||||||
|
expect(formatTimeAgo(today)).toBe('Today');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return "Yesterday" for 1 day ago', () => {
|
||||||
|
const yesterday = new Date('2023-07-14T12:00:00Z');
|
||||||
|
expect(formatTimeAgo(yesterday)).toBe('Yesterday');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return weekday name for 2-6 days ago', () => {
|
||||||
|
const threeDaysAgo = new Date('2023-07-12T12:00:00Z'); // Wednesday
|
||||||
|
expect(formatTimeAgo(threeDaysAgo)).toBe('Wednesday');
|
||||||
|
|
||||||
|
const sixDaysAgo = new Date('2023-07-09T12:00:00Z'); // Sunday
|
||||||
|
expect(formatTimeAgo(sixDaysAgo)).toBe('Sunday');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return "Last [Weekday]" for 7-13 days ago', () => {
|
||||||
|
const sevenDaysAgo = new Date('2023-07-08T12:00:00Z'); // Saturday
|
||||||
|
expect(formatTimeAgo(sevenDaysAgo)).toBe('Last Saturday');
|
||||||
|
|
||||||
|
const thirteenDaysAgo = new Date('2023-07-02T12:00:00Z'); // Sunday
|
||||||
|
expect(formatTimeAgo(thirteenDaysAgo)).toBe('Last Sunday');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return "[n] days ago" for 14-30 days ago', () => {
|
||||||
|
const fourteenDaysAgo = new Date('2023-07-01T12:00:00Z');
|
||||||
|
expect(formatTimeAgo(fourteenDaysAgo)).toBe('14 days ago');
|
||||||
|
|
||||||
|
const thirtyDaysAgo = new Date('2023-06-15T12:00:00Z');
|
||||||
|
expect(formatTimeAgo(thirtyDaysAgo)).toBe('30 days ago');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return "[Month] [Day], [Year]" for 31+ days ago', () => {
|
||||||
|
const thirtyOneDaysAgo = new Date('2023-06-14T12:00:00Z');
|
||||||
|
expect(formatTimeAgo(thirtyOneDaysAgo)).toBe('June 14, 2023');
|
||||||
|
|
||||||
|
const oneYearAgo = new Date('2022-07-15T12:00:00Z');
|
||||||
|
expect(formatTimeAgo(oneYearAgo)).toBe('July 15, 2022');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle string input', () => {
|
||||||
|
const yesterday = '2023-07-14T12:00:00Z';
|
||||||
|
expect(formatTimeAgo(yesterday)).toBe('Yesterday');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle edge cases at boundaries', () => {
|
||||||
|
// Exactly 7 days ago
|
||||||
|
const exactlySevenDaysAgo = new Date('2023-07-08T12:00:00Z');
|
||||||
|
expect(formatTimeAgo(exactlySevenDaysAgo)).toBe('Last Saturday');
|
||||||
|
|
||||||
|
// Exactly 14 days ago
|
||||||
|
const exactlyFourteenDaysAgo = new Date('2023-07-01T12:00:00Z');
|
||||||
|
expect(formatTimeAgo(exactlyFourteenDaysAgo)).toBe('14 days ago');
|
||||||
|
|
||||||
|
// Exactly 31 days ago
|
||||||
|
const exactlyThirtyOneDaysAgo = new Date('2023-06-14T12:00:00Z');
|
||||||
|
expect(formatTimeAgo(exactlyThirtyOneDaysAgo)).toBe('June 14, 2023');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import dateformat from 'dateformat';
|
import dateformat from 'dateformat';
|
||||||
|
import { i18n } from '@n8n/i18n';
|
||||||
|
|
||||||
export const convertToDisplayDateComponents = (
|
export const convertToDisplayDateComponents = (
|
||||||
fullDate: Date | string | number,
|
fullDate: Date | string | number,
|
||||||
@@ -27,3 +28,26 @@ export const toDayMonth = (fullDate: Date | string) => dateformat(fullDate, 'd m
|
|||||||
|
|
||||||
export const toTime = (fullDate: Date | string, includeMillis: boolean = false) =>
|
export const toTime = (fullDate: Date | string, includeMillis: boolean = false) =>
|
||||||
dateformat(fullDate, includeMillis ? 'HH:MM:ss.l' : 'HH:MM:ss');
|
dateformat(fullDate, includeMillis ? 'HH:MM:ss.l' : 'HH:MM:ss');
|
||||||
|
|
||||||
|
export const formatTimeAgo = (fullDate: Date | string): string => {
|
||||||
|
const now = new Date();
|
||||||
|
const date = new Date(fullDate);
|
||||||
|
const diffInMs = now.getTime() - date.getTime();
|
||||||
|
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffInDays === 0) {
|
||||||
|
return i18n.baseText('userActivity.today');
|
||||||
|
} else if (diffInDays === 1) {
|
||||||
|
return i18n.baseText('userActivity.yesterday');
|
||||||
|
} else if (diffInDays >= 2 && diffInDays <= 6) {
|
||||||
|
return dateformat(date, 'dddd');
|
||||||
|
} else if (diffInDays >= 7 && diffInDays <= 13) {
|
||||||
|
return i18n.baseText('userActivity.lastTime', {
|
||||||
|
interpolate: { time: dateformat(date, 'dddd') },
|
||||||
|
});
|
||||||
|
} else if (diffInDays >= 14 && diffInDays <= 30) {
|
||||||
|
return i18n.baseText('userActivity.daysAgo', { interpolate: { count: diffInDays.toString() } });
|
||||||
|
} else {
|
||||||
|
return dateformat(date, 'mmmm d, yyyy');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ const usersTableState = ref<TableOptions>({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
const showUMSetupWarning = computed(() => hasPermission(['defaultUser']));
|
const showUMSetupWarning = computed(() => hasPermission(['defaultUser']));
|
||||||
|
const isEnforceMFAEnabled = computed(
|
||||||
|
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.EnforceMFA],
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
documentTitle.set(i18n.baseText('settings.users'));
|
documentTitle.set(i18n.baseText('settings.users'));
|
||||||
@@ -350,11 +353,19 @@ async function onUpdateMfaEnforced(value: boolean) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
<n8n-heading tag="h1" size="2xlarge" class="mb-xl">
|
<N8nHeading tag="h1" size="2xlarge" class="mb-xl">
|
||||||
{{ i18n.baseText('settings.users') }}
|
{{ i18n.baseText('settings.users') }}
|
||||||
</n8n-heading>
|
<N8nText v-if="!showUMSetupWarning" :class="$style.userCount" color="text-light">{{
|
||||||
|
i18n.baseText('settings.users.count', {
|
||||||
|
interpolate: {
|
||||||
|
count: usersStore.usersList.state.count,
|
||||||
|
},
|
||||||
|
adjustToNumber: usersStore.usersList.state.count,
|
||||||
|
})
|
||||||
|
}}</N8nText>
|
||||||
|
</N8nHeading>
|
||||||
<div v-if="!usersStore.usersLimitNotReached" :class="$style.setupInfoContainer">
|
<div v-if="!usersStore.usersLimitNotReached" :class="$style.setupInfoContainer">
|
||||||
<n8n-action-box
|
<N8nActionBox
|
||||||
:heading="
|
:heading="
|
||||||
i18n.baseText(uiStore.contextBasedTranslationKeys.users.settings.unavailable.title)
|
i18n.baseText(uiStore.contextBasedTranslationKeys.users.settings.unavailable.title)
|
||||||
"
|
"
|
||||||
@@ -367,25 +378,30 @@ async function onUpdateMfaEnforced(value: boolean) {
|
|||||||
@click:button="goToUpgrade"
|
@click:button="goToUpgrade"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<n8n-notice v-if="!isAdvancedPermissionsEnabled">
|
<N8nNotice v-if="!isAdvancedPermissionsEnabled">
|
||||||
<I18nT keypath="settings.users.advancedPermissions.warning" scope="global">
|
<I18nT keypath="settings.users.advancedPermissions.warning" scope="global">
|
||||||
<template #link>
|
<template #link>
|
||||||
<n8n-link
|
<N8nLink
|
||||||
data-test-id="upgrade-permissions-link"
|
data-test-id="upgrade-permissions-link"
|
||||||
size="small"
|
size="small"
|
||||||
@click="goToUpgradeAdvancedPermissions"
|
@click="goToUpgradeAdvancedPermissions"
|
||||||
>
|
>
|
||||||
{{ i18n.baseText('settings.users.advancedPermissions.warning.link') }}
|
{{ i18n.baseText('generic.upgrade') }}
|
||||||
</n8n-link>
|
</N8nLink>
|
||||||
</template>
|
</template>
|
||||||
</I18nT>
|
</I18nT>
|
||||||
</n8n-notice>
|
</N8nNotice>
|
||||||
<div :class="$style.settingsContainer">
|
<div :class="$style.settingsContainer">
|
||||||
<div :class="$style.settingsContainerInfo">
|
<div :class="$style.settingsContainerInfo">
|
||||||
<n8n-text :bold="true">{{ i18n.baseText('settings.personal.mfa.enforce.title') }}</n8n-text>
|
<N8nText :bold="true"
|
||||||
<n8n-text size="small" color="text-light">{{
|
>{{ i18n.baseText('settings.personal.mfa.enforce.title') }}
|
||||||
|
<N8nBadge v-if="!isEnforceMFAEnabled" class="ml-4xs">{{
|
||||||
|
i18n.baseText('generic.upgrade')
|
||||||
|
}}</N8nBadge>
|
||||||
|
</N8nText>
|
||||||
|
<N8nText size="small" color="text-light">{{
|
||||||
i18n.baseText('settings.personal.mfa.enforce.message')
|
i18n.baseText('settings.personal.mfa.enforce.message')
|
||||||
}}</n8n-text>
|
}}</N8nText>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.settingsContainerAction">
|
<div :class="$style.settingsContainerAction">
|
||||||
<EnterpriseEdition :features="[EnterpriseEditionFeature.EnforceMFA]">
|
<EnterpriseEdition :features="[EnterpriseEditionFeature.EnforceMFA]">
|
||||||
@@ -397,12 +413,7 @@ async function onUpdateMfaEnforced(value: boolean) {
|
|||||||
/>
|
/>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<N8nTooltip>
|
<N8nTooltip>
|
||||||
<el-switch
|
<el-switch :model-value="settingsStore.isMFAEnforced" size="large" :disabled="true" />
|
||||||
:model-value="settingsStore.isMFAEnforced"
|
|
||||||
size="large"
|
|
||||||
:disabled="true"
|
|
||||||
@update:model-value="onUpdateMfaEnforced"
|
|
||||||
/>
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<I18nT :keypath="tooltipKey" tag="span" scope="global">
|
<I18nT :keypath="tooltipKey" tag="span" scope="global">
|
||||||
<template #action>
|
<template #action>
|
||||||
@@ -418,7 +429,7 @@ async function onUpdateMfaEnforced(value: boolean) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!showUMSetupWarning" :class="$style.buttonContainer">
|
<div v-if="!showUMSetupWarning" :class="$style.buttonContainer">
|
||||||
<n8n-input
|
<N8nInput
|
||||||
:class="$style.search"
|
:class="$style.search"
|
||||||
:model-value="search"
|
:model-value="search"
|
||||||
:placeholder="i18n.baseText('settings.users.search.placeholder')"
|
:placeholder="i18n.baseText('settings.users.search.placeholder')"
|
||||||
@@ -427,15 +438,15 @@ async function onUpdateMfaEnforced(value: boolean) {
|
|||||||
@update:model-value="onSearch"
|
@update:model-value="onSearch"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<n8n-icon icon="search" />
|
<N8nIcon icon="search" />
|
||||||
</template>
|
</template>
|
||||||
</n8n-input>
|
</N8nInput>
|
||||||
<n8n-tooltip :disabled="!ssoStore.isSamlLoginEnabled">
|
<N8nTooltip :disabled="!ssoStore.isSamlLoginEnabled">
|
||||||
<template #content>
|
<template #content>
|
||||||
<span> {{ i18n.baseText('settings.users.invite.tooltip') }} </span>
|
<span> {{ i18n.baseText('settings.users.invite.tooltip') }} </span>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<n8n-button
|
<N8nButton
|
||||||
:disabled="ssoStore.isSamlLoginEnabled || !usersStore.usersLimitNotReached"
|
:disabled="ssoStore.isSamlLoginEnabled || !usersStore.usersLimitNotReached"
|
||||||
:label="i18n.baseText('settings.users.invite')"
|
:label="i18n.baseText('settings.users.invite')"
|
||||||
size="large"
|
size="large"
|
||||||
@@ -443,7 +454,7 @@ async function onUpdateMfaEnforced(value: boolean) {
|
|||||||
@click="onInvite"
|
@click="onInvite"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</n8n-tooltip>
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
<!-- If there's more than 1 user it means the account quota was more than 1 in the past. So we need to allow instance owner to be able to delete users and transfer workflows.
|
<!-- If there's more than 1 user it means the account quota was more than 1 in the past. So we need to allow instance owner to be able to delete users and transfer workflows.
|
||||||
-->
|
-->
|
||||||
@@ -466,6 +477,11 @@ async function onUpdateMfaEnforced(value: boolean) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
.userCount {
|
||||||
|
display: block;
|
||||||
|
padding: var(--spacing-3xs) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.buttonContainer {
|
.buttonContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
Reference in New Issue
Block a user