datajunction-ui 0.0.1-rc.2 → 0.0.1-rc.20

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.
Files changed (135) hide show
  1. package/.env +1 -0
  2. package/.prettierignore +3 -1
  3. package/dj-logo.svg +10 -0
  4. package/package.json +43 -13
  5. package/public/favicon.ico +0 -0
  6. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  7. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -39
  8. package/src/app/components/DeleteNode.jsx +79 -0
  9. package/src/app/components/ListGroupItem.jsx +8 -1
  10. package/src/app/components/NamespaceHeader.jsx +4 -13
  11. package/src/app/components/QueryInfo.jsx +77 -0
  12. package/src/app/components/Tab.jsx +3 -2
  13. package/src/app/components/ToggleSwitch.jsx +20 -0
  14. package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
  15. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  16. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  17. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +3 -0
  18. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
  19. package/src/app/components/djgraph/Collapse.jsx +46 -0
  20. package/src/app/components/djgraph/DJNode.jsx +60 -82
  21. package/src/app/components/djgraph/DJNodeColumns.jsx +71 -0
  22. package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
  23. package/src/app/components/djgraph/LayoutFlow.jsx +104 -0
  24. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  25. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  26. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  27. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
  28. package/src/app/constants.js +2 -0
  29. package/src/app/icons/AlertIcon.jsx +32 -0
  30. package/src/app/icons/CollapsedIcon.jsx +15 -0
  31. package/src/app/icons/DJLogo.jsx +36 -0
  32. package/src/app/icons/DeleteIcon.jsx +21 -0
  33. package/src/app/icons/EditIcon.jsx +18 -0
  34. package/src/app/icons/ExpandedIcon.jsx +15 -0
  35. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  36. package/src/app/icons/InvalidIcon.jsx +14 -0
  37. package/src/app/icons/PythonIcon.jsx +52 -0
  38. package/src/app/icons/TableIcon.jsx +14 -0
  39. package/src/app/icons/ValidIcon.jsx +14 -0
  40. package/src/app/index.tsx +79 -26
  41. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +46 -0
  42. package/src/app/pages/AddEditNodePage/FullNameField.jsx +37 -0
  43. package/src/app/pages/AddEditNodePage/Loadable.jsx +16 -0
  44. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +89 -0
  45. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +77 -0
  46. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +93 -0
  47. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
  48. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
  49. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  50. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +53 -0
  51. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +53 -0
  52. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
  53. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +178 -0
  54. package/src/app/pages/AddEditNodePage/index.jsx +357 -0
  55. package/src/app/pages/LoginPage/__tests__/index.test.jsx +70 -0
  56. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  57. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  58. package/src/app/pages/LoginPage/index.jsx +90 -0
  59. package/src/app/pages/NamespacePage/Explorer.jsx +57 -0
  60. package/src/app/pages/NamespacePage/Loadable.jsx +9 -7
  61. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +95 -0
  62. package/src/app/pages/NamespacePage/index.jsx +131 -31
  63. package/src/app/pages/NodePage/ClientCodePopover.jsx +32 -0
  64. package/src/app/pages/NodePage/EditColumnPopover.jsx +102 -0
  65. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +135 -0
  66. package/src/app/pages/NodePage/Loadable.jsx +9 -7
  67. package/src/app/pages/NodePage/NodeColumnTab.jsx +106 -27
  68. package/src/app/pages/NodePage/NodeGraphTab.jsx +94 -148
  69. package/src/app/pages/NodePage/NodeHistory.jsx +212 -0
  70. package/src/app/pages/NodePage/NodeInfoTab.jsx +166 -51
  71. package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
  72. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +174 -0
  73. package/src/app/pages/NodePage/NodeSQLTab.jsx +82 -0
  74. package/src/app/pages/NodePage/NodeStatus.jsx +14 -20
  75. package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
  76. package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +49 -0
  77. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
  78. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +165 -0
  79. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +591 -0
  80. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +57 -0
  81. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +725 -0
  82. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  83. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +402 -0
  84. package/src/app/pages/NodePage/index.jsx +151 -41
  85. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  86. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  87. package/src/app/pages/RegisterTablePage/index.jsx +163 -0
  88. package/src/app/pages/Root/__tests__/index.test.jsx +77 -0
  89. package/src/app/pages/Root/index.tsx +32 -4
  90. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  91. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  92. package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
  93. package/src/app/providers/djclient.jsx +5 -0
  94. package/src/app/services/DJService.js +388 -22
  95. package/src/app/services/__tests__/DJService.test.jsx +609 -0
  96. package/src/mocks/mockNodes.jsx +1397 -0
  97. package/src/setupTests.ts +31 -1
  98. package/src/styles/dag.css +111 -5
  99. package/src/styles/index.css +467 -31
  100. package/src/styles/login.css +67 -0
  101. package/src/styles/node-creation.scss +197 -0
  102. package/src/styles/styles.scss +44 -0
  103. package/src/styles/styles.scss.d.ts +9 -0
  104. package/src/utils/form.jsx +23 -0
  105. package/tsconfig.json +1 -5
  106. package/webpack.config.js +29 -6
  107. package/.babelrc +0 -4
  108. package/.env.local +0 -4
  109. package/.env.production +0 -1
  110. package/.github/pull_request_template.md +0 -11
  111. package/.github/workflows/ci.yml +0 -33
  112. package/.vscode/extensions.json +0 -7
  113. package/.vscode/launch.json +0 -15
  114. package/.vscode/settings.json +0 -25
  115. package/Dockerfile +0 -7
  116. package/dist/5fa71a03d45dc2e3d61447f3013a303d.png +0 -0
  117. package/dist/index.html +0 -1
  118. package/dist/main.js +0 -23303
  119. package/dist/static/main.05a86d446163fd5f17d3.js +0 -2
  120. package/dist/static/main.05a86d446163fd5f17d3.js.LICENSE.txt +0 -98
  121. package/dist/static/main.9e53bed734dae98e5b10.js +0 -2
  122. package/dist/static/main.9e53bed734dae98e5b10.js.LICENSE.txt +0 -98
  123. package/dist/static/main.js +0 -2
  124. package/dist/static/main.js.LICENSE.txt +0 -98
  125. package/dist/static/vendor.05a86d446163fd5f17d3.js +0 -2
  126. package/dist/static/vendor.05a86d446163fd5f17d3.js.LICENSE.txt +0 -29
  127. package/dist/static/vendor.9e53bed734dae98e5b10.js +0 -2
  128. package/dist/static/vendor.9e53bed734dae98e5b10.js.LICENSE.txt +0 -29
  129. package/dist/static/vendor.js +0 -2
  130. package/dist/static/vendor.js.LICENSE.txt +0 -29
  131. package/dist/vendor.js +0 -281
  132. package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
  133. package/src/app/pages/ListNamespacesPage/index.jsx +0 -52
  134. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
  135. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
