Skip to content

Commit e25ea04

Browse files
Merge branch 'dev' into unittest-plots
2 parents ca6ac4f + 5d132bd commit e25ea04

13 files changed

Lines changed: 862 additions & 28 deletions

File tree

js/main.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import {
3636
ROW_HEIGHT,
3737
} from './settings';
3838
import buildExportHtml from './template/exportTemplate';
39+
import ToastContainer from './toasts/ToastContainer';
40+
import { showToast } from './toasts/toastEvents';
3941
import ConnectionIndicator from './topbar/ConnectionIndicator';
4042
import EnvControls from './topbar/EnvControls';
4143
import FilterControls from './topbar/FilterControls';
@@ -876,7 +878,7 @@ const App = () => {
876878
};
877879
const exportCurrentEnvToHtml = () => {
878880
if (!storeData.panes || Object.keys(storeData.panes).length === 0) {
879-
alert('No panes available to export.');
881+
showToast('No panes available to export.', 'error', { duration: 4000 });
880882
return;
881883
}
882884

@@ -1034,6 +1036,7 @@ const App = () => {
10341036

10351037
return (
10361038
<div>
1039+
<ToastContainer />
10371040
{modals}
10381041
<div className="navbar-form navbar-default">
10391042
<span className="navbar-brand visdom-title">visdom</span>

js/toasts/Toast.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Copyright 2017-present, The Visdom Authors
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
*/
9+
10+
import React, { useCallback, useEffect, useRef, useState } from 'react';
11+
12+
const EXIT_ANIMATION_MS = 200;
13+
14+
const Toast = ({
15+
message,
16+
type = 'info',
17+
duration = 4000,
18+
shape = 'rect',
19+
onDismiss,
20+
}) => {
21+
const [isLeaving, setIsLeaving] = useState(false);
22+
const isLeavingRef = useRef(false);
23+
const dismissTimerRef = useRef(null);
24+
const exitTimerRef = useRef(null);
25+
const onDismissRef = useRef(onDismiss);
26+
27+
useEffect(() => {
28+
onDismissRef.current = onDismiss;
29+
}, [onDismiss]);
30+
31+
const startExit = useCallback(() => {
32+
if (isLeavingRef.current) return;
33+
isLeavingRef.current = true;
34+
setIsLeaving(true);
35+
exitTimerRef.current = setTimeout(
36+
() => onDismissRef.current(),
37+
EXIT_ANIMATION_MS
38+
);
39+
}, []);
40+
41+
useEffect(() => {
42+
if (duration > 0) {
43+
dismissTimerRef.current = setTimeout(startExit, duration);
44+
}
45+
return () => {
46+
clearTimeout(dismissTimerRef.current);
47+
clearTimeout(exitTimerRef.current);
48+
};
49+
}, [duration, startExit]);
50+
51+
const isPill = shape === 'pill';
52+
53+
const className = [
54+
'visdom-toast',
55+
`visdom-toast-${type}`,
56+
isPill ? 'visdom-toast-pill' : '',
57+
isLeaving ? 'visdom-toast-leaving' : '',
58+
]
59+
.filter(Boolean)
60+
.join(' ');
61+
62+
return (
63+
<div className={className} role="alert">
64+
<span className="visdom-toast-message">{message}</span>
65+
66+
{!isPill && (
67+
<button
68+
aria-label="Dismiss notification"
69+
className="visdom-toast-close"
70+
onClick={startExit}
71+
type="button"
72+
>
73+
&times;
74+
</button>
75+
)}
76+
77+
{!isPill && duration > 0 && !isLeaving && (
78+
<div
79+
className="visdom-toast-progress"
80+
style={{ animationDuration: `${duration}ms` }}
81+
/>
82+
)}
83+
</div>
84+
);
85+
};
86+
87+
export default Toast;

js/toasts/ToastContainer.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Copyright 2017-present, The Visdom Authors
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
*/
9+
10+
import './toast.less';
11+
12+
import React, { useCallback, useEffect, useState } from 'react';
13+
14+
import EventSystem from '../EventSystem';
15+
import Toast from './Toast';
16+
17+
const POSITIONS = [
18+
'top-left',
19+
'top-center',
20+
'top-right',
21+
'bottom-left',
22+
'bottom-center',
23+
'bottom-right',
24+
];
25+
26+
const ToastContainer = () => {
27+
const [toasts, setToasts] = useState([]);
28+
29+
const removeToast = useCallback((id) => {
30+
setToasts((prev) => prev.filter((t) => t.id !== id));
31+
}, []);
32+
33+
useEffect(() => {
34+
const handleToast = (toast) => {
35+
setToasts((prev) => [...prev, toast]);
36+
};
37+
const handleDismiss = ({ id }) => {
38+
removeToast(id);
39+
};
40+
41+
EventSystem.subscribe('toast', handleToast);
42+
EventSystem.subscribe('toast-dismiss', handleDismiss);
43+
44+
return () => {
45+
EventSystem.unsubscribe('toast', handleToast);
46+
EventSystem.unsubscribe('toast-dismiss', handleDismiss);
47+
};
48+
}, [removeToast]);
49+
50+
if (toasts.length === 0) {
51+
return null;
52+
}
53+
54+
return (
55+
<>
56+
{POSITIONS.map((position) => {
57+
const positionToasts = toasts.filter(
58+
(t) => (t.position || 'top-right') === position
59+
);
60+
if (positionToasts.length === 0) return null;
61+
62+
return (
63+
<div
64+
className={`visdom-toast-container
65+
visdom-toast-container-${position}`
66+
}
67+
key={position}
68+
>
69+
{positionToasts.map((toast) => (
70+
<Toast
71+
duration={toast.duration}
72+
key={toast.id}
73+
message={toast.message}
74+
onDismiss={() => removeToast(toast.id)}
75+
shape={toast.shape}
76+
type={toast.type}
77+
/>
78+
))}
79+
</div>
80+
);
81+
})}
82+
</>
83+
);
84+
};
85+
86+
export default ToastContainer;

js/toasts/toast.less

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
.visdom-toast-container {
2+
position: fixed;
3+
z-index: 2000;
4+
display: flex;
5+
flex-direction: column;
6+
gap: 8px;
7+
max-width: 360px;
8+
pointer-events: none;
9+
}
10+
11+
.visdom-toast-container-top-left {
12+
top: 70px;
13+
left: 16px;
14+
align-items: flex-start;
15+
}
16+
17+
.visdom-toast-container-top-center {
18+
top: 70px;
19+
left: 50%;
20+
align-items: center;
21+
transform: translateX(-50%);
22+
}
23+
24+
.visdom-toast-container-top-right {
25+
top: 70px;
26+
right: 16px;
27+
align-items: flex-end;
28+
}
29+
30+
.visdom-toast-container-bottom-left {
31+
bottom: 16px;
32+
left: 16px;
33+
flex-direction: column-reverse;
34+
align-items: flex-start;
35+
}
36+
37+
.visdom-toast-container-bottom-center {
38+
bottom: 16px;
39+
left: 50%;
40+
flex-direction: column-reverse;
41+
align-items: center;
42+
transform: translateX(-50%);
43+
}
44+
45+
.visdom-toast-container-bottom-right {
46+
bottom: 16px;
47+
right: 16px;
48+
flex-direction: column-reverse;
49+
align-items: flex-end;
50+
}
51+
52+
.visdom-toast {
53+
position: relative;
54+
display: flex;
55+
align-items: center;
56+
justify-content: space-between;
57+
gap: 12px;
58+
min-width: 240px;
59+
padding: 10px 14px;
60+
overflow: hidden;
61+
border-radius: 4px;
62+
color: #fff;
63+
font-size: 13px;
64+
line-height: 1.4;
65+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
66+
pointer-events: auto;
67+
animation: visdom-toast-enter 0.2s ease-out;
68+
}
69+
70+
.visdom-toast-leaving {
71+
animation: visdom-toast-exit 0.2s ease-in forwards;
72+
}
73+
74+
.visdom-toast-message {
75+
flex: 1;
76+
word-break: break-word;
77+
}
78+
79+
.visdom-toast-close {
80+
background: transparent;
81+
border: none;
82+
color: inherit;
83+
cursor: pointer;
84+
font-size: 16px;
85+
line-height: 1;
86+
opacity: 0.8;
87+
padding: 0;
88+
89+
&:hover {
90+
opacity: 1;
91+
}
92+
}
93+
94+
.visdom-toast-progress {
95+
position: absolute;
96+
bottom: 0;
97+
left: 0;
98+
height: 3px;
99+
background: rgba(255, 255, 255, 0.7);
100+
animation: visdom-toast-progress-shrink linear forwards;
101+
}
102+
103+
.visdom-toast-pill {
104+
min-width: 0;
105+
padding: 8px 20px;
106+
justify-content: center;
107+
border-radius: 999px;
108+
font-weight: 500;
109+
text-align: center;
110+
}
111+
112+
.visdom-toast-pill.visdom-toast-success {
113+
background-color: #fff;
114+
color: #2a2a2a;
115+
border: 1px solid #e0e0e0;
116+
}
117+
118+
.visdom-toast-info {
119+
background-color: #337ab7;
120+
}
121+
122+
.visdom-toast-success {
123+
background-color: #5cb85c;
124+
}
125+
126+
.visdom-toast-warning {
127+
background-color: #f0ad4e;
128+
}
129+
130+
.visdom-toast-error {
131+
background-color: #d9534f;
132+
}
133+
134+
@keyframes visdom-toast-enter {
135+
from {
136+
opacity: 0;
137+
transform: scale(0.9);
138+
}
139+
to {
140+
opacity: 1;
141+
transform: scale(1);
142+
}
143+
}
144+
145+
@keyframes visdom-toast-exit {
146+
from {
147+
opacity: 1;
148+
transform: scale(1);
149+
}
150+
to {
151+
opacity: 0;
152+
transform: scale(0.9);
153+
}
154+
}
155+
156+
@keyframes visdom-toast-progress-shrink {
157+
from {
158+
width: 100%;
159+
}
160+
to {
161+
width: 0%;
162+
}
163+
}

0 commit comments

Comments
 (0)