datajunction-ui 0.0.1-rc.2 → 0.0.1-rc.21
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/.env +1 -0
- package/.prettierignore +3 -1
- package/dj-logo.svg +10 -0
- package/package.json +43 -13
- package/public/favicon.ico +0 -0
- package/src/__tests__/reportWebVitals.test.jsx +44 -0
- package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -39
- package/src/app/components/DeleteNode.jsx +79 -0
- package/src/app/components/ListGroupItem.jsx +8 -1
- package/src/app/components/NamespaceHeader.jsx +4 -13
- package/src/app/components/QueryInfo.jsx +77 -0
- package/src/app/components/Tab.jsx +3 -2
- package/src/app/components/ToggleSwitch.jsx +20 -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/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
- package/src/app/components/djgraph/Collapse.jsx +46 -0
- package/src/app/components/djgraph/DJNode.jsx +60 -82
- package/src/app/components/djgraph/DJNodeColumns.jsx +71 -0
- package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
- package/src/app/components/djgraph/LayoutFlow.jsx +104 -0
- 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/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
- package/src/app/constants.js +2 -0
- package/src/app/icons/AlertIcon.jsx +32 -0
- package/src/app/icons/CollapsedIcon.jsx +15 -0
- package/src/app/icons/DJLogo.jsx +36 -0
- package/src/app/icons/DeleteIcon.jsx +21 -0
- package/src/app/icons/EditIcon.jsx +18 -0
- package/src/app/icons/ExpandedIcon.jsx +15 -0
- package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
- package/src/app/icons/InvalidIcon.jsx +14 -0
- package/src/app/icons/PythonIcon.jsx +52 -0
- package/src/app/icons/TableIcon.jsx +14 -0
- package/src/app/icons/ValidIcon.jsx +14 -0
- package/src/app/index.tsx +79 -26
- package/src/app/pages/AddEditNodePage/FormikSelect.jsx +46 -0
- package/src/app/pages/AddEditNodePage/FullNameField.jsx +37 -0
- package/src/app/pages/AddEditNodePage/Loadable.jsx +16 -0
- package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +89 -0
- 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 +75 -0
- package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
- package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
- 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 +3 -0
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +178 -0
- package/src/app/pages/AddEditNodePage/index.jsx +357 -0
- package/src/app/pages/LoginPage/__tests__/index.test.jsx +70 -0
- package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
- package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
- package/src/app/pages/LoginPage/index.jsx +90 -0
- package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +86 -0
- package/src/app/pages/NamespacePage/Explorer.jsx +57 -0
- package/src/app/pages/NamespacePage/Loadable.jsx +9 -7
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +95 -0
- package/src/app/pages/NamespacePage/index.jsx +132 -31
- package/src/app/pages/NodePage/ClientCodePopover.jsx +32 -0
- package/src/app/pages/NodePage/EditColumnPopover.jsx +102 -0
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +135 -0
- package/src/app/pages/NodePage/Loadable.jsx +9 -7
- package/src/app/pages/NodePage/NodeColumnTab.jsx +106 -27
- package/src/app/pages/NodePage/NodeGraphTab.jsx +94 -148
- package/src/app/pages/NodePage/NodeHistory.jsx +212 -0
- package/src/app/pages/NodePage/NodeInfoTab.jsx +166 -51
- package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +174 -0
- package/src/app/pages/NodePage/NodeSQLTab.jsx +82 -0
- package/src/app/pages/NodePage/NodeStatus.jsx +14 -20
- package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
- 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 +151 -41
- 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/Root/index.tsx +32 -4
- package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
- package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
- package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
- package/src/app/providers/djclient.jsx +5 -0
- package/src/app/services/DJService.js +398 -22
- package/src/app/services/__tests__/DJService.test.jsx +609 -0
- package/src/mocks/mockNodes.jsx +1397 -0
- package/src/setupTests.ts +31 -1
- package/src/styles/dag.css +111 -5
- package/src/styles/index.css +467 -31
- package/src/styles/login.css +67 -0
- package/src/styles/node-creation.scss +197 -0
- package/src/styles/styles.scss +44 -0
- package/src/styles/styles.scss.d.ts +9 -0
- package/src/utils/form.jsx +23 -0
- package/tsconfig.json +1 -5
- package/webpack.config.js +29 -6
- package/.babelrc +0 -4
- package/.env.local +0 -4
- package/.env.production +0 -1
- package/.github/pull_request_template.md +0 -11
- package/.github/workflows/ci.yml +0 -33
- package/.vscode/extensions.json +0 -7
- package/.vscode/launch.json +0 -15
- package/.vscode/settings.json +0 -25
- package/Dockerfile +0 -7
- package/dist/5fa71a03d45dc2e3d61447f3013a303d.png +0 -0
- package/dist/index.html +0 -1
- package/dist/main.js +0 -23303
- package/dist/static/main.05a86d446163fd5f17d3.js +0 -2
- package/dist/static/main.05a86d446163fd5f17d3.js.LICENSE.txt +0 -98
- package/dist/static/main.9e53bed734dae98e5b10.js +0 -2
- package/dist/static/main.9e53bed734dae98e5b10.js.LICENSE.txt +0 -98
- package/dist/static/main.js +0 -2
- package/dist/static/main.js.LICENSE.txt +0 -98
- package/dist/static/vendor.05a86d446163fd5f17d3.js +0 -2
- package/dist/static/vendor.05a86d446163fd5f17d3.js.LICENSE.txt +0 -29
- package/dist/static/vendor.9e53bed734dae98e5b10.js +0 -2
- package/dist/static/vendor.9e53bed734dae98e5b10.js.LICENSE.txt +0 -29
- package/dist/static/vendor.js +0 -2
- package/dist/static/vendor.js.LICENSE.txt +0 -29
- package/dist/vendor.js +0 -281
- package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
- package/src/app/pages/ListNamespacesPage/index.jsx +0 -52
- package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
- package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
render,
|
|
3
|
+
screen,
|
|
4
|
+
fireEvent,
|
|
5
|
+
waitFor,
|
|
6
|
+
waitForElement,
|
|
7
|
+
act,
|
|
8
|
+
} from '@testing-library/react';
|
|
9
|
+
import DJClientContext from '../../../providers/djclient';
|
|
10
|
+
import { SQLBuilderPage } from '../index';
|
|
11
|
+
|
|
12
|
+
const mockDjClient = {
|
|
13
|
+
metrics: jest.fn(),
|
|
14
|
+
commonDimensions: jest.fn(),
|
|
15
|
+
sqls: jest.fn(),
|
|
16
|
+
data: jest.fn(),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const mockMetrics = [
|
|
20
|
+
'default.num_repair_orders',
|
|
21
|
+
'default.avg_repair_price',
|
|
22
|
+
'default.total_repair_cost',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const mockCommonDimensions = [
|
|
26
|
+
{
|
|
27
|
+
name: 'default.date_dim.dateint',
|
|
28
|
+
type: 'timestamp',
|
|
29
|
+
path: [
|
|
30
|
+
'default.repair_order_details.repair_order_id',
|
|
31
|
+
'default.repair_order.hard_hat_id',
|
|
32
|
+
'default.hard_hat.birth_date',
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'default.date_dim.dateint',
|
|
37
|
+
type: 'timestamp',
|
|
38
|
+
path: [
|
|
39
|
+
'default.repair_order_details.repair_order_id',
|
|
40
|
+
'default.repair_order.hard_hat_id',
|
|
41
|
+
'default.hard_hat.hire_date',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'default.date_dim.day',
|
|
46
|
+
type: 'int',
|
|
47
|
+
path: [
|
|
48
|
+
'default.repair_order_details.repair_order_id',
|
|
49
|
+
'default.repair_order.hard_hat_id',
|
|
50
|
+
'default.hard_hat.birth_date',
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'default.date_dim.day',
|
|
55
|
+
type: 'int',
|
|
56
|
+
path: [
|
|
57
|
+
'default.repair_order_details.repair_order_id',
|
|
58
|
+
'default.repair_order.hard_hat_id',
|
|
59
|
+
'default.hard_hat.hire_date',
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'default.date_dim.month',
|
|
64
|
+
type: 'int',
|
|
65
|
+
path: [
|
|
66
|
+
'default.repair_order_details.repair_order_id',
|
|
67
|
+
'default.repair_order.hard_hat_id',
|
|
68
|
+
'default.hard_hat.birth_date',
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'default.date_dim.month',
|
|
73
|
+
type: 'int',
|
|
74
|
+
path: [
|
|
75
|
+
'default.repair_order_details.repair_order_id',
|
|
76
|
+
'default.repair_order.hard_hat_id',
|
|
77
|
+
'default.hard_hat.hire_date',
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'default.date_dim.year',
|
|
82
|
+
type: 'int',
|
|
83
|
+
path: [
|
|
84
|
+
'default.repair_order_details.repair_order_id',
|
|
85
|
+
'default.repair_order.hard_hat_id',
|
|
86
|
+
'default.hard_hat.birth_date',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'default.date_dim.year',
|
|
91
|
+
type: 'int',
|
|
92
|
+
path: [
|
|
93
|
+
'default.repair_order_details.repair_order_id',
|
|
94
|
+
'default.repair_order.hard_hat_id',
|
|
95
|
+
'default.hard_hat.hire_date',
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
describe('SQLBuilderPage', () => {
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
mockDjClient.metrics.mockResolvedValue(mockMetrics);
|
|
103
|
+
mockDjClient.commonDimensions.mockResolvedValue(mockCommonDimensions);
|
|
104
|
+
mockDjClient.sqls.mockResolvedValue({ sql: 'SELECT ...' });
|
|
105
|
+
mockDjClient.data.mockResolvedValue({});
|
|
106
|
+
|
|
107
|
+
render(
|
|
108
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
109
|
+
<SQLBuilderPage />
|
|
110
|
+
</DJClientContext.Provider>,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
jest.clearAllMocks();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('renders without crashing', () => {
|
|
119
|
+
expect(screen.getByText('Using the SQL Builder')).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('renders the Metrics section', () => {
|
|
123
|
+
expect(screen.getByText('Metrics')).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('renders the Group By section', () => {
|
|
127
|
+
expect(screen.getByText('Group By')).toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('renders the Filter By section', () => {
|
|
131
|
+
expect(screen.getByText('Filter By')).toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('fetches metrics on mount', async () => {
|
|
135
|
+
render(
|
|
136
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
137
|
+
<SQLBuilderPage />
|
|
138
|
+
</DJClientContext.Provider>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const selectMetrics = screen.getAllByTestId('select-metrics')[0];
|
|
146
|
+
expect(selectMetrics).toBeDefined();
|
|
147
|
+
expect(selectMetrics).not.toBeNull();
|
|
148
|
+
expect(screen.getAllByText('3 Available Metrics')[0]).toBeInTheDocument();
|
|
149
|
+
|
|
150
|
+
fireEvent.keyDown(selectMetrics.firstChild, { key: 'ArrowDown' });
|
|
151
|
+
for (const metric of mockMetrics) {
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(screen.getByText(metric)).toBeInTheDocument();
|
|
154
|
+
fireEvent.click(screen.getByText(metric));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
fireEvent.click(screen.getAllByText('Group By')[0]);
|
|
158
|
+
|
|
159
|
+
expect(mockDjClient.commonDimensions).toHaveBeenCalled();
|
|
160
|
+
|
|
161
|
+
const selectDimensions = screen.getAllByTestId('select-dimensions')[0];
|
|
162
|
+
expect(selectDimensions).toBeDefined();
|
|
163
|
+
expect(selectDimensions).not.toBeNull();
|
|
164
|
+
expect(screen.getAllByText('8 Shared Dimensions')[0]).toBeInTheDocument();
|
|
165
|
+
fireEvent.keyDown(selectDimensions.firstChild, { key: 'ArrowDown' });
|
|
166
|
+
|
|
167
|
+
for (const dim of mockCommonDimensions) {
|
|
168
|
+
expect(screen.getAllByText(dim.name)[0]).toBeInTheDocument();
|
|
169
|
+
fireEvent.click(screen.getAllByText(dim.name)[0]);
|
|
170
|
+
}
|
|
171
|
+
expect(mockDjClient.sqls).toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -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
|
+
};
|