Smooth traffic chart paths and update version
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.17.15',
|
macClient: '0.17.16',
|
||||||
gatewayClient: '0.18.15',
|
gatewayClient: '0.18.16',
|
||||||
gatewayBackend: '0.18.1',
|
gatewayBackend: '0.18.1',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 }) {
|
function TrafficChart({ samples, scale, capacity, routeLabel, pinned }) {
|
||||||
const [hovered, setHovered] = useState(null);
|
const [hovered, setHovered] = useState(null);
|
||||||
const previousPoints = useRef([]);
|
const previousPoints = useRef([]);
|
||||||
@@ -132,52 +141,44 @@ function TrafficChart({ samples, scale, capacity, routeLabel, pinned }) {
|
|||||||
<line x1="0" x2="100" y1="100" y2="100" />
|
<line x1="0" x2="100" y1="100" y2="100" />
|
||||||
</g>}
|
</g>}
|
||||||
<g className="client-device-traffic-lines" style={{ '--sample-count': Math.max(1, capacity) }}>
|
<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
|
{animateScale && <animate
|
||||||
key={`gateway-${scale}`}
|
key={`gateway-${scale}`}
|
||||||
attributeName="points"
|
attributeName="d"
|
||||||
from={scaleFrom.slice(0, -1).map(({ x, gatewayY }) => `${x},${gatewayY}`).join(' ')}
|
from={smoothTrafficPath(scaleFrom.slice(0, -1), 'gatewayY')}
|
||||||
to={previous.map(({ x, gatewayY }) => `${x},${gatewayY}`).join(' ')}
|
to={smoothTrafficPath(previous, 'gatewayY')}
|
||||||
dur="520ms"
|
dur="520ms"
|
||||||
calcMode="spline"
|
calcMode="spline"
|
||||||
keyTimes="0;1"
|
keyTimes="0;1"
|
||||||
keySplines="0.16 1 0.3 1"
|
keySplines="0.16 1 0.3 1"
|
||||||
fill="freeze"
|
fill="freeze"
|
||||||
/>}
|
/>}
|
||||||
</polyline>}
|
</path>}
|
||||||
{hasProxy && previous.length > 0 && <polyline className="is-proxy" points={previous.map(({ x, proxyY }) => `${x},${proxyY}`).join(' ')}>
|
{hasProxy && previous.length > 0 && <path className="is-proxy" d={smoothTrafficPath(previous, 'proxyY')}>
|
||||||
{animateScale && <animate
|
{animateScale && <animate
|
||||||
key={`proxy-${scale}`}
|
key={`proxy-${scale}`}
|
||||||
attributeName="points"
|
attributeName="d"
|
||||||
from={scaleFrom.slice(0, -1).map(({ x, proxyY }) => `${x},${proxyY}`).join(' ')}
|
from={smoothTrafficPath(scaleFrom.slice(0, -1), 'proxyY')}
|
||||||
to={previous.map(({ x, proxyY }) => `${x},${proxyY}`).join(' ')}
|
to={smoothTrafficPath(previous, 'proxyY')}
|
||||||
dur="520ms"
|
dur="520ms"
|
||||||
calcMode="spline"
|
calcMode="spline"
|
||||||
keyTimes="0;1"
|
keyTimes="0;1"
|
||||||
keySplines="0.16 1 0.3 1"
|
keySplines="0.16 1 0.3 1"
|
||||||
fill="freeze"
|
fill="freeze"
|
||||||
/>}
|
/>}
|
||||||
</polyline>}
|
</path>}
|
||||||
{penultimate && newest && <line className="is-gateway is-new" pathLength="1" x1={penultimate.x} y1={penultimate.gatewayY} x2={newest.x} y2={newest.gatewayY}>
|
{penultimate && newest && <path className="is-gateway is-new" pathLength="1" d={smoothTrafficPath([penultimate, newest], 'gatewayY')}>
|
||||||
{animateScale && <>
|
{animateScale && <animate key={`gateway-new-${scale}`} attributeName="d" from={smoothTrafficPath(scaleFrom.slice(-2), 'gatewayY')} to={smoothTrafficPath([penultimate, newest], 'gatewayY')} dur="520ms" fill="freeze" />}
|
||||||
<animate key={`gateway-y1-${scale}`} attributeName="y1" from={scaleFrom.at(-2).gatewayY} to={penultimate.gatewayY} dur="520ms" fill="freeze" />
|
</path>}
|
||||||
<animate key={`gateway-y2-${scale}`} attributeName="y2" from={scaleFrom.at(-1).gatewayY} to={newest.gatewayY} dur="520ms" fill="freeze" />
|
{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" />}
|
||||||
</line>}
|
</path>}
|
||||||
{hasProxy && penultimate && newest && <line className="is-proxy is-new" pathLength="1" x1={penultimate.x} y1={penultimate.proxyY} x2={newest.x} y2={newest.proxyY}>
|
{!penultimate && newest && <line className="is-gateway is-point" x1={newest.x} x2={newest.x} y1={newest.gatewayY} y2={newest.gatewayY} />}
|
||||||
{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>}
|
|
||||||
</g>
|
</g>
|
||||||
{hovered && <g className="client-device-traffic-cursor">
|
{hovered && <g className="client-device-traffic-cursor">
|
||||||
<line x1={hovered.x} x2={hovered.x} y1={TRAFFIC_CHART_HEADROOM} y2="100" />
|
<line className="is-guide" x1={hovered.x} x2={hovered.x} y1={TRAFFIC_CHART_HEADROOM} y2="100" />
|
||||||
<circle className="is-gateway" cx={hovered.x} cy={hovered.gatewayY} r="2" />
|
<line className="is-point is-gateway" x1={hovered.x} x2={hovered.x} y1={hovered.gatewayY} y2={hovered.gatewayY} />
|
||||||
{hovered.proxy > 0n && <circle className="is-proxy" cx={hovered.x} cy={hovered.proxyY} r="2" />}
|
{hovered.proxy > 0n && <line className="is-point is-proxy" x1={hovered.x} x2={hovered.x} y1={hovered.proxyY} y2={hovered.proxyY} />}
|
||||||
</g>}
|
</g>}
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+10
-10
@@ -1451,7 +1451,7 @@ p {
|
|||||||
animation: client-device-traffic-shift 560ms cubic-bezier(0.16, 1, 0.3, 1);
|
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 {
|
.client-device-traffic-lines line {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: color-mix(in oklch, var(--client-text) 72%, var(--client-muted));
|
stroke: color-mix(in oklch, var(--client-text) 72%, var(--client-muted));
|
||||||
@@ -1465,32 +1465,32 @@ p {
|
|||||||
stroke: var(--client-accent);
|
stroke: var(--client-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-lines line.is-new {
|
.client-device-traffic-lines path.is-new {
|
||||||
stroke-dasharray: 1;
|
stroke-dasharray: 1;
|
||||||
stroke-dashoffset: 1;
|
stroke-dashoffset: 1;
|
||||||
animation: client-device-traffic-line-draw 620ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
animation: client-device-traffic-line-draw 620ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-lines circle {
|
.client-device-traffic-lines .is-point {
|
||||||
fill: color-mix(in oklch, var(--client-text) 72%, var(--client-muted));
|
stroke-width: 3;
|
||||||
vector-effect: non-scaling-stroke;
|
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: color-mix(in oklch, var(--client-text) 38%, transparent);
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
stroke-dasharray: 2 3;
|
stroke-dasharray: 2 3;
|
||||||
vector-effect: non-scaling-stroke;
|
vector-effect: non-scaling-stroke;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-cursor circle {
|
.client-device-traffic-cursor .is-point {
|
||||||
fill: var(--client-bg);
|
|
||||||
stroke: color-mix(in oklch, var(--client-text) 82%, var(--client-muted));
|
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;
|
vector-effect: non-scaling-stroke;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-device-traffic-cursor circle.is-proxy {
|
.client-device-traffic-cursor .is-point.is-proxy {
|
||||||
stroke: var(--client-accent);
|
stroke: var(--client-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.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.doesNotMatch(panel, /setTrafficHistory|TRAFFIC_HISTORY_LIMIT/);
|
||||||
assert.match(panel, /aria-pressed=\{trafficScale === 'linear'\}[\s\S]*aria-pressed=\{trafficScale === 'log'\}/);
|
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, /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, /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\}/);
|
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, /createPortal\([\s\S]*client-device-traffic-point-tooltip[\s\S]*document\.body/);
|
||||||
assert.match(panel, /onPointerMove=\{trackPointer\}/);
|
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, /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, /routeLabel=\{device\.appliedPolicy === 'direct' \? 'Напрямую' : 'Gateway'\}/);
|
||||||
assert.match(panel, /\{hovered\.proxy > 0n && <span className="is-proxy">Proxy \{formatByteString\(hovered\.proxy\)\}<\/span>\}/);
|
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>/);
|
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-plot \{[\s\S]*grid-column: 2;/);
|
||||||
assert.match(styles, /\.client-device-traffic-time \{[\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-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-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-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/);
|
assert.match(styles, /\.client-device-traffic-point-tooltip \{[\s\S]*position: fixed;[\s\S]*z-index: 1002/);
|
||||||
|
|||||||
Reference in New Issue
Block a user