datajunction-ui 0.0.1-rc.19 → 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 (66) hide show
  1. package/package.json +6 -5
  2. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  3. package/src/app/components/DeleteNode.jsx +79 -0
  4. package/src/app/components/ListGroupItem.jsx +8 -1
  5. package/src/app/components/QueryInfo.jsx +4 -4
  6. package/src/app/components/Tab.jsx +9 -1
  7. package/src/app/components/ToggleSwitch.jsx +3 -0
  8. package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
  9. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  10. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  11. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +3 -0
  12. package/src/app/components/djgraph/DJNodeColumns.jsx +4 -1
  13. package/src/app/components/djgraph/DJNodeDimensions.jsx +9 -2
  14. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  15. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  16. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  17. package/src/app/index.tsx +6 -0
  18. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +15 -2
  19. package/src/app/pages/AddEditNodePage/FullNameField.jsx +2 -1
  20. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +77 -0
  21. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +93 -0
  22. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +34 -3
  23. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +4 -2
  24. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +53 -0
  25. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +53 -0
  26. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +0 -81
  27. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +82 -257
  28. package/src/app/pages/AddEditNodePage/index.jsx +13 -5
  29. package/src/app/pages/LoginPage/__tests__/index.test.jsx +70 -0
  30. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +95 -0
  31. package/src/app/pages/NamespacePage/index.jsx +8 -5
  32. package/src/app/pages/NodePage/ClientCodePopover.jsx +3 -1
  33. package/src/app/pages/NodePage/EditColumnPopover.jsx +102 -0
  34. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +135 -0
  35. package/src/app/pages/NodePage/NodeColumnTab.jsx +80 -17
  36. package/src/app/pages/NodePage/NodeHistory.jsx +63 -30
  37. package/src/app/pages/NodePage/NodeInfoTab.jsx +52 -7
  38. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +50 -27
  39. package/src/app/pages/NodePage/NodeSQLTab.jsx +0 -10
  40. package/src/app/pages/NodePage/NodesWithDimension.jsx +4 -2
  41. package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +49 -0
  42. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
  43. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +165 -0
  44. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +591 -0
  45. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +57 -0
  46. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +725 -0
  47. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  48. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +402 -0
  49. package/src/app/pages/NodePage/index.jsx +22 -6
  50. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  51. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  52. package/src/app/pages/RegisterTablePage/index.jsx +163 -0
  53. package/src/app/pages/Root/__tests__/index.test.jsx +77 -0
  54. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  55. package/src/app/pages/SQLBuilderPage/index.jsx +61 -43
  56. package/src/app/services/DJService.js +125 -54
  57. package/src/app/services/__tests__/DJService.test.jsx +609 -0
  58. package/src/mocks/mockNodes.jsx +1397 -0
  59. package/src/setupTests.ts +30 -0
  60. package/src/styles/index.css +43 -0
  61. package/src/styles/node-creation.scss +7 -0
  62. package/src/utils/form.jsx +23 -0
  63. package/.github/pull_request_template.md +0 -11
  64. package/.github/workflows/ci.yml +0 -33
  65. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -118
  66. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.1-rc.19",
3
+ "version": "0.0.1-rc.20",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -160,10 +160,10 @@
160
160
  ],
161
161
  "coverageThreshold": {
162
162
  "global": {
163
- "branches": 90,
164
- "functions": 90,
163
+ "statements": 90,
164
+ "branches": 75,
165
165
  "lines": 90,
166
- "statements": 90
166
+ "functions": 85
167
167
  }
168
168
  }
169
169
  },
@@ -182,6 +182,7 @@
182
182
  "html-webpack-plugin": "5.5.1",
183
183
  "jest": "^29.5.0",
184
184
  "jest-fetch-mock": "3.0.3",
185
- "mini-css-extract-plugin": "2.7.6"
185
+ "mini-css-extract-plugin": "2.7.6",
186
+ "resize-observer-polyfill": "1.5.1"
186
187
  }
187
188
  }
