@mongoosejs/studio 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/DEVGUIDE.md +8 -0
  2. package/backend/actions/ChatThread/createChatMessage.js +2 -0
  3. package/backend/actions/ChatThread/streamChatMessage.js +2 -0
  4. package/backend/actions/Dashboard/getDashboard.js +15 -11
  5. package/backend/actions/Dashboard/updateDashboard.js +2 -2
  6. package/backend/actions/Task/getTaskOverview.js +102 -0
  7. package/backend/actions/Task/getTasks.js +85 -45
  8. package/backend/actions/Task/index.js +1 -0
  9. package/frontend/public/app.js +318 -148
  10. package/frontend/public/tw.css +82 -0
  11. package/frontend/src/_util/dateRange.js +82 -0
  12. package/frontend/src/ace-editor/ace-editor.js +6 -0
  13. package/frontend/src/api.js +6 -0
  14. package/frontend/src/chat/chat-message-script/chat-message-script.html +11 -16
  15. package/frontend/src/chat/chat-message-script/chat-message-script.js +0 -6
  16. package/frontend/src/chat/chat.js +2 -0
  17. package/frontend/src/dashboard/dashboard.js +13 -2
  18. package/frontend/src/dashboard/edit-dashboard/edit-dashboard.js +4 -3
  19. package/frontend/src/dashboard-result/dashboard-result.js +3 -0
  20. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.html +34 -0
  21. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.js +37 -0
  22. package/frontend/src/models/models.js +9 -3
  23. package/frontend/src/navbar/navbar.js +3 -2
  24. package/frontend/src/task-by-name/task-by-name.html +77 -7
  25. package/frontend/src/task-by-name/task-by-name.js +84 -9
  26. package/frontend/src/tasks/task-details/task-details.html +8 -8
  27. package/frontend/src/tasks/task-details/task-details.js +2 -1
  28. package/frontend/src/tasks/tasks.js +25 -118
  29. package/local.js +38 -0
  30. package/package.json +4 -1
  31. package/seed/connect.js +23 -0
  32. package/seed/index.js +101 -0
@@ -3,6 +3,7 @@
3
3
  // Page: all tasks with a given name. Reuses task-details to render the list (many tasks).
4
4
  const template = require('./task-by-name.html');
5
5
  const api = require('../api');
6
+ const { DATE_FILTERS, DATE_FILTER_VALUES, getDateRangeForRange } = require('../_util/dateRange');
6
7
 
7
8
  function buildTaskGroup(name, tasks) {
8
9
  const statusCounts = { pending: 0, succeeded: 0, failed: 0, cancelled: 0, in_progress: 0, unknown: 0 };
@@ -26,44 +27,118 @@ function buildTaskGroup(name, tasks) {
26
27
  };
27
28
  }
28
29
 