@@ -0,0 +1,174 @@
1
+ import { useEffect, useState } from 'react';
2
+ import ClientCodePopover from './ClientCodePopover';
3
+ import TableIcon from '../../icons/TableIcon';
4
+
5
+ const cronstrue = require('cronstrue');
6
+
7
+ export default function NodeMaterializationTab({ node, djClient }) {
8
+ const [materializations, setMaterializations] = useState([]);
9
+ useEffect(() => {
10
+ const fetchData = async () => {
11
+ if (node) {
12
+ const data = await djClient.materializations(node.name);
13
+ setMaterializations(data);
14
+ }
15
+ };
16
+ fetchData().catch(console.error);
17
+ }, [djClient, node]);
18
+
19
+ const rangePartition = partition => {
20
+ return (
21
+ <div>
22
+ <span className="badge partition_value">
23
+ <span className="badge partition_value">{partition.range[0]}</span>to
24
+ <span className="badge partition_value">{partition.range[1]}</span>
25
+ </span>
26
+ </div>
27
+ );
28
+ };
29
+
30
+ const cron = materialization => {
31
+ var parsedCron = '';
32
+ try {
33
+ parsedCron = cronstrue.toString(materialization.schedule);
34
+ } catch (e) {}
35
+ return parsedCron;
36
+ };
37
+
38
+ const materializationRows = materializations => {
39
+ return materializations.map(materialization => (
40
+ <tr key={materialization.name}>
41
+ <td className="text-start node_name">
42
+ <a href={materialization.urls[0]}>{materialization.name}</a>
43
+ <ClientCodePopover code={materialization.clientCode} />
44
+ </td>
45
+ <td>
46
+ <span className={`badge cron`}>{materialization.schedule}</span>
47
+ <div className={`cron-description`}>{cron(materialization)} </div>
48
+ </td>
49
+ <td>
50
+ {materialization.engine.name}
51
+ <br />
52
+ {materialization.engine.version}
53
+ </td>
54
+ <td>
55
+ {materialization.config.partitions ? (
56
+ materialization.config.partitions.map(partition =>
57
+ partition.type_ === 'categorical' ? (
58
+ <div className="partition__full" key={partition.name}>
59
+ <div className="partition__header">{partition.name}</div>
60
+ <div className="partition__body">
61
+ {partition.values !== null && partition.values.length > 0
62
+ ? partition.values.map(val => (
63
+ <span
64
+ className="badge partition_value"
65
+ key={`partition-value-${val}`}
66
+ >
67
+ {val}
68
+ </span>
69
+ ))
70
+ : null}
71
+ {partition.range !== null && partition.range.length > 0
72
+ ? rangePartition(partition)
73
+ : null}
74
+ {(partition.range === null && partition.values === null) ||
75
+ (partition.range?.length === 0 &&
76
+ partition.values?.length === 0) ? (
77
+ <span className={`badge partition_value_highlight`}>
78
+ ALL
79
+ </span>
80
+ ) : null}
81
+ </div>
82
+ </div>
83
+ ) : null,
84
+ )
85
+ ) : (
86
+ <br />
87
+ )}
88
+ </td>
89
+ <td>
90
+ {materialization.output_tables.map(table => (
91
+ <div className={`table__full`} key={table}>
92
+ <div className="table__header">
93
+ <TableIcon />{' '}
94
+ <span className={`entity-info`}>
95
+ {table.split('.')[0] + '.' + table.split('.')[1]}
96
+ </span>
97
+ </div>
98
+ <div className={`table__body upstream_tables`}>
99
+ {table.split('.')[2]}
100
+ </div>
101
+ </div>
102
+ ))}
103
+ </td>
104
+ {/*<td>{Object.keys(materialization.config.spark).map(key => <li className={`list-group-item`}>{key}: {materialization.config.spark[key]}</li>)}</td>*/}
105
+
106
+ <td>
107
+ {materialization.config.partitions ? (
108
+ materialization.config.partitions.map(partition =>
109
+ partition.type_ === 'temporal' ? (
110
+ <div className="partition__full" key={partition.name}>
111
+ <div className="partition__header">{partition.name}</div>
112
+ <div className="partition__body">
113
+ {partition.values !== null && partition.values.length > 0
114
+ ? partition.values.map(val => (
115
+ <span className="badge partition_value">{val}</span>
116
+ ))
117
+ : null}
118
+ {partition.range !== null && partition.range.length > 0
119
+ ? rangePartition(partition)
120
+ : null}
121
+ </div>
122
+ </div>
123
+ ) : null,
124
+ )
125
+ ) : (
126
+ <br />
127
+ )}
128
+ </td>
129
+ <td>
130
+ {materialization.urls.map((url, idx) => (
131
+ <a href={url} key={`url-${idx}`}>
132
+ [{idx + 1}]
133
+ </a>
134
+ ))}
135
+ </td>
136
+ </tr>
137
+ ));
138
+ };
139
+ return (
140
+ <div className="table-responsive">
141
+ {materializations.length > 0 ? (
142
+ <table
143
+ className="card-inner-table table"
144
+ aria-label="Materializations"
145
+ aria-hidden="false"
146
+ >
147
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
148
+ <tr>
149
+ <th className="text-start">Name</th>
150
+ <th>Schedule</th>
151
+ <th>Engine</th>
152
+ <th>Partitions</th>
153
+ <th>Output Tables</th>
154
+ <th>Backfills</th>
155
+ <th>URLs</th>
156
+ </tr>
157
+ </thead>
158
+ <tbody>
159
+ {materializationRows(
160
+ materializations.filter(
161
+ materialization =>
162
+ !(materialization.name === 'default' && node.type === 'cube'),
163
+ ),
164
+ )}
165
+ </tbody>
166
+ </table>
167
+ ) : (
168
+ <div className="message alert" style={{ marginTop: '10px' }}>
169
+ No materializations available for this node
170
+ </div>
171
+ )}
172
+ </div>
173
+ );
174
+ }
@@ -0,0 +1,82 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import Select from 'react-select';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
5
+ import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
6
+
7
+ const NodeSQLTab = djNode => {
8
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
9
+ const [query, setQuery] = useState('');
10
+
11
+ const [selection, setSelection] = useState({
12
+ dimensions: [],
13
+ filters: [],
14
+ });
15
+
16
+ useEffect(() => {
17
+ const fetchData = async () => {
18
+ const query = await djClient.sql(djNode.djNode.name, selection);
19
+ setQuery(query.sql);
20
+ };
21
+ fetchData().catch(console.error);
22
+ }, [djClient, djNode.djNode.name, selection]);
23
+ const dimensionsList = djNode.djNode.dimensions
24
+ ? djNode.djNode.dimensions.map(dim => ({
25
+ value: dim.name,
26
+ label: dim.name + ` (${dim.type})`,
27
+ }))
28
+ : [''];
29
+
30
+ const handleSubmit = event => {
31
+ event.preventDefault();
32
+ };
33
+
34
+ const handleChange = event => {
35
+ setSelection({ filters: [], dimensions: event.map(dim => dim.value) });
36
+ };
37
+
38
+ return (
39
+ <form
40
+ id="retrieve-sql"
41
+ name="retrieve-sql"
42
+ onSubmit={handleSubmit.bind(this)}
43
+ >
44
+ <div>
45
+ <h4>Group By</h4>
46
+ <Select
47
+ name="dimensions"
48
+ options={dimensionsList}
49
+ isMulti
50
+ isClearable
51
+ onChange={handleChange}
52
+ />
53
+ {/*<h4>Filters</h4>*/}
54
+ {/*<Select*/}
55
+ {/* name="filter_name"*/}
56
+ {/* options={dimensionsList}*/}
57
+ {/* className="filters_attribute"*/}
58
+ {/*/>*/}
59
+ {/*<Select*/}
60
+ {/* name="filter_operator"*/}
61
+ {/* options={options}*/}
62
+ {/* className="filters_attribute"*/}
63
+ {/*/>*/}
64
+ {/*<textarea name="filter_value" className="filters_attribute" />*/}
65
+
66
+ <div
67
+ style={{
68
+ width: window.innerWidth * 0.8,
69
+ marginTop: '2rem',
70
+ }}
71
+ >
72
+ <h6 className="mb-0 w-100">Query</h6>
73
+ <SyntaxHighlighter language="sql" style={foundation}>
74
+ {query}
75
+ </SyntaxHighlighter>
76
+ </div>
77
+ </div>
78
+ </form>
79
+ );
80
+ };
81
+
82
+ export default NodeSQLTab;
@@ -1,34 +1,28 @@
1
1
  import { Component } from 'react';
