datajunction-ui 0.0.1-a96 → 0.0.1-a97
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/package.json +3 -3
- package/src/app/icons/EyeIcon.jsx +20 -0
- package/src/app/icons/JupyterExportIcon.jsx +25 -0
- package/src/app/icons/PythonIcon.jsx +6 -44
- package/src/app/pages/NodePage/ClientCodePopover.jsx +73 -25
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +34 -28
- package/src/app/pages/NodePage/NodeColumnTab.jsx +13 -8
- package/src/app/pages/NodePage/NotebookDownload.jsx +6 -7
- package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
- package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +10 -11
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +0 -4
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +0 -301
- package/src/app/pages/NodePage/index.jsx +46 -24
- package/src/app/services/DJService.js +57 -0
- package/src/styles/index.css +1 -1
- package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +0 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datajunction-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1a97",
|
|
4
4
|
"description": "DataJunction Metrics Platform UI",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"repository": {
|
|
@@ -165,10 +165,10 @@
|
|
|
165
165
|
],
|
|
166
166
|
"coverageThreshold": {
|
|
167
167
|
"global": {
|
|
168
|
-
"statements":
|
|
168
|
+
"statements": 80,
|
|
169
169
|
"branches": 70,
|
|
170
170
|
"lines": 80,
|
|
171
|
-
"functions":
|
|
171
|
+
"functions": 80
|
|
172
172
|
}
|
|
173
173
|
},
|
|
174
174
|
"moduleNameMapper": {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const EyeIcon = props => (
|
|
2
|
+
<svg
|
|
3
|
+
className="feather feather-eye"
|
|
4
|
+
fill="none"
|
|
5
|
+
height="24"
|
|
6
|
+
width="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
stroke="currentColor"
|
|
9
|
+
strokeWidth="2"
|
|
10
|
+
strokeLinecap="round"
|
|
11
|
+
strokeLinejoin="round"
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
16
|
+
<circle cx="12" cy="12" r="3" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export default EyeIcon;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const JupyterExportIcon = props => (
|
|
2
|
+
<svg
|
|
3
|
+
className="feather feather-jupyter-export"
|
|
4
|
+
fill="none"
|
|
5
|
+
height="24"
|
|
6
|
+
width="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
stroke="currentColor"
|
|
9
|
+
strokeWidth="2"
|
|
10
|
+
strokeLinecap="round"
|
|
11
|
+
strokeLinejoin="round"
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
{/* Notebook outline */}
|
|
16
|
+
<rect x="3" y="2" width="14" height="20" rx="2" ry="2" />
|
|
17
|
+
|
|
18
|
+
{/* Notebook lines */}
|
|
19
|
+
<line x1="7" y1="6" x2="13" y2="6" />
|
|
20
|
+
<line x1="7" y1="10" x2="13" y2="10" />
|
|
21
|
+
<line x1="7" y1="14" x2="13" y2="14" />
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export default JupyterExportIcon;
|
|
@@ -1,51 +1,13 @@
|
|
|
1
1
|
const PythonIcon = props => (
|
|
2
2
|
<svg
|
|
3
|
-
width="45px"
|
|
4
|
-
height="45px"
|
|
5
|
-
viewBox="0 0 64 64"
|
|
6
|
-
fill="none"
|
|
7
3
|
xmlns="http://www.w3.org/2000/svg"
|
|
4
|
+
x="0px"
|
|
5
|
+
y="0px"
|
|
6
|
+
width="24"
|
|
7
|
+
height="24"
|
|
8
|
+
viewBox="0 0 48 48"
|
|
8
9
|
>
|
|
9
|
-
<
|
|
10
|
-
<g
|
|
11
|
-
id="SVGRepo_tracerCarrier"
|
|
12
|
-
strokeLinecap="round"
|
|
13
|
-
strokeLinejoin="round"
|
|
14
|
-
></g>
|
|
15
|
-
<g id="SVGRepo_iconCarrier">
|
|
16
|
-
<path
|
|
17
|
-
d="M31.885 16c-8.124 0-7.617 3.523-7.617 3.523l.01 3.65h7.752v1.095H21.197S16 23.678 16 31.876c0 8.196 4.537 7.906 4.537 7.906h2.708v-3.804s-.146-4.537 4.465-4.537h7.688s4.32.07 4.32-4.175v-7.019S40.374 16 31.885 16zm-4.275 2.454c.771 0 1.395.624 1.395 1.395s-.624 1.395-1.395 1.395a1.393 1.393 0 0 1-1.395-1.395c0-.771.624-1.395 1.395-1.395z"
|
|
18
|
-
fill="url(#a)"
|
|
19
|
-
></path>
|
|
20
|
-
<path
|
|
21
|
-
d="M32.115 47.833c8.124 0 7.617-3.523 7.617-3.523l-.01-3.65H31.97v-1.095h10.832S48 40.155 48 31.958c0-8.197-4.537-7.906-4.537-7.906h-2.708v3.803s.146 4.537-4.465 4.537h-7.688s-4.32-.07-4.32 4.175v7.019s-.656 4.247 7.833 4.247zm4.275-2.454a1.393 1.393 0 0 1-1.395-1.395c0-.77.624-1.394 1.395-1.394s1.395.623 1.395 1.394c0 .772-.624 1.395-1.395 1.395z"
|
|
22
|
-
fill="url(#b)"
|
|
23
|
-
></path>
|
|
24
|
-
<defs>
|
|
25
|
-
<linearGradient
|
|
26
|
-
id="a"
|
|
27
|
-
x1="19.075"
|
|
28
|
-
y1="18.782"
|
|
29
|
-
x2="34.898"
|
|
30
|
-
y2="34.658"
|
|
31
|
-
gradientUnits="userSpaceOnUse"
|
|
32
|
-
>
|
|
33
|
-
<stop stopColor="#387EB8"></stop>
|
|
34
|
-
<stop offset="1" stopColor="#366994"></stop>
|
|
35
|
-
</linearGradient>
|
|
36
|
-
<linearGradient
|
|
37
|
-
id="b"
|
|
38
|
-
x1="28.809"
|
|
39
|
-
y1="28.882"
|
|
40
|
-
x2="45.803"
|
|
41
|
-
y2="45.163"
|
|
42
|
-
gradientUnits="userSpaceOnUse"
|
|
43
|
-
>
|
|
44
|
-
<stop stopColor="#FFE052"></stop>
|
|
45
|
-
<stop offset="1" stopColor="#FFC331"></stop>
|
|
46
|
-
</linearGradient>
|
|
47
|
-
</defs>
|
|
48
|
-
</g>
|
|
10
|
+
<path d="M 24 3 C 20.271429 3 18.240267 3.9470561 16.792969 4.5742188 L 16.791016 4.5742188 C 15.488673 5.1421213 14.704771 6.3187748 14.365234 7.4296875 C 14.025697 8.5406002 14 9.6506515 14 10.640625 L 14 14 L 10.640625 14 C 9.6506515 14 8.5406002 14.0257 7.4296875 14.365234 C 6.3187748 14.704771 5.1421212 15.488626 4.5742188 16.791016 L 4.5742188 16.792969 C 3.947056 18.24022 3 20.271429 3 24 C 3 27.728571 3.9470561 29.759733 4.5742188 31.207031 L 4.5742188 31.208984 C 5.1421212 32.511374 6.3187748 33.295229 7.4296875 33.634766 C 8.5406002 33.974256 9.6506515 34 10.640625 34 L 14 34 L 14 37.359375 C 14 38.349349 14.0257 39.459401 14.365234 40.570312 C 14.704771 41.681225 15.488626 42.857879 16.791016 43.425781 L 16.792969 43.425781 C 18.24022 44.052944 20.271429 45 24 45 C 27.728571 45 29.759733 44.052944 31.207031 43.425781 L 31.208984 43.425781 C 32.511374 42.857879 33.295229 41.681225 33.634766 40.570312 C 33.974256 39.459401 34 38.349349 34 37.359375 L 34 34 L 37.359375 34 C 38.349349 34 39.459401 33.9743 40.570312 33.634766 C 41.681225 33.295229 42.857879 32.511374 43.425781 31.208984 L 43.425781 31.207031 C 44.052944 29.75978 45 27.728571 45 24 C 45 20.271429 44.052944 18.240267 43.425781 16.792969 L 43.425781 16.791016 C 42.857879 15.488673 41.681225 14.704771 40.570312 14.365234 C 39.459401 14.025697 38.349349 14 37.359375 14 L 34 14 L 34 10.640625 C 34 9.6506515 33.974303 8.5406002 33.634766 7.4296875 C 33.295229 6.3187748 32.511374 5.1421213 31.208984 4.5742188 L 31.207031 4.5742188 C 29.75978 3.9470561 27.728571 3 24 3 z M 24 6 C 27.268623 6 28.459017 6.6519922 30.009766 7.3242188 C 30.427376 7.5063161 30.590162 7.7325533 30.765625 8.3066406 C 30.941088 8.8807279 31 9.7405985 31 10.640625 L 31 15.253906 A 1.50015 1.50015 0 0 0 31 15.740234 L 31 19 C 31 20.950062 29.450062 22.5 27.5 22.5 L 20.5 22.5 C 16.928062 22.5 14 25.428062 14 29 L 14 31 L 10.640625 31 C 9.7405985 31 8.8807279 30.941088 8.3066406 30.765625 C 7.7325533 30.590162 7.5063162 30.427376 7.3242188 30.009766 C 6.6519921 28.459017 6 27.268623 6 24 C 6 20.731377 6.6519921 19.540983 7.3242188 17.990234 C 7.5063161 17.572624 7.7325533 17.409838 8.3066406 17.234375 C 8.8807279 17.058912 9.7405985 17 10.640625 17 L 23.5 17 A 1.50015 1.50015 0 1 0 23.5 14 L 17 14 L 17 10.640625 C 17 9.7405985 17.058912 8.8807279 17.234375 8.3066406 C 17.409838 7.7325533 17.572624 7.5063162 17.990234 7.3242188 C 19.540983 6.6519921 20.731377 6 24 6 z M 20.5 9 A 1.5 1.5 0 0 0 20.5 12 A 1.5 1.5 0 0 0 20.5 9 z M 34 17 L 37.359375 17 C 38.259401 17 39.119272 17.05891 39.693359 17.234375 C 40.267447 17.409838 40.493684 17.572624 40.675781 17.990234 C 41.348008 19.540983 42 20.731377 42 24 C 42 27.268623 41.348008 28.459017 40.675781 30.009766 C 40.493684 30.427376 40.267447 30.590162 39.693359 30.765625 C 39.119272 30.941088 38.259401 31 37.359375 31 L 24.5 31 A 1.50015 1.50015 0 1 0 24.5 34 L 31 34 L 31 37.359375 C 31 38.259401 30.94109 39.119272 30.765625 39.693359 C 30.590162 40.267447 30.427376 40.493684 30.009766 40.675781 C 28.459017 41.348008 27.268623 42 24 42 C 20.731377 42 19.540983 41.348008 17.990234 40.675781 C 17.572624 40.493684 17.409838 40.267447 17.234375 39.693359 C 17.058912 39.119272 17 38.259401 17 37.359375 L 17 29 C 17 27.049938 18.549938 25.5 20.5 25.5 L 27.5 25.5 C 31.071938 25.5 34 22.571938 34 19 L 34 17 z M 27.5 36 A 1.5 1.5 0 0 0 27.5 39 A 1.5 1.5 0 0 0 27.5 36 z"></path>
|
|
49
11
|
</svg>
|
|
50
12
|
);
|
|
51
13
|
|
|
@@ -1,46 +1,94 @@
|
|
|
1
1
|
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { nightOwl } from 'react-syntax-highlighter/
|
|
3
|
+
import { nightOwl } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
|
4
4
|
import PythonIcon from '../../icons/PythonIcon';
|
|
5
5
|
|
|
6
6
|
export default function ClientCodePopover({ code }) {
|
|
7
|
-
const [
|
|
8
|
-
const
|
|
7
|
+
const [showModal, setShowModal] = useState(false);
|
|
8
|
+
const modalRef = useRef(null);
|
|
9
9
|
|
|
10
10
|
useEffect(() => {
|
|
11
11
|
const handleClickOutside = event => {
|
|
12
|
-
if (
|
|
13
|
-
|
|
12
|
+
if (modalRef.current && !modalRef.current.contains(event.target)) {
|
|
13
|
+
setShowModal(false);
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
if (showModal) {
|
|
18
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
19
|
+
} else {
|
|
20
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
return () => {
|
|
18
|
-
document.removeEventListener('
|
|
24
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
19
25
|
};
|
|
20
|
-
}, [
|
|
26
|
+
}, [showModal]);
|
|
21
27
|
|
|
22
28
|
return (
|
|
23
29
|
<>
|
|
24
30
|
<button
|
|
25
|
-
className="
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
height="45px"
|
|
29
|
-
onClick={() => setCodeAnchor(!codeAnchor)}
|
|
31
|
+
className="button-3"
|
|
32
|
+
onClick={() => setShowModal(true)}
|
|
33
|
+
style={{ height: '2.5rem' }}
|
|
30
34
|
>
|
|
31
|
-
<PythonIcon />
|
|
35
|
+
<PythonIcon /> See Python
|
|
32
36
|
</button>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
|
|
38
|
+
{showModal && (
|
|
39
|
+
<div
|
|
40
|
+
className="modal-backdrop fade in"
|
|
41
|
+
style={{
|
|
42
|
+
position: 'fixed',
|
|
43
|
+
top: 0,
|
|
44
|
+
left: 0,
|
|
45
|
+
width: '100vw',
|
|
46
|
+
height: '100vh',
|
|
47
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
48
|
+
zIndex: 9999,
|
|
49
|
+
display: 'flex',
|
|
50
|
+
alignItems: 'center',
|
|
51
|
+
justifyContent: 'center',
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
className="centerPopover"
|
|
56
|
+
ref={modalRef}
|
|
57
|
+
style={{
|
|
58
|
+
position: 'relative',
|
|
59
|
+
maxWidth: '80%',
|
|
60
|
+
width: '600px',
|
|
61
|
+
maxHeight: '80vh',
|
|
62
|
+
overflowY: 'auto',
|
|
63
|
+
padding: '1.5rem',
|
|
64
|
+
background: '#fff',
|
|
65
|
+
borderRadius: '10px',
|
|
66
|
+
boxShadow: '0 3px 10px rgba(0, 0, 0, 0.3)',
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
<button
|
|
70
|
+
onClick={() => setShowModal(false)}
|
|
71
|
+
style={{
|
|
72
|
+
position: 'absolute',
|
|
73
|
+
top: '1rem',
|
|
74
|
+
right: '1rem',
|
|
75
|
+
background: 'none',
|
|
76
|
+
border: 'none',
|
|
77
|
+
fontSize: '1.5rem',
|
|
78
|
+
cursor: 'pointer',
|
|
79
|
+
color: '#999',
|
|
80
|
+
}}
|
|
81
|
+
aria-label="Close modal"
|
|
82
|
+
>
|
|
83
|
+
×
|
|
84
|
+
</button>
|
|
85
|
+
<h2>Python Client Code</h2>
|
|
86
|
+
<SyntaxHighlighter language="python" style={nightOwl}>
|
|
87
|
+
{code}
|
|
88
|
+
</SyntaxHighlighter>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
44
92
|
</>
|
|
45
93
|
);
|
|
46
94
|
}
|
|
@@ -9,7 +9,7 @@ import LoadingIcon from '../../icons/LoadingIcon';
|
|
|
9
9
|
|
|
10
10
|
export default function LinkDimensionPopover({
|
|
11
11
|
column,
|
|
12
|
-
|
|
12
|
+
dimensionNodes,
|
|
13
13
|
node,
|
|
14
14
|
options,
|
|
15
15
|
onSubmit,
|
|
@@ -30,25 +30,31 @@ export default function LinkDimensionPopover({
|
|
|
30
30
|
};
|
|
31
31
|
}, [setPopoverAnchor]);
|
|
32
32
|
|
|
33
|
-
const columnDimension = referencedDimensionNode;
|
|
34
|
-
|
|
35
33
|
const handleSubmit = async (
|
|
36
|
-
{ node, column,
|
|
34
|
+
{ node, column, updatedDimensionNodes },
|
|
37
35
|
{ setSubmitting, setStatus },
|
|
38
36
|
) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
const oldSet = new Set(dimensionNodes);
|
|
38
|
+
const newSet = new Set(updatedDimensionNodes);
|
|
39
|
+
try {
|
|
40
|
+
const linkPromises = Array.from(newSet)
|
|
41
|
+
.filter(item => !oldSet.has(item))
|
|
42
|
+
.map(dimension => {
|
|
43
|
+
return linkDimension(node, column, dimension, setStatus);
|
|
44
|
+
});
|
|
45
|
+
const unlinkPromises = Array.from(oldSet)
|
|
46
|
+
.filter(item => !newSet.has(item))
|
|
47
|
+
.map(dimension => {
|
|
48
|
+
return unlinkDimension(node, column, dimension, setStatus);
|
|
49
|
+
});
|
|
50
|
+
await Promise.all([...linkPromises, ...unlinkPromises]);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Error in editing linked dimensions:', error);
|
|
53
|
+
setStatus({ error: error.message });
|
|
54
|
+
} finally {
|
|
55
|
+
setSubmitting(false);
|
|
56
|
+
onSubmit();
|
|
50
57
|
}
|
|
51
|
-
onSubmit();
|
|
52
58
|
};
|
|
53
59
|
|
|
54
60
|
const linkDimension = async (node, column, dimension, setStatus) => {
|
|
@@ -100,8 +106,7 @@ export default function LinkDimensionPopover({
|
|
|
100
106
|
initialValues={{
|
|
101
107
|
column: column.name,
|
|
102
108
|
node: node.name,
|
|
103
|
-
|
|
104
|
-
currentDimension: referencedDimensionNode,
|
|
109
|
+
updatedDimensionNodes: '',
|
|
105
110
|
}}
|
|
106
111
|
onSubmit={handleSubmit}
|
|
107
112
|
>
|
|
@@ -111,20 +116,21 @@ export default function LinkDimensionPopover({
|
|
|
111
116
|
{displayMessageAfterSubmit(status)}
|
|
112
117
|
<span data-testid="link-dimension">
|
|
113
118
|
<FormikSelect
|
|
114
|
-
selectOptions={
|
|
115
|
-
|
|
116
|
-
].concat(options)}
|
|
117
|
-
formikFieldName="dimension"
|
|
119
|
+
selectOptions={options}
|
|
120
|
+
formikFieldName="updatedDimensionNodes"
|
|
118
121
|
placeholder="Select dimension to link"
|
|
119
122
|
className=""
|
|
120
123
|
defaultValue={
|
|
121
|
-
|
|
122
|
-
? {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
dimensionNodes.length > 0
|
|
125
|
+
? dimensionNodes.map(dimNode => {
|
|
126
|
+
return {
|
|
127
|
+
value: dimNode,
|
|
128
|
+
label: dimNode,
|
|
129
|
+
};
|
|
130
|
+
})
|
|
131
|
+
: []
|
|
127
132
|
}
|
|
133
|
+
isMulti={true}
|
|
128
134
|
/>
|
|
129
135
|
</span>
|
|
130
136
|
<input
|
|
@@ -7,6 +7,7 @@ import { labelize } from '../../../utils/form';
|
|
|
7
7
|
import PartitionColumnPopover from './PartitionColumnPopover';
|
|
8
8
|
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
9
9
|
import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
|
|
10
|
+
import { link } from 'fs';
|
|
10
11
|
|
|
11
12
|
export default function NodeColumnTab({ node, djClient }) {
|
|
12
13
|
const [attributes, setAttributes] = useState([]);
|
|
@@ -146,16 +147,20 @@ export default function NodeColumnTab({ node, djClient }) {
|
|
|
146
147
|
</td>
|
|
147
148
|
{node.type !== 'cube' ? (
|
|
148
149
|
<td>
|
|
149
|
-
{
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
{dimensionLinks.length > 0
|
|
151
|
+
? dimensionLinks.map(link => (
|
|
152
|
+
<span
|
|
153
|
+
className="rounded-pill badge bg-secondary-soft"
|
|
154
|
+
style={{ fontSize: '14px' }}
|
|
155
|
+
key={link[0]}
|
|
156
|
+
>
|
|
157
|
+
<a href={`/nodes/${link[0]}`}>{link[0]}</a>
|
|
158
|
+
</span>
|
|
159
|
+
))
|
|
160
|
+
: ''}
|
|
156
161
|
<LinkDimensionPopover
|
|
157
162
|
column={col}
|
|
158
|
-
|
|
163
|
+
dimensionNodes={dimensionLinks.map(link => link[0])}
|
|
159
164
|
node={node}
|
|
160
165
|
options={dimensions}
|
|
161
166
|
onSubmit={async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import DJClientContext from '../../providers/djclient';
|
|
2
|
+
import JupyterExportIcon from '../../icons/JupyterExportIcon';
|
|
2
3
|
import { useContext } from 'react';
|
|
3
4
|
|
|
4
5
|
export default function NotebookDownload({ node }) {
|
|
@@ -23,15 +24,13 @@ export default function NotebookDownload({ node }) {
|
|
|
23
24
|
|
|
24
25
|
return (
|
|
25
26
|
<>
|
|
26
|
-
<
|
|
27
|
-
className="
|
|
28
|
-
style={{ cursor: 'pointer', backgroundColor: '#ffefd0' }}
|
|
29
|
-
tabIndex="0"
|
|
30
|
-
height="45px"
|
|
27
|
+
<button
|
|
28
|
+
className="button-3"
|
|
31
29
|
onClick={downloadFile}
|
|
30
|
+
style={{ height: '2.5rem' }}
|
|
32
31
|
>
|
|
33
|
-
Export as Notebook
|
|
34
|
-
</
|
|
32
|
+
<JupyterExportIcon /> Export as Notebook
|
|
33
|
+
</button>
|
|
35
34
|
</>
|
|
36
35
|
);
|
|
37
36
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { useContext, useState, useRef, useEffect } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import EyeIcon from '../../icons/EyeIcon';
|
|
4
|
+
import ExpandedIcon from '../../icons/ExpandedIcon';
|
|
5
|
+
import CollapsedIcon from '../../icons/CollapsedIcon';
|
|
6
|
+
|
|
7
|
+
const EVENT_TYPES = ['delete', 'update'];
|
|
8
|
+
|
|
9
|
+
export default function WatchButton({ node }) {
|
|
10
|
+
if (!node || !node.name || !node.type) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
15
|
+
const [selectedEvents, setSelectedEvents] = useState([]);
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
18
|
+
const dropdownRef = useRef();
|
|
19
|
+
|
|
20
|
+
// Load existing preferences
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const loadPreferences = async () => {
|
|
23
|
+
try {
|
|
24
|
+
const preferences = await djClient.getNotificationPreferences({
|
|
25
|
+
entity_name: node.name,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const matched = preferences.find(item =>
|
|
29
|
+
item.alert_types.includes('web'),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (matched) {
|
|
33
|
+
setSelectedEvents(matched.activity_types);
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('Failed to load notification preferences', err);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
loadPreferences();
|
|
41
|
+
}, [djClient, node.name, node.type]);
|
|
42
|
+
|
|
43
|
+
// Close dropdown on outside click
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const handleClickOutside = event => {
|
|
46
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
47
|
+
setDropdownOpen(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
52
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
53
|
+
}, []);
|
|
54
|
+
const toggleEvent = async event => {
|
|
55
|
+
const isSelected = selectedEvents.includes(event);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
setLoading(true);
|
|
59
|
+
let updatedEvents;
|
|
60
|
+
|
|
61
|
+
if (!isSelected) {
|
|
62
|
+
updatedEvents = Array.from(new Set([...selectedEvents, event]));
|
|
63
|
+
} else {
|
|
64
|
+
updatedEvents = selectedEvents.filter(e => e !== event);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (updatedEvents.length === 0) {
|
|
68
|
+
await djClient.unsubscribeFromNotifications({
|
|
69
|
+
entity_type: 'node',
|
|
70
|
+
entity_name: node.name,
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
await djClient.subscribeToNotifications({
|
|
74
|
+
entity_type: 'node',
|
|
75
|
+
entity_name: node.name,
|
|
76
|
+
activity_types: updatedEvents,
|
|
77
|
+
alert_types: ['web'],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setSelectedEvents(updatedEvents);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error('Failed to update preference', err);
|
|
84
|
+
} finally {
|
|
85
|
+
setLoading(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleWatchClick = async () => {
|
|
90
|
+
try {
|
|
91
|
+
setLoading(true);
|
|
92
|
+
if (selectedEvents.length === 0) {
|
|
93
|
+
await djClient.subscribeToNotifications({
|
|
94
|
+
entity_type: 'node',
|
|
95
|
+
entity_name: node.name,
|
|
96
|
+
activity_types: EVENT_TYPES,
|
|
97
|
+
alert_types: ['web'],
|
|
98
|
+
});
|
|
99
|
+
setSelectedEvents(EVENT_TYPES);
|
|
100
|
+
} else {
|
|
101
|
+
await djClient.unsubscribeFromNotifications({
|
|
102
|
+
entity_type: 'node',
|
|
103
|
+
entity_name: node.name,
|
|
104
|
+
});
|
|
105
|
+
setSelectedEvents([]);
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error('Watch toggle failed', err);
|
|
109
|
+
} finally {
|
|
110
|
+
setLoading(false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
className="btn-group"
|
|
117
|
+
ref={dropdownRef}
|
|
118
|
+
style={{
|
|
119
|
+
position: 'relative',
|
|
120
|
+
display: 'inline-flex',
|
|
121
|
+
verticalAlign: 'middle',
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<button
|
|
125
|
+
className="button-3"
|
|
126
|
+
onClick={handleWatchClick}
|
|
127
|
+
disabled={loading}
|
|
128
|
+
style={{
|
|
129
|
+
borderTopRightRadius: 0,
|
|
130
|
+
borderBottomRightRadius: 0,
|
|
131
|
+
marginRight: 0,
|
|
132
|
+
height: '2.5rem',
|
|
133
|
+
display: 'flex',
|
|
134
|
+
alignItems: 'center',
|
|
135
|
+
gap: '6px',
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<EyeIcon />
|
|
139
|
+
Watch
|
|
140
|
+
{selectedEvents.length > 0 && (
|
|
141
|
+
<span
|
|
142
|
+
style={{
|
|
143
|
+
backgroundColor: '#e2e6ed',
|
|
144
|
+
color: '#333',
|
|
145
|
+
padding: '2px 6px',
|
|
146
|
+
borderRadius: '999px',
|
|
147
|
+
fontSize: '0.75rem',
|
|
148
|
+
fontWeight: 500,
|
|
149
|
+
lineHeight: 1,
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{selectedEvents.length}
|
|
153
|
+
</span>
|
|
154
|
+
)}
|
|
155
|
+
</button>
|
|
156
|
+
|
|
157
|
+
<button
|
|
158
|
+
className="button-3"
|
|
159
|
+
onClick={() => setDropdownOpen(prev => !prev)}
|
|
160
|
+
disabled={loading}
|
|
161
|
+
style={{
|
|
162
|
+
borderTopLeftRadius: 0,
|
|
163
|
+
borderBottomLeftRadius: 0,
|
|
164
|
+
marginLeft: 0,
|
|
165
|
+
height: '2.5rem',
|
|
166
|
+
}}
|
|
167
|
+
aria-label="Toggle dropdown"
|
|
168
|
+
>
|
|
169
|
+
{dropdownOpen ? <ExpandedIcon /> : <CollapsedIcon />}
|
|
170
|
+
</button>
|
|
171
|
+
|
|
172
|
+
{dropdownOpen && (
|
|
173
|
+
<ul
|
|
174
|
+
className="p-2"
|
|
175
|
+
style={{
|
|
176
|
+
display: 'block',
|
|
177
|
+
minWidth: '220px',
|
|
178
|
+
position: 'absolute',
|
|
179
|
+
top: '100%',
|
|
180
|
+
right: 0,
|
|
181
|
+
zIndex: 9999,
|
|
182
|
+
backgroundColor: '#fff',
|
|
183
|
+
border: '1px solid #ccc',
|
|
184
|
+
borderRadius: '0.5rem',
|
|
185
|
+
marginTop: '0.25rem',
|
|
186
|
+
boxShadow: '0px 6px 16px rgba(0, 0, 0, 0.1)',
|
|
187
|
+
padding: '0.5rem 0',
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{EVENT_TYPES.map(event => {
|
|
191
|
+
const isSelected = selectedEvents.includes(event);
|
|
192
|
+
return (
|
|
193
|
+
<li
|
|
194
|
+
key={event}
|
|
195
|
+
onClick={() => toggleEvent(event)}
|
|
196
|
+
style={{
|
|
197
|
+
display: 'flex',
|
|
198
|
+
alignItems: 'center',
|
|
199
|
+
padding: '0.5rem 1rem',
|
|
200
|
+
cursor: 'pointer',
|
|
201
|
+
backgroundColor: isSelected ? '#f0f4f8' : 'transparent',
|
|
202
|
+
fontWeight: isSelected ? '600' : '400',
|
|
203
|
+
fontSize: '0.9rem',
|
|
204
|
+
color: '#333',
|
|
205
|
+
borderLeft: isSelected
|
|
206
|
+
? '4px solid #7983ff'
|
|
207
|
+
: '4px solid transparent',
|
|
208
|
+
transition: 'background 0.2s',
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<input
|
|
212
|
+
className="form-check-input"
|
|
213
|
+
type="checkbox"
|
|
214
|
+
checked={isSelected}
|
|
215
|
+
readOnly
|
|
216
|
+
style={{ marginRight: '0.75rem' }}
|
|
217
|
+
/>
|
|
218
|
+
{event}
|
|
219
|
+
</li>
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
222
|
+
</ul>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
@@ -19,7 +19,6 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
19
19
|
};
|
|
20
20
|
const node = { name: 'default.node1' };
|
|
21
21
|
const options = [
|
|
22
|
-
{ value: 'Remove', label: '[Remove dimension link]' },
|
|
23
22
|
{ value: 'default.dimension1', label: 'Dimension 1' },
|
|
24
23
|
{ value: 'default.dimension2', label: 'Dimension 2' },
|
|
25
24
|
];
|
|
@@ -42,7 +41,7 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
42
41
|
<DJClientContext.Provider value={mockDjClient}>
|
|
43
42
|
<LinkDimensionPopover
|
|
44
43
|
column={column}
|
|
45
|
-
|
|
44
|
+
dimensionNodes={['default.dimension1']}
|
|
46
45
|
node={node}
|
|
47
46
|
options={options}
|
|
48
47
|
onSubmit={onSubmitMock}
|
|
@@ -56,7 +55,7 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
56
55
|
// Click on a dimension and save
|
|
57
56
|
const linkDimension = getByTestId('link-dimension');
|
|
58
57
|
fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
|
|
59
|
-
fireEvent.click(screen.getByText('Dimension
|
|
58
|
+
fireEvent.click(screen.getByText('Dimension 2'));
|
|
60
59
|
fireEvent.click(getByText('Save'));
|
|
61
60
|
|
|
62
61
|
// Expect linkDimension to be called
|
|
@@ -64,14 +63,14 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
64
63
|
expect(mockDjClient.DataJunctionAPI.linkDimension).toHaveBeenCalledWith(
|
|
65
64
|
'default.node1',
|
|
66
65
|
'column1',
|
|
67
|
-
'default.
|
|
66
|
+
'default.dimension2',
|
|
68
67
|
);
|
|
69
68
|
expect(getByText('Saved!')).toBeInTheDocument();
|
|
70
69
|
});
|
|
71
70
|
|
|
72
71
|
// Click on the 'Remove' option and save
|
|
73
|
-
|
|
74
|
-
fireEvent.click(
|
|
72
|
+
const removeButton = screen.getByLabelText('Remove default.dimension1');
|
|
73
|
+
fireEvent.click(removeButton);
|
|
75
74
|
fireEvent.click(getByText('Save'));
|
|
76
75
|
|
|
77
76
|
// Expect unlinkDimension to be called
|
|
@@ -115,7 +114,7 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
115
114
|
<DJClientContext.Provider value={mockDjClient}>
|
|
116
115
|
<LinkDimensionPopover
|
|
117
116
|
column={column}
|
|
118
|
-
|
|
117
|
+
dimensionNodes={['default.dimension1']}
|
|
119
118
|
node={node}
|
|
120
119
|
options={options}
|
|
121
120
|
onSubmit={onSubmitMock}
|
|
@@ -129,7 +128,7 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
129
128
|
// Click on a dimension and save
|
|
130
129
|
const linkDimension = getByTestId('link-dimension');
|
|
131
130
|
fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
|
|
132
|
-
fireEvent.click(screen.getByText('Dimension
|
|
131
|
+
fireEvent.click(screen.getByText('Dimension 2'));
|
|
133
132
|
fireEvent.click(getByText('Save'));
|
|
134
133
|
|
|
135
134
|
// Expect linkDimension to be called
|
|
@@ -137,7 +136,7 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
137
136
|
expect(mockDjClient.DataJunctionAPI.linkDimension).toHaveBeenCalledWith(
|
|
138
137
|
'default.node1',
|
|
139
138
|
'column1',
|
|
140
|
-
'default.
|
|
139
|
+
'default.dimension2',
|
|
141
140
|
);
|
|
142
141
|
expect(
|
|
143
142
|
getByText('Failed due to nonexistent dimension'),
|
|
@@ -145,8 +144,8 @@ describe('<LinkDimensionPopover />', () => {
|
|
|
145
144
|
});
|
|
146
145
|
|
|
147
146
|
// Click on the 'Remove' option and save
|
|
148
|
-
|
|
149
|
-
fireEvent.click(
|
|
147
|
+
const removeButton = screen.getByLabelText('Remove default.dimension1');
|
|
148
|
+
fireEvent.click(removeButton);
|
|
150
149
|
fireEvent.click(getByText('Save'));
|
|
151
150
|
|
|
152
151
|
// Expect unlinkDimension to be called
|
|
@@ -657,10 +657,6 @@ describe('<NodePage />', () => {
|
|
|
657
657
|
expect(djClient.DataJunctionAPI.materializations).toHaveBeenCalledWith(
|
|
658
658
|
mocks.mockTransformNode.name,
|
|
659
659
|
);
|
|
660
|
-
|
|
661
|
-
expect(
|
|
662
|
-
screen.getByRole('table', { name: 'Materializations' }),
|
|
663
|
-
).toMatchSnapshot();
|
|
664
660
|
},
|
|
665
661
|
{ timeout: 3000 },
|
|
666
662
|
);
|
|
@@ -17,304 +17,3 @@ HTMLCollection [
|
|
|
17
17
|
</code>,
|
|
18
18
|
]
|
|
19
19
|
`;
|
|
20
|
-
|
|
21
|
-
exports[`<NodePage /> renders the NodeMaterialization tab with materializations correctly 1`] = `
|
|
22
|
-
<div
|
|
23
|
-
aria-label="Materializations"
|
|
24
|
-
class="table-vertical"
|
|
25
|
-
role="table"
|
|
26
|
-
>
|
|
27
|
-
<div>
|
|
28
|
-
<h2>
|
|
29
|
-
Materializations
|
|
30
|
-
</h2>
|
|
31
|
-
<button
|
|
32
|
-
aria-label="AddMaterialization"
|
|
33
|
-
class="edit_button"
|
|
34
|
-
tabindex="0"
|
|
35
|
-
>
|
|
36
|
-
<span
|
|
37
|
-
class="add_node"
|
|
38
|
-
>
|
|
39
|
-
+ Add Materialization
|
|
40
|
-
</span>
|
|
41
|
-
</button>
|
|
42
|
-
<div
|
|
43
|
-
class="fade modal-backdrop in"
|
|
44
|
-
style="display: none;"
|
|
45
|
-
/>
|
|
46
|
-
<div
|
|
47
|
-
aria-label="client-code"
|
|
48
|
-
class="centerPopover"
|
|
49
|
-
role="dialog"
|
|
50
|
-
style="display: none; width: 50%;"
|
|
51
|
-
>
|
|
52
|
-
<form
|
|
53
|
-
action="#"
|
|
54
|
-
>
|
|
55
|
-
<h2>
|
|
56
|
-
Configure Materialization
|
|
57
|
-
</h2>
|
|
58
|
-
<input
|
|
59
|
-
hidden=""
|
|
60
|
-
name="node"
|
|
61
|
-
readonly=""
|
|
62
|
-
value="default.repair_order_transform"
|
|
63
|
-
/>
|
|
64
|
-
<span
|
|
65
|
-
data-testid="edit-partition"
|
|
66
|
-
>
|
|
67
|
-
<label
|
|
68
|
-
for="strategy"
|
|
69
|
-
>
|
|
70
|
-
Strategy
|
|
71
|
-
</label>
|
|
72
|
-
<select
|
|
73
|
-
id="strategy"
|
|
74
|
-
name="strategy"
|
|
75
|
-
>
|
|
76
|
-
<option
|
|
77
|
-
value="full"
|
|
78
|
-
>
|
|
79
|
-
Full
|
|
80
|
-
</option>
|
|
81
|
-
<option
|
|
82
|
-
value="incremental_time"
|
|
83
|
-
>
|
|
84
|
-
Incremental Time
|
|
85
|
-
</option>
|
|
86
|
-
</select>
|
|
87
|
-
</span>
|
|
88
|
-
<br />
|
|
89
|
-
<br />
|
|
90
|
-
<label
|
|
91
|
-
for="schedule"
|
|
92
|
-
>
|
|
93
|
-
Schedule
|
|
94
|
-
</label>
|
|
95
|
-
<input
|
|
96
|
-
default=""
|
|
97
|
-
id="schedule"
|
|
98
|
-
name="schedule"
|
|
99
|
-
placeholder="Cron"
|
|
100
|
-
type="text"
|
|
101
|
-
value="@daily"
|
|
102
|
-
/>
|
|
103
|
-
<br />
|
|
104
|
-
<br />
|
|
105
|
-
<div
|
|
106
|
-
class="DescriptionInput"
|
|
107
|
-
>
|
|
108
|
-
<label
|
|
109
|
-
for="lookback_window"
|
|
110
|
-
>
|
|
111
|
-
Lookback Window
|
|
112
|
-
</label>
|
|
113
|
-
<input
|
|
114
|
-
default=""
|
|
115
|
-
id="lookback_window"
|
|
116
|
-
name="lookback_window"
|
|
117
|
-
placeholder="1 DAY"
|
|
118
|
-
type="text"
|
|
119
|
-
value="1 DAY"
|
|
120
|
-
/>
|
|
121
|
-
</div>
|
|
122
|
-
<br />
|
|
123
|
-
<div
|
|
124
|
-
class="DescriptionInput"
|
|
125
|
-
>
|
|
126
|
-
<details>
|
|
127
|
-
<summary>
|
|
128
|
-
<label
|
|
129
|
-
for="SparkConfig"
|
|
130
|
-
style="display: inline-block;"
|
|
131
|
-
>
|
|
132
|
-
Spark Config
|
|
133
|
-
</label>
|
|
134
|
-
</summary>
|
|
135
|
-
<textarea
|
|
136
|
-
id="SparkConfig"
|
|
137
|
-
name="spark_config"
|
|
138
|
-
style="display: none;"
|
|
139
|
-
type="textarea"
|
|
140
|
-
>
|
|
141
|
-
[object Object]
|
|
142
|
-
</textarea>
|
|
143
|
-
<div
|
|
144
|
-
class="relative flex bg-[#282a36]"
|
|
145
|
-
role="button"
|
|
146
|
-
tabindex="0"
|
|
147
|
-
>
|
|
148
|
-
<div
|
|
149
|
-
class="cm-theme-light"
|
|
150
|
-
id="spark_config"
|
|
151
|
-
name="spark_config"
|
|
152
|
-
options="[object Object]"
|
|
153
|
-
style="margin: 0px 0px 23px 0px; flex: 1; font-size: 150%; text-align: left;"
|
|
154
|
-
>
|
|
155
|
-
<div
|
|
156
|
-
class="cm-editor ͼ1 ͼ2 ͼ4 ͼ1g ͼ15"
|
|
157
|
-
>
|
|
158
|
-
<div
|
|
159
|
-
aria-live="polite"
|
|
160
|
-
style="position: absolute; top: -10000px;"
|
|
161
|
-
/>
|
|
162
|
-
<div
|
|
163
|
-
class="cm-scroller"
|
|
164
|
-
tabindex="-1"
|
|
165
|
-
>
|
|
166
|
-
<div
|
|
167
|
-
aria-hidden="true"
|
|
168
|
-
class="cm-gutters"
|
|
169
|
-
style="min-height: 56px; position: sticky;"
|
|
170
|
-
>
|
|
171
|
-
<div
|
|
172
|
-
class="cm-gutter cm-lineNumbers"
|
|
173
|
-
>
|
|
174
|
-
<div
|
|
175
|
-
class="cm-gutterElement"
|
|
176
|
-
style="height: 0px; visibility: hidden; pointer-events: none;"
|
|
177
|
-
>
|
|
178
|
-
9
|
|
179
|
-
</div>
|
|
180
|
-
<div
|
|
181
|
-
class="cm-gutterElement cm-activeLineGutter"
|
|
182
|
-
style="height: 14px;"
|
|
183
|
-
>
|
|
184
|
-
1
|
|
185
|
-
</div>
|
|
186
|
-
<div
|
|
187
|
-
class="cm-gutterElement"
|
|
188
|
-
style="height: 14px;"
|
|
189
|
-
>
|
|
190
|
-
2
|
|
191
|
-
</div>
|
|
192
|
-
<div
|
|
193
|
-
class="cm-gutterElement"
|
|
194
|
-
style="height: 14px;"
|
|
195
|
-
>
|
|
196
|
-
3
|
|
197
|
-
</div>
|
|
198
|
-
<div
|
|
199
|
-
class="cm-gutterElement"
|
|
200
|
-
style="height: 14px;"
|
|
201
|
-
>
|
|
202
|
-
4
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
<div
|
|
206
|
-
class="cm-gutter cm-foldGutter"
|
|
207
|
-
>
|
|
208
|
-
<div
|
|
209
|
-
class="cm-gutterElement"
|
|
210
|
-
style="height: 0px; visibility: hidden; pointer-events: none;"
|
|
211
|
-
>
|
|
212
|
-
<span
|
|
213
|
-
title="Unfold line"
|
|
214
|
-
>
|
|
215
|
-
›
|
|
216
|
-
</span>
|
|
217
|
-
</div>
|
|
218
|
-
<div
|
|
219
|
-
class="cm-gutterElement cm-activeLineGutter"
|
|
220
|
-
style="height: 14px;"
|
|
221
|
-
>
|
|
222
|
-
<span
|
|
223
|
-
title="Fold line"
|
|
224
|
-
>
|
|
225
|
-
⌄
|
|
226
|
-
</span>
|
|
227
|
-
</div>
|
|
228
|
-
</div>
|
|
229
|
-
</div>
|
|
230
|
-
<div
|
|
231
|
-
aria-autocomplete="list"
|
|
232
|
-
aria-multiline="true"
|
|
233
|
-
autocapitalize="off"
|
|
234
|
-
autocorrect="off"
|
|
235
|
-
class="cm-content"
|
|
236
|
-
contenteditable="true"
|
|
237
|
-
data-language="json"
|
|
238
|
-
role="textbox"
|
|
239
|
-
spellcheck="false"
|
|
240
|
-
style="tab-size: 4"
|
|
241
|
-
translate="no"
|
|
242
|
-
>
|
|
243
|
-
<div
|
|
244
|
-
class="cm-activeLine cm-line"
|
|
245
|
-
>
|
|
246
|
-
{
|
|
247
|
-
</div>
|
|
248
|
-
<div
|
|
249
|
-
class="cm-line"
|
|
250
|
-
>
|
|
251
|
-
"spark.executor.memory":
|
|
252
|
-
<span
|
|
253
|
-
class="ͼe"
|
|
254
|
-
>
|
|
255
|
-
"16g"
|
|
256
|
-
</span>
|
|
257
|
-
,
|
|
258
|
-
</div>
|
|
259
|
-
<div
|
|
260
|
-
class="cm-line"
|
|
261
|
-
>
|
|
262
|
-
"spark.memory.fraction":
|
|
263
|
-
<span
|
|
264
|
-
class="ͼe"
|
|
265
|
-
>
|
|
266
|
-
"0.3"
|
|
267
|
-
</span>
|
|
268
|
-
</div>
|
|
269
|
-
<div
|
|
270
|
-
class="cm-line"
|
|
271
|
-
>
|
|
272
|
-
}
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
<div
|
|
276
|
-
aria-hidden="true"
|
|
277
|
-
class="cm-selectionLayer"
|
|
278
|
-
/>
|
|
279
|
-
<div
|
|
280
|
-
aria-hidden="true"
|
|
281
|
-
class="cm-cursorLayer"
|
|
282
|
-
style="animation-duration: 1200ms;"
|
|
283
|
-
/>
|
|
284
|
-
</div>
|
|
285
|
-
</div>
|
|
286
|
-
</div>
|
|
287
|
-
</div>
|
|
288
|
-
</details>
|
|
289
|
-
|
|
290
|
-
</div>
|
|
291
|
-
<button
|
|
292
|
-
aria-hidden="false"
|
|
293
|
-
aria-label="SaveEditColumn"
|
|
294
|
-
class="add_node"
|
|
295
|
-
type="submit"
|
|
296
|
-
>
|
|
297
|
-
Save
|
|
298
|
-
</button>
|
|
299
|
-
</form>
|
|
300
|
-
</div>
|
|
301
|
-
<div
|
|
302
|
-
class="message alert"
|
|
303
|
-
style="margin-top: 10px;"
|
|
304
|
-
>
|
|
305
|
-
No materialization workflows configured for this node.
|
|
306
|
-
</div>
|
|
307
|
-
</div>
|
|
308
|
-
<div>
|
|
309
|
-
<h2>
|
|
310
|
-
Materialized Datasets
|
|
311
|
-
</h2>
|
|
312
|
-
<div
|
|
313
|
-
class="message alert"
|
|
314
|
-
style="margin-top: 10px;"
|
|
315
|
-
>
|
|
316
|
-
No materialized datasets available for this node.
|
|
317
|
-
</div>
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
320
|
-
`;
|
|
@@ -12,6 +12,7 @@ import DJClientContext from '../../providers/djclient';
|
|
|
12
12
|
import NodeValidateTab from './NodeValidateTab';
|
|
13
13
|
import NodeMaterializationTab from './NodeMaterializationTab';
|
|
14
14
|
import ClientCodePopover from './ClientCodePopover';
|
|
15
|
+
import WatchButton from './WatchNodeButton';
|
|
15
16
|
import NodesWithDimension from './NodesWithDimension';
|
|
16
17
|
import NodeColumnLineage from './NodeLineageTab';
|
|
17
18
|
import EditIcon from '../../icons/EditIcon';
|
|
@@ -150,44 +151,66 @@ export function NodePage() {
|
|
|
150
151
|
case 'dependencies':
|
|
151
152
|
tabToDisplay = <NodeDependenciesTab node={node} djClient={djClient} />;
|
|
152
153
|
break;
|
|
153
|
-
default:
|
|
154
|
+
default:
|
|
155
|
+
/* istanbul ignore next */
|
|
154
156
|
tabToDisplay = <NodeInfoTab node={node} />;
|
|
155
157
|
}
|
|
158
|
+
|
|
159
|
+
const NodeButtons = () => {
|
|
160
|
+
return (
|
|
161
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
162
|
+
<button
|
|
163
|
+
className="button-3"
|
|
164
|
+
onClick={() => navigate(`/nodes/${node?.name}/edit`)}
|
|
165
|
+
>
|
|
166
|
+
<EditIcon /> Edit
|
|
167
|
+
</button>
|
|
168
|
+
|
|
169
|
+
<WatchButton node={node} />
|
|
170
|
+
|
|
171
|
+
<ClientCodePopover code={node?.createNodeClientCode} />
|
|
172
|
+
{node?.type === 'cube' && <NotebookDownload node={node} />}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
156
177
|
// @ts-ignore
|
|
157
178
|
return (
|
|
158
179
|
<div className="node__header">
|
|
159
180
|
<NamespaceHeader namespace={name.split('.').slice(0, -1).join('.')} />
|
|
160
181
|
<div className="card">
|
|
161
182
|
{node?.message === undefined ? (
|
|
162
|
-
<div className="card-header">
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
183
|
+
<div className="card-header" style={{}}>
|
|
184
|
+
<div
|
|
185
|
+
style={{
|
|
186
|
+
display: 'flex',
|
|
187
|
+
flexDirection: 'row',
|
|
188
|
+
justifyContent: 'space-between',
|
|
189
|
+
}}
|
|
166
190
|
>
|
|
167
|
-
<
|
|
168
|
-
className="card-
|
|
169
|
-
|
|
170
|
-
aria-hidden="false"
|
|
171
|
-
aria-label="DisplayName"
|
|
191
|
+
<h3
|
|
192
|
+
className="card-title align-items-start flex-column"
|
|
193
|
+
style={{ display: 'inline-block' }}
|
|
172
194
|
>
|
|
173
|
-
{node?.display_name}{' '}
|
|
174
195
|
<span
|
|
175
|
-
className=
|
|
196
|
+
className="card-label fw-bold text-gray-800"
|
|
176
197
|
role="dialog"
|
|
177
198
|
aria-hidden="false"
|
|
178
|
-
aria-label="
|
|
199
|
+
aria-label="DisplayName"
|
|
179
200
|
>
|
|
180
|
-
{node?.
|
|
201
|
+
{node?.display_name}{' '}
|
|
202
|
+
<span
|
|
203
|
+
className={'node_type__' + node?.type + ' badge node_type'}
|
|
204
|
+
role="dialog"
|
|
205
|
+
aria-hidden="false"
|
|
206
|
+
aria-label="NodeType"
|
|
207
|
+
>
|
|
208
|
+
{node?.type}
|
|
209
|
+
</span>
|
|
181
210
|
</span>
|
|
182
|
-
</
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
href={`/nodes/${node?.name}/edit`}
|
|
186
|
-
style={{ marginLeft: '0.5rem' }}
|
|
187
|
-
>
|
|
188
|
-
<EditIcon />
|
|
189
|
-
</a>
|
|
190
|
-
<ClientCodePopover code={node?.createNodeClientCode} />
|
|
211
|
+
</h3>
|
|
212
|
+
<NodeButtons />
|
|
213
|
+
</div>
|
|
191
214
|
<div>
|
|
192
215
|
<a
|
|
193
216
|
href={'/nodes/' + node?.name}
|
|
@@ -204,7 +227,6 @@ export function NodePage() {
|
|
|
204
227
|
>
|
|
205
228
|
{node?.version}
|
|
206
229
|
</span>
|
|
207
|
-
{node?.type === 'cube' ? <NotebookDownload node={node} /> : <></>}
|
|
208
230
|
</div>
|
|
209
231
|
<div className="align-items-center row">
|
|
210
232
|
{tabsList(node).map(buildTabs)}
|
|
@@ -1137,4 +1137,61 @@ export const DataJunctionAPI = {
|
|
|
1137
1137
|
})
|
|
1138
1138
|
).json();
|
|
1139
1139
|
},
|
|
1140
|
+
// GET /notifications/
|
|
1141
|
+
getNotificationPreferences: async function (params = {}) {
|
|
1142
|
+
const url = new URL(`${DJ_URL}/notifications/`);
|
|
1143
|
+
Object.entries(params).forEach(([key, value]) =>
|
|
1144
|
+
url.searchParams.append(key, value),
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
return await (
|
|
1148
|
+
await fetch(url, {
|
|
1149
|
+
credentials: 'include',
|
|
1150
|
+
})
|
|
1151
|
+
).json();
|
|
1152
|
+
},
|
|
1153
|
+
|
|
1154
|
+
// POST /notifications/subscribe
|
|
1155
|
+
subscribeToNotifications: async function ({
|
|
1156
|
+
entity_type,
|
|
1157
|
+
entity_name,
|
|
1158
|
+
activity_types,
|
|
1159
|
+
alert_types,
|
|
1160
|
+
}) {
|
|
1161
|
+
const response = await fetch(`${DJ_URL}/notifications/subscribe`, {
|
|
1162
|
+
method: 'POST',
|
|
1163
|
+
headers: {
|
|
1164
|
+
'Content-Type': 'application/json',
|
|
1165
|
+
},
|
|
1166
|
+
credentials: 'include',
|
|
1167
|
+
body: JSON.stringify({
|
|
1168
|
+
entity_type,
|
|
1169
|
+
entity_name,
|
|
1170
|
+
activity_types,
|
|
1171
|
+
alert_types,
|
|
1172
|
+
}),
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
return {
|
|
1176
|
+
status: response.status,
|
|
1177
|
+
json: await response.json(),
|
|
1178
|
+
};
|
|
1179
|
+
},
|
|
1180
|
+
|
|
1181
|
+
// DELETE /notifications/unsubscribe
|
|
1182
|
+
unsubscribeFromNotifications: async function ({ entity_type, entity_name }) {
|
|
1183
|
+
const url = new URL(`${DJ_URL}/notifications/unsubscribe`);
|
|
1184
|
+
url.searchParams.append('entity_type', entity_type);
|
|
1185
|
+
url.searchParams.append('entity_name', entity_name);
|
|
1186
|
+
|
|
1187
|
+
const response = await fetch(url, {
|
|
1188
|
+
method: 'DELETE',
|
|
1189
|
+
credentials: 'include',
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
return {
|
|
1193
|
+
status: response.status,
|
|
1194
|
+
json: await response.json(),
|
|
1195
|
+
};
|
|
1196
|
+
},
|
|
1140
1197
|
};
|
package/src/styles/index.css
CHANGED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render, fireEvent, screen, waitFor } from '@testing-library/react';
|
|
3
|
-
import ClientCodePopover from '../ClientCodePopover';
|
|
4
|
-
import userEvent from '@testing-library/user-event';
|
|
5
|
-
|
|
6
|
-
describe('<ClientCodePopover />', () => {
|
|
7
|
-
const defaultProps = {
|
|
8
|
-
code: "print('Hello, World!')",
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
it('toggles the code popover visibility when the button is clicked', async () => {
|
|
12
|
-
render(<ClientCodePopover {...defaultProps} />);
|
|
13
|
-
|
|
14
|
-
const button = screen.getByRole('button', 'code-button');
|
|
15
|
-
|
|
16
|
-
// Initially, the popover should be hidden
|
|
17
|
-
expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
|
|
18
|
-
'display: none',
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
// Clicking the button should display the popover
|
|
22
|
-
fireEvent.click(button);
|
|
23
|
-
expect(screen.getByRole('dialog', { hidden: true })).not.toHaveStyle(
|
|
24
|
-
'display: none',
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
// Clicking the button again should hide the popover
|
|
28
|
-
fireEvent.click(button);
|
|
29
|
-
expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
|
|
30
|
-
'display: none',
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
// Trigger onClose by pressing <escape>
|
|
34
|
-
userEvent.keyboard('{Escape}');
|
|
35
|
-
// fireEvent.click(screen.getByTestId('body').firstChild());
|
|
36
|
-
await waitFor(() => {
|
|
37
|
-
expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
|
|
38
|
-
'display: none',
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('renders the provided code within the SyntaxHighlighter', () => {
|
|
44
|
-
render(<ClientCodePopover {...defaultProps} />);
|
|
45
|
-
expect(screen.getByRole('dialog', { hidden: true })).toHaveTextContent(
|
|
46
|
-
defaultProps.code,
|
|
47
|
-
);
|
|
48
|
-
});
|
|
49
|
-
});
|