datajunction-ui 0.0.1-rc.0
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/.env.local +4 -0
- package/.env.production +1 -0
- package/.eslintrc.js +20 -0
- package/.gitattributes +201 -0
- package/.github/pull_request_template.md +11 -0
- package/.github/workflows/ci.yml +33 -0
- package/.husky/pre-commit +6 -0
- package/.nvmrc +1 -0
- package/.prettierignore +4 -0
- package/.prettierrc +9 -0
- package/.stylelintrc +7 -0
- package/.vscode/extensions.json +8 -0
- package/.vscode/launch.json +15 -0
- package/.vscode/settings.json +25 -0
- package/Dockerfile +6 -0
- package/LICENSE +22 -0
- package/README.md +10 -0
- package/internals/testing/loadable.mock.tsx +6 -0
- package/package.json +150 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +29 -0
- package/public/manifest.json +15 -0
- package/public/robots.txt +3 -0
- package/src/app/__tests__/__snapshots__/index.test.tsx.snap +45 -0
- package/src/app/__tests__/index.test.tsx +14 -0
- package/src/app/components/ListGroupItem.jsx +17 -0
- package/src/app/components/NamespaceHeader.jsx +40 -0
- package/src/app/components/Tab.jsx +26 -0
- package/src/app/components/__tests__/ListGroupItem.test.tsx +16 -0
- package/src/app/components/__tests__/NamespaceHeader.test.jsx +14 -0
- package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +26 -0
- package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +63 -0
- package/src/app/components/djgraph/DJNode.jsx +111 -0
- package/src/app/components/djgraph/__tests__/DJNode.test.tsx +24 -0
- package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +73 -0
- package/src/app/index.tsx +53 -0
- package/src/app/pages/ListNamespacesPage/Loadable.jsx +23 -0
- package/src/app/pages/ListNamespacesPage/index.jsx +53 -0
- package/src/app/pages/NamespacePage/Loadable.jsx +23 -0
- package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +45 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.tsx +14 -0
- package/src/app/pages/NamespacePage/index.jsx +93 -0
- package/src/app/pages/NodePage/Loadable.jsx +23 -0
- package/src/app/pages/NodePage/NodeColumnTab.jsx +44 -0
- package/src/app/pages/NodePage/NodeGraphTab.jsx +160 -0
- package/src/app/pages/NodePage/NodeInfoTab.jsx +87 -0
- package/src/app/pages/NodePage/NodeStatus.jsx +34 -0
- package/src/app/pages/NodePage/index.jsx +100 -0
- package/src/app/pages/NotFoundPage/Loadable.tsx +14 -0
- package/src/app/pages/NotFoundPage/P.ts +8 -0
- package/src/app/pages/NotFoundPage/__tests__/__snapshots__/index.test.tsx.snap +61 -0
- package/src/app/pages/NotFoundPage/__tests__/index.test.tsx +21 -0
- package/src/app/pages/NotFoundPage/index.tsx +45 -0
- package/src/app/pages/Root/Loadable.tsx +23 -0
- package/src/app/pages/Root/assets/dj-logo.png +0 -0
- package/src/app/pages/Root/index.tsx +42 -0
- package/src/app/services/DJService.js +124 -0
- package/src/index.tsx +47 -0
- package/src/react-app-env.d.ts +4 -0
- package/src/reportWebVitals.ts +15 -0
- package/src/setupTests.ts +8 -0
- package/src/styles/dag-styles.ts +117 -0
- package/src/styles/global-styles.ts +588 -0
- package/src/styles/index.css +546 -0
- package/src/utils/__tests__/__snapshots__/loadable.test.tsx.snap +17 -0
- package/src/utils/__tests__/loadable.test.tsx +53 -0
- package/src/utils/__tests__/request.test.ts +82 -0
- package/src/utils/loadable.tsx +30 -0
- package/src/utils/request.ts +54 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
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';
|
|
6
|
+
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
|
+
}
|
|
16
|
+
|
|
17
|
+
export function NamespacePage() {
|
|
18
|
+
const { namespace } = useParams();
|
|
19
|
+
|
|
20
|
+
const [state, setState] = useState({
|
|
21
|
+
namespace: namespace,
|
|
22
|
+
nodes: [],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const fetchData = async () => {
|
|
27
|
+
const djNodes = await DataJunctionAPI.namespace(namespace);
|
|
28
|
+
const nodes = djNodes.map(node => {
|
|
29
|
+
return DataJunctionAPI.node(node);
|
|
30
|
+
});
|
|
31
|
+
const foundNodes = await Promise.all(nodes);
|
|
32
|
+
setState({
|
|
33
|
+
namespace: namespace,
|
|
34
|
+
nodes: foundNodes,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
fetchData().catch(console.error);
|
|
38
|
+
}, [namespace]);
|
|
39
|
+
|
|
40
|
+
const nodesList = state.nodes.map(node => (
|
|
41
|
+
<tr>
|
|
42
|
+
<td>
|
|
43
|
+
<a href={'/namespaces/' + node.namespace}>{node.namespace}</a>
|
|
44
|
+
</td>
|
|
45
|
+
<td>
|
|
46
|
+
<a href={'/nodes/' + node.name} className="link-table">
|
|
47
|
+
{node.display_name}
|
|
48
|
+
</a>
|
|
49
|
+
<span
|
|
50
|
+
className="rounded-pill badge bg-secondary-soft"
|
|
51
|
+
style={{ marginLeft: '0.5rem' }}
|
|
52
|
+
>
|
|
53
|
+
{node.version}
|
|
54
|
+
</span>
|
|
55
|
+
</td>
|
|
56
|
+
<td>
|
|
57
|
+
<span className={'node_type__' + node.type + ' badge node_type'}>
|
|
58
|
+
{node.type}
|
|
59
|
+
</span>
|
|
60
|
+
</td>
|
|
61
|
+
<td>
|
|
62
|
+
<NodeStatus node={node} />
|
|
63
|
+
</td>
|
|
64
|
+
<td>
|
|
65
|
+
<span className="status">{node.mode}</span>
|
|
66
|
+
</td>
|
|
67
|
+
</tr>
|
|
68
|
+
));
|
|
69
|
+
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
return (
|
|
72
|
+
<div className="mid">
|
|
73
|
+
<NamespaceHeader namespace={namespace} />
|
|
74
|
+
<div className="card">
|
|
75
|
+
<div className="card-header">
|
|
76
|
+
<h2>Nodes</h2>
|
|
77
|
+
<div className="table-responsive">
|
|
78
|
+
<table className="card-table table">
|
|
79
|
+
<thead>
|
|
80
|
+
<th>Namespace</th>
|
|
81
|
+
<th>Name</th>
|
|
82
|
+
<th>Type</th>
|
|
83
|
+
<th>Status</th>
|
|
84
|
+
<th>Mode</th>
|
|
85
|
+
</thead>
|
|
86
|
+
{nodesList}
|
|
87
|
+
</table>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronously loads the component for the Node page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { lazyLoad } from 'utils/loadable';
|
|
7
|
+
import styled from 'styled-components/macro';
|
|
8
|
+
|
|
9
|
+
const LoadingWrapper = styled.div`
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100vh;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export const NodePage = lazyLoad(
|
|
18
|
+
() => import('./index'),
|
|
19
|
+
module => module.NodePage,
|
|
20
|
+
{
|
|
21
|
+
fallback: <LoadingWrapper></LoadingWrapper>,
|
|
22
|
+
},
|
|
23
|
+
);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
|
|
3
|
+
export default class NodeColumnTab extends Component {
|
|
4
|
+
columnList = node => {
|
|
5
|
+
return node.columns.map(col => (
|
|
6
|
+
<tr>
|
|
7
|
+
<td className="text-start">{col.name}</td>
|
|
8
|
+
<td>
|
|
9
|
+
<span className="node_type__transform badge node_type">
|
|
10
|
+
{col.type}
|
|
11
|
+
</span>
|
|
12
|
+
</td>
|
|
13
|
+
<td>{col.dimension ? col.dimension.name : ''}</td>
|
|
14
|
+
<td>
|
|
15
|
+
{col.attributes.find(
|
|
16
|
+
attr => attr.attribute_type.name === 'dimension',
|
|
17
|
+
) ? (
|
|
18
|
+
<span className="node_type__dimension badge node_type">
|
|
19
|
+
dimensional
|
|
20
|
+
</span>
|
|
21
|
+
) : (
|
|
22
|
+
''
|
|
23
|
+
)}
|
|
24
|
+
</td>
|
|
25
|
+
</tr>
|
|
26
|
+
));
|
|
27
|
+
};
|
|
28
|
+
|
|
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
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo } from 'react';
|
|
2
|
+
import ReactFlow, {
|
|
3
|
+
addEdge,
|
|
4
|
+
MiniMap,
|
|
5
|
+
Controls,
|
|
6
|
+
Background,
|
|
7
|
+
useNodesState,
|
|
8
|
+
useEdgesState,
|
|
9
|
+
MarkerType,
|
|
10
|
+
} from 'reactflow';
|
|
11
|
+
import 'reactflow/dist/style.css';
|
|
12
|
+
import DJNode from '../../components/djgraph/DJNode';
|
|
13
|
+
import { DataJunctionAPI } from '../../services/DJService';
|
|
14
|
+
import dagre from 'dagre';
|
|
15
|
+
|
|
16
|
+
const NodeLineage = djNode => {
|
|
17
|
+
const nodeTypes = useMemo(() => ({ DJNode: DJNode }), []);
|
|
18
|
+
|
|
19
|
+
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
20
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
21
|
+
|
|
22
|
+
const minimapStyle = {
|
|
23
|
+
height: 100,
|
|
24
|
+
width: 150,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const dagreGraph = useMemo(() => new dagre.graphlib.Graph(), []);
|
|
28
|
+
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
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
|
+
};
|
|
64
|
+
|
|
65
|
+
const dagFetch = async () => {
|
|
66
|
+
let upstreams = await DataJunctionAPI.upstreams(djNode.djNode.name);
|
|
67
|
+
let downstreams = await DataJunctionAPI.downstreams(djNode.djNode.name);
|
|
68
|
+
let djNodes = [...new Set([...upstreams, ...downstreams, djNode.djNode])];
|
|
69
|
+
let edges = [];
|
|
70
|
+
djNodes.forEach(obj => {
|
|
71
|
+
obj.parents.forEach(parent => {
|
|
72
|
+
if (parent.name) {
|
|
73
|
+
edges.push({
|
|
74
|
+
id: obj.name + '-' + parent.name,
|
|
75
|
+
target: obj.name,
|
|
76
|
+
source: parent.name,
|
|
77
|
+
animated: true,
|
|
78
|
+
markerEnd: {
|
|
79
|
+
type: MarkerType.Arrow,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
obj.columns.forEach(col => {
|
|
86
|
+
if (col.dimension) {
|
|
87
|
+
edges.push({
|
|
88
|
+
id: obj.name + '-' + col.dimension.name,
|
|
89
|
+
target: obj.name,
|
|
90
|
+
source: col.dimension.name,
|
|
91
|
+
draggable: true,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
const nodes = djNodes.map(node => {
|
|
97
|
+
const primary_key = node.columns
|
|
98
|
+
.filter(col =>
|
|
99
|
+
col.attributes.some(
|
|
100
|
+
attr => attr.attribute_type.name === 'primary_key',
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
.map(col => col.name);
|
|
104
|
+
const column_names = node.columns.map(col => {
|
|
105
|
+
return { name: col.name, type: col.type };
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
id: String(node.name),
|
|
109
|
+
type: 'DJNode',
|
|
110
|
+
data: {
|
|
111
|
+
label:
|
|
112
|
+
node.table !== null
|
|
113
|
+
? String(node.schema_ + '.' + node.table)
|
|
114
|
+
: String(node.name),
|
|
115
|
+
table: node.table,
|
|
116
|
+
name: String(node.name),
|
|
117
|
+
display_name: String(node.display_name),
|
|
118
|
+
type: node.type,
|
|
119
|
+
primary_key: primary_key,
|
|
120
|
+
column_names: column_names,
|
|
121
|
+
// dimensions: dimensions,
|
|
122
|
+
},
|
|
123
|
+
// parentNode: [node.name.split(".").slice(-2, -1)],
|
|
124
|
+
// extent: 'parent',
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
setNodes(nodes);
|
|
129
|
+
setEdges(edges);
|
|
130
|
+
setElementsLayout(nodes, edges);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
dagFetch();
|
|
134
|
+
}, [dagreGraph, djNode.djNode, setEdges, setNodes]);
|
|
135
|
+
|
|
136
|
+
const onConnect = useCallback(
|
|
137
|
+
params => setEdges(eds => addEdge(params, eds)),
|
|
138
|
+
[setEdges],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div style={{ height: '600px' }}>
|
|
143
|
+
<ReactFlow
|
|
144
|
+
nodes={nodes}
|
|
145
|
+
edges={edges}
|
|
146
|
+
nodeTypes={nodeTypes}
|
|
147
|
+
onNodesChange={onNodesChange}
|
|
148
|
+
onEdgesChange={onEdgesChange}
|
|
149
|
+
onConnect={onConnect}
|
|
150
|
+
snapToGrid={true}
|
|
151
|
+
fitView
|
|
152
|
+
>
|
|
153
|
+
<MiniMap style={minimapStyle} zoomable pannable />
|
|
154
|
+
<Controls />
|
|
155
|
+
<Background color="#aaa" gap={16} />
|
|
156
|
+
</ReactFlow>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
export default NodeLineage;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
3
|
+
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
4
|
+
import sql from 'react-syntax-highlighter/dist/esm/languages/hljs/sql';
|
|
5
|
+
import { format } from 'sql-formatter';
|
|
6
|
+
|
|
7
|
+
import NodeStatus from './NodeStatus';
|
|
8
|
+
import ListGroupItem from '../../components/ListGroupItem';
|
|
9
|
+
|
|
10
|
+
SyntaxHighlighter.registerLanguage('sql', sql);
|
|
11
|
+
foundation.hljs['padding'] = '2rem';
|
|
12
|
+
|
|
13
|
+
export default class NodeInfoTab extends Component {
|
|
14
|
+
nodeTags = this.props.node?.tags.map(tag => <div>{tag}</div>);
|
|
15
|
+
queryDiv = this.props.node?.query ? (
|
|
16
|
+
<div className="list-group-item d-flex">
|
|
17
|
+
<div className="d-flex gap-2 w-100 justify-content-between py-3">
|
|
18
|
+
<div
|
|
19
|
+
style={{
|
|
20
|
+
width: window.innerWidth * 0.8,
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
<h6 className="mb-0 w-100">Query</h6>
|
|
24
|
+
<SyntaxHighlighter language="sql" style={foundation}>
|
|
25
|
+
{format(this.props.node?.query, {
|
|
26
|
+
language: 'spark',
|
|
27
|
+
tabWidth: 2,
|
|
28
|
+
keywordCase: 'upper',
|
|
29
|
+
denseOperators: true,
|
|
30
|
+
logicalOperatorNewline: 'before',
|
|
31
|
+
expressionWidth: 10,
|
|
32
|
+
})}
|
|
33
|
+
</SyntaxHighlighter>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
) : (
|
|
38
|
+
<></>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
render() {
|
|
42
|
+
return (
|
|
43
|
+
<div className="list-group align-items-center justify-content-between flex-md-row gap-2">
|
|
44
|
+
<ListGroupItem
|
|
45
|
+
label="Description"
|
|
46
|
+
value={this.props.node?.description}
|
|
47
|
+
/>
|
|
48
|
+
<div className="list-group-item d-flex">
|
|
49
|
+
<div className="d-flex gap-2 w-100 justify-content-between py-3">
|
|
50
|
+
<div>
|
|
51
|
+
<h6 className="mb-0 w-100">Version</h6>
|
|
52
|
+
|
|
53
|
+
<p className="mb-0 opacity-75">
|
|
54
|
+
<span
|
|
55
|
+
className="rounded-pill badge bg-secondary-soft"
|
|
56
|
+
style={{ marginLeft: '0.5rem', fontSize: '100%' }}
|
|
57
|
+
>
|
|
58
|
+
{this.props.node?.version}
|
|
59
|
+
</span>
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div>
|
|
63
|
+
<h6 className="mb-0 w-100">Status</h6>
|
|
64
|
+
<p className="mb-0 opacity-75">
|
|
65
|
+
<NodeStatus node={this.props.node} />
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<h6 className="mb-0 w-100">Mode</h6>
|
|
70
|
+
<p className="mb-0 opacity-75">
|
|
71
|
+
<span className="status">{this.props.node?.mode}</span>
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
<div>
|
|
75
|
+
<h6 className="mb-0 w-100">Tags</h6>
|
|
76
|
+
<p className="mb-0 opacity-75">{this.nodeTags}</p>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
{this.queryDiv}
|
|
81
|
+
<div className="list-group-item d-flex">
|
|
82
|
+
{this.props.node?.primary_key}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
|
|
3
|
+
export default class NodeStatus extends Component {
|
|
4
|
+
render() {
|
|
5
|
+
const { node } = this.props;
|
|
6
|
+
return (
|
|
7
|
+
<span className="status__valid status" style={{ alignContent: 'center' }}>
|
|
8
|
+
{node?.status === 'valid' ? (
|
|
9
|
+
<svg
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
width="25"
|
|
12
|
+
height="25"
|
|
13
|
+
fill="currentColor"
|
|
14
|
+
className="bi bi-check-circle-fill"
|
|
15
|
+
viewBox="0 0 16 16"
|
|
16
|
+
>
|
|
17
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
|
|
18
|
+
</svg>
|
|
19
|
+
) : (
|
|
20
|
+
<svg
|
|
21
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
22
|
+
width="16"
|
|
23
|
+
height="16"
|
|
24
|
+
fill="currentColor"
|
|
25
|
+
className="bi bi-x-circle-fill"
|
|
26
|
+
viewBox="0 0 16 16"
|
|
27
|
+
>
|
|
28
|
+
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
|
|
29
|
+
</svg>
|
|
30
|
+
)}
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useParams } from 'react-router-dom';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { DataJunctionAPI } from '../../services/DJService';
|
|
5
|
+
import Tab from '../../components/Tab';
|
|
6
|
+
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
7
|
+
import NodeInfoTab from './NodeInfoTab';
|
|
8
|
+
import NodeColumnTab from './NodeColumnTab';
|
|
9
|
+
import NodeLineage from './NodeGraphTab';
|
|
10
|
+
|
|
11
|
+
export function NodePage() {
|
|
12
|
+
const [state, setState] = useState({
|
|
13
|
+
selectedTab: 0,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const [node, setNode] = useState();
|
|
17
|
+
|
|
18
|
+
const onClickTab = id => () => {
|
|
19
|
+
setState({ selectedTab: id });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const buildTabs = tab => {
|
|
23
|
+
return (
|
|
24
|
+
<Tab
|
|
25
|
+
key={tab.id}
|
|
26
|
+
id={tab.id}
|
|
27
|
+
name={tab.name}
|
|
28
|
+
onClick={onClickTab(tab.id)}
|
|
29
|
+
selectedTab={state.selectedTab}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const { name } = useParams();
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const fetchData = async () => {
|
|
38
|
+
const data = await DataJunctionAPI.node(name);
|
|
39
|
+
setNode(data);
|
|
40
|
+
};
|
|
41
|
+
fetchData().catch(console.error);
|
|
42
|
+
}, [name]);
|
|
43
|
+
|
|
44
|
+
const TabsJson = [
|
|
45
|
+
{
|
|
46
|
+
id: 0,
|
|
47
|
+
name: 'Info',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 1,
|
|
51
|
+
name: 'Columns',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 2,
|
|
55
|
+
name: 'Graph',
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
//
|
|
59
|
+
//
|
|
60
|
+
let tabToDisplay = null;
|
|
61
|
+
switch (state.selectedTab) {
|
|
62
|
+
case 0:
|
|
63
|
+
tabToDisplay = node ? <NodeInfoTab node={node} /> : '';
|
|
64
|
+
break;
|
|
65
|
+
case 1:
|
|
66
|
+
tabToDisplay = <NodeColumnTab node={node} />;
|
|
67
|
+
break;
|
|
68
|
+
case 2:
|
|
69
|
+
tabToDisplay = <NodeLineage djNode={node} />;
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
tabToDisplay = <NodeInfoTab node={node} />;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
return (
|
|
77
|
+
<div className="node__header">
|
|
78
|
+
<NamespaceHeader namespace={name.split('.').slice(0, -1).join('.')} />
|
|
79
|
+
<div className="card">
|
|
80
|
+
<div className="card-header">
|
|
81
|
+
<h3 className="card-title align-items-start flex-column">
|
|
82
|
+
<span className="card-label fw-bold text-gray-800">
|
|
83
|
+
{node?.display_name}
|
|
84
|
+
</span>
|
|
85
|
+
</h3>
|
|
86
|
+
<span
|
|
87
|
+
className="fs-6 fw-semibold text-gray-400"
|
|
88
|
+
style={{ marginTop: '-4rem' }}
|
|
89
|
+
>
|
|
90
|
+
Updated {new Date(node?.updated_at).toDateString()}
|
|
91
|
+
</span>
|
|
92
|
+
<div className="align-items-center row">
|
|
93
|
+
{TabsJson.map(buildTabs)}
|
|
94
|
+
</div>
|
|
95
|
+
{tabToDisplay}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronously loads the component for NotFoundPage
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { lazyLoad } from 'utils/loadable';
|
|
7
|
+
|
|
8
|
+
export const NotFoundPage = lazyLoad(
|
|
9
|
+
() => import('./index'),
|
|
10
|
+
module => module.NotFoundPage,
|
|
11
|
+
{
|
|
12
|
+
fallback: <></>,
|
|
13
|
+
},
|
|
14
|
+
);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`<NotFoundPage /> should match snapshot 1`] = `
|
|
4
|
+
.c2 {
|
|
5
|
+
font-size: 1rem;
|
|
6
|
+
line-height: 1.5;
|
|
7
|
+
margin: 0.625rem 0 1.5rem 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.c0 {
|
|
11
|
+
height: 100%;
|
|
12
|
+
display: -webkit-box;
|
|
13
|
+
display: -webkit-flex;
|
|
14
|
+
display: -ms-flexbox;
|
|
15
|
+
display: flex;
|
|
16
|
+
-webkit-align-items: center;
|
|
17
|
+
-webkit-box-align: center;
|
|
18
|
+
-ms-flex-align: center;
|
|
19
|
+
align-items: center;
|
|
20
|
+
-webkit-box-pack: center;
|
|
21
|
+
-webkit-justify-content: center;
|
|
22
|
+
-ms-flex-pack: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
-webkit-flex-direction: column;
|
|
25
|
+
-ms-flex-direction: column;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
min-height: 320px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.c1 {
|
|
31
|
+
margin-top: -8vh;
|
|
32
|
+
font-weight: bold;
|
|
33
|
+
font-size: 3.375rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.c1 span {
|
|
37
|
+
font-size: 3.125rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
<div
|
|
41
|
+
className="c0"
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
className="c1"
|
|
45
|
+
>
|
|
46
|
+
4
|
|
47
|
+
<span
|
|
48
|
+
aria-label="Crying Face"
|
|
49
|
+
role="img"
|
|
50
|
+
>
|
|
51
|
+
😢
|
|
52
|
+
</span>
|
|
53
|
+
4
|
|
54
|
+
</div>
|
|
55
|
+
<p
|
|
56
|
+
className="c2"
|
|
57
|
+
>
|
|
58
|
+
Page not found.
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { NotFoundPage } from '..';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import { HelmetProvider } from 'react-helmet-async';
|
|
5
|
+
import renderer from 'react-test-renderer';
|
|
6
|
+
|
|
7
|
+
const renderPage = () =>
|
|
8
|
+
renderer.create(
|
|
9
|
+
<MemoryRouter>
|
|
10
|
+
<HelmetProvider>
|
|
11
|
+
<NotFoundPage />
|
|
12
|
+
</HelmetProvider>
|
|
13
|
+
</MemoryRouter>,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
describe('<NotFoundPage />', () => {
|
|
17
|
+
it('should match snapshot', () => {
|
|
18
|
+
const notFoundPage = renderPage();
|
|
19
|
+
expect(notFoundPage.toJSON()).toMatchSnapshot();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styled from 'styled-components/macro';
|
|
3
|
+
import { P } from './P';
|
|
4
|
+
import { Helmet } from 'react-helmet-async';
|
|
5
|
+
|
|
6
|
+
export function NotFoundPage() {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<Helmet>
|
|
10
|
+
<title>404 Page Not Found</title>
|
|
11
|
+
<meta name="description" content="Page not found" />
|
|
12
|
+
</Helmet>
|
|
13
|
+
<Wrapper>
|
|
14
|
+
<Title>
|
|
15
|
+
4
|
|
16
|
+
<span role="img" aria-label="Crying Face">
|
|
17
|
+
😢
|
|
18
|
+
</span>
|
|
19
|
+
4
|
|
20
|
+
</Title>
|
|
21
|
+
<P>Page not found.</P>
|
|
22
|
+
</Wrapper>
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const Wrapper = styled.div`
|
|
28
|
+
height: 100%;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
min-height: 320px;
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const Title = styled.div`
|
|
37
|
+
margin-top: -8vh;
|
|
38
|
+
font-weight: bold;
|
|
39
|
+
color: ${p => p.theme.text};
|
|
40
|
+
font-size: 3.375rem;
|
|
41
|
+
|
|
42
|
+
span {
|
|
43
|
+
font-size: 3.125rem;
|
|
44
|
+
}
|
|
45
|
+
`;
|