mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-10 08:47:14 +00:00
feat: add CSV member import tab to Edit Group modal
Validate CSV on select, look up users in batches, and add valid users to the group.
This commit is contained in:
parent
717d24db38
commit
f9775d3f89
3 changed files with 169 additions and 1 deletions
|
|
@ -9,10 +9,12 @@
|
|||
import Permissions from './Permissions.svelte';
|
||||
import Users from './Users.svelte';
|
||||
import GroupPreviewPanel from './GroupPreviewPanel.svelte';
|
||||
import Import from './Import.svelte';
|
||||
import { DEFAULT_PERMISSIONS } from '$lib/constants/permissions';
|
||||
import { getUserDefaultPermissions, getUserDefaultPermissionsDefaults } from '$lib/apis/users';
|
||||
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
|
||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||
import DocumentArrowDown from '$lib/components/icons/DocumentArrowDown.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
|
@ -263,6 +265,24 @@
|
|||
<div class=" self-center">{$i18n.t('Preview')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if tabs.includes('import')}
|
||||
<button
|
||||
class="px-0.5 py-1 max-w-fit w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
|
||||
'import'
|
||||
? ''
|
||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'import';
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<DocumentArrowDown />
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('CSV Import')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 mt-1 lg:mt-1 lg:h-[30rem] lg:max-h-[30rem] flex flex-col">
|
||||
|
|
@ -283,6 +303,12 @@
|
|||
<Users bind:userCount groupId={group?.id} />
|
||||
{:else if selectedTab == 'preview'}
|
||||
<GroupPreviewPanel groupId={group?.id} />
|
||||
{:else if selectedTab == 'import'}
|
||||
<Import
|
||||
groupId={group?.id}
|
||||
bind:userCount
|
||||
bind:loading
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
edit
|
||||
{group}
|
||||
{defaultPermissions}
|
||||
tabs={['general', 'permissions', 'users', 'preview']}
|
||||
tabs={['general', 'permissions', 'users', 'preview', 'import']}
|
||||
onSubmit={updateHandler}
|
||||
onDelete={deleteHandler}
|
||||
/>
|
||||
|
|
|
|||
142
src/lib/components/admin/Users/Groups/Import.svelte
Normal file
142
src/lib/components/admin/Users/Groups/Import.svelte
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getUsers } from '$lib/apis/users';
|
||||
import { addUserToGroup } from '$lib/apis/groups';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let groupId: string | undefined = undefined;
|
||||
export let userCount = 0;
|
||||
export let loading = false;
|
||||
|
||||
let inputFiles: FileList | null = null;
|
||||
let fileInput: HTMLInputElement | undefined;
|
||||
|
||||
const clearFileInput = () => {
|
||||
inputFiles = null;
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const importCsv = async (file: File) => {
|
||||
if (!groupId) {
|
||||
clearFileInput();
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const csv = await file.text();
|
||||
const validRows: { idx: number; email: string }[] = [];
|
||||
|
||||
for (const [idx, row] of csv.split(/\r?\n/).entries()) {
|
||||
if (idx === 0) continue;
|
||||
|
||||
const columns = row.split(',').map((col) => col.trim());
|
||||
if (columns.length === 1 && columns[0] === '') continue;
|
||||
|
||||
if (columns.length !== 2 || !columns[1]) {
|
||||
toast.error(`Row ${idx + 1}: invalid format.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
validRows.push({ idx, email: columns[1].toLowerCase() });
|
||||
}
|
||||
|
||||
const userIds: string[] = [];
|
||||
const BATCH_SIZE = 10;
|
||||
|
||||
for (let i = 0; i < validRows.length; i += BATCH_SIZE) {
|
||||
const batch = validRows.slice(i, i + BATCH_SIZE);
|
||||
const results = await Promise.allSettled(
|
||||
batch.map(({ idx, email }) =>
|
||||
getUsers(localStorage.token, email)
|
||||
.then((res) => {
|
||||
const user = res?.users?.find((u) => u.email?.toLowerCase() === email);
|
||||
if (user?.id) {
|
||||
return user.id;
|
||||
}
|
||||
|
||||
toast.error(`Row ${idx + 1}: ${$i18n.t('User not found.')}`);
|
||||
return null;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(`Row ${idx + 1}: ${error}`);
|
||||
return null;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled' && result.value) {
|
||||
userIds.push(result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.length > 0) {
|
||||
await addUserToGroup(localStorage.token, groupId, userIds);
|
||||
userCount += userIds.length;
|
||||
toast.success(
|
||||
$i18n.t('Successfully imported {{userCount}} users.', { userCount: userIds.length })
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(`${error}`);
|
||||
} finally {
|
||||
clearFileInput();
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async () => {
|
||||
const file = inputFiles?.[0];
|
||||
if (!file || loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
await importCsv(file);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="mb-3 w-full">
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
hidden
|
||||
bind:files={inputFiles}
|
||||
type="file"
|
||||
accept=".csv"
|
||||
disabled={loading}
|
||||
on:change={handleFileChange}
|
||||
/>
|
||||
|
||||
<button
|
||||
class="w-full text-sm font-normal py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl {loading
|
||||
? 'cursor-not-allowed opacity-60'
|
||||
: ''}"
|
||||
type="button"
|
||||
disabled={loading}
|
||||
on:click={() => {
|
||||
fileInput?.click();
|
||||
}}
|
||||
>
|
||||
{#if loading}
|
||||
{$i18n.t('Importing...')}
|
||||
{:else}
|
||||
{$i18n.t('Click here to select a csv file.')}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-500">
|
||||
ⓘ {$i18n.t('Ensure your CSV file includes 2 columns in this order: Name, Email.')}
|
||||
<a class="underline dark:text-gray-200" href="{WEBUI_BASE_URL}/static/group-import.csv">
|
||||
{$i18n.t('Click here to download group import template file.')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Reference in a new issue