1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Rewrite devices page in react

This commit is contained in:
Bill Thornton 2024-08-30 10:10:58 -04:00
parent ad053d6656
commit 9c0aa85c46
5 changed files with 212 additions and 0 deletions

View file

@ -27,6 +27,14 @@ const DevicesDrawerSection = () => {
<ListItemText primary={globalize.translate('HeaderDevices')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/devices2'>
<ListItemIcon>
<Devices />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderDevices')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/activity'>
<ListItemIcon>

View file

@ -0,0 +1,36 @@
import type { DevicesApiGetDevicesRequest } from '@jellyfin/sdk/lib/generated-client';
import type { AxiosRequestConfig } from 'axios';
import type { Api } from '@jellyfin/sdk';
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
const fetchDevices = async (
api?: Api,
requestParams?: DevicesApiGetDevicesRequest,
options?: AxiosRequestConfig
) => {
if (!api) {
console.warn('[fetchDevices] No API instance available');
return;
}
const response = await getDevicesApi(api).getDevices(requestParams, {
signal: options?.signal
});
return response.data;
};
export const useDevices = (
requestParams: DevicesApiGetDevicesRequest
) => {
const { api } = useApi();
return useQuery({
queryKey: ['Devices', requestParams],
queryFn: ({ signal }) =>
fetchDevices(api, requestParams, { signal }),
enabled: !!api
});
};

View file

@ -4,6 +4,7 @@ import { AppType } from 'constants/appType';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'activity', type: AppType.Dashboard },
{ path: 'branding', type: AppType.Dashboard },
{ path: 'devices2', page: 'devices', type: AppType.Dashboard },
{ path: 'keys', type: AppType.Dashboard },
{ path: 'logs', type: AppType.Dashboard },
{ path: 'playback/trickplay', type: AppType.Dashboard },

View file

@ -0,0 +1,166 @@
import type { DeviceInfoDto } from '@jellyfin/sdk/lib/generated-client/models/device-info-dto';
import Delete from '@mui/icons-material/Delete';
import Edit from '@mui/icons-material/Edit';
import Box from '@mui/material/Box/Box';
import Button from '@mui/material/Button/Button';
import IconButton from '@mui/material/IconButton/IconButton';
import Tooltip from '@mui/material/Tooltip/Tooltip';
import React, { useMemo } from 'react';
import TablePage from 'apps/dashboard/components/TablePage';
import { useDevices } from 'apps/dashboard/features/devices/api/useDevices';
import globalize from 'lib/globalize';
import { type MRT_ColumnDef, useMaterialReactTable } from 'material-react-table';
import { parseISO8601Date, toLocaleString } from 'scripts/datetime';
import { useApi } from 'hooks/useApi';
import { getDeviceIcon } from 'utils/image';
import UserAvatarButton from 'apps/dashboard/features/activity/components/UserAvatarButton';
import { useUsers } from 'hooks/useUsers';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
type UsersRecords = Record<string, UserDto>;
const DevicesPage = () => {
const { api } = useApi();
const { data: devices, isLoading: isDevicesLoading } = useDevices({});
const { data: usersData, isLoading: isUsersLoading } = useUsers();
const isLoading = isDevicesLoading || isUsersLoading;
const users: UsersRecords = useMemo(() => {
if (!usersData) return {};
return usersData.reduce<UsersRecords>((acc, user) => {
const userId = user.Id;
if (!userId) return acc;
return {
...acc,
[userId]: user
};
}, {});
}, [ usersData ]);
const columns = useMemo<MRT_ColumnDef<DeviceInfoDto>[]>(() => [
{
id: 'DateLastActivity',
accessorFn: row => parseISO8601Date(row.DateLastActivity),
header: globalize.translate('LabelTime'),
size: 160,
Cell: ({ cell }) => toLocaleString(cell.getValue<Date>()),
filterVariant: 'datetime-range',
enableEditing: false
},
{
id: 'Name',
accessorFn: row => row.CustomName || row.Name,
header: globalize.translate('LabelDevice'),
size: 200,
Cell: ({ row, renderedCellValue }) => (
<>
<img
alt={row.original.AppName || undefined}
src={getDeviceIcon(row.original)}
style={{
display: 'inline-block',
maxWidth: '1.5em',
maxHeight: '1.5em',
marginRight: '1rem'
}}
/>
{renderedCellValue}
</>
)
},
{
id: 'App',
accessorFn: row => [row.AppName, row.AppVersion]
.filter(v => !!v) // filter missing values
.join(' '),
header: globalize.translate('LabelAppName'),
size: 200,
enableEditing: false
},
{
accessorKey: 'LastUserName',
header: globalize.translate('LabelUser'),
size: 120,
enableEditing: false,
Cell: ({ row, renderedCellValue }) => (
<>
<UserAvatarButton user={row.original.LastUserId && users[row.original.LastUserId] || undefined} />
{renderedCellValue}
</>
)
}
], [ users ]);
const mrTable = useMaterialReactTable({
columns,
data: devices?.Items || [],
// Enable custom features
enableColumnPinning: true,
enableColumnResizing: true,
enableEditing: true,
// Sticky header/footer
enableStickyFooter: true,
enableStickyHeader: true,
muiTableContainerProps: {
sx: {
maxHeight: 'calc(100% - 7rem)' // 2 x 3.5rem for header and footer
}
},
// State
initialState: {
pagination: {
pageIndex: 0,
pageSize: 25
}
},
state: {
isLoading
},
// Custom actions
enableRowActions: true,
positionActionsColumn: 'last',
renderRowActions: ({ row, table }) => (
<Box sx={{ display: 'flex', gap: 1 }}>
<Tooltip title={globalize.translate('Edit')}>
<IconButton
onClick={() => table.setEditingRow(row)}
>
<Edit />
</IconButton>
</Tooltip>
<Tooltip title={globalize.translate('Delete')}>
<IconButton
color='error'
disabled={api && api.deviceInfo.id === row.original.Id}
>
<Delete />
</IconButton>
</Tooltip>
</Box>
),
// Custom toolbar contents
renderTopToolbarCustomActions: () => (
<Button color='error'>{globalize.translate('DeleteAll')}</Button>
)
});
return (
<TablePage
id='devicesPage'
title={globalize.translate('HeaderDevices')}
className='mainAnimatedPage type-interior'
table={mrTable}
/>
);
};
export default DevicesPage;

View file

@ -661,6 +661,7 @@
"LabelDelimiterWhitelist": "Delimiter Whitelist",
"LabelDelimiterWhitelistHelp": "Items to be excluded from tag splitting. One item per line.",
"LabelDeveloper": "Developer",
"LabelDevice": "Device",
"LabelDisableCustomCss": "Disable custom CSS code for theming/branding provided from the server.",
"LabelDisableVbrAudioEncoding": "Disable VBR audio encoding",
"LabelDiscNumber": "Disc number",