datajunction-ui 0.0.1-rc.1 → 0.0.1-rc.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.github/workflows/ci.yml +3 -3
  2. package/.prettierignore +3 -1
  3. package/package.json +16 -8
  4. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +67 -25
  5. package/src/app/components/NamespaceHeader.jsx +4 -13
  6. package/src/app/components/QueryInfo.jsx +109 -0
  7. package/src/app/components/Tab.jsx +1 -8
  8. package/src/app/components/ToggleSwitch.jsx +17 -0
  9. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
  10. package/src/app/components/djgraph/Collapse.jsx +46 -0
  11. package/src/app/components/djgraph/DJNode.jsx +56 -80
  12. package/src/app/components/djgraph/DJNodeColumns.jsx +68 -0
  13. package/src/app/components/djgraph/DJNodeDimensions.jsx +69 -0
  14. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +82 -43
  15. package/src/app/icons/CollapsedIcon.jsx +15 -0
  16. package/src/app/icons/ExpandedIcon.jsx +15 -0
  17. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  18. package/src/app/icons/InvalidIcon.jsx +14 -0
  19. package/src/app/icons/PythonIcon.jsx +52 -0
  20. package/src/app/icons/TableIcon.jsx +14 -0
  21. package/src/app/icons/ValidIcon.jsx +14 -0
  22. package/src/app/index.tsx +28 -15
  23. package/src/app/pages/NamespacePage/Explorer.jsx +57 -0
  24. package/src/app/pages/NamespacePage/Loadable.jsx +9 -7
  25. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +39 -17
  26. package/src/app/pages/NamespacePage/index.jsx +90 -28
  27. package/src/app/pages/NodePage/ClientCodePopover.jsx +30 -0
  28. package/src/app/pages/NodePage/Loadable.jsx +9 -7
  29. package/src/app/pages/NodePage/NodeColumnTab.jsx +36 -20
  30. package/src/app/pages/NodePage/NodeGraphTab.jsx +83 -54
  31. package/src/app/pages/NodePage/NodeHistory.jsx +71 -0
  32. package/src/app/pages/NodePage/NodeInfoTab.jsx +132 -49
  33. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +151 -0
  34. package/src/app/pages/NodePage/NodeSQLTab.jsx +100 -0
  35. package/src/app/pages/NodePage/NodeStatus.jsx +14 -20
  36. package/src/app/pages/NodePage/index.jsx +49 -13
  37. package/src/app/pages/Root/index.tsx +5 -0
  38. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  39. package/src/app/pages/SQLBuilderPage/index.jsx +344 -0
  40. package/src/app/providers/djclient.jsx +5 -0
  41. package/src/app/services/DJService.js +125 -1
  42. package/src/styles/dag.css +111 -5
  43. package/src/styles/index.css +343 -25
  44. package/tsconfig.json +1 -1
  45. package/webpack.config.js +22 -6
  46. package/.babelrc +0 -4
  47. package/.env.local +0 -4
  48. package/.env.production +0 -1
  49. package/.vscode/extensions.json +0 -7
  50. package/.vscode/launch.json +0 -15
  51. package/.vscode/settings.json +0 -25
  52. package/Dockerfile +0 -7
  53. package/dist/5fa71a03d45dc2e3d61447f3013a303d.png +0 -0
  54. package/dist/index.html +0 -26
  55. package/dist/main.js +0 -23303
  56. package/dist/vendor.js +0 -281
  57. package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
  58. package/src/app/pages/ListNamespacesPage/index.jsx +0 -52
@@ -1,32 +1,66 @@
1
1
  import * as React from 'react';
2
2
  import { useParams } from 'react-router-dom';
3
- import { useEffect, useState } from 'react';
4
- import { DataJunctionAPI } from '../../services/DJService';
5
- import NamespaceHeader from '../../components/NamespaceHeader';
3
+ import { useContext, useEffect, useState } from 'react';
6
4
  import NodeStatus from '../NodePage/NodeStatus';
