segmented control

This commit is contained in:
EthanShoeDev
2025-09-12 02:33:04 -04:00
parent ee7e4ce8d4
commit d3a76de164
3 changed files with 96 additions and 84 deletions

View File

@@ -28,6 +28,7 @@
"@expo/vector-icons": "^15.0.2", "@expo/vector-icons": "^15.0.2",
"@fressh/assets": "workspace:*", "@fressh/assets": "workspace:*",
"@react-native-picker/picker": "2.11.1", "@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/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.4", "@react-navigation/elements": "^2.6.4",
"@react-navigation/native": "^7.1.8", "@react-navigation/native": "^7.1.8",

View File

@@ -1,17 +1,10 @@
import SSHClient, { PtyType } from '@dylankenneally/react-native-ssh-sftp'; import SSHClient, { PtyType } from '@dylankenneally/react-native-ssh-sftp';
// Removed inline Picker usage in favor of modal selection import SegmentedControl from '@react-native-segmented-control/segmented-control';
import { useStore } from '@tanstack/react-form'; import { useStore } from '@tanstack/react-form';
import { useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import React from 'react'; import React from 'react';
import { import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
Pressable,
ScrollView,
StyleSheet,
Text,
View,
Switch,
} from 'react-native';
import { useAppForm, useFieldContext } from '../components/form-components'; import { useAppForm, useFieldContext } from '../components/form-components';
import { KeyManagerModal } from '../components/key-manager-modal'; import { KeyManagerModal } from '../components/key-manager-modal';
import { import {
@@ -31,77 +24,80 @@ const defaultValues: ConnectionDetails = {
}, },
}; };
export default function Index() { const useSshConnMutation = () => {
const router = useRouter(); const router = useRouter();
// const storedConnectionsQuery = useQuery(
// secretsManager.connections.query.list,
// );
return useMutation({
mutationFn: async (value: ConnectionDetails) => {
try {
console.log('Connecting to SSH server...');
const effective = await (async () => {
if (value.security.type === 'password') return value;
if (value.security.keyId) return value;
const keys = await secretsManager.keys.utils.listEntriesWithValues();
const def = keys.find((k) => k.metadata?.isDefault);
const pick = def ?? keys[0];
if (pick) {
return {
...value,
security: { type: 'key', keyId: pick.id },
} as ConnectionDetails;
}
return value;
})();
const sshClientConnection = await (async () => {
if (effective.security.type === 'password') {
return await SSHClient.connectWithPassword(
effective.host,
effective.port,
effective.username,
effective.security.password,
);
}
const privateKey = await secretsManager.keys.utils.getPrivateKey(
effective.security.keyId,
);
return await SSHClient.connectWithKey(
effective.host,
effective.port,
effective.username,
privateKey.value,
);
})();
await secretsManager.connections.utils.upsertConnection({
id: 'default',
details: effective,
priority: 0,
});
await sshClientConnection.startShell(PtyType.XTERM);
const sshConn = sshConnectionManager.addSession({
client: sshClientConnection,
});
console.log('Connected to SSH server', sshConn.sessionId);
router.push({
pathname: '/shell',
params: {
sessionId: sshConn.sessionId,
},
});
} catch (error) {
console.error('Error connecting to SSH server', error);
throw error;
}
},
});
};
export default function Index() {
const sshConnMutation = useSshConnMutation();
const connectionForm = useAppForm({ const connectionForm = useAppForm({
// https://tanstack.com/form/latest/docs/framework/react/guides/async-initial-values // https://tanstack.com/form/latest/docs/framework/react/guides/async-initial-values
defaultValues, defaultValues,
validators: { validators: {
onChange: connectionDetailsSchema, onChange: connectionDetailsSchema,
onSubmitAsync: async ({ value }) => { onSubmitAsync: async ({ value }) => sshConnMutation.mutateAsync(value),
try {
console.log('Connecting to SSH server...');
const effective = await (async () => {
if (value.security.type === 'password') return value;
if (value.security.keyId) return value;
const keys =
await secretsManager.keys.utils.listEntriesWithValues();
const def = keys.find((k) => k.metadata?.isDefault);
const pick = def ?? keys[0];
if (pick) {
return {
...value,
security: { type: 'key', keyId: pick.id },
} as ConnectionDetails;
}
return value;
})();
const sshClientConnection = await (async () => {
if (effective.security.type === 'password') {
return await SSHClient.connectWithPassword(
effective.host,
effective.port,
effective.username,
effective.security.password,
);
}
const privateKey = await secretsManager.keys.utils.getPrivateKey(
effective.security.keyId,
);
return await SSHClient.connectWithKey(
effective.host,
effective.port,
effective.username,
privateKey.value,
);
})();
await secretsManager.connections.utils.upsertConnection({
id: 'default',
details: effective,
priority: 0,
});
await sshClientConnection.startShell(PtyType.XTERM);
const sshConn = sshConnectionManager.addSession({
client: sshClientConnection,
});
console.log('Connected to SSH server', sshConn.sessionId);
router.push({
pathname: '/shell',
params: {
sessionId: sshConn.sessionId,
},
});
} catch (error) {
console.error('Error connecting to SSH server', error);
throw error;
}
},
}, },
}); });
@@ -164,16 +160,17 @@ export default function Index() {
<connectionForm.AppField name="security.type"> <connectionForm.AppField name="security.type">
{(field) => ( {(field) => (
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<Text style={styles.label}>Use Private Key</Text> <SegmentedControl
<View> values={['Password', 'Private Key']}
{/* Map boolean switch to discriminated union */} selectedIndex={field.state.value === 'password' ? 0 : 1}
<Switch onChange={(event) => {
value={field.state.value === 'key'} field.handleChange(
onValueChange={(val) => { event.nativeEvent.selectedSegmentIndex === 0
field.handleChange(val ? 'key' : 'password'); ? 'password'
}} : 'key',
/> );
</View> }}
/>
</View> </View>
)} )}
</connectionForm.AppField> </connectionForm.AppField>

14
pnpm-lock.yaml generated
View File

@@ -47,6 +47,9 @@ importers:
'@react-native-picker/picker': '@react-native-picker/picker':
specifier: 2.11.1 specifier: 2.11.1
version: 2.11.1(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) version: 2.11.1(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
'@react-native-segmented-control/segmented-control':
specifier: 2.5.7
version: 2.5.7(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
'@react-navigation/bottom-tabs': '@react-navigation/bottom-tabs':
specifier: ^7.4.0 specifier: ^7.4.0
version: 7.4.7(@react-navigation/native@7.1.17(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) version: 7.4.7(@react-navigation/native@7.1.17(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)
@@ -1730,6 +1733,12 @@ packages:
react: '*' react: '*'
react-native: '*' react-native: '*'
'@react-native-segmented-control/segmented-control@2.5.7':
resolution: {integrity: sha512-l84YeVX8xAU3lvOJSvV4nK/NbGhIm2gBfveYolwaoCbRp+/SLXtc6mYrQmM9ScXNwU14mnzjQTpTHWl5YPnkzQ==}
peerDependencies:
react: '>=16.0'
react-native: '>=0.62'
'@react-native/assets-registry@0.81.4': '@react-native/assets-registry@0.81.4':
resolution: {integrity: sha512-AMcDadefBIjD10BRqkWw+W/VdvXEomR6aEZ0fhQRAv7igrBzb4PTn4vHKYg+sUK0e3wa74kcMy2DLc/HtnGcMA==} resolution: {integrity: sha512-AMcDadefBIjD10BRqkWw+W/VdvXEomR6aEZ0fhQRAv7igrBzb4PTn4vHKYg+sUK0e3wa74kcMy2DLc/HtnGcMA==}
engines: {node: '>= 20.19.4'} engines: {node: '>= 20.19.4'}
@@ -8692,6 +8701,11 @@ snapshots:
react: 19.1.0 react: 19.1.0
react-native: 0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0)
'@react-native-segmented-control/segmented-control@2.5.7(react-native@0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0))(react@19.1.0)':
dependencies:
react: 19.1.0
react-native: 0.81.4(@babel/core@7.28.3)(@types/react@19.1.12)(react@19.1.0)
'@react-native/assets-registry@0.81.4': {} '@react-native/assets-registry@0.81.4': {}
'@react-native/babel-plugin-codegen@0.81.4(@babel/core@7.28.3)': '@react-native/babel-plugin-codegen@0.81.4(@babel/core@7.28.3)':