@mongoosejs/studio 0.0.88 → 0.0.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/astra.js +2 -2
  2. package/backend/actions/ChatMessage/executeScript.js +7 -1
  3. package/backend/actions/ChatThread/createChatMessage.js +8 -2
  4. package/backend/actions/ChatThread/createChatThread.js +6 -0
  5. package/backend/actions/ChatThread/getChatThread.js +7 -1
  6. package/backend/actions/ChatThread/listChatThreads.js +8 -2
  7. package/backend/actions/Dashboard/createDashboard.js +9 -2
  8. package/backend/actions/Dashboard/deleteDashboard.js +8 -3
  9. package/backend/actions/Dashboard/getDashboard.js +9 -3
  10. package/backend/actions/Dashboard/getDashboards.js +5 -2
  11. package/backend/actions/Dashboard/updateDashboard.js +10 -4
  12. package/backend/actions/Model/createDocument.js +5 -6
  13. package/backend/actions/Model/deleteDocument.js +5 -5
  14. package/backend/actions/Model/deleteDocuments.js +6 -6
  15. package/backend/actions/Model/dropIndex.js +36 -0
  16. package/backend/actions/Model/exportQueryResults.js +7 -1
  17. package/backend/actions/Model/getDocument.js +9 -3
  18. package/backend/actions/Model/getDocuments.js +7 -0
  19. package/backend/actions/Model/getIndexes.js +6 -2
  20. package/backend/actions/Model/listModels.js +14 -2
  21. package/backend/actions/Model/updateDocument.js +5 -5
  22. package/backend/actions/Model/updateDocuments.js +5 -6
  23. package/backend/authorize.js +36 -0
  24. package/frontend/public/app.js +36 -9
  25. package/frontend/src/index.js +6 -4
  26. package/frontend/src/models/models.html +8 -7
  27. package/frontend/src/models/models.js +8 -1
  28. package/frontend/src/mothership.js +8 -0
  29. package/frontend/src/splash/splash.html +19 -7
  30. package/frontend/src/splash/splash.js +4 -0
  31. package/frontend/src/team/new-invitation/new-invitation.html +5 -1
  32. package/frontend/src/team/new-invitation/new-invitation.js +6 -0
  33. package/frontend/src/team/team.html +2 -1
  34. package/package.json +1 -1
package/astra.js CHANGED
@@ -154,6 +154,6 @@ void async function main() {
154
154
  # Astra Notes
155
155
 
156
156
  1. Must use collections. Tables don't support `countDocuments()` or `estimatedDocumentCount()`.
157
- 2. Annoying issue: collections don't let you store keys that start with '$'. Ended up creating separate connection to store ChatMessages in MongoDB.
158
- 3. `countDocuments()` with filter erroring out with more than 1000 documents is problematic, e.g. https://studio-astra-demo.netlify.app/imdb/#/?search={genres:'Drama'} fails
157
+ 2. Collections don't let you store keys that start with '$', which is problematic for `$chart`. Ended up creating separate connection to store ChatMessages in MongoDB.
158
+ 3. `countDocuments()` with filter erroring out with more than 1000 documents caused trouble. Worked around it by converting `countDocuments()` to `find()` using Mongoose middleware.
159
159
  */
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
4
5
  const mongoose = require('mongoose');
5
6
  const vm = require('vm');
6
7
 
@@ -12,7 +13,10 @@ const ExecuteScriptParams = new Archetype({
12
13
  $type: mongoose.Types.ObjectId
13
14
  },
14
15
  script: {
15
- $type: String
16
+ $type: 'string'
17
+ },
18
+ roles: {
19
+ $type: ['string']
16
20
  }
17
21
  }).compile('ExecuteScriptParams');
18
22
 
