Răsfoiți Sursa

[ui-taskserver] refactors task server component (#4058)

---------
Co-authored-by: Harsh Gupta <42064744+Harshg999@users.noreply.github.com>
Ram Prasad Agarwal 7 luni în urmă
părinte
comite
8116961d55
21 a modificat fișierele cu 1099 adăugiri și 642 ștergeri
  1. 5 5
      desktop/core/src/desktop/api2.py
  2. 1 1
      desktop/core/src/desktop/js/apps/admin/Configuration/Configuration.scss
  3. 8 0
      desktop/core/src/desktop/js/jest/jest.init.js
  4. 15 13
      desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.tsx
  5. 0 108
      desktop/core/src/desktop/js/reactComponents/TaskBrowser/TaskBrowser.scss
  6. 0 487
      desktop/core/src/desktop/js/reactComponents/TaskBrowser/TaskBrowser.tsx
  7. 114 0
      desktop/core/src/desktop/js/reactComponents/TaskServer/ScheduleTaskModal/ScheduleTaskModal.test.tsx
  8. 136 0
      desktop/core/src/desktop/js/reactComponents/TaskServer/ScheduleTaskModal/ScheduleTaskModal.tsx
  9. 26 0
      desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.scss
  10. 53 0
      desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.test.tsx
  11. 66 0
      desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.tsx
  12. 67 0
      desktop/core/src/desktop/js/reactComponents/TaskServer/TaskServer.scss
  13. 163 0
      desktop/core/src/desktop/js/reactComponents/TaskServer/TaskServer.test.tsx
  14. 262 0
      desktop/core/src/desktop/js/reactComponents/TaskServer/TaskServer.tsx
  15. 36 0
      desktop/core/src/desktop/js/reactComponents/TaskServer/constants.ts
  16. 43 0
      desktop/core/src/desktop/js/reactComponents/TaskServer/types.ts
  17. 93 0
      desktop/core/src/desktop/js/reactComponents/TaskServer/utils.ts
  18. 2 2
      desktop/core/src/desktop/js/reactComponents/imports.js
  19. 4 4
      desktop/core/src/desktop/js/utils/hooks/useFileUpload/useChunkUpload.ts
  20. 2 2
      desktop/core/src/desktop/js/utils/hooks/useFileUpload/utils.ts
  21. 3 20
      desktop/core/src/desktop/templates/taskserver_list_tasks.mako

+ 5 - 5
desktop/core/src/desktop/api2.py

@@ -743,8 +743,8 @@ def handle_task_submit(request):
   except json.JSONDecodeError as e:
     return JsonResponse({'error': str(e)}, status=500)
 
-  if task_name == 'document cleanup':
-    keep_days = task_params.get('keep-days')
+  if task_name == 'document_cleanup':
+    keep_days = task_params.get('keep_days')
     task_kwargs = {
       'keep_days': keep_days,
       'user_id': request.user.id,
@@ -761,9 +761,9 @@ def handle_task_submit(request):
       'task_id': task.id,  # The task ID generated by Celery
     })
 
-  elif task_name == 'tmp clean up':
-    cleanup_threshold = task_params.get('threshold for clean up')
-    disk_check_interval = task_params.get('disk check interval')
+  elif task_name == 'tmp_clean_up':
+    cleanup_threshold = task_params.get('cleanup_threshold')
+    disk_check_interval = task_params.get('disk_check_interval')
     task_kwargs = {
       'username': request.user.username,
       'cleanup_threshold': cleanup_threshold,

+ 1 - 1
desktop/core/src/desktop/js/apps/admin/Configuration/Configuration.scss

@@ -26,7 +26,7 @@
     }
 
     .config__section-header {
-      margin-top: 16px;      
+      margin-top: 16px;
     }
 
     .config__main-item {

+ 8 - 0
desktop/core/src/desktop/js/jest/jest.init.js

@@ -142,3 +142,11 @@ Object.defineProperty(window, 'matchMedia', {
     dispatchEvent: jest.fn()
   }))
 });
+
+class ResizeObserver {
+  observe() {}
+  unobserve() {}
+  disconnect() {}
+}
+
+global.ResizeObserver = ResizeObserver;

+ 15 - 13
desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.tsx

@@ -14,9 +14,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import React from 'react';
 import { Spin } from 'antd';
 import { PrimaryButton } from 'cuix/dist/components/Button';
