mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Move routes to dashboard app
This commit is contained in:
parent
c0d14715fb
commit
bd1ae96b62
17 changed files with 29 additions and 218 deletions
|
@ -1,9 +1,10 @@
|
|||
import loadable from '@loadable/component';
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { AsyncPageProps, AsyncRoute, toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
import ServerContentPage from 'components/ServerContentPage';
|
||||
|
||||
|
@ -12,12 +13,24 @@ import { ASYNC_ADMIN_ROUTES } from './routes/_asyncRoutes';
|
|||
import { LEGACY_ADMIN_ROUTES } from './routes/_legacyRoutes';
|
||||
import AppLayout from './AppLayout';
|
||||
|
||||
const DashboardAsyncPage = loadable(
|
||||
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `./routes/${props.page}`),
|
||||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||
);
|
||||
|
||||
const toDashboardAsyncPageRoute = (route: AsyncRoute) => (
|
||||
toAsyncPageRoute({
|
||||
...route,
|
||||
element: DashboardAsyncPage
|
||||
})
|
||||
);
|
||||
|
||||
const DashboardApp = () => (
|
||||
<Routes>
|
||||
<Route element={<ConnectionRequired isAdminRequired />}>
|
||||
<Route element={<AppLayout />}>
|
||||
<Route path='dashboard'>
|
||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
||||
{ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)}
|
||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
|
|
34
src/apps/dashboard/components/activityTable/LogLevelChip.tsx
Normal file
34
src/apps/dashboard/components/activityTable/LogLevelChip.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import React from 'react';
|
||||
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const LogLevelChip = ({ level }: { level: LogLevel }) => {
|
||||
let color: 'info' | 'warning' | 'error' | undefined = undefined;
|
||||
switch (level) {
|
||||
case LogLevel.Information:
|
||||
color = 'info';
|
||||
break;
|
||||
case LogLevel.Warning:
|
||||
color = 'warning';
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
case LogLevel.Critical:
|
||||
color = 'error';
|
||||
break;
|
||||
}
|
||||
|
||||
const levelText = globalize.translate(`LogLevel.${level}`);
|
||||
|
||||
return (
|
||||
<Chip
|
||||
size='small'
|
||||
color={color}
|
||||
label={levelText}
|
||||
title={levelText}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogLevelChip;
|
64
src/apps/dashboard/components/activityTable/OverviewCell.tsx
Normal file
64
src/apps/dashboard/components/activityTable/OverviewCell.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||
import Info from '@mui/icons-material/Info';
|
||||
import Box from '@mui/material/Box';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
|
||||
const OverviewCell: FC<ActivityLogEntry> = ({ Overview, ShortOverview }) => {
|
||||
const displayValue = ShortOverview ?? Overview;
|
||||
const [ open, setOpen ] = useState(false);
|
||||
|
||||
const onTooltipClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
const onTooltipOpen = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
if (!displayValue) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
component='div'
|
||||
title={displayValue}
|
||||
>
|
||||
{displayValue}
|
||||
</Box>
|
||||
{ShortOverview && Overview && (
|
||||
<ClickAwayListener onClickAway={onTooltipClose}>
|
||||
<Tooltip
|
||||
title={Overview}
|
||||
placement='top'
|
||||
arrow
|
||||
onClose={onTooltipClose}
|
||||
open={open}
|
||||
disableFocusListener
|
||||
disableHoverListener
|
||||
disableTouchListener
|
||||
>
|
||||
<IconButton onClick={onTooltipOpen}>
|
||||
<Info />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ClickAwayListener>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewCell;
|
|
@ -0,0 +1,17 @@
|
|||
import React, { type RefAttributes } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GridActionsCellItem, type GridActionsCellItemProps } from '@mui/x-data-grid';
|
||||
|
||||
type GridActionsCellLinkProps = { to: string } & GridActionsCellItemProps & RefAttributes<HTMLButtonElement>;
|
||||
|
||||
/**
|
||||
* Link component to use in mui's data-grid action column due to a current bug with passing props to custom link components.
|
||||
* @see https://github.com/mui/mui-x/issues/4654
|
||||
*/
|
||||
const GridActionsCellLink = ({ to, ...props }: GridActionsCellLinkProps) => (
|
||||
<Link to={to}>
|
||||
<GridActionsCellItem {...props} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
export default GridActionsCellLink;
|
|
@ -1,12 +1,12 @@
|
|||
import type { AsyncRoute } from 'components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'activity', page: 'dashboard/activity' },
|
||||
{ path: 'notifications', page: 'dashboard/notifications' },
|
||||
{ path: 'users/new', page: 'user/usernew' },
|
||||
{ path: 'users', page: 'user/userprofiles' },
|
||||
{ path: 'users/profile', page: 'user/useredit' },
|
||||
{ path: 'users/access', page: 'user/userlibraryaccess' },
|
||||
{ path: 'users/parentalcontrol', page: 'user/userparentalcontrol' },
|
||||
{ path: 'users/password', page: 'user/userpassword' }
|
||||
{ path: 'activity' },
|
||||
{ path: 'notifications' },
|
||||
{ path: 'users/new' },
|
||||
{ path: 'users' },
|
||||
{ path: 'users/profile' },
|
||||
{ path: 'users/access' },
|
||||
{ path: 'users/parentalcontrol' },
|
||||
{ path: 'users/password' }
|
||||
];
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import type { Redirect } from 'components/router/Redirect';
|
||||
|
||||
export const REDIRECTS: Redirect[] = [
|
||||
// FIXME: URL params are not included in redirects
|
||||
{ from: 'addplugin.html', to: '/dashboard/plugins/add' },
|
||||
{ from: 'apikeys.html', to: '/dashboard/keys' },
|
||||
{ from: 'availableplugins.html', to: '/dashboard/plugins/catalog' },
|
||||
{ from: 'dashboard.html', to: '/dashboard' },
|
||||
{ from: 'dashboardgeneral.html', to: '/dashboard/settings' },
|
||||
// FIXME: URL params are not included in redirects
|
||||
{ from: 'device.html', to: '/dashboard/devices/edit' },
|
||||
{ from: 'devices.html', to: '/dashboard/devices' },
|
||||
// FIXME: URL params are not included in redirects
|
||||
{ from: 'dlnaprofile.html', to: '/dashboard/dlna/profiles/edit' },
|
||||
{ from: 'dlnaprofiles.html', to: '/dashboard/dlna/profiles' },
|
||||
{ from: 'dlnasettings.html', to: '/dashboard/dlna' },
|
||||
|
@ -19,11 +16,9 @@ export const REDIRECTS: Redirect[] = [
|
|||
{ from: 'installedplugins.html', to: '/dashboard/plugins' },
|
||||
{ from: 'library.html', to: '/dashboard/libraries' },
|
||||
{ from: 'librarydisplay.html', to: '/dashboard/libraries/display' },
|
||||
// FIXME: URL params are not included in redirects
|
||||
{ from: 'livetvguideprovider.html', to: '/dashboard/livetv/guide' },
|
||||
{ from: 'livetvsettings.html', to: '/dashboard/recordings' },
|
||||
{ from: 'livetvstatus.html', to: '/dashboard/livetv' },
|
||||
// FIXME: URL params are not included in redirects
|
||||
{ from: 'livetvtuner.html', to: '/dashboard/livetv/tuner' },
|
||||
{ from: 'log.html', to: '/dashboard/logs' },
|
||||
{ from: 'metadataimages.html', to: '/dashboard/libraries/metadata' },
|
||||
|
@ -33,19 +28,14 @@ export const REDIRECTS: Redirect[] = [
|
|||
{ from: 'playbackconfiguration.html', to: '/dashboard/playback/resume' },
|
||||
{ from: 'quickConnect.html', to: '/dashboard/quickconnect' },
|
||||
{ from: 'repositories.html', to: '/dashboard/plugins/repositories' },
|
||||
// FIXME: URL params are not included in redirects
|
||||
{ from: 'scheduledtask.html', to: '/dashboard/tasks/edit' },
|
||||
{ from: 'scheduledtasks.html', to: '/dashboard/tasks' },
|
||||
{ from: 'serveractivity.html', to: '/dashboard/activity' },
|
||||
{ from: 'streamingsettings.html', to: '/dashboard/playback/streaming' },
|
||||
{ from: 'usernew.html', to: '/dashboard/users/add' },
|
||||
{ from: 'userprofiles.html', to: '/dashboard/users' },
|
||||
// FIXME: URL params are not included in redirects
|
||||
{ from: 'useredit.html', to: '/dashboard/users/profile' },
|
||||
// FIXME: URL params are not included in redirects
|
||||
{ from: 'userlibraryaccess.html', to: '/dashboard/users/access' },
|
||||
// FIXME: URL params are not included in redirects
|
||||
{ from: 'usernew.html', to: '/dashboard/users/add' },
|
||||
{ from: 'userparentalcontrol.html', to: '/dashboard/users/parentalcontrol' },
|
||||
// FIXME: URL params are not included in redirects
|
||||
{ from: 'userpassword.html', to: '/dashboard/users/password' }
|
||||
{ from: 'userpassword.html', to: '/dashboard/users/password' },
|
||||
{ from: 'userprofiles.html', to: '/dashboard/users' }
|
||||
];
|
||||
|
|
273
src/apps/dashboard/routes/activity.tsx
Normal file
273
src/apps/dashboard/routes/activity.tsx
Normal file
|
@ -0,0 +1,273 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api';
|
||||
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
||||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||
import PermMedia from '@mui/icons-material/PermMedia';
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ToggleButton from '@mui/material/ToggleButton';
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Page from 'components/Page';
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'scripts/datetime';
|
||||
import globalize from 'scripts/globalize';
|
||||
import { toBoolean } from 'utils/string';
|
||||
|
||||
import LogLevelChip from '../components/activityTable/LogLevelChip';
|
||||
import OverviewCell from '../components/activityTable/OverviewCell';
|
||||
import GridActionsCellLink from '../components/dataGrid/GridActionsCellLink';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
const VIEW_PARAM = 'useractivity';
|
||||
|
||||
const enum ActivityView {
|
||||
All,
|
||||
User,
|
||||
System
|
||||
}
|
||||
|
||||
const getActivityView = (param: string | null) => {
|
||||
if (param === null) return ActivityView.All;
|
||||
if (toBoolean(param)) return ActivityView.User;
|
||||
return ActivityView.System;
|
||||
};
|
||||
|
||||
const getRowId = (row: ActivityLogEntry) => row.Id ?? -1;
|
||||
|
||||
const Activity = () => {
|
||||
const { api } = useApi();
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
|
||||
const [ activityView, setActivityView ] = useState(
|
||||
getActivityView(searchParams.get(VIEW_PARAM)));
|
||||
const [ isLoading, setIsLoading ] = useState(true);
|
||||
const [ paginationModel, setPaginationModel ] = useState({
|
||||
page: 0,
|
||||
pageSize: DEFAULT_PAGE_SIZE
|
||||
});
|
||||
const [ rowCount, setRowCount ] = useState(0);
|
||||
const [ rows, setRows ] = useState<ActivityLogEntry[]>([]);
|
||||
const [ users, setUsers ] = useState<Record<string, UserDto>>({});
|
||||
|
||||
const userColDef: GridColDef[] = activityView !== ActivityView.System ? [
|
||||
{
|
||||
field: 'User',
|
||||
headerName: globalize.translate('LabelUser'),
|
||||
width: 60,
|
||||
valueGetter: ({ row }) => users[row.UserId]?.Name,
|
||||
renderCell: ({ row }) => (
|
||||
<IconButton
|
||||
size='large'
|
||||
color='inherit'
|
||||
sx={{ padding: 0 }}
|
||||
title={users[row.UserId]?.Name ?? undefined}
|
||||
component={Link}
|
||||
to={`/useredit.html?userId=${row.UserId}`}
|
||||
>
|
||||
<UserAvatar user={users[row.UserId]} />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
] : [];
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'Date',
|
||||
headerName: globalize.translate('LabelDate'),
|
||||
width: 90,
|
||||
type: 'date',
|
||||
valueGetter: ({ value }) => parseISO8601Date(value),
|
||||
valueFormatter: ({ value }) => toLocaleDateString(value)
|
||||
},
|
||||
{
|
||||
field: 'Time',
|
||||
headerName: globalize.translate('LabelTime'),
|
||||
width: 100,
|
||||
type: 'dateTime',
|
||||
valueGetter: ({ row }) => parseISO8601Date(row.Date),
|
||||
valueFormatter: ({ value }) => toLocaleTimeString(value)
|
||||
},
|
||||
{
|
||||
field: 'Severity',
|
||||
headerName: globalize.translate('LabelLevel'),
|
||||
width: 110,
|
||||
renderCell: ({ value }) => (
|
||||
value ? (
|
||||
<LogLevelChip level={value} />
|
||||
) : undefined
|
||||
)
|
||||
},
|
||||
...userColDef,
|
||||
{
|
||||
field: 'Name',
|
||||
headerName: globalize.translate('LabelName'),
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
field: 'Overview',
|
||||
headerName: globalize.translate('LabelOverview'),
|
||||
width: 200,
|
||||
valueGetter: ({ row }) => row.ShortOverview ?? row.Overview,
|
||||
renderCell: ({ row }) => (
|
||||
<OverviewCell {...row} />
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'Type',
|
||||
headerName: globalize.translate('LabelType'),
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
getActions: ({ row }) => {
|
||||
const actions = [];
|
||||
|
||||
if (row.ItemId) {
|
||||
actions.push(
|
||||
<GridActionsCellLink
|
||||
size='large'
|
||||
icon={<PermMedia />}
|
||||
label={globalize.translate('LabelMediaDetails')}
|
||||
title={globalize.translate('LabelMediaDetails')}
|
||||
to={`/details?id=${row.ItemId}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const onViewChange = useCallback((_e, newView: ActivityView | null) => {
|
||||
if (newView !== null) {
|
||||
setActivityView(newView);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (api) {
|
||||
const fetchUsers = async () => {
|
||||
const { data } = await getUserApi(api).getUsers();
|
||||
const usersById: Record<string, UserDto> = {};
|
||||
data.forEach(user => {
|
||||
if (user.Id) {
|
||||
usersById[user.Id] = user;
|
||||
}
|
||||
});
|
||||
|
||||
setUsers(usersById);
|
||||
};
|
||||
|
||||
fetchUsers()
|
||||
.catch(err => {
|
||||
console.error('[activity] failed to fetch users', err);
|
||||
});
|
||||
}
|
||||
}, [ api ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (api) {
|
||||
const fetchActivity = async () => {
|
||||
const params: {
|
||||
startIndex: number,
|
||||
limit: number,
|
||||
hasUserId?: boolean
|
||||
} = {
|
||||
startIndex: paginationModel.page * paginationModel.pageSize,
|
||||
limit: paginationModel.pageSize
|
||||
};
|
||||
if (activityView !== ActivityView.All) {
|
||||
params.hasUserId = activityView === ActivityView.User;
|
||||
}
|
||||
|
||||
const { data } = await getActivityLogApi(api)
|
||||
.getLogEntries(params);
|
||||
|
||||
setRowCount(data.TotalRecordCount ?? 0);
|
||||
setRows(data.Items ?? []);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
setIsLoading(true);
|
||||
fetchActivity()
|
||||
.catch(err => {
|
||||
console.error('[activity] failed to fetch activity log entries', err);
|
||||
});
|
||||
}
|
||||
}, [ activityView, api, paginationModel ]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentViewParam = getActivityView(searchParams.get(VIEW_PARAM));
|
||||
if (currentViewParam !== activityView) {
|
||||
if (activityView === ActivityView.All) {
|
||||
searchParams.delete(VIEW_PARAM);
|
||||
} else {
|
||||
searchParams.set(VIEW_PARAM, `${activityView === ActivityView.User}`);
|
||||
}
|
||||
setSearchParams(searchParams);
|
||||
}
|
||||
}, [ activityView, searchParams, setSearchParams ]);
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='serverActivityPage'
|
||||
title={globalize.translate('HeaderActivity')}
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<div className='content-primary'>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
marginY: 2
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant='h2'>
|
||||
{globalize.translate('HeaderActivity')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<ToggleButtonGroup
|
||||
value={activityView}
|
||||
onChange={onViewChange}
|
||||
exclusive
|
||||
>
|
||||
<ToggleButton value={ActivityView.All}>
|
||||
{globalize.translate('All')}
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ActivityView.User}>
|
||||
{globalize.translate('LabelUser')}
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ActivityView.System}>
|
||||
{globalize.translate('LabelSystem')}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
rows={rows}
|
||||
pageSizeOptions={[ 10, 25, 50, 100 ]}
|
||||
paginationMode='server'
|
||||
paginationModel={paginationModel}
|
||||
onPaginationModelChange={setPaginationModel}
|
||||
rowCount={rowCount}
|
||||
getRowId={getRowId}
|
||||
loading={isLoading}
|
||||
sx={{
|
||||
minHeight: 500
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Activity;
|
36
src/apps/dashboard/routes/notifications.tsx
Normal file
36
src/apps/dashboard/routes/notifications.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
|
||||
import Page from 'components/Page';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
const PluginLink = () => (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `<a
|
||||
is='emby-linkbutton'
|
||||
class='button-link'
|
||||
href='#/addplugin.html?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
|
||||
>
|
||||
${globalize.translate('GetThePlugin')}
|
||||
</a>`
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const Notifications = () => (
|
||||
<Page
|
||||
id='notificationSettingPage'
|
||||
title={globalize.translate('Notifications')}
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<div className='content-primary'>
|
||||
<h2>{globalize.translate('Notifications')}</h2>
|
||||
<p>
|
||||
{globalize.translate('NotificationsMovedMessage')}
|
||||
</p>
|
||||
<PluginLink />
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
|
||||
export default Notifications;
|
327
src/apps/dashboard/routes/users/access.tsx
Normal file
327
src/apps/dashboard/routes/users/access.tsx
Normal file
|
@ -0,0 +1,327 @@
|
|||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
AppName?: string;
|
||||
checkedAttribute?: string
|
||||
};
|
||||
|
||||
const UserLibraryAccess: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
|
||||
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
|
||||
const [devicesItems, setDevicesItems] = useState<ItemsArr[]>([]);
|
||||
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const triggerChange = (select: HTMLInputElement) => {
|
||||
const evt = document.createEvent('HTMLEvents');
|
||||
evt.initEvent('change', false, true);
|
||||
select.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
const loadMediaFolders = useCallback((user, mediaFolders) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
for (const folder of mediaFolders) {
|
||||
const isChecked = user.Policy.EnableAllFolders || user.Policy.EnabledFolders.indexOf(folder.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: folder.Id,
|
||||
Name: folder.Name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
setMediaFoldersItems(itemsArr);
|
||||
|
||||
const chkEnableAllFolders = page.querySelector('.chkEnableAllFolders') as HTMLInputElement;
|
||||
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
|
||||
triggerChange(chkEnableAllFolders);
|
||||
}, []);
|
||||
|
||||
const loadChannels = useCallback((user, channels) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
for (const folder of channels) {
|
||||
const isChecked = user.Policy.EnableAllChannels || user.Policy.EnabledChannels.indexOf(folder.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: folder.Id,
|
||||
Name: folder.Name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
setChannelsItems(itemsArr);
|
||||
|
||||
if (channels.length) {
|
||||
(page.querySelector('.channelAccessContainer') as HTMLDivElement).classList.remove('hide');
|
||||
} else {
|
||||
(page.querySelector('.channelAccessContainer') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
|
||||
const chkEnableAllChannels = page.querySelector('.chkEnableAllChannels') as HTMLInputElement;
|
||||
chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
|
||||
triggerChange(chkEnableAllChannels);
|
||||
}, []);
|
||||
|
||||
const loadDevices = useCallback((user, devices) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const isChecked = user.Policy.EnableAllDevices || user.Policy.EnabledDevices.indexOf(device.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: device.Id,
|
||||
Name: device.Name,
|
||||
AppName: device.AppName,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
setDevicesItems(itemsArr);
|
||||
|
||||
const chkEnableAllDevices = page.querySelector('.chkEnableAllDevices') as HTMLInputElement;
|
||||
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
|
||||
triggerChange(chkEnableAllDevices);
|
||||
|
||||
if (user.Policy.IsAdministrator) {
|
||||
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.add('hide');
|
||||
} else {
|
||||
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.remove('hide');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadUser = useCallback((user, mediaFolders, channels, devices) => {
|
||||
setUserName(user.Name);
|
||||
libraryMenu.setTitle(user.Name);
|
||||
loadChannels(user, channels);
|
||||
loadMediaFolders(user, mediaFolders);
|
||||
loadDevices(user, devices);
|
||||
loading.hide();
|
||||
}, [loadChannels, loadDevices, loadMediaFolders]);
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
const promise1 = userId ? window.ApiClient.getUser(userId) : Promise.resolve({ Configuration: {} });
|
||||
const promise2 = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
}));
|
||||
const promise3 = window.ApiClient.getJSON(window.ApiClient.getUrl('Channels'));
|
||||
const promise4 = window.ApiClient.getJSON(window.ApiClient.getUrl('Devices'));
|
||||
Promise.all([promise1, promise2, promise3, promise4]).then(function (responses) {
|
||||
loadUser(responses[0], responses[1].Items, responses[2].Items, responses[3].Items);
|
||||
}).catch(err => {
|
||||
console.error('[userlibraryaccess] failed to load data', err);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
useEffect(() => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
|
||||
const onSubmit = (e: Event) => {
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
}).catch(err => {
|
||||
console.error('[userlibraryaccess] failed to fetch user', err);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
const saveUser = (user: UserDto) => {
|
||||
if (!user.Id) {
|
||||
throw new Error('Unexpected null user.Id');
|
||||
}
|
||||
|
||||
if (!user.Policy) {
|
||||
throw new Error('Unexpected null user.Policy');
|
||||
}
|
||||
|
||||
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
|
||||
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked;
|
||||
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.EnableAllDevices = (page.querySelector('.chkEnableAllDevices') as HTMLInputElement).checked;
|
||||
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkDevice'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.BlockedChannels = null;
|
||||
user.Policy.BlockedMediaFolders = null;
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
}).catch(err => {
|
||||
console.error('[userlibraryaccess] failed to update user policy', err);
|
||||
});
|
||||
};
|
||||
|
||||
const onSaveComplete = () => {
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
};
|
||||
|
||||
(page.querySelector('.chkEnableAllDevices') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
|
||||
(page.querySelector('.deviceAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
|
||||
(page.querySelector('.channelAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
|
||||
(page.querySelector('.folderAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
(page.querySelector('.userLibraryAccessForm') as HTMLFormElement).addEventListener('submit', onSubmit);
|
||||
}, [loadData]);
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='userLibraryAccessPage'
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<div ref={element} className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<SectionTitleContainer
|
||||
title={userName}
|
||||
url='https://jellyfin.org/docs/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
<SectionTabs activeTab='userlibraryaccess'/>
|
||||
<form className='userLibraryAccessForm'>
|
||||
<AccessContainer
|
||||
containerClassName='folderAccessContainer'
|
||||
headerTitle='HeaderLibraryAccess'
|
||||
checkBoxClassName='chkEnableAllFolders'
|
||||
checkBoxTitle='OptionEnableAccessToAllLibraries'
|
||||
listContainerClassName='folderAccessListContainer'
|
||||
accessClassName='folderAccess'
|
||||
listTitle='HeaderLibraries'
|
||||
description='LibraryAccessHelp'
|
||||
>
|
||||
{mediaFoldersItems.map(Item => (
|
||||
<CheckBoxElement
|
||||
key={Item.Id}
|
||||
className='chkFolder'
|
||||
itemId={Item.Id}
|
||||
itemName={Item.Name}
|
||||
itemCheckedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</AccessContainer>
|
||||
|
||||
<AccessContainer
|
||||
containerClassName='channelAccessContainer hide'
|
||||
headerTitle='HeaderChannelAccess'
|
||||
checkBoxClassName='chkEnableAllChannels'
|
||||
checkBoxTitle='OptionEnableAccessToAllChannels'
|
||||
listContainerClassName='channelAccessListContainer'
|
||||
accessClassName='channelAccess'
|
||||
listTitle='Channels'
|
||||
description='ChannelAccessHelp'
|
||||
>
|
||||
{channelsItems.map(Item => (
|
||||
<CheckBoxElement
|
||||
key={Item.Id}
|
||||
className='chkChannel'
|
||||
itemId={Item.Id}
|
||||
itemName={Item.Name}
|
||||
itemCheckedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</AccessContainer>
|
||||
|
||||
<AccessContainer
|
||||
containerClassName='deviceAccessContainer hide'
|
||||
headerTitle='HeaderDeviceAccess'
|
||||
checkBoxClassName='chkEnableAllDevices'
|
||||
checkBoxTitle='OptionEnableAccessFromAllDevices'
|
||||
listContainerClassName='deviceAccessListContainer'
|
||||
accessClassName='deviceAccess'
|
||||
listTitle='HeaderDevices'
|
||||
description='DeviceAccessHelp'
|
||||
>
|
||||
{devicesItems.map(Item => (
|
||||
<CheckBoxElement
|
||||
key={Item.Id}
|
||||
className='chkDevice'
|
||||
itemId={Item.Id}
|
||||
itemName={Item.Name}
|
||||
itemAppName={Item.AppName}
|
||||
itemCheckedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</AccessContainer>
|
||||
<br />
|
||||
<div>
|
||||
<ButtonElement
|
||||
type='submit'
|
||||
className='raised button-submit block'
|
||||
title='Save'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Page>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default UserLibraryAccess;
|
185
src/apps/dashboard/routes/users/index.tsx
Normal file
185
src/apps/dashboard/routes/users/index.tsx
Normal file
|
@ -0,0 +1,185 @@
|
|||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import dom from '../../../../scripts/dom';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
import UserCardBox from '../../../../components/dashboard/users/UserCardBox';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
import '../../../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../../../components/cardbuilder/card.scss';
|
||||
import '../../../../components/indicators/indicators.scss';
|
||||
import '../../../../styles/flexstyles.scss';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type MenuEntry = {
|
||||
name?: string;
|
||||
id?: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
const UserProfiles: FunctionComponent = () => {
|
||||
const [ users, setUsers ] = useState<UserDto[]>([]);
|
||||
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const loadData = () => {
|
||||
loading.show();
|
||||
window.ApiClient.getUsers().then(function (result) {
|
||||
setUsers(result);
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[userprofiles] failed to fetch users', err);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
|
||||
const showUserMenu = (elem: HTMLElement) => {
|
||||
const card = dom.parentWithClass(elem, 'card');
|
||||
const userId = card?.getAttribute('data-userid');
|
||||
|
||||
if (!userId) {
|
||||
console.error('Unexpected null user id');
|
||||
return;
|
||||
}
|
||||
|
||||
const menuItems: MenuEntry[] = [];
|
||||
|
||||
menuItems.push({
|
||||
name: globalize.translate('ButtonOpen'),
|
||||
id: 'open',
|
||||
icon: 'mode_edit'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ButtonLibraryAccess'),
|
||||
id: 'access',
|
||||
icon: 'lock'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ButtonParentalControl'),
|
||||
id: 'parentalcontrol',
|
||||
icon: 'person'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('Delete'),
|
||||
id: 'delete',
|
||||
icon: 'delete'
|
||||
});
|
||||
|
||||
import('../../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: card,
|
||||
callback: function (id: string) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('useredit.html?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user edit page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'access':
|
||||
Dashboard.navigate('userlibraryaccess.html?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user library page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'parentalcontrol':
|
||||
Dashboard.navigate('userparentalcontrol.html?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to parental control page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
deleteUser(userId);
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
// action sheet closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[userprofiles] failed to load action sheet', err);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteUser = (id: string) => {
|
||||
const msg = globalize.translate('DeleteUserConfirmation');
|
||||
|
||||
confirm({
|
||||
title: globalize.translate('DeleteUser'),
|
||||
text: msg,
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(function () {
|
||||
loading.show();
|
||||
window.ApiClient.deleteUser(id).then(function () {
|
||||
loadData();
|
||||
}).catch(err => {
|
||||
console.error('[userprofiles] failed to delete user', err);
|
||||
});
|
||||
}).catch(() => {
|
||||
// confirm dialog closed
|
||||
});
|
||||
};
|
||||
|
||||
page.addEventListener('click', function (e) {
|
||||
const btnUserMenu = dom.parentWithClass(e.target as HTMLElement, 'btnUserMenu');
|
||||
|
||||
if (btnUserMenu) {
|
||||
showUserMenu(btnUserMenu);
|
||||
}
|
||||
});
|
||||
|
||||
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
|
||||
Dashboard.navigate('usernew.html')
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to new user page', err);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='userProfilesPage'
|
||||
className='mainAnimatedPage type-interior userProfilesPage fullWidthContent'
|
||||
>
|
||||
<div ref={element} className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<SectionTitleContainer
|
||||
title={globalize.translate('HeaderUsers')}
|
||||
isBtnVisible={true}
|
||||
btnId='btnAddUser'
|
||||
btnClassName='fab submit sectionTitleButton'
|
||||
btnTitle='ButtonAddUser'
|
||||
btnIcon='add'
|
||||
url='https://jellyfin.org/docs/general/server/users/adding-managing-users'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='localUsers itemsContainer vertical-wrap'>
|
||||
{users.map(user => {
|
||||
return <UserCardBox key={user.Id} user={user} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfiles;
|
269
src/apps/dashboard/routes/users/new.tsx
Normal file
269
src/apps/dashboard/routes/users/new.tsx
Normal file
|
@ -0,0 +1,269 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import InputElement from '../../../../elements/InputElement';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type userInput = {
|
||||
Name?: string;
|
||||
Password?: string;
|
||||
};
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
};
|
||||
|
||||
const UserNew: FunctionComponent = () => {
|
||||
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
|
||||
const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getItemsResult = (items: ItemsArr[]) => {
|
||||
return items.map(item =>
|
||||
({
|
||||
Id: item.Id,
|
||||
Name: item.Name
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const loadMediaFolders = useCallback((result) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaFolders = getItemsResult(result);
|
||||
|
||||
setMediaFoldersItems(mediaFolders);
|
||||
|
||||
const folderAccess = page.querySelector('.folderAccess') as HTMLDivElement;
|
||||
folderAccess.dispatchEvent(new CustomEvent('create'));
|
||||
|
||||
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked = false;
|
||||
}, []);
|
||||
|
||||
const loadChannels = useCallback((result) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = getItemsResult(result);
|
||||
|
||||
setChannelsItems(channels);
|
||||
|
||||
const channelAccess = page.querySelector('.channelAccess') as HTMLDivElement;
|
||||
channelAccess.dispatchEvent(new CustomEvent('create'));
|
||||
|
||||
const channelAccessContainer = page.querySelector('.channelAccessContainer') as HTMLDivElement;
|
||||
channels.length ? channelAccessContainer.classList.remove('hide') : channelAccessContainer.classList.add('hide');
|
||||
|
||||
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked = false;
|
||||
}, []);
|
||||
|
||||
const loadUser = useCallback(() => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
(page.querySelector('#txtUsername') as HTMLInputElement).value = '';
|
||||
(page.querySelector('#txtPassword') as HTMLInputElement).value = '';
|
||||
loading.show();
|
||||
const promiseFolders = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
}));
|
||||
const promiseChannels = window.ApiClient.getJSON(window.ApiClient.getUrl('Channels'));
|
||||
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
|
||||
loadMediaFolders(responses[0].Items);
|
||||
loadChannels(responses[1].Items);
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[usernew] failed to load data', err);
|
||||
});
|
||||
}, [loadChannels, loadMediaFolders]);
|
||||
|
||||
useEffect(() => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
loadUser();
|
||||
|
||||
const saveUser = () => {
|
||||
const userInput: userInput = {};
|
||||
userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value;
|
||||
userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value;
|
||||
window.ApiClient.createUser(userInput).then(function (user) {
|
||||
if (!user.Id || !user.Policy) {
|
||||
throw new Error('Unexpected null user id or policy');
|
||||
}
|
||||
|
||||
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
|
||||
user.Policy.EnabledFolders = [];
|
||||
|
||||
if (!user.Policy.EnableAllFolders) {
|
||||
user.Policy.EnabledFolders = Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (i) {
|
||||
return i.checked;
|
||||
}).map(function (i) {
|
||||
return i.getAttribute('data-id');
|
||||
});
|
||||
}
|
||||
|
||||
user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked;
|
||||
user.Policy.EnabledChannels = [];
|
||||
|
||||
if (!user.Policy.EnableAllChannels) {
|
||||
user.Policy.EnabledChannels = Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (i) {
|
||||
return i.checked;
|
||||
}).map(function (i) {
|
||||
return i.getAttribute('data-id');
|
||||
});
|
||||
}
|
||||
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
Dashboard.navigate('useredit.html?userId=' + user.Id)
|
||||
.catch(err => {
|
||||
console.error('[usernew] failed to navigate to edit user page', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[usernew] failed to update user policy', err);
|
||||
});
|
||||
}, function () {
|
||||
toast(globalize.translate('ErrorDefault'));
|
||||
loading.hide();
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (e: Event) => {
|
||||
loading.show();
|
||||
saveUser();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
|
||||
const channelAccessListContainer = page.querySelector('.channelAccessListContainer') as HTMLDivElement;
|
||||
this.checked ? channelAccessListContainer.classList.add('hide') : channelAccessListContainer.classList.remove('hide');
|
||||
});
|
||||
|
||||
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
|
||||
const folderAccessListContainer = page.querySelector('.folderAccessListContainer') as HTMLDivElement;
|
||||
this.checked ? folderAccessListContainer.classList.add('hide') : folderAccessListContainer.classList.remove('hide');
|
||||
});
|
||||
|
||||
(page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
|
||||
|
||||
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
|
||||
window.history.back();
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='newUserPage'
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<div ref={element} className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<SectionTitleContainer
|
||||
title={globalize.translate('HeaderAddUser')}
|
||||
url='https://jellyfin.org/docs/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form className='newUserProfileForm'>
|
||||
<div className='inputContainer'>
|
||||
<InputElement
|
||||
type='text'
|
||||
id='txtUsername'
|
||||
label='LabelName'
|
||||
options={'required'}
|
||||
/>
|
||||
</div>
|
||||
<div className='inputContainer'>
|
||||
<InputElement
|
||||
type='password'
|
||||
id='txtPassword'
|
||||
label='LabelPassword'
|
||||
/>
|
||||
</div>
|
||||
<AccessContainer
|
||||
containerClassName='folderAccessContainer'
|
||||
headerTitle='HeaderLibraryAccess'
|
||||
checkBoxClassName='chkEnableAllFolders'
|
||||
checkBoxTitle='OptionEnableAccessToAllLibraries'
|
||||
listContainerClassName='folderAccessListContainer'
|
||||
accessClassName='folderAccess'
|
||||
listTitle='HeaderLibraries'
|
||||
description='LibraryAccessHelp'
|
||||
>
|
||||
{mediaFoldersItems.map(Item => (
|
||||
<CheckBoxElement
|
||||
key={Item.Id}
|
||||
className='chkFolder'
|
||||
itemId={Item.Id}
|
||||
itemName={Item.Name}
|
||||
/>
|
||||
))}
|
||||
</AccessContainer>
|
||||
|
||||
<AccessContainer
|
||||
containerClassName='channelAccessContainer verticalSection-extrabottompadding hide'
|
||||
headerTitle='HeaderChannelAccess'
|
||||
checkBoxClassName='chkEnableAllChannels'
|
||||
checkBoxTitle='OptionEnableAccessToAllChannels'
|
||||
listContainerClassName='channelAccessListContainer'
|
||||
accessClassName='channelAccess'
|
||||
listTitle='Channels'
|
||||
description='ChannelAccessHelp'
|
||||
>
|
||||
{channelsItems.map(Item => (
|
||||
<CheckBoxElement
|
||||
key={Item.Id}
|
||||
className='chkChannel'
|
||||
itemId={Item.Id}
|
||||
itemName={Item.Name}
|
||||
/>
|
||||
))}
|
||||
</AccessContainer>
|
||||
<div>
|
||||
<ButtonElement
|
||||
type='submit'
|
||||
className='raised button-submit block'
|
||||
title='Save'
|
||||
/>
|
||||
<ButtonElement
|
||||
type='button'
|
||||
id='btnCancel'
|
||||
className='raised button-cancel block'
|
||||
title='ButtonCancel'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Page>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default UserNew;
|
439
src/apps/dashboard/routes/users/parentalcontrol.tsx
Normal file
439
src/apps/dashboard/routes/users/parentalcontrol.tsx
Normal file
|
@ -0,0 +1,439 @@
|
|||
import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import escapeHTML from 'escape-html';
|
||||
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList';
|
||||
import BlockedTagList from '../../../../components/dashboard/users/BlockedTagList';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import SelectElement from '../../../../elements/SelectElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type UnratedItem = {
|
||||
name: string;
|
||||
value: string;
|
||||
checkedAttribute: string
|
||||
};
|
||||
|
||||
const UserParentalControl: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
|
||||
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
|
||||
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
|
||||
const [ blockedTags, setBlockedTags ] = useState([]);
|
||||
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const populateRatings = useCallback((allParentalRatings) => {
|
||||
let rating;
|
||||
const ratings: ParentalRating[] = [];
|
||||
|
||||
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
|
||||
rating = allParentalRatings[i];
|
||||
|
||||
if (ratings.length) {
|
||||
const lastRating = ratings[ratings.length - 1];
|
||||
|
||||
if (lastRating.Value === rating.Value) {
|
||||
lastRating.Name += '/' + rating.Name;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ratings.push({
|
||||
Name: rating.Name,
|
||||
Value: rating.Value
|
||||
});
|
||||
}
|
||||
|
||||
setParentalRatings(ratings);
|
||||
}, []);
|
||||
|
||||
const loadUnratedItems = useCallback((user) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
const items = [{
|
||||
name: globalize.translate('Books'),
|
||||
value: 'Book'
|
||||
}, {
|
||||
name: globalize.translate('Channels'),
|
||||
value: 'ChannelContent'
|
||||
}, {
|
||||
name: globalize.translate('LiveTV'),
|
||||
value: 'LiveTvChannel'
|
||||
}, {
|
||||
name: globalize.translate('Movies'),
|
||||
value: 'Movie'
|
||||
}, {
|
||||
name: globalize.translate('Music'),
|
||||
value: 'Music'
|
||||
}, {
|
||||
name: globalize.translate('Trailers'),
|
||||
value: 'Trailer'
|
||||
}, {
|
||||
name: globalize.translate('Shows'),
|
||||
value: 'Series'
|
||||
}];
|
||||
|
||||
const itemsArr: UnratedItem[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
value: item.value,
|
||||
name: item.name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
setUnratedItems(itemsArr);
|
||||
|
||||
const blockUnratedItems = page.querySelector('.blockUnratedItems') as HTMLDivElement;
|
||||
blockUnratedItems.dispatchEvent(new CustomEvent('create'));
|
||||
}, []);
|
||||
|
||||
const loadBlockedTags = useCallback((tags) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
setBlockedTags(tags);
|
||||
|
||||
const blockedTagsElem = page.querySelector('.blockedTags') as HTMLDivElement;
|
||||
|
||||
for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) {
|
||||
btnDeleteTag.addEventListener('click', function () {
|
||||
const tag = btnDeleteTag.getAttribute('data-tag');
|
||||
const newTags = tags.filter(function (t: string) {
|
||||
return t != tag;
|
||||
});
|
||||
loadBlockedTags(newTags);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderAccessSchedule = useCallback((schedules) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
setAccessSchedules(schedules);
|
||||
|
||||
const accessScheduleList = page.querySelector('.accessScheduleList') as HTMLDivElement;
|
||||
|
||||
for (const btnDelete of accessScheduleList.querySelectorAll('.btnDelete')) {
|
||||
btnDelete.addEventListener('click', function () {
|
||||
const index = parseInt(btnDelete.getAttribute('data-index') ?? '0', 10);
|
||||
schedules.splice(index, 1);
|
||||
const newindex = schedules.filter(function (i: number) {
|
||||
return i != index;
|
||||
});
|
||||
renderAccessSchedule(newindex);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadUser = useCallback((user, allParentalRatings) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
setUserName(user.Name);
|
||||
LibraryMenu.setTitle(user.Name);
|
||||
loadUnratedItems(user);
|
||||
|
||||
loadBlockedTags(user.Policy.BlockedTags);
|
||||
populateRatings(allParentalRatings);
|
||||
let ratingValue = '';
|
||||
|
||||
if (user.Policy.MaxParentalRating != null) {
|
||||
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
|
||||
const rating = allParentalRatings[i];
|
||||
|
||||
if (user.Policy.MaxParentalRating >= rating.Value) {
|
||||
ratingValue = rating.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue;
|
||||
|
||||
if (user.Policy.IsAdministrator) {
|
||||
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
|
||||
} else {
|
||||
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide');
|
||||
}
|
||||
renderAccessSchedule(user.Policy.AccessSchedules || []);
|
||||
loading.hide();
|
||||
}, [loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
const promise1 = window.ApiClient.getUser(userId);
|
||||
const promise2 = window.ApiClient.getParentalRatings();
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
loadUser(responses[0], responses[1]);
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to load data', err);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
useEffect(() => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
|
||||
const onSaveComplete = () => {
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
};
|
||||
|
||||
const saveUser = (user: UserDto) => {
|
||||
if (!user.Id || !user.Policy) {
|
||||
throw new Error('Unexpected null user id or policy');
|
||||
}
|
||||
|
||||
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);
|
||||
user.Policy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating;
|
||||
user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) {
|
||||
return i.checked;
|
||||
}).map(function (i) {
|
||||
return i.getAttribute('data-itemtype');
|
||||
});
|
||||
user.Policy.AccessSchedules = getSchedulesFromPage();
|
||||
user.Policy.BlockedTags = getBlockedTagsFromPage();
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to update user policy', err);
|
||||
});
|
||||
};
|
||||
|
||||
const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
|
||||
schedule = schedule || {};
|
||||
import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
|
||||
accessschedule.show({
|
||||
schedule: schedule
|
||||
}).then(function (updatedSchedule) {
|
||||
const schedules = getSchedulesFromPage();
|
||||
|
||||
if (index == -1) {
|
||||
index = schedules.length;
|
||||
}
|
||||
|
||||
schedules[index] = updatedSchedule;
|
||||
renderAccessSchedule(schedules);
|
||||
}).catch(() => {
|
||||
// access schedule closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to load access schedule', err);
|
||||
});
|
||||
};
|
||||
|
||||
const getSchedulesFromPage = () => {
|
||||
return Array.prototype.map.call(page.querySelectorAll('.liSchedule'), function (elem) {
|
||||
return {
|
||||
DayOfWeek: elem.getAttribute('data-day'),
|
||||
StartHour: elem.getAttribute('data-start'),
|
||||
EndHour: elem.getAttribute('data-end')
|
||||
};
|
||||
}) as AccessSchedule[];
|
||||
};
|
||||
|
||||
const getBlockedTagsFromPage = () => {
|
||||
return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) {
|
||||
return elem.getAttribute('data-tag');
|
||||
}) as string[];
|
||||
};
|
||||
|
||||
const showBlockedTagPopup = () => {
|
||||
import('../../../../components/prompt/prompt').then(({ default: prompt }) => {
|
||||
prompt({
|
||||
label: globalize.translate('LabelTag')
|
||||
}).then(function (value) {
|
||||
const tags = getBlockedTagsFromPage();
|
||||
|
||||
if (tags.indexOf(value) == -1) {
|
||||
tags.push(value);
|
||||
loadBlockedTags(tags);
|
||||
}
|
||||
}).catch(() => {
|
||||
// prompt closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to load prompt', err);
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (e: Event) => {
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to fetch user', err);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
(page.querySelector('#btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () {
|
||||
showSchedulePopup({
|
||||
Id: 0,
|
||||
UserId: '',
|
||||
DayOfWeek: DynamicDayOfWeek.Sunday,
|
||||
StartHour: 0,
|
||||
EndHour: 0
|
||||
}, -1);
|
||||
});
|
||||
|
||||
(page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
|
||||
showBlockedTagPopup();
|
||||
});
|
||||
|
||||
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
|
||||
}, [loadBlockedTags, loadData, renderAccessSchedule]);
|
||||
|
||||
const optionMaxParentalRating = () => {
|
||||
let content = '';
|
||||
content += '<option value=\'\'></option>';
|
||||
for (const rating of parentalRatings) {
|
||||
content += `<option value='${rating.Value}'>${escapeHTML(rating.Name)}</option>`;
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='userParentalControlPage'
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<div ref={element} className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<SectionTitleContainer
|
||||
title={userName}
|
||||
url='https://jellyfin.org/docs/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
<SectionTabs activeTab='userparentalcontrol'/>
|
||||
<form className='userParentalControlForm'>
|
||||
<div className='selectContainer'>
|
||||
<SelectElement
|
||||
id='selectMaxParentalRating'
|
||||
label='LabelMaxParentalRating'
|
||||
>
|
||||
{optionMaxParentalRating()}
|
||||
</SelectElement>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('MaxParentalRatingHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='blockUnratedItems'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderBlockItemsWithNoRating')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
|
||||
{unratedItems.map(Item => {
|
||||
return <CheckBoxElement
|
||||
key={Item.value}
|
||||
className='chkUnratedItem'
|
||||
itemType={Item.value}
|
||||
itemName={Item.name}
|
||||
itemCheckedAttribute={Item.checkedAttribute}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='verticalSection' style={{ marginBottom: '2em' }}>
|
||||
<SectionTitleContainer
|
||||
SectionClassName='detailSectionHeader'
|
||||
title={globalize.translate('LabelBlockContentWithTags')}
|
||||
isBtnVisible={true}
|
||||
btnId='btnAddBlockedTag'
|
||||
btnClassName='fab submit sectionTitleButton'
|
||||
btnTitle='Add'
|
||||
btnIcon='add'
|
||||
isLinkVisible={false}
|
||||
/>
|
||||
<div className='blockedTags' style={{ marginTop: '.5em' }}>
|
||||
{blockedTags.map(tag => {
|
||||
return <BlockedTagList
|
||||
key={tag}
|
||||
tag={tag}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='accessScheduleSection verticalSection' style={{ marginBottom: '2em' }}>
|
||||
<SectionTitleContainer
|
||||
title={globalize.translate('HeaderAccessSchedule')}
|
||||
isBtnVisible={true}
|
||||
btnId='btnAddSchedule'
|
||||
btnClassName='fab submit sectionTitleButton'
|
||||
btnTitle='Add'
|
||||
btnIcon='add'
|
||||
isLinkVisible={false}
|
||||
/>
|
||||
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
|
||||
<div className='accessScheduleList paperList'>
|
||||
{accessSchedules.map((accessSchedule, index) => {
|
||||
return <AccessScheduleList
|
||||
key={accessSchedule.Id}
|
||||
index={index}
|
||||
Id={accessSchedule.Id}
|
||||
DayOfWeek={accessSchedule.DayOfWeek}
|
||||
StartHour={accessSchedule.StartHour}
|
||||
EndHour={accessSchedule.EndHour}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonElement
|
||||
type='submit'
|
||||
className='raised button-submit block'
|
||||
title='Save'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Page>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default UserParentalControl;
|
54
src/apps/dashboard/routes/users/password.tsx
Normal file
54
src/apps/dashboard/routes/users/password.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import Page from '../../../../components/Page';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
|
||||
const UserPassword: FunctionComponent = () => {
|
||||
const userId = getParameterByName('userId');
|
||||
const [ userName, setUserName ] = useState('');
|
||||
|
||||
const loadUser = useCallback(() => {
|
||||
loading.show();
|
||||
window.ApiClient.getUser(userId).then(function (user) {
|
||||
if (!user.Name) {
|
||||
throw new Error('Unexpected null user.Name');
|
||||
}
|
||||
setUserName(user.Name);
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[userpassword] failed to fetch user', err);
|
||||
});
|
||||
}, [userId]);
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
}, [loadUser]);
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='userPasswordPage'
|
||||
className='mainAnimatedPage type-interior userPasswordPage'
|
||||
>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<SectionTitleContainer
|
||||
title={userName}
|
||||
url='https://jellyfin.org/docs/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
<SectionTabs activeTab='userpassword'/>
|
||||
<div className='readOnlyContent'>
|
||||
<UserPasswordForm
|
||||
userId={userId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default UserPassword;
|
597
src/apps/dashboard/routes/users/profile.tsx
Normal file
597
src/apps/dashboard/routes/users/profile.tsx
Normal file
|
@ -0,0 +1,597 @@
|
|||
import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import escapeHTML from 'escape-html';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import InputElement from '../../../../elements/InputElement';
|
||||
import LinkEditUserPreferences from '../../../../components/dashboard/users/LinkEditUserPreferences';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SelectElement from '../../../../elements/SelectElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type ResetProvider = AuthProvider & {
|
||||
checkedAttribute: string
|
||||
};
|
||||
|
||||
type AuthProvider = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
};
|
||||
|
||||
const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
|
||||
Array.prototype.filter.call(elements, e => e.checked)
|
||||
.map(e => e.getAttribute('data-id'))
|
||||
);
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html')
|
||||
.catch(err => {
|
||||
console.error('[useredit] failed to navigate to user profile', err);
|
||||
});
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
const UserEdit: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
|
||||
const [ authProviders, setAuthProviders ] = useState<AuthProvider[]>([]);
|
||||
const [ passwordResetProviders, setPasswordResetProviders ] = useState<ResetProvider[]>([]);
|
||||
|
||||
const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
|
||||
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
|
||||
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const triggerChange = (select: HTMLInputElement) => {
|
||||
const evt = document.createEvent('HTMLEvents');
|
||||
evt.initEvent('change', false, true);
|
||||
select.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
const getUser = () => {
|
||||
const userId = getParameterByName('userId');
|
||||
return window.ApiClient.getUser(userId);
|
||||
};
|
||||
|
||||
const loadAuthProviders = useCallback((user, providers) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement;
|
||||
fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1);
|
||||
|
||||
setAuthProviders(providers);
|
||||
|
||||
const currentProviderId = user.Policy.AuthenticationProviderId;
|
||||
setAuthenticationProviderId(currentProviderId);
|
||||
}, []);
|
||||
|
||||
const loadPasswordResetProviders = useCallback((user, providers) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement;
|
||||
fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1);
|
||||
|
||||
setPasswordResetProviders(providers);
|
||||
|
||||
const currentProviderId = user.Policy.PasswordResetProviderId;
|
||||
setPasswordResetProviderId(currentProviderId);
|
||||
}, []);
|
||||
|
||||
const loadDeleteFolders = useCallback((user, mediaFolders) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', {
|
||||
SupportsMediaDeletion: true
|
||||
})).then(function (channelsResult) {
|
||||
let isChecked;
|
||||
let checkedAttribute;
|
||||
const itemsArr: ResetProvider[] = [];
|
||||
|
||||
for (const folder of mediaFolders) {
|
||||
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
|
||||
checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: folder.Id,
|
||||
Name: folder.Name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
for (const folder of channelsResult.Items) {
|
||||
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
|
||||
checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
itemsArr.push({
|
||||
Id: folder.Id,
|
||||
Name: folder.Name,
|
||||
checkedAttribute: checkedAttribute
|
||||
});
|
||||
}
|
||||
|
||||
setDeleteFoldersAccess(itemsArr);
|
||||
|
||||
const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
|
||||
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
|
||||
triggerChange(chkEnableDeleteAllFolders);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch channels', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadUser = useCallback((user) => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) {
|
||||
loadAuthProviders(user, providers);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch auth providers', err);
|
||||
});
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
|
||||
loadPasswordResetProviders(user, providers);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch password reset providers', err);
|
||||
});
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
})).then(function (folders) {
|
||||
loadDeleteFolders(user, folders.Items);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch media folders', err);
|
||||
});
|
||||
|
||||
const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement;
|
||||
disabledUserBanner.classList.toggle('hide', !user.Policy.IsDisabled);
|
||||
|
||||
const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
|
||||
txtUserName.disabled = false;
|
||||
txtUserName.removeAttribute('disabled');
|
||||
|
||||
const lnkEditUserPreferences = page.querySelector('.lnkEditUserPreferences') as HTMLDivElement;
|
||||
lnkEditUserPreferences.setAttribute('href', 'mypreferencesmenu.html?userId=' + user.Id);
|
||||
LibraryMenu.setTitle(user.Name);
|
||||
setUserName(user.Name);
|
||||
(page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name;
|
||||
(page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = user.Policy.IsAdministrator;
|
||||
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
|
||||
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
|
||||
(page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = user.Policy.EnableCollectionManagement;
|
||||
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
|
||||
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
|
||||
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
|
||||
(page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = user.Policy.EnableLiveTvManagement;
|
||||
(page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = user.Policy.EnableLiveTvAccess;
|
||||
(page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = user.Policy.EnableMediaPlayback;
|
||||
(page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableAudioPlaybackTranscoding;
|
||||
(page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableVideoPlaybackTranscoding;
|
||||
(page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = user.Policy.EnablePlaybackRemuxing;
|
||||
(page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = user.Policy.ForceRemoteSourceTranscoding;
|
||||
(page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess;
|
||||
(page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy.RemoteClientBitrateLimit > 0 ?
|
||||
(user.Policy.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 }) : '';
|
||||
(page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = user.Policy.LoginAttemptsBeforeLockout || '0';
|
||||
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = user.Policy.MaxActiveSessions || '0';
|
||||
if (window.ApiClient.isMinServerVersion('10.6.0')) {
|
||||
(page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = user.Policy.SyncPlayAccess;
|
||||
}
|
||||
loading.hide();
|
||||
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
loading.show();
|
||||
getUser().then(function (user) {
|
||||
loadUser(user);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to load data', err);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
useEffect(() => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
console.error('Unexpected null reference');
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
|
||||
const saveUser = (user: UserDto) => {
|
||||
if (!user.Id || !user.Policy) {
|
||||
throw new Error('Unexpected null user id or policy');
|
||||
}
|
||||
|
||||
user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value;
|
||||
user.Policy.IsAdministrator = (page.querySelector('.chkIsAdmin') as HTMLInputElement).checked;
|
||||
user.Policy.IsHidden = (page.querySelector('.chkIsHidden') as HTMLInputElement).checked;
|
||||
user.Policy.IsDisabled = (page.querySelector('.chkDisabled') as HTMLInputElement).checked;
|
||||
user.Policy.EnableRemoteControlOfOtherUsers = (page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked;
|
||||
user.Policy.EnableLiveTvManagement = (page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked;
|
||||
user.Policy.EnableLiveTvAccess = (page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked;
|
||||
user.Policy.EnableSharedDeviceControl = (page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked;
|
||||
user.Policy.EnableMediaPlayback = (page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked;
|
||||
user.Policy.EnableAudioPlaybackTranscoding = (page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked;
|
||||
user.Policy.EnableVideoPlaybackTranscoding = (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked;
|
||||
user.Policy.EnablePlaybackRemuxing = (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked;
|
||||
user.Policy.EnableCollectionManagement = (page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked;
|
||||
user.Policy.ForceRemoteSourceTranscoding = (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked;
|
||||
user.Policy.EnableContentDownloading = (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked;
|
||||
user.Policy.EnableRemoteAccess = (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked;
|
||||
user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat((page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value || '0'));
|
||||
user.Policy.LoginAttemptsBeforeLockout = parseInt((page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value || '0', 10);
|
||||
user.Policy.MaxActiveSessions = parseInt((page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value || '0', 10);
|
||||
user.Policy.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value;
|
||||
user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value;
|
||||
user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked;
|
||||
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : getCheckedElementDataIds(page.querySelectorAll('.chkFolder'));
|
||||
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
|
||||
|
||||
window.ApiClient.updateUser(user).then(() => (
|
||||
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {})
|
||||
)).then(() => {
|
||||
onSaveComplete();
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to update user', err);
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (e: Event) => {
|
||||
loading.show();
|
||||
getUser().then(function (result) {
|
||||
saveUser(result);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch user', err);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
(page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
|
||||
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
window.ApiClient.getNamedConfiguration('network').then(function (config) {
|
||||
(page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !config.EnableRemoteAccess);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to load network config', err);
|
||||
});
|
||||
|
||||
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
|
||||
|
||||
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
|
||||
window.history.back();
|
||||
});
|
||||
}, [loadData]);
|
||||
|
||||
const optionLoginProvider = authProviders.map((provider) => {
|
||||
const selected = provider.Id === authenticationProviderId || authProviders.length < 2 ? ' selected' : '';
|
||||
return `<option value="${provider.Id}"${selected}>${escapeHTML(provider.Name)}</option>`;
|
||||
});
|
||||
|
||||
const optionPasswordResetProvider = passwordResetProviders.map((provider) => {
|
||||
const selected = provider.Id === passwordResetProviderId || passwordResetProviders.length < 2 ? ' selected' : '';
|
||||
return `<option value="${provider.Id}"${selected}>${escapeHTML(provider.Name)}</option>`;
|
||||
});
|
||||
|
||||
const optionSyncPlayAccess = () => {
|
||||
let content = '';
|
||||
content += `<option value='CreateAndJoinGroups'>${globalize.translate('LabelSyncPlayAccessCreateAndJoinGroups')}</option>`;
|
||||
content += `<option value='JoinGroups'>${globalize.translate('LabelSyncPlayAccessJoinGroups')}</option>`;
|
||||
content += `<option value='None'>${globalize.translate('LabelSyncPlayAccessNone')}</option>`;
|
||||
return content;
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='editUserPage'
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<div ref={element} className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<SectionTitleContainer
|
||||
title={userName}
|
||||
url='https://jellyfin.org/docs/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SectionTabs activeTab='useredit'/>
|
||||
<div
|
||||
className='lnkEditUserPreferencesContainer'
|
||||
style={{ paddingBottom: '1em' }}
|
||||
>
|
||||
<LinkEditUserPreferences
|
||||
className= 'lnkEditUserPreferences button-link'
|
||||
title= 'ButtonEditOtherUserPreferences'
|
||||
/>
|
||||
</div>
|
||||
<form className='editUserProfileForm'>
|
||||
<div className='disabledUserBanner hide'>
|
||||
<div className='btn btnDarkAccent btnStatic'>
|
||||
<div>
|
||||
{globalize.translate('HeaderThisUserIsCurrentlyDisabled')}
|
||||
</div>
|
||||
<div style={{ marginTop: 5 }}>
|
||||
{globalize.translate('MessageReenableUser')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='fldUserName' className='inputContainer'>
|
||||
<InputElement
|
||||
type='text'
|
||||
id='txtUserName'
|
||||
label='LabelName'
|
||||
options={'required'}
|
||||
/>
|
||||
</div>
|
||||
<div className='selectContainer fldSelectLoginProvider hide'>
|
||||
<SelectElement
|
||||
id='selectLoginProvider'
|
||||
label='LabelAuthProvider'
|
||||
>
|
||||
{optionLoginProvider}
|
||||
</SelectElement>
|
||||
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('AuthProviderHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='selectContainer fldSelectPasswordResetProvider hide'>
|
||||
<SelectElement
|
||||
id='selectPasswordResetProvider'
|
||||
label='LabelPasswordResetProvider'
|
||||
>
|
||||
{optionPasswordResetProvider}
|
||||
</SelectElement>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('PasswordResetProviderHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide'>
|
||||
<CheckBoxElement
|
||||
className='chkRemoteAccess'
|
||||
title='AllowRemoteAccess'
|
||||
/>
|
||||
<div className='fieldDescription checkboxFieldDescription'>
|
||||
{globalize.translate('AllowRemoteAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
className='chkIsAdmin'
|
||||
title='OptionAllowUserToManageServer'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
className='chkEnableCollectionManagement'
|
||||
title='AllowCollectionManagement'
|
||||
/>
|
||||
<div id='featureAccessFields' className='verticalSection'>
|
||||
<h2 className='paperListLabel'>
|
||||
{globalize.translate('HeaderFeatureAccess')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
|
||||
<CheckBoxElement
|
||||
className='chkEnableLiveTvAccess'
|
||||
title='OptionAllowBrowsingLiveTv'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
className='chkManageLiveTv'
|
||||
title='OptionAllowManageLiveTv'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='verticalSection'>
|
||||
<h2 className='paperListLabel'>
|
||||
{globalize.translate('HeaderPlayback')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
|
||||
<CheckBoxElement
|
||||
className='chkEnableMediaPlayback'
|
||||
title='OptionAllowMediaPlayback'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
className='chkEnableAudioPlaybackTranscoding'
|
||||
title='OptionAllowAudioPlaybackTranscoding'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
className='chkEnableVideoPlaybackTranscoding'
|
||||
title='OptionAllowVideoPlaybackTranscoding'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
className='chkEnableVideoPlaybackRemuxing'
|
||||
title='OptionAllowVideoPlaybackRemuxing'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
className='chkForceRemoteSourceTranscoding'
|
||||
title='OptionForceRemoteSourceTranscoding'
|
||||
/>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionAllowMediaPlaybackTranscodingHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='verticalSection'>
|
||||
<div className='inputContainer'>
|
||||
<InputElement
|
||||
type='number'
|
||||
id='txtRemoteClientBitrateLimit'
|
||||
label='LabelRemoteClientBitrateLimit'
|
||||
options={'inputMode="decimal" pattern="[0-9]*(.[0-9]+)?" min="{0}" step=".25"'}
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('LabelRemoteClientBitrateLimitHelp')}
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('LabelUserRemoteClientBitrateLimitHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='verticalSection'>
|
||||
<div className='selectContainer fldSelectSyncPlayAccess'>
|
||||
<SelectElement
|
||||
id='selectSyncPlayAccess'
|
||||
label='LabelSyncPlayAccess'
|
||||
>
|
||||
{optionSyncPlayAccess()}
|
||||
</SelectElement>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('SyncPlayAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='verticalSection'>
|
||||
<h2 className='checkboxListLabel' style={{ marginBottom: '1em' }}>
|
||||
{globalize.translate('HeaderAllowMediaDeletionFrom')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList checkboxList-paperList'>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
className='chkEnableDeleteAllFolders'
|
||||
title='AllLibraries'
|
||||
/>
|
||||
<div className='deleteAccess'>
|
||||
{deleteFoldersAccess.map(Item => (
|
||||
<CheckBoxElement
|
||||
key={Item.Id}
|
||||
className='chkFolder'
|
||||
itemId={Item.Id}
|
||||
itemName={Item.Name}
|
||||
itemCheckedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='verticalSection'>
|
||||
<h2 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderRemoteControl')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
|
||||
<CheckBoxElement
|
||||
className='chkEnableRemoteControlOtherUsers'
|
||||
title='OptionAllowRemoteControlOthers'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
className='chkRemoteControlSharedDevices'
|
||||
title='OptionAllowRemoteSharedDevices'
|
||||
/>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionAllowRemoteSharedDevicesHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<h2 className='checkboxListLabel'>
|
||||
{globalize.translate('Other')}
|
||||
</h2>
|
||||
<div className='checkboxContainer checkboxContainer-withDescription'>
|
||||
<CheckBoxElement
|
||||
className='chkEnableDownloading'
|
||||
title='OptionAllowContentDownload'
|
||||
/>
|
||||
<div className='fieldDescription checkboxFieldDescription'>
|
||||
{globalize.translate('OptionAllowContentDownloadHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsEnabled'>
|
||||
<CheckBoxElement
|
||||
className='chkDisabled'
|
||||
title='OptionDisableUser'
|
||||
/>
|
||||
<div className='fieldDescription checkboxFieldDescription'>
|
||||
{globalize.translate('OptionDisableUserHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsHidden'>
|
||||
<CheckBoxElement
|
||||
className='chkIsHidden'
|
||||
title='OptionHideUser'
|
||||
/>
|
||||
<div className='fieldDescription checkboxFieldDescription'>
|
||||
{globalize.translate('OptionHideUserFromLoginHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='verticalSection'>
|
||||
<div className='inputContainer' id='fldLoginAttemptsBeforeLockout'>
|
||||
<InputElement
|
||||
type='number'
|
||||
id='txtLoginAttemptsBeforeLockout'
|
||||
label='LabelUserLoginAttemptsBeforeLockout'
|
||||
options={'min={-1} step={1}'}
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionLoginAttemptsBeforeLockout')}
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionLoginAttemptsBeforeLockoutHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='verticalSection'>
|
||||
<div className='inputContainer' id='fldMaxActiveSessions'>
|
||||
<InputElement
|
||||
type='number'
|
||||
id='txtMaxActiveSessions'
|
||||
label='LabelUserMaxActiveSessions'
|
||||
options={'min={0} step={1}'}
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionMaxActiveSessions')}
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('OptionMaxActiveSessionsHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<ButtonElement
|
||||
type='submit'
|
||||
className='raised button-submit block'
|
||||
title='Save'
|
||||
/>
|
||||
<ButtonElement
|
||||
type='button'
|
||||
id='btnCancel'
|
||||
className='raised button-cancel block'
|
||||
title='ButtonCancel'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Page>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default UserEdit;
|
Loading…
Add table
Add a link
Reference in a new issue