@@ -0,0 +1,44 @@
1
+ import reportWebVitals from '../reportWebVitals';
2
+
3
+ // Mocking the web-vitals module
4
+ jest.mock('web-vitals', () => ({
5
+ getCLS: jest.fn(),
6
+ getFID: jest.fn(),
7
+ getFCP: jest.fn(),
8
+ getLCP: jest.fn(),
9
+ getTTFB: jest.fn(),
10
+ }));
11
+
12
+ describe('reportWebVitals', () => {
13
+ // Mock web-vitals functions
14
+ const mockGetCLS = jest.fn();
15
+ const mockGetFID = jest.fn();
16
+ const mockGetFCP = jest.fn();
17
+ const mockGetLCP = jest.fn();
18
+ const mockGetTTFB = jest.fn();
19
+
20
+ beforeAll(() => {
21
+ jest.doMock('web-vitals', () => ({
22
+ getCLS: mockGetCLS,
23
+ getFID: mockGetFID,
24
+ getFCP: mockGetFCP,
25
+ getLCP: mockGetLCP,
26
+ getTTFB: mockGetTTFB,
27
+ }));
28
+ });
29
+
30
+ beforeEach(() => {
31
+ jest.clearAllMocks();
32
+ });
33
+
34
+ it('does not call the web vitals functions if onPerfEntry is not a function', async () => {
35
+ await reportWebVitals(undefined);
36
+
37
+ const { getCLS, getFID, getFCP, getLCP, getTTFB } = require('web-vitals');
38
+ expect(getCLS).not.toHaveBeenCalled();
39
+ expect(getFID).not.toHaveBeenCalled();
40
+ expect(getFCP).not.toHaveBeenCalled();
41
+ expect(getLCP).not.toHaveBeenCalled();
42
+ expect(getTTFB).not.toHaveBeenCalled();
43
+ });
44
+ });
@@ -0,0 +1,79 @@
1
+ import DJClientContext from '../providers/djclient';
2
+ import ValidIcon from '../icons/ValidIcon';
3
+ import AlertIcon from '../icons/AlertIcon';
4
+ import * as React from 'react';
5
+ import DeleteIcon from '../icons/DeleteIcon';
6
+ import { Form, Formik } from 'formik';
7
+ import { useContext } from 'react';
8
+
9
+ export default function DeleteNode({ nodeName }) {
10
+ console.log('nodeName', nodeName);
11
+
12
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
13
+ const deleteNode = async (values, { setSubmitting, setStatus }) => {
14
+ const { status, json } = await djClient.deactivate(values.nodeName);
15
+ if (status === 200 || status === 201 || status === 204) {
16
+ setStatus({
17
+ success: <>Successfully deleted node {values.nodeName}</>,
18
+ });
19
+ } else {
20
+ setStatus({
21
+ failure: `${json.message}`,
22
+ });
23
+ }
24
+ setSubmitting(false);
25
+ };
26
+
27
+ const displayMessageAfterSubmit = status => {
28
+ return status?.success !== undefined ? (
29
+ <div className="message success">
30
+ <ValidIcon />
31
+ {status?.success}
32
+ </div>
33
+ ) : status?.failure !== undefined ? (
34
+ alertMessage(status?.failure)
35
+ ) : (
36
+ ''
37
+ );
38
+ };
39
+
40
+ const alertMessage = message => {
41
+ return (
42
+ <div className="message alert">
43
+ <AlertIcon />
44
+ {message}
45
+ </div>
46
+ );
47
+ };
48
+ const initialValues = {
49
+ nodeName: nodeName,
50
+ };
51
+
52
+ return (
53
+ <Formik initialValues={initialValues} onSubmit={deleteNode}>
54
+ {function Render({ isSubmitting, status, setFieldValue }) {
55
+ return (
56
+ <Form className="deleteNode">
57
+ {displayMessageAfterSubmit(status)}
58
+ {
59
+ <>
60
+ <button
61
+ type="submit"
62
+ disabled={isSubmitting}
63
+ style={{
64
+ marginLeft: 0,
65
+ all: 'unset',
66
+ color: '#005c72',
67
+ cursor: 'pointer',
68
+ }}
69
+ >
70
+ <DeleteIcon />
71
+ </button>
72
+ </>
73
+ }
74
+ </Form>
75
+ );
76
+ }}
77
+ </Formik>
78
+ );
79
+ }
@@ -8,7 +8,14 @@ export default class ListGroupItem extends Component {
8
8
  <div className="d-flex gap-2 w-100 justify-content-between py-3">
9
9
  <div>
10
10
  <h6 className="mb-0 w-100">{label}</h6>
11
- <p className="mb-0 opacity-75">{value}</p>
11
+ <p
12
+ className="mb-0 opacity-75"
13
+ role="dialog"
14
+ aria-hidden="false"
15
+ aria-label={label}
16
+ >
17
+ {value}
18
+ </p>
12
19
  </div>
13
20
  </div>
14
21
  </div>
@@ -43,8 +43,8 @@ export default function QueryInfo({
43
43
  <td>{started}</td>
44
44
  <td>
45
45
  {errors?.length ? (
46
- errors.map(e => (
47
- <p>
46
+ errors.map((e, idx) => (
47
+ <p key={`error-${idx}`}>
48
48
  <span className="rounded-pill badge bg-secondary-error">
49
49
  {e}
50
50
  </span>
@@ -56,8 +56,8 @@ export default function QueryInfo({
56
56
  </td>
57
57
  <td>
58
58
  {links?.length ? (
59
- links.map(link => (
60
- <p>
59
+ links.map((link, idx) => (
60
+ <p key={idx}>
61
61
  <a href={link} target="_blank" rel="noreferrer">
62
62
  {link}
63
63
  </a>
@@ -7,7 +7,15 @@ export default class Tab extends Component {
7
7
  <div className={selectedTab === id ? 'col active' : 'col'}>
8
8
  <div className="header-tabs nav-overflow nav nav-tabs">
9
9
  <div className="nav-item">
10
- <button id={id} className="nav-link" tabIndex="0" onClick={onClick}>
10
+ <button
11
+ id={id}
12
+ className="nav-link"
13
+ tabIndex="0"
14
+ onClick={onClick}
15
+ role="button"
16
+ aria-label={this.props.name}
17
+ aria-hidden="false"
18
+ >
11
19
  {this.props.name}
12
20
  </button>
13
21
  </div>
@@ -4,6 +4,9 @@ const ToggleSwitch = ({ checked, onChange, toggleName }) => (
4
4
  <>
5
5
  <input
6
6
  id="show-compiled-sql-toggle"
7
+ role="checkbox"
8
+ aria-label="ToggleSwitch"
9
+ aria-hidden="false"
7
10
  type="checkbox"
8
11
  className="checkbox"
9
12
  checked={checked}
@@ -0,0 +1,55 @@
1
+ import * as React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import QueryInfo from '../QueryInfo';
4
+
5
+ describe('<QueryInfo />', () => {
6
+ const defaultProps = {
7
+ id: '123',
8
+ state: 'Running',
9
+ engine_name: 'Spark SQL',
10
+ engine_version: '1.0',
11
+ errors: ['Error 1', 'Error 2'],
12
+ links: ['http://example.com', 'http://example2.com'],
13
+ output_table: 'table1',
14
+ scheduled: '2023-09-06',
15
+ started: '2023-09-07',
16
+ numRows: 1000,
17
+ };
18
+
19
+ it('renders without crashing', () => {
20
+ render(<QueryInfo {...defaultProps} />);
21
+ });
22
+
23
+ it('displays correct query information', () => {
24
+ render(<QueryInfo {...defaultProps} />);
25
+
26
+ expect(screen.getByText(defaultProps.id)).toBeInTheDocument();
27
+ expect(
28
+ screen.getByText(
29
+ `${defaultProps.engine_name} - ${defaultProps.engine_version}`,
30
+ ),
31
+ ).toBeInTheDocument();
32
+ expect(screen.getByText(defaultProps.state)).toBeInTheDocument();
33
+ expect(screen.getByText(defaultProps.scheduled)).toBeInTheDocument();
34
+ expect(screen.getByText(defaultProps.started)).toBeInTheDocument();
35
+ expect(screen.getByText(defaultProps.output_table)).toBeInTheDocument();
36
+ expect(screen.getByText(String(defaultProps.numRows))).toBeInTheDocument();
37
+ defaultProps.errors.forEach(error => {
38
+ expect(screen.getByText(error)).toBeInTheDocument();
39
+ });
40
+ defaultProps.links.forEach(link => {
41
+ expect(screen.getByText(link)).toHaveAttribute('href', link);
42
+ });
43
+ });
44
+
45
+ it('does not render errors and links when they are not provided', () => {
46
+ render(<QueryInfo {...defaultProps} errors={[]} links={[]} />);
47
+
48
+ defaultProps.errors.forEach(error => {
49
+ expect(screen.queryByText(error)).not.toBeInTheDocument();
50
+ });
51
+ defaultProps.links.forEach(link => {
52
+ expect(screen.queryByText(link)).not.toBeInTheDocument();
53
+ });
54
+ });
55
+ });
@@ -0,0 +1,27 @@
1
+ import * as React from 'react';
2
+ import { render, fireEvent } from '@testing-library/react';
3
+
4
+ import Tab from '../Tab';
5
+
6
+ describe('<Tab />', () => {
7
+ it('renders without crashing', () => {
8
+ render(<Tab />);
9
+ });
10
+
11
+ it('has the active class when selectedTab matches id', () => {
12
+ const { container } = render(<Tab id="1" selectedTab="1" />);
13
+ expect(container.querySelector('.col')).toHaveClass('active');
14
+ });
15
+
16
+ it('does not have the active class when selectedTab does not match id', () => {
17
+ const { container } = render(<Tab id="1" selectedTab="2" />);
18
+ expect(container.querySelector('.col')).not.toHaveClass('active');
19
+ });
20
+
21
+ it('calls onClick when the button is clicked', () => {
22
+ const onClickMock = jest.fn();
23
+ const { getByRole } = render(<Tab id="1" onClick={onClickMock} />);
24
+ fireEvent.click(getByRole('button'));
25
+ expect(onClickMock).toHaveBeenCalledTimes(1);
26
+ });
27
+ });
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen } from '@testing-library/react';
3
+ import ToggleSwitch from '../ToggleSwitch';
4
+
5
+ describe('<ToggleSwitch />', () => {
6
+ const defaultProps = {
7
+ checked: false,
8
+ onChange: jest.fn(),
9
+ toggleName: 'Toggle Switch',
10
+ };
11
+
12
+ it('renders without crashing', () => {
13
+ render(<ToggleSwitch {...defaultProps} />);
14
+ });
15
+
16
+ it('displays the correct toggle name', () => {
17
+ render(<ToggleSwitch {...defaultProps} />);
18
+ expect(screen.getByText(defaultProps.toggleName)).toBeInTheDocument();
19
+ });
20
+
21
+ it('reflects the checked state correctly', () => {
22
+ render(<ToggleSwitch {...defaultProps} checked={true} />);
23
+ const checkbox = screen.getByRole('checkbox');
24
+ expect(checkbox).toBeChecked();
25
+ });
26
+
27
+ it('calls onChange with the correct value when toggled', () => {
28
+ render(<ToggleSwitch {...defaultProps} />);
29
+ const checkbox = screen.getByRole('checkbox');
30
+
31
+ fireEvent.click(checkbox);
32
+ expect(defaultProps.onChange).toHaveBeenCalledWith(true);
33
+
34
+ fireEvent.click(checkbox);
35
+ expect(checkbox).not.toBeChecked();
36
+ });
37
+
38
+ it('is unchecked by default if no checked prop is provided', () => {
39
+ render(<ToggleSwitch onChange={jest.fn()} toggleName="Test Toggle" />);
40
+ const checkbox = screen.getByRole('checkbox');
41
+ expect(checkbox).not.toBeChecked();
42
+ });
43
+ });
@@ -14,7 +14,10 @@ exports[`<ListGroupItem /> should render and match the snapshot 1`] = `
14
14
  Name
15
15
  </h6>
16
16
  <p
17
+ aria-hidden="false"
18
+ aria-label="Name"
17
19
  className="mb-0 opacity-75"
20
+ role="dialog"
18
21
  >
19
22
  <span>
20
23
  Something
@@ -32,7 +32,10 @@ export function DJNodeColumns({ data, limit }) {
32
32
  };
33
33
  };
34
34
  return data.column_names.slice(0, limit).map(col => (
35
- <div className={'custom-node-subheader node_type__' + data.type}>
35
+ <div
36
+ className={'custom-node-subheader node_type__' + data.type}
37
+ key={`${data.name}.${col.name}`}
38
+ >
36
39
  <div style={handleWrapperStyle}>
37
40
  <Handle
38
41
  type="target"
@@ -42,10 +42,17 @@ export function DJNodeDimensions(data) {
42
42
  Object.entries(grouped).map(([dimKey, dimValue]) => {
43
43
  if (Array.isArray(dimValue.columns)) {
44
44
  const attributes = dimValue.columns.map(col => {
45
- return <span className={'badge white_badge'}>{col}</span>;
45
+ return (
46
+ <span className={'badge white_badge'} key={`attr-${col}`}>
47
+ {col}
48
+ </span>
49
+ );
46
50
  });
47
51
  return (
48
- <div className={'custom-node-subheader node_type__' + data.type}>
52
+ <div
53
+ className={'custom-node-subheader node_type__' + data.type}
54
+ key={`dim-${dimValue.path}-${dimValue.dimension}`}
55
+ >
49
56
  <div className="custom-node-port">
50
57
  <a href={`/nodes/${dimValue.dimension}`}>{dimValue.dimension}</a>{' '}
51
58
  <div className={'badge node_type__metric text-black'}>
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen, waitFor } from '@testing-library/react';
3
+ import Collapse from '../Collapse';
4
+ import { mocks } from '../../../../mocks/mockNodes';
5
+ import { ReactFlowProvider } from 'reactflow';
6
+
7
+ jest.mock('../DJNodeDimensions', () => ({
8
+ DJNodeDimensions: jest.fn(data => <div>DJNodeDimensions content</div>),
9
+ }));
10
+
11
+ describe('<Collapse />', () => {
12
+ const defaultProps = {
13
+ collapsed: true,
14
+ text: 'Dimensions',
15
+ data: mocks.mockMetricNode,
16
+ };
17
+
18
+ it('renders without crashing', () => {
19
+ render(<Collapse {...defaultProps} />);
20
+ });
21
+
22
+ it('renders toggle for metric type', () => {
23
+ const { getByText } = render(
24
+ <Collapse {...defaultProps} data={mocks.mockMetricNode} />,
25
+ );
26
+ const button = screen.getByText('▶ Show Dimensions');
27
+ fireEvent.click(button);
28
+ expect(getByText('▼ Hide Dimensions')).toBeInTheDocument();
29
+ });
30
+
31
+ it('toggles More/Less button text for non-metric type on click', () => {
32
+ defaultProps.text = 'Columns';
33
+ const { getByText } = render(
34
+ <ReactFlowProvider>
35
+ <Collapse
36
+ {...defaultProps}
37
+ data={{
38
+ type: 'transform',
39
+ column_names: Array(11).fill(idx => {
40
+ return `column-${idx}`;
41
+ }),
42
+ primary_key: [],
43
+ }}
44
+ />
45
+ </ReactFlowProvider>,
46
+ );
47
+ const button = getByText('▶ More Columns');
48
+ fireEvent.click(button);
49
+ expect(getByText('▼ Less Columns')).toBeInTheDocument();
50
+ });
51
+ });
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { DJNodeColumns } from '../DJNodeColumns';
4
+ import { ReactFlowProvider } from 'reactflow';
5
+
6
+ describe('<DJNodeColumns />', () => {
7
+ const defaultProps = {
8
+ data: {
9
+ name: 'TestName',
10
+ type: 'metric',
11
+ column_names: [
12
+ { name: 'col1', type: 'int' },
13
+ { name: 'col2', type: 'string' },
14
+ { name: 'col3', type: 'float' },
15
+ ],
16
+ primary_key: ['col1'],
17
+ },
18
+ limit: 10,
19
+ };
20
+
21
+ const domTestingLib = require('@testing-library/dom');
22
+ const { queryHelpers } = domTestingLib;
23
+
24
+ const queryByAttribute = attribute =>
25
+ queryHelpers.queryAllByAttribute.bind(null, attribute);
26
+
27
+ function getByAttribute(container, id, attribute, ...rest) {
28
+ const result = queryByAttribute(attribute)(container, id, ...rest);
29
+ return result[0];
30
+ }
31
+
32
+ function DJNodeColumnsWithProvider(props) {
33
+ return (
34
+ <ReactFlowProvider>
35
+ <DJNodeColumns {...props} />
36
+ </ReactFlowProvider>
37
+ );
38
+ }
39
+
40
+ it('renders without crashing', () => {
41
+ render(<DJNodeColumnsWithProvider {...defaultProps} />);
42
+ });
43
+
44
+ it('renders columns correctly', () => {
45
+ const { container } = render(
46
+ <DJNodeColumnsWithProvider {...defaultProps} />,
47
+ );
48
+
49
+ // renders column names
50
+ defaultProps.data.column_names.forEach(col => {
51
+ expect(screen.getByText(col.name, { exact: false })).toBeInTheDocument();
52
+ });
53
+
54
+ // appends (PK) to primary key column name
55
+ expect(screen.getByText('col1 (PK)')).toBeInTheDocument();
56
+
57
+ // renders column type badge correctly
58
+ defaultProps.data.column_names.forEach(col => {
59
+ expect(screen.getByText(col.type)).toBeInTheDocument();
60
+ });
61
+
62
+ // renders handles correctly
63
+ defaultProps.data.column_names.forEach(col => {
64
+ expect(
65
+ getByAttribute(
66
+ container,
67
+ defaultProps.data.name + '.' + col.name,
68
+ 'data-handleid',
69
+ ),
70
+ ).toBeInTheDocument();
71
+ });
72
+ });
73
+
74
+ it('renders limited columns based on the limit prop', () => {
75
+ const limitedProps = {
76
+ ...defaultProps,
77
+ limit: 2,
78
+ };
79
+
80
+ render(<DJNodeColumnsWithProvider {...limitedProps} />);
81
+ expect(screen.queryByText('col3')).not.toBeInTheDocument();
82
+ });
83
+ });
@@ -0,0 +1,118 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import { DJNodeDimensions } from '../DJNodeDimensions';
4
+ import DJClientContext from '../../../providers/djclient';
5
+
6
+ const mockMetric = jest.fn();
7
+ describe('<DJNodeDimensions />', () => {
8
+ const defaultProps = {
9
+ type: 'metric',
10
+ name: 'TestMetric',
11
+ };
12
+ const mockDJClient = () => {
13
+ return {
14
+ DataJunctionAPI: {
15
+ metric: mockMetric,
16
+ },
17
+ };
18
+ };
19
+
20
+ const DJNodeDimensionsWithContext = (djClient, props) => {
21
+ return (
22
+ <DJClientContext.Provider value={djClient}>
23
+ <DJNodeDimensions {...props} />
24
+ </DJClientContext.Provider>
25
+ );
26
+ };
27
+
28
+ beforeEach(() => {
29
+ // Reset the mock before each test
30
+ mockMetric.mockReset();
31
+ });
32
+
33
+ it('fetches dimensions for metric type', async () => {
34
+ mockMetric.mockResolvedValueOnce({
35
+ dimensions: [{ name: 'test.dimension' }],
36
+ });
37
+ const djClient = mockDJClient();
38
+
39
+ render(
40
+ <DJClientContext.Provider value={djClient}>
41
+ <DJNodeDimensions {...defaultProps} />
42
+ </DJClientContext.Provider>,
43
+ );
44
+ waitFor(() => {
45
+ expect(mockMetric).toHaveBeenCalledWith(defaultProps.name);
46
+ });
47
+ });
48
+
49
+ it('renders dimensions correctly after processing', async () => {
50
+ const testDimensions = [
51
+ {
52
+ name: 'default.us_state.state_name',
53
+ type: 'string',
54
+ path: [
55
+ 'default.repair_order_details.repair_order_id',
56
+ 'default.repair_order.hard_hat_id',
57
+ 'default.hard_hat.state',
58
+ ],
59
+ },
60
+ {
61
+ name: 'default.us_state.state_region',
62
+ type: 'int',
63
+ path: [
64
+ 'default.repair_order_details.repair_order_id',
65
+ 'default.repair_order.hard_hat_id',
66
+ 'default.hard_hat.state',
67
+ ],
68
+ },
69
+ {
70
+ name: 'default.us_state.state_region_description',
71
+ type: 'string',
72
+ path: [
73
+ 'default.repair_order_details.repair_order_id',
74
+ 'default.repair_order.hard_hat_id',
75
+ 'default.hard_hat.state',
76
+ ],
77
+ },
78
+ ];
79
+ mockMetric.mockResolvedValueOnce({ dimensions: testDimensions });
80
+ const djClient = mockDJClient();
81
+
82
+ const { findByText } = render(
83
+ <DJClientContext.Provider value={djClient}>
84
+ <DJNodeDimensions {...defaultProps} />
85
+ </DJClientContext.Provider>,
86
+ );
87
+
88
+ for (const dim of testDimensions) {
89
+ const [attribute, ...nodeName] = dim.name.split('.').reverse();
90
+ const dimension = nodeName.reverse().join('.');
91
+ expect(await findByText(attribute)).toBeInTheDocument();
92
+ expect(await findByText(dimension)).toBeInTheDocument();
93
+ }
94
+ });
95
+
96
+ it('does not fetch dimensions if type is not metric', () => {
97
+ const djClient = mockDJClient();
98
+ render(
99
+ <DJClientContext.Provider value={djClient}>
100
+ <DJNodeDimensions {...defaultProps} type="transform" />
101
+ </DJClientContext.Provider>,
102
+ );
103
+ expect(mockMetric).not.toHaveBeenCalled();
104
+ });
105
+
106
+ it('handles errors gracefully', async () => {
107
+ mockMetric.mockRejectedValueOnce(new Error('API error'));
108
+
109
+ const djClient = mockDJClient();
110
+ render(
111
+ <DJClientContext.Provider value={djClient}>
112
+ <DJNodeDimensions {...defaultProps} />
113
+ </DJClientContext.Provider>,
114
+ );
115
+
116
+ expect(await mockMetric).toHaveBeenCalledWith(defaultProps.name);
117
+ });
118
+ });
package/src/app/index.tsx CHANGED
@@ -13,6 +13,7 @@ import { SQLBuilderPage } from './pages/SQLBuilderPage/Loadable';
13
13
  import { AddEditNodePage } from './pages/AddEditNodePage/Loadable';
14
14
  import { NotFoundPage } from './pages/NotFoundPage/Loadable';
15
15
  import { LoginPage } from './pages/LoginPage';
16
+ import { RegisterTablePage } from './pages/RegisterTablePage';
16
17
  import { Root } from './pages/Root/Loadable';
17
18
  import DJClientContext from './providers/djclient';
18
19
  import { DataJunctionAPI } from './services/DJService';
@@ -59,6 +60,11 @@ export function App() {
59
60
  key="namespaces"
60
61
  />
61
62
  </Route>
63
+ <Route
64
+ path="create/source"
65
+ key="register"
66
+ element={<RegisterTablePage />}
67
+ ></Route>
62
68
  <Route path="create/:nodeType">
63
69
  <Route
64
70
  path=":initialNamespace"