datajunction-ui 0.0.1-a1 → 0.0.1-a101

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 (110) hide show
  1. package/Makefile +7 -1
  2. package/package.json +18 -7
  3. package/public/index.html +1 -1
  4. package/src/app/components/AddNodeDropdown.jsx +44 -0
  5. package/src/app/components/ListGroupItem.jsx +2 -1
  6. package/src/app/components/NodeListActions.jsx +69 -0
  7. package/src/app/components/NodeMaterializationDelete.jsx +80 -0
  8. package/src/app/components/QueryInfo.jsx +96 -1
  9. package/src/app/components/Search.jsx +94 -0
  10. package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
  11. package/src/app/components/__tests__/Search.test.jsx +63 -0
  12. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +5 -3
  13. package/src/app/components/djgraph/Collapse.jsx +3 -2
  14. package/src/app/components/djgraph/DJNode.jsx +1 -1
  15. package/src/app/components/djgraph/DJNodeColumns.jsx +5 -1
  16. package/src/app/components/djgraph/LayoutFlow.jsx +5 -3
  17. package/src/app/components/forms/Action.jsx +8 -0
  18. package/src/app/components/forms/NodeNameField.jsx +64 -0
  19. package/src/app/components/search.css +17 -0
  20. package/src/app/icons/AddItemIcon.jsx +16 -0
  21. package/src/app/icons/CommitIcon.jsx +45 -0
  22. package/src/app/icons/DiffIcon.jsx +63 -0
  23. package/src/app/icons/EyeIcon.jsx +20 -0
  24. package/src/app/icons/FilterIcon.jsx +7 -0
  25. package/src/app/icons/JupyterExportIcon.jsx +25 -0
  26. package/src/app/icons/LoadingIcon.jsx +10 -10
  27. package/src/app/icons/PythonIcon.jsx +6 -44
  28. package/src/app/index.tsx +24 -0
  29. package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
  30. package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
  31. package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
  32. package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
  33. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +5 -0
  34. package/src/app/pages/AddEditNodePage/FullNameField.jsx +3 -2
  35. package/src/app/pages/AddEditNodePage/Loadable.jsx +6 -2
  36. package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
  37. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
  38. package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
  39. package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
  40. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +8 -3
  41. package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
  42. package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
  43. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
  44. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +15 -9
  45. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +167 -24
  46. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +55 -25
  47. package/src/app/pages/AddEditNodePage/index.jsx +275 -194
  48. package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +154 -0
  49. package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
  50. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +77 -0
  51. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +405 -0
  52. package/src/app/pages/CubeBuilderPage/index.jsx +267 -0
  53. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +5 -5
  54. package/src/app/pages/NamespacePage/Explorer.jsx +6 -2
  55. package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
  56. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
  57. package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
  58. package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
  59. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +98 -19
  60. package/src/app/pages/NamespacePage/index.jsx +272 -89
  61. package/src/app/pages/NodePage/AddBackfillPopover.jsx +60 -61
  62. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +104 -51
  63. package/src/app/pages/NodePage/ClientCodePopover.jsx +73 -25
  64. package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
  65. package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
  66. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +38 -23
  67. package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
  68. package/src/app/pages/NodePage/NodeColumnTab.jsx +183 -113
  69. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
  70. package/src/app/pages/NodePage/NodeGraphTab.jsx +56 -29
  71. package/src/app/pages/NodePage/NodeHistory.jsx +165 -161
  72. package/src/app/pages/NodePage/NodeInfoTab.jsx +148 -14
  73. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +201 -104
  74. package/src/app/pages/NodePage/NodeStatus.jsx +96 -21
  75. package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
  76. package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
  77. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +3 -5
  78. package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
  79. package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
  80. package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
  81. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +13 -4
  82. package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
  83. package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
  84. package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
  85. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +10 -14
  86. package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
  87. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
  88. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +6 -2
  89. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +3 -2
  90. package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +148 -0
  91. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +159 -57
  92. package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
  93. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +2 -386
  94. package/src/app/pages/NodePage/index.jsx +94 -57
  95. package/src/app/pages/Root/__tests__/index.test.jsx +3 -1
  96. package/src/app/pages/Root/index.tsx +62 -12
  97. package/src/app/services/DJService.js +587 -55
  98. package/src/app/services/__tests__/DJService.test.jsx +382 -45
  99. package/src/index.tsx +1 -0
  100. package/src/mocks/mockNodes.jsx +265 -227
  101. package/src/styles/dag.css +4 -2
  102. package/src/styles/index.css +474 -10
  103. package/src/styles/loading.css +1 -1
  104. package/src/styles/node-creation.scss +84 -5
  105. package/src/styles/node-list.css +4 -0
  106. package/src/styles/sorted-table.css +15 -0
  107. package/src/app/components/DeleteNode.jsx +0 -55
  108. package/src/app/components/__tests__/DeleteNode.test.jsx +0 -53
  109. package/src/app/pages/NodePage/NodeSQLTab.jsx +0 -82
  110. package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +0 -49
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Materialization configuration field.
3
+ */
4
+ import React from 'react';
5
+ import { ErrorMessage, Field, useFormikContext } from 'formik';
6
+ import CodeMirror from '@uiw/react-codemirror';
7
+ import { langs } from '@uiw/codemirror-extensions-langs';
8
+
9
+ export const ConfigField = ({ djClient, value }) => {
10
+ const formik = useFormikContext();
11
+ const jsonExt = langs.json();
12
+
13
+ const updateFormik = val => {
14
+ formik.setFieldValue('spark_config', JSON.parse(val));
15
+ };
16
+
17
+ return (
18
+ <div className="DescriptionInput">
19
+ <details>
20
+ <summary>
21
+ <label htmlFor="SparkConfig" style={{ display: 'inline-block' }}>
22
+ Spark Config
23
+ </label>
24
+ </summary>
25
+ <ErrorMessage name="spark_config" component="span" />
26
+ <Field
27
+ type="textarea"
28
+ style={{ display: 'none' }}
29
+ as="textarea"
30
+ name="spark_config"
31
+ id="SparkConfig"
32
+ />
33
+ <div role="button" tabIndex={0} className="relative flex bg-[#282a36]">
34
+ <CodeMirror
35
+ id={'spark_config'}
36
+ name={'spark_config'}
37
+ extensions={[jsonExt]}
38
+ value={JSON.stringify(value, null, ' ')}
39
+ options={{
40
+ theme: 'default',
41
+ lineNumbers: true,
42
+ }}
43
+ width="100%"
44
+ height="170px"
45
+ style={{
46
+ margin: '0 0 23px 0',
47
+ flex: 1,
48
+ fontSize: '150%',
49
+ textAlign: 'left',
50
+ }}
51
+ onChange={(value, viewUpdate) => {
52
+ updateFormik(value);
53
+ }}
54
+ />
55
+ </div>
56
+ </details>{' '}
57
+ <></>
58
+ </div>
59
+ );
60
+ };
@@ -1,18 +1,25 @@
1
1
  import { useEffect, useState } from 'react';
