@eeacms/volto-tableau 7.0.4 → 7.1.0

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.
@@ -1,30 +1,44 @@
1
- import React, { useEffect } from 'react';
2
- import { Message } from 'semantic-ui-react';
3
- import { flattenToAppURL } from '@plone/volto/helpers';
1
+ import React, { useEffect, useState } from 'react';
2
+ import { withRouter } from 'react-router';
4
3
  import { connect } from 'react-redux';
5
4
  import { compose } from 'redux';
5
+ import { isFunction } from 'lodash';
6
+ import { Message } from 'semantic-ui-react';
7
+ import { flattenToAppURL } from '@plone/volto/helpers';
6
8
  import { getContent } from '@plone/volto/actions';
7
9
  import PrivacyProtection from '@eeacms/volto-embed/PrivacyProtection/PrivacyProtection';
8
- import { pickMetadata } from '@eeacms/volto-embed/helpers';
9
10
  import Tableau from '@eeacms/volto-tableau/Tableau/Tableau';
11
+ import {
12
+ getQuery,
13
+ getTableauVisualization,
14
+ getParameters,
15
+ getFilters,
16
+ } from '@eeacms/volto-tableau/Tableau/helpers';
17
+
18
+ const timer = {};
10
19
 
11
- function getTableauVisualization(props) {
12
- const { isBlock } = props;
13
- const content = (isBlock ? props.tableauContent : props.content) || {};
14
- const tableau_visualization =
15
- (isBlock
16
- ? props.tableauContent?.tableau_visualization
17
- : props.content?.tableau_visualization) ||
18
- props.data.tableau_visualization ||
19
- {};
20
- return {
21
- ...pickMetadata(content),
22
- ...tableau_visualization,
23
- };
20
+ function debounce(func, wait = 500, id) {
21
+ if (!isFunction(func)) return;
22
+ const name = id || func.name || 'generic';
23
+ if (timer[name]) clearTimeout(timer[name]);
24
+ timer[name] = setTimeout(func, wait);
24
25
  }
25
26
 
26
27
  const View = (props) => {
27
- const { isBlock, id, mode, data, getContent, useVisibilitySensor } = props;
28
+ const {
29
+ isBlock,
30
+ id,
31
+ location,
32
+ mode,
33
+ data,
34
+ getContent,
35
+ useVisibilitySensor,
36
+ data_query,
37
+ discodata_query,
38
+ tableau_vis_url,
39
+ content,
40
+ tableauContent,
41
+ } = props;
28
42
  const {
29
43
  with_notes = true,
30
44
  with_sources = true,
@@ -35,21 +49,96 @@ const View = (props) => {
35
49
  tableau_height,
36
50
  } = data;
37
51
 
38
- const tableau_vis_url = flattenToAppURL(data.tableau_vis_url || '');
52
+ const [tableauVisualization, setTableauVisualization] = useState(() =>
53
+ getTableauVisualization({
54
+ isBlock,
55
+ data,
56
+ content,
57
+ tableauContent,
58
+ }),
59
+ );
60
+ const [query, setQuery] = useState(() => {
61
+ return getQuery({ data_query, location, tableau_vis_url, discodata_query });
62
+ });
63
+
64
+ const [extraParameters, setExtraParameters] = useState(() =>
65
+ getParameters({ tableauVisualization, query, data }),
66
+ );
67
+ const [extraFilters, setExtraFilters] = useState(() =>
68
+ getFilters({ tableauVisualization, query, data }),
69
+ );
70
+
71
+ /**
72
+ * Update tableau visualization
73
+ */
74
+ useEffect(() => {
75
+ setTableauVisualization(
76
+ getTableauVisualization({
77
+ isBlock,
78
+ data,
79
+ content,
80
+ tableauContent,
81
+ }),
82
+ );
83
+ }, [isBlock, data, content, tableauContent]);
84
+
85
+ /**
86
+ * Update query
87
+ */
88
+ useEffect(() => {
89
+ setQuery(
90
+ getQuery({ data_query, location, tableau_vis_url, discodata_query }),
91
+ );
92
+ }, [tableau_vis_url, data_query, discodata_query, location]);
93
+
94
+ /**
95
+ * Update extra parameters
96
+ */
97
+ useEffect(() => {
98
+ debounce(
99
+ () => {
100
+ setExtraParameters(
101
+ getParameters({
102
+ tableauVisualization,
103
+ query,
104
+ data,
105
+ }),
106
+ );
107
+ },
108
+ 500,
109
+ 'setExtraParameters',
110
+ );
111
+ }, [tableauVisualization, query, data]);
39
112
 
40
- const tableau_visualization = getTableauVisualization(props);
113
+ /**
114
+ * Update extra filters
115
+ */
116
+ useEffect(() => {
117
+ debounce(
118
+ () => {
119
+ setExtraFilters(
120
+ getFilters({
121
+ tableauVisualization,
122
+ query,
123
+ data,
124
+ }),
125
+ );
126
+ },
127
+ 500,
128
+ 'setExtraFilters',
129
+ );
130
+ }, [tableauVisualization, query, data]);
41
131
 
42
132
  useEffect(() => {
43
- const tableauVisId = flattenToAppURL(tableau_visualization['@id'] || '');
44
- if (
45
- isBlock &&
46
- mode === 'edit' &&
47
- tableau_vis_url &&
48
- tableau_vis_url !== tableauVisId
49
- ) {
133
+ /**
134
+ * If we are in edit mode, we need to fetch the content of the
135
+ * tableau visualization
136
+ */
137
+ const tableauVisId = flattenToAppURL(tableauVisualization['@id'] || '');
138
+ if (isBlock && tableau_vis_url && tableau_vis_url !== tableauVisId) {
50
139
  getContent(tableau_vis_url, null, id);
51
140
  }
52
- }, [id, isBlock, getContent, mode, tableau_vis_url, tableau_visualization]);
141
+ }, [id, isBlock, getContent, mode, tableau_vis_url, tableauVisualization]);
53
142
 
54
143
  if (props.mode === 'edit' && !tableau_vis_url) {
55
144
  return <Message>Please select a tableau from block editor.</Message>;
@@ -59,14 +148,14 @@ const View = (props) => {
59
148
  <div className="embed-tableau">
60
149
  <PrivacyProtection
61
150
  {...props}
62
- data={{ ...data, url: tableau_visualization?.url }}
151
+ data={{ ...data, url: tableauVisualization?.url }}
63
152
  useVisibilitySensor={useVisibilitySensor}
64
153
  >
65
154
  <Tableau
66
155
  data={{
67
- ...tableau_visualization,
156
+ ...tableauVisualization,
68
157
  tableau_height:
69
- tableau_height || tableau_visualization.tableau_height,
158
+ tableau_height || tableauVisualization.tableau_height,
70
159
  with_notes,
71
160
  with_sources,
72
161
  with_more_info,
@@ -75,6 +164,8 @@ const View = (props) => {
75
164
  with_enlarge,
76
165
  tableau_vis_url,
77
166
  }}
167
+ extraParameters={extraParameters}
168
+ extraFilters={extraFilters}
78
169
  />
79
170
  </PrivacyProtection>
80
171
  </div>
@@ -82,11 +173,19 @@ const View = (props) => {
82
173
  };
83
174
 
84
175
  export default compose(
176
+ withRouter,
85
177
  connect(
86
- (state, props) => ({
87
- tableauContent: state.content.subrequests?.[props.id]?.data,
88
- isBlock: !!props.data?.['@type'],
89
- }),
178
+ (state, props) => {
179
+ const tableau_vis_url = flattenToAppURL(props.data.tableau_vis_url || '');
180
+ const pathname = flattenToAppURL(state.content.data['@id']);
181
+ return {
182
+ tableauContent: state.content?.subrequests?.[props.id]?.data,
183
+ discodata_query: state.discodata_query,
184
+ data_query: state.connected_data_parameters.byContextPath[pathname],
185
+ isBlock: !!props.data?.['@type'],
186
+ tableau_vis_url,
187
+ };
188
+ },
90
189
  {
91
190
  getContent,
92
191
  },
@@ -50,12 +50,31 @@ Array [
50
50
  className="ui segment form attached"
51
51
  >
52
52
  <div
53
- className="mocked-url-widget"
53
+ className="mocked-default-widget"
54
54
  id="mocked-field-tableau_vis_url"
55
55
  >
56
56
  Tableau visualization
57
57
  -
58
- No description
58
+ <div>
59
+ <p>
60
+ When using context query parameters please use the corresponding field name from the Tableau service.
61
+ </p>
62
+ <p>
63
+ NOTE: The embeded tableau dashboard must have the parameters defined in the
64
+
65
+ <span
66
+ style={
67
+ Object {
68
+ "color": "rgb(15, 130, 204)",
69
+ }
70
+ }
71
+ >
72
+ 'Dynamic parameters'
73
+ </span>
74
+
75
+ list so that the context query parameters can take effect.
76
+ </p>
77
+ </div>
59
78
  </div>
60
79
  <div
61
80
  className="mocked-default-widget"
@@ -181,6 +200,75 @@ Array [
181
200
  </div>
182
201
  </div>
183
202
  </div>
203
+ <div
204
+ className="accordion ui fluid styled form"
205
+ >
206
+ <div
207
+ id="blockform-fieldset-options"
208
+ >
209
+ <div
210
+ className="title"
211
+ onClick={[Function]}
212
+ >
213
+ Parameters
214
+ <svg
215
+ className="icon"
216
+ dangerouslySetInnerHTML={
217
+ Object {
218
+ "__html": undefined,
219
+ }
220
+ }
221
+ onClick={null}
222
+ style={
223
+ Object {
224
+ "fill": "currentColor",
225
+ "height": "20px",
226
+ "width": "auto",
227
+ }
228
+ }
229
+ viewBox=""
230
+ xmlns=""
231
+ />
232
+ </div>
233
+ <div
234
+ className="content"
235
+ >
236
+ <div
237
+ aria-hidden={true}
238
+ className="rah-static rah-static--height-zero"
239
+ style={
240
+ Object {
241
+ "height": 0,
242
+ "overflow": "hidden",
243
+ }
244
+ }
245
+ >
246
+ <div
247
+ style={
248
+ Object {
249
+ "WebkitTransition": "opacity 500ms ease 0ms",
250
+ "opacity": 0,
251
+ "transition": "opacity 500ms ease 0ms",
252
+ }
253
+ }
254
+ >
255
+ <div
256
+ className="ui segment attached"
257
+ >
258
+ <div
259
+ className="mocked-default-widget"
260
+ id="mocked-field-staticParameters"
261
+ >
262
+ Static parameters
263
+ -
264
+ Set a list of static parameters.
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
184
272
  <div
185
273
  className="accordion ui fluid styled form"
186
274
  >
@@ -53,6 +53,22 @@ const getProtectionSchema = () => ({
53
53
  required: [],
54
54
  });
55
55
 
56
+ const staticParameters = {
57
+ title: 'Parameter',
58
+ fieldsets: [{ id: 'default', title: 'Default', fields: ['field', 'value'] }],
59
+ properties: {
60
+ field: {
61
+ title: 'Tableau fieldname',
62
+ type: 'text',
63
+ },
64
+ value: {
65
+ title: 'Value',
66
+ type: 'text',
67
+ },
68
+ },
69
+ required: [],
70
+ };
71
+
56
72
  export default (props) => {
57
73
  return {
58
74
  title: 'Embed Dashboard (Tableau)',
@@ -74,6 +90,11 @@ export default (props) => {
74
90
  'with_enlarge',
75
91
  ],
76
92
  },
93
+ {
94
+ id: 'options',
95
+ title: 'Parameters',
96
+ fields: ['staticParameters'],
97
+ },
77
98
  {
78
99
  id: 'privacy',
79
100
  title: 'Privacy',
@@ -83,7 +104,23 @@ export default (props) => {
83
104
  properties: {
84
105
  tableau_vis_url: {
85
106
  title: 'Tableau visualization',
86
- widget: 'url',
107
+ widget: 'internal_url',
108
+ description: (
109
+ <div>
110
+ <p>
111
+ When using context query parameters please use the corresponding
112
+ field name from the Tableau service.
113
+ </p>
114
+ <p>
115
+ NOTE: The embeded tableau dashboard must have the parameters
116
+ defined in the{' '}
117
+ <span style={{ color: 'rgb(15, 130, 204)' }}>
118
+ 'Dynamic parameters'
119
+ </span>{' '}
120
+ list so that the context query parameters can take effect.
121
+ </p>
122
+ </div>
123
+ ),
87
124
  },
88
125
  with_notes: {
89
126
  title: 'Show note',
@@ -134,6 +171,12 @@ export default (props) => {
134
171
  widget: 'object',
135
172
  schema: getProtectionSchema(),
136
173
  },
174
+ staticParameters: {
175
+ title: 'Static parameters',
176
+ widget: 'object_list',
177
+ schema: staticParameters,
178
+ description: 'Set a list of static parameters.',
179
+ },
137
180
  },
138
181
 
139
182
  required: ['tableau_vis_url'],
@@ -9,7 +9,22 @@ import React, {
9
9
  } from 'react';
10
10
  import { connect } from 'react-redux';
11
11
  import { toast } from 'react-toastify';
12
- import { isEqual, isUndefined, isNaN, isNumber } from 'lodash';
12
+ import {
13
+ isEqual,
14
+ isUndefined,
15
+ isNaN,
16
+ isNumber,
17
+ forOwn,
18
+ find,
19
+ includes,
20
+ isArray,
21
+ isString,
22
+ isInteger,
23
+ isBoolean,
24
+ toString,
25
+ toInteger,
26
+ toNumber,
27
+ } from 'lodash';
13
28
  import cx from 'classnames';
14
29
  import { Button } from 'semantic-ui-react';
15
30
  import { Toast, Icon } from '@plone/volto/components';
@@ -48,7 +63,9 @@ const TableauDebug = ({ mode, data, vizState, url, version, clearData }) => {
48
63
  return (
49
64
  <div className="tableau-debug">
50
65
  {!url && !vizState.error && <p className="tableau-error">URL required</p>}
51
- {vizState.error && <p className="tableau-error">{vizState.error}</p>}
66
+ {isString(vizState.error) && (
67
+ <p className="tableau-error">{vizState.error}</p>
68
+ )}
52
69
  {vizState.loaded && url && (
53
70
  <h3 className="tableau-version">
54
71
  Tableau <span className="version">{version}</span>
@@ -93,6 +110,7 @@ const Tableau = forwardRef((props, ref) => {
93
110
  data = {},
94
111
  breakpoints = {},
95
112
  extraFilters = {},
113
+ extraParameters = {},
96
114
  extraOptions = {},
97
115
  mode = 'view',
98
116
  screen = {},
@@ -244,10 +262,10 @@ const Tableau = forwardRef((props, ref) => {
244
262
  hideToolbar,
245
263
  toolbarPosition,
246
264
  device: !!breakpointUrl ? device : 'desktop',
265
+ ...extraOptions,
247
266
  ...data.filters,
248
- ...data.parameters,
249
267
  ...extraFilters,
250
- ...extraOptions,
268
+ ...extraParameters,
251
269
  onFirstInteractive: () => {
252
270
  onVizStateUpdate(true, true, null);
253
271
  setInitiateViz(false);
@@ -296,32 +314,11 @@ const Tableau = forwardRef((props, ref) => {
296
314
  },
297
315
  });
298
316
  } catch (e) {
299
- onVizStateUpdate(false, false, e.get_message());
317
+ onVizStateUpdate(false, false, e?.get_message?.() || e);
300
318
  setInitiateViz(false);
301
319
  }
302
320
  };
303
321
 
304
- const addExtraFilters = (extraFilters) => {
305
- const worksheets =
306
- viz.current.getWorkbook().getActiveSheet().getWorksheets() || [];
307
-
308
- worksheets.forEach((worksheet) => {
309
- if (worksheet.getSheetType() === tableau.DashboardObjectType.WORKSHEET) {
310
- Object.keys(extraFilters).forEach((filter) => {
311
- if (!extraFilters[filter]) {
312
- worksheet.clearFilterAsync(filter);
313
- } else {
314
- worksheet.applyFilterAsync(
315
- filter,
316
- extraFilters[filter],
317
- tableau.FilterUpdateType.REPLACE,
318
- );
319
- }
320
- });
321
- }
322
- });
323
- };
324
-
325
322
  const updateScale = () => {
326
323
  const iframe = vizEl.current.querySelector('iframe');
327
324
  const { sheetSize = {} } = viz.current.getVizSize() || {};
@@ -358,12 +355,99 @@ const Tableau = forwardRef((props, ref) => {
358
355
  /* eslint-disable-next-line */
359
356
  }, [loaded, loading, initiateViz]);
360
357
 
358
+ /**
359
+ * TODO: make this work
360
+ */
361
+ // useEffect(() => {
362
+ // async function addExtraFilters() {
363
+ // if (vizState.current.loaded && viz.current) {
364
+ // const dashboard = viz.current.getWorkbook().getActiveSheet();
365
+ // const tableauFilters = await dashboard.getFiltersAsync();
366
+
367
+ // forOwn(extraFilters, (value, fieldName) => {
368
+ // const tableauFilter = find(
369
+ // tableauFilters,
370
+ // (f) => f.getFieldName() === fieldName,
371
+ // );
372
+ // if (!tableauFilter) return;
373
+ // if (!value) {
374
+ // tableauFilter.getWorksheet().clearFilterAsync(fieldName);
375
+ // return;
376
+ // }
377
+ // const filterType = tableauFilter.getFilterType();
378
+ // if (filterType === 'categorical') {
379
+ // if (!isArray(value)) {
380
+ // value = [value];
381
+ // }
382
+ // dashboard.applyFilterAsync(
383
+ // fieldName,
384
+ // value,
385
+ // tableau.FilterUpdateType.REPLACE,
386
+ // );
387
+ // }
388
+ // /**
389
+ // * TODO: handle other filter types
390
+ // */
391
+ // });
392
+ // }
393
+ // }
394
+ // addExtraFilters();
395
+ // /* eslint-disable-next-line */
396
+ // }, [loaded, JSON.stringify(extraFilters)]);
397
+
361
398
  useEffect(() => {
362
- if (vizState.current.loaded && viz.current) {
363
- addExtraFilters(extraFilters);
399
+ async function addExtraParameters() {
400
+ if (vizState.current.loaded && viz.current) {
401
+ const workbook = viz.current.getWorkbook();
402
+ const tableauParameters = await workbook.getParametersAsync();
403
+ forOwn(extraParameters, (value, fieldName) => {
404
+ const tableauParameter = find(
405
+ tableauParameters,
406
+ (p) => p.getName() === fieldName,
407
+ );
408
+ if (!tableauParameter || !value) return;
409
+ const allowableValuesType = tableauParameter.getAllowableValuesType();
410
+ const dataType = tableauParameter.getDataType();
411
+ if (includes(['all', 'list'], allowableValuesType)) {
412
+ const values = tableauParameter
413
+ .getAllowableValues()
414
+ ?.map((v) => v.value);
415
+ if (!isArray(value)) {
416
+ value = [value];
417
+ }
418
+ value = value
419
+ .filter((v) => includes(values, v))
420
+ .map((v) => {
421
+ if (dataType === 'string' && !isString(v)) {
422
+ return toString(v);
423
+ }
424
+ if (dataType === 'integer' && !isInteger(v)) {
425
+ return toInteger(v);
426
+ }
427
+ if (dataType === 'float' && !isNumber(v)) {
428
+ return toNumber(v);
429
+ }
430
+ if (dataType === 'boolean' && !isBoolean(v)) {
431
+ return !!v;
432
+ }
433
+ return v;
434
+ });
435
+ if (value.length) {
436
+ workbook.changeParameterValueAsync(fieldName, value);
437
+ }
438
+ }
439
+ if (allowableValuesType === 'range' && value) {
440
+ /**
441
+ * TODO: handle range parameters
442
+ */
443
+ workbook.changeParameterValueAsync(fieldName, value);
444
+ }
445
+ });
446
+ }
364
447
  }
448
+ addExtraParameters();
365
449
  /* eslint-disable-next-line */
366
- }, [JSON.stringify(extraFilters)]);
450
+ }, [loaded, JSON.stringify(extraParameters)]);
367
451
 
368
452
  useEffect(() => {
369
453
  if (vizState.current.loaded && viz.current && autoScale) {