mirror of
https://github.com/EthanShoeDev/fressh.git
synced 2026-01-11 14:22:51 +00:00
commit
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user