2
- import ClientCodePopover from './ClientCodePopover';
3
2
  import * as React from 'react';
4
3
  import EditColumnPopover from './EditColumnPopover';
4
+ import EditColumnDescriptionPopover from './EditColumnDescriptionPopover';
5
5
  import LinkDimensionPopover from './LinkDimensionPopover';
6
6
  import { labelize } from '../../../utils/form';
7
7
  import PartitionColumnPopover from './PartitionColumnPopover';
8
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
9
+ import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
10
+ import { link } from 'fs';
8
11
 
9
12
  export default function NodeColumnTab({ node, djClient }) {
10
13
  const [attributes, setAttributes] = useState([]);
11
14
  const [dimensions, setDimensions] = useState([]);
12
15
  const [columns, setColumns] = useState([]);
16
+ const [links, setLinks] = useState([]);
17
+
13
18
  useEffect(() => {
14
19
  const fetchData = async () => {
15
- setColumns(await djClient.columns(node));
20
+ if (node) {
21
+ setColumns(await djClient.columns(node));
22
+ }
16
23
  };
17
24
  fetchData().catch(console.error);
18
25
  }, [djClient, node]);
@@ -31,8 +38,11 @@ export default function NodeColumnTab({ node, djClient }) {
31
38
  useEffect(() => {
32
39
  const fetchData = async () => {
33
40
  const dimensions = await djClient.dimensions();
34
- const options = dimensions.map(name => {
35
- return { value: name, label: name };
41
+ const options = dimensions.map(dim => {
42
+ return {
43
+ value: dim.name,
44
+ label: `${dim.name} (${dim.indegree} links)`,
45
+ };
36
46
  });
37
47
  setDimensions(options);
38
48
  };
@@ -54,28 +64,16 @@ export default function NodeColumnTab({ node, djClient }) {
54
64
  if (col.partition) {
55
65
  return (
56
66
  <>
57
- <span
58
- className="node_type badge node_type__blank"
59
- key={`col-attr-partition-type`}
60
- >
61
- <span
62
- className="partition_value badge"
63
- key={`col-attr-partition-type`}
64
- >
67
+ <span className="node_type badge node_type__blank">
68
+ <span className="partition_value badge">
65
69
  <b>Type:</b> {col.partition.type_}
66
70
  </span>
67
71
  <br />
68
- <span
69
- className="partition_value badge"
70
- key={`col-attr-partition-type`}
71
- >
72
+ <span className="partition_value badge">
72
73
  <b>Format:</b> <code>{col.partition.format}</code>
73
74
  </span>
74
75
  <br />
75
- <span
76
- className="partition_value badge"
77
- key={`col-attr-partition-type`}
78
- >
76
+ <span className="partition_value badge">
79
77
  <b>Granularity:</b> <code>{col.partition.granularity}</code>
80
78
  </span>
81
79
  </span>
@@ -86,115 +84,187 @@ export default function NodeColumnTab({ node, djClient }) {
86
84
  };
87
85
 
88
86
  const columnList = columns => {
89
- return columns.map(col => (
90
- <tr key={col.name}>
91
- <td
92
- className="text-start"
93
- role="columnheader"
94
- aria-label="ColumnName"
95
- aria-hidden="false"
96
- >
97
- {col.name}
98
- </td>
99
- <td>
100
- <span
101
- className=""
102
- role="columnheader"
103
- aria-label="ColumnDisplayName"
104
- aria-hidden="false"
105
- >
106
- {col.display_name}
107
- </span>
108
- </td>
109
- <td>
110
- <span
111
- className={`node_type__${
112
- node.type === 'cube' ? col.type : 'transform'
113
- } badge node_type`}
87
+ return columns?.map(col => {
88
+ const dimensionLinks = (links.length > 0 ? links : node?.dimension_links)
89
+ .map(link => [
90
+ link.dimension.name,
91
+ Object.entries(link.foreign_keys).filter(
92
+ entry => entry[0] === node.name + '.' + col.name,
93
+ ),
94
+ ])
95
+ .filter(keys => keys[1].length >= 1);
96
+ const referencedDimensionNode =
97
+ dimensionLinks.length > 0 ? dimensionLinks[0][0] : null;
98
+ return (
99
+ <tr key={col.name}>
100
+ <td
101
+ className="text-start"
114
102
  role="columnheader"
115
- aria-label="ColumnType"
103
+ aria-label="ColumnName"
116
104
  aria-hidden="false"
117
105
  >
118
- {col.type}
119
- </span>
120
- </td>
121
- {node.type !== 'cube' ? (
106
+ {col.name}
107
+ </td>
122
108
  <td>
123
- {col.dimension !== undefined && col.dimension !== null ? (
124
- <>
125
- <a href={`/nodes/${col.dimension.name}`}>
126
- {col.dimension.name}
127
- </a>
128
- <ClientCodePopover code={col.clientCode} />
129
- </>
130
- ) : (
131
- ''
132
- )}{' '}
133
- <LinkDimensionPopover
134
- column={col}
135
- node={node}
136
- options={dimensions}
137
- onSubmit={async () => {
138
- const res = await djClient.node(node.name);
139
- setColumns(res.columns);
140
- }}
141
- />
109
+ <span
110
+ className=""
111
+ role="columnheader"
112
+ aria-label="ColumnDisplayName"
113
+ aria-hidden="false"
114
+ >
115
+ {col.display_name}
116
+ </span>
117
+ </td>
118
+ <td>
119
+ <span
120
+ className=""
121
+ role="columnheader"
122
+ aria-label="ColumnDescription"
123
+ aria-hidden="false"
124
+ >
125
+ {col.description || ''}
126
+ <EditColumnDescriptionPopover
127
+ column={col}
128
+ node={node}
129
+ onSubmit={async () => {
130
+ const res = await djClient.node(node.name);
131
+ setColumns(res.columns);
132
+ }}
133
+ />
134
+ </span>
142
135
  </td>
143
- ) : (
144
- ''
145
- )}
146
- {node.type !== 'cube' ? (
147
136
  <td>
148
- {showColumnAttributes(col)}
149
- <EditColumnPopover
137
+ <span
138
+ className={`node_type__${
139
+ node.type === 'cube' ? col.type : 'transform'
140
+ } badge node_type`}
141
+ role="columnheader"
142
+ aria-label="ColumnType"
143
+ aria-hidden="false"
144
+ >
145
+ {col.type}
146
+ </span>
147
+ </td>
148
+ {node.type !== 'cube' ? (
149
+ <td>
150
+ {dimensionLinks.length > 0
151
+ ? dimensionLinks.map(link => (
152
+ <span
153
+ className="rounded-pill badge bg-secondary-soft"
154
+ style={{ fontSize: '14px' }}
155
+ key={link[0]}
156
+ >
157
+ <a href={`/nodes/${link[0]}`}>{link[0]}</a>
158
+ </span>
159
+ ))
160
+ : ''}
161
+ <LinkDimensionPopover
162
+ column={col}
163
+ dimensionNodes={dimensionLinks.map(link => link[0])}
164
+ node={node}
165
+ options={dimensions}
166
+ onSubmit={async () => {
167
+ const res = await djClient.node(node.name);
168
+ setLinks(res.dimension_links);
169
+ }}
170
+ />
171
+ </td>
172
+ ) : (
173
+ ''
174
+ )}
175
+ {node.type !== 'cube' ? (
176
+ <td>
177
+ {showColumnAttributes(col)}
178
+ <EditColumnPopover
179
+ column={col}
180
+ node={node}
181
+ options={attributes}
182
+ onSubmit={async () => {
183
+ const res = await djClient.node(node.name);
184
+ setColumns(res.columns);
185
+ }}
186
+ />
187
+ </td>
188
+ ) : (
189
+ ''
190
+ )}
191
+ <td>
192
+ {showColumnPartition(col)}
193
+ <PartitionColumnPopover
150
194
  column={col}
151
195
  node={node}
152
- options={attributes}
153
196
  onSubmit={async () => {
154
197
  const res = await djClient.node(node.name);
155
198
  setColumns(res.columns);
156
199
  }}
157
200
  />
158
201
  </td>
159
- ) : (
160
- ''
161
- )}
162
- <td>
163
- {showColumnPartition(col)}
164
- <PartitionColumnPopover
165
- column={col}
166
- node={node}
167
- onSubmit={async () => {
168
- const res = await djClient.node(node.name);
169
- setColumns(res.columns);
170
- }}
171
- />
172
- </td>
173
- </tr>
174
- ));
202
+ </tr>
203
+ );
204
+ });
175
205
  };
176
206
 
177
207
  return (
178
- <div className="table-responsive">
179
- <table className="card-inner-table table">
180
- <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
181
- <tr>
182
- <th className="text-start">Column</th>
183
- <th>Display Name</th>
184
- <th>Type</th>
185
- {node?.type !== 'cube' ? (
186
- <>
187
- <th>Linked Dimension</th>
188
- <th>Attributes</th>
189
- </>
190
- ) : (
191
- ''
192
- )}
193
- <th>Partition</th>
194
- </tr>
195
- </thead>
196
- <tbody>{columnList(columns)}</tbody>
197
- </table>
198
- </div>
208
+ <>
209
+ <div className="table-responsive">
210
+ <table className="card-inner-table table">
211
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
212
+ <tr>
213
+ <th className="text-start">Column</th>
214
+ <th>Display Name</th>
215
+ <th>Description</th>
216
+ <th>Type</th>
217
+ {node?.type !== 'cube' ? (
218
+ <>
219
+ <th>Linked Dimension</th>
220
+ <th>Attributes</th>
221
+ </>
222
+ ) : (
223
+ ''
224
+ )}
225
+ <th>Partition</th>
226
+ </tr>
227
+ </thead>
228
+ <tbody>{columnList(columns)}</tbody>
229
+ </table>
230
+ </div>
231
+ <div>
232
+ <h3>Linked Dimensions (Custom Join SQL)</h3>
233
+ <table className="card-inner-table table">
234
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
235
+ <tr>
236
+ <th className="text-start">Dimension Node</th>
237
+ <th>Join Type</th>
238
+ <th>Join SQL</th>
239
+ <th>Role</th>
240
+ </tr>
241
+ </thead>
242
+ <tbody>
243
+ {node?.dimension_links.map(link => {
244
+ return (
245
+ <tr key={link.dimension.name}>
246
+ <td>
247
+ <a href={'/nodes/' + link.dimension.name}>
248
+ {link.dimension.name}
249
+ </a>
250
+ </td>
251
+ <td>{link.join_type.toUpperCase()}</td>
252
+ <td style={{ width: '25rem', maxWidth: 'none' }}>
253
+ <SyntaxHighlighter
254
+ language="sql"
255
+ style={foundation}
256
+ wrapLongLines={true}
257
+ >
258
+ {link.join_sql}
259
+ </SyntaxHighlighter>
260
+ </td>
261
+ <td>{link.role}</td>
262
+ </tr>
263
+ );
264
+ })}
265
+ </tbody>
266
+ </table>
267
+ </div>
268
+ </>
199
269
  );
200
270
  }
@@ -0,0 +1,153 @@
1
+ import { useEffect, useState } from 'react';
2
+ import * as React from 'react';
3
+ import { labelize } from '../../../utils/form';
4
+ import LoadingIcon from '../../icons/LoadingIcon';
5
+
6
+ export default function NodeDependenciesTab({ node, djClient }) {
7
+ const [nodeDAG, setNodeDAG] = useState({
8
+ upstreams: [],
9
+ downstreams: [],
10
+ dimensions: [],
11
+ });
12
+
13
+ const [retrieved, setRetrieved] = useState(false);
14
+
15
+ useEffect(() => {
16
+ const fetchData = async () => {
17
+ if (node) {
18
+ let upstreams = await djClient.upstreams(node.name);
19
+ let downstreams = await djClient.downstreams(node.name);
20
+ let dimensions = await djClient.nodeDimensions(node.name);
21
+ setNodeDAG({
22
+ upstreams: upstreams,
23
+ downstreams: downstreams,
24
+ dimensions: dimensions,
25
+ });
26
+ setRetrieved(true);
27
+ }
28
+ };
29
+ fetchData().catch(console.error);
30
+ }, [djClient, node]);
31
+
32
+ // Builds the block of dimensions selectors, grouped by node name + path
33
+ return (
34
+ <div>
35
+ <h2>Upstreams</h2>
36
+ {retrieved ? (
37
+ <NodeList nodes={nodeDAG.upstreams} />
38
+ ) : (
39
+ <span style={{ display: 'block' }}>
40
+ <LoadingIcon centered={false} />
41
+ </span>
42
+ )}
43
+ <h2>Downstreams</h2>
44
+ {retrieved ? (
45
+ <NodeList nodes={nodeDAG.downstreams} />
46
+ ) : (
47
+ <span style={{ display: 'block' }}>
48
+ <LoadingIcon centered={false} />
49
+ </span>
50
+ )}
51
+ <h2>Dimensions</h2>
52
+ {retrieved ? (
53
+ <NodeDimensionsList rawDimensions={nodeDAG.dimensions} />
54
+ ) : (
55
+ <span style={{ display: 'block' }}>
56
+ <LoadingIcon centered={false} />
57
+ </span>
58
+ )}
59
+ </div>
60
+ );
61
+ }
62
+
63
+ export function NodeDimensionsList({ rawDimensions }) {
64
+ const dimensions = Object.entries(
65
+ rawDimensions.reduce((group, dimension) => {
66
+ group[dimension.node_name + dimension.path] =
67
+ group[dimension.node_name + dimension.path] ?? [];
68
+ group[dimension.node_name + dimension.path].push(dimension);
69
+ return group;
70
+ }, {}),
71
+ );
72
+ return (
73
+ <div style={{ padding: '0.5rem' }}>
74
+ {dimensions.map(grouping => {
75
+ const dimensionsInGroup = grouping[1];
76
+ const role = dimensionsInGroup[0].path
77
+ .map(pathItem => pathItem.split('.').slice(-1))
78
+ .join(' → ');
79
+ const fullPath = dimensionsInGroup[0].path.join(' → ');
80
+ const groupHeader = (
81
+ <span
82
+ style={{
83
+ fontWeight: 'normal',
84
+ marginBottom: '15px',
85
+ marginTop: '15px',
86
+ }}
87
+ >
88
+ <a href={`/nodes/${dimensionsInGroup[0].node_name}`}>
89
+ <b>{dimensionsInGroup[0].node_display_name}</b>
90
+ </a>{' '}
91
+ with role{' '}
92
+ <span className="HighlightPath">
93
+ <b>{role}</b>
94
+ </span>{' '}
95
+ via <span className="HighlightPath">{fullPath}</span>
96
+ </span>
97
+ );
98
+ const dimensionGroupOptions = dimensionsInGroup.map(dim => {
99
+ return {
100
+ value: dim.name,
101
+ label:
102
+ labelize(dim.name.split('.').slice(-1)[0]) +
103
+ (dim.properties.includes('primary_key') ? ' (PK)' : ''),
104
+ };
105
+ });
106
+ return (
107
+ <details key={grouping[0]}>
108
+ <summary style={{ marginBottom: '10px' }}>{groupHeader}</summary>
109
+ <div className="dimensionsList">
110
+ {dimensionGroupOptions.map(dimension => {
111
+ return (
112
+ <div key={dimension.value}>
113
+ {dimension.label.split('[').slice(0)[0]} ⇢{' '}
114
+ <code className="DimensionAttribute">
115
+ {dimension.value}
116
+ </code>
117
+ </div>
118
+ );
119
+ })}
120
+ </div>
121
+ </details>
122
+ );
123
+ })}
124
+ </div>
125
+ );
126
+ }
127
+
128
+ export function NodeList({ nodes }) {
129
+ return nodes && nodes.length > 0 ? (
130
+ <ul className="backfills">
131
+ {nodes?.map(node => (
132
+ <li
133
+ className="backfill"
134
+ style={{ marginBottom: '5px' }}
135
+ key={node.name}
136
+ >
137
+ <span
138
+ className={`node_type__${node.type} badge node_type`}
139
+ style={{ marginRight: '5px' }}
140
+ role="dialog"
141
+ aria-hidden="false"
142
+ aria-label="NodeType"
143
+ >
144
+ {node.type}
145
+ </span>
146
+ <a href={`/nodes/${node.name}`}>{node.name}</a>
147
+ </li>
148
+ ))}
149
+ </ul>
150
+ ) : (
151
+ <span style={{ display: 'block' }}>None</span>
152
+ );
153
+ }
@@ -6,7 +6,7 @@ import 'reactflow/dist/style.css';
6
6
  import DJClientContext from '../../providers/djclient';
7
7
  import LayoutFlow from '../../components/djgraph/LayoutFlow';
8
8
 
9
- const NodeLineage = djNode => {
9
+ const NodeGraphTab = djNode => {
10
10
  const djClient = useContext(DJClientContext).DataJunctionAPI;
11
11
 
12
12
  const createNode = node => {
@@ -15,9 +15,25 @@ const NodeLineage = djNode => {
15
15
  col.attributes.some(attr => attr.attribute_type.name === 'primary_key'),
16
16
  )
17
17
  .map(col => col.name);
18
- const column_names = node.columns.map(col => {
19
- return { name: col.name, type: col.type };
20
- });
18
+ const dimensionLinkForeignKeys = node.dimension_links
19
+ ? node.dimension_links.flatMap(link =>
20
+ Object.keys(link.foreign_keys).map(key => key.split('.').slice(-1)),
21
+ )
22
+ : [];
23
+ const column_names = node.columns
24
+ .map(col => {
25
+ return {
26
+ name: col.name,
27
+ type: col.type,
28
+ dimension: col.dimension !== null ? col.dimension.name : null,
29
+ order: primary_key.includes(col.name)
30
+ ? -1
31
+ : dimensionLinkForeignKeys.includes(col.name)
32
+ ? 0
33
+ : 1,
34
+ };
35
+ })
36
+ .sort((a, b) => a.order - b.order);
21
37
  return {
22
38
  id: String(node.name),
23
39
  type: 'DJNode',
@@ -38,28 +54,37 @@ const NodeLineage = djNode => {
38
54
  };
39
55
 
40
56
  const dimensionEdges = node => {
41
- return node.columns
42
- .filter(col => col.dimension)
43
- .map(col => {
44
- return {
45
- id: col.dimension.name + '->' + node.name + '.' + col.name,
46
- source: col.dimension.name,
47
- sourceHandle: col.dimension.name,
48
- target: node.name,
49
- targetHandle: node.name + '.' + col.name,
50
- draggable: true,
51
- markerStart: {
52
- type: MarkerType.Arrow,
53
- width: 20,
54
- height: 20,
55
- color: '#b0b9c2',
56
- },
57
- style: {
58
- strokeWidth: 3,
59
- stroke: '#b0b9c2',
60
- },
61
- };
62
- });
57
+ return node.dimension_links === undefined
58
+ ? []
59
+ : node.dimension_links.flatMap(link => {
60
+ return Object.keys(link.foreign_keys).map(fk => {
61
+ return {
62
+ id:
63
+ link.dimension.name +
64
+ '->' +
65
+ node.name +
66
+ '=' +
67
+ link.foreign_keys[fk] +
68
+ '->' +
69
+ fk,
70
+ source: link.dimension.name,
71
+ sourceHandle: link.foreign_keys[fk],
72
+ target: node.name,
73
+ targetHandle: fk,
74
+ draggable: true,
75
+ markerStart: {
76
+ type: MarkerType.Arrow,
77
+ width: 20,
78
+ height: 20,
79
+ color: '#b0b9c2',
80
+ },
81
+ style: {
82
+ strokeWidth: 3,
83
+ stroke: '#b0b9c2',
84
+ },
85
+ };
86
+ });
87
+ });
63
88
  };
64
89
 
65
90
  const parentEdges = node => {
@@ -85,8 +110,10 @@ const NodeLineage = djNode => {
85
110
  };
86
111
 
87
112
  const dagFetch = async (getLayoutedElements, setNodes, setEdges) => {
88
- let related_nodes = await djClient.node_dag(djNode.djNode.name);
89
- var djNodes = [djNode.djNode];
113
+ let related_nodes = djNode.djNode
114
+ ? await djClient.node_dag(djNode.djNode.name)
115
+ : [];
116
+ var djNodes = djNode.djNode ? [djNode.djNode] : [];
90
117
  for (const iterable of [related_nodes]) {
91
118
  for (const item of iterable) {
92
119
  if (item.type !== 'cube') {
@@ -109,4 +136,4 @@ const NodeLineage = djNode => {
109
136
  };
110
137
  return LayoutFlow(djNode, dagFetch);
111
138
  };
112
- export default NodeLineage;
139
+ export default NodeGraphTab;