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

Migrate scheduled tasks to React (#6506)

* Migrate scheduled tasks to React

* Adjust margins

* Use localeCompare

* Clean up imports

* Use legacy apiclient from useApi

* Fix import

* Fix nested typography

* Add polling fallback

* Cleanup code

* Rename to tasks

* Rename to Component

* Use constants for websocket events

* Use memo to fix timestamp rerender on run
This commit is contained in:
viown 2025-02-21 00:18:42 +03:00 committed by GitHub
parent 201a3c32f8
commit f573221643
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 362 additions and 224 deletions

View file

@ -0,0 +1,23 @@
import { ScheduledTasksApiStartTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api';
import { getScheduledTasksApi } from '@jellyfin/sdk/lib/utils/api/scheduled-tasks-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QUERY_KEY } from './useTasks';
export const useStartTask = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: ScheduledTasksApiStartTaskRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getScheduledTasksApi(api!)
.startTask(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});
}
});
};

View file

@ -0,0 +1,23 @@
import { ScheduledTasksApiStartTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api';
import { getScheduledTasksApi } from '@jellyfin/sdk/lib/utils/api/scheduled-tasks-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QUERY_KEY } from './useTasks';
export const useStopTask = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: ScheduledTasksApiStartTaskRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getScheduledTasksApi(api!)
.stopTask(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});
}
});
};

View file

