mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Migrate tasks edit page to react
This commit is contained in:
parent
e80b890bd2
commit
524d1b6574
20 changed files with 501 additions and 330 deletions
169
src/apps/dashboard/features/tasks/components/NewTriggerForm.tsx
Normal file
169
src/apps/dashboard/features/tasks/components/NewTriggerForm.tsx
Normal file
|
@ -0,0 +1,169 @@
|
|||
import React, { FunctionComponent, useCallback, useMemo, useState } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import type { TaskTriggerInfo } from '@jellyfin/sdk/lib/generated-client/models/task-trigger-info';
|
||||
import { TaskTriggerInfoType } from '@jellyfin/sdk/lib/generated-client/models/task-trigger-info-type';
|
||||
import { DayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/day-of-week';
|
||||
import globalize from 'lib/globalize';
|
||||
import { getTimeOfDayOptions } from '../utils/edit';
|
||||
import { useLocale } from 'hooks/useLocale';
|
||||
|
||||
type IProps = {
|
||||
open: boolean,
|
||||
title: string,
|
||||
onClose?: () => void,
|
||||
onSubmit?: (trigger: TaskTriggerInfo) => void
|
||||
};
|
||||
|
||||
const NewTriggerForm: FunctionComponent<IProps> = ({ open, title, onClose, onSubmit }: IProps) => {
|
||||
const { dateFnsLocale } = useLocale();
|
||||
const [triggerType, setTriggerType] = useState('DailyTrigger');
|
||||
|
||||
const timeOfDayOptions = useMemo(() => getTimeOfDayOptions(dateFnsLocale), [dateFnsLocale]);
|
||||
|
||||
const onTriggerTypeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTriggerType(e.target.value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
component: 'form',
|
||||
onSubmit: (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
const trigger: TaskTriggerInfo = {
|
||||
Type: data.TriggerType.toString() as TaskTriggerInfoType
|
||||
};
|
||||
|
||||
if (trigger.Type == TaskTriggerInfoType.WeeklyTrigger) {
|
||||
trigger.DayOfWeek = data.DayOfWeek.toString() as DayOfWeek;
|
||||
}
|
||||
|
||||
if (trigger.Type == TaskTriggerInfoType.DailyTrigger || trigger.Type == TaskTriggerInfoType.WeeklyTrigger) {
|
||||
trigger.TimeOfDayTicks = parseInt(data.TimeOfDay.toString(), 10);
|
||||
}
|
||||
|
||||
if (trigger.Type == TaskTriggerInfoType.IntervalTrigger) {
|
||||
trigger.IntervalTicks = parseInt(data.Interval.toString(), 10);
|
||||
}
|
||||
|
||||
if (data.TimeLimit.toString()) {
|
||||
trigger.MaxRuntimeTicks = parseFloat(data.TimeLimit.toString()) * 3600000 * 1e4;
|
||||
}
|
||||
|
||||
if (onSubmit) {
|
||||
onSubmit(trigger);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
name='TriggerType'
|
||||
select
|
||||
fullWidth
|
||||
value={triggerType}
|
||||
onChange={onTriggerTypeChange}
|
||||
label={globalize.translate('LabelTriggerType')}
|
||||
>
|
||||
<MenuItem value='DailyTrigger'>{globalize.translate('OptionDaily')}</MenuItem>
|
||||
<MenuItem value='WeeklyTrigger'>{globalize.translate('OptionWeekly')}</MenuItem>
|
||||
<MenuItem value='IntervalTrigger'>{globalize.translate('OptionOnInterval')}</MenuItem>
|
||||
<MenuItem value='StartupTrigger'>{globalize.translate('OnApplicationStartup')}</MenuItem>
|
||||
</TextField>
|
||||
|
||||
{triggerType == 'WeeklyTrigger' && (
|
||||
<TextField
|
||||
name='DayOfWeek'
|
||||
select
|
||||
fullWidth
|
||||
defaultValue={'Sunday'}
|
||||
label={globalize.translate('LabelDay')}
|
||||
>
|
||||
<MenuItem value='Sunday'>{globalize.translate('Sunday')}</MenuItem>
|
||||
<MenuItem value='Monday'>{globalize.translate('Monday')}</MenuItem>
|
||||
<MenuItem value='Tuesday'>{globalize.translate('Tuesday')}</MenuItem>
|
||||
<MenuItem value='Wednesday'>{globalize.translate('Wednesday')}</MenuItem>
|
||||
<MenuItem value='Thursday'>{globalize.translate('Thursday')}</MenuItem>
|
||||
<MenuItem value='Friday'>{globalize.translate('Friday')}</MenuItem>
|
||||
<MenuItem value='Saturday'>{globalize.translate('Saturday')}</MenuItem>
|
||||
</TextField>
|
||||
)}
|
||||
|
||||
{['DailyTrigger', 'WeeklyTrigger'].includes(triggerType) && (
|
||||
<TextField
|
||||
name='TimeOfDay'
|
||||
select
|
||||
fullWidth
|
||||
defaultValue={'0'}
|
||||
label={globalize.translate('LabelTime')}
|
||||
>
|
||||
{timeOfDayOptions.map((option) => {
|
||||
return <MenuItem key={option.value} value={option.value}>{option.name}</MenuItem>;
|
||||
})}
|
||||
</TextField>
|
||||
)}
|
||||
|
||||
{triggerType == 'IntervalTrigger' && (
|
||||
<TextField
|
||||
name='Interval'
|
||||
select
|
||||
fullWidth
|
||||
defaultValue={'9000000000'}
|
||||
label={globalize.translate('LabelEveryXMinutes')}
|
||||
>
|
||||
<MenuItem value='9000000000'>15 minutes</MenuItem>
|
||||
<MenuItem value='18000000000'>30 minutes</MenuItem>
|
||||
<MenuItem value='27000000000'>45 minutes</MenuItem>
|
||||
<MenuItem value='36000000000'>1 hour</MenuItem>
|
||||
<MenuItem value='72000000000'>2 hours</MenuItem>
|
||||
<MenuItem value='108000000000'>3 hours</MenuItem>
|
||||
<MenuItem value='144000000000'>4 hours</MenuItem>
|
||||
<MenuItem value='216000000000'>6 hours</MenuItem>
|
||||
<MenuItem value='288000000000'>8 hours</MenuItem>
|
||||
<MenuItem value='432000000000'>12 hours</MenuItem>
|
||||
<MenuItem value='864000000000'>24 hours</MenuItem>
|
||||
</TextField>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
name='TimeLimit'
|
||||
fullWidth
|
||||
defaultValue={''}
|
||||
type='number'
|
||||
inputProps={{
|
||||
min: 1,
|
||||
step: 0.5
|
||||
}}
|
||||
label={globalize.translate('LabelTimeLimitHours')}
|
||||
/>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
color='error'
|
||||
>{globalize.translate('ButtonCancel')}</Button>
|
||||
<Button type='submit'>{globalize.translate('Add')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewTriggerForm;
|
67
src/apps/dashboard/features/tasks/components/Task.tsx
Normal file
67
src/apps/dashboard/features/tasks/components/Task.tsx
Normal 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;
|
45
src/apps/dashboard/features/tasks/components/TaskLastRan.tsx
Normal file
45
src/apps/dashboard/features/tasks/components/TaskLastRan.tsx
Normal 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;
|
|
@ -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;
|
|
@ -0,0 +1,34 @@
|
|||
import React, { FC } from 'react';
|
||||
import type { MRT_Cell, MRT_RowData } from 'material-react-table';
|
||||
import { useLocale } from 'hooks/useLocale';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { getTriggerFriendlyName } from '../utils/edit';
|
||||
import type { TaskTriggerInfo } from '@jellyfin/sdk/lib/generated-client/models/task-trigger-info';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
interface CellProps {
|
||||
cell: MRT_Cell<MRT_RowData>
|
||||
}
|
||||
|
||||
const TaskTriggerCell: FC<CellProps> = ({ cell }) => {
|
||||
const { dateFnsLocale } = useLocale();
|
||||
const trigger = cell.getValue<TaskTriggerInfo>();
|
||||
|
||||
const timeLimitHours = trigger.MaxRuntimeTicks ? trigger.MaxRuntimeTicks / 36e9 : false;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant='body1'>{getTriggerFriendlyName(trigger, dateFnsLocale)}</Typography>
|
||||
{timeLimitHours && (
|
||||
<Typography variant='body2' color={'text.secondary'}>
|
||||
{timeLimitHours == 1 ?
|
||||
globalize.translate('ValueTimeLimitSingleHour') :
|
||||
globalize.translate('ValueTimeLimitMultiHour', timeLimitHours)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskTriggerCell;
|
29
src/apps/dashboard/features/tasks/components/Tasks.tsx
Normal file
29
src/apps/dashboard/features/tasks/components/Tasks.tsx
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue