feat: add error boundary
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
"maplibre-gl": "4.1.2",
|
"maplibre-gl": "4.1.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-error-boundary": "^5.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-map-gl": "7.1.9",
|
"react-map-gl": "7.1.9",
|
||||||
"react-qrcode-logo": "^3.0.0",
|
"react-qrcode-logo": "^3.0.0",
|
||||||
@@ -101,6 +102,8 @@
|
|||||||
|
|
||||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="],
|
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="],
|
||||||
|
|
||||||
|
"@babel/runtime": ["@babel/runtime@7.26.9", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg=="],
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9" } }, "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA=="],
|
"@babel/template": ["@babel/template@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9" } }, "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.9", "@babel/parser": "^7.26.9", "@babel/template": "^7.26.9", "@babel/types": "^7.26.9", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg=="],
|
"@babel/traverse": ["@babel/traverse@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.9", "@babel/parser": "^7.26.9", "@babel/template": "^7.26.9", "@babel/types": "^7.26.9", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg=="],
|
||||||
@@ -1119,6 +1122,8 @@
|
|||||||
|
|
||||||
"react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
|
"react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
|
||||||
|
|
||||||
|
"react-error-boundary": ["react-error-boundary@5.0.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ=="],
|
||||||
|
|
||||||
"react-hook-form": ["react-hook-form@7.54.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="],
|
"react-hook-form": ["react-hook-form@7.54.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="],
|
||||||
|
|
||||||
"react-map-gl": ["react-map-gl@7.1.9", "", { "dependencies": { "@maplibre/maplibre-gl-style-spec": "^19.2.1", "@types/mapbox-gl": ">=1.0.0" }, "peerDependencies": { "mapbox-gl": ">=1.13.0", "maplibre-gl": ">=1.13.0 <5.0.0", "react": ">=16.3.0", "react-dom": ">=16.3.0" }, "optionalPeers": ["mapbox-gl", "maplibre-gl"] }, "sha512-KsCc8Gyn05wVGlHZoopaiiCr0RCAQ6LDISo5sEy1/pV/d7RlozkF946tiX7IgyijJQMRujHol5QdwUPESjh73w=="],
|
"react-map-gl": ["react-map-gl@7.1.9", "", { "dependencies": { "@maplibre/maplibre-gl-style-spec": "^19.2.1", "@types/mapbox-gl": ">=1.0.0" }, "peerDependencies": { "mapbox-gl": ">=1.13.0", "maplibre-gl": ">=1.13.0 <5.0.0", "react": ">=16.3.0", "react-dom": ">=16.3.0" }, "optionalPeers": ["mapbox-gl", "maplibre-gl"] }, "sha512-KsCc8Gyn05wVGlHZoopaiiCr0RCAQ6LDISo5sEy1/pV/d7RlozkF946tiX7IgyijJQMRujHol5QdwUPESjh73w=="],
|
||||||
@@ -1135,6 +1140,8 @@
|
|||||||
|
|
||||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
|
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
|
||||||
|
|
||||||
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||||
|
|
||||||
"resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
|
"resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"maplibre-gl": "4.1.2",
|
"maplibre-gl": "4.1.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-error-boundary": "^5.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-map-gl": "7.1.9",
|
"react-map-gl": "7.1.9",
|
||||||
"react-qrcode-logo": "^3.0.0",
|
"react-qrcode-logo": "^3.0.0",
|
||||||
|
|||||||
+25
-23
@@ -12,7 +12,9 @@ import { useAppStore } from "@core/stores/appStore.ts";
|
|||||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
import { MapProvider } from "react-map-gl";
|
import { MapProvider } from "react-map-gl";
|
||||||
|
import { ErrorPage } from "./components/UI/ErrorPage";
|
||||||
|
|
||||||
export const App = (): JSX.Element => {
|
export const App = (): JSX.Element => {
|
||||||
const { getDevice } = useDeviceStore();
|
const { getDevice } = useDeviceStore();
|
||||||
@@ -22,7 +24,7 @@ export const App = (): JSX.Element => {
|
|||||||
const device = getDevice(selectedDevice);
|
const device = getDevice(selectedDevice);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ErrorBoundary FallbackComponent={ErrorPage}>
|
||||||
<NewDeviceDialog
|
<NewDeviceDialog
|
||||||
open={connectDialogOpen}
|
open={connectDialogOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
@@ -30,30 +32,30 @@ export const App = (): JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<MapProvider>
|
<DeviceWrapper device={device}>
|
||||||
<DeviceWrapper device={device}>
|
<div className="flex h-screen flex-col overflow-hidden bg-background-primary text-text-primary">
|
||||||
<div className="flex h-screen flex-col overflow-hidden bg-background-primary text-text-primary">
|
<div className="flex grow">
|
||||||
<div className="flex grow">
|
<DeviceSelector />
|
||||||
<DeviceSelector />
|
<div className="flex grow flex-col">
|
||||||
<div className="flex grow flex-col">
|
{device ? (
|
||||||
{device ? (
|
<div className="flex h-screen">
|
||||||
<div className="flex h-screen">
|
<DialogManager />
|
||||||
<DialogManager />
|
<KeyBackupReminder />
|
||||||
<KeyBackupReminder />
|
<CommandPalette />
|
||||||
<CommandPalette />
|
<MapProvider>
|
||||||
<PageRouter />
|
<PageRouter />
|
||||||
</div>
|
</MapProvider>
|
||||||
) : (
|
</div>
|
||||||
<>
|
) : (
|
||||||
<Dashboard />
|
<>
|
||||||
<Footer />
|
<Dashboard />
|
||||||
</>
|
<Footer />
|
||||||
)}
|
</>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DeviceWrapper>
|
</div>
|
||||||
</MapProvider>
|
</DeviceWrapper>
|
||||||
</>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+35
-7
@@ -4,16 +4,44 @@ import ChannelsPage from "@pages/Channels.tsx";
|
|||||||
import ConfigPage from "@pages/Config/index.tsx";
|
import ConfigPage from "@pages/Config/index.tsx";
|
||||||
import MessagesPage from "@pages/Messages.tsx";
|
import MessagesPage from "@pages/Messages.tsx";
|
||||||
import NodesPage from "@pages/Nodes.tsx";
|
import NodesPage from "@pages/Nodes.tsx";
|
||||||
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
|
import { ErrorPage } from "./components/UI/ErrorPage";
|
||||||
|
|
||||||
|
export const ErrorBoundaryWrapper = ({
|
||||||
|
children,
|
||||||
|
}: { children: React.ReactNode }) => (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorPage}>{children}</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
export const PageRouter = () => {
|
export const PageRouter = () => {
|
||||||
const { activePage } = useDevice();
|
const { activePage } = useDevice();
|
||||||
return (
|
return (
|
||||||
<>
|
<ErrorBoundary FallbackComponent={ErrorPage}>
|
||||||
{activePage === "messages" && <MessagesPage />}
|
{activePage === "messages" && (
|
||||||
{activePage === "map" && <MapPage />}
|
<ErrorBoundary FallbackComponent={ErrorPage}>
|
||||||
{activePage === "config" && <ConfigPage />}
|
<MessagesPage />
|
||||||
{activePage === "channels" && <ChannelsPage />}
|
</ErrorBoundary>
|
||||||
{activePage === "nodes" && <NodesPage />}
|
)}
|
||||||
</>
|
{activePage === "map" && (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorPage}>
|
||||||
|
<MapPage />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)}
|
||||||
|
{activePage === "config" && (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorPage}>
|
||||||
|
<ConfigPage />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)}
|
||||||
|
{activePage === "channels" && (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorPage}>
|
||||||
|
<ChannelsPage />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)}
|
||||||
|
{activePage === "nodes" && (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorPage}>
|
||||||
|
<NodesPage />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)}
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
} from "@components/Form/DynamicFormField.tsx";
|
} from "@components/Form/DynamicFormField.tsx";
|
||||||
import { FieldWrapper } from "@components/Form/FormWrapper.tsx";
|
import { FieldWrapper } from "@components/Form/FormWrapper.tsx";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { H4 } from "@components/UI/Typography/H4.tsx";
|
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||||
import {
|
import {
|
||||||
type Control,
|
type Control,
|
||||||
@@ -14,6 +13,7 @@ import {
|
|||||||
type SubmitHandler,
|
type SubmitHandler,
|
||||||
useForm,
|
useForm,
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
|
import { Heading } from "../UI/Typography/Heading";
|
||||||
|
|
||||||
interface DisabledBy<T> {
|
interface DisabledBy<T> {
|
||||||
fieldName: Path<T>;
|
fieldName: Path<T>;
|
||||||
@@ -94,7 +94,9 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
{fieldGroups.map((fieldGroup) => (
|
{fieldGroups.map((fieldGroup) => (
|
||||||
<div key={fieldGroup.label} className="space-y-8 sm:space-y-5">
|
<div key={fieldGroup.label} className="space-y-8 sm:space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<H4 className="font-medium">{fieldGroup.label}</H4>
|
<Heading as="h4" className="font-medium">
|
||||||
|
{fieldGroup.label}
|
||||||
|
</Heading>
|
||||||
<Subtle>{fieldGroup.description}</Subtle>
|
<Subtle>{fieldGroup.description}</Subtle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Separator } from "@app/components/UI/Seperator";
|
import { Separator } from "@app/components/UI/Seperator";
|
||||||
import { H5 } from "@app/components/UI/Typography/H5.tsx";
|
import { Heading } from "@app/components/UI/Typography/Heading";
|
||||||
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
||||||
import { formatQuantity } from "@app/core/utils/string";
|
import { formatQuantity } from "@app/core/utils/string";
|
||||||
import { Avatar } from "@components/UI/Avatar";
|
import { Avatar } from "@components/UI/Avatar";
|
||||||
@@ -62,7 +62,7 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<H5>{name}</H5>
|
<Heading as="h5">{name}</Heading>
|
||||||
|
|
||||||
{hardwareType !== "UNSET" && <Subtle>{hardwareType}</Subtle>}
|
{hardwareType !== "UNSET" && <Subtle>{hardwareType}</Subtle>}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@app/components/UI/Tooltip";
|
} from "@app/components/UI/Tooltip";
|
||||||
import { useAppStore } from "@app/core/stores/appStore";
|
|
||||||
import {
|
import {
|
||||||
type MessageWithState,
|
type MessageWithState,
|
||||||
useDeviceStore,
|
useDeviceStore,
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import newGithubIssueUrl from "@app/core/utils/github";
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import { Heading } from "./Typography/Heading";
|
||||||
|
import { Link } from "./Typography/Link";
|
||||||
|
import { P } from "./Typography/P";
|
||||||
|
|
||||||
|
export function ErrorPage({ error }: { error: Error }) {
|
||||||
|
if (!error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article>
|
||||||
|
<section className="prose mx-auto mb-20 mt-28 max-w-prose px-8 text-2xl transition-all duration-150 ease-linear space-y-2">
|
||||||
|
<Heading as="h2" className="text-text-primary">
|
||||||
|
This is a little embarrassing...
|
||||||
|
</Heading>
|
||||||
|
<P>
|
||||||
|
We are really sorry but an error occured in the web client that caused
|
||||||
|
it to crash. This is not supposed to happen and we are working hard to
|
||||||
|
fix it.
|
||||||
|
</P>
|
||||||
|
<P>
|
||||||
|
The best way to prevent this from happening again to you or anyone
|
||||||
|
else is to report the issue to us.
|
||||||
|
</P>
|
||||||
|
<P>Please include the following information in your report:</P>
|
||||||
|
<ul className="list-disc list-inside text-sm">
|
||||||
|
<li>What you were doing when the error occured</li>
|
||||||
|
<li>What you expected to happen</li>
|
||||||
|
<li>What actually happened</li>
|
||||||
|
<li>Any other information you think might be relevant</li>
|
||||||
|
</ul>
|
||||||
|
<P>
|
||||||
|
You can report the issue to our{" "}
|
||||||
|
<Link
|
||||||
|
href={newGithubIssueUrl({
|
||||||
|
repoUrl: "https://github.com/meshtastic/web",
|
||||||
|
template: "bug.yml",
|
||||||
|
title: "[Bug]: An unhandled error occurred. <Add details here>",
|
||||||
|
logs: error?.stack,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Github
|
||||||
|
</Link>
|
||||||
|
<ExternalLink size={24} className="inline-block ml-2" />
|
||||||
|
</P>
|
||||||
|
<P>
|
||||||
|
Return to the <Link href="/">dashboard</Link>
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<details className="mt-6 text-md">
|
||||||
|
<summary className="cursor-pointer">Error Details</summary>
|
||||||
|
<span className="block text-sm mt-4 overflow-auto">
|
||||||
|
{error?.message ? (
|
||||||
|
<>
|
||||||
|
<label htmlFor="message">Error message:</label>
|
||||||
|
<pre
|
||||||
|
id="message"
|
||||||
|
className="w-full text-slate-400"
|
||||||
|
>{`${error.message}`}</pre>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{error?.stack ? (
|
||||||
|
<>
|
||||||
|
<label htmlFor="stack">Stack trace:</label>
|
||||||
|
<pre
|
||||||
|
id="stack"
|
||||||
|
className="w-full text-slate-400"
|
||||||
|
>{`${error.stack}`}</pre>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{!error?.message && !error?.stack ? (
|
||||||
|
<pre className=" w-full text-slate-400">{error.toString()}</pre>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { H4 } from "@components/UI/Typography/H4.tsx";
|
import { Heading } from "../Typography/Heading";
|
||||||
|
|
||||||
export interface SidebarSectionProps {
|
export interface SidebarSectionProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -11,7 +11,9 @@ export const SidebarSection = ({
|
|||||||
children,
|
children,
|
||||||
}: SidebarSectionProps) => (
|
}: SidebarSectionProps) => (
|
||||||
<div className="px-4 py-2">
|
<div className="px-4 py-2">
|
||||||
<H4 className="mb-3 ml-2">{title}</H4>
|
<Heading as="h4" className="mb-3 ml-2">
|
||||||
|
{title}
|
||||||
|
</Heading>
|
||||||
<div className="space-y-1">{children}</div>
|
<div className="space-y-1">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface H1Props {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const H1 = ({ children }: H1Props): JSX.Element => (
|
|
||||||
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
|
|
||||||
{children}
|
|
||||||
</h1>
|
|
||||||
);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface H2Props {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const H2 = ({ children }: H2Props): JSX.Element => (
|
|
||||||
<h2 className="scroll-m-20 border-b border-b-slate-200 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0 dark:border-b-slate-700">
|
|
||||||
{children}
|
|
||||||
</h2>
|
|
||||||
);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface H3Props {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const H3 = ({ children }: H3Props): JSX.Element => (
|
|
||||||
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight">
|
|
||||||
{children}
|
|
||||||
</h3>
|
|
||||||
);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { cn } from "@app/core/utils/cn.ts";
|
|
||||||
|
|
||||||
export interface H4Props {
|
|
||||||
className?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const H4 = ({ className, children }: H4Props): JSX.Element => (
|
|
||||||
<h4
|
|
||||||
className={cn(
|
|
||||||
"scroll-m-20 text-xl font-semibold tracking-tight",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</h4>
|
|
||||||
);
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { cn } from "@app/core/utils/cn.ts";
|
|
||||||
|
|
||||||
export interface H5Props {
|
|
||||||
className?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const H5 = ({ className, children }: H5Props): JSX.Element => (
|
|
||||||
<h5
|
|
||||||
className={cn("scroll-m-20 text-lg font-medium tracking-tight", className)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</h5>
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
const headingStyles = {
|
||||||
|
h1: "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl",
|
||||||
|
h2: "scroll-m-20 border-b border-b-slate-200 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0 dark:border-b-slate-700",
|
||||||
|
h3: "scroll-m-20 text-2xl font-semibold tracking-tight",
|
||||||
|
h4: "scroll-m-20 text-xl font-semibold tracking-tight",
|
||||||
|
h5: "scroll-m-20 text-lg font-medium tracking-tight",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HeadingProps {
|
||||||
|
as?: "h1" | "h2" | "h3" | "h4" | "h5";
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Heading = ({
|
||||||
|
as: Component = "h1",
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: HeadingProps) => {
|
||||||
|
const baseStyles = headingStyles[Component] || headingStyles.h1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component className={`${baseStyles} ${className}`} {...props}>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,6 +2,6 @@ export interface PProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const P = ({ children }: PProps): JSX.Element => (
|
export const P = ({ children }: PProps) => (
|
||||||
<p className="leading-7 not-first:mt-6">{children}</p>
|
<p className="leading-7 not-first:mt-6">{children}</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
interface RepoIdentifier {
|
||||||
|
user: string;
|
||||||
|
repo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GithubIssueUrlOptions extends Partial<RepoIdentifier> {
|
||||||
|
repoUrl?: string;
|
||||||
|
body?: string;
|
||||||
|
title?: string;
|
||||||
|
labels?: string[];
|
||||||
|
template?: string;
|
||||||
|
assignee?: string;
|
||||||
|
projects?: string[];
|
||||||
|
logs?: string;
|
||||||
|
version?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidatedOptions = {
|
||||||
|
repoUrl: string;
|
||||||
|
} & Omit<GithubIssueUrlOptions, "repoUrl" | "user" | "repo">;
|
||||||
|
|
||||||
|
const VALID_PARAMS = [
|
||||||
|
"body",
|
||||||
|
"title",
|
||||||
|
"labels",
|
||||||
|
"template",
|
||||||
|
"assignee",
|
||||||
|
"projects",
|
||||||
|
"version",
|
||||||
|
"logs",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a URL for creating a new GitHub issue
|
||||||
|
* @param options Configuration options for the GitHub issue URL
|
||||||
|
* @returns A formatted URL string for creating a new GitHub issue
|
||||||
|
* @throws {Error} If repository information is missing or invalid
|
||||||
|
* @throws {TypeError} If labels or projects are not arrays when provided
|
||||||
|
*/
|
||||||
|
export default function newGithubIssueUrl(
|
||||||
|
options: GithubIssueUrlOptions = {},
|
||||||
|
): string {
|
||||||
|
const validatedOptions = validateOptions(options);
|
||||||
|
const url = new URL(`${validatedOptions.repoUrl}/issues/new`);
|
||||||
|
|
||||||
|
for (const key of VALID_PARAMS) {
|
||||||
|
const value = validatedOptions[key];
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((key === "labels" || key === "projects") && Array.isArray(value)) {
|
||||||
|
url.searchParams.set(key, value.join(","));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
url.searchParams.set(key, String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOptions(options: GithubIssueUrlOptions): ValidatedOptions {
|
||||||
|
const repoUrl =
|
||||||
|
options.repoUrl ??
|
||||||
|
(options.user && options.repo
|
||||||
|
? `https://github.com/${options.user}/${options.repo}`
|
||||||
|
: undefined);
|
||||||
|
|
||||||
|
if (!repoUrl) {
|
||||||
|
throw new Error(
|
||||||
|
"You need to specify either the `repoUrl` option or both the `user` and `repo` options",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of ["labels", "projects"] as const) {
|
||||||
|
const value = options[key];
|
||||||
|
if (value !== undefined && !Array.isArray(value)) {
|
||||||
|
throw new TypeError(`The \`${key}\` option should be an array`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
repoUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -76,6 +76,10 @@
|
|||||||
::file-selector-button {
|
::file-selector-button {
|
||||||
border-color: var(--color-gray-200, currentColor);
|
border-color: var(--color-gray-200, currentColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { Heading } from "@app/components/UI/Typography/Heading";
|
||||||
import { useAppStore } from "@app/core/stores/appStore.ts";
|
import { useAppStore } from "@app/core/stores/appStore.ts";
|
||||||
import { useDeviceStore } from "@app/core/stores/deviceStore.ts";
|
import { useDeviceStore } from "@app/core/stores/deviceStore.ts";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Separator } from "@components/UI/Seperator.tsx";
|
import { Separator } from "@components/UI/Seperator.tsx";
|
||||||
import { H3 } from "@components/UI/Typography/H3.tsx";
|
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||||
import {
|
import {
|
||||||
BluetoothIcon,
|
BluetoothIcon,
|
||||||
@@ -17,7 +17,6 @@ import { useMemo } from "react";
|
|||||||
export const Dashboard = () => {
|
export const Dashboard = () => {
|
||||||
const { setConnectDialogOpen, setSelectedDevice } = useAppStore();
|
const { setConnectDialogOpen, setSelectedDevice } = useAppStore();
|
||||||
const { getDevices } = useDeviceStore();
|
const { getDevices } = useDeviceStore();
|
||||||
const { darkMode } = useAppStore();
|
|
||||||
|
|
||||||
const devices = useMemo(() => getDevices(), [getDevices]);
|
const devices = useMemo(() => getDevices(), [getDevices]);
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ export const Dashboard = () => {
|
|||||||
<div className="flex flex-col gap-3 p-3">
|
<div className="flex flex-col gap-3 p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<H3>Connected Devices</H3>
|
<Heading as="h3">Connected Devices</Heading>
|
||||||
<Subtle>Manage, connect and disconnect devices</Subtle>
|
<Subtle>Manage, connect and disconnect devices</Subtle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,7 +89,7 @@ export const Dashboard = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="m-auto flex flex-col gap-3 text-center">
|
<div className="m-auto flex flex-col gap-3 text-center">
|
||||||
<ListPlusIcon size={48} className="mx-auto text-text-secondary" />
|
<ListPlusIcon size={48} className="mx-auto text-text-secondary" />
|
||||||
<H3>No Devices</H3>
|
<Heading as="h3">No Devices</Heading>
|
||||||
<Subtle>Connect at least one device to get started</Subtle>
|
<Subtle>Connect at least one device to get started</Subtle>
|
||||||
<Button
|
<Button
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
|
|||||||
Reference in New Issue
Block a user