datajunction-ui 0.0.20 → 0.0.22

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.20",
3
+ "version": "0.0.22",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -4,53 +4,51 @@ import { labelize } from '../../../utils/form';
4
4
  import LoadingIcon from '../../icons/LoadingIcon';
5
5
 
6
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);
7
+ const [upstreams, setUpstreams] = useState(null);
8
+ const [downstreams, setDownstreams] = useState(null);
9
+ const [dimensions, setDimensions] = useState(null);
14
10
 
15
11
  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);
12
+ if (node) {
13
+ // Reset state when node changes
14
+ setUpstreams(null);
15
+ setDownstreams(null);
16
+ setDimensions(null);
17
+
18
+ // Load all three in parallel, display each as it loads
19
+ djClient.upstreamsGQL(node.name).then(setUpstreams).catch(console.error);
20
+ djClient
21
+ .downstreamsGQL(node.name)
22
+ .then(setDownstreams)
23
+ .catch(console.error);
24
+ djClient
25
+ .nodeDimensions(node.name)
26
+ .then(setDimensions)
27
+ .catch(console.error);
28
+ }
30
29
  }, [djClient, node]);
31
30
 
32
- // Builds the block of dimensions selectors, grouped by node name + path
33
31
  return (
34
32
  <div>
35
33
  <h2>Upstreams</h2>
36
- {retrieved ? (
37
- <NodeList nodes={nodeDAG.upstreams} />
34
+ {upstreams !== null ? (
35
+ <NodeList nodes={upstreams} />
38
36
  ) : (
39
37
  <span style={{ display: 'block' }}>
40
38
  <LoadingIcon centered={false} />
41
39
  </span>
42
40
  )}
43
41
  <h2>Downstreams</h2>
44
- {retrieved ? (
45
- <NodeList nodes={nodeDAG.downstreams} />
42
+ {downstreams !== null ? (
43
+ <NodeList nodes={downstreams} />
46
44
  ) : (
47
45
  <span style={{ display: 'block' }}>
48
46
  <LoadingIcon centered={false} />
49
47
  </span>
50
48
  )}
51
49
  <h2>Dimensions</h2>
52
- {retrieved ? (
53
- <NodeDimensionsList rawDimensions={nodeDAG.dimensions} />
50
+ {dimensions !== null ? (
51
+ <NodeDimensionsList rawDimensions={dimensions} />
54
52
  ) : (
55
53
  <span style={{ display: 'block' }}>
56
54
  <LoadingIcon centered={false} />
@@ -128,24 +126,28 @@ export function NodeDimensionsList({ rawDimensions }) {
128
126
  export function NodeList({ nodes }) {
129
127
  return nodes && nodes.length > 0 ? (
130
128
  <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"
129
+ {nodes?.map(node => {
130
+ // GraphQL returns uppercase types (e.g., "SOURCE"), CSS classes expect lowercase
131
+ const nodeType = node.type?.toLowerCase() || '';
132
+ return (
133
+ <li
134
+ className="backfill"
135
+ style={{ marginBottom: '5px' }}
136
+ key={node.name}
143
137
  >
144
- {node.type}
145
- </span>
146
- <a href={`/nodes/${node.name}`}>{node.name}</a>
147
- </li>
148
- ))}
138
+ <span
139
+ className={`node_type__${nodeType} badge node_type`}
140
+ style={{ marginRight: '5px' }}
141
+ role="dialog"
142
+ aria-hidden="false"
143
+ aria-label="NodeType"
144
+ >
145
+ {nodeType}
146
+ </span>
147
+ <a href={`/nodes/${node.name}`}>{node.name}</a>
148
+ </li>
149
+ );
150
+ })}
149
151
  </ul>
150
152
  ) : (
151
153
  <span style={{ display: 'block' }}>None</span>
@@ -6,8 +6,8 @@ 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
+ upstreamsGQL: jest.fn(),
10
+ downstreamsGQL: jest.fn(),
11
11
  };
12
12
 
13
13
  const mockNode = {
@@ -131,14 +131,20 @@ describe('<NodeDependenciesTab />', () => {
131
131
  beforeEach(() => {
132
132
  // Reset the mocks before each test
133
133
  mockDjClient.nodeDimensions.mockReset();
134
- mockDjClient.upstreams.mockReset();
135
- mockDjClient.downstreams.mockReset();
134
+ mockDjClient.upstreamsGQL.mockReset();
135
+ mockDjClient.downstreamsGQL.mockReset();
136
136
  });
137
137
 
138
138
  it('renders nodes with dimensions', async () => {
139
- mockDjClient.nodeDimensions.mockReturnValue(mockDimensions);
140
- mockDjClient.upstreams.mockReturnValue([mockNode]);
141
- mockDjClient.downstreams.mockReturnValue([mockNode]);
139
+ // Use mockResolvedValue since the component now uses .then()
140
+ mockDjClient.nodeDimensions.mockResolvedValue(mockDimensions);
141
+ // GraphQL responses return uppercase types (e.g., "SOURCE")
142
+ mockDjClient.upstreamsGQL.mockResolvedValue([
143
+ { name: mockNode.name, type: 'SOURCE' },
144
+ ]);
145
+ mockDjClient.downstreamsGQL.mockResolvedValue([
146
+ { name: mockNode.name, type: 'SOURCE' },
147
+ ]);
142
148
  render(<NodeDependenciesTab node={mockNode} djClient={mockDjClient} />);
143
149
  await waitFor(() => {
144
150
  for (const dimension of mockDimensions) {
@@ -690,6 +690,49 @@ export const DataJunctionAPI = {
690
690
  ).json();
691
691
  },
692
692
 
693
+ // GraphQL-based upstream/downstream queries - more efficient as they only fetch needed fields
694
+ upstreamsGQL: async function (nodeNames) {
695
+ const names = Array.isArray(nodeNames) ? nodeNames : [nodeNames];
696
+ const query = `
697
+ query GetUpstreamNodes($nodeNames: [String!]!) {
698
+ upstreamNodes(nodeNames: $nodeNames) {
699
+ name
700
+ type
701
+ }
702
+ }
703
+ `;
704
+ const results = await (
705
+ await fetch(DJ_GQL, {
706
+ method: 'POST',
707
+ headers: { 'Content-Type': 'application/json' },
708
+ credentials: 'include',
709
+ body: JSON.stringify({ query, variables: { nodeNames: names } }),
710
+ })
711
+ ).json();
712
+ return results.data?.upstreamNodes || [];
713
+ },
714
+
715
+ downstreamsGQL: async function (nodeNames) {
716
+ const names = Array.isArray(nodeNames) ? nodeNames : [nodeNames];
717
+ const query = `
718
+ query GetDownstreamNodes($nodeNames: [String!]!) {
719
+ downstreamNodes(nodeNames: $nodeNames) {
720
+ name
721
+ type
722
+ }
723
+ }
724
+ `;
725
+ const results = await (
726
+ await fetch(DJ_GQL, {
727
+ method: 'POST',
728
+ headers: { 'Content-Type': 'application/json' },
729
+ credentials: 'include',
730
+ body: JSON.stringify({ query, variables: { nodeNames: names } }),
731
+ })
732
+ ).json();
733
+ return results.data?.downstreamNodes || [];
734
+ },
735
+
693
736
  node_dag: async function (name) {
694
737
  return await (
695
738
  await fetch(`${DJ_URL}/nodes/${name}/dag/`, {
@@ -324,6 +324,82 @@ describe('DataJunctionAPI', () => {
324
324
  );
325
325
  });
326
326
 
327
+ it('calls upstreamsGQL correctly with single node', async () => {
328
+ const nodeName = 'sampleNode';
329
+ fetch.mockResponseOnce(
330
+ JSON.stringify({
331
+ data: { upstreamNodes: [{ name: 'upstream1', type: 'SOURCE' }] },
332
+ }),
333
+ );
334
+ const result = await DataJunctionAPI.upstreamsGQL(nodeName);
335
+ expect(fetch).toHaveBeenCalledWith(
336
+ `${DJ_URL}/graphql`,
337
+ expect.objectContaining({
338
+ method: 'POST',
339
+ credentials: 'include',
340
+ headers: { 'Content-Type': 'application/json' },
341
+ }),
342
+ );
343
+ expect(result).toEqual([{ name: 'upstream1', type: 'SOURCE' }]);
344
+ });
345
+
346
+ it('calls upstreamsGQL correctly with multiple nodes', async () => {
347
+ const nodeNames = ['node1', 'node2'];
348
+ fetch.mockResponseOnce(
349
+ JSON.stringify({
350
+ data: {
351
+ upstreamNodes: [
352
+ { name: 'upstream1', type: 'SOURCE' },
353
+ { name: 'upstream2', type: 'TRANSFORM' },
354
+ ],
355
+ },
356
+ }),
357
+ );
358
+ const result = await DataJunctionAPI.upstreamsGQL(nodeNames);
359
+ expect(result).toEqual([
360
+ { name: 'upstream1', type: 'SOURCE' },
361
+ { name: 'upstream2', type: 'TRANSFORM' },
362
+ ]);
363
+ });
364
+
365
+ it('calls downstreamsGQL correctly with single node', async () => {
366
+ const nodeName = 'sampleNode';
367
+ fetch.mockResponseOnce(
368
+ JSON.stringify({
369
+ data: { downstreamNodes: [{ name: 'downstream1', type: 'METRIC' }] },
370
+ }),
371
+ );
372
+ const result = await DataJunctionAPI.downstreamsGQL(nodeName);
373
+ expect(fetch).toHaveBeenCalledWith(
374
+ `${DJ_URL}/graphql`,
375
+ expect.objectContaining({
376
+ method: 'POST',
377
+ credentials: 'include',
378
+ headers: { 'Content-Type': 'application/json' },
379
+ }),
380
+ );
381
+ expect(result).toEqual([{ name: 'downstream1', type: 'METRIC' }]);
382
+ });
383
+
384
+ it('calls downstreamsGQL correctly with multiple nodes', async () => {
385
+ const nodeNames = ['node1', 'node2'];
386
+ fetch.mockResponseOnce(
387
+ JSON.stringify({
388
+ data: {
389
+ downstreamNodes: [
390
+ { name: 'downstream1', type: 'METRIC' },
391
+ { name: 'downstream2', type: 'CUBE' },
392
+ ],
393
+ },
394
+ }),
395
+ );
396
+ const result = await DataJunctionAPI.downstreamsGQL(nodeNames);
397
+ expect(result).toEqual([
398
+ { name: 'downstream1', type: 'METRIC' },
399
+ { name: 'downstream2', type: 'CUBE' },
400
+ ]);
401
+ });
402
+
327
403
  it('calls node_dag correctly', async () => {
328
404
  const nodeName = 'sampleNode';
329
405
  fetch.mockResponseOnce(JSON.stringify({}));
@@ -135,7 +135,7 @@
135
135
 
136
136
  /* Notifications menu */
137
137
  .notifications-menu {
138
- min-width: 240px;
138
+ min-width: 340px;
139
139
  max-width: 600px;
140
140
  padding: 0;
141
141
  }
@@ -221,7 +221,7 @@
221
221
  }
222
222
 
223
223
  .notification-title {
224
- font-size: 13px;
224
+ font-size: 15px;
225
225
  color: #333;
226
226
  font-weight: 500;
227
227
  display: flex;
@@ -230,7 +230,7 @@
230
230
  }
231
231
 
232
232
  .notification-entity {
233
- font-size: 11px;
233
+ font-size: 13px;
234
234
  }
235
235
 
236
236
  .notification-title .badge.version {
@@ -243,9 +243,9 @@
243
243
  }
244
244
 
245
245
  .notification-meta {
246
- font-size: 11px;
246
+ font-size: 13px;
247
247
  color: #999;
248
- display: flex;
248
+ display: block;
249
249
  align-items: center;
250
250
  gap: 0.35rem;
251
251
  }
package/webpack.config.js CHANGED
@@ -10,6 +10,8 @@ var babelOptions = {
10
10
  presets: ['@babel/preset-react'],
11
11
  };
12
12
 
13
+ const isProduction = process.env.NODE_ENV === 'production';
14
+
13
15
  module.exports = {
14
16
  cache: true,
15
17
  entry: {
@@ -17,7 +19,7 @@ module.exports = {
17
19
  vendor: ['events', 'react', 'react-dom'],
18
20
  },
19
21
  target: 'web',
20
- mode: 'development',
22
+ mode: isProduction ? 'production' : 'development',
21
23
  stats: 'minimal',
22
24
  output: {
23
25
  path: path.resolve(__dirname, './dist'),