datajunction-ui 0.0.1-a45.dev5 → 0.0.1-a46

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.
@@ -1,28 +1,101 @@
1
- import { Component } from 'react';
1
+ import { Component, useContext, useEffect, useRef, useState } from 'react';
2
2
  import ValidIcon from '../../icons/ValidIcon';
3
3
  import InvalidIcon from '../../icons/InvalidIcon';
4
+ import DJClientContext from '../../providers/djclient';
5
+ import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
6
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
7
+ import markdown from 'react-syntax-highlighter/dist/cjs/languages/hljs/markdown';
8
+ import * as React from 'react';
9
+ import { AlertMessage } from '../AddEditNodePage/AlertMessage';
10
+ import AlertIcon from '../../icons/AlertIcon';
11
+ import { labelize } from '../../../utils/form';
4
12
 
5
- export default class NodeStatus extends Component {
6
- render() {
7
- const { node } = this.props;
8
- return (
13
+ SyntaxHighlighter.registerLanguage('markdown', markdown);
14
+
15
+ export default function NodeStatus({ node, revalidate = true }) {
16
+ const MAX_ERROR_LENGTH = 200;
17
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
18
+ const [validation, setValidation] = useState([]);
19
+
20
+ const [codeAnchor, setCodeAnchor] = useState(false);
21
+ const ref = useRef(null);
22
+
23
+ useEffect(() => {
24
+ if (revalidate) {
25
+ const fetchData = async () => {
26
+ setValidation(await djClient.revalidate(node.name));
27
+ };
28
+ fetchData().catch(console.error);
29
+ }
30
+ }, [djClient, node, revalidate]);
31
+
32
+ const displayValidation =
33
+ revalidate && validation?.errors?.length > 0 ? (
9
34
  <>
10
- {node?.status === 'valid' ? (
11
- <span
12
- className="status__valid status"
13
- style={{ alignContent: 'center' }}
14
- >
15
- <ValidIcon />
16
- </span>
17
- ) : (
18
- <span
19
- className="status__invalid status"
20
- style={{ alignContent: 'center' }}
21
- >
22
- <InvalidIcon />
23
- </span>
24
- )}
35
+ <button
36
+ className="badge"
37
+ style={{
38
+ backgroundColor: '#b34b00',
39
+ fontSize: '15px',
40
+ outline: '0',
41
+ border: '0',
42
+ cursor: 'pointer',
43
+ }}
44
+ onClick={() => setCodeAnchor(!codeAnchor)}
45
+ >
46
+ ⚠ {validation?.errors?.length} error
47
+ {validation?.errors?.length > 1 ? 's' : ''}
48
+ </button>
49
+ <div
50
+ className="popover"
51
+ ref={ref}
52
+ style={{
53
+ display: codeAnchor === false ? 'none' : 'block',
54
+ border: 'none',
55
+ paddingTop: '0px !important',
56
+ marginTop: '0px',
57
+ backgroundColor: 'transparent',
58
+ }}
59
+ >
60
+ {validation?.errors?.map((error, idx) => (
61
+ <div className="validation_error">
62
+ <b
63
+ style={{
64
+ color: '#b34b00',
65
+ }}
66
+ >
67
+ {labelize(error.type.toLowerCase())}:
68
+ </b>{' '}
69
+ {error.message.length > MAX_ERROR_LENGTH
70
+ ? error.message.slice(0, MAX_ERROR_LENGTH - 1) + '...'
71
+ : error.message}
72
+ </div>
73
+ ))}
74
+ </div>
25
75
  </>
76
+ ) : (
77
+ <></>
26
78
  );
27
- }
79
+
80
+ return (
81
+ <>
82
+ {revalidate && validation?.errors?.length > 0 ? (
83
+ displayValidation
84
+ ) : validation?.status === 'valid' || node?.status === 'valid' ? (
85
+ <span
86
+ className="status__valid status"
87
+ style={{ alignContent: 'center' }}
88
+ >
89
+ <ValidIcon />
90
+ </span>
91
+ ) : (
92
+ <span
93
+ className="status__invalid status"
94
+ style={{ alignContent: 'center' }}
95
+ >
96
+ <InvalidIcon />
97
+ </span>
98
+ )}
99
+ </>
100
+ );
28
101
  }
@@ -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
+ }
@@ -149,7 +149,7 @@ export default function RevisionDiff() {
149
149
  </div>
150
150
  {Object.keys(diffObjects).map(field => {
151
151
  return (
152
- <div className="diff">
152
+ <div className="diff" aria-label={'DiffView'} role={'gridcell'}>
153
153
  <h4>
154
154
  {labelize(field)}{' '}
155
155
  <small className="no-change-banner">
@@ -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(),
@@ -88,6 +89,7 @@ describe('<NodePage />', () => {
88
89
  created_at: '2023-08-21T16:48:56.932162+00:00',
89
90
  tags: [{ name: 'purpose', display_name: 'Purpose' }],
90
91
  primary_key: [],
92
+ incompatible_druid_functions: ['IF'],
91
93
  createNodeClientCode:
92
94
  'dj = DJBuilder(DJ_URL)\n\navg_repair_price = dj.create_metric(\n description="Average repair price",\n display_name="Default: Avg Repair Price",\n name="default.avg_repair_price",\n primary_key=[],\n query="""SELECT avg(price) default_DOT_avg_repair_price \n FROM default.repair_order_details\n\n"""\n)',
93
95
  dimensions: [
@@ -603,13 +605,17 @@ describe('<NodePage />', () => {
603
605
 
604
606
  it('renders the NodeMaterialization tab with materializations correctly', async () => {
605
607
  const djClient = mockDJClient();
606
- djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
607
- djClient.DataJunctionAPI.metric.mockReturnValue(mocks.mockMetricNode);
608
+ djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockTransformNode);
609
+ // djClient.DataJunctionAPI.metric.mockReturnValue(mocks.mockMetricNode);
608
610
  djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns);
609
611
  djClient.DataJunctionAPI.materializations.mockReturnValue(
610
612
  mocks.nodeMaterializations,
611
613
  );
612
614
 
615
+ djClient.DataJunctionAPI.materializationInfo.mockReturnValue(
616
+ mocks.materializationInfo,
617
+ );
618
+
613
619
  const element = (
614
620
  <DJClientContext.Provider value={djClient}>
615
621
  <NodePage />
@@ -617,7 +623,9 @@ describe('<NodePage />', () => {
617
623
  );
618
624
  render(
619
625
  <MemoryRouter
620
- initialEntries={['/nodes/default.num_repair_orders/materializations']}
626
+ initialEntries={[
627
+ '/nodes/default.repair_order_transform/materializations',
628
+ ]}
621
629
  >
622
630
  <Routes>
623
631
  <Route path="nodes/:name/:tab" element={element} />
@@ -630,10 +638,10 @@ describe('<NodePage />', () => {
630
638
  screen.getByRole('button', { name: 'Materializations' }),
631
639
  );
632
640
  expect(djClient.DataJunctionAPI.node).toHaveBeenCalledWith(
633
- mocks.mockMetricNode.name,
641
+ mocks.mockTransformNode.name,
634
642
  );
635
643
  expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith(
636
- mocks.mockMetricNode.name,
644
+ mocks.mockTransformNode.name,
637
645
  );
638
646
 
639
647
  expect(