Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
296417f
begin tasks + fix authorized views
IslandRhythms May 6, 2025
d725ccc
link to npm if no task package
IslandRhythms May 6, 2025
4c8f9dc
Merge branch 'main' into IslandRhythms/task-visualizer
IslandRhythms Jun 6, 2025
5bbc944
v1 task overview
IslandRhythms Jun 6, 2025
d32696c
remove task creator for v1
IslandRhythms Jun 6, 2025
3d920a0
Merge branch 'main' into IslandRhythms/task-visualizer
vkarpov15 Jun 30, 2025
9ddb57c
make requested changes
IslandRhythms Aug 29, 2025
fe02cfd
Merge branch 'main' into IslandRhythms/task-visualizer
IslandRhythms Aug 29, 2025
974a9b8
cleanup warnings and errors in console
IslandRhythms Aug 29, 2025
538b843
fix: lint
IslandRhythms Aug 29, 2025
86d3e59
fix css
IslandRhythms Aug 29, 2025
451045b
Update navbar.html
IslandRhythms Aug 29, 2025
5f57994
task details
IslandRhythms Aug 29, 2025
7f04242
more frontend work
IslandRhythms Aug 29, 2025
386b5a8
cleanup
IslandRhythms Aug 29, 2025
6e104bb
UI polishing
IslandRhythms Aug 29, 2025
4cd32ea
missed a spot
IslandRhythms Aug 29, 2025
c066231
remove unused functions
IslandRhythms Aug 29, 2025
9e2f44d
remove unnecessary watcher
IslandRhythms Aug 29, 2025
a4fd90f
fix: lint
IslandRhythms Aug 29, 2025
29c9bdf
complete task visualizer
IslandRhythms Sep 2, 2025
684d890
fix: lint
IslandRhythms Sep 2, 2025
8a27ee2
Update eslint.config.js
IslandRhythms Sep 26, 2025
24dd8ca
fix: model navigation not working
IslandRhythms Oct 2, 2025
c55eba7
Update models.js
IslandRhythms Oct 2, 2025
8b3943a
Merge branch 'main' into IslandRhythms/task-visualizer
vkarpov15 Oct 3, 2025
fd26468
manually fix lint
IslandRhythms Oct 7, 2025
57da3ce
Merge branch 'main' into IslandRhythms/task-visualizer
IslandRhythms Oct 15, 2025
5a994ee
better UX
IslandRhythms Oct 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/actions/Model/updateDocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ module.exports = ({ db }) => async function updateDocument(params) {
throw new Error(`Model ${model} not found`);
}

let setFields = {};
let unsetFields = {};
const setFields = {};
const unsetFields = {};