-import React from 'react';
+
+import { i18nReact } from '../../utils/i18nReact';
 import './LoadingErrorWrapper.scss';
 
 interface WrapperError {
@@ -39,6 +41,8 @@ const LoadingErrorWrapper = ({
   children,
   hideChildren = false
 }: LoadingErrorWrapperProps): JSX.Element => {
+  const { t } = i18nReact.useTranslation();
+
   if (loading) {
     return (
       <Spin
@@ -55,18 +59,16 @@ const LoadingErrorWrapper = ({
   if (enabledErrors.length > 0) {
     return (
       <>
-        {enabledErrors
-          .filter(error => error.enabled)
-          .map(error => (
-            <div className="loading-error-wrapper__error" key={error.message}>
-              <div>{error.message}</div>
-              {error.onClick && (
-                <PrimaryButton onClick={error.onClick} data-event="">
-                  {error.action}
-                </PrimaryButton>
-              )}
-            </div>
-          ))}
+        {enabledErrors.map(error => (
+          <div className="loading-error-wrapper__error" key={error.message}>
+            <div>{t(error.message)}</div>
+            {error.onClick && (
+              <PrimaryButton onClick={error.onClick} data-event="">
+                {error.action}
+              </PrimaryButton>
+            )}
+          </div>
+        ))}
       </>
     );
   }

+ 0 - 108
desktop/core/src/desktop/js/reactComponents/TaskBrowser/TaskBrowser.scss

@@ -1,108 +0,0 @@
-@use 'variables' as vars;
-@import '@cloudera/cuix-core/variables.scss';
-
-.log-content-scrollable {
-  max-height: 600px;
-  overflow-y: auto;
-  margin-right: -16px;
-  padding-right: 16px;
-}
-
-.customCursorClass {
-  cursor: pointer;
-}
-
-.flex-container-style {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.task-selection {
-  margin-bottom: 2rem;
-}
-
-.task-dropdown {
-  width: 100%;
-  padding: 0.5rem;
-  font-size: 1rem;
-}
-
-.parameter-inputs {
-  display: flex;
-  flex-direction: column;
-  gap: 1rem;
-  margin-top: 2rem;
-}
-
-.parameter-row {
-  display: flex;
-  align-items: center;
-}
-
-.parameter-label {
-  flex: 1;
-  width: 100px;
-  font-weight: bold;
-  margin-right: 1rem;
-}
-
-.parameter-input {
-  padding: 0.5rem;
-  flex: 1;
-  font-size: 1rem;
-  width: 100%;
-}
-
-.vertical-spacer {
-  height: 2rem;
-}
-
-.row-success {
-  border-left: 4px solid $fluidx-green-500;
-}
-
-.row-failure {
-  border-left: 4px solid $fluidx-red-500;
-}
-
-.row-running {
-  border-left: 4px solid $fluidx-amber-500;
-}
-
-.ant-btn-primary {
-  background-color: $fluidx-blue-500;
-  border-color: $fluidx-blue-600;
-}
-
-.ant-btn-danger {
-  background-color: $fluidx-red-500;
-  border-color: $fluidx-red-600;
-}
-
-.ant-tag-success {
-  background-color: $fluidx-green-500;
-  color: $fluidx-gray-900;
-}
-
-.ant-tag-error {
-  background-color: $fluidx-red-500;
-  color: $fluidx-gray-900;
-}
-
-.ant-tag-warning {
-  background-color: $fluidx-amber-500;
-  color: $fluidx-gray-900;
-}
-
-.success-text {
-  color: $fluidx-green-500;
-}
-
-.running-text {
-  color: $fluidx-amber-500;
-}
-
-.failed-text {
-  color: $fluidx-red-500;
-}

+ 0 - 487
desktop/core/src/desktop/js/reactComponents/TaskBrowser/TaskBrowser.tsx

@@ -1,487 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { i18nReact } from '../../../js/utils/i18nReact';
-import axios from 'axios';
-import { post, get, extractErrorMessage } from '../../../js/api/utils.ts';
-import { calculateDuration, formatTimestamp } from '../../../js/utils/dateTimeUtils.ts';
-import './TaskBrowser.scss';
-import huePubSub from '../../../js/utils/huePubSub';
-import Modal from 'antd/lib/modal/Modal';
-import { Button, Tag, Input, Checkbox, Form, Select } from 'antd';
-import 'antd/dist/antd.css';
-import PrimaryButton from 'cuix/dist/components/Button/PrimaryButton';
-import DangerButton from 'cuix/dist/components/Button/DangerButton';
-import Table from 'cuix/dist/components/Table';
-
-const { Option } = Select;
-
-const tasks = {
-  'document cleanup': {
-    params: [{ name: 'keep-days', type: 'integer' }]
-  },
-  'tmp clean up': {
-    params: [{ name: 'threshold for clean up', type: 'integer', max: 100 }]
-  }
-};
-
-const ShowScheduleTaskPopup = ({ onClose, onSubmit, open }) => {
-  const [selectedTask, setSelectedTask] = useState('');
-  const [params, setParams] = useState({
-    'keep-days': '30', // Default value for "keep days"
-    'threshold for clean up': '90' // Default value for "threshold for clean up"
-  });
-
-  const handleChange = e => {
-    const { name, value } = e.target;
-    setParams(prevParams => ({
-      ...prevParams,
-      [name]: value
-    }));
-  };
-
-  const handleSubmit = () => {
-    onSubmit(selectedTask, params);
-    onClose();
-  };
-
-  return (
-    <Modal
-      title={'Schedule Task'}
-      open={open}
-      onOk={handleSubmit}
-      onCancel={onClose}
-      okText={'Submit'}
-      width={530}
-      className="hue-file-chooser__modal"
-    >
-      <div className="task-selection">
-        <Select
-          value={selectedTask}
-          onChange={setSelectedTask}
-          placeholder="Select Task"
-          style={{ width: '100%' }}
-        >
-          {Object.keys(tasks).map(taskName => (
-            <Option key={taskName} value={taskName}>
-              {taskName}
-            </Option>
-          ))}
-        </Select>
-      </div>
-      <div className="vertical-spacer"></div>
-      {selectedTask && (
-        <div className="parameter-inputs">
-          {tasks[selectedTask].params.map(param => (
-            <div key={param.name} className="parameter-row">
-              <Form.Item label={param.name}>
-                <Input
-                  name={param.name}
-                  type="text"
-                  placeholder={param.name}
-                  onChange={handleChange}
-                  value={params[param.name] || ''}
-                />
-              </Form.Item>
-            </div>
-          ))}
-        </div>
-      )}
-    </Modal>
-  );
-};
-
-const TaskBrowser: React.FC = (): React.ReactElement => {
-  const [showSchedulePopup, showScheduleTaskPopup] = useState(false);
-  const [scheduledTasks] = useState([]); // Store an array of scheduled tasks
-  const [showLogPopup, setShowLogPopup] = useState(false); // This should match where you use it
-  const [, setCurrentTaskId] = useState(null);
-  const [taskLogs, setTaskLogs] = useState('');
-
-  const handleScheduleSubmit = (taskName, taskParams) => {
-    const payload = {
-      taskName,
-      taskParams
-    };
-
-    axios
-      .post('/desktop/api2/taskserver/handle_submit', payload, {
-        transformResponse: [
-          function (data) {
-            try {
-              return JSON.parse(data);
-            } catch (e) {
-              return data;
-            }
-          }
-        ]
-      })
-      .then(() => {
-        huePubSub.publish('hue.global.info', { message: `Task submitted successfully` });
-      })
-      .catch(error => {
-        const errorMessage = extractErrorMessage(error);
-        huePubSub.publish('hue.global.error', {
-          message: `Failed to submit scheduling task ${errorMessage}`
-        });
-      });
-  };
-
-  // This function will open the schedule task popup
-  const handleSchedulePopup = () => {
-    showScheduleTaskPopup(true);
-  };
-
-  const ShowTaskLogsPopup = ({ logs, onClose, open }) => {
-    return (
-      <Modal
-        title="Task Logs"
-        visible={open}
-        onOk={onClose}
-        onCancel={onClose}
-        width={830}
-        footer={[
-          <Button key="back" onClick={onClose}>
-            Close
-          </Button>
-        ]}
-      >
-        <div className="log-content-scrollable">
-          {' '}
-          <pre>{logs}</pre>
-        </div>
-      </Modal>
-    );
-  };
-
-  const ShowTaskLogsHandler = taskId => {
-    setCurrentTaskId(taskId);
-    get(`/desktop/api2/taskserver/get_task_logs/${taskId}/`)
-      .then(response => {
-        setTaskLogs(response);
-        setShowLogPopup(true);
-      })
-      .catch(error => {
-        huePubSub.publish('hue.global.error', { message: `Error fetching task logs: ${error}` });
-      });
-  };
-
-  return (
-    <div>
-      {showSchedulePopup && (
-        <ShowScheduleTaskPopup
-          onClose={() => showScheduleTaskPopup(false)}
-          onSubmit={handleScheduleSubmit}
-          open={showSchedulePopup}
-        />
-      )}
-      <div style={{ margin: '20px 0' }}>
-        <TaskBrowserTable
-          tasks={scheduledTasks}
-          ShowTaskLogsHandler={ShowTaskLogsHandler}
-          handleSchedulePopup={handleSchedulePopup}
-        />
-      </div>
-      {showLogPopup && (
-        <ShowTaskLogsPopup
-          logs={taskLogs}
-          open={showLogPopup}
-          onClose={() => setShowLogPopup(false)}
-        />
-      )}
-    </div>
-  );
-};
-
-interface TaskBrowserTableProps {
-  ShowTaskLogsHandler: (taskId: string) => void;
-  handleSchedulePopup: () => void;
-}
-
-export enum TaskStatus {
-  Success = 'SUCCESS',
-  Failure = 'FAILURE',
-  Running = 'RUNNING'
-}
-
-export interface TaskServerResponse {
-  result?: {
-    task_name: string;
-    username: string;
-  };
-  task_id: string;
-  status: TaskStatus;
-}
-
-export const TaskBrowserTable: React.FC<TaskBrowserTableProps> = ({
-  ShowTaskLogsHandler,
-  handleSchedulePopup
-}) => {
-  const [tasks, setTasks] = useState<TaskServerResponse[]>([]);
-  const [searchTerm, setSearchTerm] = useState(''); // state for the search term
-  const [statusFilter, setStatusFilter] = useState({
-    success: false,
-    failure: false,
-    running: false,
-    all: true
-  });
-  const [selectedTasks, setSelectedTasks] = useState([]);
-
-  // rowSelection object needed for the Table component to handle row selection
-  const rowSelection = {
-    selectedRowKeys: selectedTasks,
-    onChange: selectedRowKeys => {
-      setSelectedTasks(selectedRowKeys);
-    }
-  };
-
-  const { t } = i18nReact.useTranslation();
-
-  const columns = [
-    {
-      title: t('Task ID'),
-      dataIndex: 'task_id',
-      key: 'task_id',
-      render: (text, record) => <a onClick={() => ShowTaskLogsHandler(record.task_id)}>{text}</a>
-    },
-    {
-      title: t('User'),
-      dataIndex: ['result', 'username'],
-      key: 'user'
-    },
-    {
-      title: t('Progress'),
-      dataIndex: ['result', 'progress'],
-      key: 'progress'
-    },
-    {
-      title: t('Task Name'),
-      dataIndex: ['result', 'task_name'],
-      key: 'task_name'
-    },
-    {
-      title: t('Parameters'),
-      dataIndex: 'parameters',
-      key: 'parameters',
-      render: (text, record) => (
-        <div>
-          {record.result?.task_name === 'fileupload' && (
-            <span>{`{file name: ${record.result?.qqfilename}}`}</span>
-          )}
-          {record.result?.task_name === 'document_cleanup' && (
-            <span>{`{keep days: ${record.result?.parameters}}`}</span>
-          )}
-          {record.result?.task_name === 'tmp_cleanup' && (
-            <span>{`{cleanup threshold: ${record.result?.parameters}}`}</span>
-          )}
-          {record.result?.task_name === 'cleanup_stale_uploads' && (
-            <span>{`{cleanup timedelta: ${record.result?.parameters}}`}</span>
-          )}
-        </div>
-      )
-    },
-    {
-      title: t('Status'),
-      dataIndex: 'status',
-      key: 'status',
-      render: status => <Tag color={statusTagColor(status)}>{status.toUpperCase()}</Tag>
-    },
-    {
-      title: t('Started'),
-      dataIndex: ['result', 'task_start'],
-      key: 'started',
-      render: text => formatTimestamp(text)
-    },
-    {
-      title: t('Duration'),
-      key: 'duration',
-      render: (_, record) => calculateDuration(record.result?.task_start, record.result?.task_end)
-    }
-  ];
-
-  // Custom function to determine Tag color based on status
-  const statusTagColor = status => {
-    switch (status.toUpperCase()) {
-      case 'SUCCESS':
-        return 'success';
-      case 'FAILURE':
-        return 'error'; // Ant Design does not have 'failure', using 'error' instead
-      case 'RUNNING':
-        return 'warning';
-      default:
-        return '';
-    }
-  };
-
-  // Function to calculate row class names based on status
-  const getRowClassName = record => {
-    switch (record.status.toUpperCase()) {
-      case 'SUCCESS':
-        return 'row-success';
-      case 'FAILURE':
-        return 'row-failure';
-      case 'RUNNING':
-        return 'row-running';
-      default:
-        return '';
-    }
-  };
-
-  useEffect(() => {
-    const fetchTasks = () => {
-      get('/desktop/api2/taskserver/get_taskserver_tasks/', null, {
-        transformResponse: data => {
-          return data;
-        }
-      })
-        .then(tasks => {
-          if (Array.isArray(tasks)) {
-            const sortedTasks = tasks.sort((a, b) => {
-              const dateA = new Date(a.date_done);
-              const dateB = new Date(b.date_done);
-              if (!isNaN(dateA) && !isNaN(dateB)) {
-                return dateB - dateA;
-              }
-              return 0;
-            });
-            setTasks(sortedTasks);
-          } else {
-            huePubSub.publish('hue.global.error', {
-              message: `Expected an array of tasks, but received: ${tasks}`
-            });
-          }
-        })
-        .catch(error => {
-          const errorMessage = extractErrorMessage(error);
-          huePubSub.publish('hue.global.error', {
-            message: `Error fetching tasks from redis ${errorMessage}`
-          });
-        })
-        .finally(() => {
-          setTimeout(fetchTasks, 5000);
-        });
-    };
-
-    fetchTasks(); //fetch tasks initially
-  }, []);
-
-  const handleSearchChange = e => {
-    setSearchTerm(e.target.value.toLowerCase());
-  };
-
-  const handleStatusFilterChange = status => {
-    setStatusFilter(prevStatusFilter => {
-      const isAll = status === 'all';
-      const newStatusFilter = {
-        ...prevStatusFilter,
-        [status]: isAll ? true : !prevStatusFilter[status]
-      };
-
-      if (isAll) {
-        // If 'all' is selected, set everything else to false
-        newStatusFilter.success = false;
-        newStatusFilter.failure = false;
-        newStatusFilter.running = false;
-      } else {
-        // If any specific status is toggled, set 'all' to false
-        newStatusFilter.all = false;
-      }
-
-      // If no individual statuses are selected, default to 'all'
-      if (!newStatusFilter.success && !newStatusFilter.failure && !newStatusFilter.running) {
-        newStatusFilter.all = true;
-      }
-
-      return newStatusFilter;
-    });
-  };
-
-  const filteredTasks = tasks.filter(task => {
-    const taskNameMatch = task.result?.task_name?.toLowerCase().includes(searchTerm);
-    const userIdMatch = task.result?.username?.toLowerCase().includes(searchTerm);
-    const taskIdMatch = task.task_id?.toLowerCase().includes(searchTerm);
-    const statusMatch =
-      statusFilter.all ||
-      (statusFilter.success && task.status === TaskStatus.Success) ||
-      (statusFilter.failure && task.status === TaskStatus.Failure) ||
-      (statusFilter.running && task.status === TaskStatus.Running);
-
-    return (taskNameMatch || userIdMatch || taskIdMatch) && statusMatch;
-  });
-
-  const handleKillTask = taskId => {
-    post(`/desktop/api2/taskserver/kill_task/${taskId}/`)
-      .then(response => {
-        // Assuming the server response is in the expected format
-        const { status, message } = response;
-        if (status === 'success') {
-          huePubSub.publish('hue.global.error', { message: `Task: ${taskId} has been killed.` });
-        } else if (status === 'info') {
-          huePubSub.publish('', {
-            message: `Task: ${taskId} has already been completed or revoked.`
-          });
-        } else {
-          huePubSub.publish('hue.global.error', {
-            message: `Task: ${taskId} could not be killed. ${message}`
-          });
-        }
-      })
-      .catch(error => {
-        const errorMessage = extractErrorMessage(error);
-        huePubSub.publish('hue.global.error', {
-          message: `Task not killed - Error: ${errorMessage}`
-        });
-      });
-  };
-
-  const handleKillSelectedTasks = () => {
-    selectedTasks.forEach(taskId => {
-      handleKillTask(taskId);
-    });
-    setSelectedTasks([]);
-  };
-
-  // Render the table with the tasks data
-  return (
-    <div className="content-panel-inner">
-      <div className="flex-container-style">
-        <PrimaryButton onClick={handleSchedulePopup}>Schedule Task</PrimaryButton>
-        <DangerButton onClick={handleKillSelectedTasks}>Kill Task</DangerButton>
-        <Input
-          type="text"
-          placeholder="Search by task name, user ID, or task ID..."
-          onChange={handleSearchChange}
-          className="button-input-style"
-          style={{ flexGrow: 1 }}
-        />
-        <Checkbox
-          checked={statusFilter.success}
-          onChange={() => handleStatusFilterChange('success')}
-        />
-        <span className="success-text">Succeeded</span>
-        <Checkbox
-          checked={statusFilter.running}
-          onChange={() => handleStatusFilterChange('running')}
-        />
-        <span className="running-text">Running</span>
-        <Checkbox
-          checked={statusFilter.failure}
-          onChange={() => handleStatusFilterChange('failure')}
-        />
-        <span className="failed-text">Failed</span>
-        <Checkbox checked={statusFilter.all} onChange={() => handleStatusFilterChange('all')} />
-        All
-      </div>
-      <Table
-        rowSelection={rowSelection}
-        columns={columns}
-        // eslint-disable-next-line @typescript-eslint/no-unused-vars
-        dataSource={filteredTasks.map(({ children, ...task }) => task)}
-        rowKey="task_id"
-        rowClassName={getRowClassName}
-        pagination={false}
-        size="small"
-      />
-    </div>
-  );
-};
-
-export default TaskBrowser;

+ 114 - 0
desktop/core/src/desktop/js/reactComponents/TaskServer/ScheduleTaskModal/ScheduleTaskModal.test.tsx

@@ -0,0 +1,114 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import ScheduleTaskModal from './ScheduleTaskModal';
+import { SCHEDULE_NEW_TASK_URL, scheduleTasksCategory } from '../constants';
+
+const mockOnClose = jest.fn();
+const mockHandleSubmit = jest.fn();
+const mockSaveURL = jest.fn();
+jest.mock('../../../utils/hooks/useSaveData/useSaveData', () => ({
+  __esModule: true,
+  default: jest.fn(url => {
+    mockSaveURL(url);
+    return {
+      save: mockHandleSubmit,
+      loading: false,
+      error: null
+    };
+  })
+}));
+
+describe('ScheduleTaskModal', () => {
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should render the radio group', () => {
+    render(<ScheduleTaskModal onClose={mockOnClose} />);
+    const radioGroup = screen.getAllByRole('radio');
+    expect(radioGroup).toHaveLength(2);
+    expect(radioGroup[0]).toBeInTheDocument();
+    expect(radioGroup[0]).toHaveAttribute('value', scheduleTasksCategory[0].value);
+    expect(radioGroup[1]).toBeInTheDocument();
+    expect(radioGroup[1]).toHaveAttribute('value', scheduleTasksCategory[1].value);
+  });
+
+  it('should render one input when document_cleanup is selected', () => {
+    render(<ScheduleTaskModal onClose={mockOnClose} />);
+    const radioButton = screen.getByRole('radio', { name: 'Document Cleanup' });
+
+    fireEvent.click(radioButton);
+
+    const inputs = screen.getAllByRole('spinbutton');
+    expect(inputs).toHaveLength(1);
+    expect(inputs[0]).toBeInTheDocument();
+    expect(inputs[0]).toHaveAttribute('name', scheduleTasksCategory[0].children[0].value);
+  });
+
+  it('should render one input when temp_cleanup is selected', async () => {
+    render(<ScheduleTaskModal onClose={mockOnClose} />);
+    const radioButton = screen.getByRole('radio', { name: 'Tmp Cleanup' });
+
+    fireEvent.click(radioButton);
+
+    const inputs = screen.getAllByRole('spinbutton');
+    await waitFor(() => {
+      expect(inputs).toHaveLength(2);
+      expect(inputs[0]).toBeInTheDocument();
+      expect(inputs[0]).toHaveAttribute('name', scheduleTasksCategory[1].children[0].value);
+      expect(inputs[1]).toBeInTheDocument();
+      expect(inputs[1]).toHaveAttribute('name', scheduleTasksCategory[1].children[1].value);
+    });
+  });
+
+  it('should call onClose when the close button is clicked', async () => {
+    render(<ScheduleTaskModal onClose={mockOnClose} />);
+    const closeButton = screen.getByRole('button', { name: 'Close' });
+
+    fireEvent.click(closeButton);
+
+    await waitFor(() => expect(mockOnClose).toHaveBeenCalledTimes(1));
+  });
+
+  it('should trigger handleSubmit when the submit button is clicked', async () => {
+    render(<ScheduleTaskModal onClose={mockOnClose} />);
+    const radioButton = screen.getByRole('radio', { name: 'Document Cleanup' });
+
+    fireEvent.click(radioButton);
+
+    const input = screen.getByRole('spinbutton');
+    fireEvent.change(input, { target: { value: '10' } });
+
+    const submitButton = screen.getByRole('button', { name: /submit/i });
+
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(mockHandleSubmit).toHaveBeenCalledTimes(1);
+      expect(mockHandleSubmit).toHaveBeenCalledWith({
+        taskName: 'document_cleanup',
+        taskParams: {
+          keep_days: '10'
+        }
+      });
+      expect(mockSaveURL).toHaveBeenCalledWith(SCHEDULE_NEW_TASK_URL);
+    });
+  });
+});

+ 136 - 0
desktop/core/src/desktop/js/reactComponents/TaskServer/ScheduleTaskModal/ScheduleTaskModal.tsx

@@ -0,0 +1,136 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React, { useState } from 'react';
+import { Form, Input, Radio } from 'antd';
+import { AxiosError } from 'axios';
+import Modal from 'cuix/dist/components/Modal';
+
+import { TaskServerResult } from '../types';
+import useSaveData from '../../../utils/hooks/useSaveData/useSaveData';
+import { SCHEDULE_NEW_TASK_URL, scheduleTasksCategory } from '../constants';
+import { extractErrorMessage } from '../../../api/utils';
+import huePubSub from '../../../utils/huePubSub';
+import { i18nReact } from '../../../utils/i18nReact';
+import LoadingErrorWrapper from '../../LoadingErrorWrapper/LoadingErrorWrapper';
+
+interface ScheduleTaskModalProps {
+  onClose: () => void;
+  open?: boolean;
+}
+
+const ScheduleTaskModal = ({ onClose, open = true }: ScheduleTaskModalProps): JSX.Element => {
+  const { t } = i18nReact.useTranslation();
+
+  const [selectedTask, setSelectedTask] = useState<TaskServerResult['taskName']>('');
+  const [params, setParams] = useState({});
+
+  const handleChange = e => {
+    const { name, value } = e.target;
+    setParams(prevParams => ({
+      ...prevParams,
+      [name]: value
+    }));
+  };
+
+  const { save, loading, error } = useSaveData(SCHEDULE_NEW_TASK_URL, {
+    postOptions: {
+      qsEncodeData: false
+    },
+    onSuccess: () => {
+      huePubSub.publish('hue.global.info', { message: `Task submitted successfully` });
+      onClose();
+    },
+    onError: (error: Error) => {
+      const errorMessage = extractErrorMessage(error as AxiosError);
+      huePubSub.publish('hue.global.error', {
+        message: `Failed to submit scheduling task ${errorMessage}`
+      });
+    }
+  });
+
+  const handleSubmit = () => {
+    save({
+      taskName: selectedTask,
+      taskParams: params
+    });
+  };
+
+  const dropdownOptions = scheduleTasksCategory.map(task => ({
+    label: task.label,
+    value: task.value
+  }));
+
+  const errors = [
+    {
+      enabled: !!error,
+      message: error?.message ?? 'An unknown error occurred.'
+    }
+  ];
+
+  const selectedTaskChildren =
+    scheduleTasksCategory.find(task => task.value === selectedTask)?.children ?? [];
+
+  const isSubmitDisabled =
+    loading || !(selectedTask && selectedTaskChildren.every(task => params[task.value] > 0));
+
+  return (
+    <Modal
+      title={t('Schedule Task')}
+      open={open}
+      onOk={handleSubmit}
+      onCancel={onClose}
+      okText={t('Submit')}
+      width={530}
+      cancellable={false}
+      okButtonProps={{ disabled: isSubmitDisabled }}
+      cancelButtonProps={{ disabled: loading }}
+      className="hue-schedule-task__modal"
+      data-testid="hue-schedule-task__modal"
+    >
+      <LoadingErrorWrapper loading={loading} errors={errors}>
+        <div className="hue-schedule-task-selection">
+          <Form.Item label={'Task'}>
+            <Radio.Group
+              options={dropdownOptions}
+              value={selectedTask}
+              onChange={e => setSelectedTask(e.target.value)}
+            />
+          </Form.Item>
+        </div>
+        {selectedTask && (
+          <div className="hue-schedule-task__input-group">
+            {selectedTaskChildren.map(param => (
+              <div key={param.value}>
+                <Form.Item label={param.label}>
+                  <Input
+                    name={param.value}
+                    type={param.type}
+                    placeholder={param.label}
+                    onChange={handleChange}
+                    value={params[param.value]}
+                  />
+                </Form.Item>
+              </div>
+            ))}
+          </div>
+        )}
+      </LoadingErrorWrapper>
+    </Modal>
+  );
+};
+
+export default ScheduleTaskModal;

+ 26 - 0
desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.scss

@@ -0,0 +1,26 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+.hue-task-server-logs {
+  max-height: 60vh;
+  overflow: auto;
+  margin-right: -16px;
+  padding-right: 16px;
+
+  pre {
+    padding: 16px;
+  }
+}

+ 53 - 0
desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.test.tsx

@@ -0,0 +1,53 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import TaskLogsModal from './TaskLogsModal';
+import { TaskServerResponse } from '../types';
+
+jest.mock('../../../utils/hooks/useLoadData/useLoadData', () => ({
+  __esModule: true,
+  default: jest.fn(() => ({
+    data: 'Sample log data',
+    loading: false,
+    error: null
+  }))
+}));
+
+describe('TaskLogsModal', () => {
+  const mockOnClose = jest.fn();
+  const taskId: TaskServerResponse['taskId'] = '123';
+
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('renders the modal with task logs', () => {
+    render(<TaskLogsModal taskId={taskId} onClose={mockOnClose} />);
+
+    expect(screen.getByText('Task Logs')).toBeInTheDocument();
+    expect(screen.getByText('Sample log data')).toBeInTheDocument();
+  });
+
+  it('calls onClose when the close button is clicked', () => {
+    render(<TaskLogsModal taskId={taskId} onClose={mockOnClose} />);
+
+    fireEvent.click(screen.getByText('Close'));
+    expect(mockOnClose).toHaveBeenCalledTimes(1);
+  });
+});

+ 66 - 0
desktop/core/src/desktop/js/reactComponents/TaskServer/TaskLogsModal/TaskLogsModal.tsx

@@ -0,0 +1,66 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React from 'react';
+import Modal from 'cuix/dist/components/Modal';
+import { TaskServerResponse } from '../types';
+import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
+import { GET_TASK_LOG_URL } from '../constants';
+import { i18nReact } from '../../../utils/i18nReact';
+import LoadingErrorWrapper from '../../LoadingErrorWrapper/LoadingErrorWrapper';
+
+import './TaskLogsModal.scss';
+
+interface TaskLogsModalProps {
+  taskId: TaskServerResponse['taskId'];
+  onClose: () => void;
+}
+
+const TaskLogsModal: React.FC<TaskLogsModalProps> = ({ taskId, onClose }): JSX.Element => {
+  const { t } = i18nReact.useTranslation();
+
+  const { data, loading, error } = useLoadData<string>(GET_TASK_LOG_URL, {
+    params: { task_id: taskId },
+    skip: !taskId
+  });
+
+  const errors = [
+    {
+      enabled: !!error,
+      message: error?.message ?? 'An error occurred while fetching task logs.'
+    }
+  ];
+
+  return (
+    <Modal
+      title={t('Task Logs')}
+      open={!!taskId}
+      onOk={onClose}
+      onCancel={onClose}
+      width={830}
+      okText={t('Close')}
+      cancellable={false}
+    >
+      <LoadingErrorWrapper loading={loading} errors={errors}>
+        <div className="hue-task-server-logs">
+          <pre>{data}</pre>
+        </div>
+      </LoadingErrorWrapper>
+    </Modal>
+  );
+};
+
+export default TaskLogsModal;

+ 67 - 0
desktop/core/src/desktop/js/reactComponents/TaskServer/TaskServer.scss

@@ -0,0 +1,67 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@use 'variables' as vars;
+
+.cuix.antd {
+  .hue-task-server {
+    display: flex;
+    height: 100%;
+    flex: 1;
+    flex-direction: column;
+    gap: 1rem;
+    padding: 1rem;
+
+    &__table {
+      display: flex;
+      flex: 1;
+      height: 100%;
+
+      .ant-tag-success {
+        background-color: vars.$fluidx-green-500;
+        color: vars.$fluidx-gray-900;
+      }
+
+      .ant-tag-error {
+        background-color: vars.$fluidx-red-500;
+        color: vars.$fluidx-gray-900;
+      }
+
+      .ant-tag-warning {
+        background-color: vars.$fluidx-amber-500;
+        color: vars.$fluidx-gray-900;
+      }
+    }
+
+    &__actions {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+    }
+
+    &__success-text {
+      color: vars.$fluidx-green-500;
+    }
+
+    &__running-text {
+      color: vars.$fluidx-amber-500;
+    }
+
+    &__failed-text {
+      color: vars.$fluidx-red-500;
+    }
+  }
+}

+ 163 - 0
desktop/core/src/desktop/js/reactComponents/TaskServer/TaskServer.test.tsx

@@ -0,0 +1,163 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React from 'react';
+import '@testing-library/jest-dom';
+import { render, fireEvent, waitFor } from '@testing-library/react';
+import TaskServer from './TaskServer';
+import { TaskServerResponse, TaskStatus } from './types.ts';
+import { KILL_TASK_URL } from './constants.ts';
+
+const mockTasks: TaskServerResponse[] = [
+  {
+    taskId: '3cf1c861-faa2-484c-b15a-9a1533b6acf2',
+    dateDone: '2025-03-25T14:30:00Z',
+    status: TaskStatus.Success,
+    result: {
+      username: 'celery_scheduler',
+      taskName: 'tmp_cleanup',
+      parameters: 90,
+      progress: '100%',
+      taskStart: '2025-03-25T04:29:15.674345',
+      taskEnd: '2025-03-25T04:29:15.680075'
+    },
+    children: []
+  },
+  {
+    taskId: 'c098f3d1-c937-4e50-b686-52d677296b95',
+    dateDone: '2025-03-25T15:00:00Z',
+    status: TaskStatus.Running,
+    result: {
+      username: 'celery_scheduler',
+      taskName: 'cleanup_stale_uploads',
+      parameters: 60,
+      progress: '100%',
+      taskStart: '2025-03-24T07:50:10',
+      taskEnd: '2025-03-24T07:50:10.982950'
+    },
+    children: []
+  },
+  {
+    taskId: 'c098f3d1-c937-4e50-b686-52d677296b96',
+    dateDone: '2025-03-25T16:30:00Z',
+    status: TaskStatus.Failure,
+    result: {
+      taskName: 'file_upload',
+      username: 'alex_lee',
+      taskStart: '2025-03-25T16:00:00Z',
+      taskEnd: '',
+      progress: '0%',
+      qqfilename: 'file3.txt',
+      parameters: 0
+    },
+    children: []
+  }
+];
+
+const mockHandleSave = jest.fn();
+const mockSaveURL = jest.fn();
+jest.mock('../../utils/hooks/useSaveData/useSaveData', () => ({
+  __esModule: true,
+  default: jest.fn(url => {
+    mockSaveURL(url);
+    return {
+      save: mockHandleSave,
+      loading: false,
+      error: null
+    };
+  })
+}));
+
+jest.mock('../../utils/hooks/useLoadData/useLoadData', () => ({
+  __esModule: true,
+  default: jest.fn(() => ({
+    data: mockTasks,
+    loading: false,
+    error: null
+  }))
+}));
+
+describe('TaskServer Component', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should open schedule task modal on button click', async () => {
+    const { getByRole, getAllByText, getByTestId } = render(<TaskServer />);
+
+    fireEvent.click(getByRole('button', { name: 'Schedule Task' }));
+
+    await waitFor(() => {
+      expect(getAllByText('Schedule Task')).toHaveLength(2);
+      expect(getByTestId('hue-schedule-task__modal')).toBeInTheDocument();
+    });
+  });
+
+  it('should call save API when clicking "Kill Task" button', async () => {
+    const { getAllByRole, getByRole } = render(<TaskServer />);
+
+    fireEvent.click(getAllByRole('checkbox', { name: '' })[0]);
+    fireEvent.click(getByRole('button', { name: 'Kill Task' }));
+
+    const payload = new FormData();
+    payload.append('task_id', mockTasks[0].taskId);
+
+    await waitFor(() => {
+      expect(mockHandleSave).toHaveBeenCalledTimes(1);
+      expect(mockHandleSave).toHaveBeenCalledWith(payload, {
+        onError: expect.any(Function),
+        onSuccess: expect.any(Function)
+      });
+      expect(mockSaveURL).toHaveBeenCalledWith(KILL_TASK_URL);
+    });
+  });
+
+  it('should render task list with correct data', async () => {
+    const { getByText } = render(<TaskServer />);
+
+    await waitFor(() => {
+      expect(getByText(mockTasks[0].taskId)).toBeInTheDocument();
+      expect(getByText(mockTasks[1].taskId)).toBeInTheDocument();
+      expect(getByText(mockTasks[2].taskId)).toBeInTheDocument();
+    });
+  });
+
+  it('should filter tasks based on search input', async () => {
+    const { getByPlaceholderText, getByText, queryByText } = render(<TaskServer />);
+
+    fireEvent.change(getByPlaceholderText('Search by task name, user ID, or task ID...'), {
+      target: { value: mockTasks[0].taskId }
+    });
+
+    await waitFor(() => {
+      expect(getByText(mockTasks[0].taskId)).toBeInTheDocument();
+      expect(queryByText(mockTasks[1].taskId)).toBeNull();
+      expect(queryByText(mockTasks[2].taskId)).toBeNull();
+    });
+  });
+
+  it('should change status filter when status checkbox is clicked', async () => {
+    const { getByText, getByRole, queryByText } = render(<TaskServer />);
+
+    fireEvent.click(getByRole('checkbox', { name: 'Succeeded' }));
+
+    await waitFor(() => {
+      expect(getByText('3cf1c861-faa2-484c-b15a-9a1533b6acf2')).toBeInTheDocument();
+      expect(queryByText('c098f3d1-c937-4e50-b686-52d677296b95')).toBeNull();
+      expect(queryByText('c098f3d1-c937-4e50-b686-52d677296b96')).toBeNull();
+    });
+  });
+});