7
-
8
- export async function loader({ params }) {
9
- const djNode = await DataJunctionAPI.node(params.name);
10
- if (djNode.type === 'metric') {
11
- const metricNode = await DataJunctionAPI.metric(params.name);
12
- djNode.dimensions = metricNode.dimensions;
13
- }
14
- return djNode;
15
- }
5
+ import DJClientContext from '../../providers/djclient';
6
+ import Explorer from '../NamespacePage/Explorer';
16
7
 
17
8
  export function NamespacePage() {
18
- const { namespace } = useParams();
9
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
10
+ var { namespace } = useParams();
19
11
 
20
12
  const [state, setState] = useState({
21
13
  namespace: namespace,
22
14
  nodes: [],
23
15
  });
24
16
 
17
+ const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
18
+
19
+ const createNamespaceHierarchy = namespaceList => {
20
+ const hierarchy = [];
21
+
22
+ for (const item of namespaceList) {
23
+ const namespaces = item.namespace.split('.');
24
+ let currentLevel = hierarchy;
25
+
26
+ let path = '';
27
+ for (const ns of namespaces) {
28
+ path += ns;
29
+
30
+ let existingNamespace = currentLevel.find(el => el.namespace === ns);
31
+ if (!existingNamespace) {
32
+ existingNamespace = {
33
+ namespace: ns,
34
+ children: [],
35
+ path: path,
36
+ };
37
+ currentLevel.push(existingNamespace);
38
+ }
39
+
40
+ currentLevel = existingNamespace.children;
41
+ path += '.';
42
+ }
43
+ }
44
+ return hierarchy;
45
+ };
46
+
47
+ useEffect(() => {
48
+ const fetchData = async () => {
49
+ const namespaces = await djClient.namespaces();
50
+ const hierarchy = createNamespaceHierarchy(namespaces);
51
+ setNamespaceHierarchy(hierarchy);
52
+ };
53
+ fetchData().catch(console.error);
54
+ }, [djClient, djClient.namespaces]);
55
+
25
56
  useEffect(() => {
26
57
  const fetchData = async () => {
27
- const djNodes = await DataJunctionAPI.namespace(namespace);
58
+ if (namespace === undefined && namespaceHierarchy !== undefined) {
59
+ namespace = namespaceHierarchy.children[0].path;
60
+ }
61
+ const djNodes = await djClient.namespace(namespace);
28
62
  const nodes = djNodes.map(node => {
29
- return DataJunctionAPI.node(node);
63
+ return djClient.node(node);
30
64
  });
31
65
  const foundNodes = await Promise.all(nodes);
32
66
  setState({
@@ -35,13 +69,10 @@ export function NamespacePage() {
35
69
  });
36
70
  };
37
71
  fetchData().catch(console.error);
38
- }, [namespace]);
72
+ }, [djClient, namespace, namespaceHierarchy]);
39
73
 
