datajunction-ui 0.0.1-a49 → 0.0.1-a50

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.1a49",
3
+ "version": "0.0.1a50",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -164,7 +164,7 @@
164
164
  ],
165
165
  "coverageThreshold": {
166
166
  "global": {
167
- "statements": 89,
167
+ "statements": 87,
168
168
  "branches": 75,
169
169
  "lines": 80,
170
170
  "functions": 85
@@ -33,14 +33,7 @@ export default function DimensionFilter({ dimension, onChange }) {
33
33
  )
34
34
  .map(col => col.name);
35
35
 
36
- // TODO: we're disabling this for now because it's unclear how performant the dimensions node
37
- // data endpoints are. To re-enable, uncomment the following line:
38
- // const data = await djClient.nodeData(dimension.metadata.node_name);
39
- const data = { results: [] };
40
-
41
- // TODO: when the above is enabled, this will use each dimension node's 'Label' column
42
- // to build the display label for the dropdown, while continuing to pass the primary
43
- // key in for filtering
36
+ const data = await djClient.nodeData(dimension.metadata.node_name);
44
37
  /* istanbul ignore if */
45
38
  if (dimensionNode && data.results && data.results.length > 0) {
46
39
  const columnNames = data.results[0].columns.map(
@@ -1,10 +1,11 @@
1
1
  import { useContext, useEffect, useRef, useState } from 'react';
2
2
  import * as React from 'react';
3
3
  import DJClientContext from '../../providers/djclient';
4
- import { Form, Formik } from 'formik';
4
+ import { Field, Form, Formik } from 'formik';
5
5
  import { FormikSelect } from '../AddEditNodePage/FormikSelect';
6
6
  import EditIcon from '../../icons/EditIcon';
7
7
  import { displayMessageAfterSubmit } from '../../../utils/form';
8
+ import LoadingIcon from '../../icons/LoadingIcon';
8
9
 
9
10
  export default function LinkDimensionPopover({
10
11
  column,
@@ -35,11 +36,17 @@ export default function LinkDimensionPopover({
35
36
  { node, column, dimension },
36
37
  { setSubmitting, setStatus },
37
38
  ) => {
38
- setSubmitting(false);
39
39
  if (referencedDimensionNode && dimension === 'Remove') {
40
- await unlinkDimension(node, column, referencedDimensionNode, setStatus);
40
+ await unlinkDimension(
41
+ node,
42
+ column,
43
+ referencedDimensionNode,
44
+ setStatus,
45
+ ).then(_ => setSubmitting(false));
41
46
  } else {
42
- await linkDimension(node, column, dimension, setStatus);
47
+ await linkDimension(node, column, dimension, setStatus).then(_ =>
48
+ setSubmitting(false),
49
+ );
43
50
  }
44
51
  onSubmit();
45
52
  };
@@ -137,8 +144,9 @@ export default function LinkDimensionPopover({
137
144
  type="submit"
138
145
  aria-label="SaveLinkDimension"
139
146
  aria-hidden="false"
147
+ disabled={isSubmitting}
140
148
  >
141
- Save
149
+ {isSubmitting ? <LoadingIcon /> : 'Save'}
142
150
  </button>
143
151
  </Form>
144
152
  );
@@ -1,5 +1,4 @@
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';
5
4
  import LinkDimensionPopover from './LinkDimensionPopover';
@@ -35,8 +34,11 @@ export default function NodeColumnTab({ node, djClient }) {
35
34
  useEffect(() => {
36
35
  const fetchData = async () => {
37
36
  const dimensions = await djClient.dimensions();
38
- const options = dimensions.map(name => {
39
- return { value: name, label: name };
37
+ const options = dimensions.map(dim => {
38
+ return {
39
+ value: dim.name,
40
+ label: `${dim.name} (${dim.indegree} links)`,
41
+ };
40
42
  });
41
43
  setDimensions(options);
42
44
  };
@@ -0,0 +1,147 @@
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
+ let upstreams = await djClient.upstreams(node.name);
18
+ let downstreams = await djClient.downstreams(node.name);
19
+ let dimensions = await djClient.nodeDimensions(node.name);
20
+ setNodeDAG({
21
+ upstreams: upstreams,
22
+ downstreams: downstreams,
23
+ dimensions: dimensions,
24
+ });
25
+ setRetrieved(true);
26
+ };
27
+ fetchData().catch(console.error);
28
+ }, [djClient, node]);
29
+
30
+ // Builds the block of dimensions selectors, grouped by node name + path
31
+ return (
32
+ <div>
33
+ <h2>Upstreams</h2>
34
+ {retrieved ? (
35
+ <NodeList nodes={nodeDAG.upstreams} />
36
+ ) : (
37
+ <span style={{ display: 'inline-block' }}>
38
+ <LoadingIcon />
39
+ </span>
40
+ )}
41
+ <h2>Downstreams</h2>
42
+ {retrieved ? (
43
+ <NodeList nodes={nodeDAG.downstreams} />
44
+ ) : (
45
+ <span style={{ display: 'inline-block' }}>
46
+ <LoadingIcon />
47
+ </span>
48
+ )}
49
+ <h2>Dimensions</h2>
50
+ {retrieved ? (
51
+ <NodeDimensionsList rawDimensions={nodeDAG.dimensions} />
52
+ ) : (
53
+ <span style={{ display: 'inline-block' }}>
54
+ <LoadingIcon />
55
+ </span>
56
+ )}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ export function NodeDimensionsList({ rawDimensions }) {
62
+ const dimensions = Object.entries(
63
+ rawDimensions.reduce((group, dimension) => {
64
+ group[dimension.node_name + dimension.path] =
65
+ group[dimension.node_name + dimension.path] ?? [];
66
+ group[dimension.node_name + dimension.path].push(dimension);
67
+ return group;
68
+ }, {}),
69
+ );
70
+ return (
71
+ <div style={{ padding: '0.5rem' }}>
72
+ {dimensions.map(grouping => {
73
+ const dimensionsInGroup = grouping[1];
74
+ const role = dimensionsInGroup[0].path
75
+ .map(pathItem => pathItem.split('.').slice(-1))
76
+ .join(' → ');
77
+ const fullPath = dimensionsInGroup[0].path.join(' → ');
78
+ const groupHeader = (
79
+ <span
80
+ style={{
81
+ fontWeight: 'normal',
82
+ marginBottom: '15px',
83
+ marginTop: '15px',
84
+ }}
85
+ >
86
+ <a href={`/nodes/${dimensionsInGroup[0].node_name}`}>
87
+ <b>{dimensionsInGroup[0].node_display_name}</b>
88
+ </a>{' '}
89
+ with role{' '}
90
+ <span className="HighlightPath">
91
+ <b>{role}</b>
92
+ </span>{' '}
93
+ via <span className="HighlightPath">{fullPath}</span>
94
+ </span>
95
+ );
96
+ const dimensionGroupOptions = dimensionsInGroup.map(dim => {
97
+ return {
98
+ value: dim.name,
99
+ label:
100
+ labelize(dim.name.split('.').slice(-1)[0]) +
101
+ (dim.is_primary_key ? ' (PK)' : ''),
102
+ };
103
+ });
104
+ return (
105
+ <details>
106
+ <summary style={{ marginBottom: '10px' }}>{groupHeader}</summary>
107
+ <div className="dimensionsList">
108
+ {dimensionGroupOptions.map(dimension => {
109
+ return (
110
+ <div>
111
+ {dimension.label.split('[').slice(0)[0]} ⇢{' '}
112
+ <code className="DimensionAttribute">
113
+ {dimension.value}
114
+ </code>
115
+ </div>
116
+ );
117
+ })}
118
+ </div>
119
+ </details>
120
+ );
121
+ })}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ export function NodeList({ nodes }) {
127
+ return nodes && nodes.length > 0 ? (
128
+ <ul className="backfills">
129
+ {nodes?.map(node => (
130
+ <li className="backfill" style={{ marginBottom: '5px' }}>
131
+ <span
132
+ className={`node_type__${node.type} badge node_type`}
133
+ style={{ marginRight: '5px' }}
134
+ role="dialog"
135
+ aria-hidden="false"
136
+ aria-label="NodeType"
137
+ >
138
+ {node.type}
139
+ </span>
140
+ <a href={`/nodes/${node.name}`}>{node.name}</a>
141
+ </li>
142
+ ))}
143
+ </ul>
144
+ ) : (
145
+ <span style={{ display: 'inline-block' }}>None</span>
146
+ );
147
+ }
@@ -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 => {
@@ -134,4 +134,4 @@ const NodeLineage = djNode => {
134
134
  };
135
135
  return LayoutFlow(djNode, dagFetch);
136
136
  };
137
- export default NodeLineage;
137
+ export default NodeGraphTab;
@@ -74,6 +74,17 @@ export default function NodeHistory({ node, djClient }) {
74
74
  </div>
75
75
  );
76
76
  }
77
+ if (event.activity_type === 'refresh') {
78
+ return (
79
+ <div className="history-left">
80
+ <b style={{ textTransform: 'capitalize' }}>{event.activity_type}</b>{' '}
81
+ {event.entity_type}{' '}
82
+ <b>
83
+ <a href={'/nodes/' + event.entity_name}>{event.entity_name}</a>
84
+ </b>
85
+ </div>
86
+ );
87
+ }
77
88
  if (event.activity_type === 'update' && event.entity_type === 'node') {
78
89
  return (
79
90
  <div className="history-left">
@@ -93,26 +93,31 @@ export default function NodeValidateTab({ node, djClient }) {
93
93
  const sse = await djClient.streamNodeData(node?.name, selection);
94
94
  sse.onmessage = e => {
95
95
  const messageData = JSON.parse(JSON.parse(e.data));
96
+ if (
97
+ messageData !== null &&
98
+ messageData?.state !== 'FINISHED' &&
99
+ messageData?.state !== 'CANCELED' &&
100
+ messageData?.state !== 'FAILED'
101
+ ) {
102
+ setRunning(false);
103
+ }
96
104
  if (messageData.results && messageData.results?.length > 0) {
97
105
  messageData.numRows = messageData.results?.length
98
106
  ? messageData.results[0].rows.length
99
107
  : [];
100
108
  switchTab('results');
109
+ setRunning(false);
101
110
  } else {
102
111
  switchTab('info');
103
112
  }
104
113
  setQueryInfo(messageData);
105
114
  };
106
115
  sse.onerror = () => sse.close();
107
- setRunning(false);
108
116
  };
109
117
 
110
118
  // Handle form submission (runs the query)
111
119
  const handleSubmit = async (values, { setSubmitting, setStatus }) => {
112
- await runQuery(values, setStatus, setSubmitting).then(_ => {
113
- window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
114
- setSubmitting(false);
115
- });
120
+ await runQuery(values, setStatus, setSubmitting);
116
121
  };
117
122
 
118
123
  // Handle when filter values are updated. This is available for all nodes.
@@ -245,15 +250,7 @@ export default function NodeValidateTab({ node, djClient }) {
245
250
  className="button-3 execute-button"
246
251
  style={{ marginTop: '1rem' }}
247
252
  >
248
- {running ||
249
- (queryInfo !== null &&
250
- queryInfo?.state !== 'FINISHED' &&
251
- queryInfo?.state !== 'CANCELED' &&
252
- queryInfo?.state !== 'FAILED') ? (
253
- <LoadingIcon />
254
- ) : (
255
- '► Run'
256
- )}
253
+ {isSubmitting || running === true ? <LoadingIcon /> : '► Run'}
257
254
  </button>
258
255
  </div>
259
256
  <div
@@ -58,7 +58,6 @@ describe('<LinkDimensionPopover />', () => {
58
58
  fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
59
59
  fireEvent.click(screen.getByText('Dimension 1'));
60
60
  fireEvent.click(getByText('Save'));
61
- getByText('Save').click();
62
61
 
63
62
  // Expect linkDimension to be called
64
63
  await waitFor(() => {
@@ -74,7 +73,6 @@ describe('<LinkDimensionPopover />', () => {
74
73
  fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
75
74
  fireEvent.click(screen.getByText('[Remove dimension link]'));
76
75
  fireEvent.click(getByText('Save'));
77
- getByText('Save').click();
78
76
 
79
77
  // Expect unlinkDimension to be called
80
78
  await waitFor(() => {
@@ -133,7 +131,6 @@ describe('<LinkDimensionPopover />', () => {
133
131
  fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
134
132
  fireEvent.click(screen.getByText('Dimension 1'));
135
133
  fireEvent.click(getByText('Save'));
136
- getByText('Save').click();
137
134
 
138
135
  // Expect linkDimension to be called
139
136
  await waitFor(() => {
@@ -151,7 +148,6 @@ describe('<LinkDimensionPopover />', () => {
151
148
  fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
152
149
  fireEvent.click(screen.getByText('[Remove Dimension]'));
153
150
  fireEvent.click(getByText('Save'));
154
- getByText('Save').click();
155
151
 
156
152
  // Expect unlinkDimension to be called
157
153
  await waitFor(() => {
@@ -1,11 +1,13 @@
1
1
  import React from 'react';
2
2
  import { render, waitFor, screen } from '@testing-library/react';
3
- import NodeDimensionsTab from '../NodeDimensionsTab';
3
+ import NodeDependenciesTab from '../NodeDependenciesTab';
4
4
 
5
- describe('<NodeDimensionsTab />', () => {
5
+ describe('<NodeDependenciesTab />', () => {
6
6
  const mockDjClient = {
7
7
  node: jest.fn(),
8
8
  nodeDimensions: jest.fn(),
9
+ upstreams: jest.fn(),
10
+ downstreams: jest.fn(),
9
11
  };
10
12
 
11
13
  const mockNode = {
@@ -129,11 +131,15 @@ describe('<NodeDimensionsTab />', () => {
129
131
  beforeEach(() => {
130
132
  // Reset the mocks before each test
131
133
  mockDjClient.nodeDimensions.mockReset();
134
+ mockDjClient.upstreams.mockReset();
135
+ mockDjClient.downstreams.mockReset();
132
136
  });
133
137
 
134
138
  it('renders nodes with dimensions', async () => {
135
139
  mockDjClient.nodeDimensions.mockReturnValue(mockDimensions);
136
- render(<NodeDimensionsTab node={mockNode} djClient={mockDjClient} />);
140
+ mockDjClient.upstreams.mockReturnValue([mockNode]);
141
+ mockDjClient.downstreams.mockReturnValue([mockNode]);
142
+ render(<NodeDependenciesTab node={mockNode} djClient={mockDjClient} />);
137
143
  await waitFor(() => {
138
144
  for (const dimension of mockDimensions) {
139
145
  const link = screen.getByText(dimension.node_display_name).closest('a');
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { render, screen, waitFor } from '@testing-library/react';
3
- import NodeLineage from '../NodeGraphTab';
3
+ import NodeGraphTab from '../NodeGraphTab';
4
4
  import DJClientContext from '../../../providers/djclient';
5
5
 
6
6
  describe('<NodeLineage />', () => {
@@ -564,7 +564,7 @@ describe('<NodeLineage />', () => {
564
564
  // const layoutFlowMock = jest.spyOn(LayoutFlow);
565
565
  const { container } = render(
566
566
  <DJClientContext.Provider value={djClient}>
567
- <NodeLineage {...defaultProps} />
567
+ <NodeGraphTab {...defaultProps} />
568
568
  </DJClientContext.Provider>,
569
569
  );
570
570
 
@@ -706,9 +706,11 @@ describe('<NodePage />', () => {
706
706
  expect(screen.getByText('Add Filters')).toBeInTheDocument();
707
707
  expect(screen.getByText('Generated Query')).toBeInTheDocument();
708
708
  expect(screen.getByText('Results')).toBeInTheDocument();
709
+ });
710
+ // Click on the 'Validate' tab
711
+ fireEvent.click(screen.getByRole('button', { name: '► Validate' }));
709
712
 
710
- // Click on the 'Validate' tab
711
- fireEvent.click(screen.getByRole('button', { name: '► Validate' }));
713
+ await waitFor(() => {
712
714
  expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith(
713
715
  mocks.mockMetricNode.name,
714
716
  );
@@ -719,35 +721,41 @@ describe('<NodePage />', () => {
719
721
  expect(djClient.DataJunctionAPI.nodeDimensions).toHaveBeenCalledWith(
720
722
  mocks.mockMetricNode.name,
721
723
  );
724
+ });
722
725
 
723
- // Click on 'Run' to run the node query
724
- const runButton = screen.getByText('► Run');
725
- fireEvent.click(runButton);
726
+ // Click on 'Run' to run the node query
727
+ const runButton = screen.getByText('► Run');
728
+ fireEvent.click(runButton);
726
729
 
730
+ await waitFor(() => {
727
731
  expect(djClient.DataJunctionAPI.streamNodeData).toHaveBeenCalledWith(
728
732
  mocks.mockMetricNode.name,
729
733
  { dimensions: [], filters: [] },
730
734
  );
731
735
  expect(streamNodeData.onmessage).toBeDefined();
732
736
  expect(streamNodeData.onerror).toBeDefined();
737
+ });
733
738
 
734
- const infoTab = screen.getByRole('button', { name: 'QueryInfo' });
735
- const resultsTab = screen.getByText('Results');
739
+ const infoTab = screen.getByRole('button', { name: 'QueryInfo' });
740
+ const resultsTab = screen.getByText('Results');
736
741
 
737
- // Initially, the Results tab should be active
738
- expect(resultsTab).toHaveClass('active');
739
- expect(infoTab).not.toHaveClass('active');
742
+ // Initially, the Results tab should be active
743
+ expect(resultsTab).toHaveClass('active');
744
+ expect(infoTab).not.toHaveClass('active');
740
745
 
741
- // Click on the Info tab first
742
- fireEvent.click(infoTab);
746
+ // Click on the Info tab first
747
+ fireEvent.click(infoTab);
743
748
 
749
+ await waitFor(() => {
744
750
  // Now, the Info tab should be active
745
751
  expect(infoTab).toHaveClass('active');
746
752
  expect(resultsTab).not.toHaveClass('active');
753
+ });
747
754
 
748
- // Click on the Results tab
749
- fireEvent.click(resultsTab);
755
+ // Click on the Results tab
756
+ fireEvent.click(resultsTab);
750
757
 
758
+ await waitFor(() => {
751
759
  // Now, the Results tab should be active again
752
760
  expect(resultsTab).toHaveClass('active');
753
761
  expect(infoTab).not.toHaveClass('active');
@@ -5,7 +5,7 @@ import Tab from '../../components/Tab';
5
5
  import NamespaceHeader from '../../components/NamespaceHeader';
6
6
  import NodeInfoTab from './NodeInfoTab';
7
7
  import NodeColumnTab from './NodeColumnTab';
8
- import NodeLineage from './NodeGraphTab';
8
+ import NodeGraphTab from './NodeGraphTab';
9
9
  import NodeHistory from './NodeHistory';
10
10
  import DJClientContext from '../../providers/djclient';
11
11
  import NodeValidateTab from './NodeValidateTab';
@@ -15,7 +15,7 @@ import NodesWithDimension from './NodesWithDimension';
15
15
  import NodeColumnLineage from './NodeLineageTab';
16
16
  import EditIcon from '../../icons/EditIcon';
17
17
  import AlertIcon from '../../icons/AlertIcon';
18
- import NodeDimensionsTab from './NodeDimensionsTab';
18
+ import NodeDependenciesTab from './NodeDependenciesTab';
19
19
  import { useNavigate } from 'react-router-dom';
20
20
 
21
21
  export function NodePage() {
@@ -112,8 +112,8 @@ export function NodePage() {
112
112
  display: node?.type === 'metric',
113
113
  },
114
114
  {
115
- id: 'dimensions',
116
- name: 'Dimensions',
115
+ id: 'dependencies',
116
+ name: 'Dependencies',
117
117
  display: node?.type !== 'cube',
118
118
  },
119
119
  ];
@@ -129,7 +129,7 @@ export function NodePage() {
129
129
  tabToDisplay = <NodeColumnTab node={node} djClient={djClient} />;
130
130
  break;
131
131
  case 'graph':
132
- tabToDisplay = <NodeLineage djNode={node} djClient={djClient} />;
132
+ tabToDisplay = <NodeGraphTab djNode={node} djClient={djClient} />;
133
133
  break;
134
134
  case 'history':
135
135
  tabToDisplay = <NodeHistory node={node} djClient={djClient} />;
@@ -146,8 +146,8 @@ export function NodePage() {
146
146
  case 'lineage':
147
147
  tabToDisplay = <NodeColumnLineage djNode={node} djClient={djClient} />;
148
148
  break;
149
- case 'dimensions':
150
- tabToDisplay = <NodeDimensionsTab node={node} djClient={djClient} />;
149
+ case 'dependencies':
150
+ tabToDisplay = <NodeDependenciesTab node={node} djClient={djClient} />;
151
151
  break;
152
152
  default: /* istanbul ignore next */
153
153
  tabToDisplay = <NodeInfoTab node={node} />;
@@ -13,7 +13,7 @@
13
13
  border: 8px solid #fff;
14
14
  border-radius: 50%;
15
15
  animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
16
- border-color: #fff transparent transparent transparent;
16
+ border-color: #bfbfbf transparent transparent transparent;
17
17
  }
18
18
  .lds-ring div:nth-child(1) {
19
19
  animation-delay: -0.45s;
@@ -1,80 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- import * as React from 'react';
3
- import { labelize } from '../../../utils/form';
4
-
5
- export default function NodeDimensionsTab({ node, djClient }) {
6
- const [dimensions, setDimensions] = useState([]);
7
- useEffect(() => {
8
- const fetchData = async () => {
9
- if (node) {
10
- const data = await djClient.nodeDimensions(node.name);
11
- const grouped = Object.entries(
12
- data.reduce((group, dimension) => {
13
- group[dimension.node_name + dimension.path] =
14
- group[dimension.node_name + dimension.path] ?? [];
15
- group[dimension.node_name + dimension.path].push(dimension);
16
- return group;
17
- }, {}),
18
- );
19
- setDimensions(grouped);
20
- }
21
- };
22
- fetchData().catch(console.error);
23
- }, [djClient, node]);
24
-
25
- // Builds the block of dimensions selectors, grouped by node name + path
26
- return (
27
- <div style={{ padding: '1rem' }}>
28
- {dimensions.map(grouping => {
29
- const dimensionsInGroup = grouping[1];
30
- const role = dimensionsInGroup[0].path
31
- .map(pathItem => pathItem.split('.').slice(-1))
32
- .join(' → ');
33
- const fullPath = dimensionsInGroup[0].path.join(' → ');
34
- const groupHeader = (
35
- <h4
36
- style={{
37
- fontWeight: 'normal',
38
- marginBottom: '5px',
39
- marginTop: '15px',
40
- }}
41
- >
42
- <a href={`/nodes/${dimensionsInGroup[0].node_name}`}>
43
- <b>{dimensionsInGroup[0].node_display_name}</b>
44
- </a>{' '}
45
- with role{' '}
46
- <span className="HighlightPath">
47
- <b>{role}</b>
48
- </span>{' '}
49
- via <span className="HighlightPath">{fullPath}</span>
50
- </h4>
51
- );
52
- const dimensionGroupOptions = dimensionsInGroup.map(dim => {
53
- return {
54
- value: dim.name,
55
- label:
56
- labelize(dim.name.split('.').slice(-1)[0]) +
57
- (dim.is_primary_key ? ' (PK)' : ''),
58
- };
59
- });
60
- return (
61
- <>
62
- {groupHeader}
63
- <div className="dimensionsList">
64
- {dimensionGroupOptions.map(dimension => {
65
- return (
66
- <div>
67
- {dimension.label.split('[').slice(0)[0]} ⇢{' '}
68
- <code className="DimensionAttribute">
69
- {dimension.value}
70
- </code>
71
- </div>
72
- );
73
- })}
74
- </div>
75
- </>
76
- );
77
- })}
78
- </div>
79
- );
80
- }