Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite devices dashboard page in react #6489

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
63 changes: 63 additions & 0 deletions src/apps/dashboard/components/TablePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Box from '@mui/material/Box/Box';
import Typography from '@mui/material/Typography/Typography';
import { type MRT_RowData, type MRT_TableInstance, MaterialReactTable } from 'material-react-table';
import React from 'react';

import Page, { type PageProps } from 'components/Page';

interface TablePageProps<T extends MRT_RowData> extends PageProps {
title: string
table: MRT_TableInstance<T>
}

export const DEFAULT_TABLE_OPTIONS = {
// Enable custom features
enableColumnPinning: true,
enableColumnResizing: true,

// Sticky header/footer
enableStickyFooter: true,
enableStickyHeader: true,
muiTableContainerProps: {
sx: {
maxHeight: 'calc(100% - 7rem)' // 2 x 3.5rem for header and footer
}
}
};

const TablePage = <T extends MRT_RowData>({
title,
table,
children,
...pageProps
}: TablePageProps<T>) => {
return (
<Page
title={title}
{...pageProps}
>
<Box
className='content-primary'
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%'
}}
>
<Box
sx={{
marginBottom: 1
}}
>
<Typography variant='h2'>
{title}
</Typography>
</Box>
<MaterialReactTable table={table} />
</Box>
{children}
</Page>
);
};

export default TablePage;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
import type { SxProps, Theme } from '@mui/material';
import IconButton from '@mui/material/IconButton/IconButton';
import React, { type FC } from 'react';
import { Link } from 'react-router-dom';
Expand All @@ -7,14 +8,21 @@ import UserAvatar from 'components/UserAvatar';

interface UserAvatarButtonProps {
user?: UserDto
sx?: SxProps<Theme>
}

const UserAvatarButton: FC<UserAvatarButtonProps> = ({ user }) => (
const UserAvatarButton: FC<UserAvatarButtonProps> = ({
user,
sx
}) => (
user?.Id ? (
<IconButton
size='large'
color='inherit'
sx={{ padding: 0 }}
sx={{
padding: 0,
...sx
}}
title={user.Name || undefined}
component={Link}
to={`/dashboard/users/profile?userId=${user.Id}`}
Expand Down
24 changes: 24 additions & 0 deletions src/apps/dashboard/features/devices/api/useDeleteDevice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { DevicesApiDeleteDeviceRequest } from '@jellyfin/sdk/lib/generated-client/api/devices-api';
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
import { useMutation } from '@tanstack/react-query';

import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QUERY_KEY } from './useDevices';

export const useDeleteDevice = () => {
const { api } = useApi();

return useMutation({
mutationFn: (params: DevicesApiDeleteDeviceRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getDevicesApi(api!)
.deleteDevice(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});
}
});
};
38 changes: 38 additions & 0 deletions src/apps/dashboard/features/devices/api/useDevices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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';

export const QUERY_KEY = 'Devices';

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: [QUERY_KEY, requestParams],
queryFn: ({ signal }) =>
fetchDevices(api, requestParams, { signal }),
enabled: !!api
});
};
24 changes: 24 additions & 0 deletions src/apps/dashboard/features/devices/api/useUpdateDevice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { DevicesApiUpdateDeviceOptionsRequest } from '@jellyfin/sdk/lib/generated-client/api/devices-api';
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
import { useMutation } from '@tanstack/react-query';

import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QUERY_KEY } from './useDevices';