+ 262 - 0
desktop/core/src/desktop/js/reactComponents/TaskServer/TaskServer.tsx

@@ -0,0 +1,262 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React, { useState, useMemo } from 'react';
+import { i18nReact } from '../../utils/i18nReact.ts';
+import { AxiosError } from 'axios';
+import { extractErrorMessage } from '../../api/utils.ts';
+import { calculateDuration, formatTimestamp } from '../../utils/dateTimeUtils.ts';
+import huePubSub from '../../utils/huePubSub.ts';
+import { Input, Checkbox } from 'antd';
+import { Tag } from 'antd';
+import { PrimaryButton, DangerButton } from 'cuix/dist/components/Button';
+import PaginatedTable from '../PaginatedTable/PaginatedTable';
+import useLoadData from '../../utils/hooks/useLoadData/useLoadData.ts';
+import useSaveData from '../../utils/hooks/useSaveData/useSaveData.ts';
+import { KillTaskResponse, TaskServerResponse, TaskServerResult } from './types.ts';
+import { statusTagColor, getFilteredTasks, sortTasksByDate, getUpdatedFilters } from './utils.ts';
+import { GET_TASKS_URL, KILL_TASK_URL } from './constants.ts';
+import ScheduleTaskModal from './ScheduleTaskModal/ScheduleTaskModal.tsx';
+import TaskLogsModal from './TaskLogsModal/TaskLogsModal.tsx';
+import LoadingErrorWrapper from '../LoadingErrorWrapper/LoadingErrorWrapper.tsx';
+import useResizeObserver from '../../utils/hooks/useResizeObserver/useResizeObserver';
+
+import './TaskServer.scss';
+
+export const TaskServer: React.FC = () => {
+  const [selectedTaskId, setSelectedTaskId] = useState<TaskServerResponse['taskId'] | null>(null);
+  const [showSchedulePopup, showScheduleTaskPopup] = useState<boolean>(false);
+  const [searchTerm, setSearchTerm] = useState<string>('');
+  const [selectedTasks, setSelectedTasks] = useState<TaskServerResponse[]>([]);
+  const [statusFilter, setStatusFilter] = useState<Record<string, boolean>>({
+    success: false,
+    failure: false,
+    running: false,
+    all: true
+  });
+
+  const { t } = i18nReact.useTranslation();
+
+  const { save } = useSaveData(KILL_TASK_URL, {
+    postOptions: { qsEncodeData: false }
+  });
+
+  const columns = [
+    {
+      title: t('Task ID'),
+      dataIndex: 'taskId',
+      key: 'taskId',
+      width: '30%',
+      render: (text: TaskServerResponse['taskId'], record: TaskServerResponse) => (
+        <a onClick={() => setSelectedTaskId(record.taskId)}>{text}</a>
+      )
+    },
+    {
+      title: t('User'),
+      dataIndex: ['result', 'username'],
+      key: 'user',
+      width: '10%'
+    },
+    {
+      title: t('Progress'),
+      dataIndex: ['result', 'progress'],
+      key: 'progress',
+      width: '10%'
+    },
+    {
+      title: t('Task Name'),
+      dataIndex: ['result', 'taskName'],
+      key: 'taskName',
+      width: '10%'
+    },
+    {
+      title: t('Parameters'),
+      dataIndex: 'parameters',
+      key: 'parameters',
+      width: '10%',
+      render: (_, record: TaskServerResponse) => {
+        if (record.result?.taskName) {
+          const paramterText = {
+            fileupload: `{file name: ${record.result?.qqfilename}}`,
+            document_cleanup: `{keep days: ${record.result?.parameters}}`,
+            tmp_cleanup: `{cleanup threshold: ${record.result?.parameters}}`,
+            cleanup_stale_uploads: `{cleanup timedelta: ${record.result?.parameters}}`
+          }[record.result?.taskName];
+          if (paramterText) {
+            return <span>{paramterText}</span>;
+          }
+        }
+      }
+    },
+    {
+      title: t('Status'),
+      dataIndex: 'status',
+      key: 'status',
+      width: '10%',
+      render: (status: TaskServerResponse['status']) => (
+        <Tag color={statusTagColor(status)}>{status.toUpperCase()}</Tag>
+      )
+    },
+    {
+      title: t('Started'),
+      dataIndex: ['result', 'taskStart'],
+      key: 'started',
+      width: '10%',
+      render: (text: TaskServerResult['taskStart']) => formatTimestamp(text)
+    },
+    {
+      title: t('Duration'),
+      key: 'duration',
+      width: '10%',
+      render: (_, record: TaskServerResponse) => {
+        if (record.result?.taskStart && record.result?.taskEnd) {
+          return calculateDuration(record.result?.taskStart, record.result?.taskEnd);
+        }
+      }
+    }
+  ];
+
+  const {
+    data: tasks,
+    loading,
+    error
+  } = useLoadData<TaskServerResponse[]>(GET_TASKS_URL, {
+    pollInterval: 5000
+  });
+
+  const errors = [
+    {
+      enabled: !!error,
+      message: t(`Error fetching tasks: ${error}`)
+    },
+    {
+      enabled: !Array.isArray(tasks),
+      message: t(`Expected an array of tasks, but received: ${tasks}`)
+    }
+  ];
+
+  const sortedTasks = useMemo(() => sortTasksByDate(tasks), [tasks]);
+  const filteredTasks = useMemo(
+    () => getFilteredTasks(statusFilter, searchTerm, sortedTasks),
+    [statusFilter, searchTerm, sortedTasks]
+  );
+
+  const onSearchChange = e => {
+    setSearchTerm(e.target.value.toLowerCase());
+  };
+
+  const onStatusFilterChange = (status: string) => () => {
+    const updatedFilters = getUpdatedFilters(status, statusFilter);
+    setStatusFilter(updatedFilters);
+  };
+
+  const handleKillTask = (taskId: TaskServerResponse['taskId']) => {
+    const payload = new FormData();
+    payload.append('task_id', taskId);
+
+    save(payload, {
+      onSuccess: response => {
+        const { status, message } = response as KillTaskResponse;
+        if (status === 'success') {
+          huePubSub.publish('hue.global.info', { message: `Task: ${taskId} has been killed.` });
+        } else if (status === 'info') {
+          huePubSub.publish('hue.global.info', {
+            message: `Task: ${taskId} has already been completed or revoked.`
+          });
+        } else {
+          huePubSub.publish('hue.global.error', {
+            message: `Task: ${taskId} could not be killed. ${message}`
+          });
+        }
+      },
+      onError: error => {
+        const errorMessage = extractErrorMessage(error as AxiosError);
+        huePubSub.publish('hue.global.error', {
+          message: `Task not killed - Error: ${errorMessage}`
+        });
+      }
+    });
+  };
+
+  const onSchedulePopup = () => showScheduleTaskPopup(true);
+
+  const onKillTasks = () => {
+    selectedTasks.forEach(task => {
+      handleKillTask(task.taskId);
+    });
+    setSelectedTasks([]);
+  };
+
+  const [ref, rect] = useResizeObserver();
+  const tableBodyHeight = Math.max(rect.height - 40, 100);
+
+  return (
+    <div className="hue-task-server">
+      <div className="hue-task-server__actions">
+        <PrimaryButton onClick={onSchedulePopup}>{t('Schedule Task')}</PrimaryButton>
+        <DangerButton onClick={onKillTasks} disabled={selectedTasks.length === 0}>
+          {t('Kill Task')}
+        </DangerButton>
+        <Input
+          type="text"
+          placeholder={t('Search by task name, user ID, or task ID...')}
+          onChange={onSearchChange}
+          style={{ flexGrow: 1 }}
+        />
+        <Checkbox checked={statusFilter.success} onChange={onStatusFilterChange('success')}>
+          <span className="hue-task-server__success-text">{t('Succeeded')}</span>
+        </Checkbox>
+        <Checkbox
+          checked={statusFilter.running}
+          onChange={onStatusFilterChange('running')}
+          name="running"
+        >
+          <span className="hue-task-server__running-text">{t('Running')}</span>
+        </Checkbox>
+        <Checkbox
+          checked={statusFilter.failure}
+          onChange={onStatusFilterChange('failure')}
+          name="failure"
+        >
+          <span className="hue-task-server__failed-text">{t('Failed')}</span>
+        </Checkbox>
+        <Checkbox checked={statusFilter.all} onChange={onStatusFilterChange('all')} name="all">
+          <span>{t('All')}</span>
+        </Checkbox>
+      </div>
+      <div className="hue-task-server__table" ref={ref}>
+        <LoadingErrorWrapper loading={!tasks && loading} errors={errors}>
+          <PaginatedTable<TaskServerResponse>
+            onRowSelect={setSelectedTasks}
+            columns={columns}
+            data={filteredTasks}
+            rowKey="taskId"
+            scroll={{ y: tableBodyHeight }}
+            locale={{
+              emptyText: t('No tasks found')
+            }}
+          />
+        </LoadingErrorWrapper>
+      </div>
+      {showSchedulePopup && <ScheduleTaskModal onClose={() => showScheduleTaskPopup(false)} />}
+      {!!selectedTaskId && (
+        <TaskLogsModal taskId={selectedTaskId} onClose={() => setSelectedTaskId(null)} />
+      )}
+    </div>
+  );
+};
+
+export default TaskServer;

