feat(app): show the connection in use on host switcher rows
The old marker only ever tagged the local host as "Local" based on its server id. Each row now shows the connection actually in use, inline after the host name — the TCP or relay endpoint (default :443/:80 stripped), or "Local" for socket/pipe transports.
This commit is contained in:
@@ -393,9 +393,9 @@ export async function expectLocalHostEntryFirst(page: Page, _serverId: string):
|
|||||||
await expect(sidebar).toBeVisible({ timeout: 15_000 });
|
await expect(sidebar).toBeVisible({ timeout: 15_000 });
|
||||||
|
|
||||||
// Single-host fixture: the picker is a non-interactive chip (no dropdown to
|
// Single-host fixture: the picker is a non-interactive chip (no dropdown to
|
||||||
// open) that surfaces the local host by its label. The "Local" marker only
|
// open) that surfaces the local host by its label. The per-row connection
|
||||||
// appears on dropdown rows in the multi-host case, which this fixture does not
|
// endpoint only appears on dropdown rows in the multi-host case, which this
|
||||||
// exercise.
|
// fixture does not exercise.
|
||||||
const picker = sidebar.getByTestId("settings-host-picker");
|
const picker = sidebar.getByTestId("settings-host-picker");
|
||||||
await expect(picker).toBeVisible();
|
await expect(picker).toBeVisible();
|
||||||
await expect(picker.getByText(TEST_HOST_LABEL, { exact: true })).toBeVisible();
|
await expect(picker.getByText(TEST_HOST_LABEL, { exact: true })).toBeVisible();
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ test.describe("Settings host page", () => {
|
|||||||
await expectHostActionCards(page, serverId);
|
await expectHostActionCards(page, serverId);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sidebar pins the local daemon host first with a Local marker", async ({ page }) => {
|
test("sidebar pins the local daemon host first", async ({ page }) => {
|
||||||
const serverId = getServerId();
|
const serverId = getServerId();
|
||||||
|
|
||||||
// Simulate the Electron desktop bridge so `useIsLocalDaemon` resolves the
|
// Simulate the Electron desktop bridge so `useIsLocalDaemon` resolves the
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useCallback, useMemo, type ReactElement, type ReactNode } from "react";
|
import { useCallback, useMemo, type ReactElement, type ReactNode } from "react";
|
||||||
import { Pressable, Text, View } from "react-native";
|
import { Pressable, View } from "react-native";
|
||||||
import type { GestureResponderEvent } from "react-native";
|
import type { GestureResponderEvent } from "react-native";
|
||||||
import { Plus, Server, Settings } from "lucide-react-native";
|
import { Plus, Server, Settings } from "lucide-react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { HostStatusDot } from "@/components/host-status-dot";
|
import { HostStatusDot } from "@/components/host-status-dot";
|
||||||
import { Combobox, ComboboxItem, type ComboboxProps } from "@/components/ui/combobox";
|
import { Combobox, ComboboxItem, type ComboboxProps } from "@/components/ui/combobox";
|
||||||
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
|
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
|
||||||
|
import { useHostRuntimeSnapshot, type ActiveConnection } from "@/runtime/host-runtime";
|
||||||
import { orderHostsLocalFirst } from "@/types/host-connection";
|
import { orderHostsLocalFirst } from "@/types/host-connection";
|
||||||
import {
|
import {
|
||||||
ADD_HOST_OPTION_ID,
|
ADD_HOST_OPTION_ID,
|
||||||
@@ -30,30 +31,48 @@ export function HostStatusDotSlot({ serverId }: { serverId: string }): ReactElem
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Standard secure/plain web ports carry no information in the host display, so
|
||||||
|
// "relay.paseo.sh:443" reads as "relay.paseo.sh" while "127.0.0.1:6767" is kept.
|
||||||
|
function formatConnectionEndpoint(endpoint: string): string {
|
||||||
|
return endpoint.replace(/:(?:443|80)$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socket/pipe transports have no host:port — their endpoint is a filesystem
|
||||||
|
// path, so they read as "Local". TCP and relay show the address being used.
|
||||||
|
function formatActiveConnectionLabel(connection: ActiveConnection): string {
|
||||||
|
if (connection.type === "directSocket" || connection.type === "directPipe") {
|
||||||
|
return "Local";
|
||||||
|
}
|
||||||
|
return formatConnectionEndpoint(connection.endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
export interface HostPickerOptionProps {
|
export interface HostPickerOptionProps {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
label: string;
|
label: string;
|
||||||
isLocal: boolean;
|
showActiveConnection: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
onOpenHostSettings?: (serverId: string) => void;
|
onOpenHostSettings?: (serverId: string) => void;
|
||||||
localMarkerTestID?: string;
|
|
||||||
testID?: string;
|
testID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HostPickerOption({
|
export function HostPickerOption({
|
||||||
serverId,
|
serverId,
|
||||||
label,
|
label,
|
||||||
isLocal,
|
showActiveConnection,
|
||||||
selected,
|
selected,
|
||||||
active,
|
active,
|
||||||
onPress,
|
onPress,
|
||||||
onOpenHostSettings,
|
onOpenHostSettings,
|
||||||
localMarkerTestID,
|
|
||||||
testID,
|
testID,
|
||||||
}: HostPickerOptionProps): ReactElement {
|
}: HostPickerOptionProps): ReactElement {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
|
const activeConnection = useHostRuntimeSnapshot(serverId)?.activeConnection ?? null;
|
||||||
|
const connectionLabel =
|
||||||
|
showActiveConnection && activeConnection
|
||||||
|
? formatActiveConnectionLabel(activeConnection)
|
||||||
|
: undefined;
|
||||||
const leadingSlot = useMemo(() => <HostStatusDotSlot serverId={serverId} />, [serverId]);
|
const leadingSlot = useMemo(() => <HostStatusDotSlot serverId={serverId} />, [serverId]);
|
||||||
const handleSettingsPress = useCallback(
|
const handleSettingsPress = useCallback(
|
||||||
(event: GestureResponderEvent) => {
|
(event: GestureResponderEvent) => {
|
||||||
@@ -63,31 +82,20 @@ export function HostPickerOption({
|
|||||||
[onOpenHostSettings, serverId],
|
[onOpenHostSettings, serverId],
|
||||||
);
|
);
|
||||||
const trailingSlot = useMemo(() => {
|
const trailingSlot = useMemo(() => {
|
||||||
if (!isLocal && !onOpenHostSettings) return undefined;
|
if (!onOpenHostSettings) return undefined;
|
||||||
return (
|
return (
|
||||||
<>
|
<Pressable
|
||||||
{isLocal ? (
|
onPress={handleSettingsPress}
|
||||||
<Text style={styles.localMarker} testID={localMarkerTestID}>
|
hitSlop={8}
|
||||||
Local
|
accessibilityRole="button"
|
||||||
</Text>
|
accessibilityLabel={`Open ${label} settings`}
|
||||||
) : null}
|
>
|
||||||
{onOpenHostSettings ? (
|
<Settings size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||||
<Pressable
|
</Pressable>
|
||||||
onPress={handleSettingsPress}
|
|
||||||
hitSlop={8}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={`Open ${label} settings`}
|
|
||||||
>
|
|
||||||
<Settings size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
|
||||||
</Pressable>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
handleSettingsPress,
|
handleSettingsPress,
|
||||||
isLocal,
|
|
||||||
label,
|
label,
|
||||||
localMarkerTestID,
|
|
||||||
onOpenHostSettings,
|
onOpenHostSettings,
|
||||||
theme.colors.foregroundMuted,
|
theme.colors.foregroundMuted,
|
||||||
theme.iconSize.sm,
|
theme.iconSize.sm,
|
||||||
@@ -96,6 +104,7 @@ export function HostPickerOption({
|
|||||||
return (
|
return (
|
||||||
<ComboboxItem
|
<ComboboxItem
|
||||||
label={label}
|
label={label}
|
||||||
|
description={connectionLabel}
|
||||||
leadingSlot={leadingSlot}
|
leadingSlot={leadingSlot}
|
||||||
trailingSlot={trailingSlot}
|
trailingSlot={trailingSlot}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
@@ -149,7 +158,7 @@ export interface HostPickerProps {
|
|||||||
includeAllHost?: boolean;
|
includeAllHost?: boolean;
|
||||||
includeAddHost?: boolean;
|
includeAddHost?: boolean;
|
||||||
onAddHost?: () => void;
|
onAddHost?: () => void;
|
||||||
showLocalMarker?: boolean;
|
showActiveConnection?: boolean;
|
||||||
onOpenHostSettings?: (serverId: string) => void;
|
onOpenHostSettings?: (serverId: string) => void;
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -157,7 +166,6 @@ export interface HostPickerProps {
|
|||||||
desktopMinWidth?: number;
|
desktopMinWidth?: number;
|
||||||
addHostTestID?: string;
|
addHostTestID?: string;
|
||||||
hostOptionTestID?: (serverId: string) => string;
|
hostOptionTestID?: (serverId: string) => string;
|
||||||
hostLocalMarkerTestID?: (serverId: string) => string;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +179,7 @@ export function HostPicker({
|
|||||||
includeAllHost,
|
includeAllHost,
|
||||||
includeAddHost,
|
includeAddHost,
|
||||||
onAddHost,
|
onAddHost,
|
||||||
showLocalMarker,
|
showActiveConnection,
|
||||||
onOpenHostSettings,
|
onOpenHostSettings,
|
||||||
searchable,
|
searchable,
|
||||||
title,
|
title,
|
||||||
@@ -179,7 +187,6 @@ export function HostPicker({
|
|||||||
desktopMinWidth,
|
desktopMinWidth,
|
||||||
addHostTestID,
|
addHostTestID,
|
||||||
hostOptionTestID,
|
hostOptionTestID,
|
||||||
hostLocalMarkerTestID,
|
|
||||||
children,
|
children,
|
||||||
}: HostPickerProps): ReactElement {
|
}: HostPickerProps): ReactElement {
|
||||||
const localServerId = useLocalDaemonServerId();
|
const localServerId = useLocalDaemonServerId();
|
||||||
@@ -243,23 +250,20 @@ export function HostPicker({
|
|||||||
<HostPickerOption
|
<HostPickerOption
|
||||||
serverId={option.id}
|
serverId={option.id}
|
||||||
label={option.label}
|
label={option.label}
|
||||||
isLocal={showLocalMarker === true && localServerId === option.id}
|
showActiveConnection={showActiveConnection === true}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
active={active}
|
active={active}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onOpenHostSettings={onOpenHostSettings ? handleOpenHostSettings : undefined}
|
onOpenHostSettings={onOpenHostSettings ? handleOpenHostSettings : undefined}
|
||||||
localMarkerTestID={hostLocalMarkerTestID?.(option.id)}
|
|
||||||
testID={hostOptionTestID?.(option.id)}
|
testID={hostOptionTestID?.(option.id)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
addHostTestID,
|
addHostTestID,
|
||||||
hostLocalMarkerTestID,
|
|
||||||
hostOptionTestID,
|
hostOptionTestID,
|
||||||
localServerId,
|
|
||||||
onOpenHostSettings,
|
onOpenHostSettings,
|
||||||
showLocalMarker,
|
showActiveConnection,
|
||||||
handleOpenHostSettings,
|
handleOpenHostSettings,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -292,9 +296,4 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
localMarker: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
marginLeft: theme.spacing[1],
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -290,10 +290,6 @@ function sidebarHostOptionTestID(serverId: string): string {
|
|||||||
return `sidebar-host-row-${serverId}`;
|
return `sidebar-host-row-${serverId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sidebarHostLocalMarkerTestID(serverId: string): string {
|
|
||||||
return `sidebar-host-local-marker-${serverId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FooterIconButton({
|
function FooterIconButton({
|
||||||
buttonRef,
|
buttonRef,
|
||||||
onPress,
|
onPress,
|
||||||
@@ -365,13 +361,12 @@ function SidebarHostPicker({
|
|||||||
anchorRef={triggerRef}
|
anchorRef={triggerRef}
|
||||||
includeAddHost
|
includeAddHost
|
||||||
onAddHost={onAddHost}
|
onAddHost={onAddHost}
|
||||||
showLocalMarker
|
showActiveConnection
|
||||||
onOpenHostSettings={onOpenHostSettings}
|
onOpenHostSettings={onOpenHostSettings}
|
||||||
searchable
|
searchable
|
||||||
desktopMinWidth={240}
|
desktopMinWidth={240}
|
||||||
addHostTestID="sidebar-host-add"
|
addHostTestID="sidebar-host-add"
|
||||||
hostOptionTestID={sidebarHostOptionTestID}
|
hostOptionTestID={sidebarHostOptionTestID}
|
||||||
hostLocalMarkerTestID={sidebarHostLocalMarkerTestID}
|
|
||||||
>
|
>
|
||||||
<FooterIconButton
|
<FooterIconButton
|
||||||
buttonRef={triggerRef}
|
buttonRef={triggerRef}
|
||||||
|
|||||||
@@ -895,8 +895,9 @@ interface HostPickerProps {
|
|||||||
/**
|
/**
|
||||||
* Scopes the four host sections to a host. Reuses the canonical sidebar host
|
* Scopes the four host sections to a host. Reuses the canonical sidebar host
|
||||||
* switcher pattern (left-sidebar.tsx): a quiet row-styled trigger opening a
|
* switcher pattern (left-sidebar.tsx): a quiet row-styled trigger opening a
|
||||||
* <Combobox>. The local host is listed first and tagged "Local"; an "Add host"
|
* <Combobox>. The local host is listed first, each row shows the connection it
|
||||||
* row is always reachable from the list — even with a single host.
|
* is using right now; an "Add host" row is always reachable from the list —
|
||||||
|
* even with a single host.
|
||||||
*/
|
*/
|
||||||
function HostPicker({ activeServerId, sortedHosts, onSelectHost, onAddHost }: HostPickerProps) {
|
function HostPicker({ activeServerId, sortedHosts, onSelectHost, onAddHost }: HostPickerProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -906,7 +907,6 @@ function HostPicker({ activeServerId, sortedHosts, onSelectHost, onAddHost }: Ho
|
|||||||
sortedHosts.find((host) => host.serverId === activeServerId) ?? sortedHosts[0] ?? null;
|
sortedHosts.find((host) => host.serverId === activeServerId) ?? sortedHosts[0] ?? null;
|
||||||
|
|
||||||
const handleOpen = useCallback(() => setIsOpen(true), []);
|
const handleOpen = useCallback(() => setIsOpen(true), []);
|
||||||
const hostLocalMarkerTestID = useCallback(() => "settings-host-local-marker", []);
|
|
||||||
const hostOptionTestID = useCallback(
|
const hostOptionTestID = useCallback(
|
||||||
(serverId: string) => `settings-host-picker-item-${serverId}`,
|
(serverId: string) => `settings-host-picker-item-${serverId}`,
|
||||||
[],
|
[],
|
||||||
@@ -929,12 +929,11 @@ function HostPicker({ activeServerId, sortedHosts, onSelectHost, onAddHost }: Ho
|
|||||||
anchorRef={triggerRef}
|
anchorRef={triggerRef}
|
||||||
includeAddHost
|
includeAddHost
|
||||||
onAddHost={onAddHost}
|
onAddHost={onAddHost}
|
||||||
showLocalMarker
|
showActiveConnection
|
||||||
searchable={false}
|
searchable={false}
|
||||||
title={t("settings.hostPicker.switchHost")}
|
title={t("settings.hostPicker.switchHost")}
|
||||||
desktopMinWidth={240}
|
desktopMinWidth={240}
|
||||||
addHostTestID="settings-add-host"
|
addHostTestID="settings-add-host"
|
||||||
hostLocalMarkerTestID={hostLocalMarkerTestID}
|
|
||||||
hostOptionTestID={hostOptionTestID}
|
hostOptionTestID={hostOptionTestID}
|
||||||
>
|
>
|
||||||
<ComboboxTrigger
|
<ComboboxTrigger
|
||||||
|
|||||||
Reference in New Issue
Block a user