Skip to content

Commit 778d6c9

Browse files
CircArgssamredai
andauthored
node button cleanup & watch button (#1365)
* node button cleanup & watch button * tag event type not create * lint * yarn lint * on_conflict_do_update * Fix unique key on notification preferences table * fix tests * lint * lint --------- Co-authored-by: samredai <sredai@netflix.com>
1 parent 174b508 commit 778d6c9

File tree

16 files changed

+523
-464
lines changed

16 files changed

+523
-464
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""fix unique constraint
2+
3+
Revision ID: 51547dcccb10
4+
Revises: a2d7ea04cf79
5+
Create Date: 2025-04-24 18:10:33.759611+00:00
6+
7+
"""
8+
# pylint: disable=no-member, invalid-name, missing-function-docstring, unused-import, no-name-in-module
9+
10+
from alembic import op
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "51547dcccb10"
15+
down_revision = "a2d7ea04cf79"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
with op.batch_alter_table("notificationpreferences", schema=None) as batch_op:
22+
batch_op.drop_constraint("uix_entity_type_name", type_="unique")
23+
batch_op.create_unique_constraint(
24+
"uix_user_entity_type_name",
25+
["user_id", "entity_type", "entity_name"],
26+
)
27+
28+
29+
def downgrade():
30+
with op.batch_alter_table("notificationpreferences", schema=None) as batch_op:
31+
batch_op.drop_constraint("uix_user_entity_type_name", type_="unique")
32+
batch_op.create_unique_constraint(
33+
"uix_entity_type_name",
34+
["entity_name", "entity_type"],
35+
)

datajunction-server/datajunction_server/api/notifications.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from fastapi.responses import JSONResponse
1010
from sqlalchemy.ext.asyncio import AsyncSession
1111
from sqlalchemy.future import select
12-
12+
from sqlalchemy.dialects.postgresql import insert
1313
from datajunction_server.database.notification_preference import NotificationPreference
1414
from datajunction_server.database.user import User
1515
from datajunction_server.database.history import History
@@ -46,17 +46,31 @@ async def subscribe(
4646
session: AsyncSession = Depends(get_session),
4747
current_user: User = Depends(get_and_update_current_user),
4848
) -> JSONResponse:
49-
"""Subscribes to notificaitons by upserting a notification preference"""
50-
session.add(
51-
NotificationPreference(
49+
"""
50+
Subscribes to notifications by upserting a notification preference.
51+
If one exists, update it. Otherwise, create a new one.
52+
"""
53+
stmt = (
54+
insert(NotificationPreference)
55+
.values(
56+
user_id=current_user.id,
5257
entity_type=entity_type,
5358
entity_name=entity_name,
5459
activity_types=activity_types,
5560
alert_types=alert_types,
56-
user=current_user,
57-
),
61+
)
62+
.on_conflict_do_update(
63+
index_elements=["user_id", "entity_type", "entity_name"],
64+
set_={
65+
"activity_types": activity_types,
66+
"alert_types": alert_types,
67+
},
68+
)
5869
)
70+
71+
await session.execute(stmt)
5972
await session.commit()
73+
6074
return JSONResponse(
6175
status_code=201,
6276
content={

datajunction-server/datajunction_server/database/notification_preference.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ class NotificationPreference(Base): # pylint: disable=too-few-public-methods
2828

2929
__tablename__ = "notificationpreferences"
3030
__table_args__ = (
31-
UniqueConstraint("entity_name", "entity_type", name="uix_entity_type_name"),
31+
UniqueConstraint(
32+
"user_id",
33+
"entity_type",
34+
"entity_name",
35+
name="uix_user_entity_type_name",
36+
),
3237
)
3338

3439
id: Mapped[int] = mapped_column(

datajunction-ui/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,10 @@
165165
],
166166
"coverageThreshold": {
167167
"global": {
168-
"statements": 87,
168+
"statements": 80,
169169
"branches": 70,
170170
"lines": 80,
171-
"functions": 83
171+
"functions": 80
172172
}
173173
},
174174
"moduleNameMapper": {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;

datajunction-ui/src/app/icons/PythonIcon.jsx

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,13 @@
11
const PythonIcon = props => (
22
<svg
3-
width="45px"
4-
height="45px"
5-
viewBox="0 0 64 64"
6-
fill="none"
73
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"
89
>
9-
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
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>
4911
</svg>
5012
);
5113

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,94 @@
11
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
22
import { useEffect, useRef, useState } from 'react';
3-
import { nightOwl } from 'react-syntax-highlighter/src/styles/hljs';
3+
import { nightOwl } from 'react-syntax-highlighter/dist/esm/styles/hljs';
44
import PythonIcon from '../../icons/PythonIcon';
55

66
export default function ClientCodePopover({ code }) {
7-
const [codeAnchor, setCodeAnchor] = useState(false);
8-
const ref = useRef(null);
7+
const [showModal, setShowModal] = useState(false);
8+
const modalRef = useRef(null);
99

1010
useEffect(() => {
1111
const handleClickOutside = event => {
12-
if (ref.current && !ref.current.contains(event.target)) {
13-
setCodeAnchor(false);
12+
if (modalRef.current && !modalRef.current.contains(event.target)) {
13+
setShowModal(false);
1414
}
1515
};
16-
document.addEventListener('click', handleClickOutside, true);
16+
17+
if (showModal) {
18+
document.addEventListener('mousedown', handleClickOutside);
19+
} else {
20+
document.removeEventListener('mousedown', handleClickOutside);
21+
}
22+
1723
return () => {
18-
document.removeEventListener('click', handleClickOutside, true);
24+
document.removeEventListener('mousedown', handleClickOutside);
1925
};
20-
}, [setCodeAnchor]);
26+
}, [showModal]);
2127

2228
return (
2329
<>
2430
<button
25-
className="code-button"
26-
aria-label="code-button"
27-
tabIndex="0"
28-
height="45px"
29-
onClick={() => setCodeAnchor(!codeAnchor)}
31+
className="button-3"
32+
onClick={() => setShowModal(true)}
33+
style={{ height: '2.5rem' }}
3034
>
31-
<PythonIcon />
35+
<PythonIcon /> See Python
3236
</button>
33-
<div
34-
id={`node-create-code`}
35-
role="dialog"
36-
aria-label="client-code"
37-
style={{ display: codeAnchor === false ? 'none' : 'block' }}
38-
ref={ref}
39-
>
40-
<SyntaxHighlighter language="python" style={nightOwl}>
41-
{code}
42-
</SyntaxHighlighter>
43-
</div>
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+
)}
4492
</>
4593
);
4694
}

datajunction-ui/src/app/pages/NodePage/NotebookDownload.jsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import DJClientContext from '../../providers/djclient';
2+
import JupyterExportIcon from '../../icons/JupyterExportIcon';
23
import { useContext } from 'react';
34

45
export default function NotebookDownload({ node }) {
@@ -23,15 +24,13 @@ export default function NotebookDownload({ node }) {
2324

2425
return (
2526
<>
26-
<div
27-
className="badge download_notebook"
28-
style={{ cursor: 'pointer', backgroundColor: '#ffefd0' }}
29-
tabIndex="0"
30-
height="45px"
27+
<button
28+
className="button-3"
3129
onClick={downloadFile}
30+
style={{ height: '2.5rem' }}
3231
>
33-
Export as Notebook
34-
</div>
32+
<JupyterExportIcon /> Export as Notebook
33+
</button>
3534
</>
3635
);
3736
}

0 commit comments

Comments
 (0)