+ 36 - 0
desktop/core/src/desktop/js/reactComponents/TaskServer/constants.ts

@@ -0,0 +1,36 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export const SCHEDULE_NEW_TASK_URL = '/desktop/api2/taskserver/task/submit';
+export const KILL_TASK_URL = '/desktop/api2/taskserver/task/kill';
+export const GET_TASKS_URL = '/desktop/api2/taskserver/tasks';
+export const GET_TASK_LOG_URL = '/desktop/api2/taskserver/task/logs';
+
+export const scheduleTasksCategory = [
+  {
+    value: 'document_cleanup',
+    label: 'Document Cleanup',
+    children: [{ value: 'keep_days', label: 'Keep days', type: 'number' }]
+  },
+  {
+    value: 'tmp_clean_up',
+    label: 'Tmp Cleanup',
+    children: [
+      { value: 'cleanup_threshold', label: 'Cleanup threshold', type: 'number', max: 100 },
+      { value: 'disk_check_interval', label: 'Disk check interval', type: 'number', max: 100 }
+    ]
+  }
+];

+ 43 - 0
desktop/core/src/desktop/js/reactComponents/TaskServer/types.ts

@@ -0,0 +1,43 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export enum TaskStatus {
+  Success = 'SUCCESS',
+  Failure = 'FAILURE',
+  Running = 'RUNNING'
+}
+
+export interface TaskServerResult {
+  taskName: string;
+  username: string;
+  taskStart: string;
+  taskEnd: string;
+  progress: string;
+  qqfilename?: string;
+  parameters: number;
+}
+export interface TaskServerResponse {
+  result?: TaskServerResult;
+  dateDone: string;
+  taskId: string;
+  status: TaskStatus;
+  children?: TaskServerResponse[];
+}
+
+export interface KillTaskResponse {
+  status: 'success' | 'info' | 'error';
+  message: string;
+}

