This commit is contained in:
EthanShoeDev
2025-09-15 15:48:13 -04:00
parent b666ec72a7
commit b078d97af1
9 changed files with 128 additions and 60 deletions

View File

@@ -28,11 +28,11 @@
"@expo/vector-icons": "^15.0.2",
"@fressh/assets": "workspace:*",
"@fressh/react-native-uniffi-russh": "workspace:*",
"@react-native-picker/picker": "2.11.1",
"@react-native-segmented-control/segmented-control": "2.5.7",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.4",
"@react-navigation/native": "^7.1.8",
"@shopify/flash-list": "2.0.2",
"@tanstack/react-form": "^1.20.0",
"@tanstack/react-query": "^5.87.4",
"expo": "54.0.7",

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs';
import React from 'react';
export default function TabsLayout() {
return (

View File

@@ -16,6 +16,7 @@ import {
SafeAreaView,
useSafeAreaInsets,
} from 'react-native-safe-area-context';
import { KeyList } from '@/components/key-manager/KeyList';
import { AbortSignalTimeout } from '@/lib/utils';
import { useAppForm, useFieldContext } from '../components/form-components';
import {
@@ -25,8 +26,6 @@ import {
} from '../lib/secrets-manager';
// import { sshConnectionManager } from '../lib/ssh-connection-manager';
import { useTheme } from '../theme';
import { useFocusEffect } from '@react-navigation/native';
import { KeyList } from '@/components/key-manager/KeyList';
const defaultValues: ConnectionDetails = {
host: 'test.rebex.net',
@@ -260,11 +259,15 @@ function KeyIdPickerField() {
}, [listPrivateKeysQuery.data]);
const keys = listPrivateKeysQuery.data ?? [];
const fieldValue = field.state.value;
const defaultPickId = defaultPick?.id;
const fieldHandleChange = field.handleChange;
React.useEffect(() => {
if (!field.state.value && defaultPick?.id) {
field.handleChange(defaultPick.id);
if (!fieldValue && defaultPickId) {
fieldHandleChange(defaultPickId);
}
}, [field.state.value, defaultPick?.id]);
}, [fieldValue, defaultPickId, fieldHandleChange]);
const computedSelectedId = field.state.value ?? defaultPick?.id;
const selected = keys.find((k) => k.id === computedSelectedId);

View File

@@ -68,13 +68,6 @@ export default function ShellDetail() {
};
}, [connection]);
// Cleanup when leaving screen
useEffect(() => {
return () => {
if (connection) void connection.disconnect().catch(() => {});
};
}, [connection, shell]);
const scrollViewRef = useRef<ScrollView | null>(null);
useEffect(() => {

View File

@@ -1,7 +1,40 @@
import {
RnRussh,
type SshConnection,
type SshShellSession,
} from '@fressh/react-native-uniffi-russh';
import { FlashList } from '@shopify/flash-list';
import { Link } from 'expo-router';
import { StyleSheet, Text, View } from 'react-native';
type ShellWithConnection = SshShellSession & { connection: SshConnection };
export default function ShellList() {
const connectionsWithShells = RnRussh.listSshConnectionsWithShells();
const shellsFirstList = connectionsWithShells.reduce<ShellWithConnection[]>(
(acc, curr) => {
acc.push(...curr.shells.map((shell) => ({ ...shell, connection: curr })));
return acc;
},
[],
);
return (
<View style={styles.container}>
{shellsFirstList.length === 0 ? (
<EmptyState />
) : (
<FlashList
data={shellsFirstList}
renderItem={({ item }) => <ShellCard shell={item} />}
// maintainVisibleContentPosition={{ autoscrollToBottomThreshold: 0.2 }}
/>
)}
</View>
);
}
function EmptyState() {
return (
<View style={styles.container}>
<Text style={styles.text}>No active shells. Connect from Host tab.</Text>
@@ -10,6 +43,24 @@ export default function ShellList() {
);
}
function ShellCard({ shell }: { shell: ShellWithConnection }) {
return (
<View style={styles.container}>
<Text style={styles.text}>{shell.connectionId}</Text>
<Text style={styles.text}>{shell.createdAtMs}</Text>
<Text style={styles.text}>{shell.pty}</Text>
<Text style={styles.text}>{shell.connection.connectionDetails.host}</Text>
<Text style={styles.text}>{shell.connection.connectionDetails.port}</Text>
<Text style={styles.text}>
{shell.connection.connectionDetails.username}
</Text>
<Text style={styles.text}>
{shell.connection.connectionDetails.security.type}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
text: { color: '#E5E7EB', marginBottom: 8 },

View File

@@ -1,4 +1,3 @@
import { Picker } from '@react-native-picker/picker';
import {
createFormHook,
createFormHookContexts,
@@ -98,31 +97,6 @@ export function SwitchField(
);
}
export function PickerField<T>(
props: React.ComponentProps<typeof Picker<T>> & {
label?: string;
},
) {
const { label, style, ...rest } = props;
const field = useFieldContext<T>();
return (
<View style={styles.inputGroup}>
{label ? <Text style={styles.label}>{label}</Text> : null}
<View style={[styles.input, styles.pickerContainer]}>
<Picker<T>
style={styles.picker}
selectedValue={field.state.value}
onValueChange={(itemValue) => field.handleChange(itemValue)}
{...rest}
>
{props.children}
</Picker>
</View>
<FieldInfo />
</View>
);
}
export function SubmitButton(
props: {
onPress?: () => void;
@@ -162,7 +136,6 @@ export const { useAppForm, withForm, withFieldGroup } = createFormHook({
fieldComponents: {
TextField,
NumberField,
PickerField,
SwitchField,
},
formComponents: {