@genesislcap/pbc-reporting-ui 14.396.4 → 14.397.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.
Files changed (70) hide show
  1. package/dist/dts/new/main/edit-config/datasource-config/datasource-config-item.d.ts +6 -2
  2. package/dist/dts/new/main/edit-config/datasource-config/datasource-config-item.d.ts.map +1 -1
  3. package/dist/dts/new/main/edit-config/datasource-config/datasource-config-item.styles.d.ts.map +1 -1
  4. package/dist/dts/new/main/edit-config/datasource-config/datasource-config-item.template.d.ts.map +1 -1
  5. package/dist/dts/new/main/edit-config/datasource-config/datasources-config-container.helpers.d.ts.map +1 -1
  6. package/dist/dts/new/main/edit-config/datasource-config/datasources-config-container.template.d.ts.map +1 -1
  7. package/dist/dts/new/main/edit-config/shared/datasource-data-base-component.helpers.ts.d.ts.map +1 -1
  8. package/dist/dts/new/store/slices/datasources-config.d.ts +19 -6
  9. package/dist/dts/new/store/slices/datasources-config.d.ts.map +1 -1
  10. package/dist/dts/new/store/slices/types.d.ts +19 -2
  11. package/dist/dts/new/store/slices/types.d.ts.map +1 -1
  12. package/dist/dts/new/store/store.d.ts +126 -35
  13. package/dist/dts/new/store/store.d.ts.map +1 -1
  14. package/dist/dts/new/types/misc.d.ts +4 -2
  15. package/dist/dts/new/types/misc.d.ts.map +1 -1
  16. package/dist/dts/new/utils/alias-generator.d.ts +8 -0
  17. package/dist/dts/new/utils/alias-generator.d.ts.map +1 -0
  18. package/dist/dts/new/utils/alias-generator.test.d.ts +2 -0
  19. package/dist/dts/new/utils/alias-generator.test.d.ts.map +1 -0
  20. package/dist/dts/new/utils/index.d.ts +1 -0
  21. package/dist/dts/new/utils/index.d.ts.map +1 -1
  22. package/dist/dts/new/utils/tooltip.d.ts +3 -0
  23. package/dist/dts/new/utils/tooltip.d.ts.map +1 -1
  24. package/dist/dts/new/utils/transformers.d.ts +1 -1
  25. package/dist/dts/new/utils/transformers.d.ts.map +1 -1
  26. package/dist/esm/new/main/edit-config/col-filters/col-filters-grid.helpers.test.js +39 -7
  27. package/dist/esm/new/main/edit-config/col-rename-alias/col-rename-alias-grid.helpers.test.js +32 -4
  28. package/dist/esm/new/main/edit-config/data-transforms-derived-fields/data-transforms.helpers.test.js +10 -2
  29. package/dist/esm/new/main/edit-config/datasource-config/datasource-config-item.js +100 -10
  30. package/dist/esm/new/main/edit-config/datasource-config/datasource-config-item.styles.js +6 -0
  31. package/dist/esm/new/main/edit-config/datasource-config/datasource-config-item.template.js +38 -3
  32. package/dist/esm/new/main/edit-config/datasource-config/datasources-config-container.helpers.js +11 -7
  33. package/dist/esm/new/main/edit-config/datasource-config/datasources-config-container.helpers.test.js +29 -22
  34. package/dist/esm/new/main/edit-config/datasource-config/datasources-config-container.template.js +3 -0
  35. package/dist/esm/new/main/edit-config/shared/datasource-data-base-component.helpers.ts.js +16 -8
  36. package/dist/esm/new/main/edit-config/shared/datasource-data-base-component.test.js +13 -2
  37. package/dist/esm/new/main/edit-config/tabbed-datasource-container/tabbed-datasource-container.template.js +1 -1
  38. package/dist/esm/new/store/slices/datasources-config.js +41 -5
  39. package/dist/esm/new/store/slices/types.js +1 -0
  40. package/dist/esm/new/utils/alias-generator.js +16 -0
  41. package/dist/esm/new/utils/alias-generator.test.js +36 -0
  42. package/dist/esm/new/utils/index.js +1 -0
  43. package/dist/esm/new/utils/tooltip.js +16 -0
  44. package/dist/esm/new/utils/transformers.js +20 -6
  45. package/dist/esm/new/utils/transformers.test.js +61 -11
  46. package/dist/esm/new/utils/validators.test.js +35 -21
  47. package/dist/tsconfig.tsbuildinfo +1 -1
  48. package/package.json +22 -22
  49. package/src/new/main/edit-config/col-filters/col-filters-grid.helpers.test.ts +39 -7
  50. package/src/new/main/edit-config/col-rename-alias/col-rename-alias-grid.helpers.test.ts +32 -4
  51. package/src/new/main/edit-config/data-transforms-derived-fields/data-transforms.helpers.test.ts +10 -2
  52. package/src/new/main/edit-config/datasource-config/datasource-config-item.styles.ts +6 -0
  53. package/src/new/main/edit-config/datasource-config/datasource-config-item.template.ts +62 -3
  54. package/src/new/main/edit-config/datasource-config/datasource-config-item.ts +107 -8
  55. package/src/new/main/edit-config/datasource-config/datasources-config-container.helpers.test.ts +32 -23
  56. package/src/new/main/edit-config/datasource-config/datasources-config-container.helpers.ts +18 -10
  57. package/src/new/main/edit-config/datasource-config/datasources-config-container.template.ts +6 -0
  58. package/src/new/main/edit-config/shared/datasource-data-base-component.helpers.ts.ts +21 -11
  59. package/src/new/main/edit-config/shared/datasource-data-base-component.test.ts +14 -2
  60. package/src/new/main/edit-config/tabbed-datasource-container/tabbed-datasource-container.template.ts +1 -1
  61. package/src/new/store/slices/datasources-config.ts +58 -7
  62. package/src/new/store/slices/types.ts +22 -4
  63. package/src/new/types/misc.ts +9 -2
  64. package/src/new/utils/alias-generator.test.ts +44 -0
  65. package/src/new/utils/alias-generator.ts +18 -0
  66. package/src/new/utils/index.ts +1 -0
  67. package/src/new/utils/tooltip.ts +19 -0
  68. package/src/new/utils/transformers.test.ts +73 -11
  69. package/src/new/utils/transformers.ts +30 -6
  70. package/src/new/utils/validators.test.ts +35 -21
