datajunction-ui 0.0.1-a49.dev2 → 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.1-a49.dev2",
3
+ "version": "0.0.1a50",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -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;
@@ -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
 
@@ -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
- }