diff --git a/src/shared/versions.js b/src/shared/versions.js
index 15edc3c..a978a8a 100644
--- a/src/shared/versions.js
+++ b/src/shared/versions.js
@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({
- macClient: '0.17.15',
- gatewayClient: '0.18.15',
+ macClient: '0.17.16',
+ gatewayClient: '0.18.16',
gatewayBackend: '0.18.1',
});
diff --git a/src/web/components/DevicesPanel.jsx b/src/web/components/DevicesPanel.jsx
index 96044be..702258b 100644
--- a/src/web/components/DevicesPanel.jsx
+++ b/src/web/components/DevicesPanel.jsx
@@ -45,6 +45,15 @@ function chartTime(value) {
});
}
+function smoothTrafficPath(points, valueKey) {
+ if (!points.length) return '';
+ return points.slice(1).reduce((path, point, index) => {
+ const previous = points[index];
+ const midX = (previous.x + point.x) / 2;
+ return `${path} C ${midX},${previous[valueKey]} ${midX},${point[valueKey]} ${point.x},${point[valueKey]}`;
+ }, `M ${points[0].x},${points[0][valueKey]}`);
+}
+
function TrafficChart({ samples, scale, capacity, routeLabel, pinned }) {
const [hovered, setHovered] = useState(null);
const previousPoints = useRef([]);
@@ -132,52 +141,44 @@ function TrafficChart({ samples, scale, capacity, routeLabel, pinned }) {
}
- {previous.length > 0 && `${x},${gatewayY}`).join(' ')}>
+ {previous.length > 0 &&
{animateScale && `${x},${gatewayY}`).join(' ')}
- to={previous.map(({ x, gatewayY }) => `${x},${gatewayY}`).join(' ')}
+ attributeName="d"
+ from={smoothTrafficPath(scaleFrom.slice(0, -1), 'gatewayY')}
+ to={smoothTrafficPath(previous, 'gatewayY')}
dur="520ms"
calcMode="spline"
keyTimes="0;1"
keySplines="0.16 1 0.3 1"
fill="freeze"
/>}
- }
- {hasProxy && previous.length > 0 && `${x},${proxyY}`).join(' ')}>
+ }
+ {hasProxy && previous.length > 0 &&
{animateScale && `${x},${proxyY}`).join(' ')}
- to={previous.map(({ x, proxyY }) => `${x},${proxyY}`).join(' ')}
+ attributeName="d"
+ from={smoothTrafficPath(scaleFrom.slice(0, -1), 'proxyY')}
+ to={smoothTrafficPath(previous, 'proxyY')}
dur="520ms"
calcMode="spline"
keyTimes="0;1"
keySplines="0.16 1 0.3 1"
fill="freeze"
/>}
- }
- {penultimate && newest &&
- {animateScale && <>
-
-
- >}
- }
- {hasProxy && penultimate && newest &&
- {animateScale && <>
-
-
- >}
- }
- {!penultimate && newest &&
- {animateScale && }
- }
+ }
+ {penultimate && newest &&
+ {animateScale && }
+ }
+ {hasProxy && penultimate && newest &&
+ {animateScale && }
+ }
+ {!penultimate && newest && }
{hovered &&
-
-
- {hovered.proxy > 0n && }
+
+
+ {hovered.proxy > 0n && }
}
diff --git a/src/web/styles.css b/src/web/styles.css
index 9d815e4..11c4e68 100644
--- a/src/web/styles.css
+++ b/src/web/styles.css
@@ -1451,7 +1451,7 @@ p {
animation: client-device-traffic-shift 560ms cubic-bezier(0.16, 1, 0.3, 1);
}
-.client-device-traffic-lines polyline,
+.client-device-traffic-lines path,
.client-device-traffic-lines line {
fill: none;
stroke: color-mix(in oklch, var(--client-text) 72%, var(--client-muted));
@@ -1465,32 +1465,32 @@ p {
stroke: var(--client-accent);
}
-.client-device-traffic-lines line.is-new {
+.client-device-traffic-lines path.is-new {
stroke-dasharray: 1;
stroke-dashoffset: 1;
animation: client-device-traffic-line-draw 620ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
-.client-device-traffic-lines circle {
- fill: color-mix(in oklch, var(--client-text) 72%, var(--client-muted));
- vector-effect: non-scaling-stroke;
+.client-device-traffic-lines .is-point {
+ stroke-width: 3;
+ stroke-linecap: round;
}
-.client-device-traffic-cursor line {
+.client-device-traffic-cursor .is-guide {
stroke: color-mix(in oklch, var(--client-text) 38%, transparent);
stroke-width: 1;
stroke-dasharray: 2 3;
vector-effect: non-scaling-stroke;
}
-.client-device-traffic-cursor circle {
- fill: var(--client-bg);
+.client-device-traffic-cursor .is-point {
stroke: color-mix(in oklch, var(--client-text) 82%, var(--client-muted));
- stroke-width: 1.5;
+ stroke-width: 4;
+ stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
-.client-device-traffic-cursor circle.is-proxy {
+.client-device-traffic-cursor .is-point.is-proxy {
stroke: var(--client-accent);
}
diff --git a/test/web/device-inventory-contract.test.js b/test/web/device-inventory-contract.test.js
index 7cbf9de..900d88a 100644
--- a/test/web/device-inventory-contract.test.js
+++ b/test/web/device-inventory-contract.test.js
@@ -75,7 +75,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(panel, /TrafficChart[\s\S]*samples=\{device\.trafficHistory \|\| \[\]\}[\s\S]*scale=\{trafficScale\}[\s\S]*capacity=\{snapshot\.trafficHistoryCapacity/);
assert.doesNotMatch(panel, /setTrafficHistory|TRAFFIC_HISTORY_LIMIT/);
assert.match(panel, /aria-pressed=\{trafficScale === 'linear'\}[\s\S]*aria-pressed=\{trafficScale === 'log'\}/);
- assert.match(panel, /previousScale\.current !== scale[\s\S]*attributeName="points"[\s\S]*dur="520ms"/);
+ assert.match(panel, /previousScale\.current !== scale[\s\S]*attributeName="d"[\s\S]*dur="520ms"/);
+ assert.match(panel, /function smoothTrafficPath[\s\S]*const midX = \(previous\.x \+ point\.x\) \/ 2[\s\S]* C /);
assert.match(panel, /TRAFFIC_CHART_HEADROOM = 10[\s\S]*trafficChartY = \(ratio\) => 100 - ratio \* \(100 - TRAFFIC_CHART_HEADROOM\)/);
assert.match(panel, /gatewayY: trafficChartY\(trafficScaleRatio\(gateway, max, scale\)\)[\s\S]*proxyY: trafficChartY\(trafficScaleRatio\(proxy, max, scale\)\)/);
assert.match(panel, /client-device-traffic-grid[\s\S]*y1=\{TRAFFIC_CHART_HEADROOM\}[\s\S]*y1=\{\(100 \+ TRAFFIC_CHART_HEADROOM\) \/ 2\}/);
@@ -84,7 +85,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(panel, /createPortal\([\s\S]*client-device-traffic-point-tooltip[\s\S]*document\.body/);
assert.match(panel, /onPointerMove=\{trackPointer\}/);
assert.match(panel, /pinned && max > 0n && 0n && Proxy \{formatByteString\(hovered\.proxy\)\}<\/span>\}/);
assert.match(panel, /