@@ -20,6 +24,8 @@ module.exports = ({ db, studioConnection }) => async function executeScript(para
20
24
  const { userId, chatMessageId, script } = new ExecuteScriptParams(params);
21
25
  const ChatMessage = studioConnection.model('__Studio_ChatMessage');
22
26
 
27
+ await authorize('ChatMessage.executeScript', roles);
28
+
23
29
  const chatMessage = await ChatMessage.findById(chatMessageId);
24
30
  if (!chatMessage) {
25
31
  throw new Error('Chat message not found');
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
4
5
  const getModelDescriptions = require('../../helpers/getModelDescriptions');
5
6
  const mongoose = require('mongoose');
6
7
 
@@ -17,6 +18,9 @@ const CreateChatMessageParams = new Archetype({
17
18
  authorization: {
18
19
  $type: 'string',
19
20
  $required: true
21
+ },
22
+ roles: {
23
+ $type: ['string'],
20
24
  }
21
25
  }).compile('CreateChatMessageParams');
22
26
 
@@ -56,10 +60,12 @@ Here is a description of the user's models. Assume these are the only models ava
56
60
  `.trim();
57
61
 
58
62
  module.exports = ({ db, studioConnection, options }) => async function createChatMessage(params) {
59
- const { chatThreadId, userId, content, script, authorization } = new CreateChatMessageParams(params);
63
+ const { chatThreadId, userId, content, script, authorization, roles } = new CreateChatMessageParams(params);
60
64
  const ChatThread = studioConnection.model('__Studio_ChatThread');
61
65
  const ChatMessage = studioConnection.model('__Studio_ChatMessage');
62
66
 
67
+ await authorize('ChatThread.createChatMessage', roles);
68
+
63
69
  // Check that the user owns the thread
64
70
  const chatThread = await ChatThread.findOne({ _id: chatThreadId });
65
71
  if (!chatThread) {
@@ -94,7 +100,7 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
94
100
  if (options.context) {
95
101
  llmMessages.unshift({
96
102
  role: 'system',
97
- content: context
103
+ content: options.context
98
104
  });
99
105
  }
100
106
 
@@ -1,11 +1,15 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
4
5
  const mongoose = require('mongoose');
5
6
 
6
7
  const CreateChatThreadParams = new Archetype({
7
8
  userId: {
8
9
  $type: mongoose.Types.ObjectId
10
+ },
11
+ roles: {
12
+ $type: ['string'],
9
13
  }
10
14
  }).compile('CreateChatThreadParams');
11
15
 
@@ -13,6 +17,8 @@ module.exports = ({ studioConnection }) => async function createChatThread(param
13
17
  const { userId } = new CreateChatThreadParams(params);
14
18
  const ChatThread = studioConnection.model('__Studio_ChatThread');
15
19
 
20
+ await authorize('ChatThread.createChatThread', roles);
21
+
16
22
  const chatThread = await ChatThread.create({ userId });
17
23
 
18
24
  return { chatThread };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
4
5
  const mongoose = require('mongoose');
5
6
 
6
7
  const GetChatThreadParams = new Archetype({
@@ -9,14 +10,19 @@ const GetChatThreadParams = new Archetype({
9
10
  },
10
11
  userId: {
11
12
  $type: mongoose.Types.ObjectId
13
+ },
14
+ roles: {
15
+ $type: ['string']
12
16
  }
13
17
  }).compile('GetChatThreadParams');
14
18
 
15
19
  module.exports = ({ db, studioConnection }) => async function getChatThread(params) {
16
- const { chatThreadId, userId } = new GetChatThreadParams(params);
20
+ const { chatThreadId, userId, roles } = new GetChatThreadParams(params);
17
21
  const ChatThread = studioConnection.model('__Studio_ChatThread');
18
22
  const ChatMessage = studioConnection.model('__Studio_ChatMessage');
19
23
 
24
+ await authorize('ChatThread.getChatThread', roles);
25
+
20
26
  const chatThread = await ChatThread.findById(chatThreadId);
21
27
 
22
28
  if (!chatThread) {
@@ -1,19 +1,25 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
4
5
  const mongoose = require('mongoose');
5
6
 
6
7
  const ListChatThreadsParams = new Archetype({
7
8
  userId: {
8
9
  $type: mongoose.Types.ObjectId
10
+ },
11
+ roles: {
12
+ $type: ['string']
9
13
  }
10
14
  }).compile('ListChatThreadsParams');
11
15
 
12
16
  module.exports = ({ db, studioConnection }) => async function listChatThreads(params) {
13
- // Just validate the params object, but no actual parameters needed
14
- const { userId } = new ListChatThreadsParams(params);
17
+ // Validate the params object
18
+ const { userId, roles } = new ListChatThreadsParams(params);
15
19
  const ChatThread = studioConnection.model('__Studio_ChatThread');
16
20
 
21
+ await authorize('ChatThread.listChatThreads', roles);
22
+
17
23
  // Get all chat threads
18
24
  const chatThreads = await ChatThread.find(userId ? { userId } : {})
19
25
  .sort({ updatedAt: -1 }); // Sort by most recently updated
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
+
2
3
  const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
3
5
 
4
6
  const CreateDashboardParams = new Archetype({
5
7
  title: {
@@ -9,14 +11,19 @@ const CreateDashboardParams = new Archetype({
9
11
  code: {
10
12
  $type: 'string',
11
13
  $required: true
14
+ },
15
+ roles: {
16
+ $type: ['string']
12
17
  }
13
18
  }).compile('CreateDashboardParams');
14
19
 
15
20
  module.exports = ({ db }) => async function createDashboard(params) {
16
- const { title, code } = new CreateDashboardParams(params);
21
+ const { title, code, roles } = new CreateDashboardParams(params);
17
22
  const Dashboard = db.model('__Studio_Dashboard');
18
23
 
24
+ await authorize('Dashboard.createDashboard', roles);
25
+
19
26
  const dashboard = await Dashboard.create({ title, code });
20
27
 
21
28
  return { dashboard };
22
- };
29
+ };
@@ -1,19 +1,24 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
- const vm = require('vm');
4
+ const authorize = require('../../authorize');
5
5
 
6
6
  const DeleteDashboardParams = new Archetype({
7
7
  dashboardId: {
8
8
  $type: 'string',
9
9
  $required: true
10
10
  },
11
+ roles: {
12
+ $type: ['string']
13
+ }
11
14
  }).compile('DeleteDashboardParams');
12
15
 
13
16
  module.exports = ({ db }) => async function deleteDashboard(params) {
14
- const { dashboardId } = new DeleteDashboardParams(params);
17
+ const { dashboardId, roles } = new DeleteDashboardParams(params);
15
18
  const Dashboard = db.model('__Studio_Dashboard');
16
19
 
20
+ await authorize('Dashboard.deleteDashboard', roles);
21
+
17
22
  const result = await Dashboard.deleteOne({ _id: dashboardId }).orFail();
18
23
  return { result };
19
- };
24
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const vm = require('vm');
5
+ const authorize = require('../../authorize');
5
6
 
6
7
  const GetDashboardParams = new Archetype({
7
8
  dashboardId: {
@@ -10,13 +11,18 @@ const GetDashboardParams = new Archetype({
10
11
  },
11
12
  evaluate: {
12
13
  $type: 'boolean'
14
+ },
15
+ roles: {
16
+ $type: ['string']
13
17
  }
14
18
  }).compile('GetDashboardParams');
15
19
 
16
20
  module.exports = ({ db }) => async function getDashboard(params) {
17
- const { dashboardId, evaluate } = new GetDashboardParams(params);
21
+ const { dashboardId, evaluate, roles } = new GetDashboardParams(params);
18
22
  const Dashboard = db.model('__Studio_Dashboard');
19
23
 
24
+ await authorize('Dashboard.getDashboard', roles);
25
+
20
26
  const dashboard = await Dashboard.findOne({ _id: dashboardId });
21
27
  if (evaluate) {
22
28
  let result = null;
@@ -25,9 +31,9 @@ module.exports = ({ db }) => async function getDashboard(params) {
25
31
  } catch (error) {
26
32
  return { dashboard, error: { message: error.message } };
27
33
  }
28
-
34
+
29
35
  return { dashboard, result };
30
36
  }
31
37
 
32
38
  return { dashboard };
33
- };
39
+ };
@@ -1,10 +1,13 @@
1
1
  'use strict';
2
2
 
3
+ const authorize = require('../../authorize');
3
4
 
4
- module.exports = ({ db }) => async function getDashboards() {
5
+ module.exports = ({ db }) => async function getDashboards(roles) {
5
6
  const Dashboard = db.model('__Studio_Dashboard');
6
7
 
8
+ await authorize('Dashboard.getDashboards', roles);
9
+
7
10
  const dashboards = await Dashboard.find();
8
11
 
9
12
  return { dashboards }
10
- };
13
+ };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
4
5
 
5
6
  const UpdateDashboardParams = new Archetype({
6
7
  dashboardId: {
@@ -16,16 +17,21 @@ const UpdateDashboardParams = new Archetype({
16
17
  },
17
18
  description: {
18
19
  $type: 'string'
20
+ },
21
+ roles: {
22
+ $type: ['string']
19
23
  }
20
24
  }).compile('UpdateDashboardParams');
21
25
 
22
26
  module.exports = ({ db }) => async function updateDashboard(params) {
23
- const { dashboardId, code, title, description } = new UpdateDashboardParams(params);
27
+ const { dashboardId, code, title, description, roles } = new UpdateDashboardParams(params);
24
28
 
25
29
  const Dashboard = db.models[`__Studio_Dashboard`];
26
30
 
31
+ await authorize('Dashboard.updateDashboard', roles);
32
+
27
33
  const updateObj = { code };
28
-
34
+
29
35
  if (title) {
30
36
  updateObj.title = title;
31
37
  }
@@ -36,7 +42,7 @@ module.exports = ({ db }) => async function updateDashboard(params) {
36
42
 
37
43
  const doc = await Dashboard.
38
44
  findByIdAndUpdate(dashboardId, updateObj, { sanitizeFilter: true, returnDocument: 'after', overwriteImmutable: true });
39
-
45
+
40
46
  let result = null;
41
47
  try {
42
48
  result = await doc.evaluate();
@@ -45,4 +51,4 @@ module.exports = ({ db }) => async function updateDashboard(params) {
45
51
  }
46
52
 
47
53
  return { doc, result };
48
- };
54
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const { EJSON } = require('bson');
5
+ const authorize = require('../../authorize');
5
6
 
6
7
  const CreateDocumentParams = new Archetype({
7
8
  model: {
@@ -20,16 +21,14 @@ const CreateDocumentParams = new Archetype({
20
21
  module.exports = ({ db }) => async function CreateDocument(params) {
21
22
  const { model, data, roles } = new CreateDocumentParams(params);
22
23
 
23
- if (roles && roles.includes('readonly')) {
24
- throw new Error('Not authorized');
25
- }
24
+ await authorize('Model.createDocument', roles);
26
25
 
27
26
  const Model = db.models[model];
28
27
  if (Model == null) {
29
28
  throw new Error(`Model ${model} not found`);
30
29
  }
31
-
30
+
32
31
  const doc = await Model.create(EJSON.deserialize(data));
33
-
32
+
34
33
  return { doc };
35
- };
34
+ };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
4
5
 
5
6
  const DeleteDocumentParams = new Archetype({
6
7
  model: {
@@ -21,9 +22,8 @@ module.exports = ({ db }) => async function DeleteDocument(params) {
21
22
 
22
23
  const Model = db.models[model];
23
24
 
24
- if (roles && roles.includes('readonly')) {
25
- throw new Error('Not authorized');
26
- }
25
+ await authorize('Model.deleteDocument', roles);
26
+
27
27
  if (Model == null) {
28
28
  throw new Error(`Model ${model} not found`);
29
29
  }
@@ -33,6 +33,6 @@ module.exports = ({ db }) => async function DeleteDocument(params) {
33
33
  setOptions({ sanitizeFilter: true }).
34
34
  orFail();
35
35
  console.log('what is doc', doc);
36
-
36
+
37
37
  return { doc };
38
- };
38
+ };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
4
5
 
5
6
  const DeleteDocumentsParams = new Archetype({
6
7
  model: {
@@ -21,9 +22,8 @@ module.exports = ({ db }) => async function DeleteDocuments(params) {
21
22
 
22
23
  const Model = db.models[model];
23
24
 
24
- if (roles && roles.includes('readonly')) {
25
- throw new Error('Not authorized');
26
- }
25
+ await authorize('Model.deleteDocuments', roles);
26
+
27
27
  if (Model == null) {
28
28
  throw new Error(`Model ${model} not found`);
29
29
  }
@@ -32,7 +32,7 @@ module.exports = ({ db }) => async function DeleteDocuments(params) {
32
32
  deleteMany({_id: { $in: documentIds }}).
33
33
  setOptions({ sanitizeFilter: true }).
34
34
  orFail();
35
-
36
-
35
+
36
+
37
37
  return { };
38
- };
38
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
5
+
6
+ const DropIndexParams = new Archetype({
7
+ model: {
8
+ $type: 'string',
9
+ $required: true
10
+ },
11
+ name: {
12
+ $type: 'string',
13
+ $required: true
14
+ },
15
+ roles: {
16
+ $type: ['string']
17
+ }
18
+ }).compile('DropIndexParams');
19
+
20
+ module.exports = ({ db }) => async function getIndexes(params) {
21
+ const { model, name, roles } = new DropIndexParams(params);
22
+
23
+ await authorize('Model.dropIndex', roles);
24
+
25
+ const Model = db.models[model];
26
+ if (Model == null) {
27
+ throw new Error(`Model ${model} not found`);
28
+ }
29
+
30
+ await Model.dropIndex(name);
31
+
32
+ const mongoDBIndexes = await Model.listIndexes();
33
+ return {
34
+ mongoDBIndexes
35
+ };
36
+ };
@@ -3,6 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const mongoose = require('mongoose');
5
5
  const { stringify } = require('csv-stringify/sync');
6
+ const authorize = require('../../authorize');
6
7
 
7
8
  const GetDocumentsParams = new Archetype({
8
9
  model: {
@@ -21,13 +22,18 @@ const GetDocumentsParams = new Archetype({
21
22
  }
22
23
  return v;
23
24
  }
25
+ },
26
+ roles: {
27
+ $type: ['string']
24
28
  }
25
29
  }).compile('GetDocumentsParams');
26
30
 
27
31
  module.exports = ({ db }) => async function exportQueryResults(params, req, res) {
28
32
  params = new GetDocumentsParams(params);
29
33
  let { filter } = params;
30
- const { model, propertiesToInclude } = params;
34
+ const { model, propertiesToInclude, roles } = params;
35
+
36
+ await authorize('Model.exportQueryResults', roles);
31
37
 
32
38
  const Model = db.models[model];
33
39
  if (Model == null) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Archetype = require('archetype');
4
4
  const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
5
+ const authorize = require('../../authorize');
5
6
 
6
7
  const GetDocumentParams = new Archetype({
7
8
  model: {
@@ -11,11 +12,16 @@ const GetDocumentParams = new Archetype({
11
12
  documentId: {
12
13
  $type: 'string',
13
14
  $required: true
15
+ },
16
+ roles: {
17
+ $type: ['string']
14
18
  }
15
19
  }).compile('GetDocumentParams');
16
20
 
17
21
  module.exports = ({ db }) => async function getDocument(params) {
18
- const { model, documentId } = new GetDocumentParams(params);
22
+ const { model, documentId, roles } = new GetDocumentParams(params);
23
+
24
+ await authorize('Model.getDocument', roles);
19
25
 
20
26
  const Model = db.models[model];
21
27
  if (Model == null) {
@@ -35,6 +41,6 @@ module.exports = ({ db }) => async function getDocument(params) {
35
41
  };
36
42
  }
37
43
  removeSpecifiedPaths(schemaPaths, '.$*');
38
-
44
+
39
45
  return { doc: doc.toJSON({ virtuals: true, getters: false, transform: false }), schemaPaths };
40
- };
46
+ };
@@ -3,6 +3,7 @@
3
3
  const Archetype = require('archetype');
4
4
  const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
5
5
  const { EJSON } = require('bson')
6
+ const authorize = require('../../authorize');
6
7
 
7
8
  const GetDocumentsParams = new Archetype({
8
9
  model: {
@@ -24,11 +25,17 @@ const GetDocumentsParams = new Archetype({
24
25
  },
25
26
  sort: {
26
27
  $type: Archetype.Any
28
+ },
29
+ roles: {
30
+ $type: ['string']
27
31
  }
28
32
  }).compile('GetDocumentsParams');
29
33
 
30
34
  module.exports = ({ db }) => async function getDocuments(params) {
31
35
  params = new GetDocumentsParams(params);
36
+ const { roles } = params;
37
+ await authorize('Model.getDocuments', roles);
38
+
32
39
  let { filter } = params;
33
40
  if (filter != null && Object.keys(filter).length > 0) {
34
41
  filter = EJSON.parse(filter);
@@ -1,18 +1,22 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
4
5
 
5
6
  const GetDocumentsParams = new Archetype({
6
7
  model: {
7
8
  $type: 'string',
8
9
  $required: true
9
10
  },
11
+ roles: {
12
+ $type: ['string']
13
+ }
10
14
  }).compile('GetDocumentsParams');
11
15
 
12
16
  module.exports = ({ db }) => async function getIndexes(params) {
13
- params = new GetDocumentsParams(params);
17
+ const { model, roles } = new GetDocumentsParams(params);
14
18
 
15
- const { model } = params;
19
+ await authorize('Model.getIndexes', roles);
16
20
 
17
21
  const Model = db.models[model];
18
22
  if (Model == null) {
@@ -1,7 +1,19 @@
1
1
  'use strict';
2
2
 
3
- module.exports = ({ db }) => async function listModels() {
3
+ const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
5
+
6
+ const ListModelsParams = new Archetype({
7
+ roles: {
8
+ $type: ['string']
9
+ }
10
+ }).compile('ListModelsParams');
11
+
12
+ module.exports = ({ db }) => async function listModels(params) {
13
+ const { roles } = new ListModelsParams(params);
14
+ await authorize('Model.listModels', roles);
15
+
4
16
  return {
5
17
  models: Object.keys(db.models).filter(key => !key.startsWith('__Studio_')).sort()
6
18
  };
7
- };
19
+ };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
4
5
 
5
6
  const UpdateDocumentsParams = new Archetype({
6
7
  model: {
@@ -23,9 +24,8 @@ const UpdateDocumentsParams = new Archetype({
23
24
  module.exports = ({ db }) => async function updateDocument(params) {
24
25
  const { model, _id, update, roles } = new UpdateDocumentsParams(params);
25
26
 
26
- if (roles && roles.includes('readonly')) {
27
- throw new Error('Not authorized');
28
- }
27
+ await authorize('Document.updateDocument', roles);
28
+
29
29
  const Model = db.models[model];
30
30
  if (Model == null) {
31
31
  throw new Error(`Model ${model} not found`);
@@ -40,6 +40,6 @@ module.exports = ({ db }) => async function updateDocument(params) {
40
40
 
41
41
  const doc = await Model.
42
42
  findByIdAndUpdate(_id, processedUpdate, { sanitizeFilter: true, returnDocument: 'after', overwriteImmutable: true, runValidators: false });
43
-
43
+
44
44
  return { doc };
45
- };
45
+ };
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
- const mongoose = require('mongoose');
4
+ const authorize = require('../../authorize');
5
5
 
6
6
  const UpdateDocumentsParams = new Archetype({
7
7
  model: {
@@ -24,9 +24,8 @@ const UpdateDocumentsParams = new Archetype({
24
24
  module.exports = ({ db }) => async function updateDocuments(params) {
25
25
  const { model, _id, update, roles } = new UpdateDocumentsParams(params);
26
26
 
27
- if (roles && roles.includes('readonly')) {
28
- throw new Error('Not authorized');
29
- }
27
+ await authorize('Document.updateDocuments', roles);
28
+
30
29
  const Model = db.models[model];
31
30
  if (Model == null) {
32
31
  throw new Error(`Model ${model} not found`);
@@ -41,6 +40,6 @@ module.exports = ({ db }) => async function updateDocuments(params) {
41
40
 
42
41
  const result = await Model.
43
42
  updateMany({ _id: { $in: _id } }, processedUpdate, { overwriteImmutable: true, runValidators: false });
44
-
43
+
45
44
  return { result };
46
- };
45
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const actionsToRequiredRoles = {
4
+ 'ChatMessage.executeScript': ['owner', 'admin', 'member'],
5
+ 'ChatThread.createChatMessage': ['owner', 'admin', 'member'],
6
+ 'ChatThread.createChatThread': ['owner', 'admin', 'member'],
7
+ 'ChatThread.getChatThread': ['owner', 'admin', 'member'],
8
+ 'ChatThread.listChatThreads': ['owner', 'admin', 'member'],
9
+ 'Dashboard.createDashboard': ['owner', 'admin', 'member'],
10
+ 'Dashboard.deleteDashboard': ['owner', 'admin', 'member'],
11
+ 'Dashboard.getDashboard': ['owner', 'admin', 'member', 'readonly', 'dashboards'],
12
+ 'Dashboard.getDashboards': ['owner', 'admin', 'member', 'readonly', 'dashboards'],
13
+ 'Dashboard.getDashboard': ['owner', 'admin', 'member', 'readonly', 'dashboards'],
14
+ 'Dashboard.updateDashboard': ['owner', 'admin', 'member'],
15
+ 'Model.createDocument': ['owner', 'admin', 'member'],
16
+ 'Model.updateDocument': ['owner', 'admin', 'member'],
17
+ 'Model.deleteDocument': ['owner', 'admin', 'member'],
18
+ 'Model.dropIndex': ['owner', 'admin'],
19
+ 'Model.exportQueryResults': ['owner', 'admin', 'member', 'readonly'],
20
+ 'Model.getDocument': ['owner', 'admin', 'member', 'readonly'],
21
+ 'Model.getDocuments': ['owner', 'admin', 'member', 'readonly'],
22
+ 'Model.getIndexes': ['owner', 'admin', 'member', 'readonly'],
23
+ 'Model.listModels': ['owner', 'admin', 'member', 'readonly'],
24
+ 'Model.updateDocument': ['owner', 'admin', 'member'],
25
+ 'Model.updateDocuments': ['owner', 'admin', 'member']
26
+ };
27
+
28
+ module.exports = function authorize(action, roles) {
29
+ if (roles == null) {
30
+ return;
31
+ }
32
+ const authorized = actionsToRequiredRoles[action] && roles.find(role => actionsToRequiredRoles[action].includes(role));
33
+ if (!authorized) {
34
+ throw new Error(`Unauthorized to take action ${action}`);
35
+ }
36
+ }
@@ -1800,13 +1800,15 @@ app.component('app-component', {
1800
1800
  window.state = this;
1801
1801
 
1802
1802
  if (mothership.hasAPIKey) {
1803
- const href = window.location.href;
1804
- if (href.match(/\?code=([a-zA-Z0-9]+)$/)) {
1805
- const code = href.match(/\?code=([a-zA-Z0-9]+)$/)[1];
1803
+ const hashParams = new URLSearchParams(window.location.hash.replace(/^#?\/?\??/, '') || '');
1804
+ if (hashParams.has('code')) {
1805
+ const code = hashParams.get('code');
1806
+ const provider = hashParams.get('provider');
1806
1807
  try {
1807
- const { accessToken, user, roles } = await mothership.github(code);
1808
+ const { accessToken, user, roles } = provider === 'github' ? await mothership.github(code) : await mothership.google(code);
1808
1809
  if (roles == null) {
1809
1810
  this.authError = 'You are not authorized to access this workspace';
1811
+ this.status = 'loaded';
1810
1812
  return;
1811
1813
  }
1812
1814
  this.user = user;
@@ -2291,6 +2293,10 @@ module.exports = app => app.component('models', {
2291
2293
  this.status = 'loaded';
2292
2294
  },
2293
2295
  methods: {
2296
+ async dropIndex(name) {
2297
+ const { mongoDBIndexes } = await api.Model.dropIndex({ model: this.currentModel, index: name });
2298
+ this.mongoDBIndexes = mongoDBIndexes;
2299
+ },
2294
2300
  initFilter(ev) {
2295
2301
  if (!this.searchText) {
2296
2302
  this.searchText = '{}';
@@ -2389,7 +2395,7 @@ module.exports = app => app.component('models', {
2389
2395
  },
2390
2396
  async openIndexModal() {
2391
2397
  this.shouldShowIndexModal = true;
2392
- const { mongoDBIndexes, schemaIndexes } = await api.Model.getIndexes({ model: this.currentModel })
2398
+ const { mongoDBIndexes, schemaIndexes } = await api.Model.getIndexes({ model: this.currentModel });
2393
2399
  this.mongoDBIndexes = mongoDBIndexes;
2394
2400
  this.schemaIndexes = schemaIndexes;
2395
2401
  },
@@ -2493,6 +2499,9 @@ module.exports = app => app.component('models', {
2493
2499
  deselectAll() {
2494
2500
  this.selectedPaths = [];
2495
2501
  },
2502
+ selectAll() {
2503
+ this.selectedPaths = [...this.schemaPaths];
2504
+ },
2496
2505
  isSelected(path) {
2497
2506
  return this.selectedPaths.find(x => x.path == path);
2498
2507
  },
@@ -2607,6 +2616,10 @@ exports.githubLogin = function githubLogin() {
2607
2616
  return client.post('/githubLogin', { state: window.location.href }).then(res => res.data);
2608
2617
  };
2609
2618
 
2619
+ exports.googleLogin = function googleLogin() {
2620
+ return client.post('/googleLogin', { state: window.location.href }).then(res => res.data);
2621
+ };
2622
+
2610
2623
  exports.getWorkspaceTeam = function getWorkspaceTeam() {
2611
2624
  return client.post('/getWorkspaceTeam', { workspaceId: window.MONGOOSE_STUDIO_CONFIG.workspace._id }).then(res => res.data);
2612
2625
  };
@@ -2619,6 +2632,10 @@ exports.github = function github(code) {
2619
2632
  return client.post('/github', { code, workspaceId: window.MONGOOSE_STUDIO_CONFIG.workspace._id }).then(res => res.data);
2620
2633
  };
2621
2634
 
2635
+ exports.google = function google(code) {
2636
+ return client.post('/google', { code, workspaceId: window.MONGOOSE_STUDIO_CONFIG.workspace._id }).then(res => res.data);
2637
+ };
2638
+
2622
2639
  exports.inviteToWorkspace = function inviteToWorkspace(params) {
2623
2640
  return client.post('/inviteToWorkspace', { workspaceId: window.MONGOOSE_STUDIO_CONFIG.workspace._id, ...params }).then(res => res.data);
2624
2641
  };
@@ -2862,6 +2879,10 @@ module.exports = app => app.component('splash', {
2862
2879
  async loginWithGithub() {
2863
2880
  const { url } = await mothership.githubLogin();
2864
2881
  window.location.href = url;
2882
+ },
2883
+ async loginWithGoogle() {
2884
+ const { url } = await mothership.googleLogin();
2885
+ window.location.href = url;
2865
2886
  }
2866
2887
  }
2867
2888
  });
@@ -2883,12 +2904,18 @@ const template = __webpack_require__(/*! ./new-invitation.html */ "./frontend/sr
2883
2904
 
2884
2905
  module.exports = app => app.component('new-invitation', {
2885
2906
  template,
2907
+ props: ['tier'],
2886
2908
  emits: ['close', 'invitationCreated'],
2887
2909
  data: () => ({
2888
2910
  githubUsername: '',
2889
2911
  email: '',
2890
2912
  role: null
2891
2913
  }),
2914
+ mounted() {
2915
+ if (this.tier == null) {
2916
+ this.role = 'dashboards';
2917
+ }
2918
+ },
2892
2919
  methods: {
2893
2920
  async inviteToWorkspace() {
2894
2921
  const { invitation } = await mothership.inviteToWorkspace({ githubUsername: this.githubUsername, email: this.email, roles: [this.role] });
@@ -4377,7 +4404,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
4377
4404
  /***/ ((module) => {
4378
4405
 
4379
4406
  "use strict";
4380
- module.exports = "<div class=\"models\">\n <div>\n <div class=\"flex grow flex-col gap-y-5 overflow-auto border-r border-gray-200 bg-white px-2 h-[calc(100vh-55px)] w-48\">\n <div class=\"flex font-bold font-xl mt-4 pl-2\">\n Models\n </div>\n <nav class=\"flex flex-1 flex-col\">\n <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n <li>\n <ul role=\"list\">\n <li v-for=\"model in models\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"block truncate rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700\"\n :class=\"model === currentModel ? 'bg-ultramarine-100 font-bold' : 'hover:bg-ultramarine-100'\">\n {{model}}\n </router-link>\n </li>\n </ul>\n </li>\n </ul>\n </nav>\n </div>\n\n </div>\n <div class=\"documents\" ref=\"documentsList\">\n <div class=\"relative h-[42px]\">\n <div class=\"documents-menu\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <form @submit.prevent=\"search\" class=\"flex-grow m-0\">\n <input ref=\"searchInput\" class=\"w-full font-mono rounded-md p-1 border border-gray-300 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none\" type=\"text\" placeholder=\"Filter\" v-model=\"searchText\" @click=\"initFilter\" />\n </form>\n <div>\n <span v-if=\"status === 'loading'\">Loading ...</span>\n <span v-if=\"status === 'loaded'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 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\">\n Export\n </button>\n <button\n @click=\"stagingSelect\"\n type=\"button\"\n :class=\"{ 'bg-ultramarine-500 ring-inset ring-2 ring-gray-300 hover:bg-ultramarine-600': selectMultiple }\"\n class=\"rounded bg-ultramarine-600 px-2 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\"\n >\n Select\n </button>\n <button\n v-show=\"selectMultiple\"\n @click=\"shouldShowUpdateMultipleModal=true;\"\n type=\"button\"\n class=\"rounded bg-green-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\"\n >\n Update\n </button>\n <button\n @click=\"shouldShowDeleteMultipleModal=true;\"\n type=\"button\"\n v-show=\"selectMultiple\"\n class=\"rounded bg-red-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500\"\n >\n Delete\n </button>\n <button\n @click=\"openIndexModal\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 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\">\n Indexes\n </button>\n <button\n @click=\"shouldShowCreateModal = true;\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 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\">\n Create\n </button>\n <button\n @click=\"openFieldSelection\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 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\">\n Fields\n </button>\n <span class=\"isolate inline-flex rounded-md shadow-sm\">\n <button\n @click=\"outputType = 'table'\"\n type=\"button\"\n class=\"relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/table.svg\">\n </button>\n <button\n @click=\"outputType = 'json'\"\n type=\"button\"\n class=\"relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n </span>\n </div>\n </div>\n </div>\n <div class=\"documents-container relative\">\n <table v-if=\"outputType === 'table'\">\n <thead>\n <th v-for=\"path in filteredPaths\" @click=\"clickFilter(path.path)\" class=\"cursor-pointer\">\n {{path.path}}\n <span class=\"path-type\">\n ({{(path.instance || 'unknown')}})\n </span>\n <span class=\"sort-arrow\" @click=\"sortDocs(1, path.path)\">{{sortBy[path.path] == 1 ? 'X' : '↑'}}</span>\n <span class=\"sort-arrow\" @click=\"sortDocs(-1, path.path)\">{{sortBy[path.path] == -1 ? 'X' : '↓'}}</span>\n </th>\n </thead>\n <tbody>\n <tr v-for=\"document in documents\" @click=\"handleDocumentClick(document)\" :key=\"document._id\">\n <td v-for=\"schemaPath in filteredPaths\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\n <component\n :is=\"getComponentForPath(schemaPath)\"\n :value=\"getValueForPath(document, schemaPath.path)\"\n :allude=\"getReferenceModel(schemaPath)\">\n </component>\n </td>\n </tr>\n </tbody>\n </table>\n <div v-if=\"outputType === 'json'\">\n <div v-for=\"document in documents\" @click=\"handleDocumentClick(document)\" :key=\"document._id\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\n <list-json :value=\"filterDocument(document)\">\n </list-json>\n </div>\n </div>\n <div v-if=\"status === 'loading'\" class=\"loader\">\n <img src=\"images/loader.gif\">\n </div>\n </div>\n </div>\n <modal v-if=\"shouldShowExportModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowExportModal = false\">&times;</div>\n <export-query-results\n :schemaPaths=\"schemaPaths\"\n :filter=\"filter\"\n :currentModel=\"currentModel\"\n @done=\"shouldShowExportModal = false\">\n </export-query-results>\n </template>\n </modal>\n <modal v-if=\"shouldShowIndexModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowIndexModal = false\">&times;</div>\n <div class=\"text-xl font-bold mb-2\">Indexes</div>\n <div v-for=\"index in mongoDBIndexes\" class=\"w-full flex items-center\">\n <div class=\"grow shrink text-left flex justify-between items-center\" v-if=\"index.name != '_id_'\">\n <div>\n <div class=\"font-bold\">{{ index.name }}</div>\n <div class=\"text-sm font-mono\">{{ JSON.stringify(index.key) }}</div>\n </div>\n <div>\n <button\n type=\"submit\"\n @click=\"dropIndex()\"\n class=\"rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:bg-gray-400 disabled:cursor-not-allowed\">\n Drop\n </button>\n </div>\n </div>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowFieldModal = false; selectedPaths = [...filteredPaths];\">&times;</div>\n <div v-for=\"(path, index) in schemaPaths\" :key=\"index\" class=\"w-5 flex items-center\">\n <input class=\"mt-0 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-600 accent-sky-600\" type=\"checkbox\" :id=\"'path.path'+index\" @change=\"addOrRemove(path)\" :value=\"path.path\" :checked=\"isSelected(path.path)\" />\n <div class=\"ml-2 text-gray-700 grow shrink text-left\">\n <label :for=\"'path.path' + index\">{{path.path}}</label>\n </div>\n </div>\n <div class=\"mt-4 flex gap-2\">\n <button type=\"submit\" @click=\"filterDocuments()\" class=\"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-teal-600\">Filter Selection</button>\n <button type=\"submit\" @click=\"deselectAll()\" class=\"rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">Deselect All</button>\n <button type=\"submit\" @click=\"resetDocuments()\" class=\"rounded-md bg-gray-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\" >Cancel</button>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowCreateModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCreateModal = false;\">&times;</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowUpdateMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowUpdateMultipleModal = false;\">&times;</div>\n <update-document :currentModel=\"currentModel\" :document=\"selectedDocuments\" :multiple=\"true\" @update=\"updateDocuments\" @close=\"shouldShowUpdateMultipleModal=false;\"></update-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteMultipleModal = false;\">&times;</div>\n <h2>Are you sure you want to delete {{selectedDocuments.length}} documents?</h2>\n <div>\n <list-json :value=\"selectedDocuments\"></list-json>\n </div>\n <div class=\"flex gap-4\">\n <async-button @click=\"deleteDocuments\" class=\"rounded bg-red-500 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">\n Confirm\n </async-button>\n <button @click=\"shouldShowDeleteMultipleModal = false;\" class=\"rounded bg-gray-400 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500\">\n Cancel\n </button>\n </div>\n </template>\n </modal>\n</div>\n";
4407
+ module.exports = "<div class=\"models\">\n <div>\n <div class=\"flex grow flex-col gap-y-5 overflow-auto border-r border-gray-200 bg-white px-2 h-[calc(100vh-55px)] w-48\">\n <div class=\"flex font-bold font-xl mt-4 pl-2\">\n Models\n </div>\n <nav class=\"flex flex-1 flex-col\">\n <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n <li>\n <ul role=\"list\">\n <li v-for=\"model in models\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"block truncate rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700\"\n :class=\"model === currentModel ? 'bg-ultramarine-100 font-bold' : 'hover:bg-ultramarine-100'\">\n {{model}}\n </router-link>\n </li>\n </ul>\n </li>\n </ul>\n </nav>\n </div>\n\n </div>\n <div class=\"documents\" ref=\"documentsList\">\n <div class=\"relative h-[42px]\">\n <div class=\"documents-menu\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <form @submit.prevent=\"search\" class=\"flex-grow m-0\">\n <input ref=\"searchInput\" class=\"w-full font-mono rounded-md p-1 border border-gray-300 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none\" type=\"text\" placeholder=\"Filter\" v-model=\"searchText\" @click=\"initFilter\" />\n </form>\n <div>\n <span v-if=\"status === 'loading'\">Loading ...</span>\n <span v-if=\"status === 'loaded'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 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\">\n Export\n </button>\n <button\n @click=\"stagingSelect\"\n type=\"button\"\n :class=\"{ 'bg-ultramarine-500 ring-inset ring-2 ring-gray-300 hover:bg-ultramarine-600': selectMultiple }\"\n class=\"rounded bg-ultramarine-600 px-2 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\"\n >\n Select\n </button>\n <button\n v-show=\"selectMultiple\"\n @click=\"shouldShowUpdateMultipleModal=true;\"\n type=\"button\"\n class=\"rounded bg-green-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\"\n >\n Update\n </button>\n <button\n @click=\"shouldShowDeleteMultipleModal=true;\"\n type=\"button\"\n v-show=\"selectMultiple\"\n class=\"rounded bg-red-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500\"\n >\n Delete\n </button>\n <button\n @click=\"openIndexModal\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 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\">\n Indexes\n </button>\n <button\n @click=\"shouldShowCreateModal = true;\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 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\">\n Create\n </button>\n <button\n @click=\"openFieldSelection\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 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\">\n Fields\n </button>\n <span class=\"isolate inline-flex rounded-md shadow-sm\">\n <button\n @click=\"outputType = 'table'\"\n type=\"button\"\n class=\"relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/table.svg\">\n </button>\n <button\n @click=\"outputType = 'json'\"\n type=\"button\"\n class=\"relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n </span>\n </div>\n </div>\n </div>\n <div class=\"documents-container relative\">\n <table v-if=\"outputType === 'table'\">\n <thead>\n <th v-for=\"path in filteredPaths\" @click=\"clickFilter(path.path)\" class=\"cursor-pointer\">\n {{path.path}}\n <span class=\"path-type\">\n ({{(path.instance || 'unknown')}})\n </span>\n <span class=\"sort-arrow\" @click=\"sortDocs(1, path.path)\">{{sortBy[path.path] == 1 ? 'X' : '↑'}}</span>\n <span class=\"sort-arrow\" @click=\"sortDocs(-1, path.path)\">{{sortBy[path.path] == -1 ? 'X' : '↓'}}</span>\n </th>\n </thead>\n <tbody>\n <tr v-for=\"document in documents\" @click=\"handleDocumentClick(document)\" :key=\"document._id\">\n <td v-for=\"schemaPath in filteredPaths\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\n <component\n :is=\"getComponentForPath(schemaPath)\"\n :value=\"getValueForPath(document, schemaPath.path)\"\n :allude=\"getReferenceModel(schemaPath)\">\n </component>\n </td>\n </tr>\n </tbody>\n </table>\n <div v-if=\"outputType === 'json'\">\n <div v-for=\"document in documents\" @click=\"handleDocumentClick(document)\" :key=\"document._id\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\n <list-json :value=\"filterDocument(document)\">\n </list-json>\n </div>\n </div>\n <div v-if=\"status === 'loading'\" class=\"loader\">\n <img src=\"images/loader.gif\">\n </div>\n </div>\n </div>\n <modal v-if=\"shouldShowExportModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowExportModal = false\">&times;</div>\n <export-query-results\n :schemaPaths=\"schemaPaths\"\n :filter=\"filter\"\n :currentModel=\"currentModel\"\n @done=\"shouldShowExportModal = false\">\n </export-query-results>\n </template>\n </modal>\n <modal v-if=\"shouldShowIndexModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowIndexModal = false\">&times;</div>\n <div class=\"text-xl font-bold mb-2\">Indexes</div>\n <div v-for=\"index in mongoDBIndexes\" class=\"w-full flex items-center\">\n <div class=\"grow shrink text-left flex justify-between items-center\" v-if=\"index.name != '_id_'\">\n <div>\n <div class=\"font-bold\">{{ index.name }}</div>\n <div class=\"text-sm font-mono\">{{ JSON.stringify(index.key) }}</div>\n </div>\n <div>\n <async-button\n type=\"button\"\n @click=\"dropIndex(index.name)\"\n class=\"rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:bg-gray-400 disabled:cursor-not-allowed\">\n Drop\n </async-button>\n </div>\n </div>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowFieldModal = false; selectedPaths = [...filteredPaths];\">&times;</div>\n <div v-for=\"(path, index) in schemaPaths\" :key=\"index\" class=\"w-5 flex items-center\">\n <input class=\"mt-0 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-600 accent-sky-600\" type=\"checkbox\" :id=\"'path.path'+index\" @change=\"addOrRemove(path)\" :value=\"path.path\" :checked=\"isSelected(path.path)\" />\n <div class=\"ml-2 text-gray-700 grow shrink text-left\">\n <label :for=\"'path.path' + index\">{{path.path}}</label>\n </div>\n </div>\n <div class=\"mt-4 flex gap-2\">\n <button type=\"button\" @click=\"filterDocuments()\" class=\"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-teal-600\">Filter Selection</button>\n <button type=\"button\" @click=\"selectAll()\" class=\"rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\">Select All</button>\n <button type=\"button\" @click=\"deselectAll()\" class=\"rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">Deselect All</button>\n <button type=\"button\" @click=\"resetDocuments()\" class=\"rounded-md bg-gray-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\" >Cancel</button>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowCreateModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCreateModal = false;\">&times;</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowUpdateMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowUpdateMultipleModal = false;\">&times;</div>\n <update-document :currentModel=\"currentModel\" :document=\"selectedDocuments\" :multiple=\"true\" @update=\"updateDocuments\" @close=\"shouldShowUpdateMultipleModal=false;\"></update-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteMultipleModal = false;\">&times;</div>\n <h2>Are you sure you want to delete {{selectedDocuments.length}} documents?</h2>\n <div>\n <list-json :value=\"selectedDocuments\"></list-json>\n </div>\n <div class=\"flex gap-4\">\n <async-button @click=\"deleteDocuments\" class=\"rounded bg-red-500 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">\n Confirm\n </async-button>\n <button @click=\"shouldShowDeleteMultipleModal = false;\" class=\"rounded bg-gray-400 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500\">\n Cancel\n </button>\n </div>\n </template>\n </modal>\n</div>\n";
4381
4408
 
4382
4409
  /***/ }),
4383
4410
 
@@ -4410,7 +4437,7 @@ module.exports = "<div class=\"navbar\">\n <div class=\"nav-left flex items-cen
4410
4437
  /***/ ((module) => {
4411
4438
 
4412
4439
  "use strict";
4413
- module.exports = "<div class=\"w-full h-full flex items-center justify-center\">\n <div class=\"text-center\">\n <div class=\"rounded-full bg-gray-100 p-6 inline-block\">\n <img src=\"images/logo.svg\" class=\"w-48 h-48\">\n </div>\n <div class=\"text-lg mt-2 font-bold\">\n Mongoose Studio\n </div>\n <div v-if=\"loading\" class=\"mt-2\">\n <img src=\"images/loader.gif\" class=\"inline w-16 h-16\">\n </div>\n <div class=\"mt-2 text-gray-700\" v-if=\"!loading\">\n {{workspaceName}}\n </div>\n <div class=\"mt-4\" v-if=\"!loading\">\n <async-button\n type=\"button\"\n @click=\"loginWithGithub\"\n class=\"rounded bg-ultramarine-600 px-2 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\">\n Login With GitHub\n </async-button>\n </div>\n <div class=\"mt-4\" v-if=\"state.authError\">\n <div class=\"rounded-md bg-red-50 p-4\">\n <div class=\"flex\">\n <div class=\"shrink-0\">\n <svg class=\"size-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\">{{state.authError}}</h3>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n";
4440
+ module.exports = "<div class=\"w-full h-full flex items-center justify-center\">\n <div class=\"text-center\">\n <div class=\"rounded-full bg-gray-100 p-6 inline-block\">\n <img src=\"images/logo.svg\" class=\"w-48 h-48\">\n </div>\n <div class=\"text-lg mt-2 font-bold\">\n Mongoose Studio\n </div>\n <div v-if=\"loading\" class=\"mt-2\">\n <img src=\"images/loader.gif\" class=\"inline w-16 h-16\">\n </div>\n <div class=\"mt-2 text-gray-700\" v-if=\"!loading\">\n {{workspaceName}}\n </div>\n <div class=\"mt-4 flex gap-4 justify-center\" v-if=\"!loading\">\n <div>\n <async-button\n type=\"button\"\n @click=\"loginWithGithub\"\n class=\"rounded bg-ultramarine-600 px-2 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\">\n <svg viewBox=\"0 0 98 98\" class=\"inline mr-1\" height=\"1.5em\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z\" fill=\"#fff\"/></svg>\n Login With GitHub\n </async-button>\n </div>\n <div>\n <async-button\n type=\"button\"\n @click=\"loginWithGoogle\"\n class=\"rounded bg-ultramarine-600 px-2 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\">\n <svg class=\"inline\" xmlns=\"http://www.w3.org/2000/svg\" height=\"1.5em\" viewBox=\"0 0 512 512\"><path fill=\"#fff\" d=\"M386 400c45-42 65-112 53-179H260v74h102c-4 24-18 44-38 57z\"/><path fill=\"#fff\" d=\"M90 341a192 192 0 0 0 296 59l-62-48c-53 35-141 22-171-60z\"/><path fill=\"#fff\" d=\"M153 292c-8-25-8-48 0-73l-63-49c-23 46-30 111 0 171z\"/><path fill=\"#fff\" d=\"M153 219c22-69 116-109 179-50l55-54c-78-75-230-72-297 55z\"/></svg>\n Login With Google\n </async-button>\n </div>\n </div>\n <div class=\"mt-4\" v-if=\"state.authError\">\n <div class=\"rounded-md bg-red-50 p-4\">\n <div class=\"flex\">\n <div class=\"shrink-0\">\n <svg class=\"size-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\">{{state.authError}}</h3>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n";
4414
4441
 
4415
4442
  /***/ }),
4416
4443
 
@@ -4421,7 +4448,7 @@ module.exports = "<div class=\"w-full h-full flex items-center justify-center\">
4421
4448
  /***/ ((module) => {
4422
4449
 
4423
4450
  "use strict";
4424
- module.exports = "<div class=\"p-1\">\n <form class=\"space-y-4\">\n <div class=\"text-lg font-bold\">\n New Invitation\n </div>\n\n <div>\n <label for=\"githubUsername\" class=\"block text-sm/6 font-medium text-gray-900\">GitHub Username</label>\n <div class=\"mt-2\">\n <input type=\"githubUsername\" name=\"githubUsername\" id=\"githubUsername\" v-model=\"githubUsername\" class=\"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-ultramarine-600 sm:text-sm/6\" placeholder=\"johnsmith12\">\n </div>\n </div>\n\n <div>\n <label for=\"email\" class=\"block text-sm/6 font-medium text-gray-900\">Email (Optional)</label>\n <div class=\"mt-2\">\n <input type=\"email\" name=\"email\" id=\"email\" v-model=\"email\" class=\"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-ultramarine-600 sm:text-sm/6\" placeholder=\"you@example.com\">\n </div>\n </div>\n\n <div>\n <label for=\"location\" class=\"block text-sm/6 font-medium text-gray-900\">Role</label>\n <div class=\"mt-2 grid grid-cols-1\">\n <select id=\"role\" name=\"role\" v-model=\"role\" class=\"col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pl-3 pr-8 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6\">\n <option value=\"admin\">Admin</option>\n <option value=\"member\">Member</option>\n <option value=\"readonly\">Read-only</option>\n </select>\n <svg class=\"pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-500 sm:size-4\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\" data-slot=\"icon\">\n <path fill-rule=\"evenodd\" d=\"M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n </div>\n\n <async-button\n type=\"submit\"\n @click=\"inviteToWorkspace\"\n class=\"inline-flex justify-center rounded-md border border-transparent bg-forest-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-forest-green-500 focus:outline-none focus:ring-2 focus:ring-forest-green-500 focus:ring-offset-2\">\n Submit\n </async-button>\n </form>\n</div>\n";
4451
+ module.exports = "<div class=\"p-1\">\n <form class=\"space-y-4\">\n <div class=\"text-lg font-bold\">\n New Invitation\n </div>\n\n <div>\n <label for=\"githubUsername\" class=\"block text-sm/6 font-medium text-gray-900\">GitHub Username</label>\n <div class=\"mt-2\">\n <input type=\"githubUsername\" name=\"githubUsername\" id=\"githubUsername\" v-model=\"githubUsername\" class=\"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-ultramarine-600 sm:text-sm/6\" placeholder=\"johnsmith12\">\n </div>\n </div>\n\n <div>\n <label for=\"email\" class=\"block text-sm/6 font-medium text-gray-900\">Email (Optional)</label>\n <div class=\"mt-2\">\n <input type=\"email\" name=\"email\" id=\"email\" v-model=\"email\" class=\"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-ultramarine-600 sm:text-sm/6\" placeholder=\"you@example.com\">\n </div>\n </div>\n\n <div>\n <label for=\"location\" class=\"block text-sm/6 font-medium text-gray-900\">Role</label>\n <div class=\"mt-2 grid grid-cols-1\">\n <select id=\"role\" :disabled=\"tier == null\" name=\"role\" v-model=\"role\" class=\"col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pl-3 pr-8 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6\">\n <option value=\"admin\">Admin</option>\n <option value=\"member\">Member</option>\n <option value=\"readonly\">Read-only</option>\n <option value=\"dashboards\">Dashboards Only</option>\n </select>\n <svg class=\"pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-500 sm:size-4\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\" data-slot=\"icon\">\n <path fill-rule=\"evenodd\" d=\"M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n <div v-if=\"tier == null\" class=\"text-sm text-gray-700\">\n You can only invite \"Dashboards Only\" users until you set up a subscription.\n </div>\n </div>\n\n <async-button\n type=\"submit\"\n @click=\"inviteToWorkspace\"\n class=\"inline-flex justify-center rounded-md border border-transparent bg-forest-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-forest-green-500 focus:outline-none focus:ring-2 focus:ring-forest-green-500 focus:ring-offset-2\">\n Submit\n </async-button>\n </form>\n</div>\n";
4425
4452
 
4426
4453
  /***/ }),
4427
4454
 
@@ -4432,7 +4459,7 @@ module.exports = "<div class=\"p-1\">\n <form class=\"space-y-4\">\n <div cl
4432
4459
  /***/ ((module) => {
4433
4460
 
4434
4461
  "use strict";
4435
- module.exports = "<div class=\"mx-auto max-w-5xl py-6 px-2 flex flex-col gap-8\">\n <div>\n <div class=\"text-xl font-bold\">\n Subscription Details\n </div>\n <div v-if=\"status === 'loading'\" class=\"mt-4\">\n <img src=\"images/loader.gif\" class=\"inline w-8 h-8\">\n </div>\n <div v-else-if=\"workspace && workspace.subscriptionTier\" class=\"mt-4 flex justify-between items-center\">\n <div>\n <span class=\"font-bold\">Tier:</span> {{workspace.subscriptionTier ?? 'No subscription'}}\n </div>\n <div>\n <async-button\n type=\"submit\"\n @click=\"getWorkspaceCustomerPortalLink\"\n class=\"inline-flex items-center justify-center rounded-md border border-transparent bg-ultramarine-600 py-1 px-2 text-sm font-medium text-white shadow-sm hover:bg-ultramarine-500 focus:outline-none focus:ring-2 focus:ring-forest-green-500 focus:ring-offset-2\">\n View in Stripe\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 ml-1\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25\" />\n </svg>\n </async-button>\n </div>\n </div>\n <div v-else-if=\"workspace && !workspace.subscriptionTier\" class=\"mt-4 flex justify-between items-center\">\n <div>\n <span class=\"font-bold\">No active subscription</span>\n <div class=\"text-sm text-gray-700\">\n You won't be able to invite your team until you activate a subscription\n </div>\n </div>\n <div>\n <a\n :href=\"paymentLink\"\n target=\"_blank\"\n class=\"inline-flex items-center justify-center rounded-md border border-transparent bg-ultramarine-600 py-1 px-2 text-sm font-medium text-white shadow-sm hover:bg-ultramarine-500 focus:outline-none focus:ring-2 focus:ring-ultramarine-500 focus:ring-offset-2\">\n Subscribe With Stripe\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 ml-1\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25\" />\n </svg>\n </a>\n </div>\n </div>\n </div>\n <div>\n <div class=\"text-xl font-bold\">\n Current Members\n </div>\n <div v-if=\"status === 'loading'\" class=\"mt-4\">\n <img src=\"images/loader.gif\" class=\"inline w-8 h-8\">\n </div>\n <ul v-else role=\"list\" class=\"divide-y divide-gray-100\">\n <li class=\"flex justify-between gap-x-6 py-5\" v-for=\"user in users\">\n <div class=\"flex min-w-0 gap-x-4\">\n <img class=\"size-12 flex-none rounded-full bg-gray-50\" :src=\"user.picture ?? 'images/logo.svg'\" alt=\"\">\n <div class=\"min-w-0 flex-auto\">\n <p class=\"text-sm/6 font-semibold text-gray-900\">\n {{user.name || user.githubUsername}}\n <span v-if=\"user.isFreeUser\" class=\"ml-1 inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20\">Free</span>\n </p>\n <p class=\"mt-1 truncate text-xs/5 text-gray-500\">{{user.email ?? 'No Email'}}</p>\n </div>\n </div>\n <div class=\"hidden shrink-0 sm:flex sm:flex-col sm:items-end\">\n <p class=\"text-sm/6 text-gray-900 capitalize\">{{getRolesForUser(user).join(', ')}}</p>\n <div class=\"flex gap-3\">\n <p class=\"mt-1 text-xs/5 text-gray-500 cursor-pointer\">\n Edit\n </p>\n <button\n class=\"mt-1 text-xs/5 text-valencia-500 cursor-pointer disabled:cursor-not-allowed disabled:text-gray-300\"\n :disabled=\"getRolesForUser(user).includes('owner')\"\n @click=\"showRemoveModal = user\">\n Remove\n </button>\n </div>\n </div>\n </li>\n </ul>\n </div>\n <div>\n <div class=\"flex items-center justify-between\">\n <div class=\"text-xl font-bold\">\n Invitations\n </div>\n <div class=\"mt-4 sm:ml-16 sm:mt-0 sm:flex-none\">\n <button\n type=\"button\"\n @click=\"showNewInvitationModal = true\"\n :disabled=\"status === 'loading' || (workspace && !workspace.subscriptionTier)\"\n class=\"block rounded-md bg-ultramarine-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 disabled:bg-gray-500 disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n New Invitation\n <svg class=\"inline w-4 h-4 ml-1\" v-if=\"workspace && !workspace.subscriptionTier\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path fill-rule=\"evenodd\" d=\"M12 1.5a5.25 5.25 0 00-5.25 5.25v3a3 3 0 00-3 3v6.75a3 3 0 003 3h10.5a3 3 0 003-3v-6.75a3 3 0 00-3-3v-3c0-2.9-2.35-5.25-5.25-5.25zm3.75 8.25v-3a3.75 3.75 0 10-7.5 0v3h7.5z\" clip-rule=\"evenodd\" />\n </svg>\n </button>\n </div>\n </div>\n <div v-if=\"status === 'loading'\" class=\"mt-4\">\n <img src=\"images/loader.gif\" class=\"inline w-8 h-8\">\n </div>\n <div v-else-if=\"invitations?.length > 0\" class=\"mt-8 flow-root\" v-if=\"invitations?.length > 0\">\n <div class=\"-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8\">\n <div class=\"inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8\">\n <table class=\"min-w-full divide-y divide-gray-300\">\n <thead>\n <tr>\n <th scope=\"col\" class=\"py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0\">GitHub Username</th>\n <th scope=\"col\" class=\"px-3 py-3.5 text-left text-sm font-semibold text-gray-900\">Email</th>\n <th scope=\"col\" class=\"px-3 py-3.5 text-left text-sm font-semibold text-gray-900\">Status</th>\n <th scope=\"col\" class=\"px-3 py-3.5 text-left text-sm font-semibold text-gray-900\">Role</th>\n </tr>\n </thead>\n <tbody class=\"divide-y divide-gray-200 bg-white\">\n <tr v-for=\"invitation in invitations\">\n <td class=\"whitespace-nowrap py-5 pl-4 pr-3 text-sm sm:pl-0\">\n {{invitation.githubUsername}}\n </td>\n <td class=\"whitespace-nowrap px-3 py-5 text-sm text-gray-500\">\n {{invitation.email}}\n </td>\n <td class=\"whitespace-nowrap px-3 py-5 text-sm text-gray-500\">\n <span class=\"inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-600/20\">\n Pending\n </span>\n </td>\n <td class=\"whitespace-nowrap px-3 py-5 text-sm text-gray-500\">\n {{invitation.roles.join(', ')}}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n <div v-else-if=\"invitations?.length === 0\" class=\"mt-4\">\n <div class=\"text-center\">\n <svg class=\"mx-auto size-12 text-gray-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path vector-effect=\"non-scaling-stroke\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z\" />\n </svg>\n <h3 class=\"mt-2 text-sm font-semibold text-gray-900\">No invitations</h3>\n <p class=\"mt-1 text-sm text-gray-500\">You have no outstanding invitations</p>\n </div>\n </div>\n </div>\n\n <modal v-if=\"showNewInvitationModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showNewInvitationModal = false\">&times;</div>\n <new-invitation @close=\"showNewInvitationModal = false\" @invitationCreated=\"invitations.push($event.invitation)\"></new-invitation>\n </template>\n </modal>\n\n <modal v-if=\"showRemoveModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showRemoveModal = false\">&times;</div>\n <div>\n Are you sure you want to remove user <span class=\"font-bold\">{{showRemoveModal.githubUsername}}</span> from this workspace?\n </div>\n <div class=\"mt-6 grid grid-cols-2 gap-4\">\n <async-button\n @click=\"removeFromWorkspace(showConfirmDeleteModal)\"\n class=\"border-0 mt-0 flex w-full items-center justify-center gap-3 rounded-md bg-valencia-500 hover:bg-valencia-400 px-3 py-1.5 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-400\">\n <span class=\"text-sm font-semibold leading-6\">Yes, Remove</span>\n </async-button>\n\n <span @click=\"showRemoveModal = null\" class=\"cursor-pointer flex w-full items-center justify-center gap-3 rounded-md bg-slate-500 hover:bg-slate-400 px-3 py-1.5 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400\">\n <span class=\"text-sm font-semibold leading-6\">Cancel</span>\n </span>\n </div>\n </template>\n </modal>\n</div>\n";
4462
+ module.exports = "<div class=\"mx-auto max-w-5xl py-6 px-2 flex flex-col gap-8\">\n <div>\n <div class=\"text-xl font-bold\">\n Subscription Details\n </div>\n <div v-if=\"status === 'loading'\" class=\"mt-4\">\n <img src=\"images/loader.gif\" class=\"inline w-8 h-8\">\n </div>\n <div v-else-if=\"workspace && workspace.subscriptionTier\" class=\"mt-4 flex justify-between items-center\">\n <div>\n <span class=\"font-bold\">Tier:</span> {{workspace.subscriptionTier ?? 'No subscription'}}\n </div>\n <div>\n <async-button\n type=\"submit\"\n @click=\"getWorkspaceCustomerPortalLink\"\n class=\"inline-flex items-center justify-center rounded-md border border-transparent bg-ultramarine-600 py-1 px-2 text-sm font-medium text-white shadow-sm hover:bg-ultramarine-500 focus:outline-none focus:ring-2 focus:ring-forest-green-500 focus:ring-offset-2\">\n View in Stripe\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 ml-1\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25\" />\n </svg>\n </async-button>\n </div>\n </div>\n <div v-else-if=\"workspace && !workspace.subscriptionTier\" class=\"mt-4 flex justify-between items-center\">\n <div>\n <span class=\"font-bold\">No active subscription</span>\n <div class=\"text-sm text-gray-700\">\n You won't be able to invite your team until you activate a subscription\n </div>\n </div>\n <div>\n <a\n :href=\"paymentLink\"\n target=\"_blank\"\n class=\"inline-flex items-center justify-center rounded-md border border-transparent bg-ultramarine-600 py-1 px-2 text-sm font-medium text-white shadow-sm hover:bg-ultramarine-500 focus:outline-none focus:ring-2 focus:ring-ultramarine-500 focus:ring-offset-2\">\n Subscribe With Stripe\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 ml-1\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25\" />\n </svg>\n </a>\n </div>\n </div>\n </div>\n <div>\n <div class=\"text-xl font-bold\">\n Current Members\n </div>\n <div v-if=\"status === 'loading'\" class=\"mt-4\">\n <img src=\"images/loader.gif\" class=\"inline w-8 h-8\">\n </div>\n <ul v-else role=\"list\" class=\"divide-y divide-gray-100\">\n <li class=\"flex justify-between gap-x-6 py-5\" v-for=\"user in users\">\n <div class=\"flex min-w-0 gap-x-4\">\n <img class=\"size-12 flex-none rounded-full bg-gray-50\" :src=\"user.picture ?? 'images/logo.svg'\" alt=\"\">\n <div class=\"min-w-0 flex-auto\">\n <p class=\"text-sm/6 font-semibold text-gray-900\">\n {{user.name || user.githubUsername}}\n <span v-if=\"user.isFreeUser\" class=\"ml-1 inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20\">Free</span>\n </p>\n <p class=\"mt-1 truncate text-xs/5 text-gray-500\">{{user.email ?? 'No Email'}}</p>\n </div>\n </div>\n <div class=\"hidden shrink-0 sm:flex sm:flex-col sm:items-end\">\n <p class=\"text-sm/6 text-gray-900 capitalize\">{{getRolesForUser(user).join(', ')}}</p>\n <div class=\"flex gap-3\">\n <p class=\"mt-1 text-xs/5 text-gray-500 cursor-pointer\">\n Edit\n </p>\n <button\n class=\"mt-1 text-xs/5 text-valencia-500 cursor-pointer disabled:cursor-not-allowed disabled:text-gray-300\"\n :disabled=\"getRolesForUser(user).includes('owner')\"\n @click=\"showRemoveModal = user\">\n Remove\n </button>\n </div>\n </div>\n </li>\n </ul>\n </div>\n <div>\n <div class=\"flex items-center justify-between\">\n <div class=\"text-xl font-bold\">\n Invitations\n </div>\n <div class=\"mt-4 sm:ml-16 sm:mt-0 sm:flex-none\">\n <button\n type=\"button\"\n @click=\"showNewInvitationModal = true\"\n :disabled=\"status === 'loading'\"\n :tier=\"workspace?.subscriptionTier\"\n class=\"block rounded-md bg-ultramarine-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 disabled:bg-gray-500 disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n New Invitation\n <svg class=\"inline w-4 h-4 ml-1\" v-if=\"workspace && !workspace.subscriptionTier\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path fill-rule=\"evenodd\" d=\"M12 1.5a5.25 5.25 0 00-5.25 5.25v3a3 3 0 00-3 3v6.75a3 3 0 003 3h10.5a3 3 0 003-3v-6.75a3 3 0 00-3-3v-3c0-2.9-2.35-5.25-5.25-5.25zm3.75 8.25v-3a3.75 3.75 0 10-7.5 0v3h7.5z\" clip-rule=\"evenodd\" />\n </svg>\n </button>\n </div>\n </div>\n <div v-if=\"status === 'loading'\" class=\"mt-4\">\n <img src=\"images/loader.gif\" class=\"inline w-8 h-8\">\n </div>\n <div v-else-if=\"invitations?.length > 0\" class=\"mt-8 flow-root\" v-if=\"invitations?.length > 0\">\n <div class=\"-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8\">\n <div class=\"inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8\">\n <table class=\"min-w-full divide-y divide-gray-300\">\n <thead>\n <tr>\n <th scope=\"col\" class=\"py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0\">GitHub Username</th>\n <th scope=\"col\" class=\"px-3 py-3.5 text-left text-sm font-semibold text-gray-900\">Email</th>\n <th scope=\"col\" class=\"px-3 py-3.5 text-left text-sm font-semibold text-gray-900\">Status</th>\n <th scope=\"col\" class=\"px-3 py-3.5 text-left text-sm font-semibold text-gray-900\">Role</th>\n </tr>\n </thead>\n <tbody class=\"divide-y divide-gray-200 bg-white\">\n <tr v-for=\"invitation in invitations\">\n <td class=\"whitespace-nowrap py-5 pl-4 pr-3 text-sm sm:pl-0\">\n {{invitation.githubUsername}}\n </td>\n <td class=\"whitespace-nowrap px-3 py-5 text-sm text-gray-500\">\n {{invitation.email}}\n </td>\n <td class=\"whitespace-nowrap px-3 py-5 text-sm text-gray-500\">\n <span class=\"inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-600/20\">\n Pending\n </span>\n </td>\n <td class=\"whitespace-nowrap px-3 py-5 text-sm text-gray-500\">\n {{invitation.roles.join(', ')}}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n <div v-else-if=\"invitations?.length === 0\" class=\"mt-4\">\n <div class=\"text-center\">\n <svg class=\"mx-auto size-12 text-gray-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path vector-effect=\"non-scaling-stroke\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z\" />\n </svg>\n <h3 class=\"mt-2 text-sm font-semibold text-gray-900\">No invitations</h3>\n <p class=\"mt-1 text-sm text-gray-500\">You have no outstanding invitations</p>\n </div>\n </div>\n </div>\n\n <modal v-if=\"showNewInvitationModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showNewInvitationModal = false\">&times;</div>\n <new-invitation @close=\"showNewInvitationModal = false\" @invitationCreated=\"invitations.push($event.invitation)\"></new-invitation>\n </template>\n </modal>\n\n <modal v-if=\"showRemoveModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showRemoveModal = false\">&times;</div>\n <div>\n Are you sure you want to remove user <span class=\"font-bold\">{{showRemoveModal.githubUsername}}</span> from this workspace?\n </div>\n <div class=\"mt-6 grid grid-cols-2 gap-4\">\n <async-button\n @click=\"removeFromWorkspace(showConfirmDeleteModal)\"\n class=\"border-0 mt-0 flex w-full items-center justify-center gap-3 rounded-md bg-valencia-500 hover:bg-valencia-400 px-3 py-1.5 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-400\">\n <span class=\"text-sm font-semibold leading-6\">Yes, Remove</span>\n </async-button>\n\n <span @click=\"showRemoveModal = null\" class=\"cursor-pointer flex w-full items-center justify-center gap-3 rounded-md bg-slate-500 hover:bg-slate-400 px-3 py-1.5 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400\">\n <span class=\"text-sm font-semibold leading-6\">Cancel</span>\n </span>\n </div>\n </template>\n </modal>\n</div>\n";
4436
4463
 
4437
4464
  /***/ }),
4438
4465
 
@@ -67,13 +67,15 @@ app.component('app-component', {
67
67
  window.state = this;
68
68
 
69
69
  if (mothership.hasAPIKey) {
70
- const href = window.location.href;
71
- if (href.match(/\?code=([a-zA-Z0-9]+)$/)) {
72
- const code = href.match(/\?code=([a-zA-Z0-9]+)$/)[1];
70
+ const hashParams = new URLSearchParams(window.location.hash.replace(/^#?\/?\??/, '') || '');
71
+ if (hashParams.has('code')) {
72
+ const code = hashParams.get('code');
73
+ const provider = hashParams.get('provider');
73
74
  try {
74
- const { accessToken, user, roles } = await mothership.github(code);
75
+ const { accessToken, user, roles } = provider === 'github' ? await mothership.github(code) : await mothership.google(code);
75
76
  if (roles == null) {
76
77
  this.authError = 'You are not authorized to access this workspace';
78
+ this.status = 'loaded';
77
79
  return;
78
80
  }
79
81
  this.user = user;
@@ -162,12 +162,12 @@
162
162
  <div class="text-sm font-mono">{{ JSON.stringify(index.key) }}</div>
163
163
  </div>
164
164
  <div>
165
- <button
166
- type="submit"
167
- @click="dropIndex()"
165
+ <async-button
166
+ type="button"
167
+ @click="dropIndex(index.name)"
168
168
  class="rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:bg-gray-400 disabled:cursor-not-allowed">
169
169
  Drop
170
- </button>
170
+ </async-button>
171
171
  </div>
172
172
  </div>
173
173
  </div>
@@ -183,9 +183,10 @@
183
183
  </div>
184
184
  </div>
185
185
  <div class="mt-4 flex gap-2">
186
- <button type="submit" @click="filterDocuments()" class="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-teal-600">Filter Selection</button>
187
- <button type="submit" @click="deselectAll()" class="rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600">Deselect All</button>
188
- <button type="submit" @click="resetDocuments()" class="rounded-md bg-gray-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600" >Cancel</button>
186
+ <button type="button" @click="filterDocuments()" class="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-teal-600">Filter Selection</button>
187
+ <button type="button" @click="selectAll()" class="rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600">Select All</button>
188
+ <button type="button" @click="deselectAll()" class="rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600">Deselect All</button>
189
+ <button type="button" @click="resetDocuments()" class="rounded-md bg-gray-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600" >Cancel</button>
189
190
  </div>
190
191
  </template>
191
192
  </modal>
@@ -93,6 +93,10 @@ module.exports = app => app.component('models', {
93
93
  this.status = 'loaded';
94
94
  },
95
95
  methods: {
96
+ async dropIndex(name) {
97
+ const { mongoDBIndexes } = await api.Model.dropIndex({ model: this.currentModel, index: name });
98
+ this.mongoDBIndexes = mongoDBIndexes;
99
+ },
96
100
  initFilter(ev) {
97
101
  if (!this.searchText) {
98
102
  this.searchText = '{}';
@@ -191,7 +195,7 @@ module.exports = app => app.component('models', {
191
195
  },
192
196
  async openIndexModal() {
193
197
  this.shouldShowIndexModal = true;
194
- const { mongoDBIndexes, schemaIndexes } = await api.Model.getIndexes({ model: this.currentModel })
198
+ const { mongoDBIndexes, schemaIndexes } = await api.Model.getIndexes({ model: this.currentModel });
195
199
  this.mongoDBIndexes = mongoDBIndexes;
196
200
  this.schemaIndexes = schemaIndexes;
197
201
  },
@@ -295,6 +299,9 @@ module.exports = app => app.component('models', {
295
299
  deselectAll() {
296
300
  this.selectedPaths = [];
297
301
  },
302
+ selectAll() {
303
+ this.selectedPaths = [...this.schemaPaths];
304
+ },
298
305
  isSelected(path) {
299
306
  return this.selectedPaths.find(x => x.path == path);
300
307
  },
@@ -22,6 +22,10 @@ exports.githubLogin = function githubLogin() {
22
22
  return client.post('/githubLogin', { state: window.location.href }).then(res => res.data);
23
23
  };
24
24
 
25
+ exports.googleLogin = function googleLogin() {
26
+ return client.post('/googleLogin', { state: window.location.href }).then(res => res.data);
27
+ };
28
+
25
29
  exports.getWorkspaceTeam = function getWorkspaceTeam() {
26
30
  return client.post('/getWorkspaceTeam', { workspaceId: window.MONGOOSE_STUDIO_CONFIG.workspace._id }).then(res => res.data);
27
31
  };
@@ -34,6 +38,10 @@ exports.github = function github(code) {
34
38
  return client.post('/github', { code, workspaceId: window.MONGOOSE_STUDIO_CONFIG.workspace._id }).then(res => res.data);
35
39
  };
36
40
 
41
+ exports.google = function google(code) {
42
+ return client.post('/google', { code, workspaceId: window.MONGOOSE_STUDIO_CONFIG.workspace._id }).then(res => res.data);
43
+ };
44
+
37
45
  exports.inviteToWorkspace = function inviteToWorkspace(params) {
38
46
  return client.post('/inviteToWorkspace', { workspaceId: window.MONGOOSE_STUDIO_CONFIG.workspace._id, ...params }).then(res => res.data);
39
47
  };
@@ -12,13 +12,25 @@
12
12
  <div class="mt-2 text-gray-700" v-if="!loading">
13
13
  {{workspaceName}}
14
14
  </div>
15
- <div class="mt-4" v-if="!loading">
16
- <async-button
17
- type="button"
18
- @click="loginWithGithub"
19
- class="rounded bg-ultramarine-600 px-2 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">
20
- Login With GitHub
21
- </async-button>
15
+ <div class="mt-4 flex gap-4 justify-center" v-if="!loading">
16
+ <div>
17
+ <async-button
18
+ type="button"
19
+ @click="loginWithGithub"
20
+ class="rounded bg-ultramarine-600 px-2 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">
21
+ <svg viewBox="0 0 98 98" class="inline mr-1" height="1.5em" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
22
+ Login With GitHub
23
+ </async-button>
24
+ </div>
25
+ <div>
26
+ <async-button
27
+ type="button"
28
+ @click="loginWithGoogle"
29
+ class="rounded bg-ultramarine-600 px-2 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">
30
+ <svg class="inline" xmlns="http://www.w3.org/2000/svg" height="1.5em" viewBox="0 0 512 512"><path fill="#fff" d="M386 400c45-42 65-112 53-179H260v74h102c-4 24-18 44-38 57z"/><path fill="#fff" d="M90 341a192 192 0 0 0 296 59l-62-48c-53 35-141 22-171-60z"/><path fill="#fff" d="M153 292c-8-25-8-48 0-73l-63-49c-23 46-30 111 0 171z"/><path fill="#fff" d="M153 219c22-69 116-109 179-50l55-54c-78-75-230-72-297 55z"/></svg>
31
+ Login With Google
32
+ </async-button>
33
+ </div>
22
34
  </div>
23
35
  <div class="mt-4" v-if="state.authError">
24
36
  <div class="rounded-md bg-red-50 p-4">
@@ -17,6 +17,10 @@ module.exports = app => app.component('splash', {
17
17
  async loginWithGithub() {
18
18
  const { url } = await mothership.githubLogin();
19
19
  window.location.href = url;
20
+ },
21
+ async loginWithGoogle() {
22
+ const { url } = await mothership.googleLogin();
23
+ window.location.href = url;
20
24
  }
21
25
  }
22
26
  });
@@ -21,15 +21,19 @@
21
21
  <div>
22
22
  <label for="location" class="block text-sm/6 font-medium text-gray-900">Role</label>
23
23
  <div class="mt-2 grid grid-cols-1">
24
- <select id="role" name="role" v-model="role" class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pl-3 pr-8 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
24
+ <select id="role" :disabled="tier == null" name="role" v-model="role" class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pl-3 pr-8 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
25
25
  <option value="admin">Admin</option>
26
26
  <option value="member">Member</option>
27
27
  <option value="readonly">Read-only</option>
28
+ <option value="dashboards">Dashboards Only</option>
28
29
  </select>
29
30
  <svg class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-500 sm:size-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" data-slot="icon">
30
31
  <path fill-rule="evenodd" d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
31
32
  </svg>
32
33
  </div>
34
+ <div v-if="tier == null" class="text-sm text-gray-700">
35
+ You can only invite "Dashboards Only" users until you set up a subscription.
36
+ </div>
33
37
  </div>
34
38
 
35
39
  <async-button
@@ -5,12 +5,18 @@ const template = require('./new-invitation.html');
5
5
 
6
6
  module.exports = app => app.component('new-invitation', {
7
7
  template,
8
+ props: ['tier'],
8
9
  emits: ['close', 'invitationCreated'],
9
10
  data: () => ({
10
11
  githubUsername: '',
11
12
  email: '',
12
13
  role: null
13
14
  }),
15
+ mounted() {
16
+ if (this.tier == null) {
17
+ this.role = 'dashboards';
18
+ }
19
+ },
14
20
  methods: {
15
21
  async inviteToWorkspace() {
16
22
  const { invitation } = await mothership.inviteToWorkspace({ githubUsername: this.githubUsername, email: this.email, roles: [this.role] });
@@ -87,7 +87,8 @@
87
87
  <button
88
88
  type="button"
89
89
  @click="showNewInvitationModal = true"
90
- :disabled="status === 'loading' || (workspace && !workspace.subscriptionTier)"
90
+ :disabled="status === 'loading'"
91
+ :tier="workspace?.subscriptionTier"
91
92
  class="block rounded-md bg-ultramarine-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 disabled:bg-gray-500 disabled:cursor-not-allowed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600">
92
93
  New Invitation
93
94
  <svg class="inline w-4 h-4 ml-1" v-if="workspace && !workspace.subscriptionTier" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.0.88",
3
+ "version": "0.0.90",
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": {