export const useUpdateDevice = () => {
const { api } = useApi();

return useMutation({
mutationFn: (params: DevicesApiUpdateDeviceOptionsRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getDevicesApi(api!)
.updateDeviceOptions(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});
}
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { FC } from 'react';

import { DeviceInfoCell } from 'apps/dashboard/features/devices/types/deviceInfoCell';
import { getDeviceIcon } from 'utils/image';

const DeviceNameCell: FC<DeviceInfoCell> = ({ 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}
</>
);

export default DeviceNameCell;
7 changes: 7 additions & 0 deletions src/apps/dashboard/features/devices/types/deviceInfoCell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { DeviceInfoDto } from '@jellyfin/sdk/lib/generated-client/models/device-info-dto';
import type { MRT_Row } from 'material-react-table';

export interface DeviceInfoCell {
renderedCellValue: React.ReactNode
row: MRT_Row<DeviceInfoDto>
}
1 change: 1 addition & 0 deletions src/apps/dashboard/routes/_asyncRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'devices', type: AppType.Dashboard },
{ path: 'keys', type: AppType.Dashboard },
{ path: 'logs', type: AppType.Dashboard },
{ path: 'playback/trickplay', type: AppType.Dashboard },
Expand Down
14 changes: 0 additions & 14 deletions src/apps/dashboard/routes/_legacyRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
controller: 'dashboard/networking',
view: 'dashboard/networking.html'
}
}, {
path: 'devices',
pageProps: {
appType: AppType.Dashboard,
controller: 'dashboard/devices/devices',
view: 'dashboard/devices/devices.html'
}
}, {
path: 'devices/edit',
pageProps: {
appType: AppType.Dashboard,
controller: 'dashboard/devices/device',
view: 'dashboard/devices/device.html'
}
}, {
path: 'libraries',
pageProps: {
Expand Down
77 changes: 10 additions & 67 deletions src/apps/dashboard/routes/activity/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
import Box from '@mui/material/Box';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Typography from '@mui/material/Typography';
import { type MRT_ColumnDef, MaterialReactTable, useMaterialReactTable } from 'material-react-table';
import { type MRT_ColumnDef, useMaterialReactTable } from 'material-react-table';
import { useSearchParams } from 'react-router-dom';

import TablePage, { DEFAULT_TABLE_OPTIONS } from 'apps/dashboard/components/TablePage';
import { useLogEntries } from 'apps/dashboard/features/activity/api/useLogEntries';
import ActionsCell from 'apps/dashboard/features/activity/components/ActionsCell';
import LogLevelCell from 'apps/dashboard/features/activity/components/LogLevelCell';
import OverviewCell from 'apps/dashboard/features/activity/components/OverviewCell';
import UserAvatarButton from 'apps/dashboard/features/activity/components/UserAvatarButton';
import UserAvatarButton from 'apps/dashboard/components/UserAvatarButton';
import type { ActivityLogEntryCell } from 'apps/dashboard/features/activity/types/ActivityLogEntryCell';
import Page from 'components/Page';
import { useUsers } from 'hooks/useUsers';
import { type UsersRecords, useUsersDetails } from 'hooks/useUsers';
import { parseISO8601Date, toLocaleString } from 'scripts/datetime';
import globalize from 'lib/globalize';
import { toBoolean } from 'utils/string';

type UsersRecords = Record<string, UserDto>;

const DEFAULT_PAGE_SIZE = 25;
const VIEW_PARAM = 'useractivity';

Expand Down Expand Up @@ -55,29 +50,7 @@ const Activity = () => {
pageSize: DEFAULT_PAGE_SIZE
});

const { data: usersData, isLoading: isUsersLoading } = useUsers();

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 userNames = useMemo(() => {
const names: string[] = [];
usersData?.forEach(user => {
if (user.Name) names.push(user.Name);
});
return names;
}, [ usersData ]);
const { usersById: users, names: userNames, isLoading: isUsersLoading } = useUsersDetails();

const UserCell = getUserCell(users);

Expand Down Expand Up @@ -177,22 +150,11 @@ const Activity = () => {
}, [ activityView, searchParams, setSearchParams ]);

const table = useMaterialReactTable({
...DEFAULT_TABLE_OPTIONS,

columns,
data: logEntries?.Items || [],

// Enable custom features
enableColumnPinning: true,
enableColumnResizing: true,

// Sticky header/footer
enableStickyFooter: true,
enableStickyHeader: true,
muiTableContainerProps: {
sx: {
maxHeight: 'calc(100% - 7rem)' // 2 x 3.5rem for header and footer
}
},

Comment on lines -183 to -195
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do this refactor for the api keys page as well? 😅

I'm also alright with doing it in another PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It didn't exist when I started this and I didn't want to make the PR bigger. 😅

// State
initialState: {
density: 'compact'
Expand Down Expand Up @@ -229,31 +191,12 @@ const Activity = () => {
});

return (
<Page
<TablePage
id='serverActivityPage'
title={globalize.translate('HeaderActivity')}
className='mainAnimatedPage type-interior'
>
<Box
className='content-primary'
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%'
}}
>
<Box
sx={{
marginBottom: 1
}}
>
<Typography variant='h2'>
{globalize.translate('HeaderActivity')}
</Typography>
</Box>
<MaterialReactTable table={table} />
</Box>
</Page>
table={table}
/>
);
};

Expand Down
Loading
Loading