if (Object.keys(update).length > 0) {
Object.entries(update).forEach(([key, value]) => {
Expand Down
4 changes: 2 additions & 2 deletions backend/actions/Model/updateDocuments.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ module.exports = ({ db }) => async function updateDocuments(params) {
throw new Error(`Model ${model} not found`);
}

let setFields = {};
let unsetFields = {};
const setFields = {};
const unsetFields = {};

if (Object.keys(update).length > 0) {
Object.entries(update).forEach(([key, value]) => {
Expand Down
24 changes: 24 additions & 0 deletions backend/actions/Task/cancelTask.js
Original file line number Diff line number Diff line change
@@ -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
};
};
33 changes: 33 additions & 0 deletions backend/actions/Task/createTask.js
Original file line number Diff line number Diff line change
@@ -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
};
};
62 changes: 62 additions & 0 deletions backend/actions/Task/getTasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict';

const Archetype = require('archetype');

const GetTasksParams = new Archetype({
start: {
$type: Date
},
end: {
$type: Date
},
status: {
$type: 'string'
},
name: {
$type: 'string'
}
}).compile('GetTasksParams');

module.exports = ({ db }) => async function getTasks(params) {
params = new GetTasksParams(params);
const { start, end, status, name } = params;
const { Task } = db.models;

const filter = {};

if (start && 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);

// 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,
groupedTasks
};
};
7 changes: 7 additions & 0 deletions backend/actions/Task/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

exports.cancelTask = require('./cancelTask');
exports.createTask = require('./createTask');
exports.getTasks = require('./getTasks');
exports.rescheduleTask = require('./rescheduleTask');
exports.runTask = require('./runTask');
39 changes: 39 additions & 0 deletions backend/actions/Task/rescheduleTask.js
Original file line number Diff line number Diff line change
@@ -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
};
};
25 changes: 25 additions & 0 deletions backend/actions/Task/runTask.js
Original file line number Diff line number Diff line change
@@ -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
};
};
1 change: 1 addition & 0 deletions backend/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ exports.Dashboard = require('./Dashboard');
exports.Model = require('./Model');
exports.Script = require('./Script');
exports.status = require('./status');
exports.Task = require('./Task');
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module.exports = defineConfig([
fetch: true,
__dirname: true,
process: true,
clearTimeout: true,
setTimeout: true,
navigator: true,
TextDecoder: true
Expand Down
1 change: 1 addition & 0 deletions express.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ module.exports = async function(apiUrl, conn, options) {

console.log('Workspace', workspace);
const { config } = await frontend(apiUrl, false, options, workspace);
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)};`);
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,23 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
return client.post('', { action: 'Model.updateDocuments', ...params }).then(res => res.data);
}
};
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 {
exports.status = function status() {
return client.get('/status').then(res => res.data);
Expand Down Expand Up @@ -298,4 +315,21 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
return client.post('/Model/updateDocuments', params).then(res => res.data);
}
};
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);
}
};
}
6 changes: 3 additions & 3 deletions frontend/src/document-details/document-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,9 @@ module.exports = app => app.component('document-details', {
toSnakeCase(str) {
return str
.trim()
.replace(/\s+/g, '_') // Replace spaces with underscores
.replace(/[^a-zA-Z0-9_$]/g, '') // Remove invalid characters
.replace(/^[0-9]/, '_$&') // Prefix numbers with underscore
.replace(/\s+/g, '_') // Replace spaces with underscores
.replace(/[^a-zA-Z0-9_$]/g, '') // Remove invalid characters
.replace(/^[0-9]/, '_$&') // Prefix numbers with underscore
.toLowerCase();
},
getTransformedFieldName() {
Expand Down
37 changes: 36 additions & 1 deletion frontend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ console.log(`Mongoose Studio Version ${version}`);
const api = require('./api');
const format = require('./format');
const mothership = require('./mothership');
const { routes } = require('./routes');
const { routes, hasAccess } = require('./routes');
const vanillatoasts = require('vanillatoasts');

const app = Vue.createApp({
Expand Down Expand Up @@ -141,6 +141,41 @@ const router = VueRouter.createRouter({
}))
});

// Add global navigation guard
router.beforeEach((to, from, next) => {
// Skip auth check for authorized (public) routes
if (to.meta.authorized) {
Comment on lines +146 to +147
Copy link

Copilot AI Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global navigation guard's logic appears inverted: routes marked as 'authorized' are immediately permitted without an access check, while those with 'authorized' set to false require role validation. Please verify that the flag's semantics and its usage in routes match the intended access control behavior.

Suggested change
// Skip auth check for authorized (public) routes
if (to.meta.authorized) {
// Skip auth check for public routes (not authorized)
if (!to.meta.authorized) {

Copilot uses AI. Check for mistakes.
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;
}

if (to.name === 'root' && roles && roles[0] === 'dashboards') {
return next({ name: 'dashboards' });
}

next();
});

router.beforeEach((to, from, next) => {
if (to.name === 'root' && window.state.roles && window.state.roles[0] === 'dashboards') {
return next({ name: 'dashboards' });
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/models/models.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ td {
.models .documents-menu {
position: fixed;
background-color: white;
z-index: 1;
z-index: 10;
padding: 4px;
display: flex;
width: 100vw;
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/navbar/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
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</a>
<span v-else class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium text-gray-300 cursor-not-allowed">
<span v-else class="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-500 cursor-not-allowed">
Dashboards
<svg class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
Expand All @@ -39,6 +39,10 @@
<path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
</svg>
</span>
<a
:href="hasTaskVisualizer"
class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium"
:class="taskView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'">Tasks</a>

<div class="h-full flex items-center" v-if="!user && hasAPIKey">
<button
Expand Down
Loading