datajunction-ui 0.0.1-a45 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.1a45",
3
+ "version": "0.0.1-a45.dev14",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -69,6 +69,7 @@
69
69
  "react": "18.2.0",
70
70
  "react-app-polyfill": "3.0.0",
71
71
  "react-cookie": "4.1.1",
72
+ "react-diff-view": "3.2.1",
72
73
  "react-dom": "18.2.0",
73
74
  "react-helmet-async": "1.3.0",
74
75
  "react-i18next": "11.18.6",
@@ -96,6 +97,7 @@
96
97
  "ts-loader": "9.4.2",
97
98
  "ts-node": "10.9.1",
98
99
  "typescript": "4.6.4",
100
+ "unidiff": "1.0.4",
99
101
  "web-vitals": "2.1.4",
100
102
  "webpack": "5.81.0",
101
103
  "webpack-cli": "5.0.2",
@@ -0,0 +1,69 @@
1
+ import DJClientContext from '../providers/djclient';
2
+ import * as React from 'react';
3
+ import DeleteIcon from '../icons/DeleteIcon';
4
+ import EditIcon from '../icons/EditIcon';
5
+ import { Form, Formik } from 'formik';
6
+ import { useContext } from 'react';
7
+ import { displayMessageAfterSubmit } from '../../utils/form';
8
+
9
+ export default function NodeListActions({ nodeName }) {
10
+ const [editButton, setEditButton] = React.useState(<EditIcon />);
11
+ const [deleteButton, setDeleteButton] = React.useState(<DeleteIcon />);
12
+
13
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
14
+ const deleteNode = async (values, { setStatus }) => {
15
+ if (
16
+ !window.confirm('Deleting node ' + values.nodeName + '. Are you sure?')
17
+ ) {
18
+ return;
19
+ }
20
+ const { status, json } = await djClient.deactivate(values.nodeName);
21
+ if (status === 200 || status === 201 || status === 204) {
22
+ setStatus({
23
+ success: <>Successfully deleted node {values.nodeName}</>,
24
+ });
25
+ setEditButton(''); // hide the Edit button
26
+ setDeleteButton(''); // hide the Delete button
27
+ } else {
28
+ setStatus({
29
+ failure: `${json.message}`,
30
+ });
31
+ }
32
+ };
33
+
34
+ const initialValues = {
35
+ nodeName: nodeName,
36
+ };
37
+
38
+ return (
39
+ <div>
40
+ <a href={`/nodes/${nodeName}/edit`} style={{ marginLeft: '0.5rem' }}>
41
+ {editButton}
42
+ </a>
43
+ <Formik initialValues={initialValues} onSubmit={deleteNode}>
44
+ {function Render({ status, setFieldValue }) {
45
+ return (
46
+ <Form className="deleteNode">
47
+ {displayMessageAfterSubmit(status)}
48
+ {
49
+ <>
50
+ <button
51
+ type="submit"
52
+ style={{
53
+ marginLeft: 0,
54
+ all: 'unset',
55
+ color: '#005c72',
56
+ cursor: 'pointer',
57
+ }}
58
+ >
59
+ {deleteButton}
60
+ </button>
61
+ </>
62
+ }
63
+ </Form>
64
+ );
65
+ }}
66
+ </Formik>
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import fetchMock from 'jest-fetch-mock';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { render } from '../../../setupTests';
6
+ import DJClientContext from '../../providers/djclient';
7
+ import NodeListActions from '../NodeListActions';
8
+
9
+ describe('<NodeListActions />', () => {
10
+ beforeEach(() => {
11
+ fetchMock.resetMocks();
12
+ jest.clearAllMocks();
13
+ window.scrollTo = jest.fn();
14
+ });
15
+
16
+ const renderElement = djClient => {
17
+ return render(
18
+ <DJClientContext.Provider value={djClient}>
19
+ <NodeListActions nodeName="default.hard_hat" />
20
+ </DJClientContext.Provider>,
21
+ );
22
+ };
23
+
24
+ const initializeMockDJClient = () => {
25
+ return {
26
+ DataJunctionAPI: {
27
+ deactivate: jest.fn(),
28
+ },
29
+ };
30
+ };
31
+
32
+ it('deletes a node when clicked', async () => {
33
+ global.confirm = () => true;
34
+ const mockDjClient = initializeMockDJClient();
35
+ mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({
36
+ status: 204,
37
+ json: { name: 'source.warehouse.schema.some_table' },
38
+ });
39
+
40
+ renderElement(mockDjClient);
41
+
42
+ await userEvent.click(screen.getByRole('button'));
43
+
44
+ await waitFor(() => {
45
+ expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalled();
46
+ expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalledWith(
47
+ 'default.hard_hat',
48
+ );
49
+ });
50
+ expect(
51
+ screen.getByText('Successfully deleted node default.hard_hat'),
52
+ ).toBeInTheDocument();
53
+ }, 60000);
54
+
55
+ it('skips a node deletion during confirm', async () => {
56
+ global.confirm = () => false;
57
+ const mockDjClient = initializeMockDJClient();
58
+ mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({
59
+ status: 204,
60
+ json: { name: 'source.warehouse.schema.some_table' },
61
+ });
62
+
63
+ renderElement(mockDjClient);
64
+
65
+ await userEvent.click(screen.getByRole('button'));
66
+
67
+ await waitFor(() => {
68
+ expect(mockDjClient.DataJunctionAPI.deactivate).not.toBeCalled();
69
+ });
70
+ }, 60000);
71
+
72
+ it('fail deleting a node when clicked', async () => {
73
+ global.confirm = () => true;
74
+ const mockDjClient = initializeMockDJClient();
75
+ mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({
76
+ status: 777,
77
+ json: { message: 'source.warehouse.schema.some_table' },
78
+ });
79
+
80
+ renderElement(mockDjClient);
81
+
82
+ await userEvent.click(screen.getByRole('button'));
83
+
84
+ await waitFor(() => {
85
+ expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalled();
86
+ expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalledWith(
87
+ 'default.hard_hat',
88
+ );
89
+ });
90
+ expect(
91
+ screen.getByText('source.warehouse.schema.some_table'),
92
+ ).toBeInTheDocument();
93
+ }, 60000);
94
+ });
@@ -0,0 +1,45 @@
1
+ import * as React from 'react';
2
+
3
+ const CommitIcon = props => (
4
+ <svg
5
+ width="2em"
6
+ height="2em"
7
+ viewBox="0 0 256 256"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ >
10
+ <rect fill="none" height="256" width="256" />
11
+ <circle
12
+ cx="128"
13
+ cy="128"
14
+ fill="none"
15
+ r="52"
16
+ stroke="#000"
17
+ strokeLinecap="round"
18
+ strokeLinejoin="round"
19
+ strokeWidth="12"
20
+ />
21
+ <line
22
+ fill="none"
23
+ stroke="#000"
24
+ strokeLinecap="round"
25
+ strokeLinejoin="round"
26
+ strokeWidth="12"
27
+ x1="8"
28
+ x2="76"
29
+ y1="128"
30
+ y2="128"
31
+ />
32
+ <line
33
+ fill="none"
34
+ stroke="#000"
35
+ strokeLinecap="round"
36
+ strokeLinejoin="round"
37
+ strokeWidth="12"
38
+ x1="180"
39
+ x2="248"
40
+ y1="128"
41
+ y2="128"
42
+ />
43
+ </svg>
44
+ );
45
+ export default CommitIcon;
@@ -0,0 +1,63 @@
1
+ const DiffIcon = props => (
2
+ <svg
3
+ width="2em"
4
+ height="2em"
5
+ viewBox="0 0 256 256"
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ >
8
+ <rect fill="none" height="256" width="256" />
9
+ <circle
10
+ cx="196"
11
+ cy="188"
12
+ fill="none"
13
+ r="28"
14
+ stroke="#000"
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ strokeWidth="12"
18
+ />
19
+ <path
20
+ d="M196,160V119.9a48.2,48.2,0,0,0-14.1-34L144,48"
21
+ fill="none"
22
+ stroke="#000"
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ strokeWidth="12"
26
+ />
27
+ <polyline
28
+ fill="none"
29
+ points="144 88 144 48 184 48"
30
+ stroke="#000"
31
+ strokeLinecap="round"
32
+ strokeLinejoin="round"
33
+ strokeWidth="12"
34
+ />
35
+ <circle
36
+ cx="60"
37
+ cy="68"
38
+ fill="none"
39
+ r="28"
40
+ stroke="#000"
41
+ strokeLinecap="round"
42
+ strokeLinejoin="round"
43
+ strokeWidth="12"
44
+ />
45
+ <path
46
+ d="M60,96v40.1a48.2,48.2,0,0,0,14.1,34L112,208"
47
+ fill="none"
48
+ stroke="#000"
49
+ strokeLinecap="round"
50
+ strokeLinejoin="round"
51
+ strokeWidth="12"
52
+ />
53
+ <polyline
54
+ fill="none"
55
+ points="112 168 112 208 72 208"
56
+ stroke="#000"
57
+ strokeLinecap="round"
58
+ strokeLinejoin="round"
59
+ strokeWidth="12"
60
+ />
61
+ </svg>
62
+ );
63
+ export default DiffIcon;
package/src/app/index.tsx CHANGED
@@ -9,6 +9,7 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom';
9
9
 
10
10
  import { NamespacePage } from './pages/NamespacePage/Loadable';
11
11
  import { NodePage } from './pages/NodePage/Loadable';
12
+ import RevisionDiff from './pages/NodePage/RevisionDiff';
12
13
  import { SQLBuilderPage } from './pages/SQLBuilderPage/Loadable';
13
14
  import { CubeBuilderPage } from './pages/CubeBuilderPage/Loadable';
14
15
  import { TagPage } from './pages/TagPage/Loadable';
@@ -58,6 +59,10 @@ export function App() {
58
59
  key="edit-cube"
59
60
  element={<CubeBuilderPage />}
60
61
  />
62
+ <Route
63
+ path=":name/revisions/:revision"
64
+ element={<RevisionDiff />}
65
+ />
61
66
  <Route path=":name/:tab" element={<NodePage />} />
62
67
  </Route>
63
68
 
@@ -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
+ };
@@ -4,8 +4,7 @@ import { useContext, useEffect, useState } from 'react';
4
4
  import NodeStatus from '../NodePage/NodeStatus';
5
5
  import DJClientContext from '../../providers/djclient';
6
6
  import Explorer from '../NamespacePage/Explorer';
7
- import EditIcon from '../../icons/EditIcon';
8
- import DeleteNode from '../../components/DeleteNode';
7
+ import NodeListActions from '../../components/NodeListActions';
9
8
  import AddNamespacePopover from './AddNamespacePopover';
10
9
 
11
10
  export function NamespacePage() {
@@ -106,10 +105,7 @@ export function NamespacePage() {
106
105
  </span>
107
106
  </td>
108
107
  <td>
109
- <a href={`/nodes/${node?.name}/edit`} style={{ marginLeft: '0.5rem' }}>
110
- <EditIcon />
111
- </a>
112
- <DeleteNode nodeName={node?.name} />
108
+ <NodeListActions nodeName={node?.name} />
113
109
  </td>
114
110
  </tr>
115
111
  ));