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 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.17",
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.7.3",
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.2",
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.5"
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
- <BrowserRouter>
5
- <Helmet
6
- defaultTitle="DataJunction: A Metrics Platform"
7
- defer={true}
8
- encodeSpecialCharacters={true}
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
  `;
@@ -1,4 +1,4 @@
1
- import { memo, useLayoutEffect, useRef, useState } from 'react';
1
+ import { memo } from 'react';
2
2
  import { Handle, Position } from 'reactflow';
3
3
  import Collapse from './Collapse';
4
4
 
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo } from 'react';
2
2
  import ReactFlow, {
3
3
  addEdge,
4
4
  MiniMap,
@@ -0,0 +1,2 @@
1
+ export const DJ_AUTH_COOKIE = '__dj';
2
+ export const DJ_LOGGED_IN_FLAG_COOKIE = '__djlif';
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
- <BrowserRouter>
21
- <Helmet
22
- titleTemplate="DataJunction: %s"
23
- defaultTitle="DataJunction: A Metrics Platform"
24
- >
25
- <meta
26
- name="description"
27
- content="DataJunction serves as a semantic layer to help manage metrics"
28
- />
29
- </Helmet>
30
- <DJClientContext.Provider value={{ DataJunctionAPI }}>
31
- <Routes>
32
- <Route
33
- path="/"
34
- element={<Root />}
35
- children={
36
- <>
37
- <Route path="nodes" key="nodes">
38
- <Route path=":name" element={<NodePage />} />
39
- </Route>
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
- <Route path="/" element={<NamespacePage />} key="index" />
42
- <Route path="namespaces">
43
- <Route
44
- path=":namespace"
45
- element={<NamespacePage />}
46
- key="namespaces"
47
- />
48
- </Route>
49
- <Route path="sql" key="sql" element={<SQLBuilderPage />} />
50
- </>
51
- }
52
- />
53
- <Route path="*" element={<NotFoundPage />} />
54
- </Routes>
55
- </DJClientContext.Provider>
56
- </BrowserRouter>
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
  }
@@ -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
+ }
@@ -53,9 +53,6 @@ exports[`<NamespacePage /> should render and match the snapshot 1`] = `
53
53
  <th>
54
54
  Mode
55
55
  </th>
56
- <th>
57
- Tags
58
- </th>
59
56
  <th>
60
57
  Last Updated
61
58
  </th>
@@ -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 djNodes = await djClient.namespace(namespace);
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 React, { useContext } from 'react';
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(selectedMetrics, selectedDimensions);
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 (selectedMetrics.length && selectedDimensions.length) {
127
- const query = await djClient.sqls(selectedMetrics, selectedDimensions);
128
- setQuery(query.sql);
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
- }, [selectedMetrics, selectedDimensions, djClient]);
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 (await fetch(DJ_URL + '/nodes/' + name + '/')).json();
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 + '/nodes/' + name + '/upstream/')
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 + '/nodes/' + name + '/downstream/')
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 + '/nodes/' + name + '/dag/')
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 + '/nodes/' + name + '/lineage/')
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 (await fetch(DJ_URL + '/metrics/' + name + '/')).json();
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 + '/datajunction-clients/python/new_node/' + name)
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 (await fetch(DJ_URL + '/cubes/' + name + '/')).json();
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 (await fetch(DJ_URL + '/metrics/')).json();
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 + '/metrics/common/dimensions/' + metricsQuery)
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 + '/nodes/' + name + '/revisions/')
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 + '/namespaces/' + nmspce + '/')
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 (await fetch(DJ_URL + '/namespaces/')).json();
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 + '/sql/' + metric_name + '?' + new URLSearchParams(selection),
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 + '/dimensions/' + name + '/nodes/')
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 + `/nodes/${node}/materializations/`)
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
- `/datajunction-clients/python/add_materialization/${node}/${materialization.name}`,
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
- `/datajunction-clients/python/link_dimension/${node.name}/${col.name}/${col.dimension?.name}`,
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
- const data = await (await fetch(DJ_URL + '/sql/?' + params)).json();
162
- return data;
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 + '/data/?' + params + '&limit=10000')
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 + '/stream/?' + params + '&limit=10000&async_=true',
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 (await fetch(DJ_URL + `/sql/${node}/`)).json();
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 (await fetch(DJ_URL + '/nodes/')).json();
194
-
195
- // const metrics = await (await fetch(DJ_URL + '/metrics/')).json();
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
  };