boomack 0.8.2 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/client/public/js/client.js +41 -20
  2. package/client/themes/blue-night/properties.json +1 -0
  3. package/client/themes/dark/properties.json +1 -0
  4. package/client/themes/default/properties.json +1 -0
  5. package/client/themes/green-night/properties.json +1 -0
  6. package/client/themes/iron/properties.json +1 -0
  7. package/client/themes/red-night/properties.json +1 -0
  8. package/client/themes/science/properties.json +1 -0
  9. package/client/views/panel.ejs +3 -1
  10. package/client/views/parts/client-resources.ejs +6 -6
  11. package/client/views/parts/document-layout.ejs +2 -0
  12. package/client/views/parts/grid-layout.ejs +2 -0
  13. package/client/views/parts/panel-head.ejs +2 -0
  14. package/client/views/parts/slot.ejs +22 -4
  15. package/client/views/slot-panel.ejs +2 -1
  16. package/default-config.yaml +7 -2
  17. package/package.json +6 -5
  18. package/server-build/cli.js +0 -0
  19. package/server-build/index.js +47 -21
  20. package/server-build/pipeline/render.js +0 -1
  21. package/server-build/pipeline/render.test.js +5 -5
  22. package/server-build/pipeline/transform.test.js +1 -1
  23. package/server-build/pipeline.js +4 -3
  24. package/server-build/plugins/csv.js +3 -6
  25. package/server-build/plugins/generic.js +1 -1
  26. package/server-build/plugins/highlight.js +1 -1
  27. package/server-build/plugins/markdown.js +2 -2
  28. package/server-build/plugins/media.js +3 -3
  29. package/server-build/plugins/text.js +3 -3
  30. package/server-build/routes/eval-request.js +134 -0
  31. package/server-build/routes/eval-requests.js +152 -0
  32. package/server-build/routes/export.js +25 -10
  33. package/server-build/routes/panels.js +25 -28
  34. package/server-build/routes/web-client.js +80 -7
  35. package/server-build/service/client-resources.js +1 -1
  36. package/server-build/service/panels.js +5 -0
  37. package/server-build/service/plugins.js +74 -57
  38. package/server-build/service/plugins.test.js +14 -7
  39. package/server-build/service/render.js +7 -1
  40. package/server-build/service/render.test.js +11 -11
  41. package/server-build/service/transform.js +4 -2
  42. package/server-build/service/transform.test.js +4 -4
  43. package/server-build/typedefs.js +14 -5
@@ -44,7 +44,7 @@ exports.textTransformations = {
44
44
  'commonmark': textTransformationCommonmark,
45
45
  'markdown': textTransformationMarkdownIt,
46
46
  };
