diff --git a/backend/actions/Dashboard/createDashboard.js b/backend/actions/Dashboard/createDashboard.js
index dd5d0dab..6aa32045 100644
--- a/backend/actions/Dashboard/createDashboard.js
+++ b/backend/actions/Dashboard/createDashboard.js
@@ -2,6 +2,7 @@
const Archetype = require('archetype');
const authorize = require('../../authorize');
+const mongoose = require('mongoose');
const CreateDashboardParams = new Archetype({
title: {
@@ -12,18 +13,29 @@ const CreateDashboardParams = new Archetype({
$type: 'string',
$required: true
},
+ initiatedById: {
+ $type: mongoose.Types.ObjectId,
+ $required: false
+ },
roles: {
$type: ['string']
}
}).compile('CreateDashboardParams');
module.exports = ({ studioConnection }) => async function createDashboard(params) {
- const { title, code, roles } = new CreateDashboardParams(params);
+ const { title, code, initiatedById, roles } = new CreateDashboardParams(params);
const Dashboard = studioConnection.model('__Studio_Dashboard');
await authorize('Dashboard.createDashboard', roles);
- const dashboard = await Dashboard.create({ title, code });
+ console.log('Test', params)
+
+ const dashboard = await Dashboard.create({
+ title,
+ code,
+ createdById: initiatedById,
+ createdBy: params.initiatedBy
+ });
return { dashboard };
};
diff --git a/backend/actions/Dashboard/getDashboard.js b/backend/actions/Dashboard/getDashboard.js
index 1257d6f6..b9531bca 100644
--- a/backend/actions/Dashboard/getDashboard.js
+++ b/backend/actions/Dashboard/getDashboard.js
@@ -51,6 +51,7 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
return {};
}
return completeDashboardEvaluate(
+ Dashboard,
DashboardResult,
dashboardResult._id,
null,
@@ -73,6 +74,7 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
return {};
}
return completeDashboardEvaluate(
+ Dashboard,
DashboardResult,
dashboardResult._id,
result,
@@ -97,13 +99,15 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
}
};
-async function completeDashboardEvaluate(DashboardResult, dashboardResultId, result, error, status) {
+async function completeDashboardEvaluate(Dashboard, DashboardResult, dashboardResultId, result, error, status) {
const dashboardResult = await DashboardResult.findById(dashboardResultId).orFail();
- dashboardResult.finishedEvaluatingAt = new Date();
+ const finishedEvaluatingAt = new Date();
+ dashboardResult.finishedEvaluatingAt = finishedEvaluatingAt;
dashboardResult.result = result;
dashboardResult.error = error;
dashboardResult.status = status;
await dashboardResult.save();
+ await Dashboard.updateOne({ _id: dashboardResult.dashboardId }, { $set: { lastEvaluatedAt: finishedEvaluatingAt } });
return { dashboardResult };
}
diff --git a/backend/actions/Dashboard/getDashboards.js b/backend/actions/Dashboard/getDashboards.js
index f7e942a6..db40cff5 100644
--- a/backend/actions/Dashboard/getDashboards.js
+++ b/backend/actions/Dashboard/getDashboards.js
@@ -15,7 +15,7 @@ module.exports = ({ studioConnection }) => async function getDashboards(params)
await authorize('Dashboard.getDashboards', roles);
- const dashboards = await Dashboard.find();
+ const dashboards = await Dashboard.find().sort({ createdAt: -1, _id: -1 }).lean();
return { dashboards };
};
diff --git a/backend/db/dashboardSchema.js b/backend/db/dashboardSchema.js
index 229674de..80e495c2 100644
--- a/backend/db/dashboardSchema.js
+++ b/backend/db/dashboardSchema.js
@@ -13,6 +13,30 @@ const dashboardSchema = new mongoose.Schema({
},
description: {
type: String
+ },
+ createdById: {
+ type: mongoose.Schema.Types.ObjectId
+ },
+ createdBy: {
+ name: String,
+ email: String
+ },
+ lastEvaluatedAt: {
+ type: Date
+ }
+}, { timestamps: true });
+
+dashboardSchema.post(['find', 'findOne'], async function(docs) {
+ const dashboards = Array.isArray(docs) ? docs : [docs];
+ for (const dashboard of dashboards) {
+ if (dashboard != null && dashboard.createdAt == null && dashboard._id?.getTimestamp) {
+ dashboard.createdAt = dashboard._id.getTimestamp();
+ await this.model.db.model('__Studio_Dashboard').updateOne(
+ { _id: dashboard._id },
+ { createdAt: dashboard._id.getTimestamp() },
+ { overwriteImmutable: true }
+ );
+ }
}
});
diff --git a/backend/netlify.js b/backend/netlify.js
index 7993071c..742b386e 100644
--- a/backend/netlify.js
+++ b/backend/netlify.js
@@ -63,6 +63,7 @@ module.exports = function netlify(conn, options) {
params.roles = roles;
params.userId = user._id;
params.initiatedById = user._id;
+ params.initiatedBy = user;
}
if (typeof actionName !== 'string') {
diff --git a/backend/next.js b/backend/next.js
index 9a4b77fb..39833179 100644
--- a/backend/next.js
+++ b/backend/next.js
@@ -64,6 +64,7 @@ module.exports = function next(conn, options) {
params.roles = roles;
params.userId = user._id;
params.initiatedById = user._id;
+ params.initiatedBy = user;
}
if (typeof actionName !== 'string') {
diff --git a/express.js b/express.js
index 506c069f..2d742082 100644
--- a/express.js
+++ b/express.js
@@ -87,6 +87,7 @@ module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options)
req._internals.initiatedById = user._id;
req._internals.roles = roles;
req._internals.$workspaceId = workspace._id;
+ req._internals.initiatedBy = user;
next();
})
diff --git a/frontend/src/dashboards/dashboards.html b/frontend/src/dashboards/dashboards.html
index a7433f1f..cfffcdb9 100644
--- a/frontend/src/dashboards/dashboards.html
+++ b/frontend/src/dashboards/dashboards.html
@@ -10,6 +10,23 @@
Dashboards
+
+
+
+
No matching dashboards
+
Try a different title or description.
+
+
- {{ dashboard.title }}
+
-
{{ dashboard.description }}
+
+
+
+
- Created At
+ - {{ formatDate(dashboard.createdAt) }}
+
+
+
- Last Evaluated At
+ - {{ formatDate(dashboard.lastEvaluatedAt) }}
+
+
+
- Created By
+ - {{ dashboard.createdBy?.name }}
+
+
app.component('dashboards', {
data: () => ({
status: 'loading',
dashboards: [],
+ searchText: '',
showCreateDashboardModal: false,
showDeleteDashboardModal: null,
openMenuId: null
}),
+ computed: {
+ filteredDashboards() {
+ const searchText = this.searchText.trim().toLowerCase();
+ if (!searchText) {
+ return this.dashboards;
+ }
+
+ return this.dashboards.filter(dashboard => {
+ return (dashboard.title || '').toLowerCase().includes(searchText) ||
+ (dashboard.description || '').toLowerCase().includes(searchText);
+ });
+ }
+ },
methods: {
+ highlightTitle(value) {
+ return this.highlightSearchText(value, 'underline decoration-primary underline-offset-2');
+ },
+ highlightDescription(value) {
+ return this.highlightSearchText(value, 'font-semibold text-content-secondary');
+ },
+ highlightSearchText(value, className) {
+ const text = value || '';
+ const searchText = this.searchText.trim();
+ if (!searchText) {
+ return escapeHtml(text);
+ }
+
+ const pattern = new RegExp(escapeRegExp(searchText), 'ig');
+ let html = '';
+ let lastIndex = 0;
+ let match = pattern.exec(text);
+ while (match != null) {
+ html += escapeHtml(text.slice(lastIndex, match.index));
+ html += `${escapeHtml(match[0])}`;
+ lastIndex = match.index + match[0].length;
+ match = pattern.exec(text);
+ }
+ html += escapeHtml(text.slice(lastIndex));
+ return html;
+ },
+ formatDate(value) {
+ if (!value) {
+ return '-';
+ }
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) {
+ return '-';
+ }
+ return new Intl.DateTimeFormat(undefined, {
+ dateStyle: 'medium',
+ timeStyle: 'short'
+ }).format(date);
+ },
+ formatUser(dashboard) {
+ return dashboard.createdBy?.name ||
+ dashboard.createdBy?.email ||
+ (dashboard.createdById ? dashboard.createdById.toString() : '-');
+ },
toggleMenu(id) {
this.openMenuId = this.openMenuId === id ? null : id;
},
@@ -33,7 +91,7 @@ module.exports = app => app.component('dashboards', {
this.$toast.success('Dashboard deleted!');
},
insertNewDashboard(dashboard) {
- this.dashboards.push(dashboard);
+ this.dashboards.unshift(dashboard);
this.showCreateDashboardModal = false;
}
},
@@ -58,3 +116,16 @@ module.exports = app => app.component('dashboards', {
this.status = 'loaded';
}
});
+
+function escapeHtml(value) {
+ return value.toString().
+ replace(/&/g, '&').
+ replace(//g, '>').
+ replace(/"/g, '"').
+ replace(/'/g, ''');
+}
+
+function escapeRegExp(value) {
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
diff --git a/test/Dashboard.getDashboard.results.test.js b/test/Dashboard.getDashboard.results.test.js
index a8b8bfce..39eb0a8b 100644
--- a/test/Dashboard.getDashboard.results.test.js
+++ b/test/Dashboard.getDashboard.results.test.js
@@ -37,6 +37,12 @@ describe('Dashboard.getDashboard() results', function () {
assert.ok(persisted);
assert.strictEqual(persisted.status, 'completed');
assert.deepStrictEqual(persisted.result, { total: 42 });
+
+ const persistedDashboard = await Dashboard.findById(dashboard._id).lean();
+ assert.strictEqual(
+ persistedDashboard.lastEvaluatedAt.toISOString(),
+ persisted.finishedEvaluatingAt.toISOString()
+ );
});
it('returns persisted results from studioConnection', async function () {
diff --git a/test/Dashboard.getDashboards.test.js b/test/Dashboard.getDashboards.test.js
new file mode 100644
index 00000000..e0797047
--- /dev/null
+++ b/test/Dashboard.getDashboards.test.js
@@ -0,0 +1,79 @@
+'use strict';
+
+const assert = require('assert');
+const mongoose = require('mongoose');
+const { actions, studioConnection } = require('./setup.test');
+const dashboardSchema = require('../backend/db/dashboardSchema');
+
+const Dashboard = studioConnection.model('__Studio_Dashboard', dashboardSchema, 'studio__dashboards');
+
+describe('Dashboard.getDashboards()', function() {
+ afterEach(async function() {
+ await Dashboard.deleteMany();
+ });
+
+ it('returns newest dashboards first with stored evaluation time', async function() {
+ const older = await Dashboard.create({
+ title: 'Older',
+ description: 'First dashboard',
+ code: 'return 1;',
+ createdAt: new Date('2026-01-01T00:00:00.000Z'),
+ updatedAt: new Date('2026-01-01T00:00:00.000Z')
+ });
+ const newer = await Dashboard.create({
+ title: 'Newer',
+ description: 'Second dashboard',
+ code: 'return 2;',
+ createdAt: new Date('2026-01-02T00:00:00.000Z'),
+ updatedAt: new Date('2026-01-02T00:00:00.000Z'),
+ lastEvaluatedAt: new Date('2026-01-04T00:00:01.000Z')
+ });
+
+ const res = await actions.Dashboard.getDashboards({ roles: ['dashboards'] });
+
+ assert.deepStrictEqual(res.dashboards.map(dashboard => dashboard.title), ['Newer', 'Older']);
+ assert.strictEqual(
+ new Date(res.dashboards[0].lastEvaluatedAt).toISOString(),
+ '2026-01-04T00:00:01.000Z'
+ );
+ assert.strictEqual(res.dashboards[1].lastEvaluatedAt, undefined);
+ assert.strictEqual(older.title, 'Older');
+ });
+
+ it('derives createdAt from _id for legacy dashboards', async function() {
+ const dashboard = await Dashboard.create({
+ title: 'Legacy',
+ code: 'return 1;'
+ });
+ await Dashboard.collection.updateOne({ _id: dashboard._id }, { $unset: { createdAt: 1 } });
+
+ const res = await actions.Dashboard.getDashboards({ roles: ['dashboards'] });
+
+ assert.strictEqual(res.dashboards.length, 1);
+ assert.strictEqual(
+ new Date(res.dashboards[0].createdAt).toISOString(),
+ dashboard._id.getTimestamp().toISOString()
+ );
+ });
+
+ it('stores the creating user id when creating a dashboard', async function() {
+ const userId = new mongoose.Types.ObjectId();
+
+ const res = await actions.Dashboard.createDashboard({
+ title: 'Created By Test',
+ code: 'return 1;',
+ initiatedById: userId,
+ initiatedBy: {
+ name: 'Jane Doe',
+ email: 'jane@example.com'
+ },
+ roles: ['member']
+ });
+
+ assert.strictEqual(res.dashboard.createdById.toString(), userId.toString());
+ assert.deepStrictEqual(res.dashboard.createdBy.toObject(), {
+ name: 'Jane Doe',
+ email: 'jane@example.com'
+ });
+ });
+});