+ 93 - 0
desktop/core/src/desktop/js/reactComponents/TaskServer/utils.ts

@@ -0,0 +1,93 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { TaskServerResponse, TaskStatus } from './types';
+
+export const statusTagColor = (status: TaskServerResponse['status']): string =>
+  ({
+    [TaskStatus.Running]: 'processing',
+    [TaskStatus.Success]: 'success',
+    [TaskStatus.Failure]: 'error'
+  })[status] ?? '';
+
+export const getFilteredTasks = (
+  statusFilter: Record<string, boolean>,
+  searchTerm: string,
+  tasks?: TaskServerResponse[]
+): TaskServerResponse[] => {
+  if (!tasks) {
+    return [];
+  }
+  return tasks
+    .filter(task => {
+      const taskNameMatch = task.result?.taskName?.toLowerCase().includes(searchTerm);
+      const userIdMatch = task.result?.username?.toLowerCase().includes(searchTerm);
+      const taskIdMatch = task.taskId?.toLowerCase().includes(searchTerm);
+      const statusMatch =
+        statusFilter.all ||
+        (statusFilter.success && task.status === TaskStatus.Success) ||
+        (statusFilter.failure && task.status === TaskStatus.Failure) ||
+        (statusFilter.running && task.status === TaskStatus.Running);
+
+      return (taskNameMatch || userIdMatch || taskIdMatch) && statusMatch;
+    })
+    .map(task => ({
+      ...task,
+      children: task.children?.length ? task.children : undefined
+    }));
+};
+
+export const sortTasksByDate = (tasks?: TaskServerResponse[]): TaskServerResponse[] => {
+  if (!tasks) {
+    return [];
+  }
+  return tasks?.sort((a, b) => {
+    const dateA = new Date(a.dateDone).getTime();
+    const dateB = new Date(b.dateDone).getTime();
+    if (dateA === dateB || isNaN(dateA) || isNaN(dateB)) {
+      return 0;
+    }
+    return dateB - dateA;
+  });
+};
+
+export const getUpdatedFilters = (
+  status: string,
+  prevStatusFilter: Record<string, boolean>
+): Record<string, boolean> => {
+  const isAll = status === 'all';
+  const newStatusFilter = {
+    ...prevStatusFilter,
+    [status]: isAll ? true : !prevStatusFilter[status]
+  };
+
+  if (isAll) {
+    // If 'all' is selected, set everything else to false
+    newStatusFilter.success = false;
+    newStatusFilter.failure = false;
+    newStatusFilter.running = false;
+  } else {
+    // If any specific status is toggled, set 'all' to false
+    newStatusFilter.all = false;
+  }
+
+  // If no individual statuses are selected, default to 'all'
+  if (!newStatusFilter.success && !newStatusFilter.failure && !newStatusFilter.running) {
+    newStatusFilter.all = true;
+  }
+
+  return newStatusFilter;
+};

