datajunction-ui 0.0.1-rc.8 → 0.0.2

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 (205) hide show
  1. package/.env +2 -0
  2. package/.prettierignore +3 -1
  3. package/Makefile +9 -0
  4. package/dj-logo.svg +10 -0
  5. package/package.json +53 -14
  6. package/public/favicon.ico +0 -0
  7. package/public/index.html +1 -1
  8. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  9. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -109
  10. package/src/app/components/AddNodeDropdown.jsx +44 -0
  11. package/src/app/components/ListGroupItem.jsx +9 -1
  12. package/src/app/components/NamespaceHeader.jsx +4 -13
  13. package/src/app/components/NodeListActions.jsx +69 -0
  14. package/src/app/components/NodeMaterializationDelete.jsx +90 -0
  15. package/src/app/components/QueryInfo.jsx +172 -0
  16. package/src/app/components/Search.jsx +94 -0
  17. package/src/app/components/Tab.jsx +8 -1
  18. package/src/app/components/ToggleSwitch.jsx +20 -0
  19. package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
  20. package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
  21. package/src/app/components/__tests__/Search.test.jsx +63 -0
  22. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  23. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  24. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +8 -3
  25. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
  26. package/src/app/components/djgraph/Collapse.jsx +47 -0
  27. package/src/app/components/djgraph/DJNode.jsx +61 -83
  28. package/src/app/components/djgraph/DJNodeColumns.jsx +75 -0
  29. package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
  30. package/src/app/components/djgraph/LayoutFlow.jsx +106 -0
  31. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  32. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  33. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  34. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
  35. package/src/app/components/forms/Action.jsx +8 -0
  36. package/src/app/components/forms/NodeNameField.jsx +64 -0
  37. package/src/app/components/search.css +17 -0
  38. package/src/app/constants.js +2 -0
  39. package/src/app/icons/AddItemIcon.jsx +16 -0
  40. package/src/app/icons/AlertIcon.jsx +33 -0
  41. package/src/app/icons/CollapsedIcon.jsx +15 -0
  42. package/src/app/icons/CommitIcon.jsx +45 -0
  43. package/src/app/icons/DJLogo.jsx +36 -0
  44. package/src/app/icons/DeleteIcon.jsx +21 -0
  45. package/src/app/icons/DiffIcon.jsx +63 -0
  46. package/src/app/icons/EditIcon.jsx +18 -0
  47. package/src/app/icons/ExpandedIcon.jsx +15 -0
  48. package/src/app/icons/EyeIcon.jsx +20 -0
  49. package/src/app/icons/FilterIcon.jsx +7 -0
  50. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  51. package/src/app/icons/InvalidIcon.jsx +16 -0
  52. package/src/app/icons/JupyterExportIcon.jsx +25 -0
  53. package/src/app/icons/LoadingIcon.jsx +14 -0
  54. package/src/app/icons/NodeIcon.jsx +49 -0
  55. package/src/app/icons/PythonIcon.jsx +14 -0
  56. package/src/app/icons/TableIcon.jsx +14 -0
  57. package/src/app/icons/ValidIcon.jsx +16 -0
  58. package/src/app/index.tsx +118 -37
  59. package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
  60. package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
  61. package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
  62. package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
  63. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +51 -0
  64. package/src/app/pages/AddEditNodePage/FullNameField.jsx +38 -0
  65. package/src/app/pages/AddEditNodePage/Loadable.jsx +20 -0
  66. package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
  67. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
  68. package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
  69. package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
  70. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +94 -0
  71. package/src/app/pages/AddEditNodePage/OwnersField.jsx +54 -0
  72. package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
  73. package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
  74. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
  75. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +109 -0
  76. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +287 -0
  77. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
  78. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
  79. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  80. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +54 -0
  81. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +3 -0
  82. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
  83. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +224 -0
  84. package/src/app/pages/AddEditNodePage/index.jsx +506 -0
  85. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  86. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  87. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  88. package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +152 -0
  89. package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
  90. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +75 -0
  91. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +373 -0
  92. package/src/app/pages/CubeBuilderPage/index.jsx +291 -0
  93. package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
  94. package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
  95. package/src/app/pages/LoginPage/__tests__/index.test.jsx +97 -0
  96. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  97. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  98. package/src/app/pages/LoginPage/index.jsx +17 -0
  99. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +85 -0
  100. package/src/app/pages/NamespacePage/Explorer.jsx +61 -0
  101. package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
  102. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
  103. package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
  104. package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
  105. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +297 -0
  106. package/src/app/pages/NamespacePage/index.jsx +319 -42
  107. package/src/app/pages/NodePage/AddBackfillPopover.jsx +165 -0
  108. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +222 -0
  109. package/src/app/pages/NodePage/AvailabilityStateBlock.jsx +67 -0
  110. package/src/app/pages/NodePage/ClientCodePopover.jsx +94 -0
  111. package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
  112. package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
  113. package/src/app/pages/NodePage/EditColumnPopover.jsx +116 -0
  114. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +164 -0
  115. package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
  116. package/src/app/pages/NodePage/NodeColumnTab.jsx +256 -30
  117. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
  118. package/src/app/pages/NodePage/NodeGraphTab.jsx +123 -150
  119. package/src/app/pages/NodePage/NodeHistory.jsx +236 -0
  120. package/src/app/pages/NodePage/NodeInfoTab.jsx +325 -49
  121. package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
  122. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +585 -0
  123. package/src/app/pages/NodePage/NodeRevisionMaterializationTab.jsx +58 -0
  124. package/src/app/pages/NodePage/NodeStatus.jsx +100 -31
  125. package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
  126. package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
  127. package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
  128. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +151 -0
  129. package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
  130. package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
  131. package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
  132. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +56 -0
  133. package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
  134. package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
  135. package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
  136. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
  137. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +161 -0
  138. package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
  139. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
  140. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +595 -0
  141. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +58 -0
  142. package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +190 -0
  143. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +872 -0
  144. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  145. package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
  146. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +19 -0
  147. package/src/app/pages/NodePage/index.jsx +190 -44
  148. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  149. package/src/app/pages/OverviewPage/ByStatusPanel.jsx +69 -0
  150. package/src/app/pages/OverviewPage/DimensionNodeUsagePanel.jsx +48 -0
  151. package/src/app/pages/OverviewPage/GovernanceWarningsPanel.jsx +107 -0
  152. package/src/app/pages/OverviewPage/Loadable.jsx +16 -0
  153. package/src/app/pages/OverviewPage/NodesByTypePanel.jsx +63 -0
  154. package/src/app/pages/OverviewPage/OverviewPanel.jsx +94 -0
  155. package/src/app/pages/OverviewPage/TrendsPanel.jsx +66 -0
  156. package/src/app/pages/OverviewPage/__tests__/ByStatusPanel.test.jsx +36 -0
  157. package/src/app/pages/OverviewPage/__tests__/DimensionNodeUsagePanel.test.jsx +76 -0
  158. package/src/app/pages/OverviewPage/__tests__/GovernanceWarningsPanel.test.jsx +77 -0
  159. package/src/app/pages/OverviewPage/__tests__/NodesByTypePanel.test.jsx +86 -0
  160. package/src/app/pages/OverviewPage/__tests__/OverviewPanel.test.jsx +78 -0
  161. package/src/app/pages/OverviewPage/__tests__/TrendsPanel.test.jsx +120 -0
  162. package/src/app/pages/OverviewPage/__tests__/index.test.jsx +54 -0
  163. package/src/app/pages/OverviewPage/index.jsx +22 -0
  164. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  165. package/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx +110 -0
  166. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +38 -0
  167. package/src/app/pages/RegisterTablePage/index.jsx +142 -0
  168. package/src/app/pages/Root/__tests__/index.test.jsx +79 -0
  169. package/src/app/pages/Root/index.tsx +84 -6
  170. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  171. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  172. package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
  173. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  174. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  175. package/src/app/pages/TagPage/index.jsx +79 -0
  176. package/src/app/services/DJService.js +1314 -21
  177. package/src/app/services/__tests__/DJService.test.jsx +1559 -0
  178. package/src/index.tsx +1 -0
  179. package/src/mocks/mockNodes.jsx +1474 -0
  180. package/src/setupTests.ts +31 -1
  181. package/src/styles/dag.css +117 -5
  182. package/src/styles/index.css +1027 -30
  183. package/src/styles/loading.css +34 -0
  184. package/src/styles/login.css +81 -0
  185. package/src/styles/node-creation.scss +276 -0
  186. package/src/styles/node-list.css +4 -0
  187. package/src/styles/overview.css +72 -0
  188. package/src/styles/sorted-table.css +15 -0
  189. package/src/styles/styles.scss +44 -0
  190. package/src/styles/styles.scss.d.ts +9 -0
  191. package/src/utils/form.jsx +23 -0
  192. package/webpack.config.js +16 -6
  193. package/.babelrc +0 -4
  194. package/.env.local +0 -4
  195. package/.env.production +0 -1
  196. package/.github/pull_request_template.md +0 -11
  197. package/.github/workflows/ci.yml +0 -33
  198. package/.vscode/extensions.json +0 -7
  199. package/.vscode/launch.json +0 -15
  200. package/.vscode/settings.json +0 -25
  201. package/Dockerfile +0 -7
  202. package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
  203. package/src/app/pages/ListNamespacesPage/index.jsx +0 -62
  204. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
  205. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