2
+ import ValidIcon from '../../icons/ValidIcon';
3
+ import InvalidIcon from '../../icons/InvalidIcon';
2
4
 
3
5
  export default class NodeStatus extends Component {
4
6
  render() {
5
7
  const { node } = this.props;
6
8
  return (
7
- <span className="status__valid status" style={{ alignContent: 'center' }}>
9
+ <>
8
10
  {node?.status === 'valid' ? (
9
- <svg
10
- xmlns="http://www.w3.org/2000/svg"
11
- width="25"
12
- height="25"
13
- fill="currentColor"
14
- className="bi bi-check-circle-fill"
15
- viewBox="0 0 16 16"
11
+ <span
12
+ className="status__valid status"
13
+ style={{ alignContent: 'center' }}
16
14
  >
17
- <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
18
- </svg>
15
+ <ValidIcon />
16
+ </span>
19
17
  ) : (
20
- <svg
21
- xmlns="http://www.w3.org/2000/svg"
22
- width="16"
23
- height="16"
24
- fill="currentColor"
25
- className="bi bi-x-circle-fill"
26
- viewBox="0 0 16 16"
18
+ <span
19
+ className="status__invalid status"
20
+ style={{ alignContent: 'center' }}
27
21
  >
28
- <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
29
- </svg>
22
+ <InvalidIcon />
23
+ </span>
30
24
  )}
31
- </span>
25
+ </>
32
26
  );
33
27
  }
34
28
  }