+ 2 - 2
desktop/core/src/desktop/js/reactComponents/imports.js

@@ -35,8 +35,8 @@ export async function loadComponent(name) {
     case 'WelcomeTour':
       return (await import('./WelcomeTour/WelcomeTour')).default;
 
-    case 'TaskBrowser':
-      return (await import('./TaskBrowser/TaskBrowser')).default;
+    case 'TaskServer':
+      return (await import('./TaskServer/TaskServer')).default;
 
     default:
       console.error(`A placeholder component is rendered because you probably forgot to include your new component in the 

+ 4 - 4
desktop/core/src/desktop/js/utils/hooks/useFileUpload/useChunkUpload.ts

@@ -23,7 +23,7 @@ import {
   DEFAULT_CONCURRENT_MAX_CONNECTIONS
 } from '../../constants/storageBrowser';
 import useLoadData from '../useLoadData/useLoadData';
-import { TaskServerResponse, TaskStatus } from '../../../reactComponents/TaskBrowser/TaskBrowser';
+import { TaskServerResponse, TaskStatus } from '../../../reactComponents/TaskServer/types';
 import {
   getChunksCompletePayload,
   getItemProgress,
@@ -42,6 +42,7 @@ import {
   FileStatus,
   ChunkedFilesInProgress
 } from './types';
+import { GET_TASKS_URL } from 'reactComponents/TaskServer/constants';
 
 interface UseChunkUploadResponse {
   addFiles: (item: RegularFile[]) => void;
@@ -92,11 +93,10 @@ const useChunkUpload = ({
     });
   };
 
-  useLoadData<TaskServerResponse[]>('/desktop/api2/taskserver/get_taskserver_tasks/', {
+  useLoadData<TaskServerResponse[]>(GET_TASKS_URL, {
     pollInterval: 5000,
     skip: filesWaitingFinalStatus.length === 0,
-    onSuccess: processTaskServerResponse,
-    transformKeys: 'none'
+    onSuccess: processTaskServerResponse
   });
 
   const handleAllChunksUploaded = (chunk: ChunkedFile) => {

+ 2 - 2
desktop/core/src/desktop/js/utils/hooks/useFileUpload/utils.ts

@@ -20,7 +20,7 @@ import {
   CHUNK_UPLOAD_COMPLETE_URL,
   UPLOAD_AVAILABLE_SPACE_URL
 } from '../../../apps/storageBrowser/api';
-import { TaskServerResponse, TaskStatus } from '../../../reactComponents/TaskBrowser/TaskBrowser';
+import { TaskServerResponse, TaskStatus } from '../../../reactComponents/TaskServer/types';
 import {
   ChunkedFile,
   ChunkedFilesInProgress,
@@ -81,7 +81,7 @@ export const getStatusHashMap = (
   serverResponse.reduce(
     (acc, row: TaskServerResponse) => ({
       ...acc,
-      [row.task_id]: row.status
+      [row.taskId]: row.status
     }),
     {}
   );

+ 3 - 20
desktop/core/src/desktop/templates/taskserver_list_tasks.mako

@@ -1,7 +1,5 @@
 <%!
 import sys
-from desktop.views import commonheader, commonfooter
-from desktop.auth.backend import is_admin
 
 if sys.version_info[0] > 2:
   from django.utils.translation import gettext as _
@@ -12,25 +10,10 @@ else:
 <%namespace name="actionbar" file="actionbar.mako" />
 <%namespace name="layout" file="about_layout.mako" />
 
-%if not is_embeddable:
-${ commonheader(_('Task Server'), "about", user, request) | n,unicode }
-%endif
-
 ${ layout.menubar(section='task_server') }
 
+<script src="${ static('desktop/js/task-browser-inline.js') }" type="text/javascript"></script>
 
-<div class="container-fluid">
-  <div class="card card-small">
-
-    <script src="${ static('desktop/js/task-browser-inline.js') }" type="text/javascript"></script>
-
-    <div id="taskbrowser-container">
-      <MyComponent data-reactcomponent='TaskBrowser' data-props='{"myObj": {"id": 1}, "children": "mako template only", "version" : "${sys.version_info[0]}"}' ></MyComponent>
-    </div>
-
-  </div>
+<div id="taskbrowser-container" class="cuix antd" style="height: calc(100vh - 110px)">
+  <MyComponent data-reactcomponent='TaskServer'/>
 </div>
-
-%if not is_embeddable:
-${ commonfooter(request, messages) | n,unicode }
-%endif