@mongoosejs/studio 0.5.5 → 0.5.6

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const authorize = require('../../authorize');
5
+ const mongoose = require('mongoose');
5
6
 
6
7
  const CreateDashboardParams = new Archetype({
7
8
  title: {
@@ -12,18 +13,27 @@ const CreateDashboardParams = new Archetype({
12
13
  $type: 'string',
13
14
  $required: true
14
15
  },
16
+ initiatedById: {
17
+ $type: mongoose.Types.ObjectId,
18
+ $required: false
19
+ },
15
20
  roles: {
16
21
  $type: ['string']
17
22
  }
18
23
  }).compile('CreateDashboardParams');
19
24
 
20
25
  module.exports = ({ studioConnection }) => async function createDashboard(params) {
21
- const { title, code, roles } = new CreateDashboardParams(params);
26
+ const { title, code, initiatedById, roles } = new CreateDashboardParams(params);
22
27
  const Dashboard = studioConnection.model('__Studio_Dashboard');
23
28
 
24
29
  await authorize('Dashboard.createDashboard', roles);
25
30
 
26
- const dashboard = await Dashboard.create({ title, code });
31
+ const dashboard = await Dashboard.create({
32
+ title,
33
+ code,
34
+ createdById: initiatedById,
35
+ createdBy: params.initiatedBy
36
+ });
27
37
 
28
38
  return { dashboard };
29
39
  };
@@ -51,6 +51,7 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
51
51
  return {};
52
52
  }
53
53
  return completeDashboardEvaluate(
54
+ Dashboard,
54
55
  DashboardResult,
55
56
  dashboardResult._id,
56
57
  null,
@@ -73,6 +74,7 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
73
74
  return {};
74
75
  }
75
76
  return completeDashboardEvaluate(
77
+ Dashboard,
76
78
  DashboardResult,
77
79
  dashboardResult._id,
78
80
  result,
@@ -97,13 +99,15 @@ module.exports = ({ db, studioConnection, options }) => async function getDashbo
97
99
  }
98
100
  };
99
101
 
100
- async function completeDashboardEvaluate(DashboardResult, dashboardResultId, result, error, status) {
102
+ async function completeDashboardEvaluate(Dashboard, DashboardResult, dashboardResultId, result, error, status) {
101
103
  const dashboardResult = await DashboardResult.findById(dashboardResultId).orFail();
102
- dashboardResult.finishedEvaluatingAt = new Date();
104
+ const finishedEvaluatingAt = new Date();
105
+ dashboardResult.finishedEvaluatingAt = finishedEvaluatingAt;
103
106
  dashboardResult.result = result;
104
107
  dashboardResult.error = error;
105
108
  dashboardResult.status = status;
106
109
  await dashboardResult.save();
110
+ await Dashboard.updateOne({ _id: dashboardResult.dashboardId }, { $set: { lastEvaluatedAt: finishedEvaluatingAt } });
107
111
  return { dashboardResult };
108
112
  }
109
113
 
@@ -15,7 +15,9 @@ module.exports = ({ studioConnection }) => async function getDashboards(params)
15
15
 
16
16
  await authorize('Dashboard.getDashboards', roles);
17
17
 
18
- const dashboards = await Dashboard.find();
18
+ const dashboards = await Dashboard
19
+ .find()
20
+ .sort({ isPinned: -1, createdAt: -1, _id: -1 });
19
21
 
20
22
  return { dashboards };
21
23
  };
