mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-02 04:39:14 -05:00
feat: implement import users from Jellyfin button
This commit is contained in:
251
src/components/UserList/JellyfinImportModal.tsx
Normal file
251
src/components/UserList/JellyfinImportModal.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { InboxInIcon } from '@heroicons/react/solid';
|
||||
import axios from 'axios';
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import useSettings from '../../hooks/useSettings';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
import Alert from '../Common/Alert';
|
||||
import Modal from '../Common/Modal';
|
||||
|
||||
interface JellyfinImportProps {
|
||||
onCancel?: () => void;
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
importfromJellyfin: 'Import Jellyfin Users',
|
||||
importfromJellyfinerror:
|
||||
'Something went wrong while importing Jellyfin users.',
|
||||
importedfromJellyfin:
|
||||
'<strong>{userCount}</strong> Jellyfin {userCount, plural, one {user} other {users}} imported successfully!',
|
||||
user: 'User',
|
||||
noJellyfinuserstoimport: 'There are no Jellyfin users to import.',
|
||||
newJellyfinsigninenabled:
|
||||
'The <strong>Enable New Jellyfin Sign-In</strong> setting is currently enabled. Jellyfin users with library access do not need to be imported in order to sign in.',
|
||||
});
|
||||
|
||||
const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
||||
onCancel,
|
||||
onComplete,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const settings = useSettings();
|
||||
const { addToast } = useToasts();
|
||||
const [isImporting, setImporting] = useState(false);
|
||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||
const { data, error } = useSWR<
|
||||
{
|
||||
id: string;
|
||||
title: string;
|
||||
username: string;
|
||||
email: string;
|
||||
thumb: string;
|
||||
}[]
|
||||
>(`/api/v1/settings/jellyfin/users`, {
|
||||
revalidateOnMount: true,
|
||||
});
|
||||
|
||||
const importUsers = async () => {
|
||||
setImporting(true);
|
||||
|
||||
try {
|
||||
const { data: createdUsers } = await axios.post(
|
||||
'/api/v1/user/import-from-jellyfin',
|
||||
{ jellyfinUserIds: selectedUsers }
|
||||
);
|
||||
|
||||
if (!createdUsers.length) {
|
||||
throw new Error('No users were imported from Jellyfin.');
|
||||
}
|
||||
|
||||
addToast(
|
||||
intl.formatMessage(messages.importedfromJellyfin, {
|
||||
userCount: createdUsers.length,
|
||||
strong: function strong(msg) {
|
||||
return <strong>{msg}</strong>;
|
||||
},
|
||||
}),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'success',
|
||||
}
|
||||
);
|
||||
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.importfromJellyfinerror), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isSelectedUser = (JellyfinId: string): boolean =>
|
||||
selectedUsers.includes(JellyfinId);
|
||||
|
||||
const isAllUsers = (): boolean => selectedUsers.length === data?.length;
|
||||
|
||||
const toggleUser = (JellyfinId: string): void => {
|
||||
if (selectedUsers.includes(JellyfinId)) {
|
||||
setSelectedUsers((users) => users.filter((user) => user !== JellyfinId));
|
||||
} else {
|
||||
setSelectedUsers((users) => [...users, JellyfinId]);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAllUsers = (): void => {
|
||||
if (data && selectedUsers.length >= 0 && !isAllUsers()) {
|
||||
setSelectedUsers(data.map((user) => user.id));
|
||||
} else {
|
||||
setSelectedUsers([]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
loading={!data && !error}
|
||||
title={intl.formatMessage(messages.importfromJellyfin)}
|
||||
iconSvg={<InboxInIcon />}
|
||||
onOk={() => {
|
||||
importUsers();
|
||||
}}
|
||||
okDisabled={isImporting || !selectedUsers.length}
|
||||
okText={intl.formatMessage(
|
||||
isImporting ? globalMessages.importing : globalMessages.import
|
||||
)}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
{data?.length ? (
|
||||
<>
|
||||
{settings.currentSettings.newPlexLogin && (
|
||||
<Alert
|
||||
title={intl.formatMessage(messages.newJellyfinsigninenabled, {
|
||||
strong: function strong(msg) {
|
||||
return (
|
||||
<strong className="font-semibold text-white">{msg}</strong>
|
||||
);
|
||||
},
|
||||
})}
|
||||
type="info"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<div className="-mx-4 sm:mx-0">
|
||||
<div className="inline-block min-w-full py-2 align-middle">
|
||||
<div className="overflow-hidden shadow sm:rounded-lg">
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-16 bg-gray-500 px-4 py-3">
|
||||
<span
|
||||
role="checkbox"
|
||||
tabIndex={0}
|
||||
aria-checked={isAllUsers()}
|
||||
onClick={() => toggleAllUsers()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === 'Space') {
|
||||
toggleAllUsers();
|
||||
}
|
||||
}}
|
||||
className="relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllUsers() ? 'bg-indigo-500' : 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isAllUsers() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</th>
|
||||
<th className="bg-gray-500 px-1 py-3 text-left text-xs font-medium uppercase leading-4 tracking-wider text-gray-200 md:px-6">
|
||||
{intl.formatMessage(messages.user)}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700 bg-gray-600">
|
||||
{data?.map((user) => (
|
||||
<tr key={`user-${user.id}`}>
|
||||
<td className="whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100">
|
||||
<span
|
||||
role="checkbox"
|
||||
tabIndex={0}
|
||||
aria-checked={isSelectedUser(user.id)}
|
||||
onClick={() => toggleUser(user.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === 'Space') {
|
||||
toggleUser(user.id);
|
||||
}
|
||||
}}
|
||||
className="relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isSelectedUser(user.id)
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
isSelectedUser(user.id)
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
className="h-10 w-10 flex-shrink-0 rounded-full"
|
||||
src={user.thumb}
|
||||
alt=""
|
||||
/>
|
||||
<div className="ml-4">
|
||||
<div className="text-base font-bold leading-5">
|
||||
{user.username}
|
||||
</div>
|
||||
{/* {user.username &&
|
||||
user.username.toLowerCase() !==
|
||||
user.email && (
|
||||
<div className="text-sm leading-5 text-gray-300">
|
||||
{user.email}
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Alert
|
||||
title={intl.formatMessage(messages.noJellyfinuserstoimport)}
|
||||
type="info"
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default JellyfinImportModal;
|
||||
@@ -34,12 +34,13 @@ import SensitiveInput from '../Common/SensitiveInput';
|
||||
import Table from '../Common/Table';
|
||||
import Transition from '../Transition';
|
||||
import BulkEditModal from './BulkEditModal';
|
||||
import JellyfinImportModal from './JellyfinImportModal';
|
||||
import PlexImportModal from './PlexImportModal';
|
||||
|
||||
const messages = defineMessages({
|
||||
users: 'Users',
|
||||
userlist: 'User List',
|
||||
importfromplex: 'Import Plex Users',
|
||||
importfromplex: 'Import {mediaServerName} Users',
|
||||
user: 'User',
|
||||
totalrequests: 'Requests',
|
||||
accounttype: 'Type',
|
||||
@@ -464,13 +465,23 @@ const UserList: React.FC = () => {
|
||||
leaveTo="opacity-0"
|
||||
show={showImportModal}
|
||||
>
|
||||
<PlexImportModal
|
||||
onCancel={() => setShowImportModal(false)}
|
||||
onComplete={() => {
|
||||
setShowImportModal(false);
|
||||
revalidate();
|
||||
}}
|
||||
/>
|
||||
{settings.currentSettings.mediaServerType === MediaServerType.PLEX ? (
|
||||
<PlexImportModal
|
||||
onCancel={() => setShowImportModal(false)}
|
||||
onComplete={() => {
|
||||
setShowImportModal(false);
|
||||
revalidate();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<JellyfinImportModal
|
||||
onCancel={() => setShowImportModal(false)}
|
||||
onComplete={() => {
|
||||
setShowImportModal(false);
|
||||
revalidate();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
<div className="flex flex-col justify-between lg:flex-row lg:items-end">
|
||||
@@ -489,13 +500,17 @@ const UserList: React.FC = () => {
|
||||
className="flex-grow lg:mr-2"
|
||||
buttonType="primary"
|
||||
onClick={() => setShowImportModal(true)}
|
||||
disabled={
|
||||
settings.currentSettings.mediaServerType !==
|
||||
MediaServerType.PLEX
|
||||
}
|
||||
>
|
||||
<InboxInIcon />
|
||||
<span>{intl.formatMessage(messages.importfromplex)}</span>
|
||||
<span>
|
||||
{intl.formatMessage(messages.importfromplex, {
|
||||
mediaServerName:
|
||||
settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.PLEX
|
||||
? 'Plex'
|
||||
: 'Jellyfin',
|
||||
})}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-2 flex flex-grow lg:mb-0 lg:flex-grow-0">
|
||||
|
||||
Reference in New Issue
Block a user