From 296417fd628327c5285183b73249f89de93984f7 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Tue, 6 May 2025 15:14:25 -0400 Subject: [PATCH 01/25] begin tasks + fix authorized views I think I did it finally --- backend/actions/Task/getTasks.js | 0 backend/actions/Task/index.js | 3 +++ backend/actions/index.js | 1 + frontend/src/index.js | 34 +++++++++++++++++++++++++++++++- frontend/src/navbar/navbar.html | 5 ++++- frontend/src/navbar/navbar.js | 9 +++++++++ frontend/src/routes.js | 29 ++++++++++++++++++++------- frontend/src/tasks/tasks.css | 0 frontend/src/tasks/tasks.html | 1 + frontend/src/tasks/tasks.js | 10 ++++++++++ 10 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 backend/actions/Task/getTasks.js create mode 100644 backend/actions/Task/index.js create mode 100644 frontend/src/tasks/tasks.css create mode 100644 frontend/src/tasks/tasks.html create mode 100644 frontend/src/tasks/tasks.js diff --git a/backend/actions/Task/getTasks.js b/backend/actions/Task/getTasks.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/actions/Task/index.js b/backend/actions/Task/index.js new file mode 100644 index 0000000..7e10817 --- /dev/null +++ b/backend/actions/Task/index.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.getTasks = require('./getTasks') \ No newline at end of file diff --git a/backend/actions/index.js b/backend/actions/index.js index 7b96699..ba45453 100644 --- a/backend/actions/index.js +++ b/backend/actions/index.js @@ -4,3 +4,4 @@ exports.Dashboard = require('./Dashboard'); exports.Model = require('./Model'); exports.Script = require('./Script'); exports.status = require('./status'); +exports.Task = require('./Task') diff --git a/frontend/src/index.js b/frontend/src/index.js index 83b902a..d9f8a32 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -47,6 +47,7 @@ require('./modal/modal')(app); require('./models/models')(app); require('./navbar/navbar')(app); require('./splash/splash')(app); +require('./tasks/tasks')(app); require('./team/team')(app); require('./team/new-invitation/new-invitation')(app); @@ -137,7 +138,7 @@ app.component('app-component', { } }); -const { routes } = require('./routes'); +const { routes, hasAccess } = require('./routes'); const router = VueRouter.createRouter({ history: VueRouter.createWebHashHistory(), routes: routes.map(route => ({ @@ -147,6 +148,37 @@ const router = VueRouter.createRouter({ })) }); +// Add global navigation guard +router.beforeEach((to, from, next) => { + // Skip auth check for authorized (public) routes + if (to.meta.authorized) { + next(); + return; + } + + // Get roles from the app state + const roles = window.state?.roles; + + // Check if user has access to the route + if (!hasAccess(roles, to.name)) { + // Find all routes the user has access to + const allowedRoutes = routes.filter(route => hasAccess(roles, route.name)); + + // If user has no allowed routes, redirect to splash/login + if (allowedRoutes.length === 0) { + next({ name: 'root' }); + return; + } + + // Redirect to first allowed route + const firstAllowedRoute = allowedRoutes[0].name; + next({ name: firstAllowedRoute }); + return; + } + + next(); +}); + app.use(router); app.mount('#content'); diff --git a/frontend/src/navbar/navbar.html b/frontend/src/navbar/navbar.html index a482cdc..2dea6a1 100644 --- a/frontend/src/navbar/navbar.html +++ b/frontend/src/navbar/navbar.html @@ -17,7 +17,10 @@ href="#/dashboards" class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium" :class="dashboardView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'">Dashboards - + Tasks
+
+ + + + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
Scheduled
+
{{pendingCount}}
+
+
+
Completed
+
{{succeededCount}}
+
+
+
Failed
+
{{failedCount}}
+
+
+
Cancelled
+
{{cancelledCount}}
+
+
+ +
+ diff --git a/frontend/src/tasks/tasks.js b/frontend/src/tasks/tasks.js index 35bd573..9b2fdb4 100644 --- a/frontend/src/tasks/tasks.js +++ b/frontend/src/tasks/tasks.js @@ -1,10 +1,169 @@ 'use strict'; const template = require('./tasks.html'); +const api = require('../api'); module.exports = app => app.component('tasks', { data: () => ({ - status: 'init' + status: 'init', + tasks: [], + selectedRange: 'all', + start: null, + end: null, + dateFilters: [ + { value: 'all', label: 'All Time' }, + { value: 'today', label: 'Today' }, + { value: 'yesterday', label: 'Yesterday' }, + { value: 'thisWeek', label: 'This Week' }, + { value: 'lastWeek', label: 'Last Week' }, + { value: 'thisMonth', label: 'This Month' }, + { value: 'lastMonth', label: 'Last Month' }, + ], + selectedStatus: 'all', + statusFilters: [ + { label: 'All', value: 'all' }, + { label: 'Pending', value: 'pending' }, + { label: 'In Progress', value: 'in_progress' }, + { label: 'Succeeded', value: 'succeeded' }, + { label: 'Failed', value: 'failed' }, + { label: 'Cancelled', value: 'cancelled' } + ], + newTask: { status: 'pending' } }), + methods: { + async getTasks() { + const params = {}; + if (this.selectedStatus == 'all') { + params.status = null; + } else { + params.status = this.selectedStatus; + } + + if (this.start && this.end) { + params.start = this.start; + params.end = this.end + } else if (this.start) { + params.start = this.start; + } + const { tasks } = await api.Task.getTasks(params); + this.tasks = tasks; + }, + getStatusColor(status) { + if (status === 'succeeded') { + // Green (success) + return 'bg-green-100 text-green-800'; + } else if (status === 'pending') { + // Yellow (waiting) + return 'bg-yellow-100 text-yellow-800'; + } else if (status === 'cancelled') { + // Gray (neutral/aborted) + return 'bg-gray-100 text-gray-800'; + } else if (status === 'failed') { + // Red (error) + return 'bg-red-100 text-red-800'; + } else if (status === 'in_progress') { + // Blue (active/running) + return 'bg-blue-100 text-blue-800'; + } else { + // Default (fallback) + return 'bg-slate-100 text-slate-800'; + } + }, + async resetFilters() { + this.selectedStatus = 'all'; + this.selectedRange = 'all'; + this.start = null; + this.end = null; + await this.getTasks(); + }, + async updateDateRange() { + const now = new Date(); + let start, end; + + const getStartOfWeek = (d) => { + const date = new Date(d); + const day = date.getDay(); + date.setDate(date.getDate() - day); + date.setHours(0, 0, 0, 0); + return date; + }; + + const getEndOfWeek = (d) => { + const date = new Date(d); + const day = date.getDay(); + date.setDate(date.getDate() + (6 - day)); + date.setHours(23, 59, 59, 999); + return date; + }; + + switch (this.selectedRange) { + case 'today': + start = new Date(); + start.setHours(0, 0, 0, 0); + end = new Date(); + end.setHours(23, 59, 59, 999); + break; + case 'yesterday': + start = new Date(); + start.setDate(start.getDate() - 1); + start.setHours(0, 0, 0, 0); + end = new Date(); + end.setDate(end.getDate() - 1); + end.setHours(23, 59, 59, 999); + break; + case 'thisWeek': + start = getStartOfWeek(now); + end = getEndOfWeek(now); + break; + case 'lastWeek': + const lastWeekStart = getStartOfWeek(new Date(now.getTime() - 7 * 86400000)); + const lastWeekEnd = getEndOfWeek(new Date(now.getTime() - 7 * 86400000)); + start = lastWeekStart; + end = lastWeekEnd; + break; + case 'thisMonth': + start = new Date(now.getFullYear(), now.getMonth(), 1); + end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999); + break; + case 'lastMonth': + start = new Date(now.getFullYear(), now.getMonth() - 1, 1); + end = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); + break; + case 'all': + default: + this.start = null; + this.end = null; + return; + } + + this.start = start; + this.end = end; + + await this.getTasks(); + } + }, + computed: { + canCreateNewTask() { + if (this.newTask.status && this.newTask.time && this.newTask.name) { + return true; + } + return false; + }, + succeededCount() { + return this.tasks.filter(task => task.status === 'succeeded').length; + }, + failedCount() { + return this.tasks.filter(task => task.status === 'failed').length; + }, + cancelledCount() { + return this.tasks.filter(task => task.status === 'cancelled').length; + }, + pendingCount() { + return this.tasks.filter(task => task.status === 'pending').length; + } + }, + mounted: async function() { + await this.getTasks(); + }, template: template }); \ No newline at end of file From d32696ce14606abcf6db5789d49b9439639f9f0a Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 6 Jun 2025 16:49:40 -0400 Subject: [PATCH 04/25] remove task creator for v1 --- frontend/src/tasks/tasks.html | 49 ----------------------------------- 1 file changed, 49 deletions(-) diff --git a/frontend/src/tasks/tasks.html b/frontend/src/tasks/tasks.html index a08b2f6..b4dce06 100644 --- a/frontend/src/tasks/tasks.html +++ b/frontend/src/tasks/tasks.html @@ -1,54 +1,5 @@

Task Overview

- -
-

Schedule a New Task

-
-
- - -
- -
- - -
- -
- - -
- -
- -
-
-
-
From 9ddb57c76850f6b1a68a0650e63a3194666529cd Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:23:40 -0400 Subject: [PATCH 05/25] make requested changes --- backend/actions/Task/getTasks.js | 20 ++++++++++- express.js | 7 ++++ frontend/src/navbar/navbar.js | 10 +++--- frontend/src/tasks/tasks.html | 56 +++++++++++++++++++++--------- frontend/src/tasks/tasks.js | 58 ++++++++++++++++++++++++++++---- package.json | 1 - 6 files changed, 123 insertions(+), 29 deletions(-) diff --git a/backend/actions/Task/getTasks.js b/backend/actions/Task/getTasks.js index 4b6d6b9..79ec954 100644 --- a/backend/actions/Task/getTasks.js +++ b/backend/actions/Task/getTasks.js @@ -31,8 +31,26 @@ module.exports = ({ db }) => async function getTasks(params) { } const tasks = await Task.find(filter); + + // Define all possible statuses + const allStatuses = ['pending', 'in_progress', 'succeeded', 'failed', 'cancelled', 'unknown']; + + // Initialize groupedTasks with all statuses + const groupedTasks = allStatuses.reduce((groups, status) => { + groups[status] = []; + return groups; + }, {}); + + // Group tasks by status + tasks.forEach(task => { + const taskStatus = task.status || 'unknown'; + if (groupedTasks.hasOwnProperty(taskStatus)) { + groupedTasks[taskStatus].push(task); + } + }); return { - tasks + tasks, + groupedTasks }; }; \ No newline at end of file diff --git a/express.js b/express.js index b7a7163..b5be8b2 100644 --- a/express.js +++ b/express.js @@ -78,6 +78,13 @@ module.exports = async function(apiUrl, conn, options) { console.log('Workspace', workspace); const { config } = await frontend(apiUrl, false, options, workspace); + + try { + require('@mongoosejs/task') + config.enableTaskVisualizer = true; + } catch(e) { + config.enableTaskVisualizer = false; + } router.get('/config.js', function (req, res) { res.setHeader('Content-Type', 'application/javascript'); res.end(`window.MONGOOSE_STUDIO_CONFIG = ${JSON.stringify(config, null, 2)};`); diff --git a/frontend/src/navbar/navbar.js b/frontend/src/navbar/navbar.js index 1d7d75b..9c9ca6c 100644 --- a/frontend/src/navbar/navbar.js +++ b/frontend/src/navbar/navbar.js @@ -67,12 +67,10 @@ module.exports = app => app.component('navbar', { defaultRoute() { return this.allowedRoutes[0]?.name || 'dashboards'; }, - hasTasks() { - // fix this when done - try { - require.resolve('@mongoosejs/task'); - return `#/tasks` - } catch (e) { + hasTaskVisualizer() { + if (window.MONGOOSE_STUDIO_CONFIG.enableTaskVisualizer) { + return '#/tasks' + } else { return `https://www.npmjs.com/package/@mongoosejs/task` } } diff --git a/frontend/src/tasks/tasks.html b/frontend/src/tasks/tasks.html index b4dce06..d382c32 100644 --- a/frontend/src/tasks/tasks.html +++ b/frontend/src/tasks/tasks.html @@ -1,7 +1,10 @@

Task Overview

+
+ +
-
+
+
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+
diff --git a/frontend/src/tasks/task-details/task-details.js b/frontend/src/tasks/task-details/task-details.js new file mode 100644 index 0000000..cfdd09c --- /dev/null +++ b/frontend/src/tasks/task-details/task-details.js @@ -0,0 +1,125 @@ +'use strict'; + +const template = require('./task-details.html'); +const api = require('../../api'); + +module.exports = app => app.component('task-details', { + props: { + taskGroup: { + type: Object, + required: true + } + }, + data: () => ({ + showCreateTask: false, + newTask: { + name: '', + scheduledAt: '', + parameters: '' + } + }), + computed: { + sortedTasks() { + return [...this.taskGroup.tasks].sort((a, b) => { + const dateA = new Date(a.scheduledAt || a.createdAt || 0); + const dateB = new Date(b.scheduledAt || b.createdAt || 0); + return dateB - dateA; // Most recent first + }); + } + }, + methods: { + getStatusColor(status) { + if (status === 'succeeded') { + return 'bg-green-100 text-green-800'; + } else if (status === 'pending') { + return 'bg-yellow-100 text-yellow-800'; + } else if (status === 'cancelled') { + return 'bg-gray-100 text-gray-800'; + } else if (status === 'failed') { + return 'bg-red-100 text-red-800'; + } else if (status === 'in_progress') { + return 'bg-blue-100 text-blue-800'; + } else { + return 'bg-slate-100 text-slate-800'; + } + }, + formatDate(dateString) { + if (!dateString) return 'N/A'; + return new Date(dateString).toLocaleString(); + }, + async rescheduleTask(task) { + try { + // This would call a backend API to reschedule the task + // For now, we'll show a confirmation dialog + if (confirm(`Reschedule task ${task.id}?`)) { + // TODO: Implement reschedule API call + console.log('Rescheduling task:', task.id); + // await api.Task.rescheduleTask(task.id); + } + } catch (error) { + console.error('Error rescheduling task:', error); + alert('Failed to reschedule task'); + } + }, + async runTask(task) { + try { + // This would call a backend API to run the task immediately + if (confirm(`Run task ${task.id} now?`)) { + // TODO: Implement run task API call + console.log('Running task:', task.id); + // await api.Task.runTask(task.id); + } + } catch (error) { + console.error('Error running task:', error); + alert('Failed to run task'); + } + }, + async createTask() { + try { + let parameters = {}; + if (this.newTask.parameters.trim()) { + try { + parameters = JSON.parse(this.newTask.parameters); + } catch (e) { + alert('Invalid JSON in parameters field'); + return; + } + } + + const taskData = { + name: this.newTask.name || this.taskGroup.name, + scheduledAt: this.newTask.scheduledAt, + parameters: parameters + }; + + // TODO: Implement create task API call + console.log('Creating task:', taskData); + // await api.Task.createTask(taskData); + + // Reset form and close modal + this.newTask = { + name: '', + scheduledAt: '', + parameters: '' + }; + this.showCreateTask = false; + + // Emit event to refresh parent data + this.$emit('task-created'); + } catch (error) { + console.error('Error creating task:', error); + alert('Failed to create task'); + } + } + }, + mounted() { + // Set default values + this.newTask.name = this.taskGroup.name; + + // Set default scheduled time to 1 hour from now + const defaultTime = new Date(); + defaultTime.setHours(defaultTime.getHours() + 1); + this.newTask.scheduledAt = defaultTime.toISOString().slice(0, 16); + }, + template: template +}); diff --git a/frontend/src/tasks/tasks.html b/frontend/src/tasks/tasks.html index d382c32..bc994f3 100644 --- a/frontend/src/tasks/tasks.html +++ b/frontend/src/tasks/tasks.html @@ -1,89 +1,101 @@
-

Task Overview

-
- -
- -
-
- - -
-
- - -
-
- + + + + +
+

Task Overview

+
+
- -
-
-
Scheduled
-
{{pendingCount}}
+ +
+
+ +
-
-
Completed
-
{{succeededCount}}
+
+ +
-
-
Failed
-
{{failedCount}}
+
+
-
-
Cancelled
-
{{cancelledCount}}
+ +
+
+
Scheduled
+
{{pendingCount}}
+
+
+
Completed
+
{{succeededCount}}
+
+
+
Failed
+
{{failedCount}}
+
+
+
Cancelled
+
{{cancelledCount}}
+
-
- - -
-

Tasks by Name

-
    -
  • -
    -
    -
    {{ group.name }}
    -
    Total: {{ group.totalCount }} tasks
    -
    -
    - Last run: {{ group.lastRun ? new Date(group.lastRun).toLocaleString() : 'Never' }} + + +
    +

    Tasks by Name

    +
      +
    • +
      +
      +
      {{ group.name }}
      +
      Total: {{ group.totalCount }} tasks
      +
      +
      + Last run: {{ group.lastRun ? new Date(group.lastRun).toLocaleString() : 'Never' }} +
      -
    - -
    -
    -
    Pending
    -
    {{ group.statusCounts.pending || 0 }}
    + + +
    +
    +
    Pending
    +
    {{ group.statusCounts.pending || 0 }}
    +
    +
    +
    Succeeded
    +
    {{ group.statusCounts.succeeded || 0 }}
    +
    +
    +
    Failed
    +
    {{ group.statusCounts.failed || 0 }}
    +
    +
    +
    Cancelled
    +
    {{ group.statusCounts.cancelled || 0 }}
    +
    -
    -
    Succeeded
    -
    {{ group.statusCounts.succeeded || 0 }}
    -
    -
    -
    Failed
    -
    {{ group.statusCounts.failed || 0 }}
    -
    -
    -
    Cancelled
    -
    {{ group.statusCounts.cancelled || 0 }}
    -
    -
    -
  • -
+ + +
diff --git a/frontend/src/tasks/tasks.js b/frontend/src/tasks/tasks.js index 3c36e9e..419126d 100644 --- a/frontend/src/tasks/tasks.js +++ b/frontend/src/tasks/tasks.js @@ -3,6 +3,7 @@ const template = require('./tasks.html'); const api = require('../api'); + module.exports = app => app.component('tasks', { data: () => ({ status: 'init', @@ -29,7 +30,10 @@ module.exports = app => app.component('tasks', { { label: 'Failed', value: 'failed' }, { label: 'Cancelled', value: 'cancelled' } ], - newTask: { status: 'pending' } + newTask: { status: 'pending' }, + // Task details view state + showTaskDetails: false, + selectedTaskGroup: null }), methods: { async getTasks() { @@ -50,6 +54,18 @@ module.exports = app => app.component('tasks', { this.tasks = tasks; this.groupedTasks = groupedTasks; }, + openTaskGroupDetails(group) { + this.selectedTaskGroup = group; + this.showTaskDetails = true; + }, + hideTaskDetails() { + this.showTaskDetails = false; + this.selectedTaskGroup = null; + }, + async onTaskCreated() { + // Refresh the task data when a new task is created + await this.getTasks(); + }, getStatusColor(status) { if (status === 'succeeded') { // Green (success) From 7f04242f5c1759112acae9e7b6340cc15934af87 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:24:34 -0400 Subject: [PATCH 11/25] more frontend work --- .../src/tasks/task-details/task-details.html | 236 +++++++++--------- .../src/tasks/task-details/task-details.js | 11 +- frontend/src/tasks/tasks.html | 14 +- frontend/src/tasks/tasks.js | 68 +++++ 4 files changed, 200 insertions(+), 129 deletions(-) diff --git a/frontend/src/tasks/task-details/task-details.html b/frontend/src/tasks/task-details/task-details.html index e11bc9c..77eabfe 100644 --- a/frontend/src/tasks/task-details/task-details.html +++ b/frontend/src/tasks/task-details/task-details.html @@ -11,150 +11,150 @@

{{ taskGroup.name }}

Total: {{ taskGroup.totalCount }} tasks

-
-
+
- -
+ +
-
Pending
-
{{ taskGroup.statusCounts.pending || 0 }}
+
Pending
+
{{ taskGroup.statusCounts.pending || 0 }}
-
Succeeded
-
{{ taskGroup.statusCounts.succeeded || 0 }}
+
Succeeded
+
{{ taskGroup.statusCounts.succeeded || 0 }}
-
Failed
-
{{ taskGroup.statusCounts.failed || 0 }}
+
Failed
+
{{ taskGroup.statusCounts.failed || 0 }}
-
Cancelled
-
{{ taskGroup.statusCounts.cancelled || 0 }}
+
Cancelled
+
{{ taskGroup.statusCounts.cancelled || 0 }}
-
+
- -
+ +
-

Individual Tasks

+

Individual Tasks

-
-
-
-
- Task ID: {{ task.id }} - - {{ task.status }} - -
- -
-
- -
{{ formatDate(task.scheduledAt) }}
-
-
- -
{{ formatDate(task.startedAt) }}
-
-
- -
{{ formatDate(task.completedAt) }}
-
-
- -
{{ task.error }}
-
-
- - -
- -
-
{{ JSON.stringify(task.parameters, null, 2) }}
-
-
-
- -
- - -
-
-
-
-
- - -
-
-

Create New Task

- -
-
- - +
+
+
+
+ Task ID: {{ task.id }} + + {{ task.status }} +
-
- - +
+
+ +
{{ formatDate(task.scheduledAt) }}
+
+
+ +
{{ formatDate(task.startedAt) }}
+
+
+ +
{{ formatDate(task.completedAt) }}
+
+
+ +
{{ task.error }}
+
- -
- - + + +
+ +
+
{{ JSON.stringify(task.parameters, null, 2) }}
+
-
- -
+
+ +
+
+
-
+ + + + + diff --git a/frontend/src/tasks/task-details/task-details.js b/frontend/src/tasks/task-details/task-details.js index cfdd09c..1242c68 100644 --- a/frontend/src/tasks/task-details/task-details.js +++ b/frontend/src/tasks/task-details/task-details.js @@ -4,14 +4,9 @@ const template = require('./task-details.html'); const api = require('../../api'); module.exports = app => app.component('task-details', { - props: { - taskGroup: { - type: Object, - required: true - } - }, + props: ['taskGroup'], data: () => ({ - showCreateTask: false, + showCreateTaskModal: false, newTask: { name: '', scheduledAt: '', @@ -102,7 +97,7 @@ module.exports = app => app.component('task-details', { scheduledAt: '', parameters: '' }; - this.showCreateTask = false; + this.showCreateTaskModal = false; // Emit event to refresh parent data this.$emit('task-created'); diff --git a/frontend/src/tasks/tasks.html b/frontend/src/tasks/tasks.html index bc994f3..5771f2d 100644 --- a/frontend/src/tasks/tasks.html +++ b/frontend/src/tasks/tasks.html @@ -65,9 +65,17 @@

Tasks by Name

  • -
    -
    {{ group.name }}
    -
    Total: {{ group.totalCount }} tasks
    +
    +
    +
    {{ group.name }}
    + + + +
    +
    Total: {{ group.totalCount }} tasks
    +
    + Click to view details +
    Last run: {{ group.lastRun ? new Date(group.lastRun).toLocaleString() : 'Never' }} diff --git a/frontend/src/tasks/tasks.js b/frontend/src/tasks/tasks.js index 419126d..ba0b60b 100644 --- a/frontend/src/tasks/tasks.js +++ b/frontend/src/tasks/tasks.js @@ -66,6 +66,74 @@ module.exports = app => app.component('tasks', { // Refresh the task data when a new task is created await this.getTasks(); }, + formatDate(dateString) { + if (!dateString) return 'N/A'; + return new Date(dateString).toLocaleString(); + }, + async rescheduleTask(task) { + try { + // This would call a backend API to reschedule the task + // For now, we'll show a confirmation dialog + if (confirm(`Reschedule task ${task.id}?`)) { + // TODO: Implement reschedule API call + console.log('Rescheduling task:', task.id); + // await api.Task.rescheduleTask(task.id); + } + } catch (error) { + console.error('Error rescheduling task:', error); + alert('Failed to reschedule task'); + } + }, + async runTask(task) { + try { + // This would call a backend API to run the task immediately + if (confirm(`Run task ${task.id} now?`)) { + // TODO: Implement run task API call + console.log('Running task:', task.id); + // await api.Task.runTask(task.id); + } + } catch (error) { + console.error('Error running task:', error); + alert('Failed to run task'); + } + }, + async createTask() { + try { + let parameters = {}; + if (this.newTask.parameters.trim()) { + try { + parameters = JSON.parse(this.newTask.parameters); + } catch (e) { + alert('Invalid JSON in parameters field'); + return; + } + } + + const taskData = { + name: this.newTask.name || this.selectedTaskGroup.name, + scheduledAt: this.newTask.scheduledAt, + parameters: parameters + }; + + // TODO: Implement create task API call + console.log('Creating task:', taskData); + // await api.Task.createTask(taskData); + + // Reset form and close modal + this.newTask = { + name: '', + scheduledAt: '', + parameters: '' + }; + this.showCreateTask = false; + + // Refresh the task data + await this.getTasks(); + } catch (error) { + console.error('Error creating task:', error); + alert('Failed to create task'); + } + }, getStatusColor(status) { if (status === 'succeeded') { // Green (success) From 386b5a80f0006f8b50ff00579a31643167ce9427 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:29:18 -0400 Subject: [PATCH 12/25] cleanup --- .../src/tasks/task-details/task-details.js | 28 ++++++++----------- frontend/src/tasks/tasks.js | 28 ++++++++----------- package.json | 1 + 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/frontend/src/tasks/task-details/task-details.js b/frontend/src/tasks/task-details/task-details.js index 1242c68..21289c9 100644 --- a/frontend/src/tasks/task-details/task-details.js +++ b/frontend/src/tasks/task-details/task-details.js @@ -44,29 +44,22 @@ module.exports = app => app.component('task-details', { }, async rescheduleTask(task) { try { - // This would call a backend API to reschedule the task - // For now, we'll show a confirmation dialog - if (confirm(`Reschedule task ${task.id}?`)) { - // TODO: Implement reschedule API call - console.log('Rescheduling task:', task.id); - // await api.Task.rescheduleTask(task.id); - } + // TODO: Implement reschedule API call + console.log('Rescheduling task:', task.id); + // await api.Task.rescheduleTask(task.id); } catch (error) { console.error('Error rescheduling task:', error); - alert('Failed to reschedule task'); + // TODO: Add proper error handling/notification } }, async runTask(task) { try { - // This would call a backend API to run the task immediately - if (confirm(`Run task ${task.id} now?`)) { - // TODO: Implement run task API call - console.log('Running task:', task.id); - // await api.Task.runTask(task.id); - } + // TODO: Implement run task API call + console.log('Running task:', task.id); + // await api.Task.runTask(task.id); } catch (error) { console.error('Error running task:', error); - alert('Failed to run task'); + // TODO: Add proper error handling/notification } }, async createTask() { @@ -76,7 +69,8 @@ module.exports = app => app.component('task-details', { try { parameters = JSON.parse(this.newTask.parameters); } catch (e) { - alert('Invalid JSON in parameters field'); + console.error('Invalid JSON in parameters field:', e); + // TODO: Add proper validation feedback return; } } @@ -103,7 +97,7 @@ module.exports = app => app.component('task-details', { this.$emit('task-created'); } catch (error) { console.error('Error creating task:', error); - alert('Failed to create task'); + // TODO: Add proper error handling/notification } } }, diff --git a/frontend/src/tasks/tasks.js b/frontend/src/tasks/tasks.js index ba0b60b..fce8e05 100644 --- a/frontend/src/tasks/tasks.js +++ b/frontend/src/tasks/tasks.js @@ -72,29 +72,22 @@ module.exports = app => app.component('tasks', { }, async rescheduleTask(task) { try { - // This would call a backend API to reschedule the task - // For now, we'll show a confirmation dialog - if (confirm(`Reschedule task ${task.id}?`)) { - // TODO: Implement reschedule API call - console.log('Rescheduling task:', task.id); - // await api.Task.rescheduleTask(task.id); - } + // TODO: Implement reschedule API call + console.log('Rescheduling task:', task.id); + // await api.Task.rescheduleTask(task.id); } catch (error) { console.error('Error rescheduling task:', error); - alert('Failed to reschedule task'); + // TODO: Add proper error handling/notification } }, async runTask(task) { try { - // This would call a backend API to run the task immediately - if (confirm(`Run task ${task.id} now?`)) { - // TODO: Implement run task API call - console.log('Running task:', task.id); - // await api.Task.runTask(task.id); - } + // TODO: Implement run task API call + console.log('Running task:', task.id); + // await api.Task.runTask(task.id); } catch (error) { console.error('Error running task:', error); - alert('Failed to run task'); + // TODO: Add proper error handling/notification } }, async createTask() { @@ -104,7 +97,8 @@ module.exports = app => app.component('tasks', { try { parameters = JSON.parse(this.newTask.parameters); } catch (e) { - alert('Invalid JSON in parameters field'); + console.error('Invalid JSON in parameters field:', e); + // TODO: Add proper validation feedback return; } } @@ -131,7 +125,7 @@ module.exports = app => app.component('tasks', { await this.getTasks(); } catch (error) { console.error('Error creating task:', error); - alert('Failed to create task'); + // TODO: Add proper error handling/notification } }, getStatusColor(status) { diff --git a/package.json b/package.json index 0cc1fb1..e5fda64 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "devDependencies": { "@masteringjs/eslint-config": "0.1.1", + "@mongoosejs/task": "^0.3.0", "axios": "1.2.2", "dedent": "^1.6.0", "eslint": "9.30.0", From 6e104bb7839f473d92993ee02eec8fa8b3fde8d9 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:58:02 -0400 Subject: [PATCH 13/25] UI polishing --- express.js | 8 +- .../src/tasks/task-details/task-details.html | 234 +++++++++++------- .../src/tasks/task-details/task-details.js | 107 ++++---- frontend/src/tasks/tasks.html | 142 +++++++++-- frontend/src/tasks/tasks.js | 65 ++++- 5 files changed, 369 insertions(+), 187 deletions(-) diff --git a/express.js b/express.js index 721a951..fbd1539 100644 --- a/express.js +++ b/express.js @@ -79,13 +79,7 @@ module.exports = async function(apiUrl, conn, options) { console.log('Workspace', workspace); const { config } = await frontend(apiUrl, false, options, workspace); - - try { - require('@mongoosejs/task') - config.enableTaskVisualizer = true; - } catch(e) { - config.enableTaskVisualizer = false; - } + config.enableTaskVisualizer = options.enableTaskVisualizer; router.get('/config.js', function (req, res) { res.setHeader('Content-Type', 'application/javascript'); res.end(`window.MONGOOSE_STUDIO_CONFIG = ${JSON.stringify(config, null, 2)};`); diff --git a/frontend/src/tasks/task-details/task-details.html b/frontend/src/tasks/task-details/task-details.html index 77eabfe..411bd86 100644 --- a/frontend/src/tasks/task-details/task-details.html +++ b/frontend/src/tasks/task-details/task-details.html @@ -10,38 +10,62 @@

    {{ taskGroup.name }}

    Total: {{ taskGroup.totalCount }} tasks

    -
    - -
    -
    - -
    -
    -
    Pending
    -
    {{ taskGroup.statusCounts.pending || 0 }}
    -
    -
    -
    Succeeded
    -
    {{ taskGroup.statusCounts.succeeded || 0 }}
    -
    -
    -
    Failed
    -
    {{ taskGroup.statusCounts.failed || 0 }}
    -
    -
    -
    Cancelled
    -
    {{ taskGroup.statusCounts.cancelled || 0 }}
    -
    - -
    -
    -

    Individual Tasks

    -
    + +
    + + + + +
    + + +
    +
    +

    + Individual Tasks + + (Filtered by {{ currentFilter }}) + +

    + +
    @@ -84,19 +108,25 @@

    Individual Tasks

    -
    +
    @@ -105,56 +135,86 @@

    Individual Tasks

    - - - - + + + + + + + + + +
    + diff --git a/frontend/src/tasks/task-details/task-details.js b/frontend/src/tasks/task-details/task-details.js index 21289c9..740e0f1 100644 --- a/frontend/src/tasks/task-details/task-details.js +++ b/frontend/src/tasks/task-details/task-details.js @@ -4,18 +4,22 @@ const template = require('./task-details.html'); const api = require('../../api'); module.exports = app => app.component('task-details', { - props: ['taskGroup'], + props: ['taskGroup', 'currentFilter'], data: () => ({ - showCreateTaskModal: false, - newTask: { - name: '', - scheduledAt: '', - parameters: '' - } + showRescheduleModal: false, + showRunModal: false, + selectedTask: null }), computed: { sortedTasks() { - return [...this.taskGroup.tasks].sort((a, b) => { + let tasks = this.taskGroup.tasks; + + // Apply filter if one is set + if (this.currentFilter) { + tasks = tasks.filter(task => task.status === this.currentFilter); + } + + return tasks.sort((a, b) => { const dateA = new Date(a.scheduledAt || a.createdAt || 0); const dateB = new Date(b.scheduledAt || b.createdAt || 0); return dateB - dateA; // Most recent first @@ -62,53 +66,50 @@ module.exports = app => app.component('task-details', { // TODO: Add proper error handling/notification } }, - async createTask() { - try { - let parameters = {}; - if (this.newTask.parameters.trim()) { - try { - parameters = JSON.parse(this.newTask.parameters); - } catch (e) { - console.error('Invalid JSON in parameters field:', e); - // TODO: Add proper validation feedback - return; - } - } - - const taskData = { - name: this.newTask.name || this.taskGroup.name, - scheduledAt: this.newTask.scheduledAt, - parameters: parameters - }; - - // TODO: Implement create task API call - console.log('Creating task:', taskData); - // await api.Task.createTask(taskData); - - // Reset form and close modal - this.newTask = { - name: '', - scheduledAt: '', - parameters: '' - }; - this.showCreateTaskModal = false; - - // Emit event to refresh parent data - this.$emit('task-created'); - } catch (error) { - console.error('Error creating task:', error); - // TODO: Add proper error handling/notification + filterByStatus(status) { + // If clicking the same status, clear the filter + if (this.currentFilter === status) { + this.$emit('update:currentFilter', null); + } else { + this.$emit('update:currentFilter', status); } - } - }, + }, + clearFilter() { + this.$emit('update:currentFilter', null); + }, + showRescheduleConfirmation(task) { + this.selectedTask = task; + this.showRescheduleModal = true; + }, + showRunConfirmation(task) { + this.selectedTask = task; + this.showRunModal = true; + }, + async confirmRescheduleTask() { + try { + await this.rescheduleTask(this.selectedTask); + this.showRescheduleModal = false; + this.selectedTask = null; + } catch (error) { + console.error('Error in confirmRescheduleTask:', error); + } + }, + async confirmRunTask() { + try { + await this.runTask(this.selectedTask); + this.showRunModal = false; + this.selectedTask = null; + } catch (error) { + console.error('Error in confirmRunTask:', error); + } + } + + }, mounted() { - // Set default values - this.newTask.name = this.taskGroup.name; - - // Set default scheduled time to 1 hour from now - const defaultTime = new Date(); - defaultTime.setHours(defaultTime.getHours() + 1); - this.newTask.scheduledAt = defaultTime.toISOString().slice(0, 16); + // Check if the task group was already filtered when passed from parent + if (this.taskGroup.filteredStatus && !this.currentFilter) { + this.$emit('update:currentFilter', this.taskGroup.filteredStatus); + } }, template: template }); diff --git a/frontend/src/tasks/tasks.html b/frontend/src/tasks/tasks.html index 5771f2d..f382f6d 100644 --- a/frontend/src/tasks/tasks.html +++ b/frontend/src/tasks/tasks.html @@ -1,11 +1,13 @@
    - - + +
    @@ -31,41 +33,61 @@

    Task Overview

    -
    +
    +
    +
    +
    -
    +
    -
    + +
    -
    + +
    -
    + +
    +

    Tasks by Name

      -
    • -
      -
      +
    • +
      +
      {{ group.name }}
      @@ -84,26 +106,92 @@

      Tasks by Name

      -
      +
      -
      + +
      -
      + +
      -
      + +
      +
    + + + + +
    diff --git a/frontend/src/tasks/tasks.js b/frontend/src/tasks/tasks.js index fce8e05..0280754 100644 --- a/frontend/src/tasks/tasks.js +++ b/frontend/src/tasks/tasks.js @@ -25,15 +25,22 @@ module.exports = app => app.component('tasks', { statusFilters: [ { label: 'All', value: 'all' }, { label: 'Pending', value: 'pending' }, - { label: 'In Progress', value: 'in_progress' }, + // { label: 'In Progress', value: 'in_progress' }, { label: 'Succeeded', value: 'succeeded' }, { label: 'Failed', value: 'failed' }, { label: 'Cancelled', value: 'cancelled' } ], - newTask: { status: 'pending' }, // Task details view state showTaskDetails: false, - selectedTaskGroup: null + selectedTaskGroup: null, + taskDetailsFilter: null, + // Create task modal state + showCreateTaskModal: false, + newTask: { + name: '', + scheduledAt: '', + parameters: '' + } }), methods: { async getTasks() { @@ -58,9 +65,21 @@ module.exports = app => app.component('tasks', { this.selectedTaskGroup = group; this.showTaskDetails = true; }, + openTaskGroupDetailsWithFilter(group, status) { + // Create a filtered version of the task group with only the specified status + const filteredGroup = { + ...group, + tasks: group.tasks.filter(task => task.status === status), + filteredStatus: status + }; + this.selectedTaskGroup = filteredGroup; + this.taskDetailsFilter = status; + this.showTaskDetails = true; + }, hideTaskDetails() { this.showTaskDetails = false; this.selectedTaskGroup = null; + this.taskDetailsFilter = null; }, async onTaskCreated() { // Refresh the task data when a new task is created @@ -104,7 +123,7 @@ module.exports = app => app.component('tasks', { } const taskData = { - name: this.newTask.name || this.selectedTaskGroup.name, + name: this.newTask.name, scheduledAt: this.newTask.scheduledAt, parameters: parameters }; @@ -114,12 +133,8 @@ module.exports = app => app.component('tasks', { // await api.Task.createTask(taskData); // Reset form and close modal - this.newTask = { - name: '', - scheduledAt: '', - parameters: '' - }; - this.showCreateTask = false; + this.resetCreateTaskForm(); + this.showCreateTaskModal = false; // Refresh the task data await this.getTasks(); @@ -128,6 +143,19 @@ module.exports = app => app.component('tasks', { // TODO: Add proper error handling/notification } }, + resetCreateTaskForm() { + this.newTask = { + name: '', + scheduledAt: '', + parameters: '' + }; + }, + setDefaultCreateTaskValues() { + // Set default scheduled time to 1 hour from now + const defaultTime = new Date(); + defaultTime.setHours(defaultTime.getHours() + 1); + this.newTask.scheduledAt = defaultTime.toISOString().slice(0, 16); + }, getStatusColor(status) { if (status === 'succeeded') { // Green (success) @@ -151,9 +179,11 @@ module.exports = app => app.component('tasks', { }, async resetFilters() { this.selectedStatus = 'all'; - this.selectedRange = 'all'; - this.start = null; - this.end = null; + this.selectedRange = 'today'; + await this.updateDateRange(); + }, + async setStatusFilter(status) { + this.selectedStatus = status; await this.getTasks(); }, async updateDateRange() { @@ -288,6 +318,15 @@ module.exports = app => app.component('tasks', { await this.updateDateRange(); await this.getTasks(); this.status = 'loaded'; + this.setDefaultCreateTaskValues(); + }, + watch: { + showCreateTaskModal(newVal) { + if (newVal) { + this.resetCreateTaskForm(); + this.setDefaultCreateTaskValues(); + } + } }, template: template }); \ No newline at end of file From 4cd32ea0c1cf8a2e942027154ddef450d1792bf8 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:59:00 -0400 Subject: [PATCH 14/25] missed a spot --- frontend/src/tasks/task-details/task-details.html | 2 +- frontend/src/tasks/tasks.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/tasks/task-details/task-details.html b/frontend/src/tasks/task-details/task-details.html index 411bd86..1336ce8 100644 --- a/frontend/src/tasks/task-details/task-details.html +++ b/frontend/src/tasks/task-details/task-details.html @@ -13,7 +13,7 @@

    {{ taskGroup.name }}

    - +
    + @click="closeCreateTaskModal" + class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400" + > + Cancel +
diff --git a/frontend/src/tasks/tasks.js b/frontend/src/tasks/tasks.js index 9812fca..b63a53b 100644 --- a/frontend/src/tasks/tasks.js +++ b/frontend/src/tasks/tasks.js @@ -112,9 +112,8 @@ module.exports = app => app.component('tasks', { console.log('Creating task:', taskData); // await api.Task.createTask(taskData); - // Reset form and close modal - this.resetCreateTaskForm(); - this.showCreateTaskModal = false; + // Close modal (which will reset form) + this.closeCreateTaskModal(); // Refresh the task data await this.getTasks(); @@ -130,12 +129,17 @@ module.exports = app => app.component('tasks', { parameters: '' }; }, - setDefaultCreateTaskValues() { - // Set default scheduled time to 1 hour from now - const defaultTime = new Date(); - defaultTime.setHours(defaultTime.getHours() + 1); - this.newTask.scheduledAt = defaultTime.toISOString().slice(0, 16); - }, + setDefaultCreateTaskValues() { + // Set default scheduled time to 1 hour from now + const defaultTime = new Date(); + defaultTime.setHours(defaultTime.getHours() + 1); + this.newTask.scheduledAt = defaultTime.toISOString().slice(0, 16); + }, + closeCreateTaskModal() { + this.showCreateTaskModal = false; + this.resetCreateTaskForm(); + this.setDefaultCreateTaskValues(); + }, getStatusColor(status) { if (status === 'succeeded') { // Green (success) @@ -300,13 +304,6 @@ module.exports = app => app.component('tasks', { this.status = 'loaded'; this.setDefaultCreateTaskValues(); }, - watch: { - showCreateTaskModal(newVal) { - if (newVal) { - this.resetCreateTaskForm(); - this.setDefaultCreateTaskValues(); - } - } - }, + template: template }); \ No newline at end of file From a4fd90fb6cc8cdcc49295b8faffdcf8bd43a3e63 Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Fri, 29 Aug 2025 18:30:14 -0400 Subject: [PATCH 17/25] fix: lint --- .../src/tasks/task-details/task-details.js | 60 +++++++++---------- frontend/src/tasks/tasks.js | 26 ++++---- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/frontend/src/tasks/task-details/task-details.js b/frontend/src/tasks/task-details/task-details.js index 740e0f1..15348eb 100644 --- a/frontend/src/tasks/task-details/task-details.js +++ b/frontend/src/tasks/task-details/task-details.js @@ -74,37 +74,37 @@ module.exports = app => app.component('task-details', { this.$emit('update:currentFilter', status); } }, - clearFilter() { - this.$emit('update:currentFilter', null); - }, - showRescheduleConfirmation(task) { - this.selectedTask = task; - this.showRescheduleModal = true; - }, - showRunConfirmation(task) { - this.selectedTask = task; - this.showRunModal = true; - }, - async confirmRescheduleTask() { - try { - await this.rescheduleTask(this.selectedTask); - this.showRescheduleModal = false; - this.selectedTask = null; - } catch (error) { - console.error('Error in confirmRescheduleTask:', error); - } - }, - async confirmRunTask() { - try { - await this.runTask(this.selectedTask); - this.showRunModal = false; - this.selectedTask = null; - } catch (error) { - console.error('Error in confirmRunTask:', error); - } - } + clearFilter() { + this.$emit('update:currentFilter', null); + }, + showRescheduleConfirmation(task) { + this.selectedTask = task; + this.showRescheduleModal = true; + }, + showRunConfirmation(task) { + this.selectedTask = task; + this.showRunModal = true; + }, + async confirmRescheduleTask() { + try { + await this.rescheduleTask(this.selectedTask); + this.showRescheduleModal = false; + this.selectedTask = null; + } catch (error) { + console.error('Error in confirmRescheduleTask:', error); + } + }, + async confirmRunTask() { + try { + await this.runTask(this.selectedTask); + this.showRunModal = false; + this.selectedTask = null; + } catch (error) { + console.error('Error in confirmRunTask:', error); + } + } - }, + }, mounted() { // Check if the task group was already filtered when passed from parent if (this.taskGroup.filteredStatus && !this.currentFilter) { diff --git a/frontend/src/tasks/tasks.js b/frontend/src/tasks/tasks.js index b63a53b..c6016dc 100644 --- a/frontend/src/tasks/tasks.js +++ b/frontend/src/tasks/tasks.js @@ -112,8 +112,8 @@ module.exports = app => app.component('tasks', { console.log('Creating task:', taskData); // await api.Task.createTask(taskData); - // Close modal (which will reset form) - this.closeCreateTaskModal(); + // Close modal (which will reset form) + this.closeCreateTaskModal(); // Refresh the task data await this.getTasks(); @@ -129,17 +129,17 @@ module.exports = app => app.component('tasks', { parameters: '' }; }, - setDefaultCreateTaskValues() { - // Set default scheduled time to 1 hour from now - const defaultTime = new Date(); - defaultTime.setHours(defaultTime.getHours() + 1); - this.newTask.scheduledAt = defaultTime.toISOString().slice(0, 16); - }, - closeCreateTaskModal() { - this.showCreateTaskModal = false; - this.resetCreateTaskForm(); - this.setDefaultCreateTaskValues(); - }, + setDefaultCreateTaskValues() { + // Set default scheduled time to 1 hour from now + const defaultTime = new Date(); + defaultTime.setHours(defaultTime.getHours() + 1); + this.newTask.scheduledAt = defaultTime.toISOString().slice(0, 16); + }, + closeCreateTaskModal() { + this.showCreateTaskModal = false; + this.resetCreateTaskForm(); + this.setDefaultCreateTaskValues(); + }, getStatusColor(status) { if (status === 'succeeded') { // Green (success) From 29c9bdfb6ed8349a56df7a7b722cdcdf2d5796cc Mon Sep 17 00:00:00 2001 From: Daniel Diaz <39510674+IslandRhythms@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:00:30 -0400 Subject: [PATCH 18/25] complete task visualizer --- backend/actions/Task/cancelTask.js | 24 ++++++ backend/actions/Task/createTask.js | 33 ++++++++ backend/actions/Task/getTasks.js | 10 ++- backend/actions/Task/index.js | 6 +- backend/actions/Task/rescheduleTask.js | 39 +++++++++ backend/actions/Task/runTask.js | 25 ++++++ frontend/src/api.js | 24 ++++++ .../src/tasks/task-details/task-details.html | 66 ++++++++++++++- .../src/tasks/task-details/task-details.js | 40 ++++++++- frontend/src/tasks/tasks.html | 24 ++++++ frontend/src/tasks/tasks.js | 82 +++++++++++-------- 11 files changed, 331 insertions(+), 42 deletions(-) create mode 100644 backend/actions/Task/cancelTask.js create mode 100644 backend/actions/Task/createTask.js create mode 100644 backend/actions/Task/rescheduleTask.js create mode 100644 backend/actions/Task/runTask.js diff --git a/backend/actions/Task/cancelTask.js b/backend/actions/Task/cancelTask.js new file mode 100644 index 0000000..9e62343 --- /dev/null +++ b/backend/actions/Task/cancelTask.js @@ -0,0 +1,24 @@ +'use strict'; + +const Archetype = require('archetype'); +const mongoose = require('mongoose'); + +const CancelTaskParams = new Archetype({ + taskId: { + $type: mongoose.Types.ObjectId, + $required: true + } +}).compile('CancelTaskParams'); + +module.exports = ({ db }) => async function cancelTask(params) { + params = new CancelTaskParams(params); + const { taskId } = params; + const { Task } = db.models; + + const task = await Task.findOne({ _id: taskId }).orFail(); + + const cancelledTask = await Task.cancelTask({ _id: taskId }); + return { + task: cancelledTask + }; +}; \ No newline at end of file diff --git a/backend/actions/Task/createTask.js b/backend/actions/Task/createTask.js new file mode 100644 index 0000000..3639175 --- /dev/null +++ b/backend/actions/Task/createTask.js @@ -0,0 +1,33 @@ +'use strict'; + +const Archetype = require('archetype'); + +const CreateTaskParams = new Archetype({ + name: { + $type: 'string', + $required: true + }, + scheduledAt: { + $type: Date, + $required: true + }, + repeatAfterMS: { + $type: 'number' + }, + payload: { + $type: Archetype.Any + } +}).compile('CreateTaskParams'); + +module.exports = ({ db }) => async function createTask(params) { + params = new CreateTaskParams(params); + + const { name, scheduledAt, payload, repeatAfterMS } = params; + const { Task } = db.models; + + const task = await Task.schedule(name, scheduledAt, payload, repeatAfterMS); + + return { + task + }; +}; \ No newline at end of file diff --git a/backend/actions/Task/getTasks.js b/backend/actions/Task/getTasks.js index db385c6..23c1a2f 100644 --- a/backend/actions/Task/getTasks.js +++ b/backend/actions/Task/getTasks.js @@ -11,24 +11,30 @@ const GetTasksParams = new Archetype({ }, status: { $type: 'string' + }, + name: { + $type: 'string' } }).compile('GetTasksParams'); module.exports = ({ db }) => async function getTasks(params) { params = new GetTasksParams(params); - const { start, end, status } = params; + const { start, end, status, name } = params; const { Task } = db.models; const filter = {}; if (start && end) { - filter.scheduledAt = { $gte: start, $lte: end }; + filter.scheduledAt = { $gte: start, $lt: end }; } else if (start) { filter.scheduledAt = { $gte: start }; } if (status) { filter.status = status; } + if (name) { + filter.name = { $regex: name, $options: 'i' }; + } const tasks = await Task.find(filter); diff --git a/backend/actions/Task/index.js b/backend/actions/Task/index.js index 8adb3a7..4ed726e 100644 --- a/backend/actions/Task/index.js +++ b/backend/actions/Task/index.js @@ -1,3 +1,7 @@ 'use strict'; -exports.getTasks = require('./getTasks'); \ No newline at end of file +exports.cancelTask = require('./cancelTask'); +exports.createTask = require('./createTask'); +exports.getTasks = require('./getTasks'); +exports.rescheduleTask = require('./rescheduleTask'); +exports.runTask = require('./runTask'); \ No newline at end of file diff --git a/backend/actions/Task/rescheduleTask.js b/backend/actions/Task/rescheduleTask.js new file mode 100644 index 0000000..2acce1f --- /dev/null +++ b/backend/actions/Task/rescheduleTask.js @@ -0,0 +1,39 @@ +'use strict'; + +const Archetype = require('archetype'); +const mongoose = require('mongoose'); + +const RescheduleTaskParams = new Archetype({ + taskId: { + $type: mongoose.Types.ObjectId, + $required: true + }, + scheduledAt: { + $type: Date, + $required: true + } +}).compile('RescheduleTaskParams'); + +module.exports = ({ db }) => async function rescheduleTask(params) { + params = new RescheduleTaskParams(params); + const { taskId, scheduledAt } = params; + const { Task } = db.models; + + const task = await Task.findOne({ _id: taskId }).orFail(); + + if (scheduledAt < Date.now()) { + throw new Error ('Cannot reschedule a task for the past') + } + + if (task.status != 'pending') { + throw new Error('Cannot reschedule a task that is not pending') + } + + task.scheduledAt = scheduledAt; + + await task.save(); + + return { + task + }; +}; \ No newline at end of file diff --git a/backend/actions/Task/runTask.js b/backend/actions/Task/runTask.js new file mode 100644 index 0000000..9c10150 --- /dev/null +++ b/backend/actions/Task/runTask.js @@ -0,0 +1,25 @@ +'use strict'; + +const Archetype = require('archetype'); +const mongoose = require('mongoose'); + +const RunTaskParams = new Archetype({ + taskId: { + $type: mongoose.Types.ObjectId, + $required: true + } +}).compile('RunTaskParams'); + +module.exports = ({ db }) => async function runTask(params) { + params = new RunTaskParams(params); + const { taskId } = params; + const { Task } = db.models; + + const task = await Task.findOne({ _id: taskId }).orFail(); + + const executedTask = await Task.execute(task); + + return { + task: executedTask + }; +}; \ No newline at end of file diff --git a/frontend/src/api.js b/frontend/src/api.js index 62eb137..ab238da 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -131,8 +131,20 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) { } }; exports.Task = { + cancelTask: function cancelTask(params) { + return client.post('', { action: 'Task.cancelTask', ...params }).then(res => res.data); + }, + createTask: function createTask(params) { + return client.post('', { action: 'Task.createTask', ...params }).then(res => res.data); + }, getTasks: function getTasks(params) { return client.post('', { action: 'Task.getTasks', ...params }).then(res => res.data); + }, + rescheduleTask: function rescheduleTask(params) { + return client.post('', { action: 'Task.rescheduleTask', ...params }).then(res => res.data); + }, + runTask: function runTask(params) { + return client.post('', { action: 'Task.runTask', ...params }).then(res => res.data); } }; } else { @@ -241,8 +253,20 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) { } }; exports.Task = { + cancelTask: function cancelTask(params) { + return client.post('/Task/cancelTask', params).then(res => res.data); + }, + createTask: function createTask(params) { + return client.post('/Task/createTask', params).then(res => res.data); + }, getTasks: function getTasks(params) { return client.post('/Task/getTasks', params).then(res => res.data); + }, + rescheduleTask: function rescheduleTask(params) { + return client.post('/Task/rescheduleTask', params).then(res => res.data); + }, + runTask: function runTask(params) { + return client.post('/Task/runTask', params).then(res => res.data); } }; } diff --git a/frontend/src/tasks/task-details/task-details.html b/frontend/src/tasks/task-details/task-details.html index 1336ce8..e9c1370 100644 --- a/frontend/src/tasks/task-details/task-details.html +++ b/frontend/src/tasks/task-details/task-details.html @@ -125,10 +125,20 @@

:disabled="task.status === 'in_progress'" > - + Run Now +

@@ -157,6 +167,19 @@

Reschedule Task

This will reset the task's status and schedule it to run again.

+ +
+ + +
+ + + + +
diff --git a/frontend/src/tasks/task-details/task-details.js b/frontend/src/tasks/task-details/task-details.js index 15348eb..6d8e658 100644 --- a/frontend/src/tasks/task-details/task-details.js +++ b/frontend/src/tasks/task-details/task-details.js @@ -8,7 +8,9 @@ module.exports = app => app.component('task-details', { data: () => ({ showRescheduleModal: false, showRunModal: false, - selectedTask: null + showCancelModal: false, + selectedTask: null, + newScheduledTime: '' }), computed: { sortedTasks() { @@ -49,8 +51,12 @@ module.exports = app => app.component('task-details', { async rescheduleTask(task) { try { // TODO: Implement reschedule API call - console.log('Rescheduling task:', task.id); - // await api.Task.rescheduleTask(task.id); + if (!this.newScheduledTime) { + return; + } + console.log('Rescheduling task:', task.id, 'to:', this.newScheduledTime); + await api.Task.rescheduleTask({ taskId: task.id, scheduledAt: this.newScheduledTime }); + // await api.Task.rescheduleTask(task.id, { scheduledAt: this.newScheduledTime }); } catch (error) { console.error('Error rescheduling task:', error); // TODO: Add proper error handling/notification @@ -66,6 +72,16 @@ module.exports = app => app.component('task-details', { // TODO: Add proper error handling/notification } }, + async cancelTask(task) { + try { + await api.Task.cancelTask({ taskId: task.id }); + // Refresh the task data by emitting an event to the parent + this.$emit('task-cancelled'); + } catch (error) { + console.error('Error cancelling task:', error); + // TODO: Add proper error handling/notification + } + }, filterByStatus(status) { // If clicking the same status, clear the filter if (this.currentFilter === status) { @@ -79,17 +95,26 @@ module.exports = app => app.component('task-details', { }, showRescheduleConfirmation(task) { this.selectedTask = task; + // Set default time to 1 hour from now + const defaultTime = new Date(); + defaultTime.setHours(defaultTime.getHours() + 1); + this.newScheduledTime = defaultTime.toISOString().slice(0, 16); this.showRescheduleModal = true; }, showRunConfirmation(task) { this.selectedTask = task; this.showRunModal = true; }, + showCancelConfirmation(task) { + this.selectedTask = task; + this.showCancelModal = true; + }, async confirmRescheduleTask() { try { await this.rescheduleTask(this.selectedTask); this.showRescheduleModal = false; this.selectedTask = null; + this.newScheduledTime = ''; } catch (error) { console.error('Error in confirmRescheduleTask:', error); } @@ -102,6 +127,15 @@ module.exports = app => app.component('task-details', { } catch (error) { console.error('Error in confirmRunTask:', error); } + }, + async confirmCancelTask() { + try { + await this.cancelTask(this.selectedTask); + this.showCancelModal = false; + this.selectedTask = null; + } catch (error) { + console.error('Error in confirmCancelTask:', error); + } } }, diff --git a/frontend/src/tasks/tasks.html b/frontend/src/tasks/tasks.html index 4ee65e2..b263995 100644 --- a/frontend/src/tasks/tasks.html +++ b/frontend/src/tasks/tasks.html @@ -6,6 +6,7 @@ :current-filter="taskDetailsFilter" @back="hideTaskDetails" @task-created="onTaskCreated" + @task-cancelled="onTaskCancelled" @update:current-filter="taskDetailsFilter = $event" > @@ -33,6 +34,16 @@

Task Overview

+
+ + +
+
+ + +

Enter 0 or leave empty for no repetition. Use 1000 for 1 second, 60000 for 1 minute, etc.

+
+