@@ -9,8 +9,7 @@ const UpdateDashboardParams = new Archetype({
9
9
  $required: true
10
10
  },
11
11
  code: {
12
- $type: 'string',
13
- $required: true
12
+ $type: 'string'
14
13
  },
15
14
  title: {
16
15
  $type: 'string'
@@ -18,19 +17,26 @@ const UpdateDashboardParams = new Archetype({
18
17
  description: {
19
18
  $type: 'string'
20
19
  },
20
+ isPinned: {
21
+ $type: 'boolean'
22
+ },
21
23
  roles: {
22
24
  $type: ['string']
23
25
  }
24
26
  }).compile('UpdateDashboardParams');
25
27
 
26
28
  module.exports = ({ studioConnection }) => async function updateDashboard(params) {
27
- const { dashboardId, code, title, description, roles, evaluate } = new UpdateDashboardParams(params);
29
+ const { dashboardId, code, title, description, isPinned, roles, evaluate } = new UpdateDashboardParams(params);
28
30
 
29
31
  const Dashboard = studioConnection.models['__Studio_Dashboard'];
30
32
 
31
33
  await authorize('Dashboard.updateDashboard', roles);
32
34
 
33
- const updateObj = { code };
35
+ const updateObj = {};
36
+
37
+ if (code != null) {
38
+ updateObj.code = code;
39
+ }
34
40
 
35
41
  if (title != null) {
36
42
  updateObj.title = title;
@@ -40,8 +46,15 @@ module.exports = ({ studioConnection }) => async function updateDashboard(params
40
46
  updateObj.description = description;
41
47
  }
42
48
 
43
- const doc = await Dashboard.
44
- findByIdAndUpdate(dashboardId, updateObj, { sanitizeFilter: true, returnDocument: 'after', overwriteImmutable: true });
49
+ if (isPinned != null) {
50
+ updateObj.isPinned = isPinned;
51
+ }
52
+
53
+ const doc = await Dashboard.findByIdAndUpdate(
54
+ dashboardId,
55
+ updateObj,
56
+ { sanitizeFilter: true, returnDocument: 'after' }
57
+ );
45
58
 
46
59
  return { doc };
47
60
  };
@@ -13,6 +13,37 @@ const dashboardSchema = new mongoose.Schema({
13
13
  },
14
14
  description: {
15
15
  type: String
16
+ },
17
+ isPinned: {
18
+ type: Boolean
19
+ },
20
+ createdById: {
21
+ type: mongoose.Schema.Types.ObjectId
22
+ },
23
+ createdBy: {
24
+ name: String,
25
+ email: String
26
+ },
27
+ lastEvaluatedAt: {
28
+ type: Date
29
+ }
30
+ }, { timestamps: true });
31
+
32
+ dashboardSchema.post(['find', 'findOne'], async function(docs) {
33
+ const dashboards = Array.isArray(docs) ? docs : [docs];
34
+ for (const dashboard of dashboards) {
35
+ if (dashboard != null && dashboard.createdAt == null && dashboard._id?.getTimestamp) {
36
+ const createdAt = dashboard._id.getTimestamp();
37
+ if (!createdAt.valueOf()) {
38
+ continue;
39
+ }
40
+ dashboard.set('createdAt', createdAt, { overwriteImmutable: true });
41
+ await this.model.db.model('__Studio_Dashboard').updateOne(
42
+ { _id: dashboard._id },
43
+ { createdAt },
44
+ { overwriteImmutable: true }
45
+ );
46
+ }
16
47
  }
17
48
  });
18
49
 
@@ -63,6 +63,7 @@ module.exports = function netlify(conn, options) {
63
63
  params.roles = roles;
64
64
  params.userId = user._id;
65
65
  params.initiatedById = user._id;
66
+ params.initiatedBy = user;
66
67
  }
67
68
 
68
69
  if (typeof actionName !== 'string') {
package/backend/next.js CHANGED
@@ -64,6 +64,7 @@ module.exports = function next(conn, options) {
64
64
  params.roles = roles;
65
65
  params.userId = user._id;
66
66
  params.initiatedById = user._id;
67
+ params.initiatedBy = user;
67
68
  }
68
69
 
69
70
  if (typeof actionName !== 'string') {
package/express.js CHANGED
@@ -87,6 +87,7 @@ module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options)
87
87
  req._internals.initiatedById = user._id;
88
88
  req._internals.roles = roles;
89
89
  req._internals.$workspaceId = workspace._id;
90
+ req._internals.initiatedBy = user;
90
91
 
91
92
  next();
92
93
  })
@@ -3266,11 +3266,73 @@ module.exports = app => app.component('dashboards', {
3266
3266
  data: () => ({
3267
3267
  status: 'loading',
3268
3268
  dashboards: [],
3269
+ searchText: '',
3269
3270
  showCreateDashboardModal: false,
3270
3271
  showDeleteDashboardModal: null,
3271
3272
  openMenuId: null
3272
3273
  }),
3274
+ computed: {
3275
+ filteredDashboards() {
3276
+ const searchWords = getSearchWords(this.searchText);
3277
+ let dashboards = this.dashboards;
3278
+ if (searchWords.length === 0) {
3279
+ return sortPinnedFirst(dashboards);
3280
+ }
3281
+ const searchPatterns = searchWords.map(word => new RegExp(escapeRegExp(word), 'i'));
3282
+
3283
+ dashboards = dashboards.filter(dashboard => {
3284
+ const searchableText = `${dashboard.title || ''} ${dashboard.description || ''}`;
3285
+ return searchPatterns.every(pattern => pattern.test(searchableText));
3286
+ });
3287
+
3288
+ return sortPinnedFirst(dashboards);
3289
+ }
3290
+ },
3273
3291
  methods: {
3292
+ highlightTitle(value) {
3293
+ return this.highlightSearchText(value, 'underline decoration-primary underline-offset-2');
3294
+ },
3295
+ highlightDescription(value) {
3296
+ return this.highlightSearchText(value, 'font-semibold text-content-secondary');
3297
+ },
3298
+ highlightSearchText(value, className) {
3299
+ const text = value || '';
3300
+ const searchWords = getSearchWords(this.searchText);
3301
+ if (searchWords.length === 0) {
3302
+ return escapeHtml(text);
3303
+ }
3304
+ const searchPattern = new RegExp(searchWords.map(escapeRegExp).join('|'), 'ig');
3305
+
3306
+ let html = '';
3307
+ let lastIndex = 0;
3308
+ let match = searchPattern.exec(text);
3309
+ while (match != null) {
3310
+ html += escapeHtml(text.slice(lastIndex, match.index));
3311
+ html += `<span class="${className}">${escapeHtml(match[0])}</span>`;
3312
+ lastIndex = match.index + match[0].length;
3313
+ match = searchPattern.exec(text);
3314
+ }
3315
+ html += escapeHtml(text.slice(lastIndex));
3316
+ return html;
3317
+ },
3318
+ formatDate(value) {
3319
+ if (!value) {
3320
+ return '-';
3321
+ }
3322
+ const date = new Date(value);
3323
+ if (Number.isNaN(date.getTime())) {
3324
+ return '-';
3325
+ }
3326
+ return new Intl.DateTimeFormat(undefined, {
3327
+ dateStyle: 'medium',
3328
+ timeStyle: 'short'
3329
+ }).format(date);
3330
+ },
3331
+ formatUser(dashboard) {
3332
+ return dashboard.createdBy?.name ||
3333
+ dashboard.createdBy?.email ||
3334
+ (dashboard.createdById ? dashboard.createdById.toString() : '-');
3335
+ },
3274
3336
  toggleMenu(id) {
3275
3337
  this.openMenuId = this.openMenuId === id ? null : id;
3276
3338
  },
@@ -3290,8 +3352,19 @@ module.exports = app => app.component('dashboards', {
3290
3352
  this.$toast.success('Dashboard deleted!');
3291
3353
  },
3292
3354
  insertNewDashboard(dashboard) {
3293
- this.dashboards.push(dashboard);
3355
+ this.dashboards.unshift(dashboard);
3294
3356
  this.showCreateDashboardModal = false;
3357
+ },
3358
+ async togglePin(dashboard) {
3359
+ if (!dashboard) {
3360
+ return;
3361
+ }
3362
+
3363
+ const { doc } = await api.Dashboard.updateDashboard({
3364
+ dashboardId: dashboard._id,
3365
+ isPinned: !dashboard.isPinned
3366
+ });
3367
+ dashboard.isPinned = doc.isPinned;
3295
3368
  }
3296
3369
  },
3297
3370
  directives: {
@@ -3316,6 +3389,28 @@ module.exports = app => app.component('dashboards', {
3316
3389
  }
3317
3390
  });
3318
3391
 
3392
+ function escapeHtml(value) {
3393
+ return value.toString().
3394
+ replace(/&/g, '&amp;').
3395
+ replace(/</g, '&lt;').
3396
+ replace(/>/g, '&gt;').
3397
+ replace(/"/g, '&quot;').
3398
+ replace(/'/g, '&#39;');
3399
+ }
3400
+
3401
+ function escapeRegExp(value) {
3402
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3403
+ }
3404
+
3405
+ function getSearchWords(searchText) {
3406
+ return Array.from(new Set(searchText.trim().split(/\s+/).filter(Boolean))).
3407
+ sort((a, b) => b.length - a.length);
3408
+ }
3409
+
3410
+ function sortPinnedFirst(dashboards) {
3411
+ return dashboards.slice().sort((a, b) => Number(Boolean(b.isPinned)) - Number(Boolean(a.isPinned)));
3412
+ }
3413
+
3319
3414
 
3320
3415
  /***/ },
3321
3416
 
@@ -50528,6 +50623,128 @@ module.exports = {
50528
50623
 
50529
50624
  "use strict";
50530
50625
 
50626
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
50627
+ exports.setUint32LE = exports.readUint8 = exports.readInt32LE = exports.UUID = exports.Timestamp = exports.serialize = exports.ObjectId = exports.NumberUtils = exports.MinKey = exports.MaxKey = exports.Long = exports.Int32 = exports.EJSON = exports.Double = exports.deserialize = exports.Decimal128 = exports.DBRef = exports.Code = exports.calculateObjectSize = exports.ByteUtils = exports.BSONType = exports.BSONSymbol = exports.BSONRegExp = exports.BSONError = exports.BSON = exports.Binary = void 0;
50628
+ exports.parseToElementsToArray = parseToElementsToArray;
50629
+ exports.pluckBSONSerializeOptions = pluckBSONSerializeOptions;
50630
+ exports.resolveBSONOptions = resolveBSONOptions;
50631
+ exports.parseUtf8ValidationOption = parseUtf8ValidationOption;
50632
+ /* eslint-disable no-restricted-imports */
50633
+ const bson_1 = __webpack_require__(/*! bson */ "./node_modules/bson/lib/bson.mjs");
50634
+ var bson_2 = __webpack_require__(/*! bson */ "./node_modules/bson/lib/bson.mjs");
50635
+ Object.defineProperty(exports, "Binary", ({ enumerable: true, get: function () { return bson_2.Binary; } }));
50636
+ Object.defineProperty(exports, "BSON", ({ enumerable: true, get: function () { return bson_2.BSON; } }));
50637
+ Object.defineProperty(exports, "BSONError", ({ enumerable: true, get: function () { return bson_2.BSONError; } }));
50638
+ Object.defineProperty(exports, "BSONRegExp", ({ enumerable: true, get: function () { return bson_2.BSONRegExp; } }));
50639
+ Object.defineProperty(exports, "BSONSymbol", ({ enumerable: true, get: function () { return bson_2.BSONSymbol; } }));
50640
+ Object.defineProperty(exports, "BSONType", ({ enumerable: true, get: function () { return bson_2.BSONType; } }));
50641
+ Object.defineProperty(exports, "ByteUtils", ({ enumerable: true, get: function () { return bson_2.ByteUtils; } }));
50642
+ Object.defineProperty(exports, "calculateObjectSize", ({ enumerable: true, get: function () { return bson_2.calculateObjectSize; } }));
50643
+ Object.defineProperty(exports, "Code", ({ enumerable: true, get: function () { return bson_2.Code; } }));
50644
+ Object.defineProperty(exports, "DBRef", ({ enumerable: true, get: function () { return bson_2.DBRef; } }));
50645
+ Object.defineProperty(exports, "Decimal128", ({ enumerable: true, get: function () { return bson_2.Decimal128; } }));
50646
+ Object.defineProperty(exports, "deserialize", ({ enumerable: true, get: function () { return bson_2.deserialize; } }));
50647
+ Object.defineProperty(exports, "Double", ({ enumerable: true, get: function () { return bson_2.Double; } }));
50648
+ Object.defineProperty(exports, "EJSON", ({ enumerable: true, get: function () { return bson_2.EJSON; } }));
50649
+ Object.defineProperty(exports, "Int32", ({ enumerable: true, get: function () { return bson_2.Int32; } }));
50650
+ Object.defineProperty(exports, "Long", ({ enumerable: true, get: function () { return bson_2.Long; } }));
50651
+ Object.defineProperty(exports, "MaxKey", ({ enumerable: true, get: function () { return bson_2.MaxKey; } }));
50652
+ Object.defineProperty(exports, "MinKey", ({ enumerable: true, get: function () { return bson_2.MinKey; } }));
50653
+ Object.defineProperty(exports, "NumberUtils", ({ enumerable: true, get: function () { return bson_2.NumberUtils; } }));
50654
+ Object.defineProperty(exports, "ObjectId", ({ enumerable: true, get: function () { return bson_2.ObjectId; } }));
50655
+ Object.defineProperty(exports, "serialize", ({ enumerable: true, get: function () { return bson_2.serialize; } }));
50656
+ Object.defineProperty(exports, "Timestamp", ({ enumerable: true, get: function () { return bson_2.Timestamp; } }));
50657
+ Object.defineProperty(exports, "UUID", ({ enumerable: true, get: function () { return bson_2.UUID; } }));
50658
+ function parseToElementsToArray(bytes, offset) {
50659
+ const res = bson_1.BSON.onDemand.parseToElements(bytes, offset);
50660
+ return Array.isArray(res) ? res : [...res];
50661
+ }
50662
+ // validates buffer inputs, used for read operations
50663
+ const validateBufferInputs = (buffer, offset, length) => {
50664
+ if (offset < 0 || offset + length > buffer.length) {
50665
+ throw new RangeError(`Attempt to access memory outside buffer bounds: buffer length: ${buffer.length}, offset: ${offset}, length: ${length}`);
50666
+ }
50667
+ };
50668
+ // readInt32LE, reads a 32-bit integer from buffer at given offset
50669
+ // throws if offset is out of bounds
50670
+ const readInt32LE = (buffer, offset) => {
50671
+ validateBufferInputs(buffer, offset, 4);
50672
+ return bson_1.NumberUtils.getInt32LE(buffer, offset);
50673
+ };
50674
+ exports.readInt32LE = readInt32LE;
50675
+ // readUint8, reads a single unsigned byte from buffer at given offset
50676
+ const readUint8 = (buffer, offset) => {
50677
+ validateBufferInputs(buffer, offset, 1);
50678
+ return buffer[offset];
50679
+ };
50680
+ exports.readUint8 = readUint8;
50681
+ const setUint32LE = (destination, offset, value) => {
50682
+ destination[offset] = value;
50683
+ value >>>= 8;
50684
+ destination[offset + 1] = value;
50685
+ value >>>= 8;
50686
+ destination[offset + 2] = value;
50687
+ value >>>= 8;
50688
+ destination[offset + 3] = value;
50689
+ return 4;
50690
+ };
50691
+ exports.setUint32LE = setUint32LE;
50692
+ function pluckBSONSerializeOptions(options) {
50693
+ const { fieldsAsRaw, useBigInt64, promoteValues, promoteBuffers, promoteLongs, serializeFunctions, ignoreUndefined, bsonRegExp, raw, enableUtf8Validation } = options;
50694
+ return {
50695
+ fieldsAsRaw,
50696
+ useBigInt64,
50697
+ promoteValues,
50698
+ promoteBuffers,
50699
+ promoteLongs,
50700
+ serializeFunctions,
50701
+ ignoreUndefined,
50702
+ bsonRegExp,
50703
+ raw,
50704
+ enableUtf8Validation
50705
+ };
50706
+ }
50707
+ /**
50708
+ * Merge the given BSONSerializeOptions, preferring options over the parent's options, and
50709
+ * substituting defaults for values not set.
50710
+ *
50711
+ * @internal
50712
+ */
50713
+ function resolveBSONOptions(options, parent) {
50714
+ const parentOptions = parent?.bsonOptions;
50715
+ return {
50716
+ raw: options?.raw ?? parentOptions?.raw ?? false,
50717
+ useBigInt64: options?.useBigInt64 ?? parentOptions?.useBigInt64 ?? false,
50718
+ promoteLongs: options?.promoteLongs ?? parentOptions?.promoteLongs ?? true,
50719
+ promoteValues: options?.promoteValues ?? parentOptions?.promoteValues ?? true,
50720
+ promoteBuffers: options?.promoteBuffers ?? parentOptions?.promoteBuffers ?? false,
50721
+ ignoreUndefined: options?.ignoreUndefined ?? parentOptions?.ignoreUndefined ?? false,
50722
+ bsonRegExp: options?.bsonRegExp ?? parentOptions?.bsonRegExp ?? false,
50723
+ serializeFunctions: options?.serializeFunctions ?? parentOptions?.serializeFunctions ?? false,
50724
+ fieldsAsRaw: options?.fieldsAsRaw ?? parentOptions?.fieldsAsRaw ?? {},
50725
+ enableUtf8Validation: options?.enableUtf8Validation ?? parentOptions?.enableUtf8Validation ?? true
50726
+ };
50727
+ }
50728
+ /** @internal */
50729
+ function parseUtf8ValidationOption(options) {
50730
+ const enableUtf8Validation = options?.enableUtf8Validation;
50731
+ if (enableUtf8Validation === false) {
50732
+ return { utf8: false };
50733
+ }
50734
+ return { utf8: { writeErrors: false } };
50735
+ }
50736
+ //# sourceMappingURL=bson.js.map
50737
+
50738
+ /***/ },
50739
+
50740
+ /***/ "./node_modules/mongoose/node_modules/mongodb/lib/bson.js"
50741
+ /*!****************************************************************!*\
50742
+ !*** ./node_modules/mongoose/node_modules/mongodb/lib/bson.js ***!
50743
+ \****************************************************************/
50744
+ (__unused_webpack_module, exports, __webpack_require__) {
50745
+
50746
+ "use strict";
50747
+
50531
50748
  Object.defineProperty(exports, "__esModule", ({ value: true }));
50532
50749
  exports.setUint32LE = exports.readInt32LE = exports.UUID = exports.Timestamp = exports.serialize = exports.ObjectId = exports.NumberUtils = exports.MinKey = exports.MaxKey = exports.Long = exports.Int32 = exports.EJSON = exports.Double = exports.deserialize = exports.Decimal128 = exports.DBRef = exports.Code = exports.calculateObjectSize = exports.ByteUtils = exports.BSONType = exports.BSONSymbol = exports.BSONRegExp = exports.BSONError = exports.BSON = exports.Binary = void 0;
50533
50750
  exports.parseToElementsToArray = parseToElementsToArray;
@@ -52745,7 +52962,7 @@ module.exports = "<div class=\"dashboard-editor p-4 bg-muted rounded-lg shadow-l
52745
52962
  (module) {
52746
52963
 
52747
52964
  "use strict";
52748
- module.exports = "<div class=\"dashboards bg-page min-h-[calc(100vh-55px)]\">\n <div class=\"max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8\">\n <div class=\"flex items-center justify-between mb-6\">\n <h1 class=\"text-lg font-semibold text-content\">Dashboards</h1>\n <button\n type=\"button\"\n @click=\"showCreateDashboardModal = true\"\n class=\"rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\">\n New Dashboard\n </button>\n </div>\n\n <div v-if=\"status === 'loading'\" class=\"text-center py-12\">\n <svg\n class=\"inline w-8 h-8 animate-spin text-gray-400\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z\"></path>\n </svg>\n </div>\n\n <div v-else-if=\"dashboards.length === 0\" class=\"text-center py-12\">\n <h3 class=\"text-sm font-semibold text-content\">No dashboards yet</h3>\n <p class=\"mt-1 text-sm text-content-tertiary\">Get started by creating a new dashboard.</p>\n <div class=\"mt-4\">\n <button\n type=\"button\"\n @click=\"showCreateDashboardModal = true\"\n class=\"inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover\">\n <svg class=\"-ml-0.5 mr-1.5 h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z\" />\n </svg>\n New Dashboard\n </button>\n </div>\n </div>\n\n <div v-else class=\"space-y-3\">\n <div\n v-for=\"dashboard in dashboards\"\n :key=\"dashboard._id\"\n class=\"bg-surface rounded-lg border border-edge px-5 py-4 flex items-center justify-between hover:border-edge-strong transition-colors\"\n >\n <div class=\"min-w-0 flex-1 mr-4\">\n <router-link\n :to=\"'/dashboard/' + dashboard._id\"\n class=\"text-sm font-semibold text-content hover:text-primary block truncate\">\n {{ dashboard.title }}\n </router-link>\n <p v-if=\"dashboard.description\" class=\"mt-0.5 text-sm text-content-tertiary truncate\">{{ dashboard.description }}</p>\n </div>\n <div class=\"flex items-center gap-2 shrink-0\">\n <router-link\n :to=\"'/dashboard/' + dashboard._id\"\n class=\"rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover\">\n View\n </router-link>\n <div class=\"relative\" v-click-outside=\"() => closeMenu(dashboard._id)\">\n <button\n type=\"button\"\n @click=\"toggleMenu(dashboard._id)\"\n class=\"rounded-md bg-surface px-2 py-1.5 text-content-tertiary ring-1 ring-inset ring-gray-300 hover:bg-page\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-4 h-4\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z\" />\n </svg>\n </button>\n <div\n v-if=\"openMenuId === dashboard._id\"\n class=\"absolute right-0 mt-1 w-36 origin-top-right rounded-md bg-surface shadow-lg ring-1 ring-black/5 z-10\"\n >\n <div class=\"py-1\">\n <router-link\n :to=\"'/dashboard/' + dashboard._id + '?edit=true'\"\n class=\"block px-4 py-2 text-sm text-content-secondary hover:bg-muted\">\n Edit\n </router-link>\n <button\n @click=\"showDeleteDashboardModal = dashboard; openMenuId = null\"\n type=\"button\"\n class=\"block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-muted\">\n Delete\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <modal v-if=\"showCreateDashboardModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showCreateDashboardModal = false;\">&times;</div>\n <create-dashboard @close=\"insertNewDashboard\"></create-dashboard>\n </template>\n </modal>\n\n <modal v-if=\"showDeleteDashboardModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showDeleteDashboardModal = null;\">&times;</div>\n <h2>Are you sure you want to delete \"{{showDeleteDashboardModal.title}}\"?</h2>\n <div class=\"flex space-x-2 mt-4\">\n <button class=\"px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-500 text-sm font-semibold\" @click=\"deleteDashboard(showDeleteDashboardModal)\">Yes, delete</button>\n <button class=\"px-4 py-2 bg-muted text-content-secondary rounded-md hover:bg-muted text-sm font-semibold\" @click=\"showDeleteDashboardModal=null;\">Cancel</button>\n </div>\n </template>\n </modal>\n</div>\n";
52965
+ module.exports = "<div class=\"dashboards bg-page min-h-[calc(100vh-55px)]\">\n <div class=\"max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8\">\n <div class=\"flex items-center justify-between mb-6\">\n <h1 class=\"text-lg font-semibold text-content\">Dashboards</h1>\n <button\n type=\"button\"\n @click=\"showCreateDashboardModal = true\"\n class=\"rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\">\n New Dashboard\n </button>\n </div>\n\n <div v-if=\"status !== 'loading'\" class=\"mb-5\">\n <label for=\"dashboard-search\" class=\"sr-only\">Search dashboards</label>\n <div class=\"relative max-w-lg\">\n <div class=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-content-tertiary\">\n <svg class=\"h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z\" />\n </svg>\n </div>\n <input\n id=\"dashboard-search\"\n v-model=\"searchText\"\n type=\"search\"\n class=\"block w-full rounded-md border-0 bg-surface py-2 pl-9 pr-3 text-sm text-content shadow-sm ring-1 ring-inset ring-edge placeholder:text-content-tertiary focus:ring-2 focus:ring-inset focus:ring-primary\"\n placeholder=\"Search by title or description\">\n </div>\n </div>\n\n <div v-if=\"status === 'loading'\" class=\"text-center py-12\">\n <svg\n class=\"inline w-8 h-8 animate-spin text-gray-400\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z\"></path>\n </svg>\n </div>\n\n <div v-else-if=\"dashboards.length === 0\" class=\"text-center py-12\">\n <h3 class=\"text-sm font-semibold text-content\">No dashboards yet</h3>\n <p class=\"mt-1 text-sm text-content-tertiary\">Get started by creating a new dashboard.</p>\n <div class=\"mt-4\">\n <button\n type=\"button\"\n @click=\"showCreateDashboardModal = true\"\n class=\"inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover\">\n <svg class=\"-ml-0.5 mr-1.5 h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z\" />\n </svg>\n New Dashboard\n </button>\n </div>\n </div>\n\n <div v-else-if=\"filteredDashboards.length === 0\" class=\"text-center py-12\">\n <h3 class=\"text-sm font-semibold text-content\">No matching dashboards</h3>\n <p class=\"mt-1 text-sm text-content-tertiary\">Try a different title or description.</p>\n </div>\n\n <div v-else class=\"space-y-3\">\n <div\n v-for=\"dashboard in filteredDashboards\"\n :key=\"dashboard._id\"\n class=\"bg-surface rounded-lg border border-edge px-5 py-4 flex items-start justify-between hover:border-edge-strong transition-colors\"\n >\n <div class=\"min-w-0 flex-1 mr-4\">\n <router-link\n :to=\"'/dashboard/' + dashboard._id\"\n class=\"text-sm font-semibold text-content hover:text-primary block truncate\">\n <span v-html=\"highlightTitle(dashboard.title)\"></span>\n </router-link>\n <p\n v-if=\"dashboard.description\"\n class=\"mt-0.5 text-sm text-content-tertiary truncate\"\n v-html=\"highlightDescription(dashboard.description)\"></p>\n <dl class=\"mt-3 grid grid-cols-1 gap-x-4 gap-y-1 text-xs text-content-tertiary sm:grid-cols-3\">\n <div>\n <dt class=\"font-medium text-content-secondary\">Created At</dt>\n <dd>{{ formatDate(dashboard.createdAt) }}</dd>\n </div>\n <div>\n <dt class=\"font-medium text-content-secondary\">Last Evaluated At</dt>\n <dd>{{ formatDate(dashboard.lastEvaluatedAt) }}</dd>\n </div>\n <div v-show=\"dashboard.createdBy\">\n <dt class=\"font-medium text-content-secondary\">Created By</dt>\n <dd class=\"truncate\">{{ dashboard.createdBy?.name }}</dd>\n </div>\n </dl>\n </div>\n <div class=\"flex items-center gap-2 shrink-0\">\n <button\n type=\"button\"\n @click=\"togglePin(dashboard)\"\n :title=\"dashboard.isPinned ? 'Unpin dashboard' : 'Pin dashboard'\"\n :aria-label=\"dashboard.isPinned ? 'Unpin dashboard' : 'Pin dashboard'\"\n class=\"rounded-md bg-surface px-2 py-1.5 text-content-tertiary ring-1 ring-inset ring-gray-300 hover:bg-page\"\n :class=\"dashboard.isPinned ? 'text-primary ring-primary/40' : ''\">\n <img :src=\"dashboard.isPinned ? 'images/pin-off.svg' : 'images/pin.svg'\" class=\"w-4 h-4\" alt=\"\" />\n </button>\n <router-link\n :to=\"'/dashboard/' + dashboard._id\"\n class=\"rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover\">\n View\n </router-link>\n <div class=\"relative\" v-click-outside=\"() => closeMenu(dashboard._id)\">\n <button\n type=\"button\"\n @click=\"toggleMenu(dashboard._id)\"\n class=\"rounded-md bg-surface px-2 py-1.5 text-content-tertiary ring-1 ring-inset ring-gray-300 hover:bg-page\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-4 h-4\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z\" />\n </svg>\n </button>\n <div\n v-if=\"openMenuId === dashboard._id\"\n class=\"absolute right-0 mt-1 w-36 origin-top-right rounded-md bg-surface shadow-lg ring-1 ring-black/5 z-10\"\n >\n <div class=\"py-1\">\n <router-link\n :to=\"'/dashboard/' + dashboard._id + '?edit=true'\"\n class=\"block px-4 py-2 text-sm text-content-secondary hover:bg-muted\">\n Edit\n </router-link>\n <button\n @click=\"showDeleteDashboardModal = dashboard; openMenuId = null\"\n type=\"button\"\n class=\"block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-muted\">\n Delete\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <modal v-if=\"showCreateDashboardModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showCreateDashboardModal = false;\">&times;</div>\n <create-dashboard @close=\"insertNewDashboard\"></create-dashboard>\n </template>\n </modal>\n\n <modal v-if=\"showDeleteDashboardModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showDeleteDashboardModal = null;\">&times;</div>\n <h2>Are you sure you want to delete \"{{showDeleteDashboardModal.title}}\"?</h2>\n <div class=\"flex space-x-2 mt-4\">\n <button class=\"px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-500 text-sm font-semibold\" @click=\"deleteDashboard(showDeleteDashboardModal)\">Yes, delete</button>\n <button class=\"px-4 py-2 bg-muted text-content-secondary rounded-md hover:bg-muted text-sm font-semibold\" @click=\"showDeleteDashboardModal=null;\">Cancel</button>\n </div>\n </template>\n </modal>\n</div>\n";
52749
52966
 
52750
52967
  /***/ },
52751
52968
 
@@ -58836,7 +59053,7 @@ exports.validatorErrorSymbol = Symbol('mongoose#validatorError');
58836
59053
 
58837
59054
 
58838
59055
 
58839
- const ObjectId = (__webpack_require__(/*! mongodb/lib/bson */ "./node_modules/mongodb/lib/bson.js").ObjectId);
59056
+ const ObjectId = (__webpack_require__(/*! mongodb/lib/bson */ "./node_modules/mongoose/node_modules/mongodb/lib/bson.js").ObjectId);
58840
59057
  const objectIdSymbol = (__webpack_require__(/*! ../helpers/symbols */ "./node_modules/mongoose/lib/helpers/symbols.js").objectIdSymbol);
58841
59058
 
58842
59059
  /**
@@ -64736,7 +64953,7 @@ var src_default = VueToastificationPlugin;
64736
64953
  (module) {
64737
64954
 
64738
64955
  "use strict";
64739
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.5.5","description":"AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","mpath":"^0.9.0","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","time-commando":"1.0.1","typescript":"^6.0.2","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","xss":"^1.0.15"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js","eval:agent":"node eval/agent/run.js"}}');
64956
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.5.6","description":"AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","mpath":"^0.9.0","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","time-commando":"1.0.1","typescript":"^6.0.2","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","xss":"^1.0.15"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js","eval:agent":"node eval/agent/run.js"}}');
64740
64957
 
64741
64958
  /***/ }
64742
64959
 
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M680-840v80h-40v327l-80-80v-247H400v87l-87-87-33-33v-47h400ZM480-40l-40-40v-240H240v-80l80-80v-46L56-792l56-56 736 736-58 56-264-264h-6v240l-40 40ZM354-400h92l-44-44-2-2-46 46Zm126-193Zm-78 149Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="m640-480 80 80v80H520v240l-40 40-40-40v-240H240v-80l80-80v-280h-40v-80h400v80h-40v280Zm-286 80h252l-46-46v-314H400v314l-46 46Zm126 0Z"/></svg>
@@ -905,6 +905,10 @@ video {
905
905
  margin-bottom: 1rem;
906
906
  }
907
907
 
908
+ .mb-5 {
909
+ margin-bottom: 1.25rem;
910
+ }
911
+
908
912
  .mb-6 {
909
913
  margin-bottom: 1.5rem;
910
914
  }
@@ -1681,6 +1685,10 @@ video {
1681
1685
  column-gap: 1.5rem;
1682
1686
  }
1683
1687
 
1688
+ .gap-y-1 {
1689
+ row-gap: 0.25rem;
1690
+ }
1691
+
1684
1692
  .space-x-2 > :not([hidden]) ~ :not([hidden]) {
1685
1693
  --tw-space-x-reverse: 0;
1686
1694
  margin-right: calc(0.5rem * var(--tw-space-x-reverse));
@@ -2524,6 +2532,10 @@ video {
2524
2532
  padding-left: 2rem;
2525
2533
  }
2526
2534
 
2535
+ .pl-9 {
2536
+ padding-left: 2.25rem;
2537
+ }
2538
+
2527
2539
  .pr-12 {
2528
2540
  padding-right: 3rem;
2529
2541
  }
@@ -2927,6 +2939,10 @@ video {
2927
2939
  text-decoration-line: underline;
2928
2940
  }
2929
2941
 
2942
+ .decoration-primary {
2943
+ text-decoration-color: var(--color-primary);
2944
+ }
2945
+
2930
2946
  .decoration-dotted {
2931
2947
  text-decoration-style: dotted;
2932
2948
  }
@@ -3063,6 +3079,10 @@ video {
3063
3079
  --tw-ring-color: rgb(0 0 0 / 0.05);
3064
3080
  }
3065
3081
 
3082
+ .ring-edge {
3083
+ --tw-ring-color: var(--color-edge);
3084
+ }
3085
+
3066
3086
  .ring-edge-strong {
3067
3087
  --tw-ring-color: var(--color-edge-strong);
3068
3088
  }
@@ -3650,6 +3670,10 @@ video {
3650
3670
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
3651
3671
  }
3652
3672
 
3673
+ .focus\:ring-inset:focus {
3674
+ --tw-ring-inset: inset;
3675
+ }
3676
+
3653
3677
  .focus\:ring-blue-500:focus {
3654
3678
  --tw-ring-opacity: 1;
3655
3679
  --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
@@ -3896,6 +3920,10 @@ video {
3896
3920
  flex: none;
3897
3921
  }
3898
3922
 
3923
+ .sm\:grid-cols-3 {
3924
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3925
+ }
3926
+
3899
3927
  .sm\:grid-cols-4 {
3900
3928
  grid-template-columns: repeat(4, minmax(0, 1fr));
3901
3929
  }
@@ -10,6 +10,23 @@
10
10
  </button>
11
11
  </div>
12
12
 
13
+ <div v-if="status !== 'loading'" class="mb-5">
14
+ <label for="dashboard-search" class="sr-only">Search dashboards</label>
15
+ <div class="relative max-w-lg">
16
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-content-tertiary">
17
+ <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
18
+ <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
19
+ </svg>
20
+ </div>
21
+ <input
22
+ id="dashboard-search"
23
+ v-model="searchText"
24
+ type="search"
25
+ class="block w-full rounded-md border-0 bg-surface py-2 pl-9 pr-3 text-sm text-content shadow-sm ring-1 ring-inset ring-edge placeholder:text-content-tertiary focus:ring-2 focus:ring-inset focus:ring-primary"
26
+ placeholder="Search by title or description">
27
+ </div>
28
+ </div>
29
+
13
30
  <div v-if="status === 'loading'" class="text-center py-12">
14
31
  <svg
15
32
  class="inline w-8 h-8 animate-spin text-gray-400"
@@ -38,21 +55,52 @@
38
55
  </div>
39
56
  </div>
40
57
 
58
+ <div v-else-if="filteredDashboards.length === 0" class="text-center py-12">
59
+ <h3 class="text-sm font-semibold text-content">No matching dashboards</h3>
60
+ <p class="mt-1 text-sm text-content-tertiary">Try a different title or description.</p>
61
+ </div>
62
+
41
63
  <div v-else class="space-y-3">
42
64
  <div
43
- v-for="dashboard in dashboards"
65
+ v-for="dashboard in filteredDashboards"
44
66
  :key="dashboard._id"
45
- class="bg-surface rounded-lg border border-edge px-5 py-4 flex items-center justify-between hover:border-edge-strong transition-colors"
67
+ class="bg-surface rounded-lg border border-edge px-5 py-4 flex items-start justify-between hover:border-edge-strong transition-colors"
46
68
  >
47
69
  <div class="min-w-0 flex-1 mr-4">
48
70
  <router-link
49
71
  :to="'/dashboard/' + dashboard._id"
50
72
  class="text-sm font-semibold text-content hover:text-primary block truncate">
51
- {{ dashboard.title }}
73
+ <span v-html="highlightTitle(dashboard.title)"></span>
52
74
  </router-link>
53
- <p v-if="dashboard.description" class="mt-0.5 text-sm text-content-tertiary truncate">{{ dashboard.description }}</p>
75
+ <p
76
+ v-if="dashboard.description"
77
+ class="mt-0.5 text-sm text-content-tertiary truncate"
78
+ v-html="highlightDescription(dashboard.description)"></p>
79
+ <dl class="mt-3 grid grid-cols-1 gap-x-4 gap-y-1 text-xs text-content-tertiary sm:grid-cols-3">
80
+ <div>
81
+ <dt class="font-medium text-content-secondary">Created At</dt>
82
+ <dd>{{ formatDate(dashboard.createdAt) }}</dd>
83
+ </div>
84
+ <div>
85
+ <dt class="font-medium text-content-secondary">Last Evaluated At</dt>
86
+ <dd>{{ formatDate(dashboard.lastEvaluatedAt) }}</dd>
87
+ </div>
88
+ <div v-show="dashboard.createdBy">
89
+ <dt class="font-medium text-content-secondary">Created By</dt>
90
+ <dd class="truncate">{{ dashboard.createdBy?.name }}</dd>
91
+ </div>
92
+ </dl>
54
93
  </div>
55
94
  <div class="flex items-center gap-2 shrink-0">
95
+ <button
96
+ type="button"
97
+ @click="togglePin(dashboard)"
98
+ :title="dashboard.isPinned ? 'Unpin dashboard' : 'Pin dashboard'"
99
+ :aria-label="dashboard.isPinned ? 'Unpin dashboard' : 'Pin dashboard'"
100
+ class="rounded-md bg-surface px-2 py-1.5 text-content-tertiary ring-1 ring-inset ring-gray-300 hover:bg-page"
101
+ :class="dashboard.isPinned ? 'text-primary ring-primary/40' : ''">
102
+ <img :src="dashboard.isPinned ? 'images/pin-off.svg' : 'images/pin.svg'" class="w-4 h-4" alt="" />
103
+ </button>
56
104
  <router-link
57
105
  :to="'/dashboard/' + dashboard._id"
58
106
  class="rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover">
@@ -9,11 +9,73 @@ module.exports = app => app.component('dashboards', {
9
9
  data: () => ({
10
10
  status: 'loading',
11
11
  dashboards: [],
12
+ searchText: '',
12
13
  showCreateDashboardModal: false,
13
14
  showDeleteDashboardModal: null,
14
15
  openMenuId: null
15
16
  }),
17
+ computed: {
18
+ filteredDashboards() {
19
+ const searchWords = getSearchWords(this.searchText);
20
+ let dashboards = this.dashboards;
21
+ if (searchWords.length === 0) {
22
+ return sortPinnedFirst(dashboards);
23
+ }
24
+ const searchPatterns = searchWords.map(word => new RegExp(escapeRegExp(word), 'i'));
25
+
26
+ dashboards = dashboards.filter(dashboard => {
27
+ const searchableText = `${dashboard.title || ''} ${dashboard.description || ''}`;
28
+ return searchPatterns.every(pattern => pattern.test(searchableText));
29
+ });
30
+
31
+ return sortPinnedFirst(dashboards);
32
+ }
33
+ },
16
34
  methods: {
35
+ highlightTitle(value) {
36
+ return this.highlightSearchText(value, 'underline decoration-primary underline-offset-2');
37
+ },
38
+ highlightDescription(value) {
39
+ return this.highlightSearchText(value, 'font-semibold text-content-secondary');
40
+ },
41
+ highlightSearchText(value, className) {
42
+ const text = value || '';
43
+ const searchWords = getSearchWords(this.searchText);
44
+ if (searchWords.length === 0) {
45
+ return escapeHtml(text);
46
+ }
47
+ const searchPattern = new RegExp(searchWords.map(escapeRegExp).join('|'), 'ig');
48
+
49
+ let html = '';
50
+ let lastIndex = 0;
51
+ let match = searchPattern.exec(text);
52
+ while (match != null) {
53
+ html += escapeHtml(text.slice(lastIndex, match.index));
54
+ html += `<span class="${className}">${escapeHtml(match[0])}</span>`;
55
+ lastIndex = match.index + match[0].length;
56
+ match = searchPattern.exec(text);
57
+ }
58
+ html += escapeHtml(text.slice(lastIndex));
59
+ return html;
60
+ },
61
+ formatDate(value) {
62
+ if (!value) {
63
+ return '-';
64
+ }
65
+ const date = new Date(value);
66
+ if (Number.isNaN(date.getTime())) {
67
+ return '-';
68
+ }
69
+ return new Intl.DateTimeFormat(undefined, {
70
+ dateStyle: 'medium',
71
+ timeStyle: 'short'
72
+ }).format(date);
73
+ },
74
+ formatUser(dashboard) {
75
+ return dashboard.createdBy?.name ||
76
+ dashboard.createdBy?.email ||
77
+ (dashboard.createdById ? dashboard.createdById.toString() : '-');
78
+ },
17
79
  toggleMenu(id) {
18
80
  this.openMenuId = this.openMenuId === id ? null : id;
19
81
  },
@@ -33,8 +95,19 @@ module.exports = app => app.component('dashboards', {
33
95
  this.$toast.success('Dashboard deleted!');
34
96
  },
35
97
  insertNewDashboard(dashboard) {
36
- this.dashboards.push(dashboard);
98
+ this.dashboards.unshift(dashboard);
37
99
  this.showCreateDashboardModal = false;
100
+ },
101
+ async togglePin(dashboard) {
102
+ if (!dashboard) {
103
+ return;
104
+ }
105
+
106
+ const { doc } = await api.Dashboard.updateDashboard({
107
+ dashboardId: dashboard._id,
108
+ isPinned: !dashboard.isPinned
109
+ });
110
+ dashboard.isPinned = doc.isPinned;
38
111
  }
39
112
  },
40
113
  directives: {
@@ -58,3 +131,25 @@ module.exports = app => app.component('dashboards', {
58
131
  this.status = 'loaded';
59
132
  }
60
133
  });
134
+
135
+ function escapeHtml(value) {
136
+ return value.toString().
137
+ replace(/&/g, '&amp;').
138
+ replace(/</g, '&lt;').
139
+ replace(/>/g, '&gt;').
140
+ replace(/"/g, '&quot;').
141
+ replace(/'/g, '&#39;');
142
+ }
143
+
144
+ function escapeRegExp(value) {
145
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
146
+ }
147
+
148
+ function getSearchWords(searchText) {
149
+ return Array.from(new Set(searchText.trim().split(/\s+/).filter(Boolean))).
150
+ sort((a, b) => b.length - a.length);
151
+ }
152
+
153
+ function sortPinnedFirst(dashboards) {
154
+ return dashboards.slice().sort((a, b) => Number(Boolean(b.isPinned)) - Number(Boolean(a.isPinned)));
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.",
5
5
  "homepage": "https://mongoosestudio.app/",
6
6
  "repository": {