Smooth traffic chart paths and update version
Build and Deploy Gateway / build-and-push (push) Successful in 13s
Build and Deploy Gateway / deploy (push) Successful in 6s

This commit is contained in:
2026-08-07 23:02:18 +03:00
parent b084d7e42c
commit 39f3467f9b
4 changed files with 47 additions and 43 deletions
+2 -2
View File
@@ -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',
});
+29 -28
View File
@@ -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 }) {
<line x1="0" x2="100" y1="100" y2="100" />
</g>}
<g className="client-device-traffic-lines" style={{ '--sample-count': Math.max(1, capacity) }}>
{previous.length > 0 && <polyline className="is-gateway" points={previous.map(({ x, gatewayY }) => `${x},${gatewayY}`).join(' ')}>
{previous.length > 0 && <path className="is-gateway" d={smoothTrafficPath(previous, 'gatewayY')}>
{animateScale && <animate
key={`gateway-${scale}`}
attributeName="points"
from={scaleFrom.slice(0, -1).map(({ x, gatewayY }) => `${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"
/>}
</polyline>}
{hasProxy && previous.length > 0 && <polyline className="is-proxy" points={previous.map(({ x, proxyY }) => `${x},${proxyY}`).join(' ')}>
</path>}
{hasProxy && previous.length > 0 && <path className="is-proxy" d={smoothTrafficPath(previous, 'proxyY')}>
{animateScale && <animate
key={`proxy-${scale}`}
attributeName="points"
from={scaleFrom.slice(0, -1).map(({ x, proxyY }) => `${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"
/>}
</polyline>}
{penultimate && newest && <line className="is-gateway is-new" pathLength="1" x1={penultimate.x} y1={penultimate.gatewayY} x2={newest.x} y2={newest.gatewayY}>
{animateScale && <>
<animate key={`gateway-y1-${scale}`} attributeName="y1" from={scaleFrom.at(-2).gatewayY} to={penultimate.gatewayY} dur="520ms" fill="freeze" />
<animate key={`gateway-y2-${scale}`} attributeName="y2" from={scaleFrom.at(-1).gatewayY} to={newest.gatewayY} dur="520ms" fill="freeze" />
</>}
</line>}
{hasProxy && penultimate && newest && <line className="is-proxy is-new" pathLength="1" x1={penultimate.x} y1={penultimate.proxyY} x2={newest.x} y2={newest.proxyY}>
{animateScale && <>
<animate key={`proxy-y1-${scale}`} attributeName="y1" from={scaleFrom.at(-2).proxyY} to={penultimate.proxyY} dur="520ms" fill="freeze" />
<animate key={`proxy-y2-${scale}`} attributeName="y2" from={scaleFrom.at(-1).proxyY} to={newest.proxyY} dur="520ms" fill="freeze" />
</>}
</line>}
{!penultimate && newest && <circle className="is-gateway is-new" cx={newest.x} cy={newest.gatewayY} r="1.5">
{animateScale && <animate key={`gateway-cy-${scale}`} attributeName="cy" from={scaleFrom[0].gatewayY} to={newest.gatewayY} dur="520ms" fill="freeze" />}
</circle>}
</path>}
{penultimate && newest && <path className="is-gateway is-new" pathLength="1" d={smoothTrafficPath([penultimate, newest], 'gatewayY')}>
{animateScale && <animate key={`gateway-new-${scale}`} attributeName="d" from={smoothTrafficPath(scaleFrom.slice(-2), 'gatewayY')} to={smoothTrafficPath([penultimate, newest], 'gatewayY')} dur="520ms" fill="freeze" />}
</path>}
{hasProxy && penultimate && newest && <path className="is-proxy is-new" pathLength="1" d={smoothTrafficPath([penultimate, newest], 'proxyY')}>
{animateScale && <animate key={`proxy-new-${scale}`} attributeName="d" from={smoothTrafficPath(scaleFrom.slice(-2), 'proxyY')} to={smoothTrafficPath([penultimate, newest], 'proxyY')} dur="520ms" fill="freeze" />}
</path>}
{!penultimate && newest && <line className="is-gateway is-point" x1={newest.x} x2={newest.x} y1={newest.gatewayY} y2={newest.gatewayY} />}
</g>
{hovered && <g className="client-device-traffic-cursor">
<line x1={hovered.x} x2={hovered.x} y1={TRAFFIC_CHART_HEADROOM} y2="100" />
<circle className="is-gateway" cx={hovered.x} cy={hovered.gatewayY} r="2" />
{hovered.proxy > 0n && <circle className="is-proxy" cx={hovered.x} cy={hovered.proxyY} r="2" />}
<line className="is-guide" x1={hovered.x} x2={hovered.x} y1={TRAFFIC_CHART_HEADROOM} y2="100" />
<line className="is-point is-gateway" x1={hovered.x} x2={hovered.x} y1={hovered.gatewayY} y2={hovered.gatewayY} />
{hovered.proxy > 0n && <line className="is-point is-proxy" x1={hovered.x} x2={hovered.x} y1={hovered.proxyY} y2={hovered.proxyY} />}
</g>}
</svg>
</span>
+10 -10
View File
@@ -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);
}
+6 -3
View File
@@ -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 && <span className="client-device-traffic-axis"[\s\S]*is-max[\s\S]*is-mid[\s\S]*is-zero/);
assert.match(panel, /client-device-traffic-grid[\s\S]*polyline className="is-gateway"[\s\S]*polyline className="is-proxy"/);
assert.match(panel, /client-device-traffic-grid[\s\S]*path className="is-gateway"[\s\S]*path className="is-proxy"/);
assert.match(panel, /client-device-traffic-cursor[\s\S]*className="is-point is-gateway"[\s\S]*className="is-point is-proxy"/);
assert.match(panel, /routeLabel=\{device\.appliedPolicy === 'direct' \? 'Напрямую' : 'Gateway'\}/);
assert.match(panel, /\{hovered\.proxy > 0n && <span className="is-proxy">Proxy \{formatByteString\(hovered\.proxy\)\}<\/span>\}/);
assert.match(panel, /<time dateTime=\{samples\[0\]\.observedAt\}>[\s\S]*<span>15 с<\/span>/);
@@ -117,7 +119,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(styles, /\.client-device-traffic-plot \{[\s\S]*grid-column: 2;/);
assert.match(styles, /\.client-device-traffic-time \{[\s\S]*grid-column: 2;/);
assert.match(styles, /\.client-device-traffic-grid line \{[\s\S]*vector-effect: non-scaling-stroke/);
assert.match(styles, /\.client-device-traffic-lines polyline,[\s\S]*stroke-width: 1\.8/);
assert.match(styles, /\.client-device-traffic-lines path,[\s\S]*stroke-width: 1\.8/);
assert.match(styles, /\.client-device-traffic-cursor \.is-point \{[\s\S]*stroke-width: 4;[\s\S]*stroke-linecap: round;[\s\S]*vector-effect: non-scaling-stroke/);
assert.match(styles, /\.client-device-traffic-axis > \.is-max \{[\s\S]*top: var\(--traffic-chart-top\)/);
assert.match(styles, /\.client-device-traffic-axis > \.is-mid \{[\s\S]*top: var\(--traffic-chart-mid\)/);
assert.match(styles, /\.client-device-traffic-point-tooltip \{[\s\S]*position: fixed;[\s\S]*z-index: 1002/);