47
- function textTransformationCommonmark({ text }) {
47
+ function textTransformationCommonmark(text) {
48
48
  const parser = new Parser({
49
49
  smart: true,
50
50
  });
@@ -54,7 +54,7 @@ function textTransformationCommonmark({ text }) {
54
54
  const dom = parser.parse(text);
55
55
  return renderer.render(dom);
56
56
  }
57
- function textTransformationMarkdownIt({ text, options }) {
57
+ function textTransformationMarkdownIt(text, { options }) {
58
58
  // configure Markdown it! with options
59
59
  const md = new MarkdownIt({
60
60
  html: _.isBoolean(options.allowHtml) ? options.allowHtml : true,
@@ -47,10 +47,10 @@ exports.renderers = {
47
47
  'audio': renderAudio,
48
48
  'video': renderVideo,
49
49
  };
50
- function renderImage(url, type, options) {
50
+ function renderImage(url) {
51
51
  return `<img src="${url}"/>`;
52
52
  }
53
- function renderAudio(url, type, options) {
53
+ function renderAudio(url, { type, options }) {
54
54
  let switchAttributes = '';
55
55
  if (enforceBoolean(options.mediaControls, true))
56
56
  switchAttributes += ' controls';
@@ -65,7 +65,7 @@ function renderAudio(url, type, options) {
65
65
  `</a>` +
66
66
  `</audio>`;
67
67
  }
68
- function renderVideo(url, type, options) {
68
+ function renderVideo(url, { type, options }) {
69
69
  let switchAttributes = '';
70
70
  if (enforceBoolean(options.mediaControls, true))
71
71
  switchAttributes += ' controls';
@@ -26,13 +26,13 @@ exports.textTransformations = {
26
26
  'uppercase': textTransformationUpperCase,
27
27
  'lowercase': textTransformationLowerCase,
28
28
  };
29
- function textTransformationPlain({ text }) {
29
+ function textTransformationPlain(text) {
30
30
  return html.textLines(text);
31
31
  }
32
- function textTransformationUpperCase({ text }) {
32
+ function textTransformationUpperCase(text) {
33
33
  return html.textLines(_.upperCase(text));
34
34
  }
35
- function textTransformationLowerCase({ text }) {
35
+ function textTransformationLowerCase(text) {
36
36
  return html.textLines(_.lowerCase(text));
37
37
  }
38
38
  //# sourceMappingURL=text.js.map
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setup = void 0;
4
+ const _ = require('lodash');
5
+ const YAML = require('yaml');
6
+ const async = require('async');
7
+ const express = require('express');
8
+ const { requestStream } = require('../utils');
9
+ const { info, warning, error } = require('../service/response-messages');
10
+ const cfg = require('../service/config');
11
+ const { log } = require('../service/logging');
12
+ const { checkPanelAccess } = require('../service/rbac');
13
+ const panels = require('../service/panels');
14
+ const themes = require('../service/themes');
15
+ const { apiAuth, forbiddenMessage } = require('../middleware/token-auth');
16
+ const { dataBody } = require('../middleware/data-parser');
17
+ const pipeline = require('../pipeline');
18
+ function setup(app, io) {
19
+ if (!cfg.getBoolean('api.enable.evaluation')) {
20
+ log.info('Deactivated routes for evaluation requests');
21
+ return;
22
+ }
23
+ const contentNS = io.of('/boomack-content');
24
+ function checkAuthorization(authorization, request, cb) {
25
+ if (!authorization) {
26
+ cb(null, request);
27
+ return;
28
+ }
29
+ const panelId = request.panel || 'default';
30
+ if (!checkPanelAccess(authorization, panelId)) {
31
+ cb({ message: 'Forbidden', forbidden: true });
32
+ return;
33
+ }
34
+ cb(null, request);
35
+ }
36
+ function checkRequest(request, cb) {
37
+ cb(null, {
38
+ panel: request.panel || 'default',
39
+ script: typeof request.script === 'string' ? request.script : null,
40
+ });
41
+ }
42
+ function handleEvaluationRequest(requests, auth, res) {
43
+ log.verbose(`Request to evaluate: ${_.size(requests)} JS code blocks`);
44
+ const checkPermissions = (r, cb) => checkAuthorization(auth, r, cb);
45
+ async.waterfall([
46
+ cb => async.map(requests, checkPermissions, cb),
47
+ (requests, cb) => async.map(requests, checkRequest, cb),
48
+ (commands, cb) => async.filter(commands, (cmd, cb) => cb(null, !!cmd.script), cb),
49
+ (commands, cb) => {
50
+ log.verbose(`Sending ${_.size(commands)} evaluation commands`);
51
+ // distribute commands on different panels
52
+ const evalCommands = _.groupBy(commands, c => c.panel);
53
+ _.forEach(evalCommands, (cmds, id) => {
54
+ contentNS.to(id).emit('eval', cmds);
55
+ });
56
+ cb(null);
57
+ },
58
+ ], err => {
59
+ if (err) {
60
+ if (err.forbidden) {
61
+ error(res, forbiddenMessage);
62
+ }
63
+ else {
64
+ console.error('ERROR', err.message);
65
+ console.error(err.stack);
66
+ error(res, {
67
+ title: 'Sending evaluation commands failed',
68
+ message: err.message,
69
+ details: err.stack,
70
+ });
71
+ }
72
+ }
73
+ else {
74
+ res.status(204).send();
75
+ }
76
+ });
77
+ }
78
+ app.post('/v1/eval', [apiAuth('content.evaluate'), dataBody], (req, res) => {
79
+ if (!_.isObject(req.body) && !_.isArray(req.body)) {
80
+ const hint = cfg.getBoolean('api.request.yaml') ?
81
+ 'Hint: JSON and YAML is supported. Make sure to use the right Content-Type header.' :
82
+ 'Hint: Only JSON is allowed. Make sure to use the right Content-Type header.';
83
+ if (req.parsingError) {
84
+ error(res, {
85
+ status: 400,
86
+ title: 'Malformed Request Body',
87
+ message: req.parsingError,
88
+ details: hint,
89
+ });
90
+ }
91
+ else {
92
+ error(res, {
93
+ status: 400,
94
+ title: 'Malformed Request Body',
95
+ message: 'Empty body or unknown error during body parsing.',
96
+ detail: hint,
97
+ });
98
+ }
99
+ return;
100
+ }
101
+ const requests = _.isArray(req.body) ? req.body : [req.body];
102
+ handleEvaluationRequest(requests, req.authorization, res);
103
+ });
104
+ app.post('/v1/panels/:panelId/eval', [
105
+ apiAuth('content.evaluate'),
106
+ express.text({ limit: '16MB', type: 'application/javascript' })
107
+ ], (req, res) => {
108
+ const panelId = req.params.panelId;
109
+ const panel = panels.lookup(panelId);
110
+ if (!panel) {
111
+ error(res, {
112
+ status: 404,
113
+ title: 'Target Not Found',
114
+ message: `The target panel "${panelId}" does not exist.`,
115
+ });
116
+ }
117
+ const type = req.get('Content-Type');
118
+ if (type !== 'application/javascript') {
119
+ error(res, {
120
+ status: 400,
121
+ title: 'Unsupported Content-Type',
122
+ message: 'The MIME type of the evaluation request must be application/javascript.',
123
+ });
124
+ return;
125
+ }
126
+ const evalRequest = {
127
+ panel: panelId,
128
+ stream: req.body,
129
+ };
130
+ handleEvaluationRequest([evalRequest], req.authorization, res);
131
+ });
132
+ }
133
+ exports.setup = setup;
134
+ //# sourceMappingURL=eval-request.js.map
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setup = void 0;
4
+ const _ = require('lodash');
5
+ const YAML = require('yaml');
6
+ const async = require('async');
7
+ const express = require('express');
8
+ const { requestStream } = require('../utils');
9
+ const { info, warning, error } = require('../service/response-messages');
10
+ const cfg = require('../service/config');
11
+ const { log } = require('../service/logging');
12
+ const { checkPanelAccess } = require('../service/rbac');
13
+ const panels = require('../service/panels');
14
+ const themes = require('../service/themes');
15
+ const { apiAuth, forbiddenMessage } = require('../middleware/token-auth');
16
+ const { dataBody } = require('../middleware/data-parser');
17
+ const pipeline = require('../pipeline');
18
+ function setup(app, io) {
19
+ if (!cfg.getBoolean('api.enable.evaluation')) {
20
+ log.info('Deactivated routes for evaluation requests');
21
+ return;
22
+ }
23
+ const contentNS = io.of('/boomack-content');
24
+ function checkAuthorization(authorization, request, cb) {
25
+ if (!authorization) {
26
+ cb(null, request);
27
+ return;
28
+ }
29
+ const panelId = request.panel || 'default';
30
+ if (!checkPanelAccess(authorization, panelId)) {
31
+ cb({ message: 'Forbidden', forbidden: true });
32
+ return;
33
+ }
34
+ cb(null, request);
35
+ }
36
+ function checkRequest(request, cb) {
37
+ const panelId = request.panel;
38
+ const panel = panels.lookup(panelId);
39
+ if (!panel) {
40
+ cb({
41
+ notFound: true,
42
+ message: `The target panel "${panelId}" does not exist.`,
43
+ });
44
+ return;
45
+ }
46
+ cb(null, {
47
+ panel: request.panel || 'default',
48
+ script: typeof request.script === 'string' ? request.script : null,
49
+ });
50
+ }
51
+ function handleEvaluationRequest(requests, auth, res) {
52
+ log.verbose(`Request to evaluate: ${_.size(requests)} JS code blocks`);
53
+ const checkPermissions = (r, cb) => checkAuthorization(auth, r, cb);
54
+ async.waterfall([
55
+ cb => async.map(requests, checkPermissions, cb),
56
+ (requests, cb) => async.map(requests, checkRequest, cb),
57
+ (commands, cb) => async.filter(commands, (cmd, cb) => cb(null, !!cmd.script), cb),
58
+ (commands, cb) => {
59
+ if (_.size(commands) > 0) {
60
+ log.verbose(`Sending ${_.size(commands)} evaluation commands`);
61
+ // distribute commands on different panels
62
+ const evalCommands = _.groupBy(commands, c => c.panel);
63
+ _.forEach(evalCommands, (cmds, id) => {
64
+ contentNS.to(id).emit('eval', cmds);
65
+ });
66
+ cb(null);
67
+ }
68
+ },
69
+ ], err => {
70
+ if (err) {
71
+ if (err.forbidden) {
72
+ error(res, forbiddenMessage);
73
+ }
74
+ else if (err.notFound) {
75
+ error(res, {
76
+ status: 400,
77
+ title: 'Panel not found',
78
+ message: err.message,
79
+ });
80
+ }
81
+ else {
82
+ console.error('ERROR', err.message);
83
+ console.error(err.stack);
84
+ error(res, {
85
+ title: 'Sending evaluation commands failed',
86
+ message: err.message,
87
+ details: err.stack,
88
+ });
89
+ }
90
+ }
91
+ else {
92
+ res.status(204).send();
93
+ }
94
+ });
95
+ }
96
+ app.post('/v1/eval', [apiAuth('content.evaluate'), dataBody], (req, res) => {
97
+ if (!_.isObject(req.body) && !_.isArray(req.body)) {
98
+ const hint = cfg.getBoolean('api.request.yaml') ?
99
+ 'Hint: JSON and YAML is supported. Make sure to use the right Content-Type header.' :
100
+ 'Hint: Only JSON is allowed. Make sure to use the right Content-Type header.';
101
+ if (req.parsingError) {
102
+ error(res, {
103
+ status: 400,
104
+ title: 'Malformed Request Body',
105
+ message: req.parsingError,
106
+ details: hint,
107
+ });
108
+ }
109
+ else {
110
+ error(res, {
111
+ status: 400,
112
+ title: 'Malformed Request Body',
113
+ message: 'Empty body or unknown error during body parsing.',
114
+ detail: hint,
115
+ });
116
+ }
117
+ return;
118
+ }
119
+ const requests = _.isArray(req.body) ? req.body : [req.body];
120
+ handleEvaluationRequest(requests, req.authorization, res);
121
+ });
122
+ app.post('/v1/panels/:panelId/eval', [
123
+ apiAuth('content.evaluate'),
124
+ express.text({ limit: '16MB', type: 'application/javascript' })
125
+ ], (req, res) => {
126
+ const panelId = req.params.panelId;
127
+ const panel = panels.lookup(panelId);
128
+ if (!panel) {
129
+ error(res, {
130
+ status: 404,
131
+ title: 'Target Not Found',
132
+ message: `The target panel "${panelId}" does not exist.`,
133
+ });
134
+ }
135
+ const type = req.get('Content-Type');
136
+ if (type !== 'application/javascript') {
137
+ error(res, {
138
+ status: 400,
139
+ title: 'Unsupported Content-Type',
140
+ message: 'The MIME type of the evaluation request must be application/javascript.',
141
+ });
142
+ return;
143
+ }
144
+ const evalRequest = {
145
+ panel: panelId,
146
+ script: req.body,
147
+ };
148
+ handleEvaluationRequest([evalRequest], req.authorization, res);
149
+ });
150
+ }
151
+ exports.setup = setup;
152
+ //# sourceMappingURL=eval-requests.js.map
@@ -16,6 +16,7 @@ const clientRes = require('../service/client-resources');
16
16
  const plugins = require('../service/plugins');
17
17
  const resources = require('../service/resources');
18
18
  const uiPaths = require('../service/web-ui-paths');
19
+ const { error } = require('../service/response-messages');
19
20
  const { webAuthBasic } = require('../middleware/basic-auth');
20
21
  const { webAuthStandard } = require('../middleware/standard-auth');
21
22
  const { apiAuth, forbiddenMessage } = require('../middleware/token-auth');
@@ -161,9 +162,13 @@ exports.setup = function (app) {
161
162
  const clientResGroupIds = clientRes.getResourceGroupsForPanel(panel);
162
163
  // plugin resources
163
164
  _.forEach(clientResGroupIds, groupId => {
164
- const group = plugins.getClientResourceGroup(groupId);
165
- _.forEach(group, (resource, i) => {
166
- zip.file(resource.path, { name: `plugins/client-resources/${groupId}/${i}` });
165
+ const stylePaths = plugins.getStylePathsOfClientResourceGroup(groupId);
166
+ _.forEach(stylePaths, (resourcePath, i) => {
167
+ zip.file(resourcePath, { name: `plugins/styles/${groupId}/${i}` });
168
+ });
169
+ const scriptPaths = plugins.getScriptPathsOfClientResourceGroup(groupId);
170
+ _.forEach(scriptPaths, (resourcePath, i) => {
171
+ zip.file(resourcePath, { name: `plugins/scripts/${groupId}/${i}` });
167
172
  });
168
173
  });
169
174
  // resources
@@ -258,7 +263,8 @@ exports.setup = function (app) {
258
263
  const subject = slot ?
259
264
  `slot ${panel.id}/${slot.id}` :
260
265
  `panel ${panel.id}`;
261
- const pluginResourcesDir = path.join(targetDir, 'plugins', 'client-resources');
266
+ const pluginStylesDir = path.join(targetDir, 'plugins', 'styles');
267
+ const pluginScriptsDir = path.join(targetDir, 'plugins', 'scripts');
262
268
  const resourcesSubPath = 'resources/' + name;
263
269
  render(panel, slot, options, (err, html) => {
264
270
  if (err) {
@@ -269,7 +275,11 @@ exports.setup = function (app) {
269
275
  const displayCommands = getDisplayCommands(panel, slot);
270
276
  const referencedResources = findReferencedResources(displayCommands);
271
277
  html = replaceResourceUrls(html, referencedResources, basePath + resourcesSubPath);
272
- const clientResGroups = _.map(clientRes.getResourceGroupsForPanel(panel, slot), groupId => ({ id: groupId, group: plugins.getClientResourceGroup(groupId) }));
278
+ const clientResGroups = _.map(clientRes.getResourceGroupsForPanel(panel, slot), groupId => ({
279
+ id: groupId,
280
+ stylePaths: plugins.getStylePathsOfClientResourceGroup(groupId),
281
+ scriptPaths: plugins.getScriptPathsOfClientResourceGroup(groupId),
282
+ }));
273
283
  const maxResourceSize = parseDataAmount(cfg.get('export.local.resourceSizeLimit'));
274
284
  const includedResources = [];
275
285
  let resourceSize = 0;
@@ -330,12 +340,17 @@ exports.setup = function (app) {
330
340
  }
331
341
  },
332
342
  // plugin resources
333
- cb => async.each(clientResGroups, ({ id, group }, cb) => {
334
- const groupResourcesDir = path.join(pluginResourcesDir, id);
343
+ cb => async.each(clientResGroups, ({ id, stylePaths, scriptPaths }, cb) => {
344
+ const groupStylesDir = path.join(pluginStylesDir, id);
345
+ const groupScriptsDir = path.join(pluginScriptsDir, id);
335
346
  async.waterfall([
336
- cb => mkdir(path.join(groupResourcesDir), cb),
337
- cb => async.eachOf(group, (resource, i, cb) => {
338
- fs.copyFile(resource.path, path.join(groupResourcesDir, i.toString()), cb);
347
+ cb => mkdir(path.join(groupStylesDir), cb),
348
+ cb => async.eachOf(stylePaths, (p, i, cb) => {
349
+ fs.copyFile(p, path.join(groupStylesDir, i.toString()), cb);
350
+ }, cb),
351
+ cb => mkdir(path.join(groupScriptsDir), cb),
352
+ cb => async.eachOf(scriptPaths, (p, i, cb) => {
353
+ fs.copyFile(p, path.join(groupScriptsDir, i.toString()), cb);
339
354
  }, cb),
340
355
  ], cb);
341
356
  }, cb),
@@ -73,17 +73,30 @@ exports.setup = function (app, io) {
73
73
  }
74
74
  const panel = panels.lookup(panelId);
75
75
  if (panel && (panel.layout.grid || panel.layout.document)) {
76
+ const theme = themes.safeTheme(panel.layout.theme);
76
77
  res.format({
77
- html: () => res.render(panel.layout.document ? 'parts/document-layout.ejs' : 'parts/grid-layout.ejs', {
78
- _,
79
- uiPaths,
80
- query: req.query,
81
- basePath: cfg.serverBasePath(),
82
- themes: themes.getThemes(),
83
- theme: themes.safeTheme(req.query.theme || panel.layout.theme),
84
- themeOverride: !!req.query.theme,
85
- panel,
86
- offline: true,
78
+ html: () => themes.getThemeProperties(theme, (err, themeProperties) => {
79
+ if (err) {
80
+ warning(res, {
81
+ status: 500,
82
+ title: 'Theme Error',
83
+ message: 'Failed to load the properties of the selected theme.',
84
+ details: err,
85
+ });
86
+ }
87
+ else {
88
+ res.render(panel.layout.type === 'document' ? 'parts/document-layout.ejs' : 'parts/grid-layout.ejs', {
89
+ _,
90
+ uiPaths,
91
+ query: req.query,
92
+ basePath: cfg.serverBasePath(),
93
+ theme,
94
+ themeOverride: false,
95
+ themeProperties,
96
+ panel,
97
+ offline: true,
98
+ });
99
+ }
87
100
  }),
88
101
  json: () => res.json(panel.layout),
89
102
  text: () => res.type('text').send(YAML.stringify(panel.layout)),
@@ -143,6 +156,7 @@ exports.setup = function (app, io) {
143
156
  panel.layout.theme != newLayout.theme ||
144
157
  panel.layout.style != newLayout.style ||
145
158
  panel.layout.script != newLayout.script ||
159
+ panel.layout.type != newLayout.type ||
146
160
  !_.isEqual(panel.layout.grid, newLayout.grid) ||
147
161
  !_.isEqual(panel.layout.document, newLayout.document));
148
162
  action(panelId, newLayout, (err, panel2) => {
@@ -158,24 +172,7 @@ exports.setup = function (app, io) {
158
172
  contentNS.to(panelId).emit('reload', {});
159
173
  }
160
174
  else {
161
- app.render(panel2.layout.document ? 'parts/document-layout.ejs' : 'parts/grid-layout.ejs', {
162
- _,
163
- uiPaths,
164
- query: {},
165
- basePath: cfg.serverBasePath(),
166
- themes: themes.getThemes(),
167
- theme: themes.safeTheme(panel2.layout.theme),
168
- themeOverride: false,
169
- panel: panel2,
170
- offline: false,
171
- }, (err, html) => {
172
- if (err) {
173
- log.error('Rendering layout failed: ' + err.message);
174
- log.error(err.stack);
175
- return;
176
- }
177
- contentNS.to(panelId).emit('layout', html);
178
- });
175
+ contentNS.to(panelId).emit('reload-layout', {});
179
176
  }
180
177
  if (panel) {
181
178
  log.verbose(`Layout updated for '${panelId}': ` +
@@ -173,6 +173,22 @@ exports.setup = function (app, io) {
173
173
  },
174
174
  })]);
175
175
  });
176
+ _.forEach(plugins.getPlugInIds(), plugInId => {
177
+ const plugin = plugins.getPlugIn(plugInId);
178
+ const routes = _.keys(plugin.static);
179
+ _.forEach(routes, route => {
180
+ if (route.indexOf(' ') >= 0) {
181
+ log.warn('Invalid route for static resource in plugin ' + plugInId + ': ' + route);
182
+ return;
183
+ }
184
+ const resourcePath = _.get(plugin, ['static', route]);
185
+ if (!path.isAbsolute(resourcePath)) {
186
+ log.warn('Path to static resource ' + route + ' in plugin ' + plugInId + ' is not absolute');
187
+ return;
188
+ }
189
+ app.use('/plugins/' + plugInId + '/static/' + _.trimStart(route, '/'), express.static(resourcePath, staticOptions));
190
+ });
191
+ });
176
192
  function visiblePanelsForRequest(req) {
177
193
  return _.chain(panels.getPanelIds())
178
194
  .filter(panelId => isAccessToPanelAllowed(req, panelId))
@@ -231,6 +247,42 @@ exports.setup = function (app, io) {
231
247
  log.verbose('A home client disconnected from Socket.IO');
232
248
  });
233
249
  });
250
+ app.get('/panels/:panelId/layout', mw, (req, res) => {
251
+ const panelId = req.params.panelId;
252
+ if (!isAccessToPanelAllowed(req, panelId)) {
253
+ res.status(403).end();
254
+ return;
255
+ }
256
+ const panel = panels.lookup(panelId);
257
+ if (panel) {
258
+ const theme = themes.safeTheme(req.query.theme || panel.layout.theme);
259
+ themes.getThemeProperties(theme, (err, themeProperties) => {
260
+ if (err) {
261
+ res.status(500).end();
262
+ }
263
+ else {
264
+ res.render(panel.layout.type === 'document' ? 'parts/document-layout.ejs' : 'parts/grid-layout.ejs', {
265
+ _,
266
+ uiPaths,
267
+ query: req.query,
268
+ basePath: cfg.serverBasePath(),
269
+ theme,
270
+ themeProperties,
271
+ themeOverride: !!req.query.theme,
272
+ panel,
273
+ offline: false,
274
+ });
275
+ }
276
+ });
277
+ }
278
+ else {
279
+ warning(res, {
280
+ status: 404,
281
+ title: 'Panel Not Found',
282
+ message: 'Go to the home page to select an existing panel.',
283
+ });
284
+ }
285
+ });
234
286
  app.get('/panels/:panelId', mw, (req, res) => {
235
287
  const panelId = req.params.panelId;
236
288
  if (!isAccessToPanelAllowed(req, panelId)) {
@@ -417,22 +469,43 @@ exports.setup = function (app, io) {
417
469
  });
418
470
  }
419
471
  });
420
- app.get('/plugins/client-resources/:groupId/:index', mw, (req, res) => {
472
+ app.get('/plugins/scripts/:groupId/:index', mw, (req, res) => {
473
+ const groupId = req.params.groupId;
474
+ const index = _.toNumber(req.params.index);
475
+ const paths = plugins.getScriptPathsOfClientResourceGroup(groupId);
476
+ const resourcePath = _.get(paths, index);
477
+ const maxAgeVal = cfg.get('web.cache.static');
478
+ const maxAge = _.isString(maxAgeVal) ? Math.floor(ms(maxAgeVal) / 1000) : maxAgeVal;
479
+ if (resourcePath) {
480
+ res.sendFile(resourcePath, {
481
+ headers: {
482
+ 'Content-Type': 'application/javascript',
483
+ 'Cache-Control': `public, max-age=${maxAge}`,
484
+ },
485
+ });
486
+ }
487
+ else {
488
+ log.info(`Requested unknown plugin script resource: ${groupId}, ${index}`);
489
+ res.status(404).end();
490
+ }
491
+ });
492
+ app.get('/plugins/styles/:groupId/:index', mw, (req, res) => {
421
493
  const groupId = req.params.groupId;
422
494
  const index = _.toNumber(req.params.index);
423
- const rg = plugins.getClientResourceGroup(groupId);
424
- const ressource = _.get(rg, index);
495
+ const paths = plugins.getStylePathsOfClientResourceGroup(groupId);
496
+ const resourcePath = _.get(paths, index);
425
497
  const maxAgeVal = cfg.get('web.cache.static');
426
498
  const maxAge = _.isString(maxAgeVal) ? Math.floor(ms(maxAgeVal) / 1000) : maxAgeVal;
427
- if (ressource) {
428
- res.sendFile(ressource.path, {
499
+ if (resourcePath) {
500
+ res.sendFile(resourcePath, {
429
501
  headers: {
430
- 'Cache-Control': `public, max-age=${maxAge}`
502
+ 'Content-Type': 'text/css',
503
+ 'Cache-Control': `public, max-age=${maxAge}`,
431
504
  },
432
505
  });
433
506
  }
434
507
  else {
435
- log.info(`Requested unknown client resource: ${groupId}, ${index}`);
508
+ log.info(`Requested unknown plugin style resource: ${groupId}, ${index}`);
436
509
  res.status(404).end();
437
510
  }
438
511
  });
@@ -24,7 +24,7 @@ function getResourceGroupsForPanel(panel, slot) {
24
24
  resourceGroupIds.add(groupId);
25
25
  });
26
26
  });
27
- return [...resourceGroupIds];
27
+ return Array.from(resourceGroupIds);
28
28
  }
29
29
  exports.getResourceGroupsForPanel = getResourceGroupsForPanel;
30
30
  //# sourceMappingURL=client-resources.js.map
@@ -215,6 +215,7 @@ exports.pushDisplayCommand = function (displayCommand, cb) {
215
215
  // if display command replaces current content
216
216
  // update current content
217
217
  displayCommand.no = layoutSlot.history ? currentSequenceNo + 1 : 1;
218
+ displayCommand.version = 0;
218
219
  panel.content.set(slotId, displayCommand);
219
220
  // proceed with history
220
221
  }
@@ -222,10 +223,12 @@ exports.pushDisplayCommand = function (displayCommand, cb) {
222
223
  // if display command extends current content at the end
223
224
  currentContent.title = displayCommand.title;
224
225
  currentContent.content = currentContent.content + CONTENT_SEPARATOR + displayCommand.content;
226
+ currentContent.version = currentContent.version + 1;
225
227
  currentContent.extensions = currentContent.extensions + 1;
226
228
  limitExtensions(currentContent, layoutSlot.extensions, displayCommand.extend);
227
229
  // return display command, but do not store it
228
230
  displayCommand.no = currentSequenceNo;
231
+ displayCommand.version = currentContent.version;
229
232
  cb(null, displayCommand);
230
233
  return;
231
234
  }
@@ -233,10 +236,12 @@ exports.pushDisplayCommand = function (displayCommand, cb) {
233
236
  // if display command extends current content at the beginning
234
237
  currentContent.title = displayCommand.title;
235
238
  currentContent.content = displayCommand.content + CONTENT_SEPARATOR + currentContent.content;
239
+ currentContent.version = currentContent.version + 1;
236
240
  currentContent.extensions = currentContent.extensions + 1;
237
241
  limitExtensions(currentContent, layoutSlot.extensions, displayCommand.extend);
238
242
  // return display command, but do not store it
239
243
  displayCommand.no = currentSequenceNo;
244
+ displayCommand.version = currentContent.version;
240
245
  cb(null, displayCommand);
241
246
  return;
242
247
  }