fix: resolve lint warnings/errors and apply formatting (#1024)
* fix: resolve lint warnings/errors and apply formatting Fix 10 oxlint issues (2 errors, 8 warnings): - Remove unused catch parameters in Security.tsx and ImportDialog.tsx - Remove stray expression in Generator.tsx - Add eslint-disable for debounced useCallback in FilterControl.tsx - Remove unnecessary deps (resolveDB, store) in bindStoreToDevice.ts - Prefix unused variant param in AppSidebar.tsx - Memoize tabs arrays in DeviceConfig, RadioConfig, ModuleConfig - Fix channels type in RadioConfig TabItem Also applies oxfmt formatting across all files. * updating lock file * update protobuf package * fix: regenerate pnpm-lock.yaml and exclude jsr protobufs from minimumReleaseAge * prevented http card from always being polled * updated pnpm config fiile * updated actions * removed biome config and lint system leftovers * updating protobuf package
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$SKIP_SIMPLE_GIT_HOOKS" = "1" ]; then
|
||||
echo "[INFO] SKIP_SIMPLE_GIT_HOOKS is set to 1, skipping hook."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f "$SIMPLE_GIT_HOOKS_RC" ]; then
|
||||
. "$SIMPLE_GIT_HOOKS_RC"
|
||||
fi
|
||||
|
||||
# cd packages/web && bun run lint && bun run format
|
||||
@@ -24,9 +24,9 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
|
||||
- name: Prewarm & Install (workspace)
|
||||
run: |
|
||||
|
||||
@@ -28,9 +28,9 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
|
||||
- name: Install dependencies (root)
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -17,27 +17,29 @@ env:
|
||||
jobs:
|
||||
build-and-package:
|
||||
runs-on: ubuntu-latest
|
||||
# Skip for draft PRs; remove this line if you want to run on drafts too
|
||||
if: ${{ github.event.pull_request.draft == false }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
|
||||
- name: Prefetch dependencies
|
||||
run: pnpm fetch
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Run linter
|
||||
run: pnpm run lint
|
||||
|
||||
@@ -6,11 +6,11 @@ on:
|
||||
packages:
|
||||
description: 'Packages to release (comma-separated, or "all")'
|
||||
required: false
|
||||
default: 'all'
|
||||
default: "all"
|
||||
bump:
|
||||
description: 'Semver bump (patch | minor | major)'
|
||||
description: "Semver bump (patch | minor | major)"
|
||||
required: false
|
||||
default: 'patch'
|
||||
default: "patch"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -32,10 +32,10 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -82,7 +82,6 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
- name: Commit version bumps
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -43,9 +43,9 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
pnpm lint-staged
|
||||
Vendored
-4
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit"
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/i18n/locales/*-*/**": true
|
||||
},
|
||||
"vitest.workspaceConfig": "vitest.config.ts"
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
{
|
||||
"files": {
|
||||
"includes": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"!npm_modules",
|
||||
"!**/dist",
|
||||
"!**/protobufs",
|
||||
"!**/.*",
|
||||
"!npm",
|
||||
"**/*.json",
|
||||
"!**/locales/*-*/*.json",
|
||||
"!**/packages/ui/"
|
||||
],
|
||||
"ignoreUnknown": false
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80,
|
||||
"attributePosition": "auto"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"includes": ["**", "!test/**", "!**/dist/**"],
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noExplicitAny": "error",
|
||||
"noDebugger": "error"
|
||||
},
|
||||
"style": {
|
||||
"useBlockStatements": "error",
|
||||
"useSingleVarDeclarator": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "error",
|
||||
"noUnusedImports": "error",
|
||||
"useImportExtensions": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double",
|
||||
"semicolons": "always"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"formatter": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": [
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/__tests__/**/*.{ts,tsx}"
|
||||
],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+19
-13
@@ -12,31 +12,38 @@
|
||||
"url": "https://github.com/meshtastic/web/issues"
|
||||
},
|
||||
"homepage": "https://meshtastic.org",
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm run check:fix"
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
"oxlint",
|
||||
"oxfmt"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"lint": "biome lint",
|
||||
"lint:fix": "biome lint --write",
|
||||
"format": "biome format",
|
||||
"format:fix": "biome format . --write",
|
||||
"check": "biome check",
|
||||
"check:fix": "biome check --write",
|
||||
"lint": "oxlint",
|
||||
"lint:fix": "oxlint --fix",
|
||||
"format": "oxfmt --check .",
|
||||
"format:fix": "oxfmt .",
|
||||
"check": "pnpm lint && pnpm format",
|
||||
"check:fix": "pnpm lint:fix && pnpm format:fix",
|
||||
"build:all": "pnpm run --filter '*' build",
|
||||
"clean:all": "pnpm run --filter '*' clean",
|
||||
"publish:packages": "pnpm run build --filter 'packages/transport-*'",
|
||||
"test": "vitest"
|
||||
"test": "vitest",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.9.0",
|
||||
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.7.18",
|
||||
"@meshtastic/protobufs": "jsr:^2.7.18",
|
||||
"ste-simple-events": "^3.0.11",
|
||||
"tslog": "^4.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@types/node": "^24.3.1",
|
||||
"husky": "^9.1.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"oxfmt": "^0.16.0",
|
||||
"oxlint": "^1.41.0",
|
||||
"tsdown": "^0.15.0",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
@@ -46,8 +53,7 @@
|
||||
"@serialport/bindings-cpp",
|
||||
"@tailwindcss/oxide",
|
||||
"core-js",
|
||||
"esbuild",
|
||||
"simple-git-hooks"
|
||||
"esbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+34
-118
@@ -44,8 +44,7 @@ export class MeshDevice {
|
||||
constructor(transport: Transport, configId?: number) {
|
||||
this.log = new Logger({
|
||||
name: "iMeshDevice",
|
||||
prettyLogTemplate:
|
||||
"{{hh}}:{{MM}}:{{ss}}:{{ms}}\t{{logLevelName}}\t[{{name}}]\t",
|
||||
prettyLogTemplate: "{{hh}}:{{MM}}:{{ss}}:{{ms}}\t{{logLevelName}}\t[{{name}}]\t",
|
||||
});
|
||||
|
||||
this.transport = transport;
|
||||
@@ -107,9 +106,7 @@ export class MeshDevice {
|
||||
): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.SendText],
|
||||
`📤 Sending message to ${destination ?? "broadcast"} on channel ${
|
||||
channel?.toString() ?? 0
|
||||
}`,
|
||||
`📤 Sending message to ${destination ?? "broadcast"} on channel ${channel?.toString() ?? 0}`,
|
||||
);
|
||||
|
||||
const enc = new TextEncoder();
|
||||
@@ -137,9 +134,7 @@ export class MeshDevice {
|
||||
): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.SendWaypoint],
|
||||
`📤 Sending waypoint to ${destination} on channel ${
|
||||
channel?.toString() ?? 0
|
||||
}`,
|
||||
`📤 Sending waypoint to ${destination} on channel ${channel?.toString() ?? 0}`,
|
||||
);
|
||||
|
||||
waypointMessage.id = this.generateRandId();
|
||||
@@ -210,19 +205,13 @@ export class MeshDevice {
|
||||
meshPacket.rxTime = Math.trunc(Date.now() / 1000);
|
||||
this.handleMeshPacket(meshPacket);
|
||||
}
|
||||
return await this.sendRaw(
|
||||
toBinary(Protobuf.Mesh.ToRadioSchema, toRadioMessage),
|
||||
meshPacket.id,
|
||||
);
|
||||
return await this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadioMessage), meshPacket.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends raw packet over the radio
|
||||
*/
|
||||
public async sendRaw(
|
||||
toRadio: Uint8Array,
|
||||
id: number = this.generateRandId(),
|
||||
): Promise<number> {
|
||||
public async sendRaw(toRadio: Uint8Array, id: number = this.generateRandId()): Promise<number> {
|
||||
if (toRadio.length > 512) {
|
||||
throw new Error("Message longer than 512 bytes, it will not be sent!");
|
||||
}
|
||||
@@ -266,9 +255,7 @@ export class MeshDevice {
|
||||
/**
|
||||
* Writes module config to device
|
||||
*/
|
||||
public async setModuleConfig(
|
||||
moduleConfig: Protobuf.ModuleConfig.ModuleConfig,
|
||||
): Promise<number> {
|
||||
public async setModuleConfig(moduleConfig: Protobuf.ModuleConfig.ModuleConfig): Promise<number> {
|
||||
this.log.debug(Emitter[Emitter.SetModuleConfig], "⚙️ Setting module config");
|
||||
|
||||
const moduleConfigMessage = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
@@ -289,10 +276,7 @@ export class MeshDevice {
|
||||
public async setCannedMessages(
|
||||
cannedMessages: Protobuf.CannedMessages.CannedMessageModuleConfig,
|
||||
): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.SetCannedMessages],
|
||||
"⚙️ Setting CannedMessages",
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.SetCannedMessages], "⚙️ Setting CannedMessages");
|
||||
|
||||
const cannedMessagesMessage = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -332,10 +316,7 @@ export class MeshDevice {
|
||||
* Sets devices ChannelSettings
|
||||
*/
|
||||
public async setChannel(channel: Protobuf.Channel.Channel): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.SetChannel],
|
||||
`📻 Setting Channel: ${channel.index}`,
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.SetChannel], `📻 Setting Channel: ${channel.index}`);
|
||||
|
||||
const setChannelMessage = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -373,9 +354,7 @@ export class MeshDevice {
|
||||
/**
|
||||
* Sets static position of device
|
||||
*/
|
||||
public async setPosition(
|
||||
positionMessage: Protobuf.Mesh.Position,
|
||||
): Promise<number> {
|
||||
public async setPosition(positionMessage: Protobuf.Mesh.Position): Promise<number> {
|
||||
return await this.sendPacket(
|
||||
toBinary(Protobuf.Mesh.PositionSchema, positionMessage),
|
||||
Protobuf.Portnums.PortNum.POSITION_APP,
|
||||
@@ -387,10 +366,7 @@ export class MeshDevice {
|
||||
* Sets the fixed position of a device. Can be used to
|
||||
* position GPS-less devices.
|
||||
*/
|
||||
public async setFixedPosition(
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
): Promise<number> {
|
||||
public async setFixedPosition(latitude: number, longitude: number): Promise<number> {
|
||||
const setPositionMessage = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
case: "setFixedPosition",
|
||||
@@ -434,10 +410,7 @@ export class MeshDevice {
|
||||
* Gets specified channel information from the radio
|
||||
*/
|
||||
public async getChannel(index: number): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.GetChannel],
|
||||
`📻 Requesting Channel: ${index}`,
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.GetChannel], `📻 Requesting Channel: ${index}`);
|
||||
|
||||
const getChannelRequestMessage = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -456,9 +429,7 @@ export class MeshDevice {
|
||||
/**
|
||||
* Gets devices config
|
||||
*/
|
||||
public async getConfig(
|
||||
configType: Protobuf.Admin.AdminMessage_ConfigType,
|
||||
): Promise<number> {
|
||||
public async getConfig(configType: Protobuf.Admin.AdminMessage_ConfigType): Promise<number> {
|
||||
this.log.debug(Emitter[Emitter.GetConfig], "⚙️ Requesting config");
|
||||
|
||||
const getRadioRequestMessage = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
@@ -481,10 +452,7 @@ export class MeshDevice {
|
||||
public async getModuleConfig(
|
||||
moduleConfigType: Protobuf.Admin.AdminMessage_ModuleConfigType,
|
||||
): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.GetModuleConfig],
|
||||
"⚙️ Requesting module config",
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.GetModuleConfig], "⚙️ Requesting module config");
|
||||
|
||||
const getRadioRequestMessage = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -522,26 +490,17 @@ export class MeshDevice {
|
||||
* Gets devices metadata
|
||||
*/
|
||||
public async getMetadata(nodeNum: number): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.GetMetadata],
|
||||
`🏷️ Requesting metadata from ${nodeNum}`,
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.GetMetadata], `🏷️ Requesting metadata from ${nodeNum}`);
|
||||
|
||||
const getDeviceMetricsRequestMessage = create(
|
||||
Protobuf.Admin.AdminMessageSchema,
|
||||
{
|
||||
const getDeviceMetricsRequestMessage = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
case: "getDeviceMetadataRequest",
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return await this.sendPacket(
|
||||
toBinary(
|
||||
Protobuf.Admin.AdminMessageSchema,
|
||||
getDeviceMetricsRequestMessage,
|
||||
),
|
||||
toBinary(Protobuf.Admin.AdminMessageSchema, getDeviceMetricsRequestMessage),
|
||||
Protobuf.Portnums.PortNum.ADMIN_APP,
|
||||
nodeNum,
|
||||
ChannelNumber.Admin,
|
||||
@@ -552,10 +511,7 @@ export class MeshDevice {
|
||||
* Clears specific channel with the designated index
|
||||
*/
|
||||
public async clearChannel(index: number): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.ClearChannel],
|
||||
`📻 Clearing Channel ${index}`,
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.ClearChannel], `📻 Clearing Channel ${index}`);
|
||||
|
||||
const channel = create(Protobuf.Channel.ChannelSchema, {
|
||||
index,
|
||||
@@ -634,10 +590,7 @@ export class MeshDevice {
|
||||
* Removes a node from the internal NodeDB of the radio by node number
|
||||
*/
|
||||
public async removeNodeByNum(nodeNum: number): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.RemoveNodeByNum],
|
||||
`📻 Removing Node ${nodeNum} from NodeDB`,
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.RemoveNodeByNum], `📻 Removing Node ${nodeNum} from NodeDB`);
|
||||
|
||||
const removeNodeByNum = create(Protobuf.Admin.AdminMessageSchema, {
|
||||
payloadVariant: {
|
||||
@@ -762,10 +715,7 @@ export class MeshDevice {
|
||||
* Triggers the device configure process
|
||||
*/
|
||||
public configure(): Promise<number> {
|
||||
this.log.debug(
|
||||
Emitter[Emitter.Configure],
|
||||
"⚙️ Requesting device configuration",
|
||||
);
|
||||
this.log.debug(Emitter[Emitter.Configure], "⚙️ Requesting device configuration");
|
||||
this.updateDeviceStatus(DeviceStatusEnum.DeviceConfiguring);
|
||||
|
||||
const toRadio = create(Protobuf.Mesh.ToRadioSchema, {
|
||||
@@ -775,14 +725,12 @@ export class MeshDevice {
|
||||
},
|
||||
});
|
||||
|
||||
return this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio)).catch(
|
||||
(e) => {
|
||||
return this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio)).catch((e) => {
|
||||
if (this.deviceStatus === DeviceStatusEnum.DeviceDisconnected) {
|
||||
throw new Error("Device connection lost");
|
||||
}
|
||||
throw e;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -810,10 +758,7 @@ export class MeshDevice {
|
||||
}
|
||||
this._heartbeatIntervalId = setInterval(() => {
|
||||
this.heartbeat().catch((err) => {
|
||||
this.log.error(
|
||||
Emitter[Emitter.Ping],
|
||||
`⚠️ Unable to send heartbeat: ${err.message}`,
|
||||
);
|
||||
this.log.error(Emitter[Emitter.Ping], `⚠️ Unable to send heartbeat: ${err.message}`);
|
||||
});
|
||||
}, interval);
|
||||
}
|
||||
@@ -950,10 +895,7 @@ export class MeshDevice {
|
||||
case Protobuf.Portnums.PortNum.REMOTE_HARDWARE_APP: {
|
||||
this.events.onRemoteHardwarePacket.dispatch({
|
||||
...packetMetadata,
|
||||
data: fromBinary(
|
||||
Protobuf.RemoteHardware.HardwareMessageSchema,
|
||||
dataPacket.payload,
|
||||
),
|
||||
data: fromBinary(Protobuf.RemoteHardware.HardwareMessageSchema, dataPacket.payload),
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -975,10 +917,7 @@ export class MeshDevice {
|
||||
}
|
||||
|
||||
case Protobuf.Portnums.PortNum.ROUTING_APP: {
|
||||
routingPacket = fromBinary(
|
||||
Protobuf.Mesh.RoutingSchema,
|
||||
dataPacket.payload,
|
||||
);
|
||||
routingPacket = fromBinary(Protobuf.Mesh.RoutingSchema, dataPacket.payload);
|
||||
|
||||
this.events.onRoutingPacket.dispatch({
|
||||
...packetMetadata,
|
||||
@@ -986,9 +925,7 @@ export class MeshDevice {
|
||||
});
|
||||
switch (routingPacket.variant.case) {
|
||||
case "errorReason": {
|
||||
if (
|
||||
routingPacket.variant.value === Protobuf.Mesh.Routing_Error.NONE
|
||||
) {
|
||||
if (routingPacket.variant.value === Protobuf.Mesh.Routing_Error.NONE) {
|
||||
this.queue.processAck(dataPacket.requestId);
|
||||
} else {
|
||||
this.queue.processError({
|
||||
@@ -1014,15 +951,10 @@ export class MeshDevice {
|
||||
}
|
||||
|
||||
case Protobuf.Portnums.PortNum.ADMIN_APP: {
|
||||
adminMessage = fromBinary(
|
||||
Protobuf.Admin.AdminMessageSchema,
|
||||
dataPacket.payload,
|
||||
);
|
||||
adminMessage = fromBinary(Protobuf.Admin.AdminMessageSchema, dataPacket.payload);
|
||||
switch (adminMessage.payloadVariant.case) {
|
||||
case "getChannelResponse": {
|
||||
this.events.onChannelPacket.dispatch(
|
||||
adminMessage.payloadVariant.value,
|
||||
);
|
||||
this.events.onChannelPacket.dispatch(adminMessage.payloadVariant.value);
|
||||
break;
|
||||
}
|
||||
case "getOwnerResponse": {
|
||||
@@ -1033,15 +965,11 @@ export class MeshDevice {
|
||||
break;
|
||||
}
|
||||
case "getConfigResponse": {
|
||||
this.events.onConfigPacket.dispatch(
|
||||
adminMessage.payloadVariant.value,
|
||||
);
|
||||
this.events.onConfigPacket.dispatch(adminMessage.payloadVariant.value);
|
||||
break;
|
||||
}
|
||||
case "getModuleConfigResponse": {
|
||||
this.events.onModuleConfigPacket.dispatch(
|
||||
adminMessage.payloadVariant.value,
|
||||
);
|
||||
this.events.onModuleConfigPacket.dispatch(adminMessage.payloadVariant.value);
|
||||
break;
|
||||
}
|
||||
case "getDeviceMetadataResponse": {
|
||||
@@ -1124,10 +1052,7 @@ export class MeshDevice {
|
||||
case Protobuf.Portnums.PortNum.PAXCOUNTER_APP: {
|
||||
this.events.onPaxcounterPacket.dispatch({
|
||||
...packetMetadata,
|
||||
data: fromBinary(
|
||||
Protobuf.PaxCount.PaxcountSchema,
|
||||
dataPacket.payload,
|
||||
),
|
||||
data: fromBinary(Protobuf.PaxCount.PaxcountSchema, dataPacket.payload),
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -1159,10 +1084,7 @@ export class MeshDevice {
|
||||
case Protobuf.Portnums.PortNum.TELEMETRY_APP: {
|
||||
this.events.onTelemetryPacket.dispatch({
|
||||
...packetMetadata,
|
||||
data: fromBinary(
|
||||
Protobuf.Telemetry.TelemetrySchema,
|
||||
dataPacket.payload,
|
||||
),
|
||||
data: fromBinary(Protobuf.Telemetry.TelemetrySchema, dataPacket.payload),
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -1186,10 +1108,7 @@ export class MeshDevice {
|
||||
case Protobuf.Portnums.PortNum.TRACEROUTE_APP: {
|
||||
this.events.onTraceRoutePacket.dispatch({
|
||||
...packetMetadata,
|
||||
data: fromBinary(
|
||||
Protobuf.Mesh.RouteDiscoverySchema,
|
||||
dataPacket.payload,
|
||||
),
|
||||
data: fromBinary(Protobuf.Mesh.RouteDiscoverySchema, dataPacket.payload),
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -1197,10 +1116,7 @@ export class MeshDevice {
|
||||
case Protobuf.Portnums.PortNum.NEIGHBORINFO_APP: {
|
||||
this.events.onNeighborInfoPacket.dispatch({
|
||||
...packetMetadata,
|
||||
data: fromBinary(
|
||||
Protobuf.Mesh.NeighborInfoSchema,
|
||||
dataPacket.payload,
|
||||
),
|
||||
data: fromBinary(Protobuf.Mesh.NeighborInfoSchema, dataPacket.payload),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -76,9 +76,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onAtakPacket
|
||||
*/
|
||||
public readonly onAtakPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onAtakPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Text packet has been
|
||||
@@ -86,9 +85,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onMessagePacket
|
||||
*/
|
||||
public readonly onMessagePacket: SimpleEventDispatcher<
|
||||
PacketMetadata<string>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<string>>();
|
||||
public readonly onMessagePacket: SimpleEventDispatcher<PacketMetadata<string>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<string>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Remote Hardware packet has
|
||||
@@ -98,9 +96,7 @@ export class EventSystem {
|
||||
*/
|
||||
public readonly onRemoteHardwarePacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.RemoteHardware.HardwareMessage>
|
||||
> = new SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.RemoteHardware.HardwareMessage>
|
||||
>();
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.RemoteHardware.HardwareMessage>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Position packet has been
|
||||
@@ -108,9 +104,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onPositionPacket
|
||||
*/
|
||||
public readonly onPositionPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.Position>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Position>>();
|
||||
public readonly onPositionPacket: SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Position>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Position>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a User packet has been
|
||||
@@ -118,9 +113,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onUserPacket
|
||||
*/
|
||||
public readonly onUserPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.User>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.User>>();
|
||||
public readonly onUserPacket: SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.User>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.User>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Routing packet has been
|
||||
@@ -128,9 +122,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onRoutingPacket
|
||||
*/
|
||||
public readonly onRoutingPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.Routing>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Routing>>();
|
||||
public readonly onRoutingPacket: SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Routing>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Routing>>();
|
||||
|
||||
/**
|
||||
* Fires when the device receives a Metadata packet
|
||||
@@ -146,9 +139,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onCannedMessageModulePacket
|
||||
*/
|
||||
public readonly onCannedMessageModulePacket: SimpleEventDispatcher<
|
||||
PacketMetadata<string>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<string>>();
|
||||
public readonly onCannedMessageModulePacket: SimpleEventDispatcher<PacketMetadata<string>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<string>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Waypoint packet has been
|
||||
@@ -156,9 +148,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onWaypointPacket
|
||||
*/
|
||||
public readonly onWaypointPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Protobuf.Mesh.Waypoint>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Waypoint>>();
|
||||
public readonly onWaypointPacket: SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Waypoint>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Waypoint>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing an Audio packet has been
|
||||
@@ -166,9 +157,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onAudioPacket
|
||||
*/
|
||||
public readonly onAudioPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onAudioPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Detection Sensor packet has been
|
||||
@@ -176,9 +166,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onDetectionSensorPacket
|
||||
*/
|
||||
public readonly onDetectionSensorPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onDetectionSensorPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Ping packet has been
|
||||
@@ -186,9 +175,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onPingPacket
|
||||
*/
|
||||
public readonly onPingPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onPingPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a IP Tunnel packet has been
|
||||
@@ -196,9 +184,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onIpTunnelPacket
|
||||
*/
|
||||
public readonly onIpTunnelPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onIpTunnelPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Paxcounter packet has been
|
||||
@@ -216,9 +203,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onSerialPacket
|
||||
*/
|
||||
public readonly onSerialPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onSerialPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Store and Forward packet
|
||||
@@ -226,9 +212,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onStoreForwardPacket
|
||||
*/
|
||||
public readonly onStoreForwardPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onStoreForwardPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Store and Forward packet
|
||||
@@ -236,9 +221,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onRangeTestPacket
|
||||
*/
|
||||
public readonly onRangeTestPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onRangeTestPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Telemetry packet has been
|
||||
@@ -256,9 +240,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onZPSPacket
|
||||
*/
|
||||
public readonly onZpsPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onZpsPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Simulator packet has been
|
||||
@@ -266,9 +249,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onSimulatorPacket
|
||||
*/
|
||||
public readonly onSimulatorPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onSimulatorPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Trace Route packet has been
|
||||
@@ -296,9 +278,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onAtakPluginPacket
|
||||
*/
|
||||
public readonly onAtakPluginPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onAtakPluginPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Map Report packet has been
|
||||
@@ -306,9 +287,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onMapReportPacket
|
||||
*/
|
||||
public readonly onMapReportPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onMapReportPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a Private packet has been
|
||||
@@ -316,9 +296,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onPrivatePacket
|
||||
*/
|
||||
public readonly onPrivatePacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onPrivatePacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing an ATAK Forwarder packet has been
|
||||
@@ -326,9 +305,8 @@ export class EventSystem {
|
||||
*
|
||||
* @event onAtakForwarderPacket
|
||||
*/
|
||||
public readonly onAtakForwarderPacket: SimpleEventDispatcher<
|
||||
PacketMetadata<Uint8Array>
|
||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
public readonly onAtakForwarderPacket: SimpleEventDispatcher<PacketMetadata<Uint8Array>> =
|
||||
new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||
|
||||
/**
|
||||
* Fires when a new MeshPacket message containing a ClientNotification packet has been
|
||||
@@ -361,8 +339,7 @@ export class EventSystem {
|
||||
*
|
||||
* @event onMeshHeartbeat
|
||||
*/
|
||||
public readonly onMeshHeartbeat: SimpleEventDispatcher<Date> =
|
||||
new SimpleEventDispatcher<Date>();
|
||||
public readonly onMeshHeartbeat: SimpleEventDispatcher<Date> = new SimpleEventDispatcher<Date>();
|
||||
|
||||
/**
|
||||
* Outputs any debug log data (currently serial connections only)
|
||||
|
||||
@@ -54,9 +54,7 @@ export class Queue {
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Packet ${item.id} of type ${decoded.payloadVariant.case} timed out`,
|
||||
);
|
||||
console.warn(`Packet ${item.id} of type ${decoded.payloadVariant.case} timed out`);
|
||||
|
||||
reject({
|
||||
id: item.id,
|
||||
@@ -82,11 +80,7 @@ export class Queue {
|
||||
}
|
||||
|
||||
public processError(e: PacketError): void {
|
||||
console.error(
|
||||
`Error received for packet ${e.id}: ${
|
||||
Protobuf.Mesh.Routing_Error[e.error]
|
||||
}`,
|
||||
);
|
||||
console.error(`Error received for packet ${e.id}: ${Protobuf.Mesh.Routing_Error[e.error]}`);
|
||||
this.errorNotifier.dispatch(e);
|
||||
}
|
||||
|
||||
@@ -98,9 +92,7 @@ export class Queue {
|
||||
return queueItem.promise;
|
||||
}
|
||||
|
||||
public async processQueue(
|
||||
outputStream: WritableStream<Uint8Array>,
|
||||
): Promise<void> {
|
||||
public async processQueue(outputStream: WritableStream<Uint8Array>): Promise<void> {
|
||||
if (this.lock) {
|
||||
return;
|
||||
}
|
||||
@@ -117,10 +109,7 @@ export class Queue {
|
||||
await writer.write(item.data);
|
||||
item.sent = true;
|
||||
} catch (error) {
|
||||
if (
|
||||
error?.code === "ECONNRESET" ||
|
||||
error?.code === "ERR_INVALID_STATE"
|
||||
) {
|
||||
if (error?.code === "ECONNRESET" || error?.code === "ERR_INVALID_STATE") {
|
||||
writer.releaseLock();
|
||||
this.lock = false;
|
||||
throw error;
|
||||
|
||||
@@ -26,10 +26,7 @@ export const decodePacket = (device: MeshDevice) =>
|
||||
case "packet": {
|
||||
let decodedMessage: Protobuf.Mesh.FromRadio;
|
||||
try {
|
||||
decodedMessage = fromBinary(
|
||||
Protobuf.Mesh.FromRadioSchema,
|
||||
chunk.data,
|
||||
);
|
||||
decodedMessage = fromBinary(Protobuf.Mesh.FromRadioSchema, chunk.data);
|
||||
} catch (e) {
|
||||
device.log.error(
|
||||
Types.Emitter[Types.Emitter.HandleFromRadio],
|
||||
@@ -56,9 +53,7 @@ export const decodePacket = (device: MeshDevice) =>
|
||||
}
|
||||
|
||||
case "myInfo": {
|
||||
device.events.onMyNodeInfo.dispatch(
|
||||
decodedMessage.payloadVariant.value,
|
||||
);
|
||||
device.events.onMyNodeInfo.dispatch(decodedMessage.payloadVariant.value);
|
||||
device.log.info(
|
||||
Types.Emitter[Types.Emitter.HandleFromRadio],
|
||||
"📱 Received Node info for this device",
|
||||
@@ -72,9 +67,7 @@ export const decodePacket = (device: MeshDevice) =>
|
||||
`📱 Received Node Info packet for node: ${decodedMessage.payloadVariant.value.num}`,
|
||||
);
|
||||
|
||||
device.events.onNodeInfoPacket.dispatch(
|
||||
decodedMessage.payloadVariant.value,
|
||||
);
|
||||
device.events.onNodeInfoPacket.dispatch(decodedMessage.payloadVariant.value);
|
||||
|
||||
//TODO: HERE
|
||||
if (decodedMessage.payloadVariant.value.position) {
|
||||
@@ -117,9 +110,7 @@ export const decodePacket = (device: MeshDevice) =>
|
||||
);
|
||||
}
|
||||
|
||||
device.events.onConfigPacket.dispatch(
|
||||
decodedMessage.payloadVariant.value,
|
||||
);
|
||||
device.events.onConfigPacket.dispatch(decodedMessage.payloadVariant.value);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -128,9 +119,7 @@ export const decodePacket = (device: MeshDevice) =>
|
||||
Types.Emitter[Types.Emitter.HandleFromRadio],
|
||||
"Received onLogRecord",
|
||||
);
|
||||
device.events.onLogRecord.dispatch(
|
||||
decodedMessage.payloadVariant.value,
|
||||
);
|
||||
device.events.onLogRecord.dispatch(decodedMessage.payloadVariant.value);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -141,9 +130,7 @@ export const decodePacket = (device: MeshDevice) =>
|
||||
);
|
||||
|
||||
// Emit the configCompleteId event for MeshService to handle two-stage flow
|
||||
device.events.onConfigComplete.dispatch(
|
||||
decodedMessage.payloadVariant.value,
|
||||
);
|
||||
device.events.onConfigComplete.dispatch(decodedMessage.payloadVariant.value);
|
||||
|
||||
// For backward compatibility: if configId matches, update device status
|
||||
// MeshService will override this behavior for two-stage flow
|
||||
@@ -152,9 +139,7 @@ export const decodePacket = (device: MeshDevice) =>
|
||||
Types.Emitter[Types.Emitter.HandleFromRadio],
|
||||
`⚙️ Config id matches device.configId: ${device.configId}`,
|
||||
);
|
||||
device.updateDeviceStatus(
|
||||
Types.DeviceStatusEnum.DeviceConfigured,
|
||||
);
|
||||
device.updateDeviceStatus(Types.DeviceStatusEnum.DeviceConfigured);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -179,9 +164,7 @@ export const decodePacket = (device: MeshDevice) =>
|
||||
);
|
||||
}
|
||||
|
||||
device.events.onModuleConfigPacket.dispatch(
|
||||
decodedMessage.payloadVariant.value,
|
||||
);
|
||||
device.events.onModuleConfigPacket.dispatch(decodedMessage.payloadVariant.value);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -191,9 +174,7 @@ export const decodePacket = (device: MeshDevice) =>
|
||||
`🔐 Received Channel: ${decodedMessage.payloadVariant.value.index}`,
|
||||
);
|
||||
|
||||
device.events.onChannelPacket.dispatch(
|
||||
decodedMessage.payloadVariant.value,
|
||||
);
|
||||
device.events.onChannelPacket.dispatch(decodedMessage.payloadVariant.value);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -203,9 +184,7 @@ export const decodePacket = (device: MeshDevice) =>
|
||||
`🚧 Received Queue Status: ${decodedMessage.payloadVariant.value}`,
|
||||
);
|
||||
|
||||
device.events.onQueueStatus.dispatch(
|
||||
decodedMessage.payloadVariant.value,
|
||||
);
|
||||
device.events.onQueueStatus.dispatch(decodedMessage.payloadVariant.value);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -216,9 +195,8 @@ export const decodePacket = (device: MeshDevice) =>
|
||||
|
||||
case "metadata": {
|
||||
if (
|
||||
Number.parseFloat(
|
||||
decodedMessage.payloadVariant.value.firmwareVersion,
|
||||
) < Constants.minFwVer
|
||||
Number.parseFloat(decodedMessage.payloadVariant.value.firmwareVersion) <
|
||||
Constants.minFwVer
|
||||
) {
|
||||
device.log.fatal(
|
||||
Types.Emitter[Types.Emitter.HandleFromRadio],
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { DeviceOutput } from "../../types.ts";
|
||||
|
||||
export const fromDeviceStream: () => TransformStream<Uint8Array, DeviceOutput> =
|
||||
(
|
||||
export const fromDeviceStream: () => TransformStream<Uint8Array, DeviceOutput> = (
|
||||
// onReleaseEvent: SimpleEventDispatcher<boolean>,
|
||||
) => {
|
||||
) => {
|
||||
let byteBuffer = new Uint8Array([]);
|
||||
const textDecoder = new TextDecoder();
|
||||
return new TransformStream<Uint8Array, DeviceOutput>({
|
||||
@@ -28,18 +27,11 @@ export const fromDeviceStream: () => TransformStream<Uint8Array, DeviceOutput> =
|
||||
const msb = byteBuffer[2];
|
||||
const lsb = byteBuffer[3];
|
||||
|
||||
if (
|
||||
msb !== undefined &&
|
||||
lsb !== undefined &&
|
||||
byteBuffer.length >= 4 + (msb << 8) + lsb
|
||||
) {
|
||||
if (msb !== undefined && lsb !== undefined && byteBuffer.length >= 4 + (msb << 8) + lsb) {
|
||||
const packet = byteBuffer.subarray(4, 4 + (msb << 8) + lsb);
|
||||
|
||||
const malformedDetectorIndex = packet.indexOf(0x94);
|
||||
if (
|
||||
malformedDetectorIndex !== -1 &&
|
||||
packet[malformedDetectorIndex + 1] === 0xc3
|
||||
) {
|
||||
if (malformedDetectorIndex !== -1 && packet[malformedDetectorIndex + 1] === 0xc3) {
|
||||
console.warn(
|
||||
`⚠️ Malformed packet found, discarding: ${byteBuffer
|
||||
.subarray(0, malformedDetectorIndex - 1)
|
||||
@@ -66,4 +58,4 @@ export const fromDeviceStream: () => TransformStream<Uint8Array, DeviceOutput> =
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
/**
|
||||
* Pads packets with appropriate framing information before writing to the output stream.
|
||||
*/
|
||||
export const toDeviceStream: () => TransformStream<Uint8Array, Uint8Array> =
|
||||
() => {
|
||||
export const toDeviceStream: () => TransformStream<Uint8Array, Uint8Array> = () => {
|
||||
return new TransformStream<Uint8Array, Uint8Array>({
|
||||
transform(chunk: Uint8Array, controller): void {
|
||||
const bufLen = chunk.length;
|
||||
const header = new Uint8Array([
|
||||
0x94,
|
||||
0xc3,
|
||||
(bufLen >> 8) & 0xff,
|
||||
bufLen & 0xff,
|
||||
]);
|
||||
const header = new Uint8Array([0x94, 0xc3, (bufLen >> 8) & 0xff, bufLen & 0xff]);
|
||||
controller.enqueue(new Uint8Array([...header, ...chunk]));
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -74,11 +74,7 @@ export class Xmodem {
|
||||
this.rxBuffer[this.counter] = packet.buffer;
|
||||
return this.sendCommand(Protobuf.Xmodem.XModem_Control.ACK);
|
||||
}
|
||||
return await this.sendCommand(
|
||||
Protobuf.Xmodem.XModem_Control.NAK,
|
||||
undefined,
|
||||
packet.seq,
|
||||
);
|
||||
return await this.sendCommand(Protobuf.Xmodem.XModem_Control.NAK, undefined, packet.seq);
|
||||
}
|
||||
case Protobuf.Xmodem.XModem_Control.STX: {
|
||||
break;
|
||||
|
||||
@@ -13,8 +13,7 @@ await build({
|
||||
// package.json properties
|
||||
name: "@meshtastic/transport-deno",
|
||||
version: Deno.args[0],
|
||||
description:
|
||||
"A Deno transport layer for your project, enabling seamless integration with npm.",
|
||||
description: "A Deno transport layer for your project, enabling seamless integration with npm.",
|
||||
license: "GPL-3.0-only",
|
||||
repository: {
|
||||
type: "git",
|
||||
|
||||
@@ -20,9 +20,7 @@ export class TransportDeno implements Types.Transport {
|
||||
toDeviceStream.readable.pipeTo(this.connection.writable);
|
||||
|
||||
this._toDevice = toDeviceStream.writable;
|
||||
this._fromDevice = this.connection.readable.pipeThrough(
|
||||
Utils.fromDeviceStream(),
|
||||
);
|
||||
this._fromDevice = this.connection.readable.pipeThrough(Utils.fromDeviceStream());
|
||||
}
|
||||
|
||||
get toDevice(): WritableStream<Uint8Array> {
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type MockInstance,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest";
|
||||
import { runTransportContract } from "../../../tests/utils/transportContract.ts";
|
||||
import { TransportHTTP } from "./transport.ts";
|
||||
|
||||
let abortTimeoutSpy: MockInstance | undefined;
|
||||
beforeEach(() => {
|
||||
abortTimeoutSpy = vi
|
||||
.spyOn(
|
||||
globalThis.AbortSignal as unknown as { timeout(ms: number): AbortSignal },
|
||||
"timeout",
|
||||
)
|
||||
.spyOn(globalThis.AbortSignal as unknown as { timeout(ms: number): AbortSignal }, "timeout")
|
||||
.mockImplementation((ms: number) => {
|
||||
const ctrl = new AbortController();
|
||||
const abort = () =>
|
||||
ctrl.abort(new DOMException("Timeout reached", "TimeoutError"));
|
||||
const abort = () => ctrl.abort(new DOMException("Timeout reached", "TimeoutError"));
|
||||
// Uses setTimeout so vi.useFakeTimers() can fast-forward it
|
||||
setTimeout(abort, ms);
|
||||
return ctrl.signal;
|
||||
@@ -128,23 +116,21 @@ describe("TransportHTTP (contract)", () => {
|
||||
vi.unstubAllGlobals();
|
||||
},
|
||||
create: async () => {
|
||||
(
|
||||
globalThis as unknown as { __http: ReturnType<typeof stubFetch> }
|
||||
).__http = stubFetch();
|
||||
(globalThis as unknown as { __http: ReturnType<typeof stubFetch> }).__http = stubFetch();
|
||||
const transport = await TransportHTTP.create("127.0.0.1:80", false);
|
||||
await tickNextTimer();
|
||||
return transport;
|
||||
},
|
||||
pushIncoming: async (bytes) => {
|
||||
(
|
||||
globalThis as unknown as { __http: ReturnType<typeof stubFetch> }
|
||||
).__http.pushIncoming(bytes);
|
||||
(globalThis as unknown as { __http: ReturnType<typeof stubFetch> }).__http.pushIncoming(
|
||||
bytes,
|
||||
);
|
||||
await tickNextTimer();
|
||||
},
|
||||
assertLastWritten: (bytes) => {
|
||||
(
|
||||
globalThis as unknown as { __http: ReturnType<typeof stubFetch> }
|
||||
).__http.assertLastWritten(bytes);
|
||||
(globalThis as unknown as { __http: ReturnType<typeof stubFetch> }).__http.assertLastWritten(
|
||||
bytes,
|
||||
);
|
||||
},
|
||||
triggerDisconnect: async () => {
|
||||
(
|
||||
|
||||
@@ -34,8 +34,7 @@ export class TransportHTTP implements Types.Transport {
|
||||
|
||||
private inflightReadController?: AbortController;
|
||||
|
||||
private lastStatus: Types.DeviceStatusEnum =
|
||||
Types.DeviceStatusEnum.DeviceDisconnected;
|
||||
private lastStatus: Types.DeviceStatusEnum = Types.DeviceStatusEnum.DeviceDisconnected;
|
||||
private closingByUser = false;
|
||||
|
||||
/**
|
||||
@@ -44,10 +43,7 @@ export class TransportHTTP implements Types.Transport {
|
||||
* @param address Hostname or IP address (with optional port).
|
||||
* @param tls Use HTTPS if true, HTTP otherwise.
|
||||
*/
|
||||
public static async create(
|
||||
address: string,
|
||||
tls?: boolean,
|
||||
): Promise<TransportHTTP> {
|
||||
public static async create(address: string, tls?: boolean): Promise<TransportHTTP> {
|
||||
const connectionUrl = `${tls ? "https" : "http"}://${address}`;
|
||||
await fetch(`${connectionUrl}/api/v1/toradio`, {
|
||||
method: "OPTIONS",
|
||||
@@ -90,10 +86,7 @@ export class TransportHTTP implements Types.Transport {
|
||||
|
||||
// Start polling immediately
|
||||
void this.safePoll();
|
||||
this.interval = setInterval(
|
||||
() => void this.safePoll(),
|
||||
this.fetchInterval,
|
||||
);
|
||||
this.interval = setInterval(() => void this.safePoll(), this.fetchInterval);
|
||||
},
|
||||
cancel: () => {
|
||||
if (this.interval) {
|
||||
@@ -112,10 +105,7 @@ export class TransportHTTP implements Types.Transport {
|
||||
const inflight = new AbortController();
|
||||
this.inflightReadController = inflight;
|
||||
|
||||
const signal = AbortSignal.any([
|
||||
inflight.signal,
|
||||
AbortSignal.timeout(READ_TIMEOUT_MS),
|
||||
]);
|
||||
const signal = AbortSignal.any([inflight.signal, AbortSignal.timeout(READ_TIMEOUT_MS)]);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
@@ -127,9 +117,7 @@ export class TransportHTTP implements Types.Transport {
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`fromradio ${response.status} ${response.statusText}`,
|
||||
);
|
||||
throw new Error(`fromradio ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceConnected);
|
||||
@@ -217,10 +205,8 @@ export class TransportHTTP implements Types.Transport {
|
||||
|
||||
private isTimeoutOrAbort(err: unknown): boolean {
|
||||
return (
|
||||
(err instanceof DOMException &&
|
||||
(err.name === "AbortError" || err.name === "TimeoutError")) ||
|
||||
(err instanceof Error &&
|
||||
(err.name === "AbortError" || err.name === "TimeoutError"))
|
||||
(err instanceof DOMException && (err.name === "AbortError" || err.name === "TimeoutError")) ||
|
||||
(err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError"))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,16 +20,8 @@ class FakeSerialPort extends Duplex {
|
||||
|
||||
_read() {}
|
||||
|
||||
_write(
|
||||
chunk: Buffer,
|
||||
_encoding: BufferEncoding,
|
||||
callback: (error?: Error | null) => void,
|
||||
) {
|
||||
this.lastWritten = new Uint8Array(
|
||||
chunk.buffer,
|
||||
chunk.byteOffset,
|
||||
chunk.byteLength,
|
||||
);
|
||||
_write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
|
||||
this.lastWritten = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -69,16 +61,10 @@ function stubCoreTransforms() {
|
||||
|
||||
// Utils.toDeviceStream is a getter
|
||||
const transform = Utils.toDeviceStream;
|
||||
vi.spyOn(Utils, "toDeviceStream", "get").mockReturnValue(
|
||||
toDevice as unknown as typeof transform,
|
||||
);
|
||||
vi.spyOn(Utils, "toDeviceStream", "get").mockReturnValue(toDevice as unknown as typeof transform);
|
||||
|
||||
vi.spyOn(Utils, "fromDeviceStream").mockImplementation(
|
||||
() =>
|
||||
fromDeviceFactory() as unknown as TransformStream<
|
||||
Uint8Array,
|
||||
Types.DeviceOutput
|
||||
>,
|
||||
() => fromDeviceFactory() as unknown as TransformStream<Uint8Array, Types.DeviceOutput>,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -105,30 +91,24 @@ describe("TransportNodeSerial (contract)", () => {
|
||||
},
|
||||
create: async () => {
|
||||
const fakePort = new FakeSerialPort();
|
||||
const transport = new TransportNodeSerial(
|
||||
fakePort as unknown as SerialPort,
|
||||
);
|
||||
const transport = new TransportNodeSerial(fakePort as unknown as SerialPort);
|
||||
await Promise.resolve();
|
||||
(globalThis as unknown as { __fakePort: FakeSerialPort }).__fakePort =
|
||||
fakePort;
|
||||
(globalThis as unknown as { __fakePort: FakeSerialPort }).__fakePort = fakePort;
|
||||
return transport;
|
||||
},
|
||||
pushIncoming: async (bytes) => {
|
||||
(
|
||||
globalThis as unknown as { __fakePort: FakeSerialPort }
|
||||
).__fakePort.pushIncoming(bytes);
|
||||
(globalThis as unknown as { __fakePort: FakeSerialPort }).__fakePort.pushIncoming(bytes);
|
||||
await Promise.resolve();
|
||||
},
|
||||
assertLastWritten: (bytes) => {
|
||||
const port = (globalThis as unknown as { __fakePort: FakeSerialPort })
|
||||
.__fakePort;
|
||||
const port = (globalThis as unknown as { __fakePort: FakeSerialPort }).__fakePort;
|
||||
expect(port.lastWritten).toBeDefined();
|
||||
expect(port.lastWritten).toEqual(bytes);
|
||||
},
|
||||
triggerDisconnect: async () => {
|
||||
(
|
||||
globalThis as unknown as { __fakePort: FakeSerialPort }
|
||||
).__fakePort.emitErrorOnce("test-disconnect");
|
||||
(globalThis as unknown as { __fakePort: FakeSerialPort }).__fakePort.emitErrorOnce(
|
||||
"test-disconnect",
|
||||
);
|
||||
await Promise.resolve();
|
||||
},
|
||||
});
|
||||
@@ -147,9 +127,7 @@ describe("TransportNodeSerial (extras)", () => {
|
||||
|
||||
it("emits DeviceDisconnected with reason 'port-closed' on close event", async () => {
|
||||
const fakePort = new FakeSerialPort();
|
||||
const transport = new TransportNodeSerial(
|
||||
fakePort as unknown as SerialPort,
|
||||
);
|
||||
const transport = new TransportNodeSerial(fakePort as unknown as SerialPort);
|
||||
const reader = transport.fromDevice.getReader();
|
||||
|
||||
await Promise.resolve();
|
||||
@@ -157,17 +135,13 @@ describe("TransportNodeSerial (extras)", () => {
|
||||
const first = await reader.read();
|
||||
expect(isStatusEvent(first.value)).toBe(true);
|
||||
if (isStatusEvent(first.value)) {
|
||||
expect(first.value.data.status).toBe(
|
||||
Types.DeviceStatusEnum.DeviceConnecting,
|
||||
);
|
||||
expect(first.value.data.status).toBe(Types.DeviceStatusEnum.DeviceConnecting);
|
||||
}
|
||||
|
||||
const second = await reader.read();
|
||||
expect(isStatusEvent(second.value)).toBe(true);
|
||||
if (isStatusEvent(second.value)) {
|
||||
expect(second.value.data.status).toBe(
|
||||
Types.DeviceStatusEnum.DeviceConnected,
|
||||
);
|
||||
expect(second.value.data.status).toBe(Types.DeviceStatusEnum.DeviceConnected);
|
||||
}
|
||||
|
||||
fakePort.emitClose();
|
||||
|
||||
@@ -16,8 +16,7 @@ export class TransportNodeSerial implements Types.Transport {
|
||||
private port: SerialPort | undefined;
|
||||
private pipePromise?: Promise<void>;
|
||||
private abortController: AbortController;
|
||||
private lastStatus: Types.DeviceStatusEnum =
|
||||
Types.DeviceStatusEnum.DeviceDisconnected;
|
||||
private lastStatus: Types.DeviceStatusEnum = Types.DeviceStatusEnum.DeviceDisconnected;
|
||||
private closingByUser = false;
|
||||
|
||||
/**
|
||||
@@ -26,10 +25,7 @@ export class TransportNodeSerial implements Types.Transport {
|
||||
* @param baudRate - Baud rate for the serial connection (default is 115200).
|
||||
* @returns A promise that resolves with a connected TransportNode instance.
|
||||
*/
|
||||
public static create(
|
||||
path: string,
|
||||
baudRate = 115200,
|
||||
): Promise<TransportNodeSerial> {
|
||||
public static create(path: string, baudRate = 115200): Promise<TransportNodeSerial> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const port = new SerialPort({
|
||||
path,
|
||||
@@ -94,13 +90,8 @@ export class TransportNodeSerial implements Types.Transport {
|
||||
if (this.closingByUser) {
|
||||
ctrl.close(); // graceful EOF on user
|
||||
} else {
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"read-error",
|
||||
);
|
||||
ctrl.error(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "read-error");
|
||||
ctrl.error(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
try {
|
||||
await transformed.cancel();
|
||||
@@ -124,10 +115,7 @@ export class TransportNodeSerial implements Types.Transport {
|
||||
return;
|
||||
}
|
||||
console.error("Error piping data to serial port:", error);
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"write-error",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "write-error");
|
||||
try {
|
||||
this.port?.close();
|
||||
} catch {}
|
||||
|
||||
@@ -20,16 +20,8 @@ class FakeSocket extends Duplex {
|
||||
|
||||
_read() {}
|
||||
|
||||
_write(
|
||||
chunk: Buffer,
|
||||
_encoding: BufferEncoding,
|
||||
callback: (error?: Error | null) => void,
|
||||
) {
|
||||
this.lastWritten = new Uint8Array(
|
||||
chunk.buffer,
|
||||
chunk.byteOffset,
|
||||
chunk.byteLength,
|
||||
);
|
||||
_write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
|
||||
this.lastWritten = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -69,16 +61,10 @@ function stubCoreTransforms() {
|
||||
});
|
||||
|
||||
const transform = Utils.toDeviceStream;
|
||||
vi.spyOn(Utils, "toDeviceStream", "get").mockReturnValue(
|
||||
toDevice as unknown as typeof transform,
|
||||
);
|
||||
vi.spyOn(Utils, "toDeviceStream", "get").mockReturnValue(toDevice as unknown as typeof transform);
|
||||
|
||||
vi.spyOn(Utils, "fromDeviceStream").mockImplementation(
|
||||
() =>
|
||||
fromDeviceFactory() as unknown as TransformStream<
|
||||
Uint8Array,
|
||||
Types.DeviceOutput
|
||||
>,
|
||||
() => fromDeviceFactory() as unknown as TransformStream<Uint8Array, Types.DeviceOutput>,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -107,26 +93,22 @@ describe("TransportNode (contract)", () => {
|
||||
const fakeSocket = new FakeSocket();
|
||||
const transport = new TransportNode(fakeSocket as unknown as Socket);
|
||||
await Promise.resolve();
|
||||
(globalThis as unknown as { __nodeSock: FakeSocket }).__nodeSock =
|
||||
fakeSocket;
|
||||
(globalThis as unknown as { __nodeSock: FakeSocket }).__nodeSock = fakeSocket;
|
||||
return transport;
|
||||
},
|
||||
pushIncoming: async (bytes) => {
|
||||
(
|
||||
globalThis as unknown as { __nodeSock: FakeSocket }
|
||||
).__nodeSock.pushIncoming(bytes);
|
||||
(globalThis as unknown as { __nodeSock: FakeSocket }).__nodeSock.pushIncoming(bytes);
|
||||
await Promise.resolve();
|
||||
},
|
||||
assertLastWritten: (bytes) => {
|
||||
const sock = (globalThis as unknown as { __nodeSock: FakeSocket })
|
||||
.__nodeSock;
|
||||
const sock = (globalThis as unknown as { __nodeSock: FakeSocket }).__nodeSock;
|
||||
expect(sock.lastWritten).toBeDefined();
|
||||
expect(sock.lastWritten).toEqual(bytes);
|
||||
},
|
||||
triggerDisconnect: async () => {
|
||||
(
|
||||
globalThis as unknown as { __nodeSock: FakeSocket }
|
||||
).__nodeSock.emitErrorOnce("test-disconnect");
|
||||
(globalThis as unknown as { __nodeSock: FakeSocket }).__nodeSock.emitErrorOnce(
|
||||
"test-disconnect",
|
||||
);
|
||||
await Promise.resolve();
|
||||
},
|
||||
});
|
||||
@@ -153,17 +135,13 @@ describe("TransportNode (extras)", () => {
|
||||
const first = await reader.read();
|
||||
expect(isStatusEvent(first.value)).toBe(true);
|
||||
if (isStatusEvent(first.value)) {
|
||||
expect(first.value.data.status).toBe(
|
||||
Types.DeviceStatusEnum.DeviceConnecting,
|
||||
);
|
||||
expect(first.value.data.status).toBe(Types.DeviceStatusEnum.DeviceConnecting);
|
||||
}
|
||||
|
||||
const second = await reader.read();
|
||||
expect(isStatusEvent(second.value)).toBe(true);
|
||||
if (isStatusEvent(second.value)) {
|
||||
expect(second.value.data.status).toBe(
|
||||
Types.DeviceStatusEnum.DeviceConnected,
|
||||
);
|
||||
expect(second.value.data.status).toBe(Types.DeviceStatusEnum.DeviceConnected);
|
||||
}
|
||||
|
||||
fakeSocket.emitClose();
|
||||
|
||||
@@ -16,8 +16,7 @@ export class TransportNode implements Types.Transport {
|
||||
private socket: Socket | undefined;
|
||||
private pipePromise?: Promise<void>;
|
||||
private abortController: AbortController;
|
||||
private lastStatus: Types.DeviceStatusEnum =
|
||||
Types.DeviceStatusEnum.DeviceDisconnected;
|
||||
private lastStatus: Types.DeviceStatusEnum = Types.DeviceStatusEnum.DeviceDisconnected;
|
||||
|
||||
private closingByUser = false;
|
||||
private errored = false;
|
||||
@@ -29,11 +28,7 @@ export class TransportNode implements Types.Transport {
|
||||
* @param timeout - TCP socket timeout in milliseconds (defaults to 60000).
|
||||
* @returns A promise that resolves with a connected TransportNode instance.
|
||||
*/
|
||||
public static create(
|
||||
hostname: string,
|
||||
port = 4403,
|
||||
timeout = 60000,
|
||||
): Promise<TransportNode> {
|
||||
public static create(hostname: string, port = 4403, timeout = 60000): Promise<TransportNode> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = new Socket();
|
||||
|
||||
@@ -65,10 +60,7 @@ export class TransportNode implements Types.Transport {
|
||||
this.socket?.removeAllListeners();
|
||||
this.socket?.destroy();
|
||||
if (!this.closingByUser) {
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"socket-error",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "socket-error");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -82,10 +74,7 @@ export class TransportNode implements Types.Transport {
|
||||
});
|
||||
|
||||
this.socket.on("timeout", () => {
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"socket-timeout",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "socket-timeout");
|
||||
this.socket?.removeAllListeners();
|
||||
this.socket?.destroy();
|
||||
});
|
||||
@@ -94,18 +83,13 @@ export class TransportNode implements Types.Transport {
|
||||
if (this.closingByUser) {
|
||||
return; // suppress close-derived disconnect in user flow
|
||||
}
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"socket-closed",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "socket-closed");
|
||||
});
|
||||
|
||||
this.abortController = new AbortController();
|
||||
const abortController = this.abortController;
|
||||
|
||||
const fromDeviceSource = Readable.toWeb(
|
||||
connection,
|
||||
) as ReadableStream<Uint8Array>;
|
||||
const fromDeviceSource = Readable.toWeb(connection) as ReadableStream<Uint8Array>;
|
||||
const transformed = fromDeviceSource.pipeThrough(Utils.fromDeviceStream());
|
||||
|
||||
this._fromDevice = new ReadableStream<Types.DeviceOutput>({
|
||||
@@ -129,14 +113,9 @@ export class TransportNode implements Types.Transport {
|
||||
if (this.closingByUser || this.errored) {
|
||||
ctrl.close();
|
||||
} else {
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"read-error",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "read-error");
|
||||
|
||||
ctrl.error(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
ctrl.error(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
try {
|
||||
await transformed.cancel();
|
||||
|
||||
@@ -28,11 +28,7 @@ function stubWebBluetooth() {
|
||||
const fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic = {
|
||||
async readValue() {
|
||||
const next = incomingQueue.shift() ?? new Uint8Array();
|
||||
return new DataView(
|
||||
next.buffer,
|
||||
next.byteOffset,
|
||||
next.byteLength,
|
||||
) as unknown as DataView;
|
||||
return new DataView(next.buffer, next.byteOffset, next.byteLength) as unknown as DataView;
|
||||
},
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
@@ -58,11 +54,7 @@ function stubWebBluetooth() {
|
||||
const u8 =
|
||||
bufferSource instanceof ArrayBuffer
|
||||
? new Uint8Array(bufferSource)
|
||||
: new Uint8Array(
|
||||
bufferSource.buffer,
|
||||
bufferSource.byteOffset,
|
||||
bufferSource.byteLength,
|
||||
);
|
||||
: new Uint8Array(bufferSource.buffer, bufferSource.byteOffset, bufferSource.byteLength);
|
||||
lastWritten = new Uint8Array(u8);
|
||||
},
|
||||
} as unknown as BluetoothRemoteGATTCharacteristic;
|
||||
@@ -104,20 +96,10 @@ function stubWebBluetooth() {
|
||||
return primaryService;
|
||||
},
|
||||
device: {
|
||||
addEventListener: (
|
||||
...args: Parameters<EventTarget["addEventListener"]>
|
||||
) =>
|
||||
deviceEmitter.addEventListener(
|
||||
args[0] as string,
|
||||
args[1] as (e: Event) => void,
|
||||
),
|
||||
removeEventListener: (
|
||||
...args: Parameters<EventTarget["removeEventListener"]>
|
||||
) =>
|
||||
deviceEmitter.removeEventListener(
|
||||
args[0] as string,
|
||||
args[1] as (e: Event) => void,
|
||||
),
|
||||
addEventListener: (...args: Parameters<EventTarget["addEventListener"]>) =>
|
||||
deviceEmitter.addEventListener(args[0] as string, args[1] as (e: Event) => void),
|
||||
removeEventListener: (...args: Parameters<EventTarget["removeEventListener"]>) =>
|
||||
deviceEmitter.removeEventListener(args[0] as string, args[1] as (e: Event) => void),
|
||||
} as unknown as BluetoothDevice,
|
||||
} as unknown as BluetoothRemoteGATTServer;
|
||||
|
||||
@@ -134,10 +116,7 @@ function stubWebBluetooth() {
|
||||
},
|
||||
};
|
||||
|
||||
vi.stubGlobal(
|
||||
"navigator",
|
||||
Object.assign({}, globalThis.navigator, fakeNavigator),
|
||||
);
|
||||
vi.stubGlobal("navigator", Object.assign({}, globalThis.navigator, fakeNavigator));
|
||||
|
||||
// helper actions for tests/contract
|
||||
return {
|
||||
@@ -165,25 +144,20 @@ describe("TransportWebBluetooth (contract)", () => {
|
||||
name: "TransportWebBluetooth",
|
||||
setup: () => {},
|
||||
teardown: () => {
|
||||
(
|
||||
globalThis as unknown as { __ble?: ReturnType<typeof stubWebBluetooth> }
|
||||
).__ble?.cleanup();
|
||||
(
|
||||
globalThis as unknown as { __ble?: ReturnType<typeof stubWebBluetooth> }
|
||||
).__ble = undefined;
|
||||
(globalThis as unknown as { __ble?: ReturnType<typeof stubWebBluetooth> }).__ble?.cleanup();
|
||||
(globalThis as unknown as { __ble?: ReturnType<typeof stubWebBluetooth> }).__ble = undefined;
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
},
|
||||
create: async () => {
|
||||
(
|
||||
globalThis as unknown as { __ble: ReturnType<typeof stubWebBluetooth> }
|
||||
).__ble = stubWebBluetooth();
|
||||
(globalThis as unknown as { __ble: ReturnType<typeof stubWebBluetooth> }).__ble =
|
||||
stubWebBluetooth();
|
||||
return await TransportWebBluetooth.create();
|
||||
},
|
||||
pushIncoming: async (bytes) => {
|
||||
(
|
||||
globalThis as unknown as { __ble: ReturnType<typeof stubWebBluetooth> }
|
||||
).__ble.pushIncoming(bytes);
|
||||
(globalThis as unknown as { __ble: ReturnType<typeof stubWebBluetooth> }).__ble.pushIncoming(
|
||||
bytes,
|
||||
);
|
||||
await Promise.resolve();
|
||||
},
|
||||
assertLastWritten: (bytes) => {
|
||||
|
||||
@@ -28,8 +28,7 @@ export class TransportWebBluetooth implements Types.Transport {
|
||||
private fromNumCharacteristic: BluetoothRemoteGATTCharacteristic;
|
||||
private gattServer: BluetoothRemoteGATTServer;
|
||||
|
||||
private lastStatus: Types.DeviceStatusEnum =
|
||||
Types.DeviceStatusEnum.DeviceDisconnected;
|
||||
private lastStatus: Types.DeviceStatusEnum = Types.DeviceStatusEnum.DeviceDisconnected;
|
||||
|
||||
private closingByUser = false;
|
||||
private reading = false;
|
||||
@@ -46,10 +45,7 @@ export class TransportWebBluetooth implements Types.Transport {
|
||||
if (this.closingByUser) {
|
||||
return;
|
||||
}
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"gatt-disconnected",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "gatt-disconnected");
|
||||
};
|
||||
private onFromNumChanged = () => {
|
||||
void this.readFromRadio();
|
||||
@@ -68,9 +64,7 @@ export class TransportWebBluetooth implements Types.Transport {
|
||||
/**
|
||||
* Creates a transport from an existing, user-provided {@link BluetoothDevice}.
|
||||
*/
|
||||
public static async createFromDevice(
|
||||
device: BluetoothDevice,
|
||||
): Promise<TransportWebBluetooth> {
|
||||
public static async createFromDevice(device: BluetoothDevice): Promise<TransportWebBluetooth> {
|
||||
return await TransportWebBluetooth.prepareConnection(device);
|
||||
}
|
||||
|
||||
@@ -80,17 +74,13 @@ export class TransportWebBluetooth implements Types.Transport {
|
||||
*
|
||||
* @throws if required services or characteristics are missing.
|
||||
*/
|
||||
public static async prepareConnection(
|
||||
device: BluetoothDevice,
|
||||
): Promise<TransportWebBluetooth> {
|
||||
public static async prepareConnection(device: BluetoothDevice): Promise<TransportWebBluetooth> {
|
||||
const gattServer = await device.gatt?.connect();
|
||||
if (!gattServer) {
|
||||
throw new Error("Failed to connect to GATT server");
|
||||
}
|
||||
|
||||
const service = await gattServer.getPrimaryService(
|
||||
TransportWebBluetooth.ServiceUuid,
|
||||
);
|
||||
const service = await gattServer.getPrimaryService(TransportWebBluetooth.ServiceUuid);
|
||||
const toRadioCharacteristic = await service.getCharacteristic(
|
||||
TransportWebBluetooth.ToRadioUuid,
|
||||
);
|
||||
@@ -101,11 +91,7 @@ export class TransportWebBluetooth implements Types.Transport {
|
||||
TransportWebBluetooth.FromNumUuid,
|
||||
);
|
||||
|
||||
if (
|
||||
!toRadioCharacteristic ||
|
||||
!fromRadioCharacteristic ||
|
||||
!fromNumCharacteristic
|
||||
) {
|
||||
if (!toRadioCharacteristic || !fromRadioCharacteristic || !fromNumCharacteristic) {
|
||||
throw new Error("Failed to find required characteristics");
|
||||
}
|
||||
|
||||
@@ -137,10 +123,7 @@ export class TransportWebBluetooth implements Types.Transport {
|
||||
this.fromDeviceController = ctrl;
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceConnecting);
|
||||
|
||||
this.gattServer.device.addEventListener(
|
||||
"gattserverdisconnected",
|
||||
this.onGattDisconnected,
|
||||
);
|
||||
this.gattServer.device.addEventListener("gattserverdisconnected", this.onGattDisconnected);
|
||||
|
||||
try {
|
||||
await this.fromNumCharacteristic.startNotifications();
|
||||
@@ -152,10 +135,7 @@ export class TransportWebBluetooth implements Types.Transport {
|
||||
// prime once in case data already queued
|
||||
void this.readFromRadio();
|
||||
} catch {
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"notify-failed",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "notify-failed");
|
||||
this.gattServer.device.removeEventListener(
|
||||
"gattserverdisconnected",
|
||||
this.onGattDisconnected,
|
||||
@@ -171,10 +151,7 @@ export class TransportWebBluetooth implements Types.Transport {
|
||||
await this.toRadioCharacteristic.writeValue(ab);
|
||||
void this.readFromRadio(); // ensure we read any response
|
||||
} catch (error) {
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"write-error",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "write-error");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -205,10 +182,7 @@ export class TransportWebBluetooth implements Types.Transport {
|
||||
"characteristicvaluechanged",
|
||||
this.onFromNumChanged,
|
||||
);
|
||||
this.gattServer.device.removeEventListener(
|
||||
"gattserverdisconnected",
|
||||
this.onGattDisconnected,
|
||||
);
|
||||
this.gattServer.device.removeEventListener("gattserverdisconnected", this.onGattDisconnected);
|
||||
|
||||
this.gattServer.disconnect();
|
||||
} finally {
|
||||
@@ -238,10 +212,7 @@ export class TransportWebBluetooth implements Types.Transport {
|
||||
}
|
||||
} catch (error) {
|
||||
if (!this.closingByUser) {
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"read-error",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "read-error");
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
|
||||
@@ -27,11 +27,7 @@ function stubCoreTransforms() {
|
||||
const restoreFrom = vi
|
||||
.spyOn(Utils, "fromDeviceStream")
|
||||
.mockImplementation(
|
||||
() =>
|
||||
fromDeviceFactory() as unknown as TransformStream<
|
||||
Uint8Array,
|
||||
Types.DeviceOutput
|
||||
>,
|
||||
() => fromDeviceFactory() as unknown as TransformStream<Uint8Array, Types.DeviceOutput>,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -47,18 +43,12 @@ function stubNavigatorSerial() {
|
||||
const handlers = new Set<SerialDisconnectHandler>();
|
||||
|
||||
const serialStub = {
|
||||
addEventListener: (
|
||||
type: string,
|
||||
handler: EventListenerOrEventListenerObject,
|
||||
) => {
|
||||
addEventListener: (type: string, handler: EventListenerOrEventListenerObject) => {
|
||||
if (type === "disconnect") {
|
||||
handlers.add(handler as any as SerialDisconnectHandler);
|
||||
}
|
||||
},
|
||||
removeEventListener: (
|
||||
type: string,
|
||||
handler: EventListenerOrEventListenerObject,
|
||||
) => {
|
||||
removeEventListener: (type: string, handler: EventListenerOrEventListenerObject) => {
|
||||
if (type === "disconnect") {
|
||||
handlers.delete(handler as any as SerialDisconnectHandler);
|
||||
}
|
||||
@@ -182,9 +172,7 @@ describe("TransportWebSerial (contract)", () => {
|
||||
expect((globalThis as any).__ws.fake.lastWritten).toEqual(bytes);
|
||||
},
|
||||
triggerDisconnect: async () => {
|
||||
(globalThis as any).__ws.serial.dispatchDisconnect(
|
||||
(globalThis as any).__ws.fake,
|
||||
);
|
||||
(globalThis as any).__ws.serial.dispatchDisconnect((globalThis as any).__ws.fake);
|
||||
await Promise.resolve();
|
||||
},
|
||||
});
|
||||
@@ -230,10 +218,7 @@ describe("TransportWebSerial (extras)", () => {
|
||||
let saw = false;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const { value } = await reader.read();
|
||||
if (
|
||||
value?.type === "status" &&
|
||||
value.data.reason === "serial-disconnected"
|
||||
) {
|
||||
if (value?.type === "status" && value.data.reason === "serial-disconnected") {
|
||||
saw = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@ export class TransportWebSerial implements Types.Transport {
|
||||
private abortController: AbortController;
|
||||
private portReadable: ReadableStream<Uint8Array>;
|
||||
|
||||
private lastStatus: Types.DeviceStatusEnum =
|
||||
Types.DeviceStatusEnum.DeviceDisconnected;
|
||||
private lastStatus: Types.DeviceStatusEnum = Types.DeviceStatusEnum.DeviceDisconnected;
|
||||
private closingByUser = false;
|
||||
|
||||
/**
|
||||
@@ -68,10 +67,7 @@ export class TransportWebSerial implements Types.Transport {
|
||||
}
|
||||
console.error("Error piping data to serial port:", err);
|
||||
this.connection.close().catch(() => {});
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"write-error",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "write-error");
|
||||
});
|
||||
|
||||
this._toDevice = toDeviceTransform.writable;
|
||||
@@ -83,18 +79,13 @@ export class TransportWebSerial implements Types.Transport {
|
||||
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceConnecting);
|
||||
|
||||
const transformed = this.portReadable.pipeThrough(
|
||||
Utils.fromDeviceStream(),
|
||||
);
|
||||
const transformed = this.portReadable.pipeThrough(Utils.fromDeviceStream());
|
||||
const reader = transformed.getReader();
|
||||
|
||||
const onOsDisconnect = (ev: Event) => {
|
||||
const { port } = ev as unknown as { port?: SerialPort };
|
||||
if (port && port === this.connection) {
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"serial-disconnected",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "serial-disconnected");
|
||||
}
|
||||
};
|
||||
navigator.serial.addEventListener("disconnect", onOsDisconnect);
|
||||
@@ -112,10 +103,7 @@ export class TransportWebSerial implements Types.Transport {
|
||||
ctrl.close();
|
||||
} catch (error) {
|
||||
if (!this.closingByUser) {
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"read-error",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "read-error");
|
||||
}
|
||||
ctrl.error(error instanceof Error ? error : new Error(String(error)));
|
||||
try {
|
||||
@@ -210,19 +198,13 @@ export class TransportWebSerial implements Types.Transport {
|
||||
return;
|
||||
}
|
||||
console.error("Error piping data to serial port (reconnect):", error);
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"write-error",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "write-error");
|
||||
});
|
||||
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceConnected, "reconnected");
|
||||
} catch (error) {
|
||||
// Couldn’t re-pipe
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"reconnect-failed",
|
||||
);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "reconnect-failed");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,7 @@ export function ThemeProvider({
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
@@ -68,9 +67,7 @@ export const useTheme = () => {
|
||||
|
||||
// If the provider is missing, context will be initialState (setTheme is a no-op)
|
||||
if (context.setTheme === initialState.setTheme) {
|
||||
throw new Error(
|
||||
"useTheme must be used within a ThemeProvider: provider is missing",
|
||||
);
|
||||
throw new Error("useTheme must be used within a ThemeProvider: provider is missing");
|
||||
}
|
||||
|
||||
return context;
|
||||
|
||||
@@ -9,14 +9,12 @@ const badgeVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -30,16 +28,11 @@ function Badge({
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
<Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,8 @@ const buttonVariants = cva(
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />;
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
|
||||
@@ -4,29 +4,20 @@ import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
@@ -49,12 +40,8 @@ function DropdownMenuContent({
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
);
|
||||
function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
@@ -109,12 +96,7 @@ function DropdownMenuCheckboxItem({
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
@@ -152,10 +134,7 @@ function DropdownMenuLabel({
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
className={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -174,25 +153,17 @@ function DropdownMenuSeparator({
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,21 +10,15 @@ function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
@@ -101,10 +95,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
|
||||
@@ -15,12 +15,7 @@ import {
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -96,10 +91,7 @@ function SidebarProvider({
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
@@ -253,11 +245,7 @@ function Sidebar({
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
@@ -318,10 +306,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Input>) {
|
||||
function SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
@@ -354,10 +339,7 @@ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
@@ -437,10 +419,7 @@ function SidebarGroupAction({
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
function SidebarGroupContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
@@ -577,10 +556,7 @@ function SidebarMenuAction({
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-badge"
|
||||
@@ -618,12 +594,7 @@ function SidebarMenuSkeleton({
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
|
||||
<Skeleton
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
@@ -652,10 +623,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
|
||||
@@ -16,9 +16,7 @@ function TooltipProvider({
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
@@ -26,9 +24,7 @@ function Tooltip({
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ import * as React from "react";
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
|
||||
@@ -2,11 +2,7 @@ import type { LucideIcon } from "lucide-react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -60,7 +56,7 @@ const AppSidebar = ({
|
||||
navigationLabel,
|
||||
items,
|
||||
footer,
|
||||
variant,
|
||||
variant: _variant,
|
||||
...props
|
||||
}: SidebarSectionProps & AppSidebarProps) => {
|
||||
const { state } = useSidebar();
|
||||
@@ -69,17 +65,11 @@ const AppSidebar = ({
|
||||
return (
|
||||
<Sidebar collapsible="icon" variant="sidebar" {...props}>
|
||||
<SidebarHeader
|
||||
className={cn(
|
||||
"h-14 mt-2 flex-shrink-0 transition-all duration-300 ease-in-out",
|
||||
)}
|
||||
className={cn("h-14 mt-2 flex-shrink-0 transition-all duration-300 ease-in-out")}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{logo && (
|
||||
<img
|
||||
src={logo.src}
|
||||
alt={logo.alt}
|
||||
className="size-10 flex-shrink-0 rounded-xl"
|
||||
/>
|
||||
<img src={logo.src} alt={logo.alt} className="size-10 flex-shrink-0 rounded-xl" />
|
||||
)}
|
||||
{title && (
|
||||
<h2
|
||||
@@ -150,12 +140,9 @@ const AppSidebar = ({
|
||||
onClick={subLink.onClick}
|
||||
isActive={subLink.active}
|
||||
>
|
||||
{subLink.icon && (
|
||||
<subLink.icon className="size-4" />
|
||||
)}
|
||||
{subLink.icon && <subLink.icon className="size-4" />}
|
||||
<span>{subLink.name}</span>
|
||||
{subLink.count !== undefined &&
|
||||
subLink.count > 0 && (
|
||||
{subLink.count !== undefined && subLink.count > 0 && (
|
||||
<Badge
|
||||
variant="default"
|
||||
className="ml-auto bg-blue-500 text-white text-xs"
|
||||
@@ -184,10 +171,7 @@ const AppSidebar = ({
|
||||
{link.icon && <link.icon className="size-5" />}
|
||||
<span>{link.name}</span>
|
||||
{link.count !== undefined && link.count > 0 && (
|
||||
<Badge
|
||||
variant="default"
|
||||
className="ml-auto bg-blue-500 text-white text-xs"
|
||||
>
|
||||
<Badge variant="default" className="ml-auto bg-blue-500 text-white text-xs">
|
||||
{link.count}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -200,11 +184,7 @@ const AppSidebar = ({
|
||||
</SidebarGroup>
|
||||
|
||||
{children && (
|
||||
<div
|
||||
className={cn("flex-1 min-h-0", isCollapsed && "overflow-hidden")}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className={cn("flex-1 min-h-0", isCollapsed && "overflow-hidden")}>{children}</div>
|
||||
)}
|
||||
</SidebarContent>
|
||||
|
||||
|
||||
@@ -21,15 +21,9 @@ export function ThemeToggle() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxfmt/configuration_schema.json",
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 80,
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"protobufs",
|
||||
"**/locales/*-*/*.json",
|
||||
"**/packages/ui/"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "react-perf", "jsx-a11y", "typescript", "import"],
|
||||
"rules": {
|
||||
"no-debugger": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"import/no-unused-modules": "error"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"protobufs",
|
||||
"**/locales/*-*/*.json",
|
||||
"**/packages/ui/"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.test.ts", "**/*.test.tsx", "**/__tests__/**/*.{ts,tsx}"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -17,8 +17,12 @@
|
||||
"setup:certs": "mkcert localhost 127.0.0.1 ::1",
|
||||
"build": "vite build",
|
||||
"build:analyze": "BUNDLE_ANALYZE=true pnpm run build",
|
||||
"check": "biome check src/",
|
||||
"check:fix": "biome check --write src/",
|
||||
"lint": "oxlint",
|
||||
"lint:fix": "oxlint --fix",
|
||||
"format": "oxfmt --check .",
|
||||
"format:fix": "oxfmt .",
|
||||
"check": "pnpm lint && pnpm format",
|
||||
"check:fix": "pnpm lint:fix && pnpm format:fix",
|
||||
"dev": "vite",
|
||||
"dev:https": "USE_HTTPS=true vite",
|
||||
"test": "vitest",
|
||||
@@ -104,13 +108,14 @@
|
||||
"autoprefixer": "^10.4.21",
|
||||
"gzipper": "^8.2.1",
|
||||
"happy-dom": "^20.0.2",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tar": "^7.5.11",
|
||||
"testing-library": "^0.0.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^3.2.4",
|
||||
"oxfmt": "^0.16.0",
|
||||
"oxlint": "^1.41.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,5 @@ export function App() {
|
||||
</div>
|
||||
</DeviceWrapper>
|
||||
</ErrorBoundary>
|
||||
// </ThemeProvider>
|
||||
);
|
||||
); // </ThemeProvider>
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ export interface DeviceWrapperProps {
|
||||
|
||||
export const DeviceWrapper = ({ children, deviceId }: DeviceWrapperProps) => {
|
||||
return (
|
||||
<CurrentDeviceContext.Provider value={{ deviceId }}>
|
||||
{children}
|
||||
</CurrentDeviceContext.Provider>
|
||||
<CurrentDeviceContext.Provider value={{ deviceId }}>{children}</CurrentDeviceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("@components/UI/Checkbox.tsx", () => ({
|
||||
Checkbox: ({
|
||||
id,
|
||||
checked,
|
||||
onChange,
|
||||
}: {
|
||||
id: string;
|
||||
checked: boolean;
|
||||
onChange: () => void;
|
||||
}) => (
|
||||
<input
|
||||
data-testid="checkbox"
|
||||
type="checkbox"
|
||||
id={id}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
/>
|
||||
Checkbox: ({ id, checked, onChange }: { id: string; checked: boolean; onChange: () => void }) => (
|
||||
<input data-testid="checkbox" type="checkbox" id={id} checked={checked} onChange={onChange} />
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
BatteryFullIcon,
|
||||
BatteryLowIcon,
|
||||
BatteryMediumIcon,
|
||||
PlugZapIcon,
|
||||
} from "lucide-react";
|
||||
import { BatteryFullIcon, BatteryLowIcon, BatteryMediumIcon, PlugZapIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { DeviceMetrics } from "./types.ts";
|
||||
@@ -43,10 +38,7 @@ export const getBatteryStatus = (level: number): BatteryStatusKey => {
|
||||
const BatteryStatus: React.FC<BatteryStatusProps> = ({ deviceMetrics }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (
|
||||
deviceMetrics?.batteryLevel === undefined ||
|
||||
deviceMetrics?.batteryLevel === null
|
||||
) {
|
||||
if (deviceMetrics?.batteryLevel === undefined || deviceMetrics?.batteryLevel === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,7 @@ import {
|
||||
CommandList,
|
||||
} from "@components/UI/Command.tsx";
|
||||
import { usePinnedItems } from "@core/hooks/usePinnedItems.ts";
|
||||
import {
|
||||
useAppStore,
|
||||
useDevice,
|
||||
useDeviceStore,
|
||||
useNodeDB,
|
||||
} from "@core/stores";
|
||||
import { useAppStore, useDevice, useDeviceStore, useNodeDB } from "@core/stores";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useCommandState } from "cmdk";
|
||||
@@ -62,12 +57,8 @@ export interface SubItem {
|
||||
}
|
||||
|
||||
export const CommandPalette = () => {
|
||||
const {
|
||||
commandPaletteOpen,
|
||||
setCommandPaletteOpen,
|
||||
setConnectDialogOpen,
|
||||
setSelectedDevice,
|
||||
} = useAppStore();
|
||||
const { commandPaletteOpen, setCommandPaletteOpen, setConnectDialogOpen, setSelectedDevice } =
|
||||
useAppStore();
|
||||
const { getDevices } = useDeviceStore();
|
||||
const { setDialogOpen, connection } = useDevice();
|
||||
const { getNode } = useNodeDB();
|
||||
@@ -123,9 +114,7 @@ export const CommandPalette = () => {
|
||||
label: t("manage.command.switchNode"),
|
||||
icon: ArrowLeftRightIcon,
|
||||
subItems: getDevices().map((device) => ({
|
||||
label:
|
||||
getNode(device.hardware.myNodeNum)?.user?.longName ??
|
||||
t("unknown.shortName"),
|
||||
label: getNode(device.hardware.myNodeNum)?.user?.longName ?? t("unknown.shortName"),
|
||||
icon: <Avatar nodeNum={device.hardware.myNodeNum} />,
|
||||
action() {
|
||||
setSelectedDevice(device.id);
|
||||
@@ -268,10 +257,7 @@ export const CommandPalette = () => {
|
||||
}, [setCommandPaletteOpen]);
|
||||
|
||||
return (
|
||||
<CommandDialog
|
||||
open={commandPaletteOpen}
|
||||
onOpenChange={setCommandPaletteOpen}
|
||||
>
|
||||
<CommandDialog open={commandPaletteOpen} onOpenChange={setCommandPaletteOpen}>
|
||||
<CommandInput placeholder={t("search.commandPalette")} />
|
||||
<CommandList>
|
||||
<CommandEmpty>{t("emptyState")}</CommandEmpty>
|
||||
|
||||
@@ -107,9 +107,7 @@ export const DeviceInfoPanel = ({
|
||||
label: t("batteryVoltage.title"),
|
||||
icon: ZapIcon,
|
||||
value:
|
||||
voltage !== undefined
|
||||
? `${voltage?.toPrecision(3)} V`
|
||||
: t("unknown.notAvailable", "N/A"),
|
||||
voltage !== undefined ? `${voltage?.toPrecision(3)} V` : t("unknown.notAvailable", "N/A"),
|
||||
},
|
||||
{
|
||||
id: "firmware",
|
||||
@@ -207,9 +205,7 @@ export const DeviceInfoPanel = ({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />
|
||||
)}
|
||||
{!isCollapsed && <div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
@@ -241,9 +237,7 @@ export const DeviceInfoPanel = ({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />
|
||||
)}
|
||||
{!isCollapsed && <div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
@@ -257,9 +251,7 @@ export const DeviceInfoPanel = ({
|
||||
{actionButtons.map((buttonItem) => {
|
||||
const Icon = buttonItem.icon;
|
||||
if (buttonItem.render) {
|
||||
return (
|
||||
<Fragment key={buttonItem.id}>{buttonItem.render()}</Fragment>
|
||||
);
|
||||
return <Fragment key={buttonItem.id}>{buttonItem.render()}</Fragment>;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
@@ -281,8 +273,7 @@ export const DeviceInfoPanel = ({
|
||||
"flex-shrink-0 w-4",
|
||||
"text-gray-500 dark:text-gray-400",
|
||||
"transition-colors duration-150",
|
||||
!disableHover &&
|
||||
"group-hover:text-gray-700 dark:group-hover:text-gray-200",
|
||||
!disableHover && "group-hover:text-gray-700 dark:group-hover:text-gray-200",
|
||||
)}
|
||||
/>
|
||||
<Subtle
|
||||
@@ -290,8 +281,7 @@ export const DeviceInfoPanel = ({
|
||||
"text-sm",
|
||||
"text-gray-600 dark:text-gray-300",
|
||||
"transition-colors duration-150",
|
||||
!disableHover &&
|
||||
"group-hover:text-gray-800 dark:group-hover:text-gray-100",
|
||||
!disableHover && "group-hover:text-gray-800 dark:group-hover:text-gray-100",
|
||||
)}
|
||||
>
|
||||
{buttonItem.label}
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import { SupportBadge } from "@app/components/Badge/SupportedBadge.tsx";
|
||||
import { Switch } from "@app/components/UI/Switch.tsx";
|
||||
import type {
|
||||
ConnectionType,
|
||||
NewConnection,
|
||||
} from "@app/core/stores/deviceStore/types.ts";
|
||||
import type { ConnectionType, NewConnection } from "@app/core/stores/deviceStore/types.ts";
|
||||
import { testHttpReachable } from "@app/pages/Connections/utils";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@components/UI/Tabs.tsx";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx";
|
||||
import { Link } from "@components/UI/Typography/Link.tsx";
|
||||
import {
|
||||
type BrowserFeature,
|
||||
@@ -45,9 +37,7 @@ type DialogState = {
|
||||
protocol: "http" | "https";
|
||||
url: string;
|
||||
testStatus: TestingStatus;
|
||||
btSelected:
|
||||
| { id: string; name?: string; device?: BluetoothDevice }
|
||||
| undefined;
|
||||
btSelected: { id: string; name?: string; device?: BluetoothDevice } | undefined;
|
||||
serialSelected: { vendorId?: number; productId?: number } | undefined;
|
||||
};
|
||||
|
||||
@@ -60,9 +50,7 @@ type DialogAction =
|
||||
| { type: "SET_TEST_STATUS"; payload: TestingStatus }
|
||||
| {
|
||||
type: "SET_BT_SELECTED";
|
||||
payload:
|
||||
| { id: string; name?: string; device?: BluetoothDevice }
|
||||
| undefined;
|
||||
payload: { id: string; name?: string; device?: BluetoothDevice } | undefined;
|
||||
}
|
||||
| {
|
||||
type: "SET_SERIAL_SELECTED";
|
||||
@@ -82,8 +70,7 @@ type Pane = {
|
||||
build: () => NewConnection | null;
|
||||
};
|
||||
|
||||
const featureErrors: Record<BrowserFeature, { href: string; i18nKey: string }> =
|
||||
{
|
||||
const featureErrors: Record<BrowserFeature, { href: string; i18nKey: string }> = {
|
||||
"Web Bluetooth": {
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
|
||||
i18nKey: "addConnection.validation.requiresWebBluetooth",
|
||||
@@ -96,16 +83,14 @@ const featureErrors: Record<BrowserFeature, { href: string; i18nKey: string }> =
|
||||
href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
|
||||
i18nKey: "addConnection.validation.requiresSecureContext",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const FeatureErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
|
||||
if (missingFeatures.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const browserFeatures = missingFeatures.filter(
|
||||
(feature) => feature !== "Secure Context",
|
||||
);
|
||||
const browserFeatures = missingFeatures.filter((feature) => feature !== "Secure Context");
|
||||
const needsSecureContext = missingFeatures.includes("Secure Context");
|
||||
|
||||
const needsFeature =
|
||||
@@ -167,22 +152,17 @@ const initialState: DialogState = {
|
||||
btSelected: undefined,
|
||||
serialSelected: undefined,
|
||||
};
|
||||
export const createInitialDialogState = (
|
||||
overrides?: Partial<DialogState>,
|
||||
): DialogState => {
|
||||
export const createInitialDialogState = (overrides?: Partial<DialogState>): DialogState => {
|
||||
return { ...initialState, ...(overrides ?? {}) };
|
||||
};
|
||||
|
||||
export const dialogStateInitializer = (
|
||||
overrides?: Partial<DialogState>,
|
||||
): DialogState => createInitialDialogState(overrides);
|
||||
export const dialogStateInitializer = (overrides?: Partial<DialogState>): DialogState =>
|
||||
createInitialDialogState(overrides);
|
||||
|
||||
function dialogReducer(state: DialogState, action: DialogAction): DialogState {
|
||||
switch (action.type) {
|
||||
case "RESET":
|
||||
return createInitialDialogState(
|
||||
action.payload?.isHTTPS ? { protocol: "https" } : {},
|
||||
);
|
||||
return createInitialDialogState(action.payload?.isHTTPS ? { protocol: "https" } : {});
|
||||
case "SET_TAB":
|
||||
return { ...state, tab: action.payload };
|
||||
case "SET_NAME":
|
||||
@@ -223,22 +203,13 @@ function PickerRow({
|
||||
<div className="grid gap-2">
|
||||
<Label>{label}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="subtle"
|
||||
className="gap-2"
|
||||
onClick={onPick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button variant="subtle" className="gap-2" onClick={onPick} disabled={disabled}>
|
||||
<MousePointerClick className="h-4 w-4" />
|
||||
{buttonText}
|
||||
</Button>
|
||||
<div className="text-sm text-slate-500 dark:text-slate-400 truncate">
|
||||
{display}
|
||||
<div className="text-sm text-slate-500 dark:text-slate-400 truncate">{display}</div>
|
||||
</div>
|
||||
</div>
|
||||
{helper ? (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{helper}</p>
|
||||
) : null}
|
||||
{helper ? <p className="text-xs text-slate-500 dark:text-slate-400">{helper}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -267,10 +238,8 @@ export default function AddConnectionDialog({
|
||||
const { unsupported } = useBrowserFeatureDetection();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const bluetoothSupported =
|
||||
typeof navigator !== "undefined" && "bluetooth" in navigator;
|
||||
const serialSupported =
|
||||
typeof navigator !== "undefined" && "serial" in navigator;
|
||||
const bluetoothSupported = typeof navigator !== "undefined" && "bluetooth" in navigator;
|
||||
const serialSupported = typeof navigator !== "undefined" && "serial" in navigator;
|
||||
const isURLHTTPS = isHTTPS;
|
||||
|
||||
const reset = useCallback(() => {
|
||||
@@ -299,9 +268,7 @@ export default function AddConnectionDialog({
|
||||
if (!bluetoothSupported) {
|
||||
toast({
|
||||
title: t("addConnection.bluetoothConnection.notSupported.title"),
|
||||
description: t(
|
||||
"addConnection.bluetoothConnection.notSupported.description",
|
||||
),
|
||||
description: t("addConnection.bluetoothConnection.notSupported.description"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -336,9 +303,7 @@ export default function AddConnectionDialog({
|
||||
if (!serialSupported) {
|
||||
toast({
|
||||
title: t("addConnection.serialConnection.notSupported.title"),
|
||||
description: t(
|
||||
"addConnection.serialConnection.notSupported.description",
|
||||
),
|
||||
description: t("addConnection.serialConnection.notSupported.description"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -370,9 +335,7 @@ export default function AddConnectionDialog({
|
||||
}
|
||||
toast({
|
||||
title: t("addConnection.serialConnection.portSelected.title"),
|
||||
description: t(
|
||||
"addConnection.serialConnection.portSelected.description",
|
||||
),
|
||||
description: t("addConnection.serialConnection.portSelected.description"),
|
||||
});
|
||||
} catch (err) {
|
||||
makeToastErrorHandler("Serial")(err);
|
||||
@@ -397,9 +360,7 @@ export default function AddConnectionDialog({
|
||||
dispatch({ type: "SET_TEST_STATUS", payload: "failure" });
|
||||
toast({
|
||||
title: t("addConnection.httpConnection.connectionTest.failure.title"),
|
||||
description: t(
|
||||
"addConnection.httpConnection.connectionTest.failure.description",
|
||||
),
|
||||
description: t("addConnection.httpConnection.connectionTest.failure.description"),
|
||||
});
|
||||
}
|
||||
}, [state.protocol, state.url, toast, t]);
|
||||
@@ -410,9 +371,7 @@ export default function AddConnectionDialog({
|
||||
placeholder: t("addConnection.httpConnection.namePlaceholder"),
|
||||
children: () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Label htmlFor="url">
|
||||
{t("addConnection.httpConnection.heading")}
|
||||
</Label>
|
||||
<Label htmlFor="url">{t("addConnection.httpConnection.heading")}</Label>
|
||||
|
||||
<Input
|
||||
id={"url"}
|
||||
@@ -448,23 +407,19 @@ export default function AddConnectionDialog({
|
||||
className="gap-2"
|
||||
onClick={handleTestHttp}
|
||||
disabled={
|
||||
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`)
|
||||
.success === false || state.testStatus === "testing"
|
||||
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`).success === false ||
|
||||
state.testStatus === "testing"
|
||||
}
|
||||
>
|
||||
{state.testStatus === "testing" ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{t(
|
||||
"addConnection.httpConnection.connectionTest.button.loading",
|
||||
)}
|
||||
{t("addConnection.httpConnection.connectionTest.button.loading")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MousePointerClick className="h-4 w-4" />
|
||||
{t(
|
||||
"addConnection.httpConnection.connectionTest.button.label",
|
||||
)}
|
||||
{t("addConnection.httpConnection.connectionTest.button.label")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -477,9 +432,7 @@ export default function AddConnectionDialog({
|
||||
{state.testStatus === "failure" && (
|
||||
<div className="flex items-center gap-1 text-sm text-red-600 dark:text-red-400">
|
||||
<XCircle className="h-4 w-4" />
|
||||
{t(
|
||||
"addConnection.httpConnection.connectionTest.notReachable",
|
||||
)}
|
||||
{t("addConnection.httpConnection.connectionTest.notReachable")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -489,8 +442,8 @@ export default function AddConnectionDialog({
|
||||
</div>
|
||||
),
|
||||
validate: () =>
|
||||
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`)
|
||||
.success === true && state.testStatus === "success",
|
||||
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`).success === true &&
|
||||
state.testStatus === "success",
|
||||
build: () => ({
|
||||
type: "http",
|
||||
name: state.name.trim(),
|
||||
@@ -503,12 +456,8 @@ export default function AddConnectionDialog({
|
||||
<>
|
||||
<SupportBadge
|
||||
supported={bluetoothSupported}
|
||||
labelSupported={t(
|
||||
"addConnection.bluetoothConnection.supported.title",
|
||||
)}
|
||||
labelUnsupported={t(
|
||||
"addConnection.bluetoothConnection.notSupported.title",
|
||||
)}
|
||||
labelSupported={t("addConnection.bluetoothConnection.supported.title")}
|
||||
labelUnsupported={t("addConnection.bluetoothConnection.notSupported.title")}
|
||||
/>
|
||||
<PickerRow
|
||||
label={t("addConnection.bluetoothConnection.device")}
|
||||
@@ -522,10 +471,7 @@ export default function AddConnectionDialog({
|
||||
}
|
||||
helper={t("addConnection.bluetoothConnection.helperText")}
|
||||
/>
|
||||
<FeatureErrorMessage
|
||||
missingFeatures={unsupported}
|
||||
tabId="bluetooth"
|
||||
/>
|
||||
<FeatureErrorMessage missingFeatures={unsupported} tabId="bluetooth" />
|
||||
</>
|
||||
),
|
||||
validate: () => state.name.trim().length > 0 && !!state.btSelected,
|
||||
@@ -543,12 +489,8 @@ export default function AddConnectionDialog({
|
||||
<>
|
||||
<SupportBadge
|
||||
supported={serialSupported}
|
||||
labelSupported={t(
|
||||
"addConnection.serialConnection.supported.title",
|
||||
)}
|
||||
labelUnsupported={t(
|
||||
"addConnection.serialConnection.notSupported.title",
|
||||
)}
|
||||
labelSupported={t("addConnection.serialConnection.supported.title")}
|
||||
labelUnsupported={t("addConnection.serialConnection.notSupported.title")}
|
||||
/>
|
||||
<PickerRow
|
||||
label={t("addConnection.serialConnection.port")}
|
||||
@@ -558,10 +500,8 @@ export default function AddConnectionDialog({
|
||||
display={
|
||||
state.serialSelected
|
||||
? t("addConnection.serialConnection.deviceName", {
|
||||
vendorId:
|
||||
state.serialSelected.vendorId?.toString(16) ?? "?",
|
||||
productId:
|
||||
state.serialSelected.productId?.toString(16) ?? "?",
|
||||
vendorId: state.serialSelected.vendorId?.toString(16) ?? "?",
|
||||
productId: state.serialSelected.productId?.toString(16) ?? "?",
|
||||
})
|
||||
: t("addConnection.serialConnection.notSelected")
|
||||
}
|
||||
@@ -571,8 +511,7 @@ export default function AddConnectionDialog({
|
||||
</>
|
||||
),
|
||||
validate: () =>
|
||||
state.name.trim().length > 0 &&
|
||||
(!!state.serialSelected || !serialSupported),
|
||||
state.name.trim().length > 0 && (!!state.serialSelected || !serialSupported),
|
||||
build: () => ({
|
||||
type: "serial",
|
||||
name: state.name.trim(),
|
||||
@@ -598,8 +537,7 @@ export default function AddConnectionDialog({
|
||||
const canCreate = useMemo(() => currentPane.validate(), [currentPane]);
|
||||
|
||||
const submit =
|
||||
(fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) =>
|
||||
async () => {
|
||||
(fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) => async () => {
|
||||
if (!canCreate) {
|
||||
return;
|
||||
}
|
||||
@@ -608,8 +546,7 @@ export default function AddConnectionDialog({
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
const btDevice =
|
||||
state.tab === "bluetooth" ? state.btSelected?.device : undefined;
|
||||
const btDevice = state.tab === "bluetooth" ? state.btSelected?.device : undefined;
|
||||
await fn(payload, btDevice);
|
||||
};
|
||||
|
||||
@@ -625,9 +562,7 @@ export default function AddConnectionDialog({
|
||||
>
|
||||
<Tabs
|
||||
value={state.tab}
|
||||
onValueChange={(v) =>
|
||||
dispatch({ type: "SET_TAB", payload: v as TabKey })
|
||||
}
|
||||
onValueChange={(v) => dispatch({ type: "SET_TAB", payload: v as TabKey })}
|
||||
>
|
||||
<TabsList className="grid grid-cols-3">
|
||||
{TAB_META.map(({ key, label, Icon }) => (
|
||||
@@ -647,20 +582,14 @@ export default function AddConnectionDialog({
|
||||
<Input
|
||||
id={`name-${state.tab}`}
|
||||
value={state.name}
|
||||
onChange={(evt) =>
|
||||
dispatch({ type: "SET_NAME", payload: evt.target.value })
|
||||
}
|
||||
onChange={(evt) => dispatch({ type: "SET_NAME", payload: evt.target.value })}
|
||||
placeholder={currentPane.placeholder}
|
||||
/>
|
||||
</div>
|
||||
{PANES[key].children()}
|
||||
<div className="flex justify-end">
|
||||
<div className="inline-flex rounded-md shadow-sm overflow-hidden border">
|
||||
<Button
|
||||
onClick={submit(onSave)}
|
||||
disabled={!canCreate}
|
||||
className="rounded-none"
|
||||
>
|
||||
<Button onClick={submit(onSave)} disabled={!canCreate} className="rounded-none">
|
||||
{t("button.saveConnection")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -28,18 +28,13 @@ export const urlOrIpv4Schema = z
|
||||
}
|
||||
|
||||
// IPv4 pattern
|
||||
const ipv4Regex =
|
||||
/^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
||||
const ipv4Regex = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
||||
// Domain pattern (e.g. example.com, meshtastic.local)
|
||||
const domainRegex = /^(?!-)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,}$/;
|
||||
// Local domain (e.g. meshtastic.local)
|
||||
const localDomainRegex = /^(?!-)[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.local$/;
|
||||
|
||||
return (
|
||||
ipv4Regex.test(host) ||
|
||||
domainRegex.test(host) ||
|
||||
localDomainRegex.test(host)
|
||||
);
|
||||
return ipv4Regex.test(host) || domainRegex.test(host) || localDomainRegex.test(host);
|
||||
}, "Must be a valid IPv4 address or domain name with optional port (10-65535)")
|
||||
.transform((val) => {
|
||||
return /^https?:\/\//i.test(val) ? val : `http://${val}`;
|
||||
|
||||
+1
-3
@@ -47,9 +47,7 @@ describe("ClearAllStoresDialog", () => {
|
||||
|
||||
it("calls clearAllStores and navigates to '/' when confirm is clicked", () => {
|
||||
render(<ClearAllStoresDialog open onOpenChange={mockOnOpenChange} />);
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Clear all local storage" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Clear all local storage" }));
|
||||
|
||||
expect(mockClearAllStores).toHaveBeenCalledTimes(1);
|
||||
expect(assignedHref).toBe("/"); // forced reload target
|
||||
|
||||
@@ -7,10 +7,7 @@ export interface ClearAllStoresDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const ClearAllStoresDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ClearAllStoresDialogProps) => {
|
||||
export const ClearAllStoresDialog = ({ open, onOpenChange }: ClearAllStoresDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
|
||||
const handleClearAllStores = () => {
|
||||
|
||||
+1
-4
@@ -14,10 +14,7 @@ export interface ClientNotificationDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const ClientNotificationDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ClientNotificationDialogProps) => {
|
||||
export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificationDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { getClientNotification, removeClientNotification } = useDevice();
|
||||
|
||||
|
||||
+3
-9
@@ -46,19 +46,13 @@ describe("DeleteMessagesDialog", () => {
|
||||
it("renders the dialog when open is true", () => {
|
||||
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
|
||||
expect(screen.getByText("Clear All Messages")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/This action will clear all message history./),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/This action will clear all message history./)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Dismiss" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Clear Messages" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Clear Messages" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render the dialog when open is false", () => {
|
||||
render(
|
||||
<DeleteMessagesDialog open={false} onOpenChange={mockOnOpenChange} />,
|
||||
);
|
||||
render(<DeleteMessagesDialog open={false} onOpenChange={mockOnOpenChange} />);
|
||||
expect(screen.queryByText("Clear All Messages")).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -8,10 +8,7 @@ export interface DeleteMessagesDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const DeleteMessagesDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: DeleteMessagesDialogProps) => {
|
||||
export const DeleteMessagesDialog = ({ open, onOpenChange }: DeleteMessagesDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const messageStore = useMessages();
|
||||
|
||||
|
||||
+1
-3
@@ -33,9 +33,7 @@ describe("FactoryResetConfigDialog", () => {
|
||||
it("calls factoryResetConfig and then closes the dialog on confirm", async () => {
|
||||
render(<FactoryResetConfigDialog open onOpenChange={mockOnOpenChange} />);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Factory Reset Config" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Factory Reset Config" }));
|
||||
|
||||
expect(mockFactoryReset).toHaveBeenCalledTimes(1);
|
||||
|
||||
|
||||
+1
-4
@@ -8,10 +8,7 @@ export interface FactoryResetConfigDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const FactoryResetConfigDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: FactoryResetConfigDialogProps) => {
|
||||
export const FactoryResetConfigDialog = ({ open, onOpenChange }: FactoryResetConfigDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { connection } = useDevice();
|
||||
|
||||
|
||||
+1
-3
@@ -61,9 +61,7 @@ describe("FactoryResetDeviceDialog", () => {
|
||||
);
|
||||
|
||||
render(<FactoryResetDeviceDialog open onOpenChange={mockOnOpenChange} />);
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Factory Reset Device" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Factory Reset Device" }));
|
||||
|
||||
// Called immediately
|
||||
expect(mockFactoryResetDevice).toHaveBeenCalledTimes(1);
|
||||
|
||||
+2
-10
@@ -1,10 +1,5 @@
|
||||
import { toast } from "@core/hooks/useToast.ts";
|
||||
import {
|
||||
useDevice,
|
||||
useDeviceStore,
|
||||
useMessageStore,
|
||||
useNodeDBStore,
|
||||
} from "@core/stores";
|
||||
import { useDevice, useDeviceStore, useMessageStore, useNodeDBStore } from "@core/stores";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DialogWrapper } from "../DialogWrapper.tsx";
|
||||
|
||||
@@ -13,10 +8,7 @@ export interface FactoryResetDeviceDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const FactoryResetDeviceDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: FactoryResetDeviceDialogProps) => {
|
||||
export const FactoryResetDeviceDialog = ({ open, onOpenChange }: FactoryResetDeviceDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { connection, id } = useDevice();
|
||||
|
||||
|
||||
@@ -47,8 +47,7 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
try {
|
||||
const channelsUrl = new URL(importDialogInput);
|
||||
if (
|
||||
(channelsUrl.hostname !== "meshtastic.org" &&
|
||||
channelsUrl.pathname !== "/e/") ||
|
||||
(channelsUrl.hostname !== "meshtastic.org" && channelsUrl.pathname !== "/e/") ||
|
||||
!channelsUrl.hash
|
||||
) {
|
||||
throw t("import.error.invalidUrl");
|
||||
@@ -56,11 +55,7 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
|
||||
const encodedChannelConfig = channelsUrl.hash.substring(1);
|
||||
const paddedString = encodedChannelConfig
|
||||
.padEnd(
|
||||
encodedChannelConfig.length +
|
||||
((4 - (encodedChannelConfig.length % 4)) % 4),
|
||||
"=",
|
||||
)
|
||||
.padEnd(encodedChannelConfig.length + ((4 - (encodedChannelConfig.length % 4)) % 4), "=")
|
||||
.replace(/-/g, "+")
|
||||
.replace(/_/g, "/");
|
||||
|
||||
@@ -75,15 +70,14 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
setImportIndex(newImportChannelArray);
|
||||
setUpdateConfig(newChannelSet?.loraConfig !== undefined);
|
||||
setValidUrl(true);
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
setValidUrl(false);
|
||||
setChannelSet(undefined);
|
||||
}
|
||||
}, [importDialogInput, t]);
|
||||
|
||||
const apply = () => {
|
||||
channelSet?.settings.forEach(
|
||||
(ch: Protobuf.Channel.ChannelSettings, index: number) => {
|
||||
channelSet?.settings.forEach((ch: Protobuf.Channel.ChannelSettings, index: number) => {
|
||||
if (importIndex[index] === -1) {
|
||||
return;
|
||||
}
|
||||
@@ -97,21 +91,14 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
settings: ch,
|
||||
});
|
||||
|
||||
if (
|
||||
!deepCompareConfig(
|
||||
channels.get(importIndex[index] ?? 0),
|
||||
payload,
|
||||
true,
|
||||
)
|
||||
) {
|
||||
if (!deepCompareConfig(channels.get(importIndex[index] ?? 0), payload, true)) {
|
||||
setChange(
|
||||
{ type: "channel", index: importIndex[index] ?? 0 },
|
||||
payload,
|
||||
channels.get(importIndex[index] ?? 0),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (channelSet?.loraConfig && updateConfig) {
|
||||
const payload = {
|
||||
@@ -146,23 +133,14 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("import.title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans
|
||||
i18nKey={"import.description"}
|
||||
components={{ italic: <i />, br: <br /> }}
|
||||
/>
|
||||
<Trans i18nKey={"import.description"} components={{ italic: <i />, br: <br /> }} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>{t("import.channelSetUrl")}</Label>
|
||||
<Input
|
||||
value={importDialogInput}
|
||||
variant={
|
||||
importDialogInput === ""
|
||||
? "default"
|
||||
: validUrl
|
||||
? "dirty"
|
||||
: "invalid"
|
||||
}
|
||||
variant={importDialogInput === "" ? "default" : validUrl ? "dirty" : "invalid"}
|
||||
onChange={(e) => {
|
||||
setImportDialogInput(e.target.value);
|
||||
}}
|
||||
@@ -191,10 +169,7 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
<span className="flex-1">{t("import.channelSlot")}</span>
|
||||
</div>
|
||||
{channelSet?.settings.map((channel, index) => (
|
||||
<div
|
||||
className="flex items-center"
|
||||
key={`channel_${channel.id}_${index}`}
|
||||
>
|
||||
<div className="flex items-center" key={`channel_${channel.id}_${index}`}>
|
||||
<Label className="flex-1">
|
||||
{channel.name.length
|
||||
? channel.name
|
||||
@@ -214,14 +189,10 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
|
||||
disabled={importIndex.includes(i) && index !== i}
|
||||
value={i.toString()}
|
||||
>
|
||||
{i === 0
|
||||
? t("import.primary")
|
||||
: `${t("import.channelPrefix")}${i}`}
|
||||
{i === 0 ? t("import.primary") : `${t("import.channelPrefix")}${i}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="-1">
|
||||
{t("import.doNotImport")}
|
||||
</SelectItem>
|
||||
<SelectItem value="-1">{t("import.doNotImport")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -27,13 +27,10 @@ export const LocationResponseDialog = ({
|
||||
|
||||
const from = getNode(location?.from ?? 0);
|
||||
const longName =
|
||||
from?.user?.longName ??
|
||||
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
|
||||
from?.user?.longName ?? (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
|
||||
const shortName =
|
||||
from?.user?.shortName ??
|
||||
(from
|
||||
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
|
||||
: t("unknown.shortName"));
|
||||
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : t("unknown.shortName"));
|
||||
|
||||
const position = location?.data;
|
||||
|
||||
@@ -70,23 +67,18 @@ export const LocationResponseDialog = ({
|
||||
rel="noreferrer"
|
||||
>
|
||||
{" "}
|
||||
{position.latitudeI ?? 0 / 1e7},{" "}
|
||||
{position.longitudeI ?? 0 / 1e7}
|
||||
{position.latitudeI ?? 0 / 1e7}, {position.longitudeI ?? 0 / 1e7}
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
{t("locationResponse.altitude")} {position.altitude}
|
||||
{(position.altitude ?? 0) < 1
|
||||
? t("unit.meter.one")
|
||||
: t("unit.meter.plural")}
|
||||
{(position.altitude ?? 0) < 1 ? t("unit.meter.one") : t("unit.meter.plural")}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
// Optional: Show a message if coordinates are not available
|
||||
<p className="text-textPrimary">
|
||||
{t("locationResponse.noCoordinates")}
|
||||
</p>
|
||||
<p className="text-textPrimary">{t("locationResponse.noCoordinates")}</p>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogContent>
|
||||
|
||||
@@ -18,11 +18,7 @@ export interface ManagedModeDialogProps {
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
export const ManagedModeDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
}: ManagedModeDialogProps) => {
|
||||
export const ManagedModeDialog = ({ open, onOpenChange, onSubmit }: ManagedModeDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const [confirmState, setConfirmState] = useState(false);
|
||||
|
||||
@@ -47,9 +43,7 @@ export const ManagedModeDialog = ({
|
||||
onChange={() => setConfirmState(!confirmState)}
|
||||
name="confirmUnderstanding"
|
||||
>
|
||||
<p className="dark:text-white pt-1">
|
||||
{t("managedMode.confirmUnderstanding")}
|
||||
</p>
|
||||
<p className="dark:text-white pt-1">{t("managedMode.confirmUnderstanding")}</p>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -8,12 +8,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@components/UI/Dialog.tsx";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@components/UI/Tabs.tsx";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx";
|
||||
import {
|
||||
type BrowserFeature,
|
||||
useBrowserFeatureDetection,
|
||||
@@ -63,9 +58,7 @@ const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const browserFeatures = missingFeatures.filter(
|
||||
(feature) => feature !== "Secure Context",
|
||||
);
|
||||
const browserFeatures = missingFeatures.filter((feature) => feature !== "Secure Context");
|
||||
const needsSecureContext = missingFeatures.includes("Secure Context");
|
||||
|
||||
const needsFeature =
|
||||
@@ -133,17 +126,13 @@ export const NewDeviceDialog = ({ open, onOpenChange }: NewDeviceProps) => {
|
||||
id: "BLE",
|
||||
label: t("newDeviceDialog.tabBluetooth"),
|
||||
element: BLE,
|
||||
isDisabled:
|
||||
unsupported.includes("Web Bluetooth") ||
|
||||
unsupported.includes("Secure Context"),
|
||||
isDisabled: unsupported.includes("Web Bluetooth") || unsupported.includes("Secure Context"),
|
||||
},
|
||||
{
|
||||
id: "Serial",
|
||||
label: t("newDeviceDialog.tabSerial"),
|
||||
element: Serial,
|
||||
isDisabled:
|
||||
unsupported.includes("Web Serial") ||
|
||||
unsupported.includes("Secure Context"),
|
||||
isDisabled: unsupported.includes("Web Serial") || unsupported.includes("Secure Context"),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -50,10 +50,7 @@ export interface NodeDetailsDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const NodeDetailsDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: NodeDetailsDialogProps) => {
|
||||
export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { setDialogOpen, connection } = useDevice();
|
||||
const { getNode } = useNodeDB();
|
||||
@@ -64,12 +61,8 @@ export const NodeDetailsDialog = ({
|
||||
|
||||
const node = getNode(nodeNumDetails);
|
||||
|
||||
const [isFavoriteState, setIsFavoriteState] = useState<boolean>(
|
||||
node?.isFavorite ?? false,
|
||||
);
|
||||
const [isIgnoredState, setIsIgnoredState] = useState<boolean>(
|
||||
node?.isIgnored ?? false,
|
||||
);
|
||||
const [isFavoriteState, setIsFavoriteState] = useState<boolean>(node?.isFavorite ?? false);
|
||||
const [isIgnoredState, setIsIgnoredState] = useState<boolean>(node?.isIgnored ?? false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!node) {
|
||||
@@ -168,8 +161,7 @@ export const NodeDetailsDialog = ({
|
||||
key: "batteryLevel",
|
||||
label: t("nodeDetails.batteryLevel"),
|
||||
value: node.deviceMetrics?.batteryLevel,
|
||||
format: (val: number) =>
|
||||
val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`,
|
||||
format: (val: number) => (val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`),
|
||||
},
|
||||
{
|
||||
key: "voltage",
|
||||
@@ -202,27 +194,17 @@ export const NodeDetailsDialog = ({
|
||||
<DialogFooter>
|
||||
<div className="w-full ">
|
||||
<div className="flex flex-row flex-wrap space-y-1">
|
||||
<Button
|
||||
className="mr-1"
|
||||
name="message"
|
||||
onClick={handleDirectMessage}
|
||||
>
|
||||
<Button className="mr-1" name="message" onClick={handleDirectMessage}>
|
||||
<MessageSquareIcon className="mr-2" />
|
||||
{t("nodeDetails.message")}
|
||||
</Button>
|
||||
<Button
|
||||
className="mr-1"
|
||||
name="traceRoute"
|
||||
onClick={handleTraceroute}
|
||||
>
|
||||
<Button className="mr-1" name="traceRoute" onClick={handleTraceroute}>
|
||||
<WaypointsIcon className="mr-2" />
|
||||
{t("nodeDetails.traceRoute")}
|
||||
</Button>
|
||||
<Button className="mr-1" onClick={handleToggleFavorite}>
|
||||
<StarIcon
|
||||
className={cn(
|
||||
isFavoriteState ? " fill-yellow-400 stroke-yellow-400" : "",
|
||||
)}
|
||||
className={cn(isFavoriteState ? " fill-yellow-400 stroke-yellow-400" : "")}
|
||||
/>
|
||||
</Button>
|
||||
<div className="flex flex-1 justify-start" />
|
||||
@@ -243,9 +225,7 @@ export const NodeDetailsDialog = ({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-slate-800 dark:bg-slate-600 text-white px-4 py-1 rounded text-xs">
|
||||
{isIgnoredState
|
||||
? t("nodeDetails.unignoreNode")
|
||||
: t("nodeDetails.ignoreNode")}
|
||||
{isIgnoredState ? t("nodeDetails.unignoreNode") : t("nodeDetails.ignoreNode")}
|
||||
<TooltipArrow className="fill-slate-800 dark:fill-slate-600" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -275,9 +255,7 @@ export const NodeDetailsDialog = ({
|
||||
<div className="flex flex-col flex-wrap space-x-1 space-y-1">
|
||||
<div className="flex flex-row space-x-2">
|
||||
<div className="w-full bg-slate-100 text-slate-900 dark:text-slate-100 dark:bg-slate-800 p-3 rounded-lg">
|
||||
<p className="text-lg font-semibold">
|
||||
{t("nodeDetails.details")}
|
||||
</p>
|
||||
<p className="text-lg font-semibold">{t("nodeDetails.details")}</p>
|
||||
<table className="table-fixed w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -291,9 +269,10 @@ export const NodeDetailsDialog = ({
|
||||
<tr>
|
||||
<td>{t("nodeDetails.role")}</td>
|
||||
<td>
|
||||
{Protobuf.Config.Config_DeviceConfig_Role[
|
||||
node.user?.role ?? 0
|
||||
]?.replace(/_/g, " ")}
|
||||
{Protobuf.Config.Config_DeviceConfig_Role[node.user?.role ?? 0]?.replace(
|
||||
/_/g,
|
||||
" ",
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -312,44 +291,35 @@ export const NodeDetailsDialog = ({
|
||||
<td>{t("nodeDetails.hardware")}</td>
|
||||
<td>
|
||||
{(
|
||||
Protobuf.Mesh.HardwareModel[
|
||||
node.user?.hwModel ?? 0
|
||||
] ?? t("unknown.shortName")
|
||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
|
||||
t("unknown.shortName")
|
||||
).replace(/_/g, " ")}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("nodeDetails.messageable")}</td>
|
||||
<td>
|
||||
{node.user?.isUnmessagable ? t("no") : t("yes")}
|
||||
</td>
|
||||
<td>{node.user?.isUnmessagable ? t("no") : t("yes")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DeviceImage
|
||||
className="w-40 p-2 rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
||||
deviceType={
|
||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
|
||||
"UNKNOWN"
|
||||
}
|
||||
deviceType={Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ?? "UNKNOWN"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={sectionClassName}>
|
||||
<p className="text-lg font-semibold">
|
||||
{t("nodeDetails.security")}
|
||||
</p>
|
||||
<p className="text-lg font-semibold">{t("nodeDetails.security")}</p>
|
||||
<table className="table-auto w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="pr-2">{t("nodeDetails.publicKey")}</td>
|
||||
<td>
|
||||
<pre className="text-xs pt-0.5">
|
||||
{node.user?.publicKey &&
|
||||
node.user?.publicKey.length > 0
|
||||
{node.user?.publicKey && node.user?.publicKey.length > 0
|
||||
? fromByteArray(node.user.publicKey)
|
||||
: t("unknown.longName")}
|
||||
</pre>
|
||||
@@ -368,9 +338,7 @@ export const NodeDetailsDialog = ({
|
||||
</div>
|
||||
|
||||
<div className={sectionClassName}>
|
||||
<p className="text-lg font-semibold">
|
||||
{t("nodeDetails.position")}
|
||||
</p>
|
||||
<p className="text-lg font-semibold">{t("nodeDetails.position")}</p>
|
||||
|
||||
{node.position ? (
|
||||
<table className="table-auto w-full">
|
||||
@@ -387,8 +355,7 @@ export const NodeDetailsDialog = ({
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{node.position.latitudeI / 1e7},{" "}
|
||||
{node.position.longitudeI / 1e7}
|
||||
{node.position.latitudeI / 1e7}, {node.position.longitudeI / 1e7}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -407,11 +374,7 @@ export const NodeDetailsDialog = ({
|
||||
) : (
|
||||
<p>{t("unknown.longName")}</p>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleRequestPosition}
|
||||
name="requestPosition"
|
||||
className="mt-2"
|
||||
>
|
||||
<Button onClick={handleRequestPosition} name="requestPosition" className="mt-2">
|
||||
<MapPinnedIcon className="mr-2" />
|
||||
{t("nodeDetails.requestPosition")}
|
||||
</Button>
|
||||
@@ -436,9 +399,7 @@ export const NodeDetailsDialog = ({
|
||||
<tr>
|
||||
<td>{t("nodeDetails.uptime")}</td>
|
||||
<td>
|
||||
<Uptime
|
||||
seconds={node.deviceMetrics.uptimeSeconds}
|
||||
/>
|
||||
<Uptime seconds={node.deviceMetrics.uptimeSeconds} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -457,9 +418,7 @@ export const NodeDetailsDialog = ({
|
||||
</p>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="overflow-x-scroll">
|
||||
<pre className="text-xs w-full">
|
||||
{JSON.stringify(node, null, 2)}
|
||||
</pre>
|
||||
<pre className="text-xs w-full">{JSON.stringify(node, null, 2)}</pre>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
@@ -19,25 +19,19 @@ export interface PkiBackupDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const PkiBackupDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: PkiBackupDialogProps) => {
|
||||
export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { config, setDialogOpen } = useDevice();
|
||||
const { getMyNode } = useNodeDB();
|
||||
const privateKey = config.security?.privateKey;
|
||||
const publicKey = config.security?.publicKey;
|
||||
|
||||
const decodeKeyData = React.useCallback(
|
||||
(key: Uint8Array<ArrayBufferLike>) => {
|
||||
const decodeKeyData = React.useCallback((key: Uint8Array<ArrayBufferLike>) => {
|
||||
if (!key) {
|
||||
return "";
|
||||
}
|
||||
return fromByteArray(key ?? new Uint8Array(0));
|
||||
},
|
||||
[],
|
||||
);
|
||||
}, []);
|
||||
|
||||
const closeDialog = React.useCallback(() => {
|
||||
setDialogOpen("pkiBackup", false);
|
||||
@@ -133,9 +127,7 @@ export const PkiBackupDialog = ({
|
||||
<DialogTitle>{t("pkiBackup.title")}</DialogTitle>
|
||||
<DialogDescription>{t("pkiBackup.secureBackup")}</DialogDescription>
|
||||
<DialogDescription>
|
||||
<span className="font-bold break-before-auto">
|
||||
{t("pkiBackup.loseKeysWarning")}
|
||||
</span>
|
||||
<span className="font-bold break-before-auto">{t("pkiBackup.loseKeysWarning")}</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="mt-6">
|
||||
|
||||
@@ -46,11 +46,7 @@ export const PkiRegenerateDialog = ({
|
||||
<DialogDescription>{dialogText.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="destructive"
|
||||
name="regenerate"
|
||||
onClick={() => onSubmit()}
|
||||
>
|
||||
<Button variant="destructive" name="regenerate" onClick={() => onSubmit()}>
|
||||
{dialogText.button}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -24,12 +24,7 @@ export interface QRDialogProps {
|
||||
channels: Map<Types.ChannelNumber, Protobuf.Channel.Channel>;
|
||||
}
|
||||
|
||||
export const QRDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
loraConfig,
|
||||
channels,
|
||||
}: QRDialogProps) => {
|
||||
export const QRDialog = ({ open, onOpenChange, loraConfig, channels }: QRDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
|
||||
@@ -49,16 +44,12 @@ export const QRDialog = ({
|
||||
settings: channelsToEncode,
|
||||
}),
|
||||
);
|
||||
const base64 = fromByteArray(
|
||||
toBinary(Protobuf.AppOnly.ChannelSetSchema, encoded),
|
||||
)
|
||||
const base64 = fromByteArray(toBinary(Protobuf.AppOnly.ChannelSetSchema, encoded))
|
||||
.replace(/=/g, "")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_");
|
||||
|
||||
setQrCodeUrl(
|
||||
`https://meshtastic.org/e/${qrCodeAdd ? "?add=true" : ""}#${base64}`,
|
||||
);
|
||||
setQrCodeUrl(`https://meshtastic.org/e/${qrCodeAdd ? "?add=true" : ""}#${base64}`);
|
||||
}, [allChannels, selectedChannels, qrCodeAdd, loraConfig]);
|
||||
|
||||
return (
|
||||
@@ -89,14 +80,9 @@ export const QRDialog = ({
|
||||
checked={selectedChannels.includes(channel.index)}
|
||||
onChange={() => {
|
||||
if (selectedChannels.includes(channel.index)) {
|
||||
setSelectedChannels(
|
||||
selectedChannels.filter((c) => c !== channel.index),
|
||||
);
|
||||
setSelectedChannels(selectedChannels.filter((c) => c !== channel.index));
|
||||
} else {
|
||||
setSelectedChannels([
|
||||
...selectedChannels,
|
||||
channel.index,
|
||||
]);
|
||||
setSelectedChannels([...selectedChannels, channel.index]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import type {
|
||||
ButtonHTMLAttributes,
|
||||
ClassAttributes,
|
||||
InputHTMLAttributes,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import type { ButtonHTMLAttributes, ClassAttributes, InputHTMLAttributes, ReactNode } from "react";
|
||||
import type { JSX } from "react/jsx-runtime";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { RebootDialog } from "./RebootDialog.tsx";
|
||||
@@ -54,16 +49,10 @@ vi.mock("@components/UI/Input.tsx", async () => {
|
||||
vi.mock("@components/UI/Dialog.tsx", () => {
|
||||
return {
|
||||
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogContent: ({ children }: { children: ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
DialogHeader: ({ children }: { children: ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
DialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
DialogTitle: ({ children }: { children: ReactNode }) => <h1>{children}</h1>,
|
||||
DialogDescription: ({ children }: { children: ReactNode }) => (
|
||||
<p>{children}</p>
|
||||
),
|
||||
DialogDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
|
||||
DialogClose: () => null,
|
||||
};
|
||||
});
|
||||
@@ -81,12 +70,8 @@ describe("RebootDialog", () => {
|
||||
it("renders dialog with default input value", () => {
|
||||
render(<RebootDialog open onOpenChange={() => {}} />);
|
||||
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
|
||||
expect(
|
||||
screen.getByRole("heading", { name: /reboot device/i, level: 1 }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: /reboot now/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: /reboot device/i, level: 1 })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /reboot now/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls correct reboot function based on OTA checkbox state", () => {
|
||||
@@ -199,8 +184,6 @@ describe("RebootDialog", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
|
||||
});
|
||||
expect(rebootMock).toHaveBeenCalledWith(-1);
|
||||
expect(
|
||||
screen.queryByText(/reboot has been scheduled/i),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/reboot has been scheduled/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,11 +96,7 @@ export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
|
||||
<Separator />
|
||||
{!isScheduled ? (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={isOTA}
|
||||
onChange={(checked) => setIsOTA(checked)}
|
||||
className="px-2"
|
||||
>
|
||||
<Checkbox checked={isOTA} onChange={(checked) => setIsOTA(checked)} className="px-2">
|
||||
{t("reboot.ota")}
|
||||
</Checkbox>
|
||||
<div className="flex gap-2 px-2 items-center relative">
|
||||
@@ -137,9 +133,7 @@ export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
|
||||
) : (
|
||||
<div className="px-2">
|
||||
<div className="pb-6 pt-2 text-center">
|
||||
<Label className=" text-gray-700 dark:text-gray-300 ">
|
||||
{t("reboot.scheduled")}
|
||||
</Label>
|
||||
<Label className=" text-gray-700 dark:text-gray-300 ">{t("reboot.scheduled")}</Label>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import {
|
||||
CurrentDeviceContext,
|
||||
useDeviceStore,
|
||||
useMessageStore,
|
||||
} from "@core/stores";
|
||||
import { CurrentDeviceContext, useDeviceStore, useMessageStore } from "@core/stores";
|
||||
import { render } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
|
||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||
|
||||
vi.mock("@core/stores", async () => {
|
||||
const actual = (await vi.importActual(
|
||||
"@core/stores",
|
||||
)) as typeof import("@core/stores");
|
||||
const actual = (await vi.importActual("@core/stores")) as typeof import("@core/stores");
|
||||
return {
|
||||
...actual,
|
||||
useMessageStore: vi.fn(),
|
||||
|
||||
@@ -16,10 +16,7 @@ export interface RefreshKeysDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const RefreshKeysDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: RefreshKeysDialogProps) => {
|
||||
export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { activeChat } = useMessages();
|
||||
const { nodeErrors, getNode } = useNodeDB();
|
||||
@@ -48,10 +45,7 @@ export const RefreshKeysDialog = ({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="max-w-8 flex flex-col gap-2"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogContent className="max-w-8 flex flex-col gap-2" aria-describedby={undefined}>
|
||||
<DialogClose onClick={handleCloseDialog} />
|
||||
<DialogHeader>
|
||||
<DialogTitle>{text.title}</DialogTitle>
|
||||
@@ -60,30 +54,17 @@ export const RefreshKeysDialog = ({
|
||||
<ul className="mt-2">
|
||||
<li className="flex place-items-center gap-2 items-start">
|
||||
<div className="p-2 bg-slate-500 rounded-lg mt-1">
|
||||
<LockKeyholeOpenIcon
|
||||
size={30}
|
||||
className="text-white justify-center"
|
||||
/>
|
||||
<LockKeyholeOpenIcon size={30} className="text-white justify-center" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
<p className="font-bold mb-0.5">
|
||||
{t("refreshKeys.label.acceptNewKeys")}
|
||||
</p>
|
||||
<p className="font-bold mb-0.5">{t("refreshKeys.label.acceptNewKeys")}</p>
|
||||
<p>{t("refreshKeys.description.acceptNewKeys")}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
name="requestNewKeys"
|
||||
onClick={handleNodeRemove}
|
||||
>
|
||||
<Button variant="default" name="requestNewKeys" onClick={handleNodeRemove}>
|
||||
{t("button.requestNewKeys")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
name="dismiss"
|
||||
onClick={handleCloseDialog}
|
||||
>
|
||||
<Button variant="outline" name="dismiss" onClick={handleCloseDialog}>
|
||||
{t("button.dismiss")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -8,10 +8,7 @@ export interface RemoveNodeDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const RemoveNodeDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: RemoveNodeDialogProps) => {
|
||||
export const RemoveNodeDialog = ({ open, onOpenChange }: RemoveNodeDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { connection } = useDevice();
|
||||
const { getNode, removeNode } = useNodeDB();
|
||||
|
||||
@@ -48,9 +48,7 @@ describe("ResetNodeDbDialog", () => {
|
||||
);
|
||||
|
||||
render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />);
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Reset Node Database" }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Reset Node Database" }));
|
||||
|
||||
// Called immediately
|
||||
expect(mockResetNodes).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -8,10 +8,7 @@ export interface ResetNodeDbDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const ResetNodeDbDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ResetNodeDbDialogProps) => {
|
||||
export const ResetNodeDbDialog = ({ open, onOpenChange }: ResetNodeDbDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const { connection } = useDevice();
|
||||
const { removeAllNodeErrors, removeAllNodes } = useNodeDB();
|
||||
|
||||
@@ -32,13 +32,10 @@ export const TracerouteResponseDialog = ({
|
||||
const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4);
|
||||
const from = getNode(traceroute?.to ?? 0); // The origin of the traceroute = the "to" node of the mesh packet
|
||||
const fromLongName =
|
||||
from?.user?.longName ??
|
||||
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
|
||||
from?.user?.longName ?? (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
|
||||
const fromShortName =
|
||||
from?.user?.shortName ??
|
||||
(from
|
||||
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
|
||||
: t("unknown.shortName"));
|
||||
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : t("unknown.shortName"));
|
||||
|
||||
const toUser = getNode(traceroute?.from ?? 0); // The destination of the traceroute = the "from" node of the mesh packet
|
||||
|
||||
|
||||
@@ -20,16 +20,12 @@ export interface RouterRoleDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const UnsafeRolesDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: RouterRoleDialogProps) => {
|
||||
export const UnsafeRolesDialog = ({ open, onOpenChange }: RouterRoleDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const [confirmState, setConfirmState] = useState(false);
|
||||
const { setDialogOpen } = useDevice();
|
||||
|
||||
const deviceRoleLink =
|
||||
"https://meshtastic.org/docs/configuration/radio/device/";
|
||||
const deviceRoleLink = "https://meshtastic.org/docs/configuration/radio/device/";
|
||||
const choosingTheRightDeviceRoleLink =
|
||||
"https://meshtastic.org/blog/choosing-the-right-device-role/";
|
||||
|
||||
@@ -63,17 +59,11 @@ export const UnsafeRolesDialog = ({
|
||||
onChange={() => setConfirmState(!confirmState)}
|
||||
name="confirmUnderstanding"
|
||||
>
|
||||
<span className="dark:text-white">
|
||||
{t("unsafeRoles.confirmUnderstanding")}
|
||||
</span>
|
||||
<span className="dark:text-white">{t("unsafeRoles.confirmUnderstanding")}</span>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<DialogFooter className="mt-6">
|
||||
<Button
|
||||
variant="default"
|
||||
name="dismiss"
|
||||
onClick={() => handleCloseDialog("dismiss")}
|
||||
>
|
||||
<Button variant="default" name="dismiss" onClick={() => handleCloseDialog("dismiss")}>
|
||||
{t("button.dismiss")}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
+14
-52
@@ -4,20 +4,11 @@ import {
|
||||
} from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
|
||||
import { eventBus } from "@core/utils/eventBus.ts";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from "vitest";
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock("@tanstack/react-router", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("@tanstack/react-router")>();
|
||||
const actual = await importOriginal<typeof import("@tanstack/react-router")>();
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
@@ -65,10 +56,7 @@ describe("useUnsafeRolesDialog", () => {
|
||||
|
||||
result.current.handleCloseDialog();
|
||||
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
|
||||
"unsafeRoles",
|
||||
false,
|
||||
);
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith("unsafeRoles", false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,8 +65,7 @@ describe("useUnsafeRolesDialog", () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
const safeRole = "SAFE_ROLE";
|
||||
|
||||
const validationResult =
|
||||
await result.current.validateRoleSelection(safeRole);
|
||||
const validationResult = await result.current.validateRoleSelection(safeRole);
|
||||
|
||||
expect(validationResult).toBe(true);
|
||||
expect(mockDevice.setDialogOpen).not.toHaveBeenCalled();
|
||||
@@ -87,61 +74,40 @@ describe("useUnsafeRolesDialog", () => {
|
||||
it("should open dialog for unsafe roles and resolve with true when confirmed", async () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
|
||||
const validationPromise = result.current.validateRoleSelection(
|
||||
UNSAFE_ROLES[0]!,
|
||||
);
|
||||
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[0]!);
|
||||
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
|
||||
"unsafeRoles",
|
||||
true,
|
||||
);
|
||||
expect(eventBus.on).toHaveBeenCalledWith(
|
||||
"dialog:unsafeRoles",
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith("unsafeRoles", true);
|
||||
expect(eventBus.on).toHaveBeenCalledWith("dialog:unsafeRoles", expect.any(Function));
|
||||
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||
onHandler({ action: "confirm" });
|
||||
const validationResult = await validationPromise;
|
||||
|
||||
expect(validationResult).toBe(true);
|
||||
expect(eventBus.off).toHaveBeenCalledWith(
|
||||
"dialog:unsafeRoles",
|
||||
onHandler,
|
||||
);
|
||||
expect(eventBus.off).toHaveBeenCalledWith("dialog:unsafeRoles", onHandler);
|
||||
});
|
||||
|
||||
it("should resolve with false when user dismisses the dialog", async () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
const validationPromise = result.current.validateRoleSelection(
|
||||
UNSAFE_ROLES[0]!,
|
||||
);
|
||||
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[0]!);
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||
onHandler({ action: "dismiss" });
|
||||
|
||||
const validationResult = await validationPromise;
|
||||
expect(validationResult).toBe(false);
|
||||
expect(eventBus.off).toHaveBeenCalledWith(
|
||||
"dialog:unsafeRoles",
|
||||
onHandler,
|
||||
);
|
||||
expect(eventBus.off).toHaveBeenCalledWith("dialog:unsafeRoles", onHandler);
|
||||
});
|
||||
|
||||
it("should clean up event listener after response", async () => {
|
||||
const { result } = renderUnsafeRolesHook();
|
||||
|
||||
const validationPromise = result.current.validateRoleSelection(
|
||||
UNSAFE_ROLES[1]!,
|
||||
);
|
||||
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[1]!);
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||
|
||||
onHandler({ action: "confirm" });
|
||||
await validationPromise;
|
||||
|
||||
expect(eventBus.off).toHaveBeenCalledWith(
|
||||
"dialog:unsafeRoles",
|
||||
onHandler,
|
||||
);
|
||||
expect(eventBus.off).toHaveBeenCalledWith("dialog:unsafeRoles", onHandler);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,13 +118,9 @@ describe("useUnsafeRolesDialog", () => {
|
||||
mockDevice.setDialogOpen.mockClear();
|
||||
(eventBus.on as Mock).mockClear();
|
||||
|
||||
const validationPromise =
|
||||
result.current.validateRoleSelection(unsafeRole);
|
||||
const validationPromise = result.current.validateRoleSelection(unsafeRole);
|
||||
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
|
||||
"unsafeRoles",
|
||||
true,
|
||||
);
|
||||
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith("unsafeRoles", true);
|
||||
|
||||
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
|
||||
onHandler({ action: "confirm" });
|
||||
|
||||
@@ -21,11 +21,7 @@ export const useUnsafeRolesDialog = () => {
|
||||
setDialogOpen("unsafeRoles", true);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const handleResponse = ({
|
||||
action,
|
||||
}: {
|
||||
action: "confirm" | "dismiss";
|
||||
}) => {
|
||||
const handleResponse = ({ action }: { action: "confirm" | "dismiss" }) => {
|
||||
eventBus.off("dialog:unsafeRoles", handleResponse);
|
||||
resolve(action === "confirm");
|
||||
};
|
||||
|
||||
@@ -62,9 +62,7 @@ export interface UseDialogReturn {
|
||||
}
|
||||
|
||||
export const useDialog = (initialState?: DialogState): UseDialogReturn => {
|
||||
const [state, setState] = useState<DialogState>(
|
||||
initialState ?? { isOpen: false },
|
||||
);
|
||||
const [state, setState] = useState<DialogState>(initialState ?? { isOpen: false });
|
||||
|
||||
const openDialog = useCallback((config: DialogConfig) => {
|
||||
setState({ isOpen: true, config });
|
||||
@@ -97,10 +95,7 @@ export const useDialog = (initialState?: DialogState): UseDialogReturn => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getDefaultTexts = (
|
||||
type: DialogType,
|
||||
t: ReturnType<typeof useTranslation>["t"],
|
||||
) => {
|
||||
export const getDefaultTexts = (type: DialogType, t: ReturnType<typeof useTranslation>["t"]) => {
|
||||
switch (type) {
|
||||
case "confirm":
|
||||
return {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
||||
import {
|
||||
DynamicFormField,
|
||||
type FieldProps,
|
||||
} from "@components/Form/DynamicFormField.tsx";
|
||||
import { DynamicFormField, type FieldProps } from "@components/Form/DynamicFormField.tsx";
|
||||
import { FieldWrapper } from "@components/Form/FormWrapper.tsx";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
||||
@@ -66,9 +63,7 @@ export interface DynamicFormProps<T extends FieldValues> {
|
||||
validationSchema?: ZodType<T>;
|
||||
}
|
||||
|
||||
export type DynamicFormFormInit<T extends FieldValues> = (
|
||||
methods: UseFormReturn<T, T, T>,
|
||||
) => void;
|
||||
export type DynamicFormFormInit<T extends FieldValues> = (methods: UseFormReturn<T, T, T>) => void;
|
||||
|
||||
export function DynamicForm<T extends FieldValues>({
|
||||
propMethods,
|
||||
@@ -86,9 +81,7 @@ export function DynamicForm<T extends FieldValues>({
|
||||
const internalMethods = useForm<T>({
|
||||
mode: "onChange",
|
||||
defaultValues: defaultValues,
|
||||
resolver: validationSchema
|
||||
? createZodResolver(validationSchema)
|
||||
: undefined,
|
||||
resolver: validationSchema ? createZodResolver(validationSchema) : undefined,
|
||||
shouldFocusError: false,
|
||||
resetOptions: { keepDefaultValues: true },
|
||||
values,
|
||||
@@ -96,8 +89,7 @@ export function DynamicForm<T extends FieldValues>({
|
||||
|
||||
const methods = propMethods ?? internalMethods;
|
||||
|
||||
const { handleSubmit, control, getValues, formState, getFieldState } =
|
||||
methods;
|
||||
const { handleSubmit, control, getValues, formState, getFieldState } = methods;
|
||||
|
||||
useEffect(() => {
|
||||
if (!propMethods) {
|
||||
@@ -105,10 +97,7 @@ export function DynamicForm<T extends FieldValues>({
|
||||
}
|
||||
}, [onFormInit, propMethods, internalMethods]);
|
||||
|
||||
const isDisabled = (
|
||||
disabledBy?: DisabledBy<T>[],
|
||||
disabled?: boolean,
|
||||
): boolean => {
|
||||
const isDisabled = (disabledBy?: DisabledBy<T>[], disabled?: boolean): boolean => {
|
||||
if (disabled) {
|
||||
return true;
|
||||
}
|
||||
@@ -125,9 +114,7 @@ export function DynamicForm<T extends FieldValues>({
|
||||
return field.invert ? value : !value;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return field.invert
|
||||
? field.selector !== value
|
||||
: field.selector === value;
|
||||
return field.invert ? field.selector !== value : field.selector === value;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
import {
|
||||
GenericInput,
|
||||
type InputFieldProps,
|
||||
} from "@components/Form/FormInput.tsx";
|
||||
import { GenericInput, type InputFieldProps } from "@components/Form/FormInput.tsx";
|
||||
import {
|
||||
PasswordGenerator,
|
||||
type PasswordGeneratorProps,
|
||||
} from "@components/Form/FormPasswordGenerator.tsx";
|
||||
import {
|
||||
type SelectFieldProps,
|
||||
SelectInput,
|
||||
} from "@components/Form/FormSelect.tsx";
|
||||
import {
|
||||
type ToggleFieldProps,
|
||||
ToggleInput,
|
||||
} from "@components/Form/FormToggle.tsx";
|
||||
import { type SelectFieldProps, SelectInput } from "@components/Form/FormSelect.tsx";
|
||||
import { type ToggleFieldProps, ToggleInput } from "@components/Form/FormToggle.tsx";
|
||||
import type { Control, FieldValues } from "react-hook-form";
|
||||
import {
|
||||
type MultiSelectFieldProps,
|
||||
MultiSelectInput,
|
||||
} from "./FormMultiSelect.tsx";
|
||||
import { type MultiSelectFieldProps, MultiSelectInput } from "./FormMultiSelect.tsx";
|
||||
|
||||
export type FieldProps<T> =
|
||||
| InputFieldProps<T>
|
||||
@@ -46,14 +34,7 @@ export function DynamicFormField<T extends FieldValues>({
|
||||
case "text":
|
||||
case "password":
|
||||
case "number":
|
||||
return (
|
||||
<GenericInput
|
||||
field={field}
|
||||
control={control}
|
||||
disabled={disabled}
|
||||
isDirty={isDirty}
|
||||
/>
|
||||
);
|
||||
return <GenericInput field={field} control={control} disabled={disabled} isDirty={isDirty} />;
|
||||
|
||||
case "toggle":
|
||||
return (
|
||||
|
||||
@@ -48,10 +48,7 @@ export function GenericInput<T extends FieldValues>({
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
|
||||
if (
|
||||
field.properties?.fieldLength?.max &&
|
||||
newValue.length > field.properties.fieldLength.max
|
||||
) {
|
||||
if (field.properties?.fieldLength?.max && newValue.length > field.properties.fieldLength.max) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,32 +57,22 @@ export function GenericInput<T extends FieldValues>({
|
||||
}
|
||||
|
||||
controllerField.onChange(
|
||||
field.type === "number"
|
||||
? Number.parseFloat(newValue).toString()
|
||||
: newValue,
|
||||
field.type === "number" ? Number.parseFloat(newValue).toString() : newValue,
|
||||
);
|
||||
};
|
||||
|
||||
const currentLength = controllerField.value
|
||||
? String(controllerField.value).length
|
||||
: 0;
|
||||
const currentLength = controllerField.value ? String(controllerField.value).length : 0;
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
type={field.type}
|
||||
step={field.properties?.step}
|
||||
value={
|
||||
field.type === "number"
|
||||
? String(controllerField.value)
|
||||
: controllerField.value
|
||||
}
|
||||
value={field.type === "number" ? String(controllerField.value) : controllerField.value}
|
||||
id={field.name}
|
||||
onChange={handleInputChange}
|
||||
showCopyButton={field.properties?.showCopyButton}
|
||||
showPasswordToggle={
|
||||
field.properties?.showPasswordToggle || field.type === "password"
|
||||
}
|
||||
showPasswordToggle={field.properties?.showPasswordToggle || field.type === "password"}
|
||||
className={field.properties?.className}
|
||||
{...restProperties}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -33,11 +33,9 @@ export function MultiSelectInput<T extends FieldValues>({
|
||||
Object.values(enumValue)[0] !== null &&
|
||||
"i18nKey" in Object.values(enumValue)[0];
|
||||
|
||||
const optionsToRender = Object.entries(enumValue).map(
|
||||
([key, configOrValue]) => {
|
||||
const optionsToRender = Object.entries(enumValue).map(([key, configOrValue]) => {
|
||||
if (isNewConfigStructure) {
|
||||
const config =
|
||||
configOrValue as (typeof FLAGS_CONFIG)[keyof typeof FLAGS_CONFIG];
|
||||
const config = configOrValue as (typeof FLAGS_CONFIG)[keyof typeof FLAGS_CONFIG];
|
||||
return {
|
||||
key,
|
||||
display: t(config.i18nKey),
|
||||
@@ -45,8 +43,7 @@ export function MultiSelectInput<T extends FieldValues>({
|
||||
};
|
||||
}
|
||||
return { key, display: key, value: configOrValue as number };
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
|
||||
@@ -49,13 +49,8 @@ export function SelectInput<T extends FieldValues>({
|
||||
control,
|
||||
});
|
||||
|
||||
const {
|
||||
enumValue,
|
||||
formatEnumName,
|
||||
defaultValue,
|
||||
className,
|
||||
...remainingProperties
|
||||
} = field.properties;
|
||||
const { enumValue, formatEnumName, defaultValue, className, ...remainingProperties } =
|
||||
field.properties;
|
||||
const valueToKeyMap: Record<string, string> = {};
|
||||
const optionsEnumValues: [string, number][] = [];
|
||||
|
||||
@@ -100,9 +95,7 @@ export function SelectInput<T extends FieldValues>({
|
||||
className={cn([
|
||||
className,
|
||||
isDirty ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2" : "",
|
||||
invalid
|
||||
? "focus:ring-red-500 ring-red-500 ring-2 outline-offset-2"
|
||||
: "",
|
||||
invalid ? "focus:ring-red-500 ring-red-500 ring-2 outline-offset-2" : "",
|
||||
])}
|
||||
ref={ref}
|
||||
onBlur={onBlur}
|
||||
|
||||
@@ -34,12 +34,8 @@ export function ToggleInput<T extends FieldValues>({
|
||||
{...field.properties}
|
||||
className={cn([
|
||||
field.properties?.className,
|
||||
isDirty
|
||||
? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2"
|
||||
: "",
|
||||
invalid
|
||||
? "focus:ring-red-500 ring-red-500 ring-2 ring-offset-2"
|
||||
: "",
|
||||
isDirty ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2" : "",
|
||||
invalid ? "focus:ring-red-500 ring-red-500 ring-2 ring-offset-2" : "",
|
||||
])}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -24,9 +24,7 @@ export const FieldWrapper = ({
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[0.6fr_2fr_.1fr] sm:items-baseline gap-4">
|
||||
<Label htmlFor={fieldName}>{label}</Label>
|
||||
<div className="max-w-3xl">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{description}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{description}</p>
|
||||
<p hidden={valid ?? true} className="text-sm text-red-500">
|
||||
{validationText}
|
||||
</p>
|
||||
|
||||
@@ -10,11 +10,7 @@ import type { ZodType } from "zod/v4";
|
||||
export function createZodResolver<T extends FieldValues>(
|
||||
schema: ZodType<T, unknown>,
|
||||
): Resolver<T, unknown> {
|
||||
return (
|
||||
values: T,
|
||||
_context: unknown,
|
||||
_options?: ResolverOptions<T>,
|
||||
): ResolverResult<T> => {
|
||||
return (values: T, _context: unknown, _options?: ResolverOptions<T>): ResolverResult<T> => {
|
||||
const result = schema.safeParse(values);
|
||||
if (result.success) {
|
||||
return {
|
||||
@@ -23,10 +19,7 @@ export function createZodResolver<T extends FieldValues>(
|
||||
};
|
||||
}
|
||||
|
||||
const errors: Record<
|
||||
string,
|
||||
FieldError & { params?: Record<string, unknown> }
|
||||
> = {};
|
||||
const errors: Record<string, FieldError & { params?: Record<string, unknown> }> = {};
|
||||
|
||||
for (const issue of result.error.issues) {
|
||||
const { path, code, message, ...params } = issue;
|
||||
@@ -42,8 +35,7 @@ export function createZodResolver<T extends FieldValues>(
|
||||
: "";
|
||||
|
||||
const newCode =
|
||||
code.replace(/_([a-z])/g, (_, char) => char.toUpperCase()) +
|
||||
(suffix ? `.${suffix}` : "");
|
||||
code.replace(/_([a-z])/g, (_, char) => char.toUpperCase()) + (suffix ? `.${suffix}` : "");
|
||||
|
||||
const fieldError: FieldError & { params?: Record<string, unknown> } = {
|
||||
type: newCode,
|
||||
|
||||
@@ -18,9 +18,7 @@ interface LanguageSwitcherProps {
|
||||
disableHover?: boolean;
|
||||
}
|
||||
|
||||
export default function LanguageSwitcher({
|
||||
disableHover = false,
|
||||
}: LanguageSwitcherProps) {
|
||||
export default function LanguageSwitcher({ disableHover = false }: LanguageSwitcherProps) {
|
||||
const { i18n } = useTranslation("ui");
|
||||
const { set: setLanguage, current, getSupportedLangs } = useLang();
|
||||
|
||||
@@ -46,15 +44,13 @@ export default function LanguageSwitcher({
|
||||
size={16}
|
||||
className={cn(
|
||||
"text-gray-500 dark:text-gray-400 w-4 flex-shrink-0 transition-colors duration-150",
|
||||
!disableHover &&
|
||||
"group-hover:text-gray-700 dark:group-hover:text-gray-200",
|
||||
!disableHover && "group-hover:text-gray-700 dark:group-hover:text-gray-200",
|
||||
)}
|
||||
/>
|
||||
<Subtle
|
||||
className={cn(
|
||||
"text-sm text-gray-600 dark:text-gray-100 transition-colors duration-150",
|
||||
!disableHover &&
|
||||
"group-hover:text-gray-800 dark:group-hover:text-gray-100",
|
||||
!disableHover && "group-hover:text-gray-800 dark:group-hover:text-gray-100",
|
||||
)}
|
||||
>
|
||||
{`${t("languagePicker.label")}:`}
|
||||
@@ -62,8 +58,7 @@ export default function LanguageSwitcher({
|
||||
<Subtle
|
||||
className={cn(
|
||||
"text-sm font-medium text-gray-700 dark:text-gray-200 transition-colors duration-150",
|
||||
!disableHover &&
|
||||
"group-hover:text-gray-900 dark:group-hover:text-white",
|
||||
!disableHover && "group-hover:text-gray-900 dark:group-hover:text-white",
|
||||
)}
|
||||
>
|
||||
{current?.name}
|
||||
@@ -81,9 +76,7 @@ export default function LanguageSwitcher({
|
||||
<span>{language.flag}</span>
|
||||
<span>{language.name}</span>
|
||||
</div>
|
||||
{i18n.language === language.code && (
|
||||
<Check className="h-4 w-4 text-primary" />
|
||||
)}
|
||||
{i18n.language === language.code && <Check className="h-4 w-4 text-primary" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -45,9 +45,7 @@ export const BaseMap = ({
|
||||
|
||||
const locale = useMemo(() => {
|
||||
return {
|
||||
"GeolocateControl.FindMyLocation": t(
|
||||
"maplibre.GeolocateControl.FindMyLocation",
|
||||
),
|
||||
"GeolocateControl.FindMyLocation": t("maplibre.GeolocateControl.FindMyLocation"),
|
||||
"NavigationControl.ZoomIn": t("maplibre.NavigationControl.ZoomIn"),
|
||||
"NavigationControl.ZoomOut": t("maplibre.NavigationControl.ZoomOut"),
|
||||
"ScaleControl.Meters": t("unit.meter.suffix"),
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import {
|
||||
type ChannelValidation,
|
||||
makeChannelSchema,
|
||||
} from "@app/validation/channel.ts";
|
||||
import { type ChannelValidation, makeChannelSchema } from "@app/validation/channel.ts";
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
||||
import { createZodResolver } from "@components/Form/createZodResolver.ts";
|
||||
import {
|
||||
DynamicForm,
|
||||
type DynamicFormFormInit,
|
||||
} from "@components/Form/DynamicForm.tsx";
|
||||
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx";
|
||||
import { useDevice } from "@core/stores";
|
||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
@@ -37,8 +31,7 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
|
||||
moduleSettings: {
|
||||
...defaultConfig?.settings?.moduleSettings,
|
||||
positionPrecision:
|
||||
defaultConfig?.settings?.moduleSettings?.positionPrecision ===
|
||||
undefined
|
||||
defaultConfig?.settings?.moduleSettings?.positionPrecision === undefined
|
||||
? 10
|
||||
: defaultConfig?.settings?.moduleSettings?.positionPrecision,
|
||||
},
|
||||
@@ -60,8 +53,7 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
|
||||
moduleSettings: {
|
||||
...effectiveConfig?.settings?.moduleSettings,
|
||||
positionPrecision:
|
||||
effectiveConfig?.settings?.moduleSettings?.positionPrecision ===
|
||||
undefined
|
||||
effectiveConfig?.settings?.moduleSettings?.positionPrecision === undefined
|
||||
? 10
|
||||
: effectiveConfig?.settings?.moduleSettings?.positionPrecision,
|
||||
},
|
||||
@@ -69,11 +61,8 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
|
||||
},
|
||||
};
|
||||
|
||||
const [preSharedDialogOpen, setPreSharedDialogOpen] =
|
||||
useState<boolean>(false);
|
||||
const [byteCount, setBytes] = useState<number>(
|
||||
effectiveConfig?.settings?.psk.length ?? 16,
|
||||
);
|
||||
const [preSharedDialogOpen, setPreSharedDialogOpen] = useState<boolean>(false);
|
||||
const [byteCount, setBytes] = useState<number>(effectiveConfig?.settings?.psk.length ?? 16);
|
||||
const ChannelValidationSchema = useMemo(() => {
|
||||
return makeChannelSchema(byteCount);
|
||||
}, [byteCount]);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { Channel } from "@app/components/PageComponents/Channels/Channel";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { Spinner } from "@components/UI/Spinner.tsx";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@components/UI/Tabs.tsx";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx";
|
||||
import { useDevice } from "@core/stores";
|
||||
import type { Protobuf } from "@meshtastic/core";
|
||||
import i18next from "i18next";
|
||||
@@ -36,13 +31,7 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
|
||||
|
||||
const allChannels = Array.from(channels.values());
|
||||
const flags = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
allChannels.map((channel) => [
|
||||
channel.index,
|
||||
hasChannelChange(channel.index),
|
||||
]),
|
||||
),
|
||||
() => new Map(allChannels.map((channel) => [channel.index, hasChannelChange(channel.index)])),
|
||||
[allChannels, hasChannelChange],
|
||||
);
|
||||
|
||||
@@ -64,10 +53,7 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
|
||||
)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
<Button
|
||||
className="ml-auto mr-1 h-8"
|
||||
onClick={() => setDialogOpen("import", true)}
|
||||
>
|
||||
<Button className="ml-auto mr-1 h-8" onClick={() => setDialogOpen("import", true)}>
|
||||
<UploadIcon className="mr-2" size={14} />
|
||||
{t("page.import")}
|
||||
</Button>
|
||||
@@ -77,16 +63,9 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
|
||||
</Button>
|
||||
</TabsList>
|
||||
{allChannels.map((channel) => (
|
||||
<TabsContent
|
||||
key={`channel_${channel.index}`}
|
||||
value={`channel_${channel.index}`}
|
||||
>
|
||||
<TabsContent key={`channel_${channel.index}`} value={`channel_${channel.index}`}>
|
||||
<Suspense fallback={<Spinner size="lg" className="my-5" />}>
|
||||
<Channel
|
||||
key={channel.index}
|
||||
onFormInit={onFormInit}
|
||||
channel={channel}
|
||||
/>
|
||||
<Channel key={channel.index} onFormInit={onFormInit} channel={channel} />
|
||||
</Suspense>
|
||||
</TabsContent>
|
||||
))}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Button } from "@app/components/UI/Button";
|
||||
import type { Connection } from "@app/core/stores/deviceStore/types";
|
||||
|
||||
export function ConnectionStatusBadge({
|
||||
status,
|
||||
}: {
|
||||
status: Connection["status"];
|
||||
}) {
|
||||
export function ConnectionStatusBadge({ status }: { status: Connection["status"] }) {
|
||||
let color = "";
|
||||
let displayStatus = status;
|
||||
|
||||
@@ -30,13 +26,8 @@ export function ConnectionStatusBadge({
|
||||
}
|
||||
return (
|
||||
<Button variant="subtle" className="inline-flex items-center gap-2">
|
||||
<span
|
||||
className={`h-2.5 w-2.5 rounded-full ${color}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-xs capitalize text-slate-500 dark:text-slate-400">
|
||||
{displayStatus}
|
||||
</span>
|
||||
<span className={`h-2.5 w-2.5 rounded-full ${color}`} aria-hidden="true" />
|
||||
<span className="text-xs capitalize text-slate-500 dark:text-slate-400">{displayStatus}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,11 +13,7 @@ export interface HeatmapLayerProps {
|
||||
mode: HeatmapMode;
|
||||
}
|
||||
|
||||
export const HeatmapLayer = ({
|
||||
id,
|
||||
filteredNodes,
|
||||
mode,
|
||||
}: HeatmapLayerProps) => {
|
||||
export const HeatmapLayer = ({ id, filteredNodes, mode }: HeatmapLayerProps) => {
|
||||
const data: FeatureCollection = useMemo(() => {
|
||||
const features: Feature[] = filteredNodes
|
||||
.filter((node) => hasPos(node.position))
|
||||
@@ -45,9 +41,7 @@ export const HeatmapLayer = ({
|
||||
const paintProps: HeatmapLayerSpecification["paint"] = useMemo(
|
||||
() => ({
|
||||
"heatmap-weight":
|
||||
mode === "density"
|
||||
? 1
|
||||
: ["interpolate", ["linear"], ["get", "snr"], -20, 0, 10, 1],
|
||||
mode === "density" ? 1 : ["interpolate", ["linear"], ["get", "snr"], -20, 0, 10, 1],
|
||||
"heatmap-intensity": ["interpolate", ["linear"], ["zoom"], 0, 1, 15, 3],
|
||||
// Color ramp for heatmap. Domain is 0 (low) to 1 (high).
|
||||
// Begin color ramp at 0-stop with a 0-transparancy color
|
||||
@@ -69,17 +63,7 @@ export const HeatmapLayer = ({
|
||||
1,
|
||||
"rgb(178,24,43)",
|
||||
],
|
||||
"heatmap-radius": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
0,
|
||||
2,
|
||||
9,
|
||||
20,
|
||||
15,
|
||||
30,
|
||||
],
|
||||
"heatmap-radius": ["interpolate", ["linear"], ["zoom"], 0, 2, 9, 20, 15, 30],
|
||||
// Opacity 0.7 to be visible but not blocking
|
||||
"heatmap-opacity": 0.7,
|
||||
}),
|
||||
|
||||
@@ -50,8 +50,7 @@ export const NodesLayer = ({
|
||||
() =>
|
||||
popupState?.type !== "node"
|
||||
? undefined
|
||||
: (filteredNodes.find((node) => node.num === popupState.num) ??
|
||||
undefined),
|
||||
: (filteredNodes.find((node) => node.num === popupState.num) ?? undefined),
|
||||
[popupState, filteredNodes],
|
||||
);
|
||||
|
||||
@@ -78,9 +77,7 @@ export const NodesLayer = ({
|
||||
const isExpanded = expandedCluster === key;
|
||||
|
||||
// Precompute pixel offsets for expanded state
|
||||
const expandedOffsets = isExpanded
|
||||
? fanOutOffsetsPx(nodes.length, key)
|
||||
: undefined;
|
||||
const expandedOffsets = isExpanded ? fanOutOffsetsPx(nodes.length, key) : undefined;
|
||||
|
||||
// Always render all node markers in the cluster
|
||||
for (const [i, node] of nodes.entries()) {
|
||||
|
||||
@@ -36,10 +36,7 @@ export function generatePrecisionCircles(
|
||||
>();
|
||||
|
||||
for (const node of filteredNodes) {
|
||||
if (
|
||||
node.position?.precisionBits === undefined ||
|
||||
node.position.precisionBits === 0
|
||||
) {
|
||||
if (node.position?.precisionBits === undefined || node.position.precisionBits === 0) {
|
||||
continue;
|
||||
}
|
||||
const [lng, lat] = toLngLat(node.position);
|
||||
@@ -63,9 +60,7 @@ export function generatePrecisionCircles(
|
||||
}
|
||||
}
|
||||
|
||||
const items = Array.from(unique.values()).sort(
|
||||
(a, b) => a.radiusM - b.radiusM,
|
||||
);
|
||||
const items = Array.from(unique.values()).sort((a, b) => a.radiusM - b.radiusM);
|
||||
|
||||
const features: Feature<Polygon, CircleProps>[] = items.map(
|
||||
({ lng, lat, radiusM, r, g, b, a }) => {
|
||||
@@ -96,13 +91,7 @@ export const SourcePrecisionCircles = ({
|
||||
type="fill"
|
||||
layout={{ visibility: isVisible ? "visible" : "none" }}
|
||||
paint={{
|
||||
"fill-color": [
|
||||
"rgba",
|
||||
["get", "r"],
|
||||
["get", "g"],
|
||||
["get", "b"],
|
||||
["get", "a"],
|
||||
],
|
||||
"fill-color": ["rgba", ["get", "r"], ["get", "g"], ["get", "b"], ["get", "a"]],
|
||||
}}
|
||||
/>
|
||||
<Layer
|
||||
|
||||
@@ -64,11 +64,7 @@ type Pair = {
|
||||
ba?: number; // SNR b->a
|
||||
};
|
||||
|
||||
function arcSegment(
|
||||
aLngLat: LngLat,
|
||||
bLngLat: LngLat,
|
||||
curved: boolean,
|
||||
): LngLat[] | undefined {
|
||||
function arcSegment(aLngLat: LngLat, bLngLat: LngLat, curved: boolean): LngLat[] | undefined {
|
||||
if (!curved && distanceMeters(aLngLat, bLngLat) < MIN_LEN) {
|
||||
// Straight line
|
||||
return [aLngLat, bLngLat];
|
||||
@@ -180,9 +176,7 @@ function pushIfFeature(
|
||||
}
|
||||
}
|
||||
|
||||
function generateNeighborLines(
|
||||
neighborInfos: NeighborInfos[],
|
||||
): FeatureCollection {
|
||||
function generateNeighborLines(neighborInfos: NeighborInfos[]): FeatureCollection {
|
||||
// Collect positions for all referenced nodes, discard pairs with missing positions
|
||||
const idToLngLat = new Map<number, LngLat>();
|
||||
const ensure = (node?: Protobuf.Mesh.NodeInfo | NeighborPlus) => {
|
||||
@@ -246,12 +240,7 @@ function generateNeighborLines(
|
||||
return { type: "FeatureCollection", features };
|
||||
}
|
||||
|
||||
export const SNRTooltip = ({
|
||||
pos,
|
||||
snr,
|
||||
from,
|
||||
to,
|
||||
}: Partial<SNRTooltipProps> = {}) => {
|
||||
export const SNRTooltip = ({ pos, snr, from, to }: Partial<SNRTooltipProps> = {}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!pos) {
|
||||
@@ -301,9 +290,7 @@ export const SNRLayer = ({
|
||||
neighborInfo: {
|
||||
...neighborInfo,
|
||||
neighbors: neighborInfo.neighbors.map((n) => {
|
||||
const node = filteredNodes.find(
|
||||
(node) => node.num === n.nodeId,
|
||||
);
|
||||
const node = filteredNodes.find((node) => node.num === n.nodeId);
|
||||
return { ...n, num: node?.num, position: node?.position };
|
||||
}),
|
||||
},
|
||||
@@ -325,10 +312,7 @@ export const SNRLayer = ({
|
||||
}))
|
||||
: [];
|
||||
|
||||
const featureCollection = generateNeighborLines([
|
||||
...remotePairs,
|
||||
...directPairs,
|
||||
]);
|
||||
const featureCollection = generateNeighborLines([...remotePairs, ...directPairs]);
|
||||
|
||||
return (
|
||||
<Source type="geojson" data={featureCollection}>
|
||||
|
||||
@@ -82,11 +82,7 @@ export const WaypointLayer = ({
|
||||
offset={[0, 25]}
|
||||
onClose={() => setPopupState(undefined)}
|
||||
>
|
||||
<WaypointDetail
|
||||
waypoint={popupState.waypoint}
|
||||
myNode={myNode}
|
||||
onEdit={() => {}}
|
||||
/>
|
||||
<WaypointDetail waypoint={popupState.waypoint} myNode={myNode} onEdit={() => {}} />
|
||||
</PopupWrapper>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,7 @@ type StackBadgeProps = {
|
||||
onClick: (e: { originalEvent: MouseEvent }) => void;
|
||||
};
|
||||
|
||||
export const StackBadge = ({
|
||||
lng,
|
||||
lat,
|
||||
count,
|
||||
isVisible = true,
|
||||
onClick,
|
||||
}: StackBadgeProps) => {
|
||||
export const StackBadge = ({ lng, lat, count, isVisible = true, onClick }: StackBadgeProps) => {
|
||||
return (
|
||||
<Marker
|
||||
longitude={lng}
|
||||
|
||||
@@ -16,14 +16,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@radix-ui/react-tooltip";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
Dot,
|
||||
LockIcon,
|
||||
LockOpenIcon,
|
||||
MessageSquareIcon,
|
||||
MountainSnow,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import { Dot, LockIcon, LockOpenIcon, MessageSquareIcon, MountainSnow, Star } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface NodeDetailProps {
|
||||
@@ -104,9 +97,7 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
||||
fill={node.isFavorite ? "black" : "none"}
|
||||
size={15}
|
||||
aria-label={
|
||||
node.isFavorite
|
||||
? t("nodeDetail.favorite.label")
|
||||
: t("nodeDetail.notFavorite.label")
|
||||
node.isFavorite ? t("nodeDetail.favorite.label") : t("nodeDetail.notFavorite.label")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -127,15 +118,12 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
||||
|
||||
<div
|
||||
className="flex gap-1"
|
||||
title={new Date(node.lastHeard * 1000).toLocaleString(
|
||||
navigator.language,
|
||||
)}
|
||||
title={new Date(node.lastHeard * 1000).toLocaleString(navigator.language)}
|
||||
>
|
||||
<div>
|
||||
{node.lastHeard > 0 && (
|
||||
<div>
|
||||
{t("nodeDetail.status.heard")}{" "}
|
||||
<TimeAgo timestamp={node.lastHeard * 1000} />
|
||||
{t("nodeDetail.status.heard")} <TimeAgo timestamp={node.lastHeard * 1000} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -153,13 +141,9 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
||||
<div className="flex mt-2 text-sm">
|
||||
<div className="flex items-center grow">
|
||||
<div className="border-2 border-slate-900 rounded-sm px-0.5 mr-1">
|
||||
{Number.isNaN(node.hopsAway)
|
||||
? t("unit.hopsAway.unknown")
|
||||
: node.hopsAway}
|
||||
</div>
|
||||
<div>
|
||||
{node.hopsAway === 1 ? t("unit.hop.one") : t("unit.hop.plural")}
|
||||
{Number.isNaN(node.hopsAway) ? t("unit.hopsAway.unknown") : node.hopsAway}
|
||||
</div>
|
||||
<div>{node.hopsAway === 1 ? t("unit.hop.one") : t("unit.hop.plural")}</div>
|
||||
</div>
|
||||
{node.position?.altitude && (
|
||||
<div className="flex items-center grow">
|
||||
@@ -182,17 +166,13 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
||||
{!!node.deviceMetrics?.channelUtilization && (
|
||||
<div className="grow">
|
||||
<div>{t("channelUtilization.short")}</div>
|
||||
<Mono>
|
||||
{node.deviceMetrics?.channelUtilization.toPrecision(3)}%
|
||||
</Mono>
|
||||
<Mono>{node.deviceMetrics?.channelUtilization.toPrecision(3)}%</Mono>
|
||||
</div>
|
||||
)}
|
||||
{!!node.deviceMetrics?.airUtilTx && (
|
||||
<div className="grow">
|
||||
<div>{t("airtimeUtilization.short")}</div>
|
||||
<Mono className="text-gray-500">
|
||||
{node.deviceMetrics?.airUtilTx.toPrecision(3)}%
|
||||
</Mono>
|
||||
<Mono className="text-gray-500">{node.deviceMetrics?.airUtilTx.toPrecision(3)}%</Mono>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user