30
+ const PAGE_SIZE_OPTIONS = [25, 50, 100, 200];
31
+
29
32
  module.exports = app => app.component('task-by-name', {
30
33
  template,
31
34
  data: () => ({
32
35
  status: 'init',
33
36
  taskGroup: null,
34
- errorMessage: ''
37
+ errorMessage: '',
38
+ selectedRange: 'last_hour',
39
+ start: null,
40
+ end: null,
41
+ dateFilters: DATE_FILTERS,
42
+ page: 1,
43
+ pageSize: 50,
44
+ numDocs: 0,
45
+ pageSizeOptions: PAGE_SIZE_OPTIONS,
46
+ _loadId: 0,
47
+ _lastQueryFilters: null
35
48
  }),
36
49
  computed: {
37
50
  taskName() {
38
51
  return this.$route.params.name || '';
39
52
  }
40
53
  },
54
+ created() {
55
+ const fromQuery = this.$route.query.dateRange;
56
+ this.selectedRange = (fromQuery && DATE_FILTER_VALUES.includes(fromQuery)) ? fromQuery : 'last_hour';
57
+ if (!this.$route.query.dateRange) {
58
+ this.$router.replace({
59
+ path: this.$route.path,
60
+ query: { ...this.$route.query, dateRange: this.selectedRange }
61
+ });
62
+ }
63
+ },
41
64
  watch: {
42
65
  taskName: {
43
66
  immediate: true,
44
67
  handler() {
68
+ this.page = 1;
45
69
  this.loadTasks();
46
70
  }
71
+ },
72
+ '$route.query': {
73
+ handler(query) {
74
+ const dateRange = query.dateRange;
75
+ if (dateRange && DATE_FILTER_VALUES.includes(dateRange) && this.selectedRange !== dateRange) {
76
+ this.selectedRange = dateRange;
77
+ }
78
+ const effectiveDateRange = (dateRange && DATE_FILTER_VALUES.includes(dateRange)) ? dateRange : (this.selectedRange || 'last_hour');
79
+ const effectiveStatus = query.status ?? '';
80
+ const key = `${effectiveDateRange}|${effectiveStatus}`;
81
+ if (this._lastQueryFilters === key) return;
82
+ this.page = 1;
83
+ this.loadTasks();
84
+ },
85
+ deep: true
47
86
  }
48
87
  },
49
88
  methods: {
89
+ updateDateRange() {
90
+ this.page = 1;
91
+ this.$router.replace({
92
+ path: this.$route.path,
93
+ query: { ...this.$route.query, dateRange: this.selectedRange }
94
+ });
95
+ },
96
+ goToPage(page) {
97
+ const maxPage = Math.max(1, Math.ceil(this.numDocs / this.pageSize));
98
+ const next = Math.max(1, Math.min(page, maxPage));
99
+ if (next === this.page) return;
100
+ this.page = next;
101
+ this.loadTasks();
102
+ },
103
+ onPageSizeChange() {
104
+ this.page = 1;
105
+ this.loadTasks();
106
+ },
50
107
  async loadTasks() {
51
108
  if (!this.taskName) return;
109
+ const loadId = ++this._loadId;
52
110
  this.status = 'init';
53
111
  this.taskGroup = null;
54
112
  this.errorMessage = '';
55
- const start = new Date();
56
- start.setHours(start.getHours() - 1);
57
- const end = new Date();
113
+ const dateRangeFromQuery = this.$route.query.dateRange;
114
+ const dateRange = (dateRangeFromQuery && DATE_FILTER_VALUES.includes(dateRangeFromQuery))
115
+ ? dateRangeFromQuery
116
+ : (this.selectedRange || 'last_hour');
117
+ this.selectedRange = dateRange;
118
+ const { start, end } = getDateRangeForRange(dateRange);
119
+ this.start = start;
120
+ this.end = end;
121
+ const skip = (this.page - 1) * this.pageSize;
122
+ const params = {
123
+ name: this.taskName,
124
+ start: start instanceof Date ? start.toISOString() : start,
125
+ end: end instanceof Date ? end.toISOString() : end,
126
+ skip,
127
+ limit: this.pageSize
128
+ };
129
+ const statusFromQuery = this.$route.query.status;
130
+ if (statusFromQuery) params.status = statusFromQuery;
131
+ this._lastQueryFilters = `${dateRange}|${statusFromQuery ?? ''}`;
58
132
  try {
59
- const { tasks } = await api.Task.getTasks({
60
- name: this.taskName,
61
- start,
62
- end
63
- });
133
+ const { tasks, numDocs, statusCounts } = await api.Task.getTasks(params);
134
+ if (loadId !== this._loadId) return;
135
+ this.numDocs = numDocs ?? tasks.length;
64
136
  this.taskGroup = buildTaskGroup(this.taskName, tasks);
137
+ this.taskGroup.totalCount = this.numDocs;
138
+ if (statusCounts) this.taskGroup.statusCounts = statusCounts;
65
139
  this.status = 'loaded';
66
140
  } catch (err) {
141
+ if (loadId !== this._loadId) return;
67
142
  this.status = 'error';
68
143
  this.errorMessage = err?.response?.data?.message || err.message || 'Failed to load tasks';
69
144
  }
@@ -1,14 +1,14 @@
1
1
  <div class="p-4 space-y-6">
2
2
  <div class="flex items-center justify-between">
3
3
  <div>
4
- <button @click="goBack" class="text-content-tertiary hover:text-content-secondary mb-2">
5
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
7
- </svg>
8
- {{ backLabel }}
9
- </button>
10
- <h1 class="text-2xl font-bold text-content-secondary">{{ taskGroup.name }}</h1>
11
- <p class="text-content-tertiary">Total: {{ taskGroup.totalCount }} tasks</p>
4
+ <button v-if="showBackButton" @click="goBack" class="text-content-tertiary hover:text-content-secondary mb-2">
5
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
7
+ </svg>
8
+ {{ backLabel }}
9
+ </button>
10
+ <h1 class="text-2xl font-bold text-content-secondary">{{ taskGroup.name }}</h1>
11
+ <p class="text-content-tertiary">Total: {{ taskGroup.totalCount }} tasks</p>
12
12
  </div>
13
13
 
14
14
  </div>
@@ -10,7 +10,8 @@ const PIE_HOVER = ['#ca8a04', '#16a34a', '#dc2626', '#4b5563'];
10
10
  module.exports = app => app.component('task-details', {
11
11
  props: {
12
12
  taskGroup: { type: Object, required: true },
13
- backTo: { type: Object, default: null }
13
+ backTo: { type: Object, default: null },
14
+ showBackButton: { type: Boolean, default: true }
14
15
  },
15
16
  data: () => ({
16
17
  currentFilter: null,
@@ -2,25 +2,17 @@
2
2
 
3
3
  const template = require('./tasks.html');
4
4
  const api = require('../api');
5
+ const { DATE_FILTERS, getDateRangeForRange } = require('../_util/dateRange');
5
6
 
6
7
  module.exports = app => app.component('tasks', {
7
8
  data: () => ({
8
9
  status: 'init',
9
- tasks: [],
10
- groupedTasks: {},
10
+ statusCounts: { pending: 0, succeeded: 0, failed: 0, cancelled: 0 },
11
+ tasksByName: [],
11
12
  selectedRange: 'last_hour',
12
13
  start: null,
13
14
  end: null,
14
- dateFilters: [
15
- { value: 'all', label: 'All Time' },
16
- { value: 'last_hour', label: 'Last Hour' },
17
- { value: 'today', label: 'Today' },
18
- { value: 'yesterday', label: 'Yesterday' },
19
- { value: 'thisWeek', label: 'This Week' },
20
- { value: 'lastWeek', label: 'Last Week' },
21
- { value: 'thisMonth', label: 'This Month' },
22
- { value: 'lastMonth', label: 'Last Month' }
23
- ],
15
+ dateFilters: DATE_FILTERS,
24
16
  selectedStatus: 'all',
25
17
  statusFilters: [
26
18
  { label: 'All', value: 'all' },
@@ -50,26 +42,31 @@ module.exports = app => app.component('tasks', {
50
42
  params.status = this.selectedStatus;
51
43
  }
52
44
 
53
- if (this.start && this.end) {
54
- params.start = this.start;
55
- params.end = this.end;
56
- } else if (this.start) {
57
- params.start = this.start;
45
+ if (this.start != null && this.end != null) {
46
+ params.start = this.start instanceof Date ? this.start.toISOString() : this.start;
47
+ params.end = this.end instanceof Date ? this.end.toISOString() : this.end;
48
+ } else if (this.start != null) {
49
+ params.start = this.start instanceof Date ? this.start.toISOString() : this.start;
58
50
  }
59
51
 
60
52
  if (this.searchQuery.trim()) {
61
53
  params.name = this.searchQuery.trim();
62
54
  }
63
55
 
64
- const { tasks, groupedTasks } = await api.Task.getTasks(params);
65
- this.tasks = tasks;
66
- this.groupedTasks = groupedTasks;
56
+ const { statusCounts, tasksByName } = await api.Task.getTaskOverview(params);
57
+ this.statusCounts = statusCounts || this.statusCounts;
58
+ this.tasksByName = tasksByName || [];
67
59
  },
68
60
  openTaskGroupDetails(group) {
69
- this.$router.push({ path: `/tasks/${encodeURIComponent(group.name || '')}` });
61
+ const query = { dateRange: this.selectedRange || 'last_hour' };
62
+ if (this.selectedStatus && this.selectedStatus !== 'all') query.status = this.selectedStatus;
63
+ this.$router.push({ path: `/tasks/${encodeURIComponent(group.name || '')}`, query });
70
64
  },
71
65
  openTaskGroupDetailsWithFilter(group, status) {
72
- this.$router.push({ path: `/tasks/${encodeURIComponent(group.name || '')}`, query: status ? { status } : {} });
66
+ const query = { dateRange: this.selectedRange || 'last_hour' };
67
+ if (status) query.status = status;
68
+ else if (this.selectedStatus && this.selectedStatus !== 'all') query.status = this.selectedStatus;
69
+ this.$router.push({ path: `/tasks/${encodeURIComponent(group.name || '')}`, query });
73
70
  },
74
71
  async onTaskCreated() {
75
72
  // Refresh the task data when a new task is created
@@ -213,114 +210,24 @@ module.exports = app => app.component('tasks', {
213
210
  }, 300);
214
211
  },
215
212
  async updateDateRange() {
216
- const now = new Date();
217
- let start, end;
218
-
219
- switch (this.selectedRange) {
220
- case 'last_hour':
221
- start = new Date();
222
- start.setHours(start.getHours() - 1);
223
- end = new Date();
224
- break;
225
- case 'today':
226
- start = new Date();
227
- start.setHours(0, 0, 0, 0);
228
- end = new Date();
229
- end.setHours(23, 59, 59, 999);
230
- break;
231
- case 'yesterday':
232
- start = new Date();
233
- start.setDate(start.getDate() - 1);
234
- start.setHours(0, 0, 0, 0);
235
- end = new Date();
236
- break;
237
- case 'thisWeek':
238
- start = new Date(now.getTime() - (7 * 86400000));
239
- start.setHours(0, 0, 0, 0);
240
- end = new Date();
241
- end.setHours(23, 59, 59, 999);
242
- break;
243
- case 'lastWeek':
244
- start = new Date(now.getTime() - (14 * 86400000));
245
- start.setHours(0, 0, 0, 0);
246
- end = new Date(now.getTime() - (7 * 86400000));
247
- end.setHours(23, 59, 59, 999);
248
- break;
249
- case 'thisMonth':
250
- start = new Date(now.getFullYear(), now.getMonth(), 1);
251
- end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
252
- break;
253
- case 'lastMonth':
254
- start = new Date(now.getFullYear(), now.getMonth() - 1, 1);
255
- end = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
256
- break;
257
- case 'all':
258
- default:
259
- this.start = null;
260
- this.end = null;
261
- break;
262
- }
263
-
213
+ const { start, end } = getDateRangeForRange(this.selectedRange);
264
214
  this.start = start;
265
215
  this.end = end;
266
-
267
216
  await this.getTasks();
268
217
  }
269
218
  },
270
219
  computed: {
271
- tasksByName() {
272
- const groups = {};
273
-
274
- // Process tasks from groupedTasks to create name-based groups
275
- Object.entries(this.groupedTasks).forEach(([status, tasks]) => {
276
- tasks.forEach(task => {
277
- if (!groups[task.name]) {
278
- groups[task.name] = {
279
- name: task.name,
280
- tasks: [],
281
- statusCounts: {
282
- pending: 0,
283
- succeeded: 0,
284
- failed: 0,
285
- cancelled: 0
286
- },
287
- totalCount: 0,
288
- lastRun: null
289
- };
290
- }
291
-
292
- groups[task.name].tasks.push(task);
293
- groups[task.name].totalCount++;
294
-
295
- // Count status using the status from groupedTasks
296
- if (groups[task.name].statusCounts.hasOwnProperty(status)) {
297
- groups[task.name].statusCounts[status]++;
298
- }
299
-
300
- // Track last run time
301
- const taskTime = new Date(task.scheduledAt || task.createdAt || 0);
302
- if (!groups[task.name].lastRun || taskTime > new Date(groups[task.name].lastRun)) {
303
- groups[task.name].lastRun = taskTime;
304
- }
305
- });
306
- });
307
-
308
- // Convert to array and sort alphabetically by name
309
- return Object.values(groups).sort((a, b) => {
310
- return a.name.localeCompare(b.name);
311
- });
220
+ pendingCount() {
221
+ return this.statusCounts.pending || 0;
312
222
  },
313
223
  succeededCount() {
314
- return this.groupedTasks.succeeded ? this.groupedTasks.succeeded.length : 0;
224
+ return this.statusCounts.succeeded || 0;
315
225
  },
316
226
  failedCount() {
317
- return this.groupedTasks.failed ? this.groupedTasks.failed.length : 0;
227
+ return this.statusCounts.failed || 0;
318
228
  },
319
229
  cancelledCount() {
320
- return this.groupedTasks.cancelled ? this.groupedTasks.cancelled.length : 0;
321
- },
322
- pendingCount() {
323
- return this.groupedTasks.pending ? this.groupedTasks.pending.length : 0;
230
+ return this.statusCounts.cancelled || 0;
324
231
  }
325
232
  },
326
233
  mounted: async function() {
package/local.js ADDED
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const mongoose = require('mongoose');
4
+ const connect = require('./seed/connect');
5
+
6
+ const express = require('express');
7
+ const studio = require('./express');
8
+
9
+ const getModelDescriptions = require('./backend/helpers/getModelDescriptions');
10
+
11
+ Error.stackTraceLimit = 25;
12
+
13
+ run().catch(err => {
14
+ console.error(err);
15
+ process.exit(-1);
16
+ });
17
+
18
+ async function run() {
19
+ const app = express();
20
+ await connect();
21
+
22
+ console.log(getModelDescriptions(mongoose.connection));
23
+
24
+ app.use('/studio', await studio(
25
+ null,
26
+ null,
27
+ {
28
+ __build: true,
29
+ __watch: process.env.WATCH,
30
+ _mothershipUrl: 'http://localhost:7777/.netlify/functions',
31
+ apiKey: 'TACO',
32
+ openAIAPIKey: process.env.OPENAI_API_KEY
33
+ })
34
+ );
35
+
36
+ await app.listen(3333);
37
+ console.log('Listening on port 3333');
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.",
5
5
  "homepage": "https://mongoosestudio.app/",
6
6
  "repository": {
@@ -20,6 +20,7 @@
20
20
  "extrovert": "^0.2.0",
21
21
  "marked": "15.0.12",
22
22
  "node-inspect-extracted": "3.x",
23
+ "regexp.escape": "^2.0.1",
23
24
  "tailwindcss": "3.4.0",
24
25
  "vue": "3.x",
25
26
  "vue-toastification": "^2.0.0-rc.5",
@@ -45,6 +46,8 @@
45
46
  },
46
47
  "scripts": {
47
48
  "lint": "eslint .",
49
+ "seed": "node seed/index.js",
50
+ "start": "node ./local.js",
48
51
  "tailwind": "tailwindcss -o ./frontend/public/tw.css",
49
52
  "tailwind:watch": "tailwindcss -o ./frontend/public/tw.css --watch",
50
53
  "test": "mocha test/*.test.js",
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const mongoose = require('mongoose');
4
+
5
+ const uri = 'mongodb://127.0.0.1:27017/mongoose_studio_test';
6
+
7
+ module.exports = async function connect() {
8
+ await mongoose.connect(uri);
9
+
10
+ mongoose.model('User', new mongoose.Schema({
11
+ name: String,
12
+ email: String,
13
+ role: String,
14
+ plan: String,
15
+ status: String,
16
+ isDeleted: Boolean,
17
+ lastLoginAt: Date,
18
+ createdAt: Date,
19
+ updatedAt: Date
20
+ }, { collection: 'User' }));
21
+
22
+ return mongoose.connection;
23
+ };
package/seed/index.js ADDED
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const connect = require('./connect');
4
+ const mongoose = require('mongoose');
5
+
6
+ if (require.main === module) {
7
+ run().catch(err => {
8
+ console.error(err);
9
+ process.exit(1);
10
+ });
11
+ }
12
+
13
+ async function run() {
14
+ await connect();
15
+ const { User } = mongoose.models;
16
+ const dashboardCollection = mongoose.connection.collection('studio__dashboards');
17
+ const now = new Date();
18
+
19
+ const users = [
20
+ {
21
+ name: 'Ada Lovelace',
22
+ email: 'ada@example.com',
23
+ role: 'admin',
24
+ plan: 'enterprise',
25
+ status: 'active',
26
+ isDeleted: false,
27
+ lastLoginAt: new Date('2026-03-11T14:21:00.000Z'),
28
+ createdAt: new Date('2025-10-03T09:15:00.000Z'),
29
+ updatedAt: now
30
+ },
31
+ {
32
+ name: 'Grace Hopper',
33
+ email: 'grace@example.com',
34
+ role: 'analyst',
35
+ plan: 'pro',
36
+ status: 'active',
37
+ isDeleted: false,
38
+ lastLoginAt: new Date('2026-03-10T17:42:00.000Z'),
39
+ createdAt: new Date('2025-11-16T13:05:00.000Z'),
40
+ updatedAt: now
41
+ },
42
+ {
43
+ name: 'Linus Torvalds',
44
+ email: 'linus@example.com',
45
+ role: 'editor',
46
+ plan: 'starter',
47
+ status: 'invited',
48
+ isDeleted: false,
49
+ lastLoginAt: null,
50
+ createdAt: new Date('2026-01-09T11:30:00.000Z'),
51
+ updatedAt: now
52
+ },
53
+ {
54
+ name: 'Margaret Hamilton',
55
+ email: 'margaret@example.com',
56
+ role: 'viewer',
57
+ plan: 'pro',
58
+ status: 'inactive',
59
+ isDeleted: false,
60
+ lastLoginAt: new Date('2026-02-22T08:00:00.000Z'),
61
+ createdAt: new Date('2025-12-01T16:20:00.000Z'),
62
+ updatedAt: now
63
+ }
64
+ ];
65
+
66
+ const dashboard = {
67
+ title: 'User directory table',
68
+ description: 'Sample dashboard for testing the dashboard-table component.',
69
+ code: `const users = await db.model('User')
70
+ .find({ isDeleted: false })
71
+ .sort({ createdAt: -1 })
72
+ .lean();
73
+
74
+ return {
75
+ $table: {
76
+ columns: ['Name', 'Email', 'Role', 'Plan', 'Status', 'Last Login'],
77
+ rows: users.map(user => [
78
+ user.name,
79
+ user.email,
80
+ user.role,
81
+ user.plan,
82
+ user.status,
83
+ user.lastLoginAt ? new Date(user.lastLoginAt).toISOString().slice(0, 10) : 'Never'
84
+ ])
85
+ }
86
+ };`
87
+ };
88
+
89
+ await User.deleteMany({ email: { $in: users.map(user => user.email) } });
90
+ await dashboardCollection.deleteMany({ title: dashboard.title });
91
+
92
+ const insertedUsers = await User.insertMany(users);
93
+ const insertedDashboard = await dashboardCollection.insertOne(dashboard);
94
+
95
+ console.log(`Inserted ${insertedUsers.length} users`);
96
+ console.log(`Inserted dashboard ${insertedDashboard.insertedId.toString()}`);
97
+
98
+ await mongoose.disconnect();
99
+ }
100
+
101
+ module.exports = run;