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.
- package/package.json +6 -5
- package/src/__tests__/reportWebVitals.test.jsx +44 -0
- package/src/app/components/DeleteNode.jsx +79 -0
- package/src/app/components/ListGroupItem.jsx +8 -1
- package/src/app/components/QueryInfo.jsx +4 -4
- package/src/app/components/Tab.jsx +9 -1
- package/src/app/components/ToggleSwitch.jsx +3 -0
- package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
- package/src/app/components/__tests__/Tab.test.jsx +27 -0
- package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
- package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +3 -0
- package/src/app/components/djgraph/DJNodeColumns.jsx +4 -1
- package/src/app/components/djgraph/DJNodeDimensions.jsx +9 -2
- package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
- package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
- package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
- package/src/app/index.tsx +6 -0
- package/src/app/pages/AddEditNodePage/FormikSelect.jsx +15 -2
- package/src/app/pages/AddEditNodePage/FullNameField.jsx +2 -1
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +77 -0
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +93 -0
- package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +34 -3
- package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +4 -2
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +53 -0
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +53 -0
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +0 -81
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +82 -257
- package/src/app/pages/AddEditNodePage/index.jsx +13 -5
- package/src/app/pages/LoginPage/__tests__/index.test.jsx +70 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +95 -0
- package/src/app/pages/NamespacePage/index.jsx +8 -5
- package/src/app/pages/NodePage/ClientCodePopover.jsx +3 -1
- package/src/app/pages/NodePage/EditColumnPopover.jsx +102 -0
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +135 -0
- package/src/app/pages/NodePage/NodeColumnTab.jsx +80 -17
- package/src/app/pages/NodePage/NodeHistory.jsx +63 -30
- package/src/app/pages/NodePage/NodeInfoTab.jsx +52 -7
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +50 -27
- package/src/app/pages/NodePage/NodeSQLTab.jsx +0 -10
- package/src/app/pages/NodePage/NodesWithDimension.jsx +4 -2
- package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +49 -0
- package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
- package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +165 -0
- package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +591 -0
- package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +57 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +725 -0
- package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +402 -0
- package/src/app/pages/NodePage/index.jsx +22 -6
- package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
- package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
- package/src/app/pages/RegisterTablePage/index.jsx +163 -0
- package/src/app/pages/Root/__tests__/index.test.jsx +77 -0
- package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
- package/src/app/pages/SQLBuilderPage/index.jsx +61 -43
- package/src/app/services/DJService.js +125 -54
- package/src/app/services/__tests__/DJService.test.jsx +609 -0
- package/src/mocks/mockNodes.jsx +1397 -0
- package/src/setupTests.ts +30 -0
- package/src/styles/index.css +43 -0
- package/src/styles/node-creation.scss +7 -0
- package/src/utils/form.jsx +23 -0
- package/.github/pull_request_template.md +0 -11
- package/.github/workflows/ci.yml +0 -33
- package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -118
- 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.
|
|
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
|
-
"
|
|
164
|
-
"
|
|
163
|
+
"statements": 90,
|
|
164
|
+
"branches": 75,
|
|
165
165
|
"lines": 90,
|
|
166
|
-
"
|
|
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
|
|
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
|
|
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>
|
|
@@ -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
|
+
});
|
|
@@ -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
|
|
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
|
|
45
|
+
return (
|
|
46
|
+
<span className={'badge white_badge'} key={`attr-${col}`}>
|
|
47
|
+
{col}
|
|
48
|
+
</span>
|
|
49
|
+
);
|
|
46
50
|
});
|
|
47
51
|
return (
|
|
48
|
-
<div
|
|
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"
|