40
74
  const nodesList = state.nodes.map(node => (
41
75
  <tr>
42
- <td>
43
- <a href={'/namespaces/' + node.namespace}>{node.namespace}</a>
44
- </td>
45
76
  <td>
46
77
  <a href={'/nodes/' + node.name} className="link-table">
47
78
  {node.display_name}
@@ -64,26 +95,57 @@ export function NamespacePage() {
64
95
  <td>
65
96
  <span className="status">{node.mode}</span>
66
97
  </td>
98
+ <td>
99
+ <span className="status">{node.tags}</span>
100
+ </td>
101
+ <td>
102
+ <span className="status">
103
+ {new Date(node.updated_at).toLocaleString('en-us')}
104
+ </span>
105
+ </td>
67
106
  </tr>
68
107
  ));
69
108
 
70
- // @ts-ignore
71
109
  return (
72
110
  <div className="mid">
73
- <NamespaceHeader namespace={namespace} />
74
111
  <div className="card">
75
112
  <div className="card-header">
76
- <h2>Nodes</h2>
113
+ <h2>Explore</h2>
77
114
  <div className="table-responsive">
115
+ <div className={`sidebar`}>
116
+ <span
117
+ style={{
118
+ textTransform: 'uppercase',
119
+ fontSize: '0.8125rem',
120
+ fontWeight: '600',
121
+ color: '#95aac9',
122
+ padding: '1rem 1rem 1rem 0',
123
+ }}
124
+ >
125
+ Namespaces
126
+ </span>
127
+ {namespaceHierarchy
128
+ ? namespaceHierarchy.map(child => (
129
+ <Explorer
130
+ item={child}
131
+ current={state.namespace}
132
+ defaultExpand={true}
133
+ />
134
+ ))
135
+ : null}
136
+ </div>
78
137
  <table className="card-table table">
79
138
  <thead>
80
- <th>Namespace</th>
81
- <th>Name</th>
82
- <th>Type</th>
83
- <th>Status</th>
84
- <th>Mode</th>
139
+ <tr>
140
+ <th>Name</th>
141
+ <th>Type</th>
142
+ <th>Status</th>
143
+ <th>Mode</th>
144
+ <th>Tags</th>
145
+ <th>Last Updated</th>
146
+ </tr>
85
147
  </thead>
86
- {nodesList}
148
+ <tbody>{nodesList}</tbody>
87
149
  </table>
88
150
  </div>
89
151
  </div>
@@ -0,0 +1,30 @@
1
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
2
+ import { useState } from 'react';
3
+ import { nightOwl } from 'react-syntax-highlighter/src/styles/hljs';
4
+ import PythonIcon from '../../icons/PythonIcon';
5
+
6
+ export default function ClientCodePopover({ code }) {
7
+ const [codeAnchor, setCodeAnchor] = useState(false);
8
+
9
+ return (
10
+ <>
11
+ <button
12
+ className="code-button"
13
+ tabIndex="0"
14
+ height="45px"
15
+ onClick={() => setCodeAnchor(!codeAnchor)}
16
+ >
17
+ <PythonIcon />
18
+ </button>
19
+ <div
20
+ id={`node-create-code`}
21
+ onClose={() => setCodeAnchor(null)}
22
+ style={{ display: codeAnchor === false ? 'none' : 'block' }}
23
+ >
24
+ <SyntaxHighlighter language="python" style={nightOwl}>
25
+ {code}
26
+ </SyntaxHighlighter>
27
+ </div>
28
+ </>
29
+ );
30
+ }
@@ -5,10 +5,12 @@
5
5
  import * as React from 'react';
6
6
  import { lazyLoad } from '../../../utils/loadable';
7
7
 
8
- export const NodePage = lazyLoad(
9
- () => import('./index'),
10
- module => module.NodePage,
11
- {
12
- fallback: <div></div>,
13
- },
14
- );
8
+ export const NodePage = props => {
9
+ return lazyLoad(
10
+ () => import('./index'),
11
+ module => module.NodePage,
12
+ {
13
+ fallback: <div></div>,
14
+ },
15
+ )(props);
16
+ };
@@ -1,8 +1,17 @@
1
- import { Component } from 'react';
1
+ import { Component, useEffect, useState } from 'react';
2
+ import ClientCodePopover from './ClientCodePopover';
2
3
 
3
- export default class NodeColumnTab extends Component {
4
- columnList = node => {
5
- return node.columns.map(col => (
4
+ export default function NodeColumnTab({ node, djClient }) {
5
+ const [columns, setColumns] = useState([]);
6
+ useEffect(() => {
7
+ const fetchData = async () => {
8
+ setColumns(await djClient.columns(node));
9
+ };
10
+ fetchData().catch(console.error);
11
+ }, [djClient, node]);
12
+
13
+ const columnList = columns => {
14
+ return columns.map(col => (
6
15
  <tr>
7
16
  <td className="text-start">{col.name}</td>
8
17
  <td>
@@ -10,7 +19,16 @@ export default class NodeColumnTab extends Component {
10
19
  {col.type}
11
20
  </span>
12
21
  </td>
13
- <td>{col.dimension ? col.dimension.name : ''}</td>
22
+ <td>
23
+ {col.dimension ? (
24
+ <>
25
+ <a href={`/nodes/${col.dimension.name}`}>{col.dimension.name}</a>
26
+ <ClientCodePopover code={col.clientCode} />
27
+ </>
28
+ ) : (
29
+ ''
30
+ )}{' '}
31
+ </td>
14
32
  <td>
15
33
  {col.attributes.find(
16
34
  attr => attr.attribute_type.name === 'dimension',
@@ -26,19 +44,17 @@ export default class NodeColumnTab extends Component {
26
44
  ));
27
45
  };
28
46
 
29
- render() {
30
- return (
31
- <div className="table-responsive">
32
- <table className="card-inner-table table">
33
- <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
34
- <th className="text-start">Column</th>
35
- <th>Type</th>
36
- <th>Dimension</th>
37
- <th>Attributes</th>
38
- </thead>
39
- {this.columnList(this.props.node)}
40
- </table>
41
- </div>
42
- );
43
- }
47
+ return (
48
+ <div className="table-responsive">
49
+ <table className="card-inner-table table">
50
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
51
+ <th className="text-start">Column</th>
52
+ <th>Type</th>
53
+ <th>Dimension</th>
54
+ <th>Attributes</th>
55
+ </thead>
56
+ {columnList(columns)}
57
+ </table>
58
+ </div>
59
+ );
44
60
  }
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useMemo } from 'react';
1
+ import React, { useCallback, useContext, useEffect, useMemo } from 'react';
2
2
  import ReactFlow, {
3
3
  addEdge,
4
4
  MiniMap,
@@ -12,10 +12,11 @@ import ReactFlow, {
12
12
  import '../../../styles/dag.css';
13
13
  import 'reactflow/dist/style.css';
14
14
  import DJNode from '../../components/djgraph/DJNode';
15
- import { DataJunctionAPI } from '../../services/DJService';
16
15
  import dagre from 'dagre';
16
+ import DJClientContext from '../../providers/djclient';
17
17
 
18
18
  const NodeLineage = djNode => {
19
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
19
20
  const nodeTypes = useMemo(() => ({ DJNode: DJNode }), []);
20
21
 
21
22
  const [nodes, setNodes, onNodesChange] = useNodesState([]);
@@ -25,51 +26,60 @@ const NodeLineage = djNode => {
25
26
  height: 100,
26
27
  width: 150,
27
28
  };
28
- const dagreGraph = new dagre.graphlib.Graph();
29
- dagreGraph.setDefaultEdgeLabel(() => ({}));
30
29
 
31
- const setElementsLayout = (
32
- nodes,
33
- edges,
34
- direction = 'LR',
35
- nodeWidth = 800,
36
- nodeHeight = 150,
37
- ) => {
38
- const isHorizontal = direction === 'TB';
39
- dagreGraph.setGraph({ rankdir: direction });
40
-
41
- nodes.forEach(node => {
42
- dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
43
- });
44
-
45
- edges.forEach(edge => {
46
- dagreGraph.setEdge(edge.source, edge.target);
47
- });
48
-
49
- dagre.layout(dagreGraph);
50
-
51
- nodes.forEach(node => {
52
- const nodeWithPosition = dagreGraph.node(node.id);
53
- node.targetPosition = isHorizontal ? 'left' : 'top';
54
- node.sourcePosition = isHorizontal ? 'right' : 'bottom';
55
- node.position = {
56
- x: nodeWithPosition.x - nodeWidth / 2,
57
- y: nodeWithPosition.y - nodeHeight / 2,
58
- };
59
- return node;
60
- });
61
-
62
- return { nodes, edges };
63
- };
30
+ const dagreGraph = useMemo(() => new dagre.graphlib.Graph(), []);
31
+ dagreGraph.setDefaultEdgeLabel(() => ({}));
64
32
 
65
33
  useEffect(() => {
34
+ const setElementsLayout = (
35
+ nodes,
36
+ edges,
37
+ direction = 'LR',
38
+ nodeWidth = 800,
39
+ ) => {
40
+ const isHorizontal = direction === 'TB';
41
+ dagreGraph.setGraph({ rankdir: direction });
42
+ const nodeHeightTracker = {};
43
+
44
+ nodes.forEach(node => {
45
+ nodeHeightTracker[node.id] =
46
+ Math.min(node.data.column_names.length, 10) * 37 + 250;
47
+ dagreGraph.setNode(node.id, {
48
+ width: nodeWidth,
49
+ height: nodeHeightTracker[node.id],
50
+ });
51
+ });
52
+
53
+ edges.forEach(edge => {
54
+ dagreGraph.setEdge(edge.source, edge.target);
55
+ });
56
+
57
+ dagre.layout(dagreGraph);
58
+
59
+ nodes.forEach(node => {
60
+ const nodeWithPosition = dagreGraph.node(node.id);
61
+ node.targetPosition = isHorizontal ? 'left' : 'top';
62
+ node.sourcePosition = isHorizontal ? 'right' : 'bottom';
63
+ node.position = {
64
+ x: nodeWithPosition.x - nodeWidth / 2,
65
+ y: nodeWithPosition.y - nodeHeightTracker[node.id] / 2,
66
+ };
67
+ node.width = nodeWidth;
68
+ node.height = nodeHeightTracker[node.id];
69
+ return node;
70
+ });
71
+ return { nodes, edges };
72
+ };
73
+
66
74
  const dagFetch = async () => {
67
- let upstreams = await DataJunctionAPI.upstreams(djNode.djNode.name);
68
- let downstreams = await DataJunctionAPI.downstreams(djNode.djNode.name);
75
+ let related_nodes = await djClient.node_dag(djNode.djNode.name);
76
+ // djNode.djNode.is_current = true;
69
77
  var djNodes = [djNode.djNode];
70
- for (const iterable of [upstreams, downstreams]) {
78
+ for (const iterable of [related_nodes]) {
71
79
  for (const item of iterable) {
72
- djNodes.push(item);
80
+ if (item.type !== 'cube') {
81
+ djNodes.push(item);
82
+ }
73
83
  }
74
84
  }
75
85
  let edges = [];
@@ -78,24 +88,43 @@ const NodeLineage = djNode => {
78
88
  if (parent.name) {
79
89
  edges.push({
80
90
  id: obj.name + '-' + parent.name,
81
- target: obj.name,
82
91
  source: parent.name,
92
+ sourceHandle: parent.name,
93
+ target: obj.name,
94
+ targetHandle: obj.name,
83
95
  animated: true,
84
96
  markerEnd: {
85
97
  type: MarkerType.Arrow,
86
98
  },
99
+ style: {
100
+ strokeWidth: 3,
101
+ stroke: '#b0b9c2',
102
+ },
87
103
  });
88
104
  }
89
105
  });
90
106
 
91
107
  obj.columns.forEach(col => {
92
108
  if (col.dimension) {
93
- edges.push({
94
- id: obj.name + '-' + col.dimension.name,
95
- target: obj.name,
109
+ const edge = {
110
+ id: col.dimension.name + '->' + obj.name + '.' + col.name,
96
111
  source: col.dimension.name,
112
+ sourceHandle: col.dimension.name,
113
+ target: obj.name,
114
+ targetHandle: obj.name + '.' + col.name,
97
115
  draggable: true,
98
- });
116
+ markerStart: {
117
+ type: MarkerType.Arrow,
118
+ width: 20,
119
+ height: 20,
120
+ color: '#b0b9c2',
121
+ },
122
+ style: {
123
+ strokeWidth: 3,
124
+ stroke: '#b0b9c2',
125
+ },
126
+ };
127
+ edges.push(edge);
99
128
  }
100
129
  });
101
130
  });
@@ -117,35 +146,35 @@ const NodeLineage = djNode => {
117
146
  label:
118
147
  node.table !== null
119
148
  ? String(node.schema_ + '.' + node.table)
120
- : String(node.name),
149
+ : 'default.' + node.name,
121
150
  table: node.table,
122
151
  name: String(node.name),
123
152
  display_name: String(node.display_name),
124
153
  type: node.type,
125
154
  primary_key: primary_key,
126
155
  column_names: column_names,
127
- // dimensions: dimensions,
156
+ is_current: node.name === djNode.djNode.name,
128
157
  },
129
- // parentNode: [node.name.split(".").slice(-2, -1)],
130
- // extent: 'parent',
131
158
  };
132
159
  });
133
- console.log(djNodes);
134
160
  setNodes(nodes);
135
161
  setEdges(edges);
162
+
163
+ // use dagre to determine the position of the parents (the DJ nodes)
164
+ // the positions of the columns are relative to each DJ node
136
165
  setElementsLayout(nodes, edges);
137
166
  };
138
167
 
139
168
  dagFetch();
140
- }, []);
169
+ }, [dagreGraph, djClient, djNode.djNode, setEdges, setNodes]);
141
170
 
142
171
  const onConnect = useCallback(
143
172
  params => setEdges(eds => addEdge(params, eds)),
144
- [],
173
+ [setEdges],
145
174
  );
146
175
 
147
176
  return (
148
- <div style={{ height: '600px' }}>
177
+ <div style={{ height: '800px' }}>
149
178
  <ReactFlow
150
179
  nodes={nodes}
151
180
  edges={edges}
@@ -0,0 +1,71 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export default function NodeHistory({ node, djClient }) {
4
+ const [history, setHistory] = useState([]);
5
+ const [revisions, setRevisions] = useState([]);
6
+
7
+ useEffect(() => {
8
+ const fetchData = async () => {
9
+ const data = await djClient.history('node', node.name);
10
+ setHistory(data);
11
+ const revisions = await djClient.revisions(node.name);
12
+ setRevisions(revisions);
13
+ };
14
+ fetchData().catch(console.error);
15
+ }, [djClient, node]);
16
+ const tableData = history => {
17
+ return history.map(event => (
18
+ <tr>
19
+ <td className="text-start">
20
+ <span
21
+ className={`history_type__${event.activity_type} badge node_type`}
22
+ >
23
+ {event.activity_type}
24
+ </span>
25
+ </td>
26
+ <td>{event.entity_type}</td>
27
+ <td>{event.entity_name}</td>
28
+ <td>{event.user ? event.user : 'unknown'}</td>
29
+ <td>{event.created_at}</td>
30
+ </tr>
31
+ ));
32
+ };
33
+
34
+ const revisionsTable = revisions => {
35
+ return revisions.map(revision => (
36
+ <tr>
37
+ <td className="text-start">
38
+ <span className={`badge node_type__source`}>{revision.version}</span>
39
+ </td>
40
+ <td>{revision.display_name}</td>
41
+ <td>{revision.description}</td>
42
+ <td>{revision.query}</td>
43
+ <td>{revision.tags}</td>
44
+ </tr>
45
+ ));
46
+ };
47
+ return (
48
+ <div className="table-responsive">
49
+ <table className="card-inner-table table">
50
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
51
+ <th className="text-start">Version</th>
52
+ <th>Display Name</th>
53
+ <th>Description</th>
54
+ <th>Query</th>
55
+ <th>Tags</th>
56
+ </thead>
57
+ {revisionsTable(revisions)}
58
+ </table>
59
+ <table className="card-inner-table table">
60
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
61
+ <th className="text-start">Activity</th>
62
+ <th>Type</th>
63
+ <th>Name</th>
64
+ <th>User</th>
65
+ <th>Timestamp</th>
66
+ </thead>
67
+ {tableData(history)}
68
+ </table>
69
+ </div>
70
+ );
71
+ }