datajunction-ui 0.0.1-a46.dev3 → 0.0.1-a47

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.
@@ -1,9 +1,10 @@
1
1
  import { useEffect, useState } from 'react';
2
- import ClientCodePopover from './ClientCodePopover';
3
2
  import TableIcon from '../../icons/TableIcon';
4
3
  import AddMaterializationPopover from './AddMaterializationPopover';
5
4
  import * as React from 'react';
6
5
  import AddBackfillPopover from './AddBackfillPopover';
6
+ import { labelize } from '../../../utils/form';
7
+ import NodeMaterializationDelete from '../../components/NodeMaterializationDelete';
7
8
 
8
9
  const cronstrue = require('cronstrue');
9
10
 
@@ -18,17 +19,6 @@ export default function NodeMaterializationTab({ node, djClient }) {
18
19
  };
19
20
  fetchData().catch(console.error);
20
21
  }, [djClient, node]);
21
- //
22
- // const rangePartition = partition => {
23
- // return (
24
- // <div>
25
- // <span className="badge partition_value">
26
- // <span className="badge partition_value">{partition.range[0]}</span>to
27
- // <span className="badge partition_value">{partition.range[1]}</span>
28
- // </span>
29
- // </div>
30
- // );
31
- // };
32
22
 
33
23
  const partitionColumnsMap = node