@ -0,0 +1,35 @@
import type { ScheduledTasksApiGetTasksRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api';
import type { AxiosRequestConfig } from 'axios';
import type { Api } from '@jellyfin/sdk';
import { getScheduledTasksApi } from '@jellyfin/sdk/lib/utils/api/scheduled-tasks-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
export const QUERY_KEY = 'Tasks';
const fetchTasks = async (
api?: Api,
params?: ScheduledTasksApiGetTasksRequest,
options?: AxiosRequestConfig
) => {
if (!api) {
console.warn('[fetchTasks] No API instance available');
return;
}
const response = await getScheduledTasksApi(api).getTasks(params, options);
return response.data;
};
export const useTasks = (params?: ScheduledTasksApiGetTasksRequest) => {
const { api } = useApi();
return useQuery({
queryKey: [QUERY_KEY],
queryFn: ({ signal }) =>
fetchTasks(api, params, { signal }),
enabled: !!api
});
};

View file

@ -0,0 +1,67 @@
import React, { FunctionComponent, useCallback } from 'react';
import ListItem from '@mui/material/ListItem';
import Avatar from '@mui/material/Avatar';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography';
import Dashboard from 'utils/dashboard';
import { TaskProps } from '../types/taskProps';
import TaskProgress from './TaskProgress';
import TaskLastRan from './TaskLastRan';
import IconButton from '@mui/material/IconButton';
import PlayArrow from '@mui/icons-material/PlayArrow';
import Stop from '@mui/icons-material/Stop';
import { useStartTask } from '../api/useStartTask';
import { useStopTask } from '../api/useStopTask';
const Task: FunctionComponent<TaskProps> = ({ task }: TaskProps) => {
const startTask = useStartTask();
const stopTask = useStopTask();
const navigateTaskEdit = useCallback(() => {
Dashboard.navigate(`/dashboard/tasks/edit?id=${task.Id}`)
.catch(err => {
console.error('[Task] failed to navigate to task edit page', err);
});
}, [task]);
const handleStartTask = useCallback(() => {
if (task.Id) {
startTask.mutate({ taskId: task.Id });
}
}, [task, startTask]);
const handleStopTask = useCallback(() => {
if (task.Id) {
stopTask.mutate({ taskId: task.Id });
}
}, [task, stopTask]);
return (
<ListItem
disablePadding
secondaryAction={
<IconButton onClick={task.State == 'Running' ? handleStopTask : handleStartTask}>
{task.State == 'Running' ? <Stop /> : <PlayArrow />}
</IconButton>
}
>
<ListItemButton onClick={navigateTaskEdit}>
<ListItemAvatar>
<Avatar sx={{ bgcolor: 'primary.main' }}>
<AccessTimeIcon sx={{ color: '#fff' }} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={<Typography variant='h3'>{task.Name}</Typography>}
secondary={task.State == 'Running' ? <TaskProgress task={task} /> : <TaskLastRan task={task} />}
disableTypography
/>
</ListItemButton>
</ListItem>
);
};
export default Task;

View file

@ -0,0 +1,45 @@
import React, { FunctionComponent, useMemo } from 'react';
import { TaskProps } from '../types/taskProps';
import { useLocale } from 'hooks/useLocale';
import { formatDistance, formatDistanceToNow, parseISO } from 'date-fns';
import Typography from '@mui/material/Typography';
import globalize from 'lib/globalize';
const TaskLastRan: FunctionComponent<TaskProps> = ({ task }: TaskProps) => {
const { dateFnsLocale } = useLocale();
const [ lastRan, timeTaken ] = useMemo(() => {
if (task.LastExecutionResult?.StartTimeUtc && task.LastExecutionResult?.EndTimeUtc) {
const endTime = parseISO(task.LastExecutionResult.EndTimeUtc);
const startTime = parseISO(task.LastExecutionResult.StartTimeUtc);
return [
formatDistanceToNow(endTime, { locale: dateFnsLocale, addSuffix: true }),
formatDistance(startTime, endTime, { locale: dateFnsLocale })
];
}
return [];
}, [task, dateFnsLocale]);
if (task.State == 'Idle') {
if (task.LastExecutionResult?.StartTimeUtc && task.LastExecutionResult?.EndTimeUtc) {
const lastResultStatus = task.LastExecutionResult.Status;
return (
<Typography sx={{ lineHeight: '1.2rem', color: 'text.secondary' }} variant='body1'>
{globalize.translate('LabelScheduledTaskLastRan', lastRan, timeTaken)}
{lastResultStatus == 'Failed' && <Typography display='inline' color='error'>{` (${globalize.translate('LabelFailed')})`}</Typography>}
{lastResultStatus == 'Cancelled' && <Typography display='inline' color='blue'>{` (${globalize.translate('LabelCancelled')})`}</Typography>}
{lastResultStatus == 'Aborted' && <Typography display='inline' color='error'>{` (${globalize.translate('LabelAbortedByServerShutdown')})`}</Typography>}
</Typography>
);
}
} else {
return (
<Typography sx={{ color: 'text.secondary' }}>{globalize.translate('LabelStopping')}</Typography>
);
}
};
export default TaskLastRan;

View file

@ -0,0 +1,32 @@
import React, { FunctionComponent } from 'react';
import { TaskProps } from '../types/taskProps';
import Box from '@mui/material/Box';
import LinearProgress from '@mui/material/LinearProgress';
import Typography from '@mui/material/Typography';
const TaskProgress: FunctionComponent<TaskProps> = ({ task }: TaskProps) => {
const progress = task.CurrentProgressPercentage;
return (
<Box sx={{ display: 'flex', alignItems: 'center', height: '1.2rem', mr: 2 }}>
{progress != null ? (
<>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant='determinate' value={progress} />
</Box>
<Box>
<Typography
variant='body1'
>{`${Math.round(progress)}%`}</Typography>
</Box>
</>
) : (
<Box sx={{ width: '100%' }}>
<LinearProgress />
</Box>
)}
</Box>
);
};
export default TaskProgress;

View file

@ -0,0 +1,29 @@
import React, { FunctionComponent } from 'react';
import type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info';
import List from '@mui/material/List';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import Task from './Task';
type TasksProps = {
category: string;
tasks: TaskInfo[];
};
const Tasks: FunctionComponent<TasksProps> = ({ category, tasks }: TasksProps) => {
return (
<Stack spacing={2}>
<Typography variant='h2'>{category}</Typography>
<List sx={{ bgcolor: 'background.paper' }}>
{tasks.map(task => {
return <Task
key={task.Id}
task={task}
/>;
})}
</List>
</Stack>
);
};
export default Tasks;

View file

@ -0,0 +1,5 @@
import type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info';
export type TaskProps = {
task: TaskInfo;
};

View file

@ -0,0 +1,27 @@
import type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info';
export function getCategories(tasks: TaskInfo[] | undefined) {
if (!tasks) return [];
const categories: string[] = [];
for (const task of tasks) {
if (task.Category && !categories.includes(task.Category)) {
categories.push(task.Category);
}
}
return categories.sort((a, b) => a.localeCompare(b));
}
export function getTasksByCategory(tasks: TaskInfo[] | undefined, category: string) {
if (!tasks) return [];
return tasks.filter(task => task.Category == category).sort((a, b) => {
if (a.Name && b.Name) {
return a.Name?.localeCompare(b.Name);
} else {
return 0;
}
});
}