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:
parent
201a3c32f8
commit
f573221643
14 changed files with 362 additions and 224 deletions
|
@ -1,20 +0,0 @@
|
|||
<div id="scheduledTasksPage" data-role="page" class="page type-interior scheduledTasksConfigurationPage" data-title="${TabScheduledTasks}">
|
||||
<style>
|
||||
.taskProgressOuter {
|
||||
height: 6px;
|
||||
background: #eee;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.taskProgressInner {
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
background: #00a4dc;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="divScheduledTasks readOnlyContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,197 +0,0 @@
|
|||
import { formatDistance, formatDistanceToNow } from 'date-fns';
|
||||
import 'jquery';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import dom from 'scripts/dom';
|
||||
import serverNotifications from 'scripts/serverNotifications';
|
||||
import { getLocale, getLocaleWithSuffix } from 'utils/dateFnsLocale.ts';
|
||||
import Events from 'utils/events.ts';
|
||||
|
||||
import 'components/listview/listview.scss';
|
||||
import 'elements/emby-button/emby-button';
|
||||
|
||||
function reloadList(page) {
|
||||
ApiClient.getScheduledTasks({
|
||||
isHidden: false
|
||||
}).then(function(tasks) {
|
||||
populateList(page, tasks);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function populateList(page, tasks) {
|
||||
tasks = tasks.sort(function(a, b) {
|
||||
a = a.Category + ' ' + a.Name;
|
||||
b = b.Category + ' ' + b.Name;
|
||||
if (a > b) {
|
||||
return 1;
|
||||
} else if (a < b) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
let currentCategory;
|
||||
let html = '';
|
||||
for (const task of tasks) {
|
||||
if (task.Category != currentCategory) {
|
||||
currentCategory = task.Category;
|
||||
if (currentCategory) {
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
html += '<div class="verticalSection verticalSection-extrabottompadding">';
|
||||
html += '<div class="sectionTitleContainer" style="margin-bottom:1em;">';
|
||||
html += '<h2 class="sectionTitle">';
|
||||
html += currentCategory;
|
||||
html += '</h2>';
|
||||
html += '</div>';
|
||||
html += '<div class="paperList">';
|
||||
}
|
||||
html += '<div class="listItem listItem-border scheduledTaskPaperIconItem" data-status="' + task.State + '">';
|
||||
html += "<a is='emby-linkbutton' style='margin:0;padding:0;' class='clearLink listItemIconContainer' href='/dashboard/tasks/edit?id=" + task.Id + "'>";
|
||||
html += '<span class="material-icons listItemIcon schedule" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
html += '<div class="listItemBody two-line">';
|
||||
const textAlignStyle = globalize.getIsRTL() ? 'right' : 'left';
|
||||
html += "<a class='clearLink' style='margin:0;padding:0;display:block;text-align:" + textAlignStyle + ";' is='emby-linkbutton' href='/dashboard/tasks/edit?id=" + task.Id + "'>";
|
||||
html += "<h3 class='listItemBodyText'>" + task.Name + '</h3>';
|
||||
html += "<div class='secondary listItemBodyText' id='taskProgress" + task.Id + "'>" + getTaskProgressHtml(task) + '</div>';
|
||||
html += '</a>';
|
||||
html += '</div>';
|
||||
if (task.State === 'Running') {
|
||||
html += '<button type="button" is="paper-icon-button-light" id="btnTask' + task.Id + '" class="btnStopTask" data-taskid="' + task.Id + '" title="' + globalize.translate('ButtonStop') + '"><span class="material-icons stop" aria-hidden="true"></span></button>';
|
||||
} else if (task.State === 'Idle') {
|
||||
html += '<button type="button" is="paper-icon-button-light" id="btnTask' + task.Id + '" class="btnStartTask" data-taskid="' + task.Id + '" title="' + globalize.translate('ButtonStart') + '"><span class="material-icons play_arrow" aria-hidden="true"></span></button>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
if (tasks.length) {
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
page.querySelector('.divScheduledTasks').innerHTML = html;
|
||||
}
|
||||
|
||||
function getTaskProgressHtml(task) {
|
||||
let html = '';
|
||||
if (task.State === 'Idle') {
|
||||
if (task.LastExecutionResult) {
|
||||
const endtime = Date.parse(task.LastExecutionResult.EndTimeUtc);
|
||||
const starttime = Date.parse(task.LastExecutionResult.StartTimeUtc);
|
||||
html += globalize.translate('LabelScheduledTaskLastRan', formatDistanceToNow(endtime, getLocaleWithSuffix()),
|
||||
formatDistance(starttime, endtime, { locale: getLocale() }));
|
||||
if (task.LastExecutionResult.Status === 'Failed') {
|
||||
html += " <span style='color:#FF0000;'>(" + globalize.translate('LabelFailed') + ')</span>';
|
||||
} else if (task.LastExecutionResult.Status === 'Cancelled') {
|
||||
html += " <span style='color:#0026FF;'>(" + globalize.translate('LabelCancelled') + ')</span>';
|
||||
} else if (task.LastExecutionResult.Status === 'Aborted') {
|
||||
html += " <span style='color:#FF0000;'>" + globalize.translate('LabelAbortedByServerShutdown') + '</span>';
|
||||
}
|
||||
}
|
||||
} else if (task.State === 'Running') {
|
||||
const progress = (task.CurrentProgressPercentage || 0).toFixed(1);
|
||||
html += '<div style="display:flex;align-items:center;">';
|
||||
html += '<div class="taskProgressOuter" title="' + progress + '%" style="flex-grow:1;">';
|
||||
html += '<div class="taskProgressInner" style="width:' + progress + '%;">';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += "<span style='color:#00a4dc;margin-left:5px;'>" + progress + '%</span>';
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += "<span style='color:#FF0000;'>" + globalize.translate('LabelStopping') + '</span>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function setTaskButtonIcon(button, icon) {
|
||||
const inner = button.querySelector('.material-icons');
|
||||
inner.classList.remove('stop', 'play_arrow');
|
||||
inner.classList.add(icon);
|
||||
}
|
||||
|
||||
function updateTaskButton(elem, state) {
|
||||
if (state === 'Running') {
|
||||
elem.classList.remove('btnStartTask');
|
||||
elem.classList.add('btnStopTask');
|
||||
setTaskButtonIcon(elem, 'stop');
|
||||
elem.title = globalize.translate('ButtonStop');
|
||||
} else if (state === 'Idle') {
|
||||
elem.classList.add('btnStartTask');
|
||||
elem.classList.remove('btnStopTask');
|
||||
setTaskButtonIcon(elem, 'play_arrow');
|
||||
elem.title = globalize.translate('ButtonStart');
|
||||
}
|
||||
dom.parentWithClass(elem, 'listItem').setAttribute('data-status', state);
|
||||
}
|
||||
|
||||
export default function(view) {
|
||||
function updateTasks(tasks) {
|
||||
for (const task of tasks) {
|
||||
const taskProgress = view.querySelector(`#taskProgress${task.Id}`);
|
||||
if (taskProgress) taskProgress.innerHTML = getTaskProgressHtml(task);
|
||||
|
||||
const taskButton = view.querySelector(`#btnTask${task.Id}`);
|
||||
if (taskButton) updateTaskButton(taskButton, task.State);
|
||||
}
|
||||
}
|
||||
|
||||
function onPollIntervalFired() {
|
||||
if (!ApiClient.isMessageChannelOpen()) {
|
||||
reloadList(view);
|
||||
}
|
||||
}
|
||||
|
||||
function onScheduledTasksUpdate(e, apiClient, info) {
|
||||
if (apiClient.serverId() === serverId) {
|
||||
updateTasks(info);
|
||||
}
|
||||
}
|
||||
|
||||
function startInterval() {
|
||||
ApiClient.sendMessage('ScheduledTasksInfoStart', '1000,1000');
|
||||
pollInterval && clearInterval(pollInterval);
|
||||
pollInterval = setInterval(onPollIntervalFired, 1e4);
|
||||
}
|
||||
|
||||
function stopInterval() {
|
||||
ApiClient.sendMessage('ScheduledTasksInfoStop');
|
||||
pollInterval && clearInterval(pollInterval);
|
||||
}
|
||||
|
||||
let pollInterval;
|
||||
const serverId = ApiClient.serverId();
|
||||
|
||||
$('.divScheduledTasks', view).on('click', '.btnStartTask', function() {
|
||||
const button = this;
|
||||
const id = button.getAttribute('data-taskid');
|
||||
ApiClient.startScheduledTask(id).then(function() {
|
||||
updateTaskButton(button, 'Running');
|
||||
reloadList(view);
|
||||
});
|
||||
});
|
||||
|
||||
$('.divScheduledTasks', view).on('click', '.btnStopTask', function() {
|
||||
const button = this;
|
||||
const id = button.getAttribute('data-taskid');
|
||||
ApiClient.stopScheduledTask(id).then(function() {
|
||||
updateTaskButton(button, '');
|
||||
reloadList(view);
|
||||
});
|
||||
});
|
||||
|
||||
view.addEventListener('viewbeforehide', function() {
|
||||
Events.off(serverNotifications, 'ScheduledTasksInfo', onScheduledTasksUpdate);
|
||||
stopInterval();
|
||||
});
|
||||
|
||||
view.addEventListener('viewshow', function() {
|
||||
loading.show();
|
||||
startInterval();
|
||||
reloadList(view);
|
||||
Events.on(serverNotifications, 'ScheduledTasksInfo', onScheduledTasksUpdate);
|
||||
});
|
||||
}
|
||||
|
|
@ -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 ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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 ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
35
src/apps/dashboard/features/scheduledtasks/api/useTasks.ts
Normal file
35
src/apps/dashboard/features/scheduledtasks/api/useTasks.ts
Normal 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
|
||||
});
|
||||
};
|
|
@ -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;
|
|
@ -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,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;
|
|
@ -0,0 +1,5 @@
|
|||
import type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info';
|
||||
|
||||
export type TaskProps = {
|
||||
task: TaskInfo;
|
||||
};
|
27
src/apps/dashboard/features/scheduledtasks/utils/tasks.ts
Normal file
27
src/apps/dashboard/features/scheduledtasks/utils/tasks.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -11,6 +11,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
|||
{ path: 'playback/streaming', type: AppType.Dashboard },
|
||||
{ path: 'playback/trickplay', type: AppType.Dashboard },
|
||||
{ path: 'plugins/:pluginId', page: 'plugins/plugin', type: AppType.Dashboard },
|
||||
{ path: 'tasks', type: AppType.Dashboard },
|
||||
{ path: 'users', type: AppType.Dashboard },
|
||||
{ path: 'users/access', type: AppType.Dashboard },
|
||||
{ path: 'users/add', type: AppType.Dashboard },
|
||||
|
|
|
@ -114,12 +114,5 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
|||
controller: 'scheduledtasks/scheduledtask',
|
||||
view: 'scheduledtasks/scheduledtask.html'
|
||||
}
|
||||
}, {
|
||||
path: 'tasks',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'scheduledtasks/scheduledtasks',
|
||||
view: 'scheduledtasks/scheduledtasks.html'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
75
src/apps/dashboard/routes/tasks/index.tsx
Normal file
75
src/apps/dashboard/routes/tasks/index.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import Page from 'components/Page';
|
||||
import globalize from 'lib/globalize';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { QUERY_KEY, useTasks } from '../../features/scheduledtasks/api/useTasks';
|
||||
import { getCategories, getTasksByCategory } from '../../features/scheduledtasks/utils/tasks';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import Tasks from '../../features/scheduledtasks/components/Tasks';
|
||||
import type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info';
|
||||
import { SessionMessageType } from '@jellyfin/sdk/lib/generated-client/models/session-message-type';
|
||||
import serverNotifications from 'scripts/serverNotifications';
|
||||
import Events, { Event } from 'utils/events';
|
||||
import { ApiClient } from 'jellyfin-apiclient';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
export const Component = () => {
|
||||
const { __legacyApiClient__ } = useApi();
|
||||
const { data: tasks, isPending } = useTasks({ isHidden: false });
|
||||
|
||||
// TODO: Replace usage of the legacy apiclient when websocket support is added to the TS SDK.
|
||||
useEffect(() => {
|
||||
const onScheduledTasksUpdate = (_e: Event, _apiClient: ApiClient, info: TaskInfo[]) => {
|
||||
queryClient.setQueryData([ QUERY_KEY ], info);
|
||||
};
|
||||
|
||||
const fallbackInterval = setInterval(() => {
|
||||
if (!__legacyApiClient__?.isMessageChannelOpen()) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY ]
|
||||
});
|
||||
}
|
||||
}, 1e4);
|
||||
|
||||
__legacyApiClient__?.sendMessage(SessionMessageType.ScheduledTasksInfoStart, '1000,1000');
|
||||
Events.on(serverNotifications, SessionMessageType.ScheduledTasksInfo, onScheduledTasksUpdate);
|
||||
|
||||
return () => {
|
||||
clearInterval(fallbackInterval);
|
||||
__legacyApiClient__?.sendMessage(SessionMessageType.ScheduledTasksInfoStop, null);
|
||||
Events.off(serverNotifications, SessionMessageType.ScheduledTasksInfo, onScheduledTasksUpdate);
|
||||
};
|
||||
}, [__legacyApiClient__]);
|
||||
|
||||
if (isPending || !tasks) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const categories = getCategories(tasks);
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='scheduledTasksPage'
|
||||
title={globalize.translate('TabScheduledTasks')}
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<Box className='content-primary'>
|
||||
<Box className='readOnlyContent'>
|
||||
<Stack spacing={3} mt={2}>
|
||||
{categories.map(category => {
|
||||
return <Tasks
|
||||
key={category}
|
||||
category={category}
|
||||
tasks={getTasksByCategory(tasks, category)}
|
||||
/>;
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
Component.displayName = 'TasksPage';
|
Loading…
Add table
Add a link
Reference in a new issue