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

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-a45.dev5",
3
+ "version": "0.0.1a46.dev1",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Query tester component
3
+ */
4
+ import { ErrorMessage, useFormikContext } from 'formik';
5
+ import React, { useContext, useEffect, useState } from 'react';
6
+ import DJClientContext from '../../providers/djclient';
7
+ import { FormikSelect } from './FormikSelect';
8
+ import QueryBuilder from 'react-querybuilder';
9
+
10
+ export const QueryTesterSection = ({}) => {
11
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
12
+
13
+ const [fields, setFields] = useState([]);
14
+ const [filters, setFilters] = useState({
15
+ combinator: 'and',
16
+ rules: [],
17
+ });
18
+
19
+ // Used to pull out current form values for node validation
20
+ const { values } = useFormikContext();
21
+
22
+ // Select options, i.e., the available dimensions
23
+ const [selectOptions, setSelectOptions] = useState([]);
24
+
25
+ useEffect(() => {
26
+ const fetchData = async () => {
27
+ if (values.query) {
28
+ const data = await djClient.node(values.upstream_node);
29
+ setSelectOptions(
30
+ data.columns.map(col => {
31
+ return {
32
+ value: col.name,
33
+ label: col.name,
34
+ };
35
+ }),
36
+ );
37
+ }
38
+ };
39
+ fetchData().catch(console.error);
40
+ }, [djClient, values.upstream_node]);
41
+
42
+ return (
43
+ <>
44
+ <h4>Test Query</h4>
45
+ <label>Add Filters</label>
46
+ <QueryBuilder
47
+ fields={fields}
48
+ query={filters}
49
+ onQueryChange={q => setFilters(q)}
50
+ />
51
+ <span
52
+ className="button-3 execute-button"
53
+ // onClick={getData}
54
+ role="button"
55
+ aria-label="RunQuery"
56
+ aria-hidden="false"
57
+ >
58
+ {'Run Query'}
59
+ </span>
60
+ </>
61
+ );
62
+ };
@@ -94,7 +94,7 @@ export function NamespacePage() {
94
94
  </span>
95
95
  </td>
96
96
  <td>
97
- <NodeStatus node={node} />
97
+ <NodeStatus node={node} revalidate={false} />
98
98
  </td>
99
99
  <td>
100
100
  <span className="status">{node.mode}</span>
@@ -11,7 +11,7 @@ export default function NodeHistory({ node, djClient }) {
11
11
  const fetchData = async () => {
12
12
  if (node) {
13
13
  const data = await djClient.history('node', node.name);
14
- setHistory(data.reverse());
14
+ setHistory(data);
15
15
  }
16
16
  };
17
17
  fetchData().catch(console.error);
@@ -7,6 +7,9 @@ import ListGroupItem from '../../components/ListGroupItem';
7
7
  import ToggleSwitch from '../../components/ToggleSwitch';
8
8
  import DJClientContext from '../../providers/djclient';
9
9
  import { labelize } from '../../../utils/form';
10
+ import { AlertMessage } from '../AddEditNodePage/AlertMessage';
11
+ import AlertIcon from '../../icons/AlertIcon';
12
+ import InvalidIcon from '../../icons/InvalidIcon';
10
13
 
11
14
  SyntaxHighlighter.registerLanguage('sql', sql);
12
15
  foundation.hljs['padding'] = '2rem';
@@ -39,6 +42,35 @@ export default function NodeInfoTab({ node }) {
39
42
  function toggle(value) {
40
43
  return !value;
41
44
  }
45
+ const metricsWarning =
46
+ node?.type === 'metric' && node?.incompatible_druid_functions.length > 0 ? (
47
+ <div className="message warning" style={{ marginTop: '0.7rem' }}>
48
+ ⚠{' '}
49
+ <small>
50
+ The following functions used in the metric definition may not be
51
+ compatible with Druid SQL:{' '}
52
+ {node?.incompatible_druid_functions.map(func => (
53
+ <li style={{ listStyleType: 'none', margin: '0.7rem 0.7rem' }}>
54
+ ⇢{' '}
55
+ <span style={{ background: '#fff', padding: '0.3rem' }}>
56
+ {func}
57
+ </span>
58
+ </li>
59
+ ))}
60
+ If you need your metrics to be compatible with Druid, please use{' '}
61
+ <a
62
+ href={
63
+ 'https://druid.apache.org/docs/latest/querying/sql-functions/'
64
+ }
65
+ >
66
+ these supported functions
67
+ </a>
68
+ .
69
+ </small>
70
+ </div>
71
+ ) : (
72
+ ''
73
+ );
42
74
  const metricQueryDiv = (
43
75
  <div className="list-group-item d-flex">
44
76
  <div className="gap-2 w-100 justify-content-between py-3">
@@ -205,6 +237,7 @@ export default function NodeInfoTab({ node }) {
205
237
  className="list-group align-items-center justify-content-between flex-md-row gap-2"
206
238
  style={{ minWidth: '700px' }}
207
239
  >
240
+ {node?.type === 'metric' ? metricsWarning : ''}
208
241
  <ListGroupItem label="Description" value={node?.description} />
209
242
  <div className="list-group-item d-flex">
210
243
  <div className="d-flex gap-2 w-100 justify-content-between py-3">
@@ -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,63 @@
1
+ import { useContext, useEffect, useRef, useState } from 'react';
2
+ import * as React from 'react';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { Field, Form, Formik } from 'formik';
5
+ import { displayMessageAfterSubmit } from '../../../utils/form';
6
+
7
+ export default function PartitionValueForm({ col, materialization }) {
8
+ if (col.partition.type_ === 'temporal') {
9
+ return (
10
+ <>
11
+ <div
12
+ className="partition__full"
13
+ key={col.name}
14
+ style={{ width: '50%' }}
15
+ >
16
+ <div className="partition__header">{col.display_name}</div>
17
+ <div className="partition__body">
18
+ <span style={{ padding: '0.5rem' }}>From</span>{' '}
19
+ <Field
20
+ type="text"
21
+ name={`partitionValues.['${col.name}'].from`}
22
+ id={`${col.name}.from`}
23
+ placeholder="20230101"
24
+ default="20230101"
25
+ style={{ width: '7rem', paddingRight: '1rem' }}
26
+ />{' '}
27
+ <span style={{ padding: '0.5rem' }}>To</span>
28
+ <Field
29
+ type="text"
30
+ name={`partitionValues.['${col.name}'].to`}
31
+ id={`${col.name}.to`}
32
+ placeholder="20230102"
33
+ default="20230102"
34
+ style={{ width: '7rem' }}
35
+ />
36
+ </div>
37
+ </div>
38
+ </>
39
+ );
40
+ } else {
41
+ return (
42
+ <>
43
+ <div
44
+ className="partition__full"
45
+ key={col.name}
46
+ style={{ width: '50%' }}
47
+ >
48
+ <div className="partition__header">{col.display_name}</div>
49
+ <div className="partition__body">
50
+ <Field
51
+ type="text"
52
+ name={`partitionValues.['${col.name}']`}
53
+ id={col.name}
54
+ placeholder=""
55
+ default=""
56
+ style={{ width: '7rem', paddingRight: '1rem' }}
57
+ />
58
+ </div>
59
+ </div>
60
+ </>
61
+ );
62
+ }
63
+ }
@@ -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">
@@ -88,6 +88,7 @@ describe('<NodePage />', () => {
88
88
  created_at: '2023-08-21T16:48:56.932162+00:00',
89
89
  tags: [{ name: 'purpose', display_name: 'Purpose' }],
90
90
  primary_key: [],
91
+ incompatible_druid_functions: ['IF'],
91
92
  createNodeClientCode:
92
93
  '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
94
  dimensions: [
@@ -14,11 +14,11 @@ describe('<RevisionDiff />', () => {
14
14
 
15
15
  const mockNodesWithDimension = [
16
16
  {
17
- node_revision_id: 2,
18
- node_id: 2,
19
- type: 'source',
20
- name: 'default.repair_order_details',
21
- display_name: 'Default: Repair Order Details',
17
+ node_revision_id: 1,
18
+ node_id: 1,
19
+ type: 'dimension',
20
+ name: 'default.repair_order',
21
+ display_name: 'Repair Orders',
22
22
  version: 'v1.0',
23
23
  status: 'valid',
24
24
  mode: 'published',
@@ -30,10 +30,10 @@ describe('<RevisionDiff />', () => {
30
30
  extra_params: {},
31
31
  name: 'warehouse',
32
32
  },
33
- schema_: 'roads',
34
- table: 'repair_order_details',
35
- description: 'Details on repair orders',
36
- query: null,
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
37
  availability: null,
38
38
  columns: [
39
39
  {
@@ -45,26 +45,26 @@ describe('<RevisionDiff />', () => {
45
45
  },
46
46
  },
47
47
  {
48
- name: 'repair_type_id',
48
+ name: 'municipality_id',
49
49
  type: 'int',
50
50
  attributes: [],
51
51
  dimension: null,
52
52
  },
53
53
  {
54
- name: 'price',
55
- type: 'float',
54
+ name: 'hard_hat_id',
55
+ type: 'int',
56
56
  attributes: [],
57
57
  dimension: null,
58
58
  },
59
59
  {
60
- name: 'quantity',
61
- type: 'int',
60
+ name: 'order_date',
61
+ type: 'date',
62
62
  attributes: [],
63
63
  dimension: null,
64
64
  },
65
65
  {
66
- name: 'discount',
67
- type: 'float',
66
+ name: 'dispatcher_id',
67
+ type: 'int',
68
68
  attributes: [],
69
69
  dimension: null,
70
70
  },
@@ -74,12 +74,12 @@ describe('<RevisionDiff />', () => {
74
74
  parents: [],
75
75
  },
76
76
  {
77
- node_revision_id: 1,
78
- node_id: 1,
79
- type: 'source',
80
- name: 'default.repair_orders',
81
- display_name: 'Default: Repair Orders',
82
- version: 'v1.0',
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
83
  status: 'valid',
84
84
  mode: 'published',
85
85
  catalog: {
@@ -90,10 +90,10 @@ describe('<RevisionDiff />', () => {
90
90
  extra_params: {},
91
91
  name: 'warehouse',
92
92
  },
93
- schema_: 'roads',
94
- table: 'repair_orders',
95
- description: 'Repair orders',
96
- query: null,
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
97
  availability: null,
98
98
  columns: [
99
99
  {
@@ -106,7 +106,7 @@ describe('<RevisionDiff />', () => {
106
106
  },
107
107
  {
108
108
  name: 'municipality_id',
109
- type: 'string',
109
+ type: 'int',
110
110
  attributes: [],
111
111
  dimension: null,
112
112
  },
@@ -116,24 +116,6 @@ describe('<RevisionDiff />', () => {
116
116
  attributes: [],
117
117
  dimension: null,
118
118
  },
119
- {
120
- name: 'order_date',
121
- type: 'date',
122
- attributes: [],
123
- dimension: null,
124
- },
125
- {
126
- name: 'required_date',
127
- type: 'date',
128
- attributes: [],
129
- dimension: null,
130
- },
131
- {
132
- name: 'dispatched_date',
133
- type: 'date',
134
- attributes: [],
135
- dimension: null,
136
- },
137
119
  {
138
120
  name: 'dispatcher_id',
139
121
  type: 'int',
@@ -141,7 +123,7 @@ describe('<RevisionDiff />', () => {
141
123
  dimension: null,
142
124
  },
143
125
  ],
144
- updated_at: '2023-08-21T16:48:52.880498+00:00',
126
+ updated_at: '2023-08-21T16:48:52.981201+00:00',
145
127
  materializations: [],
146
128
  parents: [],
147
129
  },
@@ -163,7 +145,7 @@ describe('<RevisionDiff />', () => {
163
145
  );
164
146
  const { container } = render(
165
147
  <MemoryRouter
166
- initialEntries={['/nodes/default.repair_orders_cube/revisions/v1.0']}
148
+ initialEntries={['/nodes/default.repair_orders_cube/revisions/v2.0']}
167
149
  >
168
150
  <Routes>
169
151
  <Route path="nodes/:name/revisions/:revision" element={element} />
@@ -174,6 +156,9 @@ describe('<RevisionDiff />', () => {
174
156
  expect(mockDjClient.DataJunctionAPI.revisions).toHaveBeenCalledWith(
175
157
  'default.repair_orders_cube',
176
158
  );
159
+
160
+ const diffViews = screen.getAllByRole('gridcell', 'DiffView');
161
+ diffViews.map(diffView => expect(diffView).toBeInTheDocument());
177
162
  });
178
163
  });
179
164
  });
@@ -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
+ }
@@ -844,4 +844,15 @@ export const DataJunctionAPI = {
844
844
  })
845
845
  ).json();
846
846
  },
847
+ revalidate: async function (node) {
848
+ return await (
849
+ await fetch(`${DJ_URL}/nodes/${node}/validate`, {
850
+ method: 'POST',
851
+ headers: {
852
+ 'Content-Type': 'application/json',
853
+ },
854
+ credentials: 'include',
855
+ })
856
+ ).json();
857
+ },
847
858
  };
@@ -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);
@@ -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',
@@ -1202,3 +1202,18 @@ pre {
1202
1202
  text-transform: uppercase;
1203
1203
  padding-left: 10px;
1204
1204
  }
1205
+
1206
+ .validation_error {
1207
+ border: #b34b0025 1px solid;
1208
+ border-left: #b34b00 5px solid;
1209
+ padding-left: 20px;
1210
+ padding-top: 5px;
1211
+ padding-bottom: 5px;
1212
+ font-size: small;
1213
+ width: 600px;
1214
+ word-wrap: break-word;
1215
+ margin-top: 3px;
1216
+ background-color: #ffffff;
1217
+ margin-bottom: 3px;
1218
+ margin-left: -20px;
1219
+ }
@@ -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;