@@ -3,14 +3,17 @@ import { Combobox } from '@genesislcap/foundation-ui';
3
3
  import { attr, customElement, GenesisElement, observable } from '@genesislcap/web-core';
4
4
  import {
5
5
  actions,
6
+ DatasourceGroupingStrategy,
6
7
  DatasourceInputTypes,
7
8
  DatasourceName,
8
9
  DatasourceOutputTypes,
10
+ selectors,
9
11
  } from '../../../store';
10
12
  import { Display } from '../../../types';
11
13
  import {
12
14
  buildDatasourceName,
13
15
  datasourceInputFromDisplay,
16
+ generateUniqueAlias,
14
17
  getDatasourceSchema,
15
18
  showNotificationToast,
16
19
  } from '../../../utils';
@@ -30,11 +33,23 @@ export class DatasourceConfigItem extends GenesisElement {
30
33
 
31
34
  @observable datasourceName: DatasourceName | null = null;
32
35
  @observable datasourceChoices: DatasourceChoice[] | null = null;
36
+ @observable groupingByFieldsNames: string[] | null = null;
33
37
  @attr({ mode: 'boolean', attribute: 'can-remove' }) canRemove: boolean = false;
34
38
 
35
39
  @attr position: 'below' | 'above' = 'below';
36
40
  @attr error: 'none' | 'invalid-datasource' | 'unknown' = 'none';
37
41
 
42
+ async connectedCallback(): Promise<void> {
43
+ super.connectedCallback();
44
+ try {
45
+ const datasource = selectors.datasourceConfig.getDatasource(this.datasourceName);
46
+ const schema = await this.getSchema(datasource.NAME);
47
+ this.groupingByFieldsNames = Object.keys(schema.properties);
48
+ } catch (e) {
49
+ console.error(e);
50
+ }
51
+ }
52
+
38
53
  deleteDatasourceHandler() {
39
54
  if (!this.canRemove) throw new Error('Trying to remove final datasource!');
40
55
  if (!this.datasourceName) throw new Error('deleteDatasourceHandler - Datasource key unset!');
@@ -43,13 +58,31 @@ export class DatasourceConfigItem extends GenesisElement {
43
58
  });
44
59
  }
45
60
 
