|
|
@ -18,9 +18,7 @@ import { |
|
|
TabsList, |
|
|
TabsList, |
|
|
TabsTrigger, |
|
|
TabsTrigger, |
|
|
} from "@components/UI/Tabs.tsx"; |
|
|
} from "@components/UI/Tabs.tsx"; |
|
|
import { Subtle } from "@components/UI/Typography/Subtle.tsx"; |
|
|
|
|
|
import { AlertCircle } from "lucide-react"; |
|
|
import { AlertCircle } from "lucide-react"; |
|
|
import { useMemo } from "react"; |
|
|
|
|
|
import { Trans, useTranslation } from "react-i18next"; |
|
|
import { Trans, useTranslation } from "react-i18next"; |
|
|
import { Link } from "../UI/Typography/Link.tsx"; |
|
|
import { Link } from "../UI/Typography/Link.tsx"; |
|
|
|
|
|
|
|
|
@ -29,6 +27,7 @@ export interface TabElementProps { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export interface TabManifest { |
|
|
export interface TabManifest { |
|
|
|
|
|
id: "HTTP" | "BLE" | "Serial"; |
|
|
label: string; |
|
|
label: string; |
|
|
element: React.FC<TabElementProps>; |
|
|
element: React.FC<TabElementProps>; |
|
|
isDisabled: boolean; |
|
|
isDisabled: boolean; |
|
|
@ -41,29 +40,28 @@ export interface NewDeviceProps { |
|
|
|
|
|
|
|
|
interface FeatureErrorProps { |
|
|
interface FeatureErrorProps { |
|
|
missingFeatures: BrowserFeature[]; |
|
|
missingFeatures: BrowserFeature[]; |
|
|
|
|
|
tabId: "HTTP" | "BLE" | "Serial"; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const links: { [key: string]: string } = { |
|
|
const errors: Record<BrowserFeature, { href: string; i18nKey: string }> = { |
|
|
"Web Bluetooth": |
|
|
"Web Bluetooth": { |
|
|
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility", |
|
|
href: |
|
|
"Web Serial": |
|
|
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility", |
|
|
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility", |
|
|
i18nKey: "newDeviceDialog.validation.requiresWebBluetooth", |
|
|
"Secure Context": |
|
|
}, |
|
|
"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts", |
|
|
"Web Serial": { |
|
|
|
|
|
href: |
|
|
|
|
|
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility", |
|
|
|
|
|
i18nKey: "newDeviceDialog.validation.requiresWebSerial", |
|
|
|
|
|
}, |
|
|
|
|
|
"Secure Context": { |
|
|
|
|
|
href: |
|
|
|
|
|
"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts", |
|
|
|
|
|
i18nKey: "newDeviceDialog.validation.requiresSecureContext", |
|
|
|
|
|
}, |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => { |
|
|
const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => { |
|
|
const { i18n } = useTranslation("dialog"); |
|
|
|
|
|
|
|
|
|
|
|
const listFormatter = useMemo( |
|
|
|
|
|
() => |
|
|
|
|
|
new Intl.ListFormat(i18n.language, { |
|
|
|
|
|
style: "long", |
|
|
|
|
|
type: "disjunction", |
|
|
|
|
|
}), |
|
|
|
|
|
[i18n.language], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
if (missingFeatures.length === 0) return null; |
|
|
if (missingFeatures.length === 0) return null; |
|
|
|
|
|
|
|
|
const browserFeatures = missingFeatures.filter( |
|
|
const browserFeatures = missingFeatures.filter( |
|
|
@ -71,37 +69,32 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => { |
|
|
); |
|
|
); |
|
|
const needsSecureContext = missingFeatures.includes("Secure Context"); |
|
|
const needsSecureContext = missingFeatures.includes("Secure Context"); |
|
|
|
|
|
|
|
|
const formatFeatureList = (features: string[]) => { |
|
|
const needsFeature = |
|
|
const parts = listFormatter.formatToParts(features); |
|
|
(tabId === "BLE" && browserFeatures.includes("Web Bluetooth")) |
|
|
return parts.map((part) => { |
|
|
? "Web Bluetooth" |
|
|
if (part.type === "element") { |
|
|
: (tabId === "Serial" && browserFeatures.includes("Web Serial")) |
|
|
return ( |
|
|
? "Web Serial" |
|
|
<Link key={part.value} href={links[part.value]}> |
|
|
: undefined; |
|
|
{part.value} |
|
|
|
|
|
</Link> |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
return <span key={part.value}>{part.value}</span>; |
|
|
|
|
|
}); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const featureNodes = formatFeatureList(browserFeatures); |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<Subtle className="flex flex-col items-start gap-2 bg-red-500 p-4 rounded-md"> |
|
|
<div className="flex flex-col items-start gap-2 bg-red-500 p-4 rounded-md text-sm text-slate-500 dark:text-slate-400"> |
|
|
<div className="flex items-center gap-2 w-full"> |
|
|
<div className="flex items-center gap-2 w-full"> |
|
|
<AlertCircle size={40} className="mr-2 shrink-0 text-white" /> |
|
|
<AlertCircle size={40} className="mr-2 shrink-0 text-white" /> |
|
|
<div className="flex flex-col gap-3"> |
|
|
<div className="flex flex-col gap-3"> |
|
|
<p className="text-sm text-white"> |
|
|
<div className="text-sm text-white"> |
|
|
{browserFeatures.length > 0 && ( |
|
|
{needsFeature && ( |
|
|
<Trans |
|
|
<Trans |
|
|
i18nKey="newDeviceDialog.validation.requiresFeatures" |
|
|
i18nKey={errors[needsFeature].i18nKey} |
|
|
components={{ |
|
|
components={[ |
|
|
"0": <>{featureNodes}</>, |
|
|
<Link |
|
|
}} |
|
|
key="0" |
|
|
|
|
|
href={errors[needsFeature].href} |
|
|
|
|
|
className="underline hover:text-slate-200 text-white dark:text-white dark:hover:text-slate-300" |
|
|
|
|
|
/>, |
|
|
|
|
|
]} |
|
|
/> |
|
|
/> |
|
|
)} |
|
|
)} |
|
|
{browserFeatures.length > 0 && needsSecureContext && " "} |
|
|
{needsFeature && needsSecureContext && " "} |
|
|
{needsSecureContext && ( |
|
|
{needsSecureContext && ( |
|
|
<Trans |
|
|
<Trans |
|
|
i18nKey={browserFeatures.length > 0 |
|
|
i18nKey={browserFeatures.length > 0 |
|
|
@ -110,17 +103,17 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => { |
|
|
components={{ |
|
|
components={{ |
|
|
"0": ( |
|
|
"0": ( |
|
|
<Link |
|
|
<Link |
|
|
href={links["Secure Context"]} |
|
|
href={errors["Secure Context"].href} |
|
|
className="underline hover:text-slate-200" |
|
|
className="underline hover:text-slate-200" |
|
|
/> |
|
|
/> |
|
|
), |
|
|
), |
|
|
}} |
|
|
}} |
|
|
/> |
|
|
/> |
|
|
)} |
|
|
)} |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</Subtle> |
|
|
</div> |
|
|
); |
|
|
); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
@ -133,17 +126,20 @@ export const NewDeviceDialog = ({ |
|
|
|
|
|
|
|
|
const tabs: TabManifest[] = [ |
|
|
const tabs: TabManifest[] = [ |
|
|
{ |
|
|
{ |
|
|
|
|
|
id: "HTTP", |
|
|
label: t("newDeviceDialog.tabHttp"), |
|
|
label: t("newDeviceDialog.tabHttp"), |
|
|
element: HTTP, |
|
|
element: HTTP, |
|
|
isDisabled: false, |
|
|
isDisabled: false, |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
{ |
|
|
|
|
|
id: "BLE", |
|
|
label: t("newDeviceDialog.tabBluetooth"), |
|
|
label: t("newDeviceDialog.tabBluetooth"), |
|
|
element: BLE, |
|
|
element: BLE, |
|
|
isDisabled: unsupported.includes("Web Bluetooth") || |
|
|
isDisabled: unsupported.includes("Web Bluetooth") || |
|
|
unsupported.includes("Secure Context"), |
|
|
unsupported.includes("Secure Context"), |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
{ |
|
|
|
|
|
id: "Serial", |
|
|
label: t("newDeviceDialog.tabSerial"), |
|
|
label: t("newDeviceDialog.tabSerial"), |
|
|
element: Serial, |
|
|
element: Serial, |
|
|
isDisabled: unsupported.includes("Web Serial") || |
|
|
isDisabled: unsupported.includes("Web Serial") || |
|
|
@ -161,21 +157,27 @@ export const NewDeviceDialog = ({ |
|
|
<Tabs defaultValue="HTTP"> |
|
|
<Tabs defaultValue="HTTP"> |
|
|
<TabsList> |
|
|
<TabsList> |
|
|
{tabs.map((tab) => ( |
|
|
{tabs.map((tab) => ( |
|
|
<TabsTrigger key={tab.label} value={tab.label}> |
|
|
<TabsTrigger key={tab.id} value={tab.id}> |
|
|
{tab.label} |
|
|
{tab.label} |
|
|
</TabsTrigger> |
|
|
</TabsTrigger> |
|
|
))} |
|
|
))} |
|
|
</TabsList> |
|
|
</TabsList> |
|
|
{tabs.map((tab) => ( |
|
|
{tabs.map((tab) => ( |
|
|
<TabsContent key={tab.label} value={tab.label}> |
|
|
<TabsContent key={tab.id} value={tab.id}> |
|
|
<fieldset disabled={tab.isDisabled}> |
|
|
<fieldset disabled={tab.isDisabled}> |
|
|
{(tab.label !== "HTTP" && |
|
|
{(tab.id !== "HTTP" && |
|
|
tab.isDisabled) |
|
|
tab.isDisabled) |
|
|
? <ErrorMessage missingFeatures={unsupported} /> |
|
|
? ( |
|
|
: null} |
|
|
<ErrorMessage |
|
|
<tab.element |
|
|
missingFeatures={unsupported} |
|
|
closeDialog={() => onOpenChange(false)} |
|
|
tabId={tab.id} |
|
|
/> |
|
|
/> |
|
|
|
|
|
) |
|
|
|
|
|
: ( |
|
|
|
|
|
<tab.element |
|
|
|
|
|
closeDialog={() => onOpenChange(false)} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
</fieldset> |
|
|
</fieldset> |
|
|
</TabsContent> |
|
|
</TabsContent> |
|
|
))} |
|
|
))} |
|
|
|