mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Refactor(status-tone): consolidate 4 toneOf implementations into ipFieldTone
ConnectivityTest / WebRtcTest / DnsLeaksTest / RuleTest each reimplemented
the same "status string → wait / ok-fast / ok-slow / fail" branching, with
the first three being near-byte-identical and ConnectivityTest adding a
time-threshold twist.
Export a single ipFieldTone(value, opts) from use-status-tone.js that
covers all four shapes via optional { waitLabels, errorLabels, isSuccess,
time, fastMs }. The three IP-content call sites collapse to a 4-line call;
ConnectivityTest passes a custom isSuccess predicate to preserve its
"Available" label + time split semantics.
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
7 files changed
+151
-29
No files matched your search
@@ -101,6 +101,8 @@ Badge adds `success` to the defaults. Badge hover is globally disabled — Badge
|
||||
|
||||
All "business state → visual color" mapping goes through `frontend/composables/use-status-tone.js`. It exposes four tones: `wait` / `ok-fast` / `ok-slow` / `fail`.
|
||||
|
||||
For the common "status string → tone" shape (WebRTC / DnsLeak / RuleTest / Connectivity style), use the `ipFieldTone(value, { waitLabels, errorLabels, isSuccess?, time?, fastMs? })` helper from the same module rather than hand-rolling the switch locally. Default `isSuccess` treats any string containing `.` or `:` as a successful IP-shaped payload; pass a custom predicate when the success signal is elsewhere (Connectivity uses the localized "Available" label).
|
||||
|
||||
Rule: any new module that surfaces a status reuses these tones. Do not hand-write `switch` statements that map states to hex codes or Tailwind classes.
|
||||
|
||||
### Canonical patterns
|
||||
|
||||
@@ -126,7 +126,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useStatusTone } from '@/composables/use-status-tone.js';
|
||||
import { useStatusTone, ipFieldTone } from '@/composables/use-status-tone.js';
|
||||
import {
|
||||
Play, Chrome, Cloud, Compass, CirclePlus, Frown, Github, Meh, MessageCircle,
|
||||
MessageSquareQuote, RotateCw, Smile, Store, X, Youtube,
|
||||
@@ -224,15 +224,19 @@ const letterColor = (name) => {
|
||||
return `hsl(${hue}, 50%, 45%)`;
|
||||
};
|
||||
|
||||
// Business status → 4 tone levels (wait / ok-fast / ok-slow / fail)
|
||||
// Business status → 4 tone levels (wait / ok-fast / ok-slow / fail).
|
||||
// Unlike the other toneOf call sites (WebRTC / DnsLeak / RuleTest), here the
|
||||
// value is a status string like "Available (123ms)" rather than a raw IP,
|
||||
// so we pass a custom isSuccess predicate plus test.time for the ok-fast /
|
||||
// ok-slow split at 200ms.
|
||||
const toneOf = (test) => {
|
||||
const waitLabel = t('connectivity.StatusWait');
|
||||
const okLabel = t('connectivity.StatusAvailable');
|
||||
const unavailableLabels = [t('connectivity.StatusUnavailable'), t('connectivity.StatusTimeout')];
|
||||
if (test.status === waitLabel) return 'wait';
|
||||
if (unavailableLabels.includes(test.status)) return 'fail';
|
||||
if (test.status.includes(okLabel)) return test.time < 200 ? 'ok-fast' : 'ok-slow';
|
||||
return 'wait';
|
||||
return ipFieldTone(test.status, {
|
||||
waitLabels: t('connectivity.StatusWait'),
|
||||
errorLabels: [t('connectivity.StatusUnavailable'), t('connectivity.StatusTimeout')],
|
||||
isSuccess: (s) => typeof s === 'string' && s.includes(okLabel),
|
||||
time: test.time,
|
||||
});
|
||||
};
|
||||
const { dotClass, textClass } = useStatusTone();
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ import { JnTooltip } from '@/components/ui/tooltip';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import getCountryName from '@/data/country-name.js';
|
||||
import { useStatusTone } from '@/composables/use-status-tone.js';
|
||||
import { useStatusTone, ipFieldTone } from '@/composables/use-status-tone.js';
|
||||
import { EthernetPort, Play, HeartPulse, MapPin, RotateCw } from 'lucide-vue-next';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import FitText from '@/components/widgets/FitText.vue';
|
||||
@@ -108,12 +108,10 @@ const isStarted = ref(false);
|
||||
const { dotClass, textClass } = useStatusTone();
|
||||
|
||||
// Business status → 4 tone levels
|
||||
const toneOf = (leak) => {
|
||||
if (leak.ip === t('dnsleaktest.StatusWait')) return 'wait';
|
||||
if (leak.ip === t('dnsleaktest.StatusError')) return 'fail';
|
||||
if (leak.ip.includes('.') || leak.ip.includes(':')) return 'ok-fast';
|
||||
return 'wait';
|
||||
};
|
||||
const toneOf = (leak) => ipFieldTone(leak.ip, {
|
||||
waitLabels: t('dnsleaktest.StatusWait'),
|
||||
errorLabels: t('dnsleaktest.StatusError'),
|
||||
});
|
||||
|
||||
|
||||
// Status
|
||||
|
||||
@@ -89,7 +89,7 @@ import getCountryName from '@/data/country-name.js';
|
||||
import { JnTooltip } from '@/components/ui/tooltip';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { useStatusTone } from '@/composables/use-status-tone.js';
|
||||
import { useStatusTone, ipFieldTone } from '@/composables/use-status-tone.js';
|
||||
import { Play, MapPin, Flower, Network, RotateCw } from 'lucide-vue-next';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import FitText from '@/components/widgets/FitText.vue';
|
||||
@@ -110,12 +110,10 @@ const stunServers = reactive([
|
||||
]);
|
||||
|
||||
// Business status → 4 tone levels
|
||||
const toneOf = (stun) => {
|
||||
if (stun.ip === t('webrtc.StatusWait')) return 'wait';
|
||||
if (stun.ip === t('webrtc.StatusError')) return 'fail';
|
||||
if (stun.ip.includes('.') || stun.ip.includes(':')) return 'ok-fast';
|
||||
return 'wait';
|
||||
};
|
||||
const toneOf = (stun) => ipFieldTone(stun.ip, {
|
||||
waitLabels: t('webrtc.StatusWait'),
|
||||
errorLabels: t('webrtc.StatusError'),
|
||||
});
|
||||
|
||||
// Single field in dl block is in "no data" state (waiting/error)
|
||||
// These fields may fail independently (e.g. IP success but country query fails), so determine by field.
|
||||
|
||||
@@ -78,7 +78,7 @@ import getCountryName from '@/data/country-name.js';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { useStatusTone } from '@/composables/use-status-tone.js';
|
||||
import { useStatusTone, ipFieldTone } from '@/composables/use-status-tone.js';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { MapPin, RotateCw, Waypoints, SignpostBig } from 'lucide-vue-next';
|
||||
import FitText from '@/components/widgets/FitText.vue';
|
||||
@@ -110,12 +110,10 @@ const testCount = ref(ruleTests.value.length);
|
||||
const finishAll = ref(false);
|
||||
|
||||
// Business status → 4 tone levels
|
||||
const toneOf = (test) => {
|
||||
if (test.ip === t('ruletest.StatusWait')) return 'wait';
|
||||
if (test.ip === t('ruletest.StatusError')) return 'fail';
|
||||
if (test.ip.includes('.') || test.ip.includes(':')) return 'ok-fast';
|
||||
return 'wait';
|
||||
};
|
||||
const toneOf = (test) => ipFieldTone(test.ip, {
|
||||
waitLabels: t('ruletest.StatusWait'),
|
||||
errorLabels: t('ruletest.StatusError'),
|
||||
});
|
||||
|
||||
const isFieldPending = (value) => {
|
||||
return !value || value === t('ruletest.StatusWait') || value === t('ruletest.StatusError');
|
||||
|
||||
@@ -29,3 +29,45 @@ export function useStatusTone() {
|
||||
textClass: (tone) => textColorMap[tone] || textColorMap.wait,
|
||||
};
|
||||
}
|
||||
|
||||
// Unified "IP/MAC/status string → tone" mapping for card-like rows.
|
||||
//
|
||||
// Four call sites share the same shape: a single string value that's either
|
||||
// a "wait" sentinel (localized), an "error" sentinel (localized), or a
|
||||
// successful payload (IP/host). Business variations are expressed via
|
||||
// options rather than by copying the whole function:
|
||||
//
|
||||
// - waitLabels / errorLabels: single string or array; compared by ===
|
||||
// - isSuccess(value): custom predicate for "this value represents a
|
||||
// successful result". Default checks for IP-shaped tokens (contains
|
||||
// '.' or ':'), which fits WebRTC / DnsLeak / RuleTest. Connectivity
|
||||
// passes a custom predicate based on its localized "Available" label.
|
||||
// - time / fastMs: if time is a number, success splits into ok-fast
|
||||
// (< fastMs) vs ok-slow; otherwise success is flat 'ok-fast'.
|
||||
export function ipFieldTone(value, {
|
||||
waitLabels = [],
|
||||
errorLabels = [],
|
||||
isSuccess = defaultIsSuccess,
|
||||
time,
|
||||
fastMs = 200,
|
||||
} = {}) {
|
||||
const waits = asArray(waitLabels);
|
||||
const errs = asArray(errorLabels);
|
||||
if (waits.includes(value)) return 'wait';
|
||||
if (errs.includes(value)) return 'fail';
|
||||
if (isSuccess(value)) {
|
||||
if (typeof time === 'number') return time < fastMs ? 'ok-fast' : 'ok-slow';
|
||||
return 'ok-fast';
|
||||
}
|
||||
return 'wait';
|
||||
}
|
||||
|
||||
function asArray(x) {
|
||||
if (Array.isArray(x)) return x;
|
||||
if (x == null) return [];
|
||||
return [x];
|
||||
}
|
||||
|
||||
function defaultIsSuccess(v) {
|
||||
return typeof v === 'string' && (v.includes('.') || v.includes(':'));
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Tests for ipFieldTone — the unified "status string → tone" mapping that
|
||||
// WebRtcTest / DnsLeaksTest / RuleTest / ConnectivityTest all use.
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import { ipFieldTone, useStatusTone } from '../frontend/composables/use-status-tone.js';
|
||||
|
||||
describe('ipFieldTone()', () => {
|
||||
it('returns "wait" when value equals the wait label', () => {
|
||||
assert.equal(ipFieldTone('Waiting', { waitLabels: 'Waiting', errorLabels: 'Error' }), 'wait');
|
||||
});
|
||||
|
||||
it('returns "fail" when value equals the error label', () => {
|
||||
assert.equal(ipFieldTone('Error', { waitLabels: 'Waiting', errorLabels: 'Error' }), 'fail');
|
||||
});
|
||||
|
||||
it('accepts arrays of wait/error labels', () => {
|
||||
const opts = { waitLabels: ['W1', 'W2'], errorLabels: ['E1', 'E2'] };
|
||||
assert.equal(ipFieldTone('W2', opts), 'wait');
|
||||
assert.equal(ipFieldTone('E1', opts), 'fail');
|
||||
});
|
||||
|
||||
it('returns "ok-fast" for an IPv4-shaped value (default success predicate)', () => {
|
||||
assert.equal(ipFieldTone('192.168.1.1', { waitLabels: 'W', errorLabels: 'E' }), 'ok-fast');
|
||||
});
|
||||
|
||||
it('returns "ok-fast" for an IPv6-shaped value (default success predicate)', () => {
|
||||
assert.equal(ipFieldTone('2001:db8::1', { waitLabels: 'W', errorLabels: 'E' }), 'ok-fast');
|
||||
});
|
||||
|
||||
it('falls back to "wait" for unrecognized non-IP strings', () => {
|
||||
assert.equal(ipFieldTone('hello', { waitLabels: 'W', errorLabels: 'E' }), 'wait');
|
||||
});
|
||||
|
||||
it('splits success into ok-fast vs ok-slow when time is a number', () => {
|
||||
const opts = { waitLabels: 'W', errorLabels: 'E' };
|
||||
assert.equal(ipFieldTone('1.2.3.4', { ...opts, time: 150 }), 'ok-fast');
|
||||
assert.equal(ipFieldTone('1.2.3.4', { ...opts, time: 350 }), 'ok-slow');
|
||||
});
|
||||
|
||||
it('respects a custom fastMs cutoff', () => {
|
||||
const opts = { waitLabels: 'W', errorLabels: 'E', fastMs: 100 };
|
||||
assert.equal(ipFieldTone('1.2.3.4', { ...opts, time: 80 }), 'ok-fast');
|
||||
assert.equal(ipFieldTone('1.2.3.4', { ...opts, time: 150 }), 'ok-slow');
|
||||
});
|
||||
|
||||
it('uses a custom isSuccess predicate (ConnectivityTest-style)', () => {
|
||||
// ConnectivityTest: status string like "Available (123ms)" includes an
|
||||
// "Available" label rather than being IP-shaped.
|
||||
const opts = {
|
||||
waitLabels: 'Waiting',
|
||||
errorLabels: ['Unavailable', 'Timeout'],
|
||||
isSuccess: (s) => s.includes('Available'),
|
||||
time: 80,
|
||||
fastMs: 200,
|
||||
};
|
||||
assert.equal(ipFieldTone('Available (80ms)', opts), 'ok-fast');
|
||||
assert.equal(ipFieldTone('Available (500ms)', { ...opts, time: 500 }), 'ok-slow');
|
||||
assert.equal(ipFieldTone('Waiting', opts), 'wait');
|
||||
assert.equal(ipFieldTone('Timeout', opts), 'fail');
|
||||
});
|
||||
|
||||
it('is resilient to non-string values', () => {
|
||||
assert.equal(ipFieldTone(undefined, { waitLabels: 'W', errorLabels: 'E' }), 'wait');
|
||||
assert.equal(ipFieldTone(null, { waitLabels: 'W', errorLabels: 'E' }), 'wait');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useStatusTone() (existing contract still holds)', () => {
|
||||
it('dotClass maps tones to bg-* classes', () => {
|
||||
const { dotClass } = useStatusTone();
|
||||
assert.equal(dotClass('wait'), 'bg-info');
|
||||
assert.equal(dotClass('ok-fast'), 'bg-success');
|
||||
assert.equal(dotClass('ok-slow'), 'bg-warning');
|
||||
assert.equal(dotClass('fail'), 'bg-destructive');
|
||||
// unknown tone falls back to wait
|
||||
assert.equal(dotClass('bogus'), 'bg-info');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user