46
- updateKeyHandler(newKey: string) {
61
+ updateKeyHandler(newName: string, event?: Event) {
47
62
  if (!this.datasourceName) {
48
63
  throw new Error('updateKeyHandler - datasourceName unset');
49
64
  }
50
- actions.datasourceConfig.updateDatasourceKey({
65
+
66
+ const allDatasources = selectors.datasourceConfig.getAllConfigSet();
67
+ const nameExists = Object.entries(allDatasources)
68
+ .filter(([key]) => key !== this.datasourceName)
69
+ .some(([, ds]) => ds.KEY.toLowerCase() === newName.toLowerCase());
70
+
71
+ if (nameExists) {
72
+ showNotificationToast({
73
+ title: 'Reporting Config',
74
+ body: `Name '${newName}' is already in use by another datasource.`,
75
+ toast: { type: 'error' as any },
76
+ });
77
+ if (event) {
78
+ (event.target as HTMLInputElement).value = allDatasources[this.datasourceName].KEY;
79
+ }
80
+ return;
81
+ }
82
+
83
+ actions.datasourceConfig.renameDatasource({
51
84
  key: this.datasourceName,
52
- newKey,
85
+ newName,
53
86
  });
54
87
  }
55
88
 
@@ -63,6 +96,45 @@ export class DatasourceConfigItem extends GenesisElement {
63
96
  });
64
97
  }
65
98
 
99
+ async updateGroupingStrategyHandler(value: DatasourceGroupingStrategy) {
100
+ try {
101
+ if (value === 'NONE') {
102
+ actions.datasourceConfig.deleteGrouping({ key: this.datasourceName });
103
+ return;
104
+ }
105
+
106
+ const dsResource = selectors.datasourceConfig.getDatasource(this.datasourceName).NAME;
107
+ const schema = await this.getSchema(dsResource);
108
+ if (!schema.properties) throw new Error(`schema properties for ${dsResource} undefined`);
109
+
110
+ const fields = Object.keys(schema.properties);
111
+ actions.datasourceConfig.setGrouping({
112
+ key: this.datasourceName,
113
+ grouping: { GROUPING_STRATEGY: value, GROUP_KEY: fields[0] },
114
+ });
115
+ this.groupingByFieldsNames = Object.keys(schema.properties);
116
+ } catch (e) {
117
+ const errorMsg = e instanceof Error ? `\n\n${e.message}` : '';
118
+ showNotificationToast({
119
+ title: 'Reporting Config',
120
+ body: `Error fetching field data for datasource ${this.datasourceName}.${errorMsg}`,
121
+ toast: { type: 'critical' as any },
122
+ });
123
+ this.error = 'unknown';
124
+ console.error(e);
125
+ }
126
+ }
127
+
128
+ updateGroupingByHandler(value: string) {
129
+ const datasource = selectors.datasourceConfig.getDatasource(this.datasourceName);
130
+ if (datasource.GROUPING_STRATEGY === 'NONE')
131
+ throw new Error('Cannot set group by field when GROUPING_STRATEGY is NONE');
132
+ actions.datasourceConfig.setGrouping({
133
+ key: this.datasourceName,
134
+ grouping: { GROUPING_STRATEGY: datasource.GROUPING_STRATEGY, GROUP_KEY: value },
135
+ });
136
+ }
137
+
66
138
  async updateDatasourceSourceHandler(selectedOption: Combobox) {
67
139
  try {
68
140
  if ((selectedOption?.selectedOptions ?? []).filter((x) => x).length === 0) {
@@ -72,7 +144,7 @@ export class DatasourceConfigItem extends GenesisElement {
72
144
  return;
73
145
  }
74
146
  const option = selectedOption.selectedOptions[0];
75
- const KEY = option.value;
147
+ const RESOURCE_NAME = option.value;
76
148
  const dataAttr = <Display.DatasourceInputTypes | null>(
77
149
  option.getAttribute('data-bubble-content')
78
150
  );
@@ -83,22 +155,49 @@ export class DatasourceConfigItem extends GenesisElement {
83
155
  throw new Error('updateKeyHandler - datasourceName or INPUT_TYPE null');
84
156
  }
85
157
 
86
- const schema = await this.getSchema(KEY);
87
- if (!schema.properties) throw new Error(`schema properties for ${KEY} undefined`);
158
+ const schema = await this.getSchema(RESOURCE_NAME);
159
+ if (!schema.properties) throw new Error(`schema properties for ${RESOURCE_NAME} undefined`);
88
160
 
89
161
  const fields = Object.keys(schema.properties);
90
162
 
163
+ // Check for duplicate aliases
164
+ const allDatasources = selectors.datasourceConfig.getAllConfigSet();
165
+ const existingAliases = new Set(
166
+ Object.entries(allDatasources)
167
+ .filter(([key]) => key !== this.datasourceName)
168
+ .map(([, ds]) => ds.KEY),
169
+ );
170
+
171
+ const uniqueAlias = generateUniqueAlias(RESOURCE_NAME, existingAliases);
172
+
173
+ // Preserve index
174
+ const originalIndex = Object.keys(allDatasources).indexOf(this.datasourceName);
175
+
91
176
  actions.datasourceConfig.deleteDatasource({ key: this.datasourceName });
92
177
  actions.datasourceConfig.initDatasourceConfiguration({
93
178
  base: {
94
- KEY,
179
+ KEY: uniqueAlias,
180
+ NAME: RESOURCE_NAME,
95
181
  INPUT_TYPE: INPUT_TYPE as DatasourceInputTypes,
96
182
  },
97
183
  fields,
98
184
  });
99
- const datasourceName = buildDatasourceName(KEY, INPUT_TYPE as DatasourceInputTypes);
185
+ const datasourceName = buildDatasourceName(
186
+ uniqueAlias,
187
+ RESOURCE_NAME,
188
+ INPUT_TYPE as DatasourceInputTypes,
189
+ );
190
+
191
+ if (originalIndex !== -1) {
192
+ actions.datasourceConfig.reorderDatasource({
193
+ key: datasourceName,
194
+ newIndex: originalIndex,
195
+ });
196
+ }
197
+
100
198
  setDefaultFormatsForDatasource(datasourceName, schema);
101
199
  this.error = 'none';
200
+ this.groupingByFieldsNames = Object.keys(schema.properties);
102
201
  } catch (e: unknown) {
103
202
  const errorMsg = e instanceof Error ? `\n\n${e.message}` : '';
104
203
  showNotificationToast({
@@ -190,15 +190,17 @@ CreateNewDatasourceConfig.before.each(() => {
190
190
  });
191
191
 
192
192
  CreateNewDatasourceConfig('should exit when no valid datasources are available', async () => {
193
- const mockChoices = [{ name: 'test', inputType: 'REQ_REP' as DatasourceInputTypes }];
193
+ const mockChoices = [{ name: 'testSource', inputType: 'REQ_REP' as DatasourceInputTypes }];
194
194
 
195
195
  const getAllConfigSetStub = sinon.stub(selectors.datasourceConfig, 'getAllConfigSet').returns({
196
- REQ_REP_test: {
197
- KEY: 'REQ_REP_test',
198
- NAME: 'Test Datasource',
196
+ REQ_REP_testSource_testSource: {
197
+ KEY: 'testSource',
198
+ NAME: 'testSource',
199
199
  INPUT_TYPE: 'REQ_REP' as const,
200
200
  OUTPUT_TYPE: 'TABLE' as const,
201
201
  TRANSFORMER_CONFIGURATION: { INCLUDE_COLUMNS: [] },
202
+ GROUPING_STRATEGY: 'NONE',
203
+ GROUP_KEY: null,
202
204
  },
203
205
  });
204
206
 
@@ -215,15 +217,17 @@ CreateNewDatasourceConfig('should exit when no valid datasources are available',
215
217
  CreateNewDatasourceConfig(
216
218
  'should exit when trying to add multiple datasources with CSV format',
217
219
  async () => {
218
- const mockChoices = [{ name: 'test', inputType: 'REQ_REP' as DatasourceInputTypes }];
220
+ const mockChoices = [{ name: 'testSource', inputType: 'REQ_REP' as DatasourceInputTypes }];
219
221
 
220
222
  const getAllConfigSetStub = sinon.stub(selectors.datasourceConfig, 'getAllConfigSet').returns({
221
- REQ_REP_existing: {
222
- KEY: 'REQ_REP_existing',
223
- NAME: 'Existing Datasource',
223
+ REQ_REP_existingSource_existingAlias: {
224
+ KEY: 'existingAlias',
225
+ NAME: 'existingSource',
224
226
  INPUT_TYPE: 'REQ_REP' as const,
225
227
  OUTPUT_TYPE: 'TABLE' as const,
226
228
  TRANSFORMER_CONFIGURATION: { INCLUDE_COLUMNS: [] },
229
+ GROUPING_STRATEGY: 'NONE' as const,
230
+ GROUP_KEY: null,
227
231
  },
228
232
  });
229
233
 
@@ -238,7 +242,7 @@ CreateNewDatasourceConfig(
238
242
 
239
243
  await createNewDatasourceConfig(getSchemaStub)(mockChoices);
240
244
 
241
- assert.equal(getAllConfigSetStub.callCount, 2, 'getAllConfigSetStub should be called twice');
245
+ assert.equal(getAllConfigSetStub.callCount, 1, 'getAllConfigSetStub should be called once');
242
246
  assert.equal(getConfigStub.callCount, 1, 'getConfigStub should be called once');
243
247
  // assert.ok(loggerStub.calledWith('Cannot add multiple datasources when using csv file without a template'));
244
248
  if (loggerStub.args.length > 0) {
@@ -256,15 +260,17 @@ CreateNewDatasourceConfig(
256
260
  CreateNewDatasourceConfig(
257
261
  'should allow adding multiple datasources with CSV format IF template is selected',
258
262
  async () => {
259
- const mockChoices = [{ name: 'test', inputType: 'REQ_REP' as DatasourceInputTypes }];
263
+ const mockChoices = [{ name: 'testSource', inputType: 'REQ_REP' as DatasourceInputTypes }];
260
264
 
261
265
  const getAllConfigSetStub = sinon.stub(selectors.datasourceConfig, 'getAllConfigSet').returns({
262
- REQ_REP_existing: {
263
- KEY: 'REQ_REP_existing',
264
- NAME: 'Existing Datasource',
266
+ REQ_REP_existingSource_existingAlias: {
267
+ KEY: 'existingAlias',
268
+ NAME: 'existingSource',
265
269
  INPUT_TYPE: 'REQ_REP' as const,
266
270
  OUTPUT_TYPE: 'TABLE' as const,
267
271
  TRANSFORMER_CONFIGURATION: { INCLUDE_COLUMNS: [] },
272
+ GROUPING_STRATEGY: 'NONE' as const,
273
+ GROUP_KEY: null,
268
274
  },
269
275
  });
270
276
 
@@ -287,14 +293,14 @@ CreateNewDatasourceConfig(
287
293
 
288
294
  await createNewDatasourceConfig(getSchemaStub)(mockChoices);
289
295
 
290
- assert.ok(getAllConfigSetStub.calledTwice);
296
+ assert.ok(getAllConfigSetStub.calledOnce);
291
297
  assert.ok(getSchemaStub.calledOnce);
292
298
  assert.ok(initConfigStub.called);
293
299
  },
294
300
  );
295
301
 
296
302
  CreateNewDatasourceConfig('should successfully create new datasource configuration', async () => {
297
- const mockChoices = [{ name: 'test', inputType: 'REQ_REP' as DatasourceInputTypes }];
303
+ const mockChoices = [{ name: 'testSource', inputType: 'REQ_REP' as DatasourceInputTypes }];
298
304
 
299
305
  const getAllConfigSetStub = sinon.stub(selectors.datasourceConfig, 'getAllConfigSet').returns({});
300
306
 
@@ -316,16 +322,19 @@ CreateNewDatasourceConfig('should successfully create new datasource configurati
316
322
 
317
323
  await createNewDatasourceConfig(getSchemaStub)(mockChoices);
318
324
 
319
- assert.ok(getAllConfigSetStub.calledTwice);
325
+ assert.ok(getAllConfigSetStub.calledOnce);
320
326
  assert.ok(getSchemaStub.calledOnce);
321
327
  assert.ok(
322
- initConfigStub.calledWith({
323
- base: {
324
- KEY: 'test',
325
- INPUT_TYPE: 'REQ_REP',
326
- },
327
- fields: ['field1', 'field2'],
328
- }),
328
+ initConfigStub.calledWith(
329
+ sinon.match({
330
+ base: sinon.match({
331
+ KEY: 'testSource',
332
+ NAME: 'testSource',
333
+ INPUT_TYPE: 'REQ_REP',
334
+ }),
335
+ fields: sinon.match.array.deepEquals(['field1', 'field2']),
336
+ }),
337
+ ),
329
338
  );
330
339
  });
331
340
 
@@ -1,7 +1,7 @@
1
1
  import { Connect, logger } from '@genesislcap/foundation-comms';
2
2
  import { actions, selectors } from '../../../store';
3
3
  import { Genesis } from '../../../types';
4
- import { buildDatasourceName } from '../../../utils';
4
+ import { buildDatasourceName, generateUniqueAlias } from '../../../utils';
5
5
  import { setDefaultFormatsForDatasource } from '../data-transforms-derived-fields/data-transforms.helpers';
6
6
  import { DatasourceChoice } from './types';
7
7
 
@@ -10,21 +10,19 @@ export type GetGenesisSchema = (resource: string) => Promise<Genesis.JSONSchema7
10
10
  export const createNewDatasourceConfig =
11
11
  (getSchema: GetGenesisSchema) =>
12
12
  async (choices: DatasourceChoice[]): Promise<void> => {
13
+ const allDatasources = selectors.datasourceConfig.getAllConfigSet();
13
14
  const validDatasources =
14
15
  choices.filter(
15
- (ds) =>
16
- !(
17
- buildDatasourceName(ds.name, ds.inputType) in
18
- selectors.datasourceConfig.getAllConfigSet()
19
- ),
16
+ (ds) => !(buildDatasourceName(ds.name, ds.name, ds.inputType) in allDatasources),
20
17
  ) ?? [];
18
+
21
19
  if (validDatasources?.length < 1) {
22
20
  logger.error('No datasources available to create new config from');
23
21
  return;
24
22
  }
25
23
  const baseConfig = selectors.baseConfig.getConfig();
26
24
  if (
27
- Object.keys(selectors.datasourceConfig.getAllConfigSet()).length > 0 &&
25
+ Object.keys(allDatasources).length > 0 &&
28
26
  baseConfig.OUTPUT_FORMAT === 'CSV' &&
29
27
  !baseConfig.DOCUMENT_TEMPLATE_ID
30
28
  ) {
@@ -32,13 +30,23 @@ export const createNewDatasourceConfig =
32
30
  return;
33
31
  }
34
32
 
35
- const { KEY, INPUT_TYPE, schema } = await findFirstValidDatasource(getSchema)(validDatasources);
33
+ const {
34
+ KEY: RESOURCE_NAME,
35
+ INPUT_TYPE,
36
+ schema,
37
+ } = await findFirstValidDatasource(getSchema)(validDatasources);
38
+
39
+ // Check for duplicate aliases
40
+ const existingAliases = new Set(Object.values(allDatasources).map((ds) => ds.KEY));
41
+
42
+ const uniqueAlias = generateUniqueAlias(RESOURCE_NAME, existingAliases);
36
43
 
37
- const datasourceName = buildDatasourceName(KEY, INPUT_TYPE);
44
+ const datasourceName = buildDatasourceName(uniqueAlias, RESOURCE_NAME, INPUT_TYPE);
38
45
 
39
46
  actions.datasourceConfig.initDatasourceConfiguration({
40
47
  base: {
41
- KEY,
48
+ KEY: uniqueAlias,
49
+ NAME: RESOURCE_NAME,
42
50
  INPUT_TYPE,
43
51
  },
44
52
  fields: Object.keys(schema.properties!), // Properties is valid as we check in findFirstValidDatasource
@@ -37,6 +37,12 @@ const container = html<DatasourcesConfigContainer>`
37
37
  ${repeat(
38
38
  (_) => Object.keys(selectors.datasourceConfig.getAllConfigSet()),
39
39
  html<string, DatasourcesConfigContainer>`
40
+ ${when(
41
+ (_, ctx) => !ctx.isFirst,
42
+ html`
43
+ <rapid-divider></rapid-divider>
44
+ `,
45
+ )}
40
46
  <datasource-config-item
41
47
  :datasourceName=${(x) => x}
42
48
  :datasourceChoices=${(_, c) => c.parent.datasourceChoices}
@@ -127,9 +127,22 @@ export function datasourceNameFromDisplay(input: string): DatasourceName {
127
127
  .replace(/[)]/g, '')
128
128
  .split('(')
129
129
  .map((x) => x.trim());
130
- return (datasourceInputFromDisplay(parts[1] as Display.DatasourceInputTypes) +
131
- '_' +
132
- parts[0]) as DatasourceName;
130
+ const type = datasourceInputFromDisplay(parts[1] as Display.DatasourceInputTypes);
131
+ const name = parts[0];
132
+
133
+ const match = Object.entries(selectors.datasourceConfig.getAllConfigSet()).find(
134
+ ([_, ds]) => ds.KEY === name && ds.INPUT_TYPE === type,
135
+ );
136
+
137
+ if (!match) {
138
+ /**
139
+ * This can occur when the datasource is first being initialized and the name
140
+ * hasn't yet been persisted to the store.
141
+ */
142
+ return `${type}_${name}_${name}` as DatasourceName;
143
+ }
144
+
145
+ return match[0] as DatasourceName;
133
146
  }
134
147
 
135
148
  /**
@@ -198,14 +211,11 @@ export async function fetchDatasourceSpecs(
198
211
 
199
212
  try {
200
213
  return await Promise.all(
201
- Object.values(selectors.datasourceConfig.getAllConfigSet())
202
- .filter(
203
- ({ NAME, INPUT_TYPE }) =>
204
- INPUT_TYPE === 'REQ_REP' && `${INPUT_TYPE}_${NAME}` === datasourceName,
205
- )
206
- .map(async ({ NAME }) => ({
207
- spec: await getSchema(NAME),
208
- name: datasourceNameForDisplay(NAME, 'REQ_REP'),
214
+ Object.entries(selectors.datasourceConfig.getAllConfigSet())
215
+ .filter(([key, ds]) => ds.INPUT_TYPE === 'REQ_REP' && key === datasourceName)
216
+ .map(async ([_, { KEY, NAME }]) => ({
217
+ spec: await getSchema(NAME), // Fetch schema by Resource Name
218
+ name: datasourceNameForDisplay(KEY, 'REQ_REP'), // Display Alias
209
219
  })),
210
220
  );
211
221
  } catch (e) {
@@ -42,11 +42,15 @@ export type SelectRendererParams = Parameters<SelectRenderer['init']>[typeof FIR
42
42
  // DatasourceNameFromDisplay tests
43
43
  const DatasourceNameFromDisplayTests = suite('datasourceNameFromDisplay()');
44
44
 
45
+ DatasourceNameFromDisplayTests.before.each(() => {
46
+ sinon.restore();
47
+ });
48
+
45
49
  DatasourceNameFromDisplayTests(
46
50
  'Correctly builds datasource key from Snapshot (REQ_REP) input',
47
51
  () => {
48
52
  const input = 'myService (Snapshot)';
49
- const expected = 'REQ_REP_myService';
53
+ const expected = 'REQ_REP_myService_myService';
50
54
  assert.equal(datasourceNameFromDisplay(input), expected);
51
55
  },
52
56
  );
@@ -55,7 +59,7 @@ DatasourceNameFromDisplayTests(
55
59
  'Correctly builds datasource key from Data Pipeline (DATA_PIPELINE) input',
56
60
  () => {
57
61
  const input = 'dataFlow (Data Pipeline)';
58
- const expected = 'DATA_PIPELINE_dataFlow';
62
+ const expected = 'DATA_PIPELINE_dataFlow_dataFlow';
59
63
  assert.equal(datasourceNameFromDisplay(input), expected);
60
64
  },
61
65
  );
@@ -101,6 +105,8 @@ LookupColumnIsIncluded('returns true when column is in included', () => {
101
105
  NAME: 'Test Datasource',
102
106
  INPUT_TYPE: 'REQ_REP' as const,
103
107
  OUTPUT_TYPE: 'TABLE' as const,
108
+ GROUPING_STRATEGY: 'NONE' as const,
109
+ GROUP_KEY: null,
104
110
  TRANSFORMER_CONFIGURATION: { INCLUDE_COLUMNS: ['testColumn', 'otherColumn'] },
105
111
  };
106
112
 
@@ -122,6 +128,8 @@ LookupColumnIsIncluded('returns false when column is not in filters', () => {
122
128
  NAME: 'Test Datasource',
123
129
  INPUT_TYPE: 'REQ_REP' as const,
124
130
  OUTPUT_TYPE: 'TABLE' as const,
131
+ GROUPING_STRATEGY: 'NONE' as const,
132
+ GROUP_KEY: null,
125
133
  TRANSFORMER_CONFIGURATION: { INCLUDE_COLUMNS: ['testColumn', 'otherColumn'] },
126
134
  };
127
135
 
@@ -143,6 +151,8 @@ LookupColumnIsIncluded('returns false when INCLUDE_COLUMNS is undefined', () =>
143
151
  NAME: 'Test Datasource',
144
152
  INPUT_TYPE: 'REQ_REP' as const,
145
153
  OUTPUT_TYPE: 'TABLE' as const,
154
+ GROUPING_STRATEGY: 'NONE' as const,
155
+ GROUP_KEY: null,
146
156
  TRANSFORMER_CONFIGURATION: { INCLUDE_COLUMNS: undefined },
147
157
  };
148
158
 
@@ -164,6 +174,8 @@ LookupColumnIsIncluded('returns false when INCLUDE_COLUMNS is empty array', () =
164
174
  NAME: 'Test Datasource',
165
175
  INPUT_TYPE: 'REQ_REP' as const,
166
176
  OUTPUT_TYPE: 'TABLE' as const,
177
+ GROUPING_STRATEGY: 'NONE' as const,
178
+ GROUP_KEY: null,
167
179
  TRANSFORMER_CONFIGURATION: { INCLUDE_COLUMNS: [] },
168
180
  };
169
181
 
@@ -11,7 +11,7 @@ export const template = html<TabbedDatasourceContainer>`
11
11
  (_) => Object.entries(selectors.datasourceConfig.getAllConfigSet()),
12
12
  html<[DatasourceName, DatasourceConfig[DatasourceName]], TabbedDatasourceContainer>`
13
13
  <rapid-tab @click=${(x, c) => c.parent.handleDatasourceChanged(x[0])}>
14
- ${(x) => x[1].NAME}
14
+ ${(x) => x[1].KEY}
15
15
  </rapid-tab>
16
16
  `,
17
17
  )}
@@ -6,6 +6,7 @@ import { buildDatasourceName } from '../../utils';
6
6
  import { getDefaultFormat } from '../../utils/format-utils';
7
7
  import {
8
8
  DatasourceConfig,
9
+ DatasourceGrouping,
9
10
  DatasourceName,
10
11
  DatasourceOutputTypes,
11
12
  deselectedColumns,
@@ -21,18 +22,20 @@ export const datasourceSlice = createSlice({
21
22
  initDatasourceConfiguration(
22
23
  state: DatasourceConfig,
23
24
  action: PayloadAction<{
24
- base: Pick<DatasourceConfig[DatasourceName], 'KEY' | 'INPUT_TYPE'>;
25
+ base: Pick<DatasourceConfig[DatasourceName], 'KEY' | 'NAME' | 'INPUT_TYPE'>;
25
26
  fields: string[];
26
27
  }>,
27
28
  ) {
28
- const { KEY, INPUT_TYPE } = action.payload.base;
29
- const key: DatasourceName = buildDatasourceName(KEY, INPUT_TYPE);
29
+ const { KEY, NAME, INPUT_TYPE } = action.payload.base;
30
+ const key: DatasourceName = buildDatasourceName(KEY, NAME, INPUT_TYPE);
30
31
  state[key] = {
31
32
  KEY: KEY,
32
33
  INPUT_TYPE: INPUT_TYPE,
33
- NAME: KEY,
34
+ NAME: NAME,
34
35
  OUTPUT_TYPE: 'TABLE',
35
36
  TRANSFORMER_CONFIGURATION: {},
37
+ GROUPING_STRATEGY: 'NONE',
38
+ GROUP_KEY: null,
36
39
  };
37
40
  action.payload.fields.forEach((field) => {
38
41
  datasourceSlice.caseReducers.setColumnRename(state, {
@@ -54,11 +57,43 @@ export const datasourceSlice = createSlice({
54
57
  });
55
58
  });
56
59
  },
57
- updateDatasourceKey(
60
+ reorderDatasource(
58
61
  state: DatasourceConfig,
59
- action: PayloadAction<{ key: DatasourceName; newKey: string }>,
62
+ action: PayloadAction<{ key: DatasourceName; newIndex: number }>,
60
63
  ) {
61
- state[action.payload.key].KEY = action.payload.newKey;
64
+ const { key, newIndex } = action.payload;
65
+ const entries = Object.entries(state);
66
+ const currentIndex = entries.findIndex(([k]) => k === key);
67
+
68
+ if (currentIndex === -1) return state;
69
+
70
+ const [item] = entries.splice(currentIndex, 1);
71
+ entries.splice(newIndex, 0, item);
72
+
73
+ return Object.fromEntries(entries) as DatasourceConfig;
74
+ },
75
+ renameDatasource(
76
+ state: DatasourceConfig,
77
+ action: PayloadAction<{ key: DatasourceName; newName: string }>,
78
+ ) {
79
+ const { key: oldKey, newName } = action.payload;
80
+ const datasource = state[oldKey];
81
+ if (!datasource) {
82
+ return;
83
+ }
84
+ const newKey = buildDatasourceName(newName, datasource.NAME, datasource.INPUT_TYPE);
85
+ if (state[newKey] && newKey !== oldKey) {
86
+ return;
87
+ }
88
+
89
+ return Object.fromEntries(
90
+ Object.entries(state).map(([k, v]) => {
91
+ if (k === oldKey) {
92
+ return [newKey, { ...v, KEY: newName }];
93
+ }
94
+ return [k, v];
95
+ }),
96
+ ) as DatasourceConfig;
62
97
  },
63
98
  updateDatasourceOutputType(
64
99
  state: DatasourceConfig,
@@ -224,6 +259,22 @@ export const datasourceSlice = createSlice({
224
259
  const { [column]: _, ...rest } = state[key].TRANSFORMER_CONFIGURATION?.COLUMN_FORMATS ?? {};
225
260
  state[key].TRANSFORMER_CONFIGURATION.COLUMN_FORMATS = rest;
226
261
  },
262
+ setGrouping(
263
+ state: DatasourceConfig,
264
+ action: PayloadAction<{
265
+ key: DatasourceName;
266
+ grouping: Exclude<DatasourceGrouping, { GROUPING_STRATEGY: 'NONE' }>;
267
+ }>,
268
+ ) {
269
+ const { key, grouping } = action.payload;
270
+ state[key].GROUPING_STRATEGY = grouping.GROUPING_STRATEGY;
271
+ state[key].GROUP_KEY = grouping.GROUP_KEY;
272
+ },
273
+ deleteGrouping(state: DatasourceConfig, action: PayloadAction<{ key: DatasourceName }>) {
274
+ const { key } = action.payload;
275
+ state[key].GROUPING_STRATEGY = 'NONE';
276
+ state[key].GROUP_KEY = null;
277
+ },
227
278
  },
228
279
  selectors: {
229
280
  getAllConfigSet(state: DatasourceConfig) {
@@ -16,9 +16,27 @@ export const datasourceOutputs = [
16
16
  ] as const;
17
17
  export type DatasourceOutputTypes = (typeof datasourceOutputs)[number];
18
18
 
19
- // Key to uniquely identify each data source, must be keyed on the input type as
20
- // the names are not unique between the different data sources
21
- export type DatasourceName = `${DatasourceInputTypes}_${string}`;
19
+ export const datasourceGroupingStrategies = ['NONE', 'MULTI_SHEET', 'LOOKUP_TABLE'] as const;
20
+ export type DatasourceGroupingStrategy = (typeof datasourceGroupingStrategies)[number];
21
+ export type DatasourceGrouping =
22
+ | {
23
+ GROUPING_STRATEGY: Extract<DatasourceGroupingStrategy, 'NONE'>;
24
+ GROUP_KEY: null;
25
+ }
26
+ | {
27
+ GROUPING_STRATEGY: Extract<DatasourceGroupingStrategy, 'MULTI_SHEET' | 'LOOKUP_TABLE'>;
28
+ GROUP_KEY: string;
29
+ };
30
+
31
+ /**
32
+ * Server KEY => This is an alias (this needs to be unique). This is presented as "Name" input on the UI, rapid-text-field
33
+ * Server NAME => The name of the datasource (matches req_rep). This is presented as rapid-select
34
+ * DatasoureName to uniquely identify each data source, must be keyed on the input type as
35
+ * the names are not unique between the different data sources
36
+ */
37
+ export type AliasKey = string;
38
+ export type ServerDatasourceName = string;
39
+ export type DatasourceName = `${DatasourceInputTypes}_${ServerDatasourceName}_${AliasKey}`;
22
40
 
23
41
  export type TransformerConfig = {
24
42
  COLUMN_RENAMES?: { [k: string]: string };
@@ -44,7 +62,7 @@ export type DatasourceConfig = {
44
62
  INPUT_TYPE: DatasourceInputTypes;
45
63
  OUTPUT_TYPE: DatasourceOutputTypes;
46
64
  TRANSFORMER_CONFIGURATION: TransformerConfig;
47
- };
65
+ } & DatasourceGrouping;
48
66
  };
49
67
 
50
68
  // BASE-CONFIG
@@ -1,5 +1,11 @@
1
1
  import type { JSONSchema7 as JSONShemaBase } from 'json-schema';
2
- import type { BaseConfig, DatasourceConfig, DatasourceName, Schedule } from '../store';
2
+ import type {
3
+ BaseConfig,
4
+ DatasourceConfig,
5
+ DatasourceName,
6
+ Schedule,
7
+ DatasourceGrouping,
8
+ } from '../store';
3
9
 
4
10
  export namespace Genesis {
5
11
  export const genesisFieldTypes = [
@@ -28,7 +34,8 @@ export namespace Genesis {
28
34
  };
29
35
 
30
36
  export type ServerReportConfig = Omit<BaseConfig, 'SCHEDULE'> & {
31
- DATA_SOURCES: DatasourceConfig[DatasourceName][];
37
+ DATA_SOURCES: (Omit<DatasourceConfig[DatasourceName], 'GROUPING_STRATEGY' | 'GROUP_KEY'> &
38
+ (Exclude<DatasourceGrouping, { GROUPING_STRATEGY: 'NONE' }> | {}))[];
32
39
  SCHEDULES: Schedule[];
33
40
  };
34
41