From f5732216434bbc2c53c355173b918ef7ec77c610 Mon Sep 17 00:00:00 2001
From: viown <48097677+viown@users.noreply.github.com>
Date: Fri, 21 Feb 2025 00:18:42 +0300
Subject: [PATCH] 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
---
.../scheduledtasks/scheduledtasks.html | 20 --
.../scheduledtasks/scheduledtasks.js | 197 ------------------
.../scheduledtasks/api/useStartTask.ts | 23 ++
.../scheduledtasks/api/useStopTask.ts | 23 ++
.../features/scheduledtasks/api/useTasks.ts | 35 ++++
.../scheduledtasks/components/Task.tsx | 67 ++++++
.../scheduledtasks/components/TaskLastRan.tsx | 45 ++++
.../components/TaskProgress.tsx | 32 +++
.../scheduledtasks/components/Tasks.tsx | 29 +++
.../scheduledtasks/types/taskProps.ts | 5 +
.../features/scheduledtasks/utils/tasks.ts | 27 +++
src/apps/dashboard/routes/_asyncRoutes.ts | 1 +
src/apps/dashboard/routes/_legacyRoutes.ts | 7 -
src/apps/dashboard/routes/tasks/index.tsx | 75 +++++++
14 files changed, 362 insertions(+), 224 deletions(-)
delete mode 100644 src/apps/dashboard/controllers/scheduledtasks/scheduledtasks.html
delete mode 100644 src/apps/dashboard/controllers/scheduledtasks/scheduledtasks.js
create mode 100644 src/apps/dashboard/features/scheduledtasks/api/useStartTask.ts
create mode 100644 src/apps/dashboard/features/scheduledtasks/api/useStopTask.ts
create mode 100644 src/apps/dashboard/features/scheduledtasks/api/useTasks.ts
create mode 100644 src/apps/dashboard/features/scheduledtasks/components/Task.tsx
create mode 100644 src/apps/dashboard/features/scheduledtasks/components/TaskLastRan.tsx
create mode 100644 src/apps/dashboard/features/scheduledtasks/components/TaskProgress.tsx
create mode 100644 src/apps/dashboard/features/scheduledtasks/components/Tasks.tsx
create mode 100644 src/apps/dashboard/features/scheduledtasks/types/taskProps.ts
create mode 100644 src/apps/dashboard/features/scheduledtasks/utils/tasks.ts
create mode 100644 src/apps/dashboard/routes/tasks/index.tsx
diff --git a/src/apps/dashboard/controllers/scheduledtasks/scheduledtasks.html b/src/apps/dashboard/controllers/scheduledtasks/scheduledtasks.html
deleted file mode 100644
index 299bed531f..0000000000
--- a/src/apps/dashboard/controllers/scheduledtasks/scheduledtasks.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
diff --git a/src/apps/dashboard/controllers/scheduledtasks/scheduledtasks.js b/src/apps/dashboard/controllers/scheduledtasks/scheduledtasks.js
deleted file mode 100644
index 114c97f40f..0000000000
--- a/src/apps/dashboard/controllers/scheduledtasks/scheduledtasks.js
+++ /dev/null
@@ -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 += '';
- html += '';
- }
- html += '';
- }
- 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 += " (" + globalize.translate('LabelFailed') + ')';
- } else if (task.LastExecutionResult.Status === 'Cancelled') {
- html += " (" + globalize.translate('LabelCancelled') + ')';
- } else if (task.LastExecutionResult.Status === 'Aborted') {
- html += " " + globalize.translate('LabelAbortedByServerShutdown') + '';
- }
- }
- } else if (task.State === 'Running') {
- const progress = (task.CurrentProgressPercentage || 0).toFixed(1);
- html += '';
- html += '
';
- html += '
';
- html += '
';
- html += '
';
- html += "
" + progress + '%';
- html += '
';
- } else {
- html += "" + globalize.translate('LabelStopping') + '';
- }
- 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);
- });
-}
-
diff --git a/src/apps/dashboard/features/scheduledtasks/api/useStartTask.ts b/src/apps/dashboard/features/scheduledtasks/api/useStartTask.ts
new file mode 100644
index 0000000000..6775ae3cf9
--- /dev/null
+++ b/src/apps/dashboard/features/scheduledtasks/api/useStartTask.ts
@@ -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 ]
+ });
+ }
+ });
+};
diff --git a/src/apps/dashboard/features/scheduledtasks/api/useStopTask.ts b/src/apps/dashboard/features/scheduledtasks/api/useStopTask.ts
new file mode 100644
index 0000000000..9edc866245
--- /dev/null
+++ b/src/apps/dashboard/features/scheduledtasks/api/useStopTask.ts
@@ -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 ]
+ });
+ }
+ });
+};
diff --git a/src/apps/dashboard/features/scheduledtasks/api/useTasks.ts b/src/apps/dashboard/features/scheduledtasks/api/useTasks.ts
new file mode 100644
index 0000000000..9ac9851e9c
--- /dev/null
+++ b/src/apps/dashboard/features/scheduledtasks/api/useTasks.ts
@@ -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
+ });
+};
diff --git a/src/apps/dashboard/features/scheduledtasks/components/Task.tsx b/src/apps/dashboard/features/scheduledtasks/components/Task.tsx
new file mode 100644
index 0000000000..0c5fda280b
--- /dev/null
+++ b/src/apps/dashboard/features/scheduledtasks/components/Task.tsx
@@ -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 = ({ 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 (
+
+ {task.State == 'Running' ? : }
+
+ }
+ >
+
+
+
+
+
+
+ {task.Name}}
+ secondary={task.State == 'Running' ? : }
+ disableTypography
+ />
+
+
+ );
+};
+
+export default Task;
diff --git a/src/apps/dashboard/features/scheduledtasks/components/TaskLastRan.tsx b/src/apps/dashboard/features/scheduledtasks/components/TaskLastRan.tsx
new file mode 100644
index 0000000000..7261cf685d
--- /dev/null
+++ b/src/apps/dashboard/features/scheduledtasks/components/TaskLastRan.tsx
@@ -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 = ({ 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 (
+
+ {globalize.translate('LabelScheduledTaskLastRan', lastRan, timeTaken)}
+
+ {lastResultStatus == 'Failed' && {` (${globalize.translate('LabelFailed')})`}}
+ {lastResultStatus == 'Cancelled' && {` (${globalize.translate('LabelCancelled')})`}}
+ {lastResultStatus == 'Aborted' && {` (${globalize.translate('LabelAbortedByServerShutdown')})`}}
+
+ );
+ }
+ } else {
+ return (
+ {globalize.translate('LabelStopping')}
+ );
+ }
+};
+
+export default TaskLastRan;
diff --git a/src/apps/dashboard/features/scheduledtasks/components/TaskProgress.tsx b/src/apps/dashboard/features/scheduledtasks/components/TaskProgress.tsx
new file mode 100644
index 0000000000..6482dd57fe
--- /dev/null
+++ b/src/apps/dashboard/features/scheduledtasks/components/TaskProgress.tsx
@@ -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 = ({ task }: TaskProps) => {
+ const progress = task.CurrentProgressPercentage;
+
+ return (
+
+ {progress != null ? (
+ <>
+
+
+
+
+ {`${Math.round(progress)}%`}
+
+ >
+ ) : (
+
+
+
+ )}
+
+ );
+};
+
+export default TaskProgress;
diff --git a/src/apps/dashboard/features/scheduledtasks/components/Tasks.tsx b/src/apps/dashboard/features/scheduledtasks/components/Tasks.tsx
new file mode 100644
index 0000000000..6635ce0b3a
--- /dev/null
+++ b/src/apps/dashboard/features/scheduledtasks/components/Tasks.tsx
@@ -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 = ({ category, tasks }: TasksProps) => {
+ return (
+
+ {category}
+
+ {tasks.map(task => {
+ return ;
+ })}
+
+
+ );
+};
+
+export default Tasks;
diff --git a/src/apps/dashboard/features/scheduledtasks/types/taskProps.ts b/src/apps/dashboard/features/scheduledtasks/types/taskProps.ts
new file mode 100644
index 0000000000..31683422ea
--- /dev/null
+++ b/src/apps/dashboard/features/scheduledtasks/types/taskProps.ts
@@ -0,0 +1,5 @@
+import type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info';
+
+export type TaskProps = {
+ task: TaskInfo;
+};
diff --git a/src/apps/dashboard/features/scheduledtasks/utils/tasks.ts b/src/apps/dashboard/features/scheduledtasks/utils/tasks.ts
new file mode 100644
index 0000000000..b0bc1aa5b1
--- /dev/null
+++ b/src/apps/dashboard/features/scheduledtasks/utils/tasks.ts
@@ -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;
+ }
+ });
+}
diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts
index cce1471000..19cf7ea1a5 100644
--- a/src/apps/dashboard/routes/_asyncRoutes.ts
+++ b/src/apps/dashboard/routes/_asyncRoutes.ts
@@ -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 },
diff --git a/src/apps/dashboard/routes/_legacyRoutes.ts b/src/apps/dashboard/routes/_legacyRoutes.ts
index 19a3bedc79..848c242b67 100644
--- a/src/apps/dashboard/routes/_legacyRoutes.ts
+++ b/src/apps/dashboard/routes/_legacyRoutes.ts
@@ -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'
- }
}
];
diff --git a/src/apps/dashboard/routes/tasks/index.tsx b/src/apps/dashboard/routes/tasks/index.tsx
new file mode 100644
index 0000000000..f742ef8ede
--- /dev/null
+++ b/src/apps/dashboard/routes/tasks/index.tsx
@@ -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 ;
+ }
+
+ const categories = getCategories(tasks);
+
+ return (
+
+
+
+
+ {categories.map(category => {
+ return ;
+ })}
+
+
+
+
+ );
+};
+
+Component.displayName = 'TasksPage';