diff --git a/packages/design-system/src/components/N8nUserStack/UserStack.stories.ts b/packages/design-system/src/components/N8nUserStack/UserStack.stories.ts
new file mode 100644
index 0000000000..b19bbe3e67
--- /dev/null
+++ b/packages/design-system/src/components/N8nUserStack/UserStack.stories.ts
@@ -0,0 +1,191 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import type { StoryFn } from '@storybook/vue3';
+import UserStack from './UserStack.vue';
+
+export default {
+ title: 'Modules/UserStack',
+ component: UserStack,
+};
+
+const Template: StoryFn = (args) => ({
+ setup: () => ({ args }),
+ props: args,
+ components: {
+ UserStack,
+ },
+ template: '',
+});
+
+export const WithGroups = Template.bind({});
+WithGroups.args = {
+ currentUserEmail: 'sunny@n8n.io',
+ users: {
+ Owners: [
+ {
+ id: '1',
+ firstName: 'Sunny',
+ lastName: 'Side',
+ fullName: 'Sunny Side',
+ email: 'sunny@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: true,
+ signInType: 'email',
+ disabled: false,
+ },
+ {
+ id: '2',
+ firstName: 'Kobi',
+ lastName: 'Dog',
+ fullName: 'Kobi Dog',
+ email: 'kobi@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: false,
+ signInType: 'ldap',
+ disabled: true,
+ },
+ ],
+ 'Other users': [
+ {
+ id: '3',
+ firstName: 'John',
+ lastName: 'Doe',
+ fullName: 'John Doe',
+ email: 'john@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: false,
+ signInType: 'email',
+ disabled: false,
+ },
+ {
+ id: '4',
+ firstName: 'Jane',
+ lastName: 'Doe',
+ fullName: 'Jane Doe',
+ email: 'jane@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: false,
+ signInType: 'ldap',
+ disabled: true,
+ },
+ {
+ id: '5',
+ firstName: 'Test',
+ lastName: 'User',
+ fullName: 'Test User',
+ email: 'test@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: true,
+ isOwner: false,
+ signInType: 'email',
+ disabled: false,
+ },
+ ],
+ 'Empty Group': [],
+ },
+};
+
+export const SingleGroup = Template.bind({});
+SingleGroup.args = {
+ currentUserEmail: 'sunny@n8n.io',
+ users: {
+ Owners: [
+ {
+ id: '1',
+ firstName: 'Sunny',
+ lastName: 'Side',
+ fullName: 'Sunny Side',
+ email: 'sunny@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: true,
+ signInType: 'email',
+ disabled: false,
+ },
+ {
+ id: '2',
+ firstName: 'Kobi',
+ lastName: 'Dog',
+ fullName: 'Kobi Dog',
+ email: 'kobi@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: false,
+ signInType: 'ldap',
+ disabled: true,
+ },
+ {
+ id: '4',
+ firstName: 'Jane',
+ lastName: 'Doe',
+ fullName: 'Jane Doe',
+ email: 'jane@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: false,
+ signInType: 'ldap',
+ disabled: true,
+ },
+ {
+ id: '5',
+ firstName: 'Test',
+ lastName: 'User',
+ fullName: 'Test User',
+ email: 'test@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: true,
+ isOwner: false,
+ signInType: 'email',
+ disabled: false,
+ },
+ ],
+ },
+};
+
+export const NoCutoff = Template.bind({});
+NoCutoff.args = {
+ currentUserEmail: 'sunny@n8n.io',
+ users: {
+ Owners: [
+ {
+ id: '1',
+ firstName: 'Sunny',
+ lastName: 'Side',
+ fullName: 'Sunny Side',
+ email: 'sunny@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: true,
+ signInType: 'email',
+ disabled: false,
+ },
+ {
+ id: '2',
+ firstName: 'Kobi',
+ lastName: 'Dog',
+ fullName: 'Kobi Dog',
+ email: 'kobi@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: false,
+ signInType: 'ldap',
+ disabled: true,
+ },
+ {
+ id: '3',
+ firstName: 'John',
+ lastName: 'Doe',
+ fullName: 'John Doe',
+ email: 'john@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: false,
+ signInType: 'email',
+ disabled: false,
+ },
+ ],
+ },
+};
diff --git a/packages/design-system/src/components/N8nUserStack/UserStack.vue b/packages/design-system/src/components/N8nUserStack/UserStack.vue
new file mode 100644
index 0000000000..a437006d09
--- /dev/null
+++ b/packages/design-system/src/components/N8nUserStack/UserStack.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+{{ hiddenUsersCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/design-system/src/components/N8nUserStack/__tests__/UserStack.spec.ts b/packages/design-system/src/components/N8nUserStack/__tests__/UserStack.spec.ts
new file mode 100644
index 0000000000..ff3ac1b897
--- /dev/null
+++ b/packages/design-system/src/components/N8nUserStack/__tests__/UserStack.spec.ts
@@ -0,0 +1,118 @@
+import { render } from '@testing-library/vue';
+import UserStack from '../UserStack.vue';
+import { N8nAvatar, N8nUserInfo } from '@/main';
+
+describe('UserStack', () => {
+ it('should render flat user list', () => {
+ const { container } = render(UserStack, {
+ props: {
+ currentUserEmail: 'hello@n8n.io',
+ users: {
+ Owners: [
+ {
+ id: '1',
+ firstName: 'Sunny',
+ lastName: 'Side',
+ fullName: 'Sunny Side',
+ email: 'hello@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: true,
+ signInType: 'email',
+ disabled: false,
+ },
+ {
+ id: '2',
+ firstName: 'Kobi',
+ lastName: 'Dog',
+ fullName: 'Kobi Dog',
+ email: 'kobi@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: false,
+ signInType: 'ldap',
+ disabled: true,
+ },
+ ],
+ },
+ },
+ global: {
+ components: {
+ 'n8n-avatar': N8nAvatar,
+ 'n8n-user-info': N8nUserInfo,
+ },
+ },
+ });
+ expect(container.querySelector('.user-stack')).toBeInTheDocument();
+ expect(container.querySelectorAll('svg')).toHaveLength(2);
+ });
+
+ it('should not show all avatars', async () => {
+ const { container } = render(UserStack, {
+ props: {
+ currentUserEmail: 'hello@n8n.io',
+ users: {
+ Owners: [
+ {
+ id: '1',
+ firstName: 'Sunny',
+ lastName: 'Side',
+ fullName: 'Sunny Side',
+ email: 'hello@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: true,
+ signInType: 'email',
+ disabled: false,
+ },
+ {
+ id: '2',
+ firstName: 'Kobi',
+ lastName: 'Dog',
+ fullName: 'Kobi Dog',
+ email: 'kobi@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: false,
+ signInType: 'ldap',
+ disabled: true,
+ },
+ {
+ id: '3',
+ firstName: 'John',
+ lastName: 'Doe',
+ fullName: 'John Doe',
+ email: 'john@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: false,
+ signInType: 'email',
+ disabled: false,
+ },
+ {
+ id: '4',
+ firstName: 'Jane',
+ lastName: 'Doe',
+ fullName: 'Jane Doe',
+ email: 'jane@n8n.io',
+ isDefaultUser: false,
+ isPendingUser: false,
+ isOwner: false,
+ signInType: 'ldap',
+ disabled: true,
+ },
+ ],
+ },
+ },
+ global: {
+ components: {
+ 'n8n-avatar': N8nAvatar,
+ 'n8n-user-info': N8nUserInfo,
+ },
+ },
+ });
+ expect(container.querySelector('.user-stack')).toBeInTheDocument();
+ expect(container.querySelectorAll('svg')).toHaveLength(2);
+ expect(container.querySelector('.hiddenBadge')).toHaveTextContent('+2');
+ });
+});
diff --git a/packages/design-system/src/components/N8nUserStack/index.ts b/packages/design-system/src/components/N8nUserStack/index.ts
new file mode 100644
index 0000000000..8b8266be89
--- /dev/null
+++ b/packages/design-system/src/components/N8nUserStack/index.ts
@@ -0,0 +1,3 @@
+import N8nUserStack from './UserStack.vue';
+
+export default N8nUserStack;
diff --git a/packages/design-system/src/components/index.ts b/packages/design-system/src/components/index.ts
index 9e9cbd616e..2d4d1b185f 100644
--- a/packages/design-system/src/components/index.ts
+++ b/packages/design-system/src/components/index.ts
@@ -46,6 +46,7 @@ export { default as N8nTags } from './N8nTags';
export { default as N8nText } from './N8nText';
export { default as N8nTooltip } from './N8nTooltip';
export { default as N8nTree } from './N8nTree';
+export { default as N8nUserStack } from './N8nUserStack';
export { default as N8nUserInfo } from './N8nUserInfo';
export { default as N8nUserSelect } from './N8nUserSelect';
export { default as N8nUsersList } from './N8nUsersList';
diff --git a/packages/design-system/src/plugin.ts b/packages/design-system/src/plugin.ts
index b7a153469d..2904f35dc1 100644
--- a/packages/design-system/src/plugin.ts
+++ b/packages/design-system/src/plugin.ts
@@ -51,6 +51,7 @@ import {
N8nUserInfo,
N8nUserSelect,
N8nUsersList,
+ N8nUserStack,
} from './components';
export const N8nPlugin: Plugin<{}> = {
@@ -103,6 +104,7 @@ export const N8nPlugin: Plugin<{}> = {
app.component('n8n-text', N8nText);
app.component('n8n-tooltip', N8nTooltip);
app.component('n8n-tree', N8nTree);
+ app.component('n8n-user-stack', N8nUserStack);
app.component('n8n-user-info', N8nUserInfo);
app.component('n8n-users-list', N8nUsersList);
app.component('n8n-user-select', N8nUserSelect);
diff --git a/packages/design-system/src/types/user.ts b/packages/design-system/src/types/user.ts
index d67cdc2151..b5b7f512cf 100644
--- a/packages/design-system/src/types/user.ts
+++ b/packages/design-system/src/types/user.ts
@@ -18,3 +18,5 @@ export interface UserAction {
type?: 'external-link';
guard?: (user: IUser) => boolean;
}
+
+export type UserStackGroups = { [groupName: string]: IUser[] };