@mongoosejs/studio 0.0.101 → 0.0.102

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.
@@ -32,24 +32,27 @@ module.exports = ({ db }) => async function getDashboard(params) {
32
32
  const dashboard = await Dashboard.findOne({ _id: dashboardId });
33
33
  if (evaluate) {
34
34
  let result = null;
35
- const startExec = startDashboardExec(dashboardId, $workspaceId, authorization);
35
+ const startExec = startDashboardEvaluate(dashboardId, $workspaceId, authorization);
36
+ // Avoid unhandled promise rejection since we handle the promise later.
37
+ startExec.catch(() => {});
36
38
  try {
37
39
  result = await dashboard.evaluate();
38
40
  } catch (error) {
39
41
  return { dashboard, error: { message: error.message } };
40
42
  }
41
43
 
42
- await startExec.then(({ dashboardResult }) => {
44
+ const { dashboardResult } = await startExec.then(({ dashboardResult }) => {
43
45
  if (!dashboardResult) {
44
46
  return;
45
47
  }
46
48
  return completeDashboardEvaluate(dashboardResult._id, $workspaceId, authorization, result);
47
49
  });
48
50
 
49
- return { dashboard, result };
51
+ return { dashboard, dashboardResult };
52
+ } else {
53
+ const { dashboardResults } = await getDashboardResults(dashboardId, $workspaceId, authorization);
54
+ return { dashboard, dashboardResults };
50
55
  }
51
-
52
- return { dashboard };
53
56
  };
54
57
 
55
58
  async function completeDashboardEvaluate(dashboardResultId, workspaceId, authorization, result) {
@@ -60,7 +63,7 @@ async function completeDashboardEvaluate(dashboardResultId, workspaceId, authori
60
63
  if (authorization) {
61
64
  headers.Authorization = authorization;
62
65
  }
63
- const response = await fetch('http://localhost:7777/.netlify/functions/completeDashboardEvaluate', {
66
+ const response = await fetch('https://mongoose-js.netlify.app/.netlify/functions/completeDashboardEvaluate', {
64
67
  method: 'POST',
65
68
  headers,
66
69
  body: JSON.stringify({
@@ -81,7 +84,7 @@ async function completeDashboardEvaluate(dashboardResultId, workspaceId, authori
81
84
  return await response.json();
82
85
  }
83
86
 
84
- async function startDashboardExec(dashboardId, workspaceId, authorization) {
87
+ async function startDashboardEvaluate(dashboardId, workspaceId, authorization) {
85
88
  if (!workspaceId) {
86
89
  return {};
87
90
  }
@@ -89,7 +92,7 @@ async function startDashboardExec(dashboardId, workspaceId, authorization) {
89
92
  if (authorization) {
90
93
  headers.Authorization = authorization;
91
94
  }
92
- const response = await fetch('http://localhost:7777/.netlify/functions/startDashboardExec', {
95
+ const response = await fetch('https://mongoose-js.netlify.app/.netlify/functions/startDashboardEvaluate', {
93
96
  method: 'POST',
94
97
  headers,
95
98
  body: JSON.stringify({
@@ -100,7 +103,34 @@ async function startDashboardExec(dashboardId, workspaceId, authorization) {
100
103
  }).then(response => {
101
104
  if (response.status < 200 || response.status >= 400) {
102
105
  return response.json().then(data => {
103
- throw new Error(`startDashboardExec error: ${data.message}`);
106
+ throw new Error(`startDashboardEvaluate error: ${data.message}`);
107
+ });
108
+ }
109
+ return response;
110
+ });
111
+
112
+ return await response.json();
113
+ }
114
+
115
+ async function getDashboardResults(dashboardId, workspaceId, authorization) {
116
+ if (!workspaceId) {
117
+ return {};
118
+ }
119
+ const headers = { 'Content-Type': 'application/json' };
120
+ if (authorization) {
121
+ headers.Authorization = authorization;
122
+ }
123
+ const response = await fetch('https://mongoose-js.netlify.app/.netlify/functions/getDashboardResults', {
124
+ method: 'POST',
125
+ headers,
126
+ body: JSON.stringify({
127
+ dashboardId,
128
+ workspaceId
129
+ })
130
+ }).then(response => {
131
+ if (response.status < 200 || response.status >= 400) {
132
+ return response.json().then(data => {
133
+ throw new Error(`getDashboardResults error: ${data.message}`);
104
134
  });
105
135
  }
106
136
  return response;
@@ -52,11 +52,16 @@ module.exports = ({ db }) => async function getDocuments(params) {
52
52
  }
53
53
 
54
54
  const hasSort = typeof sort === 'object' && sort != null && Object.keys(sort).length > 0;
55
- const docs = await Model.
55
+ const cursor = await Model.
56
56
  find(filter == null ? {} : filter).
57
57
  limit(limit).
58
58
  skip(skip).
59
- sort(hasSort ? sort : { _id: -1 });
59
+ sort(hasSort ? sort : { _id: -1 }).
60
+ cursor();
61
+ const docs = [];
62
+ for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
63
+ docs.push(doc);
64
+ }
60
65
 
61
66
  const schemaPaths = {};
62
67
  for (const path of Object.keys(Model.schema.paths)) {
@@ -783,7 +783,7 @@ const template = __webpack_require__(/*! ./dashboard-chart.html */ "./frontend/s
783
783
 
784
784
  module.exports = app => app.component('dashboard-chart', {
785
785
  template: template,
786
- props: ['value'],
786
+ props: ['value', 'responsive'],
787
787
  mounted() {
788
788
  const ctx = this.$refs.chart.getContext('2d');
789
789
  const chart = new Chart(ctx, this.value.$chart);
@@ -892,7 +892,7 @@ const template = __webpack_require__(/*! ./dashboard-result.html */ "./frontend/
892
892
 
893
893
  module.exports = app => app.component('dashboard-result', {
894
894
  template: template,
895
- props: ['result'],
895
+ props: ['result', 'finishedEvaluatingAt'],
896
896
  mounted: async function() {
897
897
  },
898
898
  methods: {
@@ -961,7 +961,7 @@ module.exports = app => app.component('dashboard', {
961
961
  description: '',
962
962
  showEditor: false,
963
963
  dashboard: null,
964
- result: null,
964
+ dashboardResults: [],
965
965
  errorMessage: null
966
966
  };
967
967
  },
@@ -978,11 +978,32 @@ module.exports = app => app.component('dashboard', {
978
978
  } else {
979
979
  this.errorMessage = update.error.message;
980
980
  }
981
+ },
982
+ async evaluateDashboard() {
983
+ this.status = 'evaluating';
984
+ try {
985
+ const { dashboard, dashboardResult, error } = await api.Dashboard.getDashboard({ dashboardId: this.dashboardId, evaluate: true });
986
+ this.dashboard = dashboard;
987
+ if (error) {
988
+ this.errorMessage = error.message;
989
+ }
990
+ this.code = this.dashboard.code;
991
+ this.title = this.dashboard.title;
992
+ this.description = this.dashboard.description ?? '';
993
+ this.dashboardResults.unshift(dashboardResult);
994
+ } finally {
995
+ this.status = 'loaded';
996
+ }
997
+ }
998
+ },
999
+ computed: {
1000
+ dashboardResult() {
1001
+ return this.dashboardResults.length > 0 ? this.dashboardResults[0] : null;
981
1002
  }
982
1003
  },
983
1004
  mounted: async function() {
984
1005
  this.showEditor = this.$route.query.edit;
985
- const { dashboard, result, error } = await api.Dashboard.getDashboard({ dashboardId: this.dashboardId, evaluate: !this.showEditor });
1006
+ const { dashboard, dashboardResults, error } = await api.Dashboard.getDashboard({ dashboardId: this.dashboardId, evaluate: false });
986
1007
  if (!dashboard) {
987
1008
  return;
988
1009
  }
@@ -993,7 +1014,7 @@ module.exports = app => app.component('dashboard', {
993
1014
  this.code = this.dashboard.code;
994
1015
  this.title = this.dashboard.title;
995
1016
  this.description = this.dashboard.description ?? '';
996
- this.result = result;
1017
+ this.dashboardResults = dashboardResults;
997
1018
  this.status = 'loaded';
998
1019
  }
999
1020
  });
@@ -1756,6 +1777,26 @@ module.exports = app => app.component('export-query-results', {
1756
1777
  }
1757
1778
  });
1758
1779
 
1780
+ /***/ }),
1781
+
1782
+ /***/ "./frontend/src/format.js":
1783
+ /*!********************************!*\
1784
+ !*** ./frontend/src/format.js ***!
1785
+ \********************************/
1786
+ /***/ ((__unused_webpack_module, exports) => {
1787
+
1788
+ "use strict";
1789
+
1790
+
1791
+ exports.isoToLongDateTime = function isoToLongDateTime(str) {
1792
+ if (!str) {
1793
+ return 'Unknown';
1794
+ }
1795
+ const date = new Date(str);
1796
+ return date.toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', hour: 'numeric', minute: 'numeric' });
1797
+ };
1798
+
1799
+
1759
1800
  /***/ }),
1760
1801
 
1761
1802
  /***/ "./frontend/src/index.js":
@@ -1775,6 +1816,7 @@ const { version } = __webpack_require__(/*! ../../package.json */ "./package.jso
1775
1816
  console.log(`Mongoose Studio Version ${version}`);
1776
1817
 
1777
1818
  const api = __webpack_require__(/*! ./api */ "./frontend/src/api.js");
1819
+ const format = __webpack_require__(/*! ./format */ "./frontend/src/format.js");
1778
1820
  const mothership = __webpack_require__(/*! ./mothership */ "./frontend/src/mothership.js");
1779
1821
  const { routes } = __webpack_require__(/*! ./routes */ "./frontend/src/routes.js");
1780
1822
  const vanillatoasts = __webpack_require__(/*! vanillatoasts */ "./node_modules/vanillatoasts/vanillatoasts.js");
@@ -1912,6 +1954,7 @@ router.beforeEach((to, from, next) => {
1912
1954
  }
1913
1955
  });
1914
1956
 
1957
+ app.config.globalProperties = { format };
1915
1958
  app.use(router);
1916
1959
 
1917
1960
  app.mount('#content');
@@ -3220,6 +3263,8 @@ var map = {
3220
3263
  "./export-query-results/export-query-results.css": "./frontend/src/export-query-results/export-query-results.css",
3221
3264
  "./export-query-results/export-query-results.html": "./frontend/src/export-query-results/export-query-results.html",
3222
3265
  "./export-query-results/export-query-results.js": "./frontend/src/export-query-results/export-query-results.js",
3266
+ "./format": "./frontend/src/format.js",
3267
+ "./format.js": "./frontend/src/format.js",
3223
3268
  "./index": "./frontend/src/index.js",
3224
3269
  "./index.js": "./frontend/src/index.js",
3225
3270
  "./list-array/list-array": "./frontend/src/list-array/list-array.js",
@@ -3917,7 +3962,7 @@ module.exports = "<button v-bind=\"attrsToBind\" :disabled=\"isDisabled\" @click
3917
3962
  /***/ ((module) => {
3918
3963
 
3919
3964
  "use strict";
3920
- module.exports = "<div class=\"relative border rounded bg-gray-100 text-black text-sm overflow-hidden\">\n <div class=\"flex border-b pt-[1px] text-xs font-medium bg-gray-200\">\n <button\n class=\"px-3 py-1 border-r border-gray-300 hover:bg-green-300\"\n :class=\"{'bg-gray-300': activeTab === 'code', 'bg-green-300': activeTab === 'code'}\"\n @click=\"activeTab = 'code'\">\n Code\n </button>\n <button\n class=\"px-3 py-1 hover:bg-green-300\"\n :class=\"{'bg-green-300': activeTab === 'output'}\"\n @click=\"activeTab = 'output'\">\n Output\n </button>\n <div class=\"ml-auto mr-1 flex\">\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-gray-500 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center\"\n @click=\"copyOutput\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3\" />\n </svg>\n </button>\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-blue-500 text-white border-none rounded cursor-pointer hover:bg-blue-600 transition-colors flex items-center\"\n @click=\"openDetailModal\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 1v4m0 0h-4m4 0l-5-5\" />\n </svg>\n </button>\n <async-button\n class=\"px-2 py-1 text-xs bg-green-500 text-white border-none rounded cursor-pointer hover:bg-green-600 transition-colors disabled:bg-gray-400\"\n @click=\"executeScript(message, script)\">\n Execute\n </async-button>\n </div>\n </div>\n\n <pre class=\"p-3 whitespace-pre-wrap max-h-[50vh] max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] overflow-y-auto\" v-show=\"activeTab === 'code'\"><code v-text=\"script\" ref=\"code\" :class=\"'language-' + language\"></code></pre>\n\n <div class=\"p-3 whitespace-pre-wrap max-h-[50vh] overflow-y-auto bg-white border-t max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] relative\" v-show=\"activeTab === 'output'\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" />\n <pre v-else>{{ message.executionResult?.output || 'No output' }}</pre>\n </div>\n\n <modal ref=\"outputModal\" v-if=\"showDetailModal\" containerClass=\"!h-[90vh] !w-[90vw]\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showDetailModal = false;\">&times;</div>\n <div class=\"h-full overflow-auto\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" />\n <pre v-else class=\"whitespace-pre-wrap\">{{ message.executionResult?.output || 'No output' }}</pre>\n </div>\n </template>\n </modal>\n</div>\n";
3965
+ module.exports = "<div class=\"relative border rounded bg-gray-100 text-black text-sm overflow-hidden\">\n <div class=\"flex border-b pt-[1px] text-xs font-medium bg-gray-200\">\n <button\n class=\"px-3 py-1 border-r border-gray-300 hover:bg-green-300\"\n :class=\"{'bg-gray-300': activeTab === 'code', 'bg-green-300': activeTab === 'code'}\"\n @click=\"activeTab = 'code'\">\n Code\n </button>\n <button\n class=\"px-3 py-1 hover:bg-green-300\"\n :class=\"{'bg-green-300': activeTab === 'output'}\"\n @click=\"activeTab = 'output'\">\n Output\n </button>\n <div class=\"ml-auto mr-1 flex\">\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-gray-500 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center\"\n @click=\"copyOutput\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3\" />\n </svg>\n </button>\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-blue-500 text-white border-none rounded cursor-pointer hover:bg-blue-600 transition-colors flex items-center\"\n @click=\"openDetailModal\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 1v4m0 0h-4m4 0l-5-5\" />\n </svg>\n </button>\n <async-button\n class=\"px-2 py-1 text-xs bg-green-500 text-white border-none rounded cursor-pointer hover:bg-green-600 transition-colors disabled:bg-gray-400\"\n @click=\"executeScript(message, script)\">\n Execute\n </async-button>\n </div>\n </div>\n\n <pre class=\"p-3 whitespace-pre-wrap max-h-[50vh] max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] overflow-y-auto\" v-show=\"activeTab === 'code'\"><code v-text=\"script\" ref=\"code\" :class=\"'language-' + language\"></code></pre>\n\n <div class=\"p-3 whitespace-pre-wrap max-h-[50vh] overflow-y-auto bg-white border-t max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] relative\" v-show=\"activeTab === 'output'\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" />\n <pre v-else>{{ message.executionResult?.output || 'No output' }}</pre>\n </div>\n\n <modal ref=\"outputModal\" v-if=\"showDetailModal\" containerClass=\"!h-[90vh] !w-[90vw]\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showDetailModal = false;\">&times;</div>\n <div class=\"h-full overflow-auto\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" :responsive=\"true\" />\n <pre v-else class=\"whitespace-pre-wrap\">{{ message.executionResult?.output || 'No output' }}</pre>\n </div>\n </template>\n </modal>\n</div>\n";
3921
3966
 
3922
3967
  /***/ }),
3923
3968
 
@@ -4005,7 +4050,7 @@ module.exports = "<div>\n <div class=\"mb-2\">\n <textarea class=\"border bo
4005
4050
  /***/ ((module) => {
4006
4051
 
4007
4052
  "use strict";
4008
- module.exports = "<div>\n <div v-if=\"header\" class=\"border-b border-gray-100 px-2 pb-2 text-xl font-bold\">\n {{header}}\n </div>\n <div class=\"text-xl\">\n <canvas ref=\"chart\"></canvas>\n </div>\n</div>\n";
4053
+ module.exports = "<div :class=\"responsive ? 'h-full' : ''\">\n <div v-if=\"header\" class=\"border-b border-gray-100 px-2 pb-2 text-xl font-bold\">\n {{header}}\n </div>\n <div class=\"text-xl\" :class=\"responsive ? 'h-full' : ''\">\n <canvas ref=\"chart\"></canvas>\n </div>\n</div>\n";
4009
4054
 
4010
4055
  /***/ }),
4011
4056
 
@@ -4038,7 +4083,7 @@ module.exports = "<div class=\"py-2\">\n <div v-if=\"header\" class=\"border-b
4038
4083
  /***/ ((module) => {
4039
4084
 
4040
4085
  "use strict";
4041
- module.exports = "<div>\n <div v-if=\"Array.isArray(result)\">\n <div v-for=\"el in result\">\n <component\n class=\"bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl\"\n :is=\"getComponentForValue(res)\"\n :value=\"res\">\n </component>\n </div>\n </div>\n <div v-else>\n <component\n class=\"bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl\"\n :is=\"getComponentForValue(result)\"\n :value=\"result\">\n </component>\n </div>\n</div>\n";
4086
+ module.exports = "<div>\n <div v-if=\"Array.isArray(result)\">\n <div v-for=\"el in result\">\n <component\n class=\"bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl\"\n :is=\"getComponentForValue(res)\"\n :value=\"res\">\n </component>\n </div>\n </div>\n <div v-else>\n <component\n class=\"bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl\"\n :is=\"getComponentForValue(result)\"\n :value=\"result\">\n </component>\n </div>\n <div class=\"text-right text-sm text-gray-700 mt-1\" v-if=\"finishedEvaluatingAt\">\n Last Evaluated: {{ format.isoToLongDateTime(finishedEvaluatingAt) }}\n </div>\n</div>\n";
4042
4087
 
4043
4088
  /***/ }),
4044
4089
 
@@ -4060,7 +4105,7 @@ module.exports = "<div class=\"py-2\">\n <div v-if=\"header\" class=\"border-b
4060
4105
  /***/ ((module) => {
4061
4106
 
4062
4107
  "use strict";
4063
- module.exports = "<div class=\"dashboard px-1\">\n <div v-if=\"status === 'loading'\" class=\"max-w-5xl mx-auto text-center\">\n <img src=\"images/loader.gif\" class=\"inline mt-10\">\n </div>\n <div v-if=\"dashboard && status === 'loaded'\" class=\"max-w-5xl mx-auto\">\n <div class=\"flex items-center w-full\" v-if=\"!showEditor\">\n <h2 class=\"mt-4 mb-4 text-gray-900 font-semibold text-xl grow shrink\">{{title}}</h2>\n <div>\n <button\n @click=\"showEditor = true\"\n type=\"button\"\n class=\"rounded-md bg-teal-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-teal-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">\n <img src=\"images/edit.svg\" class=\"inline h-[1em]\" /> Edit\n </button>\n </div>\n </div>\n <div v-if=\"!showEditor\" class=\"mt-4 mb-4\">\n <dashboard-result :result=\"result\"></dashboard-result>\n </div>\n <div v-if=\"showEditor\" class=\"mt-4\">\n <edit-dashboard\n :dashboardId=\"dashboard._id\"\n :code=\"code\"\n :currentDescription=\"description\"\n :currentTitle=\"title\"\n @close=\"showEditor=false;\"\n @update=\"updateCode\"></edit-dashboard>\n </div>\n <div v-if=\"errorMessage\" class=\"rounded-md bg-red-50 p-4 mt-4\">\n <div class=\"flex\">\n <div class=\"flex-shrink-0\">\n <svg class=\"h-5 w-5 text-red-400\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\" data-slot=\"icon\">\n <path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-1.06-1.06L10 8.94 8.28 7.22Z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-sm font-medium text-red-800\">{{errorMessage}}</h3>\n </div>\n </div>\n </div>\n\n </div>\n <div v-if=\"!dashboard && status === 'loaded'\">\n No dashboard with the given id could be found.\n </div>\n</div>\n";
4108
+ module.exports = "<div class=\"dashboard px-1\">\n <div v-if=\"status === 'loading'\" class=\"max-w-5xl mx-auto text-center\">\n <img src=\"images/loader.gif\" class=\"inline mt-10\">\n </div>\n <div v-if=\"dashboard && status !== 'loading'\" class=\"max-w-5xl mx-auto\">\n <div class=\"flex items-center w-full\" v-if=\"!showEditor\">\n <h2 class=\"mt-4 mb-4 text-gray-900 font-semibold text-xl grow shrink\">{{title}}</h2>\n <div class=\"flex gap-2\">\n <button\n @click=\"showEditor = true\"\n type=\"button\"\n :disabled=\"status === 'evaluating'\"\n class=\"flex items-center rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600\">\n <img src=\"images/edit.svg\" class=\"inline h-[1.25em] mr-1\" /> Edit\n </button>\n\n <async-button\n @click=\"evaluateDashboard\"\n type=\"button\"\n :disabled=\"status === 'evaluating'\"\n class=\"flex items-center rounded-md bg-ultramarine-600 px-4 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600\"\n >\n <svg class=\"inline h-[1.25em] mr-1\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"m670-140 160-100-160-100v200ZM240-600h480v-80H240v80ZM720-40q-83 0-141.5-58.5T520-240q0-83 58.5-141.5T720-440q83 0 141.5 58.5T920-240q0 83-58.5 141.5T720-40ZM120-80v-680q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v267q-19-9-39-15t-41-9v-243H200v562h243q5 31 15.5 59T486-86l-6 6-60-60-60 60-60-60-60 60-60-60-60 60Zm120-200h203q3-21 9-41t15-39H240v80Zm0-160h284q38-37 88.5-58.5T720-520H240v80Zm-40 242v-562 562Z\"/></svg>\n Evaluate\n </async-button>\n </div>\n </div>\n <div v-if=\"!showEditor\" class=\"mt-4 mb-4\">\n <div v-if=\"dashboardResults.length === 0\">\n <div class=\"flex flex-col items-center justify-center py-8\">\n <p class=\"text-gray-700 text-base mb-4\">This dashboard hasn't been evaluated yet.</p>\n <async-button\n @click=\"evaluateDashboard\"\n type=\"button\"\n :disabled=\"status === 'evaluating'\"\n class=\"rounded-md bg-ultramarine-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600\"\n >\n Evaluate Dashboard\n </async-button>\n </div>\n </div>\n <div v-else>\n <dashboard-result :result=\"dashboardResult.result\" :finishedEvaluatingAt=\"dashboardResult.finishedEvaluatingAt\"></dashboard-result>\n </div>\n </div>\n <div v-if=\"showEditor\" class=\"mt-4\">\n <edit-dashboard\n :dashboardId=\"dashboard._id\"\n :code=\"code\"\n :currentDescription=\"description\"\n :currentTitle=\"title\"\n @close=\"showEditor=false;\"\n @update=\"updateCode\"></edit-dashboard>\n </div>\n <div v-if=\"errorMessage\" class=\"rounded-md bg-red-50 p-4 mt-4\">\n <div class=\"flex\">\n <div class=\"flex-shrink-0\">\n <svg class=\"h-5 w-5 text-red-400\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\" data-slot=\"icon\">\n <path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-1.06-1.06L10 8.94 8.28 7.22Z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-sm font-medium text-red-800\">{{errorMessage}}</h3>\n </div>\n </div>\n </div>\n\n </div>\n <div v-if=\"!dashboard && status !== 'loading'\">\n No dashboard with the given id could be found.\n </div>\n</div>\n";
4064
4109
 
4065
4110
  /***/ }),
4066
4111
 
@@ -14574,7 +14619,7 @@ var bson = /*#__PURE__*/Object.freeze({
14574
14619
  /***/ ((module) => {
14575
14620
 
14576
14621
  "use strict";
14577
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.101","description":"A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.","homepage":"https://studio.mongoosejs.io/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"dependencies":{"archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"0.0.26","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vanillatoasts":"^1.6.0","vue":"3.x","webpack":"5.x"},"peerDependencies":{"bson":"^5.5.1 || 6.x","express":"4.x","mongoose":"7.x || 8.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongoose":"8.x"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js"}}');
14622
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.102","description":"A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.","homepage":"https://studio.mongoosejs.io/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"dependencies":{"archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"0.0.26","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vanillatoasts":"^1.6.0","vue":"3.x","webpack":"5.x"},"peerDependencies":{"bson":"^5.5.1 || 6.x","express":"4.x","mongoose":"7.x || 8.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongoose":"8.x"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js"}}');
14578
14623
 
14579
14624
  /***/ })
14580
14625
 
@@ -866,8 +866,8 @@ video {
866
866
  height: 2rem;
867
867
  }
868
868
 
869
- .h-\[1em\] {
870
- height: 1em;
869
+ .h-\[1\.25em\] {
870
+ height: 1.25em;
871
871
  }
872
872
 
873
873
  .h-\[300px\] {
@@ -1563,6 +1563,11 @@ video {
1563
1563
  padding-bottom: 1.5rem;
1564
1564
  }
1565
1565
 
1566
+ .py-8 {
1567
+ padding-top: 2rem;
1568
+ padding-bottom: 2rem;
1569
+ }
1570
+
1566
1571
  .pb-2 {
1567
1572
  padding-bottom: 0.5rem;
1568
1573
  }
@@ -48,7 +48,7 @@
48
48
  <template #body>
49
49
  <div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showDetailModal = false;">&times;</div>
50
50
  <div class="h-full overflow-auto">
51
- <dashboard-chart v-if="message.executionResult?.output?.$chart" :value="message.executionResult?.output" />
51
+ <dashboard-chart v-if="message.executionResult?.output?.$chart" :value="message.executionResult?.output" :responsive="true" />
52
52
  <pre v-else class="whitespace-pre-wrap">{{ message.executionResult?.output || 'No output' }}</pre>
53
53
  </div>
54
54
  </template>
@@ -2,20 +2,46 @@
2
2
  <div v-if="status === 'loading'" class="max-w-5xl mx-auto text-center">
3
3
  <img src="images/loader.gif" class="inline mt-10">
4
4
  </div>
5
- <div v-if="dashboard && status === 'loaded'" class="max-w-5xl mx-auto">
5
+ <div v-if="dashboard && status !== 'loading'" class="max-w-5xl mx-auto">
6
6
  <div class="flex items-center w-full" v-if="!showEditor">
7
7
  <h2 class="mt-4 mb-4 text-gray-900 font-semibold text-xl grow shrink">{{title}}</h2>
8
- <div>
8
+ <div class="flex gap-2">
9
9
  <button
10
10
  @click="showEditor = true"
11
11
  type="button"
12
- class="rounded-md bg-teal-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-teal-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600">
13
- <img src="images/edit.svg" class="inline h-[1em]" /> Edit
12
+ :disabled="status === 'evaluating'"
13
+ class="flex items-center rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600">
14
+ <img src="images/edit.svg" class="inline h-[1.25em] mr-1" /> Edit
14
15
  </button>
16
+
17
+ <async-button
18
+ @click="evaluateDashboard"
19
+ type="button"
20
+ :disabled="status === 'evaluating'"
21
+ class="flex items-center rounded-md bg-ultramarine-600 px-4 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600"
22
+ >
23
+ <svg class="inline h-[1.25em] mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="m670-140 160-100-160-100v200ZM240-600h480v-80H240v80ZM720-40q-83 0-141.5-58.5T520-240q0-83 58.5-141.5T720-440q83 0 141.5 58.5T920-240q0 83-58.5 141.5T720-40ZM120-80v-680q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v267q-19-9-39-15t-41-9v-243H200v562h243q5 31 15.5 59T486-86l-6 6-60-60-60 60-60-60-60 60-60-60-60 60Zm120-200h203q3-21 9-41t15-39H240v80Zm0-160h284q38-37 88.5-58.5T720-520H240v80Zm-40 242v-562 562Z"/></svg>
24
+ Evaluate
25
+ </async-button>
15
26
  </div>
16
27
  </div>
17
28
  <div v-if="!showEditor" class="mt-4 mb-4">
18
- <dashboard-result :result="result"></dashboard-result>
29
+ <div v-if="dashboardResults.length === 0">
30
+ <div class="flex flex-col items-center justify-center py-8">
31
+ <p class="text-gray-700 text-base mb-4">This dashboard hasn't been evaluated yet.</p>
32
+ <async-button
33
+ @click="evaluateDashboard"
34
+ type="button"
35
+ :disabled="status === 'evaluating'"
36
+ class="rounded-md bg-ultramarine-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600"
37
+ >
38
+ Evaluate Dashboard
39
+ </async-button>
40
+ </div>
41
+ </div>
42
+ <div v-else>
43
+ <dashboard-result :result="dashboardResult.result" :finishedEvaluatingAt="dashboardResult.finishedEvaluatingAt"></dashboard-result>
44
+ </div>
19
45
  </div>
20
46
  <div v-if="showEditor" class="mt-4">
21
47
  <edit-dashboard
@@ -40,7 +66,7 @@
40
66
  </div>
41
67
 
42
68
  </div>
43
- <div v-if="!dashboard && status === 'loaded'">
69
+ <div v-if="!dashboard && status !== 'loading'">
44
70
  No dashboard with the given id could be found.
45
71
  </div>
46
72
  </div>
@@ -14,7 +14,7 @@ module.exports = app => app.component('dashboard', {
14
14
  description: '',
15
15
  showEditor: false,
16
16
  dashboard: null,
17
- result: null,
17
+ dashboardResults: [],
18
18
  errorMessage: null
19
19
  };
20
20
  },
@@ -31,11 +31,32 @@ module.exports = app => app.component('dashboard', {
31
31
  } else {
32
32
  this.errorMessage = update.error.message;
33
33
  }
34
+ },
35
+ async evaluateDashboard() {
36
+ this.status = 'evaluating';
37
+ try {
38
+ const { dashboard, dashboardResult, error } = await api.Dashboard.getDashboard({ dashboardId: this.dashboardId, evaluate: true });
39
+ this.dashboard = dashboard;
40
+ if (error) {
41
+ this.errorMessage = error.message;
42
+ }
43
+ this.code = this.dashboard.code;
44
+ this.title = this.dashboard.title;
45
+ this.description = this.dashboard.description ?? '';
46
+ this.dashboardResults.unshift(dashboardResult);
47
+ } finally {
48
+ this.status = 'loaded';
49
+ }
50
+ }
51
+ },
52
+ computed: {
53
+ dashboardResult() {
54
+ return this.dashboardResults.length > 0 ? this.dashboardResults[0] : null;
34
55
  }
35
56
  },
36
57
  mounted: async function() {
37
58
  this.showEditor = this.$route.query.edit;
38
- const { dashboard, result, error } = await api.Dashboard.getDashboard({ dashboardId: this.dashboardId, evaluate: !this.showEditor });
59
+ const { dashboard, dashboardResults, error } = await api.Dashboard.getDashboard({ dashboardId: this.dashboardId, evaluate: false });
39
60
  if (!dashboard) {
40
61
  return;
41
62
  }
@@ -46,7 +67,7 @@ module.exports = app => app.component('dashboard', {
46
67
  this.code = this.dashboard.code;
47
68
  this.title = this.dashboard.title;
48
69
  this.description = this.dashboard.description ?? '';
49
- this.result = result;
70
+ this.dashboardResults = dashboardResults;
50
71
  this.status = 'loaded';
51
72
  }
52
73
  });
@@ -1,8 +1,8 @@
1
- <div>
1
+ <div :class="responsive ? 'h-full' : ''">
2
2
  <div v-if="header" class="border-b border-gray-100 px-2 pb-2 text-xl font-bold">
3
3
  {{header}}
4
4
  </div>
5
- <div class="text-xl">
5
+ <div class="text-xl" :class="responsive ? 'h-full' : ''">
6
6
  <canvas ref="chart"></canvas>
7
7
  </div>
8
8
  </div>
@@ -4,7 +4,7 @@ const template = require('./dashboard-chart.html');
4
4
 
5
5
  module.exports = app => app.component('dashboard-chart', {
6
6
  template: template,
7
- props: ['value'],
7
+ props: ['value', 'responsive'],
8
8
  mounted() {
9
9
  const ctx = this.$refs.chart.getContext('2d');
10
10
  const chart = new Chart(ctx, this.value.$chart);
@@ -15,4 +15,7 @@
15
15
  :value="result">
16
16
  </component>
17
17
  </div>
18
+ <div class="text-right text-sm text-gray-700 mt-1" v-if="finishedEvaluatingAt">
19
+ Last Evaluated: {{ format.isoToLongDateTime(finishedEvaluatingAt) }}
20
+ </div>
18
21
  </div>
@@ -7,7 +7,7 @@ const template = require('./dashboard-result.html');
7
7
 
8
8
  module.exports = app => app.component('dashboard-result', {
9
9
  template: template,
10
- props: ['result'],
10
+ props: ['result', 'finishedEvaluatingAt'],
11
11
  mounted: async function() {
12
12
  },
13
13
  methods: {
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ exports.isoToLongDateTime = function isoToLongDateTime(str) {
4
+ if (!str) {
5
+ return 'Unknown';
6
+ }
7
+ const date = new Date(str);
8
+ return date.toLocaleString('en-US', { month: 'short', day: '2-digit', year: 'numeric', hour: 'numeric', minute: 'numeric' });
9
+ };
@@ -8,6 +8,7 @@ const { version } = require('../../package.json');
8
8
  console.log(`Mongoose Studio Version ${version}`);
9
9
 
10
10
  const api = require('./api');
11
+ const format = require('./format');
11
12
  const mothership = require('./mothership');
12
13
  const { routes } = require('./routes');
13
14
  const vanillatoasts = require('vanillatoasts');
@@ -148,6 +149,7 @@ router.beforeEach((to, from, next) => {
148
149
  }
149
150
  });
150
151
 
152
+ app.config.globalProperties = { format };
151
153
  app.use(router);
152
154
 
153
155
  app.mount('#content');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.0.101",
3
+ "version": "0.0.102",
4
4
  "description": "A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.",
5
5
  "homepage": "https://studio.mongoosejs.io/",
6
6
  "repository": {