datajunction-ui 0.0.1-rc.9 → 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 +119 -148
  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,390 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import NamespaceHeader from '../../components/NamespaceHeader';
3
+ import { DataJunctionAPI } from '../../services/DJService';
4
+ import DJClientContext from '../../providers/djclient';
5
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
6
+ import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
7
+ import Select from 'react-select';
8
+ import QueryInfo from '../../components/QueryInfo';
9
+ import 'react-querybuilder/dist/query-builder.scss';
10
+ import QueryBuilder, { formatQuery } from 'react-querybuilder';
11
+ import 'styles/styles.scss';
12
+
13
+ export function SQLBuilderPage() {
14
+ const DEFAULT_NUM_ROWS = 100;
15
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
16
+ const validator = ruleType => !!ruleType.value;
17
+ const [stagedMetrics, setStagedMetrics] = useState([]);
18
+ const [metrics, setMetrics] = useState([]);
19
+ const [commonDimensionsList, setCommonDimensionsList] = useState([]);
20
+ const [selectedDimensions, setSelectedDimensions] = useState([]);
21
+ const [stagedDimensions, setStagedDimensions] = useState([]);
22
+ const [selectedMetrics, setSelectedMetrics] = useState([]);
23
+ const [query, setQuery] = useState('');
24
+ const [fields, setFields] = useState([]);
25
+ const [filters, setFilters] = useState({ combinator: 'and', rules: [] });
26
+ const [queryInfo, setQueryInfo] = useState({});
27
+ const [data, setData] = useState(null);
28
+ const [loadingData, setLoadingData] = useState(false);
29
+ const [viewData, setViewData] = useState(false);
30
+ const [showNumRows, setShowNumRows] = useState(DEFAULT_NUM_ROWS);
31
+ const [displayedRows, setDisplayedRows] = useState(<></>);
32
+ const numRowsOptions = [
33
+ {
34
+ value: 10,
35
+ label: '10 Rows',
36
+ isFixed: true,
37
+ },
38
+ {
39
+ value: 100,
40
+ label: '100 Rows',
41
+ isFixed: true,
42
+ },
43
+ {
44
+ value: 1000,
45
+ label: '1,000 Rows',
46
+ isFixed: true,
47
+ },
48
+ ];
49
+ const toggleViewData = () => setViewData(current => !current);
50
+
51
+ // Get data for the current selection of metrics and dimensions
52
+ const getData = () => {
53
+ setLoadingData(true);
54
+ setQueryInfo({});
55
+ const fetchData = async () => {
56
+ if (process.env.REACT_USE_SSE) {
57
+ const sse = await djClient.stream(
58
+ selectedMetrics,
59
+ selectedDimensions,
60
+ formatQuery(filters, { format: 'sql', parseNumbers: true }),
61
+ );
62
+ sse.onmessage = e => {
63
+ const messageData = JSON.parse(JSON.parse(e.data));
64
+ setQueryInfo(messageData);
65
+ if (messageData.results) {
66
+ setLoadingData(false);
67
+ setData(messageData.results);
68
+ messageData.numRows = messageData.results?.length
69
+ ? messageData.results[0].rows.length
70
+ : [];
71
+ setViewData(true);
72
+ setShowNumRows(DEFAULT_NUM_ROWS);
73
+ }
74
+ };
75
+ sse.onerror = () => sse.close();
76
+ } else {
77
+ const response = await djClient.data(
78
+ selectedMetrics,
79
+ selectedDimensions,
80
+ );
81
+ setQueryInfo(response);
82
+ if (response.results) {
83
+ setLoadingData(false);
84
+ setData(response.results);
85
+ response.numRows = response.results?.length
86
+ ? response.results[0].rows.length
87
+ : [];
88
+ setViewData(true);
89
+ setShowNumRows(DEFAULT_NUM_ROWS);
90
+ }
91
+ }
92
+ };
93
+ fetchData().catch(console.error);
94
+ };
95
+
96
+ const resetView = () => {
97
+ setQuery('');
98
+ setData(null);
99
+ setViewData(false);
100
+ setQueryInfo({});
101
+ };
102
+
103
+ // Get metrics
104
+ useEffect(() => {
105
+ const fetchData = async () => {
106
+ const metrics = await djClient.metrics();
107
+ setMetrics(metrics.map(m => ({ value: m, label: m })));
108
+ };
109
+ fetchData().catch(console.error);
110
+ }, [djClient, djClient.metrics]);
111
+
112
+ const attributeToFormInput = dimension => {
113
+ const attribute = {
114
+ name: dimension.name,
115
+ label: `${dimension.name} (via ${dimension.path.join(' ▶ ')})`,
116
+ placeholder: `from ${dimension.path}`,
117
+ defaultOperator: '=',
118
+ validator,
119
+ };
120
+ if (dimension.type === 'bool') {
121
+ attribute.valueEditorType = 'checkbox';
122
+ }
123
+ if (dimension.type === 'timestamp') {
124
+ attribute.inputType = 'datetime-local';
125
+ attribute.defaultOperator = 'between';
126
+ }
127
+ return [dimension.name, attribute];
128
+ };
129
+
130
+ // Get common dimensions
131
+ useEffect(() => {
132
+ const fetchData = async () => {
133
+ if (selectedMetrics.length) {
134
+ const commonDimensions = await djClient.commonDimensions(
135
+ selectedMetrics,
136
+ );
137
+ setCommonDimensionsList(
138
+ commonDimensions.map(d => ({
139
+ value: d.name,
140
+ label: d.name,
141
+ path: d.path.join(' ▶ '),
142
+ })),
143
+ );
144
+ const uniqueFields = Object.fromEntries(
145
+ new Map(
146
+ commonDimensions.map(dimension => attributeToFormInput(dimension)),
147
+ ),
148
+ );
149
+ setFields(Object.keys(uniqueFields).map(f => uniqueFields[f]));
150
+ } else {
151
+ setCommonDimensionsList([]);
152
+ setFields([]);
153
+ }
154
+ };
155
+ fetchData().catch(console.error);
156
+ }, [selectedMetrics, djClient]);
157
+
158
+ // Get SQL
159
+ useEffect(() => {
160
+ const fetchData = async () => {
161
+ if (
162
+ selectedMetrics.length > 0 &&
163
+ (selectedDimensions.length > 0 || filters.rules.length > 0)
164
+ ) {
165
+ const result = await djClient.sqls(
166
+ selectedMetrics,
167
+ selectedDimensions,
168
+ formatQuery(filters, { format: 'sql', parseNumbers: true }),
169
+ );
170
+ setQuery(result.sql);
171
+ } else {
172
+ resetView();
173
+ }
174
+ };
175
+ fetchData().catch(console.error);
176
+ }, [djClient, filters, selectedDimensions, selectedMetrics]);
177
+
178
+ // Set number of rows to display
179
+ useEffect(() => {
180
+ if (data) {
181
+ setDisplayedRows(
182
+ data[0]?.rows.slice(0, showNumRows).map((rowData, index) => (
183
+ <tr key={`data-row:${index}`}>
184
+ {rowData.map(rowValue => (
185
+ <td key={rowValue}>{rowValue}</td>
186
+ ))}
187
+ </tr>
188
+ )),
189
+ );
190
+ }
191
+ }, [showNumRows, data]);
192
+ const formatOptionLabel = ({ value, label, path }) => (
193
+ <div className={`badge dimension_option`}>
194
+ <div>{label}</div>
195
+ <span className={`badge dimension_option_subheading`}>{path}</span>
196
+ </div>
197
+ );
198
+
199
+ // @ts-ignore
200
+ return (
201
+ <>
202
+ <div className="mid">
203
+ <NamespaceHeader namespace="" />
204
+ <div className="card">
205
+ <div className="card-header">
206
+ <h4>Metrics</h4>
207
+ <span data-testid="select-metrics">
208
+ <Select
209
+ name="metrics"
210
+ options={metrics}
211
+ isDisabled={
212
+ !!(selectedMetrics.length && selectedDimensions.length)
213
+ }
214
+ noOptionsMessage={() => 'No metrics found.'}
215
+ placeholder={`${metrics.length} Available Metrics`}
216
+ isMulti
217
+ isClearable
218
+ closeMenuOnSelect={false}
219
+ onChange={e => {
220
+ setSelectedDimensions([]);
221
+ resetView();
222
+ setStagedMetrics(e.map(m => m.value));
223
+ setSelectedMetrics(stagedMetrics);
224
+ }}
225
+ onMenuClose={() => {
226
+ resetView();
227
+ setSelectedDimensions([]);
228
+ setSelectedMetrics(stagedMetrics);
229
+ }}
230
+ />
231
+ </span>
232
+ <h4>Group By</h4>
233
+ <span data-testid="select-dimensions">
234
+ <Select
235
+ name="dimensions"
236
+ formatOptionLabel={formatOptionLabel}
237
+ options={commonDimensionsList}
238
+ noOptionsMessage={() =>
239
+ 'No shared dimensions found. Try selecting different metrics.'
240
+ }
241
+ placeholder={`${commonDimensionsList.length} Shared Dimensions`}
242
+ isMulti
243
+ isClearable
244
+ closeMenuOnSelect={false}
245
+ onChange={e => {
246
+ resetView();
247
+ setStagedDimensions(e.map(d => d.value));
248
+ setSelectedDimensions(stagedDimensions);
249
+ }}
250
+ onMenuClose={() => {
251
+ setSelectedDimensions(stagedDimensions);
252
+ }}
253
+ />
254
+ </span>
255
+ <h4>Filter By</h4>
256
+ <QueryBuilder
257
+ fields={fields}
258
+ query={filters}
259
+ onQueryChange={q => setFilters(q)}
260
+ />
261
+ </div>
262
+ <div className="card-header">
263
+ {!viewData && !query ? (
264
+ <div className="card-light-shadow">
265
+ <h6>Using the SQL Builder</h6>
266
+ <p>
267
+ The sql builder allows you to group multiple metrics along
268
+ with their shared dimensions. Using your selections,
269
+ DataJunction will generate the corresponding SQL.
270
+ </p>
271
+ <ol>
272
+ <li>
273
+ <b>Select Metrics:</b> Start by selecting one or more
274
+ metrics from the metrics dropdown.
275
+ </li>
276
+ <li>
277
+ <b>Select Dimensions:</b> Next, select the dimension
278
+ attributes you would like to include. As you select
279
+ additional metrics, the list of available dimensions will be
280
+ filtered to those shared by the selected metrics. If the
281
+ dimensions list is empty, no shared dimensions were
282
+ discovered.
283
+ </li>
284
+ <li>
285
+ <b>View the generated SQL Query:</b> As you make your
286
+ selections, the SQL required to retrieve the set of metrics
287
+ and dimensions will be generated below.
288
+ </li>
289
+ <li>
290
+ <b>Run the Query:</b> If query running is enabled by your
291
+ server, you can also run the generated SQL query to view a
292
+ sample of 100 records.
293
+ </li>
294
+ </ol>
295
+ </div>
296
+ ) : (
297
+ <></>
298
+ )}
299
+ {query ? (
300
+ <>
301
+ {loadingData ? (
302
+ <span
303
+ className="button-3 executing-button"
304
+ onClick={getData}
305
+ role="button"
306
+ aria-label="RunQuery"
307
+ aria-hidden="false"
308
+ >
309
+ {'Running Query'}
310
+ </span>
311
+ ) : (
312
+ <span
313
+ className="button-3 execute-button"
314
+ onClick={getData}
315
+ role="button"
316
+ aria-label="RunQuery"
317
+ aria-hidden="false"
318
+ >
319
+ {'Run Query'}
320
+ </span>
321
+ )}
322
+ {data ? (
323
+ viewData ? (
324
+ <>
325
+ <span
326
+ className="button-3 neutral-button"
327
+ onClick={toggleViewData}
328
+ >
329
+ {'View Query'}
330
+ </span>
331
+ <span style={{ display: 'inline-block' }}>
332
+ <Select
333
+ name="num-rows"
334
+ defaultValue={numRowsOptions[0]}
335
+ options={numRowsOptions}
336
+ onChange={e => setShowNumRows(e.value)}
337
+ />
338
+ </span>
339
+ </>
340
+ ) : (
341
+ <span
342
+ className="button-3 neutral-button"
343
+ onClick={toggleViewData}
344
+ >
345
+ {'View Data'}
346
+ </span>
347
+ )
348
+ ) : (
349
+ <></>
350
+ )}
351
+ </>
352
+ ) : (
353
+ <></>
354
+ )}
355
+ {queryInfo && queryInfo.id ? <QueryInfo {...queryInfo} /> : <></>}
356
+ <div>
357
+ {query && !viewData ? (
358
+ <SyntaxHighlighter language="sql" style={foundation}>
359
+ {query}
360
+ </SyntaxHighlighter>
361
+ ) : (
362
+ ''
363
+ )}
364
+ </div>
365
+ {data && viewData ? (
366
+ <div className="table-responsive">
367
+ <table className="card-inner-table table">
368
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
369
+ <tr>
370
+ {data[0]?.columns.map(columnName => (
371
+ <th key={columnName.name}>{columnName.name}</th>
372
+ ))}
373
+ </tr>
374
+ </thead>
375
+ <tbody>{displayedRows}</tbody>
376
+ </table>
377
+ </div>
378
+ ) : (
379
+ <></>
380
+ )}
381
+ </div>
382
+ </div>
383
+ </div>
384
+ </>
385
+ );
386
+ }
387
+
388
+ SQLBuilderPage.defaultProps = {
389
+ djClient: DataJunctionAPI,
390
+ };
@@ -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 TagPage = () => {
9
+ return lazyLoad(
10
+ () => import('./index'),
11
+ module => module.TagPage,
12
+ {
13
+ fallback: <div></div>,
14
+ },
15
+ )();
16
+ };
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import fetchMock from 'jest-fetch-mock';
4
+ import { render } from '../../../../setupTests';
5
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
6
+ import DJClientContext from '../../../providers/djclient';
7
+ import { TagPage } from '../index';
8
+
9
+ describe('<TagPage />', () => {
10
+ const initializeMockDJClient = () => {
11
+ return {
12
+ DataJunctionAPI: {
13
+ getTag: jest.fn(),
14
+ listNodesForTag: jest.fn(),
15
+ },
16
+ };
17
+ };
18
+
19
+ const mockDjClient = initializeMockDJClient();
20
+
21
+ beforeEach(() => {
22
+ fetchMock.resetMocks();
23
+ jest.clearAllMocks();
24
+ window.scrollTo = jest.fn();
25
+
26
+ mockDjClient.DataJunctionAPI.getTag.mockReturnValue({
27
+ name: 'domains.com',
28
+ tag_type: 'domains',
29
+ description: 'Top-level domain .com',
30
+ });
31
+ mockDjClient.DataJunctionAPI.listNodesForTag.mockReturnValue([
32
+ {
33
+ name: 'random.node_a',
34
+ type: 'metric',
35
+ display_name: 'Node A',
36
+ },
37
+ ]);
38
+ });
39
+
40
+ const renderTagsPage = element => {
41
+ return render(
42
+ <MemoryRouter initialEntries={['/tags/:name']}>
43
+ <Routes>
44
+ <Route path="tags/:name" element={element} />
45
+ </Routes>
46
+ </MemoryRouter>,
47
+ );
48
+ };
49
+
50
+ const testElement = djClient => {
51
+ return (
52
+ <DJClientContext.Provider value={djClient}>
53
+ <TagPage />
54
+ </DJClientContext.Provider>
55
+ );
56
+ };
57
+
58
+ it('renders the tag page correctly', async () => {
59
+ const element = testElement(mockDjClient);
60
+ renderTagsPage(element);
61
+ await waitFor(() => {
62
+ expect(mockDjClient.DataJunctionAPI.getTag).toHaveBeenCalledTimes(1);
63
+ expect(
64
+ mockDjClient.DataJunctionAPI.listNodesForTag,
65
+ ).toHaveBeenCalledTimes(1);
66
+ });
67
+ expect(screen.getByText('Nodes')).toBeInTheDocument();
68
+ expect(screen.getByText('Node A')).toBeInTheDocument();
69
+ }, 60000);
70
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * For a given tag, displays nodes tagged with it
3
+ */
4
+ import NamespaceHeader from '../../components/NamespaceHeader';
5
+ import React, { useContext, useEffect, useState } from 'react';
6
+ import 'styles/node-creation.scss';
7
+ import DJClientContext from '../../providers/djclient';
8
+ import { useParams } from 'react-router-dom';
9
+
10
+ export function TagPage() {
11
+ const [nodes, setNodes] = useState([]);
12
+ const [tag, setTag] = useState([]);
13
+
14
+ const { name } = useParams();
15
+
16
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
17
+ useEffect(() => {
18
+ const fetchData = async () => {
19
+ const data = await djClient.listNodesForTag(name);
20
+ const tagData = await djClient.getTag(name);
21
+ setNodes(data);
22
+ setTag(tagData);
23
+ };
24
+ fetchData().catch(console.error);
25
+ }, [djClient, name]);
26
+
27
+ return (
28
+ <div className="mid">
29
+ <NamespaceHeader namespace="" />
30
+ <div className="card">
31
+ <div className="card-header">
32
+ <h3
33
+ className="card-title align-items-start flex-column"
34
+ style={{ display: 'inline-block' }}
35
+ >
36
+ Tag
37
+ </h3>
38
+ <div>
39
+ <div style={{ marginBottom: '1.5rem' }}>
40
+ <h6 className="mb-0 w-100">Display Name</h6>
41
+ <p className="mb-0 opacity-75">{tag.display_name}</p>
42
+ </div>
43
+ <div style={{ marginBottom: '1.5rem' }}>
44
+ <h6 className="mb-0 w-100">Name</h6>
45
+ <p className="mb-0 opacity-75">{name}</p>
46
+ </div>
47
+ <div style={{ marginBottom: '1.5rem' }}>
48
+ <h6 className="mb-0 w-100">Tag Type</h6>
49
+ <p className="mb-0 opacity-75">{tag.tag_type}</p>
50
+ </div>
51
+ <div style={{ marginBottom: '1.5rem' }}>
52
+ <h6 className="mb-0 w-100">Description</h6>
53
+ <p className="mb-0 opacity-75">{tag.description}</p>
54
+ </div>
55
+ <div style={{ marginBottom: '1.5rem' }}>
56
+ <h6 className="mb-0 w-100">Nodes</h6>
57
+ <div className={`list-group-item`}>
58
+ {nodes?.map(node => (
59
+ <div
60
+ className="button-3 cube-element"
61
+ key={node.name}
62
+ role="cell"
63
+ aria-label="CubeElement"
64
+ aria-hidden="false"
65
+ >
66
+ <a href={`/nodes/${node.name}`}>{node.display_name}</a>
67
+ <span className={`badge node_type__${node.type}`}>
68
+ {node.type}
69
+ </span>
70
+ </div>
71
+ ))}
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ );
79
+ }