datajunction-ui 0.0.1-a44.dev5 → 0.0.1-a45.dev14

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.
@@ -0,0 +1,164 @@
1
+ import React from 'react';
2
+ import { render, waitFor, screen } from '@testing-library/react';
3
+ import RevisionDiff from '../RevisionDiff';
4
+ import DJClientContext from '../../../providers/djclient';
5
+ import { NodePage } from '../Loadable';
6
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
7
+
8
+ describe('<RevisionDiff />', () => {
9
+ const mockDjClient = {
10
+ DataJunctionAPI: {
11
+ revisions: jest.fn(),
12
+ },
13
+ };
14
+
15
+ const mockNodesWithDimension = [
16
+ {
17
+ node_revision_id: 1,
18
+ node_id: 1,
19
+ type: 'dimension',
20
+ name: 'default.repair_order',
21
+ display_name: 'Repair Orders',
22
+ version: 'v1.0',
23
+ status: 'valid',
24
+ mode: 'published',
25
+ catalog: {
26
+ id: 1,
27
+ uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488',
28
+ created_at: '2023-08-21T16:48:51.146121+00:00',
29
+ updated_at: '2023-08-21T16:48:51.146122+00:00',
30
+ extra_params: {},
31
+ name: 'warehouse',
32
+ },
33
+ description: 'Repair order dimension',
34
+ query:
35
+ 'SELECT repair_order_id, municipality_id, hard_hat_id, order_date, ' +
36
+ 'dispatcher_id FROM default.repair_orders',
37
+ availability: null,
38
+ columns: [
39
+ {
40
+ name: 'repair_order_id',
41
+ type: 'int',
42
+ attributes: [],
43
+ dimension: {
44
+ name: 'default.repair_order',
45
+ },
46
+ },
47
+ {
48
+ name: 'municipality_id',
49
+ type: 'int',
50
+ attributes: [],
51
+ dimension: null,
52
+ },
53
+ {
54
+ name: 'hard_hat_id',
55
+ type: 'int',
56
+ attributes: [],
57
+ dimension: null,
58
+ },
59
+ {
60
+ name: 'order_date',
61
+ type: 'date',
62
+ attributes: [],
63
+ dimension: null,
64
+ },
65
+ {
66
+ name: 'dispatcher_id',
67
+ type: 'int',
68
+ attributes: [],
69
+ dimension: null,
70
+ },
71
+ ],
72
+ updated_at: '2023-08-21T16:48:52.981201+00:00',
73
+ materializations: [],
74
+ parents: [],
75
+ },
76
+ {
77
+ node_revision_id: 2,
78
+ node_id: 2,
79
+ type: 'dimension',
80
+ name: 'default.repair_order',
81
+ display_name: 'Repair Orders',
82
+ version: 'v2.0',
83
+ status: 'valid',
84
+ mode: 'published',
85
+ catalog: {
86
+ id: 1,
87
+ uuid: '0fc18295-e1a2-4c3c-b72a-894725c12488',
88
+ created_at: '2023-08-21T16:48:51.146121+00:00',
89
+ updated_at: '2023-08-21T16:48:51.146122+00:00',
90
+ extra_params: {},
91
+ name: 'warehouse',
92
+ },
93
+ description: 'Repair order dimension',
94
+ query:
95
+ 'SELECT repair_order_id, municipality_id, hard_hat_id, ' +
96
+ 'dispatcher_id FROM default.repair_orders',
97
+ availability: null,
98
+ columns: [
99
+ {
100
+ name: 'repair_order_id',
101
+ type: 'int',
102
+ attributes: [],
103
+ dimension: {
104
+ name: 'default.repair_order',
105
+ },
106
+ },
107
+ {
108
+ name: 'municipality_id',
109
+ type: 'int',
110
+ attributes: [],
111
+ dimension: null,
112
+ },
113
+ {
114
+ name: 'hard_hat_id',
115
+ type: 'int',
116
+ attributes: [],
117
+ dimension: null,
118
+ },
119
+ {
120
+ name: 'dispatcher_id',
121
+ type: 'int',
122
+ attributes: [],
123
+ dimension: null,
124
+ },
125
+ ],
126
+ updated_at: '2023-08-21T16:48:52.981201+00:00',
127
+ materializations: [],
128
+ parents: [],
129
+ },
130
+ ];
131
+
132
+ beforeEach(() => {
133
+ // Reset the mocks before each test
134
+ mockDjClient.DataJunctionAPI.revisions.mockReset();
135
+ });
136
+
137
+ it('renders revision diff', async () => {
138
+ mockDjClient.DataJunctionAPI.revisions.mockReturnValue(
139
+ mockNodesWithDimension,
140
+ );
141
+ const element = (
142
+ <DJClientContext.Provider value={mockDjClient}>
143
+ <RevisionDiff />
144
+ </DJClientContext.Provider>
145
+ );
146
+ const { container } = render(
147
+ <MemoryRouter
148
+ initialEntries={['/nodes/default.repair_orders_cube/revisions/v2.0']}
149
+ >
150
+ <Routes>
151
+ <Route path="nodes/:name/revisions/:revision" element={element} />
152
+ </Routes>
153
+ </MemoryRouter>,
154
+ );
155
+ await waitFor(() => {
156
+ expect(mockDjClient.DataJunctionAPI.revisions).toHaveBeenCalledWith(
157
+ 'default.repair_orders_cube',
158
+ );
159
+
160
+ const diffViews = screen.getAllByRole('gridcell', 'DiffView');
161
+ diffViews.map(diffView => expect(diffView).toBeInTheDocument());
162
+ });
163
+ });
164
+ });
@@ -1,105 +1,5 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`<NodePage /> renders the NodeHistory tab correctly 1`] = `
4
- <table
5
- aria-label="Revisions"
6
- class="card-inner-table table"
7
- >
8
- <thead
9
- class="fs-7 fw-bold text-gray-400 border-bottom-0"
10
- >
11
- <tr>
12
- <th
13
- class="text-start"
14
- >
15
- Version
16
- </th>
17
- <th>
18
- Display Name
19
- </th>
20
- <th>
21
- Description
22
- </th>
23
- <th>
24
- Query
25
- </th>
26
- <th>
27
- Tags
28
- </th>
29
- </tr>
30
- </thead>
31
- <tbody>
32
- <tr>
33
- <td
34
- class="text-start"
35
- >
36
- <span
37
- class="badge node_type__source"
38
- >
39
- v1.0
40
- </span>
41
- </td>
42
- <td>
43
- Default: Avg Repair Price
44
- </td>
45
- <td>
46
- Average repair price
47
- </td>
48
- <td>
49
- <pre
50
- style="display: block; overflow-x: auto; padding: 2rem; background: rgb(238, 238, 238); color: black;"
51
- >
52
- <code
53
- class="language-sql"
54
- style="white-space: pre-wrap;"
55
- >
56
- <span>
57
- <span
58
- style="color: rgb(0, 153, 153);"
59
- >
60
- SELECT
61
- </span>
62
- <span>
63
-
64
- </span>
65
- <span
66
- class="hljs-built_in"
67
- >
68
- avg
69
- </span>
70
- <span>
71
- (price) default_DOT_avg_repair_price
72
-
73
- </span>
74
- </span>
75
- <span>
76
- <span>
77
-
78
- </span>
79
- <span
80
- style="color: rgb(0, 153, 153);"
81
- >
82
- FROM
83
- </span>
84
- <span>
85
- default.repair_order_details
86
-
87
- </span>
88
- </span>
89
- <span>
90
-
91
-
92
- </span>
93
- <span />
94
- </code>
95
- </pre>
96
- </td>
97
- <td />
98
- </tr>
99
- </tbody>
100
- </table>
101
- `;
102
-
103
3
  exports[`<NodePage /> renders the NodeInfo tab correctly for a metric node 1`] = `
104
4
  HTMLCollection [
105
5
  <code
@@ -58,6 +58,7 @@ export function NodePage() {
58
58
  data.required_dimensions = metric.required_dimensions;
59
59
  data.upstream_node = metric.upstream_node;
60
60
  data.expression = metric.expression;
61
+ data.incompatible_druid_functions = metric.incompatible_druid_functions;
61
62
  }
62
63
  if (data.type === 'cube') {
63
64
  const cube = await djClient.cube(name);
@@ -0,0 +1,31 @@
1
+ import QueryBuilder from 'react-querybuilder';
2
+ import { useState } from 'react';
3
+
4
+ export function QueryRunner() {
5
+ const [fields, setFields] = useState([]);
6
+ const [filters, setFilters] = useState({
7
+ combinator: 'and',
8
+ rules: [],
9
+ });
10
+
11
+ return (
12
+ <>
13
+ <h4>Test Query</h4>
14
+ <label>Add Filters</label>
15
+ <QueryBuilder
16
+ fields={fields}
17
+ query={filters}
18
+ onQueryChange={q => setFilters(q)}
19
+ />
20
+ <span
21
+ className="button-3 execute-button"
22
+ // onClick={getData}
23
+ role="button"
24
+ aria-label="RunQuery"
25
+ aria-hidden="false"
26
+ >
27
+ {'Run Query'}
28
+ </span>
29
+ </>
30
+ );
31
+ }
@@ -613,6 +613,51 @@ export const DataJunctionAPI = {
613
613
  );
614
614
  return { status: response.status, json: await response.json() };
615
615
  },
616
+
617
+ addComplexDimensionLink: async function (
618
+ nodeName,
619
+ dimensionNode,
620
+ joinOn,
621
+ joinType = null,
622
+ joinCardinality = null,
623
+ role = null,
624
+ ) {
625
+ const response = await fetch(`${DJ_URL}/nodes/${nodeName}/link`, {
626
+ method: 'POST',
627
+ headers: {
628
+ 'Content-Type': 'application/json',
629
+ },
630
+ body: JSON.stringify({
631
+ dimensionNode: dimensionNode,
632
+ joinType: joinType,
633
+ joinOn: joinOn,
634
+ joinCardinality: joinCardinality,
635
+ role: role,
636
+ }),
637
+ credentials: 'include',
638
+ });
639
+ return { status: response.status, json: await response.json() };
640
+ },
641
+
642
+ removeComplexDimensionLink: async function (
643
+ nodeName,
644
+ dimensionNode,
645
+ role = null,
646
+ ) {
647
+ const response = await fetch(`${DJ_URL}/nodes/${nodeName}/link`, {
648
+ method: 'DELETE',
649
+ headers: {
650
+ 'Content-Type': 'application/json',
651
+ },
652
+ body: JSON.stringify({
653
+ dimensionNode: dimensionNode,
654
+ role: role,
655
+ }),
656
+ credentials: 'include',
657
+ });
658
+ return { status: response.status, json: await response.json() };
659
+ },
660
+
616
661
  deactivate: async function (nodeName) {
617
662
  const response = await fetch(`${DJ_URL}/nodes/${nodeName}`, {
618
663
  method: 'DELETE',
@@ -34,6 +34,22 @@ describe('DataJunctionAPI', () => {
34
34
  });
35
35
  });
36
36
 
37
+ it('calls catalogs correctly', async () => {
38
+ fetch.mockResponseOnce(JSON.stringify({}));
39
+ await DataJunctionAPI.catalogs();
40
+ expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/catalogs`, {
41
+ credentials: 'include',
42
+ });
43
+ });
44
+
45
+ it('calls engines correctly', async () => {
46
+ fetch.mockResponseOnce(JSON.stringify({}));
47
+ await DataJunctionAPI.engines();
48
+ expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/engines`, {
49
+ credentials: 'include',
50
+ });
51
+ });
52
+
37
53
  it('calls node correctly', async () => {
38
54
  fetch.mockResponseOnce(JSON.stringify(mocks.mockMetricNode));
39
55
  const nodeData = await DataJunctionAPI.node(mocks.mockMetricNode.name);
@@ -49,6 +65,57 @@ describe('DataJunctionAPI', () => {
49
65
  expect(nodeData.primary_key).toEqual([]);
50
66
  });
51
67
 
68
+ it('calls nodeDetails correctly', async () => {
69
+ fetch.mockResponseOnce(JSON.stringify([mocks.mockMetricNode]));
70
+ const nodeData = await DataJunctionAPI.nodeDetails();
71
+ expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/details/`, {
72
+ credentials: 'include',
73
+ });
74
+ expect(nodeData).toEqual([mocks.mockMetricNode]);
75
+ });
76
+
77
+ it('calls validate correctly', async () => {
78
+ fetch.mockResponseOnce(JSON.stringify([mocks.mockMetricNode]));
79
+ await DataJunctionAPI.validateNode(
80
+ 'metric',
81
+ 'default.num_repair_orders',
82
+ 'aa',
83
+ 'desc',
84
+ 'select 1',
85
+ );
86
+ expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/validate`, {
87
+ credentials: 'include',
88
+ body: JSON.stringify({
89
+ name: 'default.num_repair_orders',
90
+ display_name: 'aa',
91
+ description: 'desc',
92
+ query: 'select 1',
93
+ type: 'metric',
94
+ mode: 'published',
95
+ }),
96
+
97
+ headers: {
98
+ 'Content-Type': 'application/json',
99
+ },
100
+ method: 'POST',
101
+ });
102
+ });
103
+
104
+ it('calls registerTable correctly', async () => {
105
+ fetch.mockResponseOnce(JSON.stringify([mocks.mockMetricNode]));
106
+ await DataJunctionAPI.registerTable('default', 'xyz', 'abc');
107
+ expect(fetch).toHaveBeenCalledWith(
108
+ `${DJ_URL}/register/table/default/xyz/abc`,
109
+ {
110
+ credentials: 'include',
111
+ headers: {
112
+ 'Content-Type': 'application/json',
113
+ },
114
+ method: 'POST',
115
+ },
116
+ );
117
+ });
118
+
52
119
  it('node with errors are handled', async () => {
53
120
  const mockNotFound = { message: 'node not found' };
54
121
  fetch.mockResponseOnce(JSON.stringify(mockNotFound));
@@ -656,6 +723,46 @@ describe('DataJunctionAPI', () => {
656
723
  );
657
724
  });
658
725
 
726
+ it('calls add and remove complex dimension link correctly', async () => {
727
+ const nodeName = 'default.transform1';
728
+ const dimensionNode = 'default.dimension1';
729
+ const joinOn = 'blah';
730
+ fetch.mockResponseOnce(JSON.stringify({}));
731
+ await DataJunctionAPI.removeComplexDimensionLink(nodeName, dimensionNode);
732
+ expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/${nodeName}/link`, {
733
+ credentials: 'include',
734
+ headers: {
735
+ 'Content-Type': 'application/json',
736
+ },
737
+ body: JSON.stringify({
738
+ dimensionNode: dimensionNode,
739
+ role: null,
740
+ }),
741
+ method: 'DELETE',
742
+ });
743
+
744
+ fetch.mockResponseOnce(JSON.stringify({}));
745
+ await DataJunctionAPI.addComplexDimensionLink(
746
+ nodeName,
747
+ dimensionNode,
748
+ joinOn,
749
+ );
750
+ expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/${nodeName}/link`, {
751
+ credentials: 'include',
752
+ headers: {
753
+ 'Content-Type': 'application/json',
754
+ },
755
+ body: JSON.stringify({
756
+ dimensionNode: dimensionNode,
757
+ joinType: null,
758
+ joinOn: joinOn,
759
+ joinCardinality: null,
760
+ role: null,
761
+ }),
762
+ method: 'POST',
763
+ });
764
+ });
765
+
659
766
  it('calls deactivate correctly', async () => {
660
767
  const nodeName = 'default.transform1';
661
768
  fetch.mockResponseOnce(JSON.stringify({}));
package/src/index.tsx CHANGED
@@ -15,6 +15,7 @@ import FontFaceObserver from 'fontfaceobserver';
15
15
  // Use consistent styling
16
16
  import 'sanitize.css/sanitize.css';
17
17
  import './styles/index.css';
18
+ import 'react-diff-view/style/index.css';
18
19
 
19
20
  import { App } from './app';
20
21
 
@@ -103,6 +103,7 @@ export const mocks = {
103
103
  created_at: '2023-08-21T16:48:56.841631+00:00',
104
104
  tags: [{ name: 'purpose', display_name: 'Purpose' }],
105
105
  dimension_links: [],
106
+ incompatible_druid_functions: ['IF'],
106
107
  dimensions: [
107
108
  {
108
109
  value: 'default.date_dim.dateint',
@@ -408,6 +409,53 @@ export const mocks = {
408
409
  },
409
410
  created_at: '2023-08-21T16:48:56.950482+00:00',
410
411
  },
412
+ {
413
+ id: 7,
414
+ entity_type: 'backfill',
415
+ entity_name: 'default.avg_repair_price',
416
+ node: 'default.avg_repair_price',
417
+ activity_type: 'create',
418
+ user: null,
419
+ pre: { status: 'valid' },
420
+ post: { status: 'invalid' },
421
+ details: {
422
+ materialization: 'druid_metrics_cube__incremental_time__xyz',
423
+ partition: {
424
+ column_name: 'xyz.abc',
425
+ values: null,
426
+ range: ['20240201', '20240301'],
427
+ },
428
+ },
429
+ created_at: '2023-08-21T16:48:56.950482+00:00',
430
+ },
431
+ {
432
+ id: 8,
433
+ entity_type: 'node',
434
+ entity_name: 'default.avg_repair_price',
435
+ node: 'default.avg_repair_price',
436
+ activity_type: 'tag',
437
+ user: null,
438
+ pre: {},
439
+ post: {},
440
+ details: {
441
+ tags: ['a', 'b'],
442
+ },
443
+ created_at: '2023-08-21T16:48:56.950482+00:00',
444
+ },
445
+ {
446
+ id: 9,
447
+ entity_type: 'link',
448
+ entity_name: 'default.avg_repair_price',
449
+ node: 'default.avg_repair_price',
450
+ activity_type: 'create',
451
+ user: null,
452
+ pre: {},
453
+ post: {},
454
+ details: {
455
+ dimension: 'abcde',
456
+ },
457
+ created_at: '2023-08-21T16:48:56.950482+00:00',
458
+ },
411
459
  ],
412
460
  nodeMaterializations: [
413
461
  {
@@ -1113,3 +1113,92 @@ pre {
1113
1113
  font-size: 100%;
1114
1114
  margin-bottom: 5px;
1115
1115
  }
1116
+
1117
+ .history {
1118
+ padding: 20px;
1119
+ list-style: none;
1120
+ border-top-left-radius: 6px;
1121
+ border-top-right-radius: 6px;
1122
+ border-bottom-width: 1px;
1123
+ border-bottom-style: solid;
1124
+ border-color: #748f7c75;
1125
+ display: grid;
1126
+
1127
+ position: relative;
1128
+ min-height: 2rem;
1129
+ font-size: 14px;
1130
+ grid-template-rows: repeat(3, auto);
1131
+ grid-template-areas:
1132
+ 'description description details'
1133
+ 'description description details'
1134
+ 'metadata metadata details';
1135
+ grid-template-columns: min-content minmax(30%, 1fr);
1136
+ gap: 4px;
1137
+ }
1138
+
1139
+ .history-border {
1140
+ display: block;
1141
+ list-style-type: disc;
1142
+ padding-inline-start: 0;
1143
+ unicode-bidi: isolate;
1144
+ border-radius: 6px !important;
1145
+ border: 1px solid #748f7c75;
1146
+ margin-block-start: 1em;
1147
+ margin-block-end: 1em;
1148
+ margin-inline-start: 0;
1149
+ margin-inline-end: 0;
1150
+ }
1151
+ .history-left {
1152
+ grid-area: description;
1153
+ -webkit-box-flex: 2;
1154
+ flex-grow: 2;
1155
+ display: inline;
1156
+ font-size: 16px;
1157
+ }
1158
+ .history-small {
1159
+ display: flex;
1160
+ flex-direction: row;
1161
+ -webkit-box-align: center;
1162
+ align-items: center;
1163
+ gap: 5px;
1164
+ padding-right: 8px;
1165
+ -webkit-box-flex: 1;
1166
+ flex-grow: 1;
1167
+ overflow: hidden;
1168
+ grid-area: metadata;
1169
+ font-size: 12px;
1170
+ }
1171
+ .history-right {
1172
+ display: flex;
1173
+ flex-flow: wrap;
1174
+ padding-top: 10px;
1175
+ gap: 19px;
1176
+ grid-area: details;
1177
+ -webkit-box-flex: 2;
1178
+ }
1179
+
1180
+ .version {
1181
+ color: #055d20;
1182
+ font-weight: bold;
1183
+ font-size: 13px;
1184
+ border: solid #055d20 1px;
1185
+ background-color: #ccf7e545;
1186
+ margin: 0.25rem;
1187
+ }
1188
+
1189
+ .highlight-svg:hover {
1190
+ filter: invert(56%) sepia(68%) saturate(4415%) hue-rotate(162deg)
1191
+ brightness(96%) contrast(101%);
1192
+ text-decoration: none;
1193
+ }
1194
+ .no-change {
1195
+ padding: 10px;
1196
+ display: inline-block;
1197
+ margin-top: -25px;
1198
+ }
1199
+
1200
+ .no-change-banner {
1201
+ color: #055d20;
1202
+ text-transform: uppercase;
1203
+ padding-left: 10px;
1204
+ }
@@ -254,6 +254,15 @@ form {
254
254
  }
255
255
  }
256
256
 
257
+ .warning {
258
+ background-color: rgb(253, 248, 242);
259
+ color: rgb(190, 105, 37);
260
+ svg {
261
+ filter: invert(16%) sepia(68%) saturate(2827%) hue-rotate(344deg)
262
+ brightness(96%) contrast(100%);
263
+ }
264
+ }
265
+
257
266
  .SourceCreationInput {
258
267
  margin: 0.5rem 0;
259
268
  display: inline-grid;