datajunction-ui 0.0.1-rc.17 → 0.0.1-rc.18
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 -1
- package/package.json +10 -4
- package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -84
- package/src/app/components/djgraph/DJNode.jsx +1 -1
- package/src/app/components/djgraph/LayoutFlow.jsx +1 -1
- package/src/app/constants.js +2 -0
- package/src/app/index.tsx +52 -36
- 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/__tests__/__snapshots__/index.test.tsx.snap +0 -3
- package/src/app/pages/NamespacePage/index.jsx +2 -9
- package/src/app/pages/NodePage/NodeGraphTab.jsx +1 -2
- package/src/app/pages/Root/index.tsx +17 -0
- package/src/app/pages/SQLBuilderPage/index.jsx +54 -8
- package/src/app/services/DJService.js +109 -32
- package/src/styles/login.css +67 -0
- package/src/styles/styles.scss +44 -0
- package/webpack.config.js +11 -1
package/.env
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
REACT_APP_DJ_URL=http://localhost:8000
|
|
2
|
-
REACT_USE_SSE=true
|
|
2
|
+
REACT_USE_SSE=true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datajunction-ui",
|
|
3
|
-
"version": "0.0.1-rc.
|
|
3
|
+
"version": "0.0.1-rc.18",
|
|
4
4
|
"description": "DataJunction Metrics Platform UI",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"repository": {
|
|
@@ -43,27 +43,31 @@
|
|
|
43
43
|
"chalk": "4.1.2",
|
|
44
44
|
"cronstrue": "2.27.0",
|
|
45
45
|
"cross-env": "7.0.3",
|
|
46
|
-
"css-loader": "6.
|
|
46
|
+
"css-loader": "6.8.1",
|
|
47
47
|
"dagre": "^0.8.5",
|
|
48
48
|
"datajunction": "0.0.1-rc.0",
|
|
49
49
|
"file-loader": "6.2.0",
|
|
50
50
|
"fontfaceobserver": "2.3.0",
|
|
51
|
+
"formik": "2.4.3",
|
|
51
52
|
"husky": "8.0.1",
|
|
52
53
|
"i18next": "21.9.2",
|
|
53
54
|
"i18next-browser-languagedetector": "6.1.5",
|
|
54
55
|
"i18next-scanner": "4.0.0",
|
|
55
56
|
"inquirer": "7.3.3",
|
|
56
57
|
"inquirer-directory": "2.2.0",
|
|
58
|
+
"js-cookie": "3.0.5",
|
|
57
59
|
"lint-staged": "13.0.3",
|
|
58
60
|
"node-plop": "0.26.3",
|
|
59
61
|
"plop": "2.7.6",
|
|
60
62
|
"prettier": "2.7.1",
|
|
61
63
|
"react": "18.2.0",
|
|
62
64
|
"react-app-polyfill": "3.0.0",
|
|
65
|
+
"react-cookie": "4.1.1",
|
|
63
66
|
"react-dom": "18.2.0",
|
|
64
67
|
"react-helmet-async": "1.3.0",
|
|
65
68
|
"react-i18next": "11.18.6",
|
|
66
69
|
"react-is": "18.2.0",
|
|
70
|
+
"react-querybuilder": "6.5.1",
|
|
67
71
|
"react-redux": "7.2.8",
|
|
68
72
|
"react-router-dom": "6.3.0",
|
|
69
73
|
"react-scripts": "5.0.1",
|
|
@@ -75,10 +79,12 @@
|
|
|
75
79
|
"redux-saga": "1.2.1",
|
|
76
80
|
"rimraf": "3.0.2",
|
|
77
81
|
"sanitize.css": "13.0.0",
|
|
82
|
+
"sass": "1.66.1",
|
|
83
|
+
"sass-loader": "13.3.2",
|
|
78
84
|
"serve": "14.0.1",
|
|
79
85
|
"shelljs": "0.8.5",
|
|
80
86
|
"sql-formatter": "^12.2.0",
|
|
81
|
-
"style-loader": "3.3.
|
|
87
|
+
"style-loader": "3.3.3",
|
|
82
88
|
"stylelint": "14.12.0",
|
|
83
89
|
"stylelint-config-recommended": "9.0.0",
|
|
84
90
|
"ts-loader": "9.4.2",
|
|
@@ -160,6 +166,6 @@
|
|
|
160
166
|
"eslint-plugin-react-hooks": "4.6.0",
|
|
161
167
|
"html-webpack-plugin": "5.5.1",
|
|
162
168
|
"jest": "^29.5.0",
|
|
163
|
-
"mini-css-extract-plugin": "2.7.
|
|
169
|
+
"mini-css-extract-plugin": "2.7.6"
|
|
164
170
|
}
|
|
165
171
|
}
|
|
@@ -1,88 +1,9 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
3
|
exports[`<App /> should render and match the snapshot 1`] = `
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
prioritizeSeoTags={false}
|
|
10
|
-
titleTemplate="DataJunction: %s"
|
|
11
|
-
>
|
|
12
|
-
<meta
|
|
13
|
-
content="DataJunction serves as a semantic layer to help manage metrics"
|
|
14
|
-
name="description"
|
|
15
|
-
/>
|
|
16
|
-
</Helmet>
|
|
17
|
-
<Context.Provider
|
|
18
|
-
value={
|
|
19
|
-
Object {
|
|
20
|
-
"DataJunctionAPI": Object {
|
|
21
|
-
"clientCode": [Function],
|
|
22
|
-
"columns": [Function],
|
|
23
|
-
"commonDimensions": [Function],
|
|
24
|
-
"compiledSql": [Function],
|
|
25
|
-
"cube": [Function],
|
|
26
|
-
"dag": [Function],
|
|
27
|
-
"data": [Function],
|
|
28
|
-
"downstreams": [Function],
|
|
29
|
-
"history": [Function],
|
|
30
|
-
"lineage": [Function],
|
|
31
|
-
"materializations": [Function],
|
|
32
|
-
"metric": [Function],
|
|
33
|
-
"metrics": [Function],
|
|
34
|
-
"namespace": [Function],
|
|
35
|
-
"namespaces": [Function],
|
|
36
|
-
"node": [Function],
|
|
37
|
-
"node_dag": [Function],
|
|
38
|
-
"node_lineage": [Function],
|
|
39
|
-
"nodesWithDimension": [Function],
|
|
40
|
-
"revisions": [Function],
|
|
41
|
-
"sql": [Function],
|
|
42
|
-
"sqls": [Function],
|
|
43
|
-
"stream": [Function],
|
|
44
|
-
"upstreams": [Function],
|
|
45
|
-
},
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
>
|
|
49
|
-
<Routes>
|
|
50
|
-
<Route
|
|
51
|
-
element={<Unknown />}
|
|
52
|
-
path="/"
|
|
53
|
-
>
|
|
54
|
-
<React.Fragment>
|
|
55
|
-
<Route
|
|
56
|
-
path="nodes"
|
|
57
|
-
>
|
|
58
|
-
<Route
|
|
59
|
-
element={<NodePage />}
|
|
60
|
-
path=":name"
|
|
61
|
-
/>
|
|
62
|
-
</Route>
|
|
63
|
-
<Route
|
|
64
|
-
element={<NamespacePage />}
|
|
65
|
-
path="/"
|
|
66
|
-
/>
|
|
67
|
-
<Route
|
|
68
|
-
path="namespaces"
|
|
69
|
-
>
|
|
70
|
-
<Route
|
|
71
|
-
element={<NamespacePage />}
|
|
72
|
-
path=":namespace"
|
|
73
|
-
/>
|
|
74
|
-
</Route>
|
|
75
|
-
<Route
|
|
76
|
-
element={<SQLBuilderPage />}
|
|
77
|
-
path="sql"
|
|
78
|
-
/>
|
|
79
|
-
</React.Fragment>
|
|
80
|
-
</Route>
|
|
81
|
-
<Route
|
|
82
|
-
element={<Unknown />}
|
|
83
|
-
path="*"
|
|
84
|
-
/>
|
|
85
|
-
</Routes>
|
|
86
|
-
</Context.Provider>
|
|
87
|
-
</BrowserRouter>
|
|
4
|
+
<CookiesProvider>
|
|
5
|
+
<BrowserRouter>
|
|
6
|
+
<LoginPage />
|
|
7
|
+
</BrowserRouter>
|
|
8
|
+
</CookiesProvider>
|
|
88
9
|
`;
|
package/src/app/index.tsx
CHANGED
|
@@ -11,48 +11,64 @@ import { NamespacePage } from './pages/NamespacePage/Loadable';
|
|
|
11
11
|
import { NodePage } from './pages/NodePage/Loadable';
|
|
12
12
|
import { SQLBuilderPage } from './pages/SQLBuilderPage/Loadable';
|
|
13
13
|
import { NotFoundPage } from './pages/NotFoundPage/Loadable';
|
|
14
|
+
import { LoginPage } from './pages/LoginPage';
|
|
14
15
|
import { Root } from './pages/Root/Loadable';
|
|
15
16
|
import DJClientContext from './providers/djclient';
|
|
16
17
|
import { DataJunctionAPI } from './services/DJService';
|
|
18
|
+
import { CookiesProvider, useCookies } from 'react-cookie';
|
|
19
|
+
import * as Constants from './constants';
|
|
17
20
|
|
|
18
21
|
export function App() {
|
|
22
|
+
const [cookies] = useCookies([Constants.DJ_LOGGED_IN_FLAG_COOKIE]);
|
|
19
23
|
return (
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
<CookiesProvider>
|
|
25
|
+
<BrowserRouter>
|
|
26
|
+
{cookies.__djlif || process.env.REACT_DISABLE_AUTH === 'true' ? (
|
|
27
|
+
<>
|
|
28
|
+
<Helmet
|
|
29
|
+
titleTemplate="DataJunction: %s"
|
|
30
|
+
defaultTitle="DataJunction: A Metrics Platform"
|
|
31
|
+
>
|
|
32
|
+
<meta
|
|
33
|
+
name="description"
|
|
34
|
+
content="DataJunction serves as a semantic layer to help manage metrics"
|
|
35
|
+
/>
|
|
36
|
+
</Helmet>
|
|
37
|
+
<DJClientContext.Provider value={{ DataJunctionAPI }}>
|
|
38
|
+
<Routes>
|
|
39
|
+
<Route
|
|
40
|
+
path="/"
|
|
41
|
+
element={<Root />}
|
|
42
|
+
children={
|
|
43
|
+
<>
|
|
44
|
+
<Route path="nodes" key="nodes">
|
|
45
|
+
<Route path=":name" element={<NodePage />} />
|
|
46
|
+
</Route>
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
<Route path="/" element={<NamespacePage />} key="index" />
|
|
49
|
+
<Route path="namespaces">
|
|
50
|
+
<Route
|
|
51
|
+
path=":namespace"
|
|
52
|
+
element={<NamespacePage />}
|
|
53
|
+
key="namespaces"
|
|
54
|
+
/>
|
|
55
|
+
</Route>
|
|
56
|
+
<Route
|
|
57
|
+
path="sql"
|
|
58
|
+
key="sql"
|
|
59
|
+
element={<SQLBuilderPage />}
|
|
60
|
+
/>
|
|
61
|
+
</>
|
|
62
|
+
}
|
|
63
|
+
/>
|
|
64
|
+
<Route path="*" element={<NotFoundPage />} />
|
|
65
|
+
</Routes>
|
|
66
|
+
</DJClientContext.Provider>
|
|
67
|
+
</>
|
|
68
|
+
) : (
|
|
69
|
+
<LoginPage />
|
|
70
|
+
)}
|
|
71
|
+
</BrowserRouter>
|
|
72
|
+
</CookiesProvider>
|
|
57
73
|
);
|
|
58
74
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Formik, Form, Field, ErrorMessage } from 'formik';
|
|
3
|
+
import '../../../styles/login.css';
|
|
4
|
+
import logo from '../Root/assets/dj-logo.png';
|
|
5
|
+
import GitHubLoginButton from './assets/sign-in-with-github.png';
|
|
6
|
+
|
|
7
|
+
export function LoginPage() {
|
|
8
|
+
const [, setError] = useState('');
|
|
9
|
+
const githubLoginURL = new URL('/github/login/', process.env.REACT_APP_DJ_URL)
|
|
10
|
+
.href;
|
|
11
|
+
|
|
12
|
+
const handleBasicLogin = async ({ username, password }) => {
|
|
13
|
+
const data = new FormData();
|
|
14
|
+
data.append('username', username);
|
|
15
|
+
data.append('password', password);
|
|
16
|
+
await fetch(`${process.env.REACT_APP_DJ_URL}/basic/login/`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
body: data,
|
|
19
|
+
credentials: 'include',
|
|
20
|
+
}).catch(error => {
|
|
21
|
+
setError(error ? JSON.stringify(error) : '');
|
|
22
|
+
});
|
|
23
|
+
window.location.reload();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="container">
|
|
28
|
+
<div className="login">
|
|
29
|
+
<center>
|
|
30
|
+
<Formik
|
|
31
|
+
initialValues={{ username: '', password: '' }}
|
|
32
|
+
validate={values => {
|
|
33
|
+
const errors = {};
|
|
34
|
+
if (!values.username) {
|
|
35
|
+
errors.username = 'Required';
|
|
36
|
+
}
|
|
37
|
+
if (!values.password) {
|
|
38
|
+
errors.password = 'Required';
|
|
39
|
+
}
|
|
40
|
+
return errors;
|
|
41
|
+
}}
|
|
42
|
+
onSubmit={(values, { setSubmitting }) => {
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
handleBasicLogin(values);
|
|
45
|
+
setSubmitting(false);
|
|
46
|
+
}, 400);
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{({ isSubmitting }) => (
|
|
50
|
+
<Form>
|
|
51
|
+
<div className="logo-title">
|
|
52
|
+
<img src={logo} alt="DJ Logo" width="75px" height="75px" />
|
|
53
|
+
<h2>DataJunction</h2>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="inputContainer">
|
|
56
|
+
<ErrorMessage name="username" component="span" />
|
|
57
|
+
<Field type="text" name="username" placeholder="Username" />
|
|
58
|
+
</div>
|
|
59
|
+
<div>
|
|
60
|
+
<ErrorMessage name="password" component="span" />
|
|
61
|
+
<Field
|
|
62
|
+
type="password"
|
|
63
|
+
name="password"
|
|
64
|
+
placeholder="Password"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
<button type="submit" disabled={isSubmitting}>
|
|
68
|
+
Login
|
|
69
|
+
</button>
|
|
70
|
+
{process.env.REACT_ENABLE_GITHUB_OAUTH === 'true' ? (
|
|
71
|
+
<div>
|
|
72
|
+
<a href={githubLoginURL}>
|
|
73
|
+
<img
|
|
74
|
+
src={GitHubLoginButton}
|
|
75
|
+
alt="Sign in with GitHub"
|
|
76
|
+
width="200px"
|
|
77
|
+
/>
|
|
78
|
+
</a>
|
|
79
|
+
</div>
|
|
80
|
+
) : (
|
|
81
|
+
''
|
|
82
|
+
)}
|
|
83
|
+
</Form>
|
|
84
|
+
)}
|
|
85
|
+
</Formik>
|
|
86
|
+
</center>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -20,7 +20,7 @@ export function NamespacePage() {
|
|
|
20
20
|
const hierarchy = [];
|
|
21
21
|
|
|
22
22
|
for (const item of namespaceList) {
|
|
23
|
-
const namespaces = item.split('.');
|
|
23
|
+
const namespaces = item.namespace.split('.');
|
|
24
24
|
let currentLevel = hierarchy;
|
|
25
25
|
|
|
26
26
|
let path = '';
|
|
@@ -58,10 +58,7 @@ export function NamespacePage() {
|
|
|
58
58
|
if (namespace === undefined && namespaceHierarchy !== undefined) {
|
|
59
59
|
namespace = namespaceHierarchy.children[0].path;
|
|
60
60
|
}
|
|
61
|
-
const
|
|
62
|
-
const nodes = djNodes.map(node => {
|
|
63
|
-
return djClient.node(node);
|
|
64
|
-
});
|
|
61
|
+
const nodes = await djClient.namespace(namespace);
|
|
65
62
|
const foundNodes = await Promise.all(nodes);
|
|
66
63
|
setState({
|
|
67
64
|
namespace: namespace,
|
|
@@ -100,9 +97,6 @@ export function NamespacePage() {
|
|
|
100
97
|
<td>
|
|
101
98
|
<span className="status">{node.mode}</span>
|
|
102
99
|
</td>
|
|
103
|
-
<td>
|
|
104
|
-
<span className="status">{node.tags}</span>
|
|
105
|
-
</td>
|
|
106
100
|
<td>
|
|
107
101
|
<span className="status">
|
|
108
102
|
{new Date(node.updated_at).toLocaleString('en-us')}
|
|
@@ -147,7 +141,6 @@ export function NamespacePage() {
|
|
|
147
141
|
<th>Type</th>
|
|
148
142
|
<th>Status</th>
|
|
149
143
|
<th>Mode</th>
|
|
150
|
-
<th>Tags</th>
|
|
151
144
|
<th>Last Updated</th>
|
|
152
145
|
</tr>
|
|
153
146
|
</thead>
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useContext } from 'react';
|
|
2
2
|
import { MarkerType } from 'reactflow';
|
|
3
3
|
|
|
4
4
|
import '../../../styles/dag.css';
|
|
5
5
|
import 'reactflow/dist/style.css';
|
|
6
|
-
import DJNode from '../../components/djgraph/DJNode';
|
|
7
6
|
import DJClientContext from '../../providers/djclient';
|
|
8
7
|
import LayoutFlow from '../../components/djgraph/LayoutFlow';
|
|
9
8
|
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
1
2
|
import { Outlet } from 'react-router-dom';
|
|
2
3
|
import logo from './assets/dj-logo.png';
|
|
3
4
|
import { Helmet } from 'react-helmet-async';
|
|
5
|
+
import DJClientContext from '../../providers/djclient';
|
|
4
6
|
|
|
5
7
|
export function Root() {
|
|
8
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
9
|
+
|
|
10
|
+
const handleLogout = async () => {
|
|
11
|
+
await djClient.logout();
|
|
12
|
+
window.location.reload();
|
|
13
|
+
};
|
|
6
14
|
return (
|
|
7
15
|
<>
|
|
8
16
|
<Helmet>
|
|
@@ -46,6 +54,15 @@ export function Root() {
|
|
|
46
54
|
</div>
|
|
47
55
|
</div>
|
|
48
56
|
</div>
|
|
57
|
+
{process.env.REACT_DISABLE_AUTH === 'true' ? (
|
|
58
|
+
''
|
|
59
|
+
) : (
|
|
60
|
+
<span className="menu-link">
|
|
61
|
+
<span className="menu-title">
|
|
62
|
+
<button onClick={handleLogout}>Logout</button>
|
|
63
|
+
</span>
|
|
64
|
+
</span>
|
|
65
|
+
)}
|
|
49
66
|
</div>
|
|
50
67
|
<Outlet />
|
|
51
68
|
</>
|
|
@@ -6,10 +6,14 @@ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
|
6
6
|
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
7
7
|
import Select from 'react-select';
|
|
8
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';
|
|
9
12
|
|
|
10
13
|
export function SQLBuilderPage() {
|
|
11
14
|
const DEFAULT_NUM_ROWS = 100;
|
|
12
15
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
16
|
+
const validator = ruleType => !!ruleType.value;
|
|
13
17
|
const [stagedMetrics, setStagedMetrics] = useState([]);
|
|
14
18
|
const [metrics, setMetrics] = useState([]);
|
|
15
19
|
const [commonDimensionsList, setCommonDimensionsList] = useState([]);
|
|
@@ -17,6 +21,8 @@ export function SQLBuilderPage() {
|
|
|
17
21
|
const [stagedDimensions, setStagedDimensions] = useState([]);
|
|
18
22
|
const [selectedMetrics, setSelectedMetrics] = useState([]);
|
|
19
23
|
const [query, setQuery] = useState('');
|
|
24
|
+
const [fields, setFields] = useState([]);
|
|
25
|
+
const [filters, setFilters] = useState({ combinator: 'and', rules: [] });
|
|
20
26
|
const [queryInfo, setQueryInfo] = useState({});
|
|
21
27
|
const [data, setData] = useState(null);
|
|
22
28
|
const [loadingData, setLoadingData] = useState(false);
|
|
@@ -48,7 +54,11 @@ export function SQLBuilderPage() {
|
|
|
48
54
|
setQueryInfo({});
|
|
49
55
|
const fetchData = async () => {
|
|
50
56
|
if (process.env.REACT_USE_SSE) {
|
|
51
|
-
const sse = await djClient.stream(
|
|
57
|
+
const sse = await djClient.stream(
|
|
58
|
+
selectedMetrics,
|
|
59
|
+
selectedDimensions,
|
|
60
|
+
formatQuery(filters, { format: 'sql', parseNumbers: true }),
|
|
61
|
+
);
|
|
52
62
|
sse.onmessage = e => {
|
|
53
63
|
const messageData = JSON.parse(JSON.parse(e.data));
|
|
54
64
|
setQueryInfo(messageData);
|
|
@@ -99,6 +109,24 @@ export function SQLBuilderPage() {
|
|
|
99
109
|
fetchData().catch(console.error);
|
|
100
110
|
}, [djClient, djClient.metrics]);
|
|
101
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
|
+
|
|
102
130
|
// Get common dimensions
|
|
103
131
|
useEffect(() => {
|
|
104
132
|
const fetchData = async () => {
|
|
@@ -113,8 +141,15 @@ export function SQLBuilderPage() {
|
|
|
113
141
|
path: d.path.join(' ▶ '),
|
|
114
142
|
})),
|
|
115
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]));
|
|
116
150
|
} else {
|
|
117
151
|
setCommonDimensionsList([]);
|
|
152
|
+
setFields([]);
|
|
118
153
|
}
|
|
119
154
|
};
|
|
120
155
|
fetchData().catch(console.error);
|
|
@@ -123,15 +158,22 @@ export function SQLBuilderPage() {
|
|
|
123
158
|
// Get SQL
|
|
124
159
|
useEffect(() => {
|
|
125
160
|
const fetchData = async () => {
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
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);
|
|
129
171
|
} else {
|
|
130
172
|
resetView();
|
|
131
173
|
}
|
|
132
174
|
};
|
|
133
175
|
fetchData().catch(console.error);
|
|
134
|
-
}, [
|
|
176
|
+
}, [djClient, filters, selectedDimensions, selectedMetrics]);
|
|
135
177
|
|
|
136
178
|
// Set number of rows to display
|
|
137
179
|
useEffect(() => {
|
|
@@ -166,9 +208,7 @@ export function SQLBuilderPage() {
|
|
|
166
208
|
name="metrics"
|
|
167
209
|
options={metrics}
|
|
168
210
|
isDisabled={
|
|
169
|
-
selectedMetrics.length && selectedDimensions.length
|
|
170
|
-
? true
|
|
171
|
-
: false
|
|
211
|
+
!!(selectedMetrics.length && selectedDimensions.length)
|
|
172
212
|
}
|
|
173
213
|
noOptionsMessage={() => 'No metrics found.'}
|
|
174
214
|
placeholder={`${metrics.length} Available Metrics`}
|
|
@@ -206,6 +246,12 @@ export function SQLBuilderPage() {
|
|
|
206
246
|
setSelectedDimensions(stagedDimensions);
|
|
207
247
|
}}
|
|
208
248
|
/>
|
|
249
|
+
<h4>Filter By</h4>
|
|
250
|
+
<QueryBuilder
|
|
251
|
+
fields={fields}
|
|
252
|
+
query={filters}
|
|
253
|
+
onQueryChange={q => setFilters(q)}
|
|
254
|
+
/>
|
|
209
255
|
</div>
|
|
210
256
|
<div className="card-header">
|
|
211
257
|
{!viewData && !query ? (
|
|
@@ -5,8 +5,26 @@ const DJ_URL = process.env.REACT_APP_DJ_URL
|
|
|
5
5
|
: 'http://localhost:8000';
|
|
6
6
|
|
|
7
7
|
export const DataJunctionAPI = {
|
|
8
|
+
whoami: async function () {
|
|
9
|
+
const data = await (
|
|
10
|
+
await fetch(`${DJ_URL}/whoami/`, { credentials: 'include' })
|
|
11
|
+
).json();
|
|
12
|
+
return data;
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
logout: async function () {
|
|
16
|
+
await await fetch(`${DJ_URL}/basic/logout/`, {
|
|
17
|
+
credentials: 'include',
|
|
18
|
+
method: 'POST',
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
|
|
8
22
|
node: async function (name) {
|
|
9
|
-
const data = await (
|
|
23
|
+
const data = await (
|
|
24
|
+
await fetch(`${DJ_URL}/nodes/${name}/`, {
|
|
25
|
+
credentials: 'include',
|
|
26
|
+
})
|
|
27
|
+
).json();
|
|
10
28
|
data.primary_key = data.columns
|
|
11
29
|
.filter(col =>
|
|
12
30
|
col.attributes.some(attr => attr.attribute_type.name === 'primary_key'),
|
|
@@ -17,58 +35,82 @@ export const DataJunctionAPI = {
|
|
|
17
35
|
|
|
18
36
|
upstreams: async function (name) {
|
|
19
37
|
const data = await (
|
|
20
|
-
await fetch(DJ_URL
|
|
38
|
+
await fetch(`${DJ_URL}/nodes/${name}/upstream/`, {
|
|
39
|
+
credentials: 'include',
|
|
40
|
+
})
|
|
21
41
|
).json();
|
|
22
42
|
return data;
|
|
23
43
|
},
|
|
24
44
|
|
|
25
45
|
downstreams: async function (name) {
|
|
26
46
|
const data = await (
|
|
27
|
-
await fetch(DJ_URL
|
|
47
|
+
await fetch(`${DJ_URL}/nodes/` + name + '/downstream/', {
|
|
48
|
+
credentials: 'include',
|
|
49
|
+
})
|
|
28
50
|
).json();
|
|
29
51
|
return data;
|
|
30
52
|
},
|
|
31
53
|
|
|
32
54
|
node_dag: async function (name) {
|
|
33
55
|
const data = await (
|
|
34
|
-
await fetch(DJ_URL
|
|
56
|
+
await fetch(`${DJ_URL}/nodes/` + name + '/dag/', {
|
|
57
|
+
credentials: 'include',
|
|
58
|
+
})
|
|
35
59
|
).json();
|
|
36
60
|
return data;
|
|
37
61
|
},
|
|
38
62
|
|
|
39
63
|
node_lineage: async function (name) {
|
|
40
64
|
const data = await (
|
|
41
|
-
await fetch(DJ_URL
|
|
65
|
+
await fetch(`${DJ_URL}/nodes/` + name + '/lineage/', {
|
|
66
|
+
credentials: 'include',
|
|
67
|
+
})
|
|
42
68
|
).json();
|
|
43
69
|
return data;
|
|
44
70
|
},
|
|
45
71
|
|
|
46
72
|
metric: async function (name) {
|
|
47
|
-
const data = await (
|
|
73
|
+
const data = await (
|
|
74
|
+
await fetch(`${DJ_URL}/metrics/` + name + '/', {
|
|
75
|
+
credentials: 'include',
|
|
76
|
+
})
|
|
77
|
+
).json();
|
|
48
78
|
return data;
|
|
49
79
|
},
|
|
50
80
|
|
|
51
81
|
clientCode: async function (name) {
|
|
52
82
|
const data = await (
|
|
53
|
-
await fetch(DJ_URL
|
|
83
|
+
await fetch(`${DJ_URL}/datajunction-clients/python/new_node/` + name, {
|
|
84
|
+
credentials: 'include',
|
|
85
|
+
})
|
|
54
86
|
).json();
|
|
55
87
|
return data;
|
|
56
88
|
},
|
|
57
89
|
|
|
58
90
|
cube: async function (name) {
|
|
59
|
-
const data = await (
|
|
91
|
+
const data = await (
|
|
92
|
+
await fetch(`${DJ_URL}/cubes/` + name + '/', {
|
|
93
|
+
credentials: 'include',
|
|
94
|
+
})
|
|
95
|
+
).json();
|
|
60
96
|
return data;
|
|
61
97
|
},
|
|
62
98
|
|
|
63
99
|
metrics: async function (name) {
|
|
64
|
-
const data = await (
|
|
100
|
+
const data = await (
|
|
101
|
+
await fetch(`${DJ_URL}/metrics/`, {
|
|
102
|
+
credentials: 'include',
|
|
103
|
+
})
|
|
104
|
+
).json();
|
|
65
105
|
return data;
|
|
66
106
|
},
|
|
67
107
|
|
|
68
108
|
commonDimensions: async function (metrics) {
|
|
69
109
|
const metricsQuery = '?' + metrics.map(m => `metric=${m}`).join('&');
|
|
70
110
|
const data = await (
|
|
71
|
-
await fetch(DJ_URL
|
|
111
|
+
await fetch(`${DJ_URL}/metrics/common/dimensions/` + metricsQuery, {
|
|
112
|
+
credentials: 'include',
|
|
113
|
+
})
|
|
72
114
|
).json();
|
|
73
115
|
return data;
|
|
74
116
|
},
|
|
@@ -76,10 +118,12 @@ export const DataJunctionAPI = {
|
|
|
76
118
|
history: async function (type, name, offset, limit) {
|
|
77
119
|
const data = await (
|
|
78
120
|
await fetch(
|
|
79
|
-
DJ_URL +
|
|
80
|
-
'/history?node=' +
|
|
121
|
+
`${DJ_URL}/history?node=` +
|
|
81
122
|
name +
|
|
82
123
|
`&offset=${offset ? offset : 0}&limit=${limit ? limit : 100}`,
|
|
124
|
+
{
|
|
125
|
+
credentials: 'include',
|
|
126
|
+
},
|
|
83
127
|
)
|
|
84
128
|
).json();
|
|
85
129
|
return data;
|
|
@@ -87,27 +131,36 @@ export const DataJunctionAPI = {
|
|
|
87
131
|
|
|
88
132
|
revisions: async function (name) {
|
|
89
133
|
const data = await (
|
|
90
|
-
await fetch(DJ_URL
|
|
134
|
+
await fetch(`${DJ_URL}/nodes/` + name + '/revisions/')
|
|
91
135
|
).json();
|
|
92
136
|
return data;
|
|
93
137
|
},
|
|
94
138
|
|
|
95
139
|
namespace: async function (nmspce) {
|
|
96
140
|
const data = await (
|
|
97
|
-
await fetch(DJ_URL
|
|
141
|
+
await fetch(`${DJ_URL}/namespaces/` + nmspce + '/', {
|
|
142
|
+
credentials: 'include',
|
|
143
|
+
})
|
|
98
144
|
).json();
|
|
99
145
|
return data;
|
|
100
146
|
},
|
|
101
147
|
|
|
102
148
|
namespaces: async function () {
|
|
103
|
-
const data = await (
|
|
149
|
+
const data = await (
|
|
150
|
+
await fetch(`${DJ_URL}/namespaces/`, {
|
|
151
|
+
credentials: 'include',
|
|
152
|
+
})
|
|
153
|
+
).json();
|
|
104
154
|
return data;
|
|
105
155
|
},
|
|
106
156
|
|
|
107
157
|
sql: async function (metric_name, selection) {
|
|
108
158
|
const data = await (
|
|
109
159
|
await fetch(
|
|
110
|
-
DJ_URL
|
|
160
|
+
`${DJ_URL}/sql/` + metric_name + '?' + new URLSearchParams(selection),
|
|
161
|
+
{
|
|
162
|
+
credentials: 'include',
|
|
163
|
+
},
|
|
111
164
|
)
|
|
112
165
|
).json();
|
|
113
166
|
return data;
|
|
@@ -115,22 +168,28 @@ export const DataJunctionAPI = {
|
|
|
115
168
|
|
|
116
169
|
nodesWithDimension: async function (name) {
|
|
117
170
|
const data = await (
|
|
118
|
-
await fetch(DJ_URL
|
|
171
|
+
await fetch(`${DJ_URL}/dimensions/` + name + '/nodes/', {
|
|
172
|
+
credentials: 'include',
|
|
173
|
+
})
|
|
119
174
|
).json();
|
|
120
175
|
return data;
|
|
121
176
|
},
|
|
122
177
|
|
|
123
178
|
materializations: async function (node) {
|
|
124
179
|
const data = await (
|
|
125
|
-
await fetch(DJ_URL
|
|
180
|
+
await fetch(`${DJ_URL}/nodes/${node}/materializations/`, {
|
|
181
|
+
credentials: 'include',
|
|
182
|
+
})
|
|
126
183
|
).json();
|
|
127
184
|
|
|
128
185
|
return await Promise.all(
|
|
129
186
|
data.map(async materialization => {
|
|
130
187
|
materialization.clientCode = await (
|
|
131
188
|
await fetch(
|
|
132
|
-
DJ_URL
|
|
133
|
-
|
|
189
|
+
`${DJ_URL}/datajunction-clients/python/add_materialization/${node}/${materialization.name}`,
|
|
190
|
+
{
|
|
191
|
+
credentials: 'include',
|
|
192
|
+
},
|
|
134
193
|
)
|
|
135
194
|
).json();
|
|
136
195
|
return materialization;
|
|
@@ -144,8 +203,10 @@ export const DataJunctionAPI = {
|
|
|
144
203
|
if (col.dimension) {
|
|
145
204
|
col.clientCode = await (
|
|
146
205
|
await fetch(
|
|
147
|
-
DJ_URL
|
|
148
|
-
|
|
206
|
+
`${DJ_URL}/datajunction-clients/python/link_dimension/${node.name}/${col.name}/${col.dimension?.name}`,
|
|
207
|
+
{
|
|
208
|
+
credentials: 'include',
|
|
209
|
+
},
|
|
149
210
|
)
|
|
150
211
|
).json();
|
|
151
212
|
}
|
|
@@ -154,12 +215,16 @@ export const DataJunctionAPI = {
|
|
|
154
215
|
);
|
|
155
216
|
},
|
|
156
217
|
|
|
157
|
-
sqls: async function (metricSelection, dimensionSelection) {
|
|
218
|
+
sqls: async function (metricSelection, dimensionSelection, filters) {
|
|
158
219
|
const params = new URLSearchParams();
|
|
159
220
|
metricSelection.map(metric => params.append('metrics', metric));
|
|
160
221
|
dimensionSelection.map(dimension => params.append('dimensions', dimension));
|
|
161
|
-
|
|
162
|
-
return
|
|
222
|
+
params.append('filters', filters);
|
|
223
|
+
return await (
|
|
224
|
+
await fetch(`${DJ_URL}/sql/?${params}`, {
|
|
225
|
+
credentials: 'include',
|
|
226
|
+
})
|
|
227
|
+
).json();
|
|
163
228
|
},
|
|
164
229
|
|
|
165
230
|
data: async function (metricSelection, dimensionSelection) {
|
|
@@ -167,32 +232,44 @@ export const DataJunctionAPI = {
|
|
|
167
232
|
metricSelection.map(metric => params.append('metrics', metric));
|
|
168
233
|
dimensionSelection.map(dimension => params.append('dimensions', dimension));
|
|
169
234
|
const data = await (
|
|
170
|
-
await fetch(DJ_URL
|
|
235
|
+
await fetch(`${DJ_URL}/data/?` + params + '&limit=10000', {
|
|
236
|
+
credentials: 'include',
|
|
237
|
+
})
|
|
171
238
|
).json();
|
|
172
239
|
return data;
|
|
173
240
|
},
|
|
174
241
|
|
|
175
|
-
stream: async function (metricSelection, dimensionSelection) {
|
|
242
|
+
stream: async function (metricSelection, dimensionSelection, filters) {
|
|
176
243
|
const params = new URLSearchParams();
|
|
177
244
|
metricSelection.map(metric => params.append('metrics', metric));
|
|
178
245
|
dimensionSelection.map(dimension => params.append('dimensions', dimension));
|
|
246
|
+
params.append('filters', filters);
|
|
179
247
|
return new EventSource(
|
|
180
|
-
DJ_URL
|
|
248
|
+
`${DJ_URL}/stream/?${params}&limit=10000&async_=true`,
|
|
249
|
+
{
|
|
250
|
+
withCredentials: true,
|
|
251
|
+
},
|
|
181
252
|
);
|
|
182
253
|
},
|
|
183
254
|
|
|
184
255
|
lineage: async function (node) {},
|
|
185
256
|
|
|
186
257
|
compiledSql: async function (node) {
|
|
187
|
-
const data = await (
|
|
258
|
+
const data = await (
|
|
259
|
+
await fetch(`${DJ_URL}/sql/${node}/`, {
|
|
260
|
+
credentials: 'include',
|
|
261
|
+
})
|
|
262
|
+
).json();
|
|
188
263
|
return data;
|
|
189
264
|
},
|
|
190
265
|
|
|
191
266
|
dag: async function (namespace = 'default') {
|
|
192
267
|
const edges = [];
|
|
193
|
-
const data = await (
|
|
194
|
-
|
|
195
|
-
|
|
268
|
+
const data = await (
|
|
269
|
+
await fetch(`${DJ_URL}/nodes/`, {
|
|
270
|
+
credentials: 'include',
|
|
271
|
+
})
|
|
272
|
+
).json();
|
|
196
273
|
|
|
197
274
|
data.forEach(obj => {
|
|
198
275
|
obj.parents.forEach(parent => {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
.login form {
|
|
2
|
+
border-radius: 25px;
|
|
3
|
+
border: 2px solid #000000;
|
|
4
|
+
background: linear-gradient(120deg, #ffed7c, #fef7c8);
|
|
5
|
+
width: 40vw;
|
|
6
|
+
height: 30rem;
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
padding: 2rem;
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-columns: 1fr;
|
|
11
|
+
gap: 1rem;
|
|
12
|
+
margin: auto;
|
|
13
|
+
position: absolute;
|
|
14
|
+
top: 0;
|
|
15
|
+
bottom: 0;
|
|
16
|
+
left: 0;
|
|
17
|
+
right: 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.login form button {
|
|
21
|
+
width: 15vw;
|
|
22
|
+
margin: auto;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.login form span {
|
|
26
|
+
padding: 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.login input[type='text'] {
|
|
30
|
+
width: 20vw;
|
|
31
|
+
padding: 12px 20px;
|
|
32
|
+
margin: auto;
|
|
33
|
+
display: inline-block;
|
|
34
|
+
border: 2px solid #000000;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.login input[type='password'] {
|
|
40
|
+
-webkit-text-security: disc;
|
|
41
|
+
width: 20vw;
|
|
42
|
+
padding: 12px 20px;
|
|
43
|
+
margin: auto;
|
|
44
|
+
display: inline-block;
|
|
45
|
+
border: 2px solid #000000;
|
|
46
|
+
border-radius: 4px;
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.login button[type='submit'] {
|
|
51
|
+
width: 10vw;
|
|
52
|
+
background-color: #01b268;
|
|
53
|
+
border: 2px solid #000000;
|
|
54
|
+
color: white;
|
|
55
|
+
padding: 14px 20px;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.login .logo-title {
|
|
61
|
+
display: flex;
|
|
62
|
+
margin: auto;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.login .logo-title img {
|
|
66
|
+
padding: 0.5rem;
|
|
67
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
@use 'sass:color';
|
|
2
|
+
|
|
3
|
+
.svg-font-color svg > path {
|
|
4
|
+
fill: var(--ifm-font-color-base);
|
|
5
|
+
}
|
|
6
|
+
select {
|
|
7
|
+
border-radius: 0.25rem;
|
|
8
|
+
font-size: 0.875rem;
|
|
9
|
+
padding-bottom: 0.25rem;
|
|
10
|
+
padding-left: 0.5rem;
|
|
11
|
+
padding-top: 0.25rem;
|
|
12
|
+
max-width: 20vw;
|
|
13
|
+
}
|
|
14
|
+
.queryBuilder {
|
|
15
|
+
min-width: 420px;
|
|
16
|
+
|
|
17
|
+
.ruleGroup div select,
|
|
18
|
+
.ruleGroup div input {
|
|
19
|
+
-webkit-text-security: none;
|
|
20
|
+
border-radius: 0.25rem;
|
|
21
|
+
font-size: 0.875rem;
|
|
22
|
+
padding-bottom: 0.35rem;
|
|
23
|
+
padding-left: 0.5rem;
|
|
24
|
+
padding-top: 0.35rem;
|
|
25
|
+
border: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.ruleGroup div button {
|
|
29
|
+
background-color: #f0f8ff;
|
|
30
|
+
color: #24518f;
|
|
31
|
+
border: 1px solid #365b8f;
|
|
32
|
+
border-radius: 0.25rem;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
display: inline-block;
|
|
35
|
+
font-size: 0.875rem;
|
|
36
|
+
font-weight: 400;
|
|
37
|
+
line-height: 1.5;
|
|
38
|
+
padding: 0.25rem 0.5rem;
|
|
39
|
+
text-align: center;
|
|
40
|
+
text-decoration: none;
|
|
41
|
+
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
|
|
42
|
+
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/webpack.config.js
CHANGED
|
@@ -2,6 +2,8 @@ const webpack = require('webpack');
|
|
|
2
2
|
const dotenv = require('dotenv').config();
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
5
|
+
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
6
|
+
|
|
5
7
|
require('dotenv').config({ path: './.env' });
|
|
6
8
|
|
|
7
9
|
var babelOptions = {
|
|
@@ -31,7 +33,7 @@ module.exports = {
|
|
|
31
33
|
},
|
|
32
34
|
},
|
|
33
35
|
resolve: {
|
|
34
|
-
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
|
|
36
|
+
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '.scss'],
|
|
35
37
|
modules: ['src', 'node_modules'],
|
|
36
38
|
fallback: {
|
|
37
39
|
path: false,
|
|
@@ -72,6 +74,10 @@ module.exports = {
|
|
|
72
74
|
test: /\.css$/,
|
|
73
75
|
use: ['style-loader', 'css-loader'],
|
|
74
76
|
},
|
|
77
|
+
{
|
|
78
|
+
test: /\.(s(a|c)ss)$/,
|
|
79
|
+
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
|
|
80
|
+
},
|
|
75
81
|
{
|
|
76
82
|
test: /\.js$/,
|
|
77
83
|
exclude: /node_modules/,
|
|
@@ -104,5 +110,9 @@ module.exports = {
|
|
|
104
110
|
new webpack.DefinePlugin({
|
|
105
111
|
'process.env': JSON.stringify(process.env),
|
|
106
112
|
}),
|
|
113
|
+
new MiniCssExtractPlugin({
|
|
114
|
+
filename: '[name].css', // isDevelopment ? '[name].css' : '[name].[hash].css',
|
|
115
|
+
chunkFilename: '[id].css', // isDevelopment ? '[id].css' : '[id].[hash].css'
|
|
116
|
+
}),
|
|
107
117
|
],
|
|
108
118
|
};
|