34
24
  ? Object.fromEntries(
@@ -47,116 +37,195 @@ export default function NodeMaterializationTab({ node, djClient }) {
47
37
 
48
38
  const materializationRows = materializations => {
49
39
  return materializations.map(materialization => (
50
- <tr key={materialization.name}>
51
- <td className="text-start node_name">
52
- <span className={`badge cron`}>{materialization.schedule}</span>
53
- <div className={`cron-description`}>{cron(materialization)} </div>
54
- </td>
55
- <td>
56
- {materialization.job?.replace('MaterializationJob', '').toUpperCase()}
57
- </td>
58
- <td>{materialization.strategy?.toUpperCase()}</td>
59
- <td>
60
- {node.columns
61
- .filter(col => col.partition !== null)
62
- .map(column => {
63
- return (
64
- <div className="partition__full" key={column.name}>
65
- <div className="partition__header">{column.display_name}</div>
66
- <div className="partition__body">
67
- <code>{column.name}</code>
68
- <span className="badge partition_value">
69
- {column.partition.type_}
70
- </span>
71
- </div>
72
- </div>
73
- );
74
- })}
75
- </td>
76
- <td>
77
- {materialization.output_tables.map(table => (
78
- <div className={`table__full`} key={table}>
79
- <div className="table__header">
80
- <TableIcon />{' '}
81
- <span className={`entity-info`}>
82
- {table.split('.')[0] + '.' + table.split('.')[1]}
83
- </span>
84
- </div>
85
- <div className={`table__body upstream_tables`}>
86
- {table.split('.')[2]}
87
- </div>
40
+ <>
41
+ <div className="tr">
42
+ <div key={materialization.name} style={{ fontSize: 'large' }}>
43
+ <div
44
+ className="text-start node_name td"
45
+ style={{ fontWeight: '600' }}
46
+ >
47
+ {materialization.job
48
+ ?.replace('MaterializationJob', '')
49
+ .match(/[A-Z][a-z]+/g)
50
+ .join(' ')}
88
51
  </div>
89
- ))}
90
- </td>
91
- {materializations[0].strategy === 'incremental_time' ? (
92
- <td>
93
- {materialization.backfills.map(backfill => (
94
- <a href={backfill.urls[0]} className="partitionLink">
95
- <div
96
- className="partition__full"
97
- key={backfill.spec.column_name}
98
- >
99
- <div className="partition__header">
100
- {partitionColumnsMap[backfill.spec.column_name]}
101
- </div>
102
- <div className="partition__body">
103
- <span className="badge partition_value">
104
- {backfill.spec.range[0]}
105
- </span>
106
- to
107
- <span className="badge partition_value">
108
- {backfill.spec.range[1]}
109
- </span>
52
+ <div className="td">
53
+ <NodeMaterializationDelete
54
+ nodeName={node.name}
55
+ materializationName={materialization.name}
56
+ />
57
+ </div>
58
+ <div className="td">
59
+ <span className={`badge cron`}>{materialization.schedule}</span>
60
+ <div className={`cron-description`}>{cron(materialization)} </div>
61
+ </div>
62
+ <div className="td">
63
+ <span className={`badge strategy`}>
64
+ {labelize(materialization.strategy)}
65
+ </span>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ <div style={{ display: 'table-row' }}>
70
+ <div style={{ display: 'inline-flex' }}>
71
+ <ul className="backfills">
72
+ <li className="backfill">
73
+ <div className="backfills_header">Output Tables</div>{' '}
74
+ {materialization.output_tables.map(table => (
75
+ <div className={`table__full`} key={table}>
76
+ <div className="table__header">
77
+ <TableIcon />{' '}
78
+ <span className={`entity-info`}>
79
+ {table.split('.')[0] + '.' + table.split('.')[1]}
80
+ </span>
81
+ </div>
82
+ <div className={`table__body upstream_tables`}>
83
+ {table.split('.')[2]}
84
+ </div>
110
85
  </div>
111
- </div>
112
- </a>
113
- ))}
114
- <AddBackfillPopover node={node} materialization={materialization} />
115
- </td>
116
- ) : (
117
- <></>
118
- )}
119
- <td>
120
- {materialization.urls.map((url, idx) => (
121
- <a href={url} key={`url-${idx}`}>
122
- [{idx + 1}]
123
- </a>
124
- ))}
125
- </td>
126
- <td>
127
- <ClientCodePopover code={materialization.clientCode} />
128
- </td>
129
- </tr>
86
+ ))}
87
+ </li>
88
+ </ul>
89
+ </div>
90
+
91
+ <div style={{ display: 'inline-flex' }}>
92
+ <ul className="backfills">
93
+ <li>
94
+ <div className="backfills_header">Workflows</div>{' '}
95
+ <ul>
96
+ {materialization.urls.map((url, idx) => (
97
+ <li style={{ listStyle: 'none' }} key={idx}>
98
+ <div
99
+ className="partitionLink"
100
+ style={{ fontSize: 'revert' }}
101
+ >
102
+ <a
103
+ href={url}
104
+ key={`url-${idx}`}
105
+ className=""
106
+ target="blank"
107
+ >
108
+ {idx === 0 ? 'main' : 'backfill'}
109
+ </a>
110
+ </div>
111
+ </li>
112
+ ))}
113
+ </ul>
114
+ </li>
115
+ </ul>
116
+ </div>
117
+
118
+ <div style={{ display: 'inline-flex' }}>
119
+ <ul className="backfills">
120
+ <li className="backfill">
121
+ <details open>
122
+ <summary>
123
+ <span className="backfills_header">Backfills</span>{' '}
124
+ </summary>
125
+ {materialization.strategy === 'incremental_time' ? (
126
+ <ul>
127
+ <li>
128
+ <AddBackfillPopover
129
+ node={node}
130
+ materialization={materialization}
131
+ />
132
+ </li>
133
+ {materialization.backfills.map(backfill => (
134
+ <li className="backfill">
135
+ <div className="partitionLink">
136
+ <a href={backfill.urls[0]}>
137
+ {backfill.spec.map(partition => {
138
+ const partitionBody =
139
+ 'range' in partition &&
140
+ partition['range'] !== null ? (
141
+ <>
142
+ <span className="badge partition_value">
143
+ {partition.range[0]}
144
+ </span>
145
+ to
146
+ <span className="badge partition_value">
147
+ {partition.range[1]}
148
+ </span>
149
+ </>
150
+ ) : (
151
+ <span className="badge partition_value">
152
+ {partition.values.join(', ')}
153
+ </span>
154
+ );
155
+ return (
156
+ <>
157
+ <div>
158
+ {
159
+ partitionColumnsMap[
160
+ partition.column_name.replaceAll(
161
+ '_DOT_',
162
+ '.',
163
+ )
164
+ ]
165
+ }{' '}
166
+ {partitionBody}
167
+ </div>
168
+ </>
169
+ );
170
+ })}
171
+ </a>
172
+ </div>
173
+ </li>
174
+ ))}
175
+ </ul>
176
+ ) : (
177
+ <ul>
178
+ <li>N/A</li>
179
+ </ul>
180
+ )}
181
+ </details>
182
+ </li>
183
+ </ul>
184
+ </div>
185
+ <div className="td">
186
+ <ul className="backfills">
187
+ <li className="backfill">
188
+ <div className="backfills_header">Partitions</div>{' '}
189
+ <ul>
190
+ {node.columns
191
+ .filter(col => col.partition !== null)
192
+ .map(column => {
193
+ return (
194
+ <li>
195
+ <div className="partitionLink">
196
+ {column.display_name}
197
+ <span className="badge partition_value">
198
+ {column.partition.type_}
199
+ </span>
200
+ </div>
201
+ </li>
202
+ );
203
+ })}
204
+ </ul>
205
+ </li>
206
+ </ul>
207
+ </div>
208
+ </div>
209
+ </>
130
210
  ));
131
211
  };
132
212
  return (
133
213
  <>
134
- <div className="table-vertical">
214
+ <div
215
+ className="table-vertical"
216
+ role="table"
217
+ aria-label="Materializations"
218
+ >
135
219
  <div>
136
220
  <h2>Materializations</h2>
137
221
  {node ? <AddMaterializationPopover node={node} /> : <></>}
138
222
  {materializations.length > 0 ? (
139
- <table
223
+ <div
140
224
  className="card-inner-table table"
141
225
  aria-label="Materializations"
142
226
  aria-hidden="false"
143
227
  >
144
- <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
145
- <tr>
146
- <th className="text-start">Schedule</th>
147
- <th>Job Type</th>
148
- <th>Strategy</th>
149
- <th>Partitions</th>
150
- <th>Intended Output Tables</th>
151
- {materializations[0].strategy === 'incremental_time' ? (
152
- <th>Backfills</th>
153
- ) : (
154
- <></>
155
- )}
156
- <th>URLs</th>
157
- </tr>
158
- </thead>
159
- <tbody>
228
+ <div style={{ display: 'table' }}>
160
229
  {materializationRows(
161
230
  materializations.filter(
162
231
  materialization =>
@@ -166,8 +235,8 @@ export default function NodeMaterializationTab({ node, djClient }) {
166
235
  ),
167
236
  ),
168
237
  )}
169
- </tbody>
170
- </table>
238
+ </div>
239
+ </div>
171
240
  ) : (
172
241
  <div className="message alert" style={{ marginTop: '10px' }}>
173
242
  No materialization workflows configured for this node.
@@ -0,0 +1,60 @@
1
+ import * as React from 'react';
2
+ import { Field } from 'formik';
3
+
4
+ export default function PartitionValueForm({ col, materialization }) {
5
+ if (col.partition.type_ === 'temporal') {
6
+ return (
7
+ <>
8
+ <div
9
+ className="partition__full"
10
+ key={col.name}
11
+ style={{ width: '50%' }}
12
+ >
13
+ <div className="partition__header">{col.display_name}</div>
14
+ <div className="partition__body">
15
+ <span style={{ padding: '0.5rem' }}>From</span>{' '}
16
+ <Field
17
+ type="text"
18
+ name={`partitionValues.['${col.name}'].from`}
19
+ id={`${col.name}.from`}
20
+ placeholder="20230101"
21
+ default="20230101"
22
+ style={{ width: '7rem', paddingRight: '1rem' }}
23
+ />{' '}
24
+ <span style={{ padding: '0.5rem' }}>To</span>
25
+ <Field
26
+ type="text"
27
+ name={`partitionValues.['${col.name}'].to`}
28
+ id={`${col.name}.to`}
29
+ placeholder="20230102"
30
+ default="20230102"
31
+ style={{ width: '7rem' }}
32
+ />
33
+ </div>
34
+ </div>
35
+ </>
36
+ );
37
+ } else {
38
+ return (
39
+ <>
40
+ <div
41
+ className="partition__full"
42
+ key={col.name}
43
+ style={{ width: '50%' }}
44
+ >
45
+ <div className="partition__header">{col.display_name}</div>
46
+ <div className="partition__body">
47
+ <Field
48
+ type="text"
49
+ name={`partitionValues.['${col.name}']`}
50
+ id={col.name}
51
+ placeholder=""
52
+ default=""
53
+ style={{ width: '7rem', paddingRight: '1rem' }}
54
+ />
55
+ </div>
56
+ </div>
57
+ </>
58
+ );
59
+ }
60
+ }
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
- import { render, fireEvent, waitFor, screen } from '@testing-library/react';
3
- import EditColumnPopover from '../EditColumnPopover';
2
+ import { render, fireEvent, waitFor } from '@testing-library/react';
4
3
  import DJClientContext from '../../../providers/djclient';
5
4
  import AddBackfillPopover from '../AddBackfillPopover';
6
5
  import { mocks } from '../../../../mocks/mockNodes';
@@ -11,6 +10,17 @@ const mockDjClient = {
11
10
  },
12
11
  };
13
12
 
13
+ let reloadMock = jest.fn();
14
+
15
+ beforeEach(() => {
16
+ delete window.location;
17
+ window.location = { reload: reloadMock };
18
+ });
19
+
20
+ afterEach(() => {
21
+ reloadMock.mockClear();
22
+ });
23
+
14
24
  describe('<AddBackfillPopover />', () => {
15
25
  it('renders correctly and handles form submission', async () => {
16
26
  // Mock onSubmit function
@@ -25,7 +35,7 @@ describe('<AddBackfillPopover />', () => {
25
35
  const { getByLabelText, getByText } = render(
26
36
  <DJClientContext.Provider value={mockDjClient}>
27
37
  <AddBackfillPopover
28
- node={mocks.mockMetricNode}
38
+ node={mocks.mockTransformNode}
29
39
  materialization={mocks.nodeMaterializations}
30
40
  onSubmit={onSubmitMock}
31
41
  />
@@ -29,6 +29,7 @@ describe('<NodePage />', () => {
29
29
  history: jest.fn(),
30
30
  revisions: jest.fn(),
31
31
  materializations: jest.fn(),
32
+ materializationInfo: jest.fn(),
32
33
  sql: jest.fn(),
33
34
  cube: jest.fn(),
34
35
  compiledSql: jest.fn(),
@@ -604,13 +605,17 @@ describe('<NodePage />', () => {
604
605
 
605
606
  it('renders the NodeMaterialization tab with materializations correctly', async () => {
606
607
  const djClient = mockDJClient();
607
- djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
608
- djClient.DataJunctionAPI.metric.mockReturnValue(mocks.mockMetricNode);
608
+ djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockTransformNode);
609
+ // djClient.DataJunctionAPI.metric.mockReturnValue(mocks.mockMetricNode);
609
610
  djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns);
610
611
  djClient.DataJunctionAPI.materializations.mockReturnValue(
611
612
  mocks.nodeMaterializations,
612
613
  );
613
614
 
615
+ djClient.DataJunctionAPI.materializationInfo.mockReturnValue(
616
+ mocks.materializationInfo,
617
+ );
618
+
614
619
  const element = (
615
620
  <DJClientContext.Provider value={djClient}>
616
621
  <NodePage />
@@ -618,7 +623,9 @@ describe('<NodePage />', () => {
618
623
  );
619
624
  render(
620
625
  <MemoryRouter
621
- initialEntries={['/nodes/default.num_repair_orders/materializations']}
626
+ initialEntries={[
627
+ '/nodes/default.repair_order_transform/materializations',
628
+ ]}
622
629
  >
623
630
  <Routes>
624
631
  <Route path="nodes/:name/:tab" element={element} />
@@ -631,10 +638,10 @@ describe('<NodePage />', () => {
631
638
  screen.getByRole('button', { name: 'Materializations' }),
632
639
  );
633
640
  expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith(
634
- mocks.mockMetricNode.name,
641
+ mocks.mockTransformNode.name,
635
642
  );
636
643
  expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith(
637
- mocks.mockMetricNode.name,
644
+ mocks.mockTransformNode.name,
638
645
  );
639
646
 
640
647
  expect(