@@ -0,0 +1,42 @@
1
+ import { useEffect, useState } from 'react';
2
+ import * as React from 'react';
3
+
4
+ export default function NodesWithDimension({ node, djClient }) {
5
+ const [availableNodes, setAvailableNodes] = useState([]);
6
+
7
+ useEffect(() => {
8
+ const fetchData = async () => {
9
+ const data = await djClient.nodesWithDimension(node.name);
10
+ setAvailableNodes(data);
11
+ };
12
+ fetchData().catch(console.error);
13
+ }, [djClient, node]);
14
+ return (
15
+ <div className="table-responsive">
16
+ <table className="card-inner-table table">
17
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
18
+ <tr>
19
+ <th className="text-start">Name</th>
20
+ <th>Type</th>
21
+ </tr>
22
+ </thead>
23
+ <tbody>
24
+ {availableNodes.map(node => (
25
+ <tr>
26
+ <td>
27
+ <a href={`/nodes/${node.name}`}>{node.display_name}</a>
28
+ </td>
29
+ <td>
30
+ <span
31
+ className={'node_type__' + node.type + ' badge node_type'}
32
+ >
33
+ {node.type}
34
+ </span>
35
+ </td>
36
+ </tr>
37
+ ))}
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen, waitFor } from '@testing-library/react';
3
+ import ClientCodePopover from '../ClientCodePopover';
4
+ import userEvent from '@testing-library/user-event';
5
+
6
+ describe('<ClientCodePopover />', () => {
7
+ const defaultProps = {
8
+ code: "print('Hello, World!')",
9
+ };
10
+
11
+ it('toggles the code popover visibility when the button is clicked', async () => {
12
+ render(<ClientCodePopover {...defaultProps} />);
13
+
14
+ const button = screen.getByRole('button', 'code-button');
15
+
16
+ // Initially, the popover should be hidden
17
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
18
+ 'display: none',
19
+ );
20
+
21
+ // Clicking the button should display the popover
22
+ fireEvent.click(button);
23
+ expect(screen.getByRole('dialog', { hidden: true })).not.toHaveStyle(
24
+ 'display: none',
25
+ );
26
+
27
+ // Clicking the button again should hide the popover
28
+ fireEvent.click(button);
29
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
30
+ 'display: none',
31
+ );
32
+
33
+ // Trigger onClose by pressing <escape>
34
+ userEvent.keyboard('{Escape}');
35
+ // fireEvent.click(screen.getByTestId('body').firstChild());
36
+ await waitFor(() => {
37
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
38
+ 'display: none',
39
+ );
40
+ });
41
+ });
42
+
43
+ it('renders the provided code within the SyntaxHighlighter', () => {
44
+ render(<ClientCodePopover {...defaultProps} />);
45
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveTextContent(
46
+ defaultProps.code,
47
+ );
48
+ });
49
+ });
@@ -0,0 +1,148 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, waitFor, screen } from '@testing-library/react';
3
+ import EditColumnPopover from '../EditColumnPopover';
4
+ import DJClientContext from '../../../providers/djclient';
5
+
6
+ const mockDjClient = {
7
+ DataJunctionAPI: {
8
+ setAttributes: jest.fn(),
9
+ },
10
+ };
11
+
12
+ describe('<EditColumnPopover />', () => {
13
+ it('renders correctly and handles form submission', async () => {
14
+ // Mock necessary data
15
+ const column = {
16
+ name: 'column1',
17
+ dimension: { name: 'dimension1' },
18
+ attributes: [
19
+ { attribute_type: { name: 'primary_key', namespace: 'system' } },
20
+ ],
21
+ };
22
+ const node = { name: 'default.node1' };
23
+ const options = [
24
+ { value: 'dimension', label: 'Dimension' },
25
+ { value: 'primary_key', label: 'Primary Key' },
26
+ ];
27
+
28
+ // Mock onSubmit function
29
+ const onSubmitMock = jest.fn();
30
+
31
+ mockDjClient.DataJunctionAPI.setAttributes.mockReturnValue({
32
+ status: 201,
33
+ json: [
34
+ {
35
+ name: 'id',
36
+ type: 'int',
37
+ attributes: [
38
+ { attribute_type: { name: 'primary_key', namespace: 'system' } },
39
+ ],
40
+ dimension: null,
41
+ },
42
+ ],
43
+ });
44
+
45
+ // Render the component
46
+ const { getByLabelText, getByText, getByTestId } = render(
47
+ <DJClientContext.Provider value={mockDjClient}>
48
+ <EditColumnPopover
49
+ column={column}
50
+ node={node}
51
+ options={options}
52
+ onSubmit={onSubmitMock}
53
+ />
54
+ </DJClientContext.Provider>,
55
+ );
56
+
57
+ // Open the popover
58
+ fireEvent.click(getByLabelText('EditColumn'));
59
+
60
+ // Click on one attribute in the select
61
+ const editAttributes = getByTestId('edit-attributes');
62
+ fireEvent.keyDown(editAttributes.firstChild, { key: 'ArrowDown' });
63
+ fireEvent.click(screen.getByText('Dimension'));
64
+ fireEvent.click(getByText('Save'));
65
+ getByText('Save').click();
66
+
67
+ // Expect setAttributes to be called
68
+ await waitFor(() => {
69
+ expect(mockDjClient.DataJunctionAPI.setAttributes).toHaveBeenCalled();
70
+ expect(getByText('Saved!')).toBeInTheDocument();
71
+ });
72
+
73
+ // Click on two attributes in the select
74
+ fireEvent.keyDown(editAttributes.firstChild, { key: 'ArrowDown' });
75
+ fireEvent.click(screen.getByText('Dimension'));
76
+ fireEvent.click(screen.getByText('Primary Key'));
77
+ fireEvent.click(getByText('Save'));
78
+ getByText('Save').click();
79
+
80
+ // Expect setAttributes to be called
81
+ await waitFor(() => {
82
+ expect(mockDjClient.DataJunctionAPI.setAttributes).toHaveBeenCalledWith(
83
+ 'default.node1',
84
+ 'column1',
85
+ ['primary_key', 'dimension'],
86
+ );
87
+ expect(getByText('Saved!')).toBeInTheDocument();
88
+ });
89
+
90
+ // Close the popover
91
+ fireEvent.click(getByLabelText('EditColumn'));
92
+ });
93
+
94
+ it('handles failed form submission', async () => {
95
+ // Mock necessary data
96
+ const column = {
97
+ name: 'column1',
98
+ dimension: { name: 'dimension1' },
99
+ attributes: [
100
+ { attribute_type: { name: 'primary_key', namespace: 'system' } },
101
+ ],
102
+ };
103
+ const node = { name: 'default.node1' };
104
+ const options = [
105
+ { value: 'dimension', label: 'Dimension' },
106
+ { value: 'primary_key', label: 'Primary Key' },
107
+ ];
108
+
109
+ // Mock onSubmit function
110
+ const onSubmitMock = jest.fn();
111
+
112
+ mockDjClient.DataJunctionAPI.setAttributes.mockReturnValue({
113
+ status: 500,
114
+ json: { message: 'bad request' },
115
+ });
116
+
117
+ // Render the component
118
+ const { getByLabelText, getByText, getByTestId } = render(
119
+ <DJClientContext.Provider value={mockDjClient}>
120
+ <EditColumnPopover
121
+ column={column}
122
+ node={node}
123
+ options={options}
124
+ onSubmit={onSubmitMock}
125
+ />
126
+ </DJClientContext.Provider>,
127
+ );
128
+
129
+ // Open the popover
130
+ fireEvent.click(getByLabelText('EditColumn'));
131
+
132
+ // Click on one attribute in the select
133
+ const editAttributes = getByTestId('edit-attributes');
134
+ fireEvent.keyDown(editAttributes.firstChild, { key: 'ArrowDown' });
135
+ fireEvent.click(screen.getByText('Dimension'));
136
+ fireEvent.click(getByText('Save'));
137
+ getByText('Save').click();
138
+
139
+ // Expect setAttributes to be called and the failure message to show up
140
+ await waitFor(() => {
141
+ expect(mockDjClient.DataJunctionAPI.setAttributes).toHaveBeenCalled();
142
+ expect(getByText('bad request')).toBeInTheDocument();
143
+ });
144
+
145
+ // Close the popover
146
+ fireEvent.click(getByLabelText('EditColumn'));
147
+ });
148
+ });