@@ -0,0 +1,86 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import DJClientContext from '../../../providers/djclient';
3
+ import { NodesByTypePanel } from '../NodesByTypePanel';
4
+
5
+ describe('<NodesByTypePanel />', () => {
6
+ it('fetches nodes & materializations by type and renders them correctly', async () => {
7
+ const mockNodesByType = [
8
+ { name: 'source', value: 5 },
9
+ { name: 'cube', value: 2 },
10
+ ];
11
+ const mockMaterializationsByType = [
12
+ { name: 'transform', value: 3 },
13
+ { name: 'metric', value: 1 },
14
+ ];
15
+
16
+ const mockDjClient = {
17
+ system: {
18
+ node_counts_by_type: jest.fn().mockResolvedValue(mockNodesByType),
19
+ materialization_counts_by_type: jest
20
+ .fn()
21
+ .mockResolvedValue(mockMaterializationsByType),
22
+ },
23
+ };
24
+
25
+ render(
26
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
27
+ <NodesByTypePanel />
28
+ </DJClientContext.Provider>,
29
+ );
30
+
31
+ // Wait for async calls to complete
32
+ await waitFor(() => {
33
+ expect(mockDjClient.system.node_counts_by_type).toHaveBeenCalled();
34
+ expect(
35
+ mockDjClient.system.materialization_counts_by_type,
36
+ ).toHaveBeenCalled();
37
+ });
38
+
39
+ // Check that nodes by type appear
40
+ expect(screen.getByText('Nodes by Type')).toBeInTheDocument();
41
+ expect(screen.getByText('5')).toBeInTheDocument();
42
+ expect(screen.getByText('sources')).toBeInTheDocument();
43
+ expect(screen.getByText('2')).toBeInTheDocument();
44
+ expect(screen.getByText('cubes')).toBeInTheDocument();
45
+
46
+ // Check that materializations by type appear
47
+ expect(screen.getByText('Materializations by Type')).toBeInTheDocument();
48
+ expect(screen.getByText('3')).toBeInTheDocument();
49
+ expect(screen.getByText('transforms')).toBeInTheDocument();
50
+ expect(screen.getByText('1')).toBeInTheDocument();
51
+ expect(screen.getByText('metrics')).toBeInTheDocument();
52
+
53
+ // Should render an icon for each entry
54
+ const icons = screen.getAllByTestId('node-icon');
55
+ expect(icons).toHaveLength(
56
+ mockNodesByType.length + mockMaterializationsByType.length,
57
+ );
58
+ });
59
+
60
+ it('renders nothing if no data returned', async () => {
61
+ const mockDjClient = {
62
+ system: {
63
+ node_counts_by_type: jest.fn().mockResolvedValue([]),
64
+ materialization_counts_by_type: jest.fn().mockResolvedValue([]),
65
+ },
66
+ };
67
+
68
+ render(
69
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
70
+ <NodesByTypePanel />
71
+ </DJClientContext.Provider>,
72
+ );
73
+
74
+ await waitFor(() => {
75
+ expect(mockDjClient.system.node_counts_by_type).toHaveBeenCalled();
76
+ expect(
77
+ mockDjClient.system.materialization_counts_by_type,
78
+ ).toHaveBeenCalled();
79
+ });
80
+
81
+ expect(screen.getByText('Nodes by Type')).toBeInTheDocument();
82
+ expect(screen.getByText('Materializations by Type')).toBeInTheDocument();
83
+ expect(screen.queryByText('sources')).not.toBeInTheDocument();
84
+ expect(screen.queryByText('metrics')).not.toBeInTheDocument();
85
+ });
86
+ });
@@ -0,0 +1,78 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import DJClientContext from '../../../providers/djclient';
3
+ import { OverviewPanel } from '../OverviewPanel';
4
+
5
+ describe('<OverviewPanel />', () => {
6
+ it('renders Active Nodes and Valid/Invalid nodes correctly', async () => {
7
+ const mockNodesByActive = [
8
+ { name: 'true', value: 7 },
9
+ { name: 'false', value: 2 }, // Should be filtered out
10
+ ];
11
+
12
+ const mockNodesByStatus = [
13
+ { name: 'VALID', value: 5 },
14
+ { name: 'INVALID', value: 3 },
15
+ ];
16
+
17
+ const mockDjClient = {
18
+ system: {
19
+ node_counts_by_active: jest.fn().mockResolvedValue(mockNodesByActive),
20
+ node_counts_by_status: jest.fn().mockResolvedValue(mockNodesByStatus),
21
+ },
22
+ };
23
+
24
+ render(
25
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
26
+ <OverviewPanel />
27
+ </DJClientContext.Provider>,
28
+ );
29
+
30
+ await waitFor(() => {
31
+ expect(mockDjClient.system.node_counts_by_active).toHaveBeenCalled();
32
+ expect(mockDjClient.system.node_counts_by_status).toHaveBeenCalled();
33
+ });
34
+
35
+ // Chart title
36
+ expect(screen.getByText('Overview')).toBeInTheDocument();
37
+
38
+ // Active Nodes section
39
+ expect(screen.getByText('7')).toBeInTheDocument();
40
+ expect(screen.getByText('Active Nodes')).toBeInTheDocument();
41
+ expect(screen.getAllByTestId('node-icon')).toHaveLength(1);
42
+
43
+ // Valid/Invalid status section
44
+ expect(screen.getByText('5')).toBeInTheDocument();
45
+ expect(screen.getByText('valid')).toBeInTheDocument();
46
+ expect(screen.getByText('3')).toBeInTheDocument();
47
+ expect(screen.getByText('invalid')).toBeInTheDocument();
48
+
49
+ // Should render one ValidIcon and one InvalidIcon
50
+ expect(screen.getAllByTestId('valid-icon')).toHaveLength(1);
51
+ expect(screen.getAllByTestId('invalid-icon')).toHaveLength(1);
52
+ });
53
+
54
+ it('renders no badges if data is empty', async () => {
55
+ const mockDjClient = {
56
+ system: {
57
+ node_counts_by_active: jest.fn().mockResolvedValue([]),
58
+ node_counts_by_status: jest.fn().mockResolvedValue([]),
59
+ },
60
+ };
61
+
62
+ render(
63
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
64
+ <OverviewPanel />
65
+ </DJClientContext.Provider>,
66
+ );
67
+
68
+ await waitFor(() => {
69
+ expect(mockDjClient.system.node_counts_by_active).toHaveBeenCalled();
70
+ expect(mockDjClient.system.node_counts_by_status).toHaveBeenCalled();
71
+ });
72
+
73
+ expect(screen.getByText('Overview')).toBeInTheDocument();
74
+ expect(screen.queryByTestId('node-icon')).not.toBeInTheDocument();
75
+ expect(screen.queryByTestId('valid-icon')).not.toBeInTheDocument();
76
+ expect(screen.queryByTestId('invalid-icon')).not.toBeInTheDocument();
77
+ });
78
+ });
@@ -0,0 +1,120 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import DJClientContext from '../../../providers/djclient';
3
+ import { TrendsPanel } from '../TrendsPanel';
4
+
5
+ jest.mock('recharts', () => {
6
+ const Original = jest.requireActual('recharts');
7
+ return {
8
+ ...Original,
9
+ ResponsiveContainer: ({ children, ...props }) => (
10
+ <div data-testid="responsive-container">{children}</div>
11
+ ),
12
+ BarChart: ({ children, data, ...props }) => (
13
+ <div data-testid="barchart" data-data={JSON.stringify(data)}>
14
+ {children}
15
+ </div>
16
+ ),
17
+ CartesianGrid: props => <div data-testid="cartesian-grid" />,
18
+ XAxis: props => <div data-testid="x-axis" />,
19
+ YAxis: props => <div data-testid="y-axis" />,
20
+ Tooltip: props => <div data-testid="tooltip" />,
21
+ Legend: props => <div data-testid="legend" />,
22
+ Bar: props => (
23
+ <div data-testid={`bar-${props.dataKey}`} data-fill={props.fill} />
24
+ ),
25
+ };
26
+ });
27
+
28
+ describe('<TrendsPanel />', () => {
29
+ it('fetches and renders node trends', async () => {
30
+ const mockNodeTrends = [
31
+ {
32
+ date: '2024-01-01',
33
+ source: 2,
34
+ dimension: 1,
35
+ transform: 3,
36
+ metric: 5,
37
+ cube: 0,
38
+ },
39
+ {
40
+ date: '2024-01-02',
41
+ source: 1,
42
+ dimension: 2,
43
+ transform: 1,
44
+ metric: 3,
45
+ cube: 4,
46
+ },
47
+ ];
48
+
49
+ const mockDjClient = {
50
+ system: {
51
+ node_trends: jest.fn().mockResolvedValue(mockNodeTrends),
52
+ },
53
+ };
54
+
55
+ render(
56
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
57
+ <TrendsPanel />
58
+ </DJClientContext.Provider>,
59
+ );
60
+
61
+ await waitFor(() => {
62
+ expect(mockDjClient.system.node_trends).toHaveBeenCalled();
63
+ });
64
+
65
+ // Chart title
66
+ expect(screen.getByText('Trends')).toBeInTheDocument();
67
+
68
+ // The Recharts containers render
69
+ expect(screen.getByTestId('responsive-container')).toBeInTheDocument();
70
+ expect(screen.getByTestId('barchart')).toBeInTheDocument();
71
+
72
+ // Should contain grid, axes, tooltip, legend
73
+ expect(screen.getByTestId('cartesian-grid')).toBeInTheDocument();
74
+ expect(screen.getByTestId('x-axis')).toBeInTheDocument();
75
+ expect(screen.getByTestId('y-axis')).toBeInTheDocument();
76
+ expect(screen.getByTestId('tooltip')).toBeInTheDocument();
77
+ expect(screen.getByTestId('legend')).toBeInTheDocument();
78
+
79
+ // Should render bars with expected colors
80
+ const expectedBars = {
81
+ source: '#00C49F',
82
+ dimension: '#FFBB28',
83
+ transform: '#0088FE',
84
+ metric: '#FF91A3',
85
+ cube: '#AA46BE',
86
+ };
87
+
88
+ Object.entries(expectedBars).forEach(([key, color]) => {
89
+ const bar = screen.getByTestId(`bar-${key}`);
90
+ expect(bar).toBeInTheDocument();
91
+ expect(bar).toHaveAttribute('data-fill', color);
92
+ });
93
+
94
+ // BarChart gets correct data
95
+ const barChart = screen.getByTestId('barchart');
96
+ expect(barChart.getAttribute('data-data')).toContain('2024-01-01');
97
+ });
98
+
99
+ it('renders with empty trends', async () => {
100
+ const mockDjClient = {
101
+ system: {
102
+ node_trends: jest.fn().mockResolvedValue([]),
103
+ },
104
+ };
105
+
106
+ render(
107
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
108
+ <TrendsPanel />
109
+ </DJClientContext.Provider>,
110
+ );
111
+
112
+ await waitFor(() => {
113
+ expect(mockDjClient.system.node_trends).toHaveBeenCalled();
114
+ });
115
+
116
+ expect(screen.getByText('Trends')).toBeInTheDocument();
117
+ expect(screen.getByTestId('responsive-container')).toBeInTheDocument();
118
+ expect(screen.getByTestId('barchart')).toBeInTheDocument();
119
+ });
120
+ });
@@ -0,0 +1,54 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { OverviewPage } from '../index';
3
+
4
+ jest.mock('../OverviewPanel', () => ({
5
+ OverviewPanel: () => <div data-testid="overview-panel">OverviewPanel</div>,
6
+ }));
7
+ jest.mock('../NodesByTypePanel', () => ({
8
+ NodesByTypePanel: () => (
9
+ <div data-testid="nodes-by-type-panel">NodesByTypePanel</div>
10
+ ),
11
+ }));
12
+ jest.mock('../GovernanceWarningsPanel', () => ({
13
+ GovernanceWarningsPanel: () => (
14
+ <div data-testid="governance-warnings-panel">GovernanceWarningsPanel</div>
15
+ ),
16
+ }));
17
+ jest.mock('../TrendsPanel', () => ({
18
+ TrendsPanel: () => <div data-testid="trends-panel">TrendsPanel</div>,
19
+ }));
20
+ jest.mock('../DimensionNodeUsagePanel', () => ({
21
+ DimensionNodeUsagePanel: () => (
22
+ <div data-testid="dimension-node-usage-panel">DimensionNodeUsagePanel</div>
23
+ ),
24
+ }));
25
+
26
+ describe('<OverviewPage />', () => {
27
+ it('renders the OverviewPage with all panels', () => {
28
+ render(<OverviewPage />);
29
+
30
+ // Check the main containers
31
+ const containers = screen.getAllByClassName
32
+ ? screen.getAllByClassName('chart-container')
33
+ : document.querySelectorAll('.chart-container');
34
+
35
+ expect(containers.length).toBe(2);
36
+
37
+ // Check each mocked panel appears
38
+ expect(screen.getByTestId('overview-panel')).toBeInTheDocument();
39
+ expect(screen.getByTestId('nodes-by-type-panel')).toBeInTheDocument();
40
+ expect(screen.getByTestId('governance-warnings-panel')).toBeInTheDocument();
41
+ expect(screen.getByTestId('trends-panel')).toBeInTheDocument();
42
+ expect(
43
+ screen.getByTestId('dimension-node-usage-panel'),
44
+ ).toBeInTheDocument();
45
+
46
+ // Check panels are in the correct containers
47
+ expect(containers[0].innerHTML).toContain('OverviewPanel');
48
+ expect(containers[0].innerHTML).toContain('NodesByTypePanel');
49
+ expect(containers[0].innerHTML).toContain('GovernanceWarningsPanel');
50
+
51
+ expect(containers[1].innerHTML).toContain('TrendsPanel');
52
+ expect(containers[1].innerHTML).toContain('DimensionNodeUsagePanel');
53
+ });
54
+ });
@@ -0,0 +1,22 @@
1
+ import { OverviewPanel } from './OverviewPanel';
2
+ import { NodesByTypePanel } from './NodesByTypePanel';
3
+ import { GovernanceWarningsPanel } from './GovernanceWarningsPanel';
4
+ import { TrendsPanel } from './TrendsPanel';
5
+ import { DimensionNodeUsagePanel } from './DimensionNodeUsagePanel';
6
+
7
+ export function OverviewPage() {
8
+ return (
9
+ <div className="mid">
10
+ <div className="chart-container">
11
+ <OverviewPanel />
12
+ <NodesByTypePanel />
13
+ <GovernanceWarningsPanel />
14
+ </div>
15
+
16
+ <div className="chart-container">
17
+ <TrendsPanel />
18
+ <DimensionNodeUsagePanel />
19
+ </div>
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Asynchronously loads the component for the Node page
3
+ */
4
+
5
+ import * as React from 'react';
6
+ import { lazyLoad } from '../../../utils/loadable';
7
+
8
+ export const RegisterTablePage = () => {
9
+ return lazyLoad(
10
+ () => import('./index'),
11
+ module => module.RegisterTablePage,
12
+ {
13
+ fallback: <div></div>,
14
+ },
15
+ )();
16
+ };
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+ import { fireEvent, 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 { MemoryRouter, Route, Routes } from 'react-router-dom';
7
+ import DJClientContext from '../../../providers/djclient';
8
+ import { RegisterTablePage } from '../index';
9
+
10
+ describe('<RegisterTablePage />', () => {
11
+ const initializeMockDJClient = () => {
12
+ return {
13
+ DataJunctionAPI: {
14
+ catalogs: jest.fn(),
15
+ registerTable: jest.fn(),
16
+ },
17
+ };
18
+ };
19
+
20
+ const mockDjClient = initializeMockDJClient();
21
+
22
+ beforeEach(() => {
23
+ fetchMock.resetMocks();
24
+ jest.clearAllMocks();
25
+ window.scrollTo = jest.fn();
26
+
27
+ mockDjClient.DataJunctionAPI.catalogs.mockReturnValue([
28
+ {
29
+ name: 'warehouse',
30
+ engines: [
31
+ {
32
+ name: 'duckdb',
33
+ version: '0.7.1',
34
+ uri: null,
35
+ dialect: null,
36
+ },
37
+ ],
38
+ },
39
+ ]);
40
+ });
41
+
42
+ const renderRegisterTable = element => {
43
+ return render(
44
+ <MemoryRouter initialEntries={['/create/source']}>
45
+ <Routes>
46
+ <Route path="create/source" element={element} />
47
+ </Routes>
48
+ </MemoryRouter>,
49
+ );
50
+ };
51
+
52
+ const testElement = djClient => {
53
+ return (
54
+ <DJClientContext.Provider value={djClient}>
55
+ <RegisterTablePage />
56
+ </DJClientContext.Provider>
57
+ );
58
+ };
59
+
60
+ it('registers a table correctly', async () => {
61
+ mockDjClient.DataJunctionAPI.registerTable.mockReturnValue({
62
+ status: 201,
63
+ json: { name: 'source.warehouse.schema.some_table' },
64
+ });
65
+
66
+ const element = testElement(mockDjClient);
67
+ const { container, getByTestId } = renderRegisterTable(element);
68
+
69
+ const catalog = getByTestId('choose-catalog');
70
+ await waitFor(async () => {
71
+ fireEvent.keyDown(catalog.firstChild, { key: 'ArrowDown' });
72
+ fireEvent.click(screen.getByText('warehouse'));
73
+ });
74
+
75
+ await userEvent.type(screen.getByLabelText('Schema'), 'schema');
76
+ await userEvent.type(screen.getByLabelText('Table'), 'some_table');
77
+ await userEvent.click(screen.getByRole('button'));
78
+
79
+ await waitFor(() => {
80
+ expect(mockDjClient.DataJunctionAPI.registerTable).toBeCalled();
81
+ expect(mockDjClient.DataJunctionAPI.registerTable).toBeCalledWith(
82
+ 'warehouse',
83
+ 'schema',
84
+ 'some_table',
85
+ );
86
+ });
87
+ expect(container.getElementsByClassName('message')).toMatchSnapshot();
88
+ }, 60000);
89
+
90
+ it('fails to register a table', async () => {
91
+ mockDjClient.DataJunctionAPI.registerTable.mockReturnValue({
92
+ status: 500,
93
+ json: { message: 'table not found' },
94
+ });
95
+
96
+ const element = testElement(mockDjClient);
97
+ const { getByTestId } = renderRegisterTable(element);
98
+
99
+ const catalog = getByTestId('choose-catalog');
100
+ await waitFor(async () => {
101
+ fireEvent.keyDown(catalog.firstChild, { key: 'ArrowDown' });
102
+ fireEvent.click(screen.getByText('warehouse'));
103
+ });
104
+
105
+ await userEvent.type(screen.getByLabelText('Schema'), 'schema');
106
+ await userEvent.type(screen.getByLabelText('Table'), 'some_table');
107
+ await userEvent.click(screen.getByRole('button'));
108
+ expect(screen.getByText('table not found')).toBeInTheDocument();
109
+ }, 60000);
110
+ });
@@ -0,0 +1,38 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<RegisterTablePage /> registers a table correctly 1`] = `
4
+ HTMLCollection [
5
+ <div
6
+ class="message success"
7
+ data-testid="success"
8
+ >
9
+ <svg
10
+ class="bi bi-check-circle-fill"
11
+ data-testid="valid-icon"
12
+ fill="currentColor"
13
+ height="25px"
14
+ viewBox="0 0 16 16"
15
+ width="25px"
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ >
18
+ <path
19
+ 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"
20
+ />
21
+ </svg>
22
+ Successfully registered source node
23
+
24
+ <a
25
+ href="/nodes/source.warehouse.schema.some_table"
26
+ >
27
+ source.warehouse.schema.some_table
28
+ </a>
29
+ , which references table
30
+ warehouse
31
+ .
32
+ schema
33
+ .
34
+ some_table
35
+ .
36
+ </div>,
37
+ ]
38
+ `;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Node add + edit page for transforms, metrics, and dimensions. The creation and edit flow for these
3
+ * node types is largely the same, with minor differences handled server-side. For the `query`
4
+ * field, this page will render a CodeMirror SQL editor with autocompletion and syntax highlighting.
5
+ */
6
+ import { ErrorMessage, Field, Form, Formik } from 'formik';
7
+
8
+ import NamespaceHeader from '../../components/NamespaceHeader';
9
+ import React, { useContext, useEffect, useState } from 'react';
10
+ import DJClientContext from '../../providers/djclient';
11
+ import 'styles/node-creation.scss';
12
+ import { FormikSelect } from '../AddEditNodePage/FormikSelect';
13
+ import { displayMessageAfterSubmit } from '../../../utils/form';
14
+
15
+ export function RegisterTablePage() {
16
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
17
+ const [catalogs, setCatalogs] = useState([]);
18
+
19
+ useEffect(() => {
20
+ const fetchData = async () => {
21
+ const catalogs = await djClient.catalogs();
22
+ setCatalogs(
23
+ catalogs.map(catalog => {
24
+ return { value: catalog.name, label: catalog.name };
25
+ }),
26
+ );
27
+ };
28
+ fetchData().catch(console.error);
29
+ }, [djClient, djClient.namespaces]);
30
+
31
+ const initialValues = {
32
+ catalog: '',
33
+ schema: '',
34
+ table: '',
35
+ };
36
+
37
+ const validator = values => {
38
+ const errors = {};
39
+ if (!values.table) {
40
+ errors.table = 'Required';
41
+ }
42
+ if (!values.schema) {
43
+ errors.schema = 'Required';
44
+ }
45
+ return errors;
46
+ };
47
+
48
+ const handleSubmit = async (values, { setSubmitting, setStatus }) => {
49
+ const { status, json } = await djClient.registerTable(
50
+ values.catalog,
51
+ values.schema,
52
+ values.table,
53
+ );
54
+ if (status === 200 || status === 201) {
55
+ setStatus({
56
+ success: (
57
+ <>
58
+ Successfully registered source node{' '}
59
+ <a href={`/nodes/${json.name}`}>{json.name}</a>, which references
60
+ table {values.catalog}.{values.schema}.{values.table}.
61
+ </>
62
+ ),
63
+ });
64
+ } else {
65
+ setStatus({
66
+ failure: `${json.message}`,
67
+ });
68
+ }
69
+ setSubmitting(false);
70
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
71
+ };
72
+
73
+ return (
74
+ <div className="mid">
75
+ <NamespaceHeader namespace="" />
76
+ <div className="card">
77
+ <div className="card-header">
78
+ <h2>
79
+ Register{' '}
80
+ <span className={`node_type__source node_type_creation_heading`}>
81
+ Source
82
+ </span>
83
+ </h2>
84
+ <center>
85
+ <Formik
86
+ initialValues={initialValues}
87
+ validate={validator}
88
+ onSubmit={handleSubmit}
89
+ >
90
+ {function Render({ isSubmitting, status }) {
91
+ return (
92
+ <Form>
93
+ {displayMessageAfterSubmit(status)}
94
+ {
95
+ <>
96
+ <div className="SourceCreationInput">
97
+ <ErrorMessage name="catalog" component="span" />
98
+ <label htmlFor="catalog">Catalog</label>
99
+ <span data-testid="choose-catalog">
100
+ <FormikSelect
101
+ selectOptions={catalogs}
102
+ formikFieldName="catalog"
103
+ placeholder="Choose Catalog"
104
+ defaultValue={catalogs[0]}
105
+ />
106
+ </span>
107
+ </div>
108
+ <div className="SourceCreationInput">
109
+ <ErrorMessage name="schema" component="span" />
110
+ <label htmlFor="schema">Schema</label>
111
+ <Field
112
+ type="text"
113
+ name="schema"
114
+ id="schema"
115
+ placeholder="Schema"
116
+ />
117
+ </div>
118
+ <div className="SourceCreationInput NodeCreationInput">
119
+ <ErrorMessage name="table" component="span" />
120
+ <label htmlFor="table">Table</label>
121
+ <Field
122
+ type="text"
123
+ name="table"
124
+ id="table"
125
+ placeholder="Table"
126
+ />
127
+ </div>
128
+ <button type="submit" disabled={isSubmitting}>
129
+ Register
130
+ </button>
131
+ </>
132
+ }
133
+ </Form>
134
+ );
135
+ }}
136
+ </Formik>
137
+ </center>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ );
142
+ }