From ef9b892ef10a0c38023d03393ab38f1bbcad6045 Mon Sep 17 00:00:00 2001 From: Drew Volz Date: Sun, 19 May 2024 01:21:56 +0000 Subject: [PATCH 1/2] refactor api tester for better separation of concerns * add a detail screen for api debugging as a pushed view * remove config for preventing dismissing the api debugger * create navigation button to open network logger * add network logger button to tester index and detail * move shared debounceSearch call to @frogpond/useDebounce --- modules/navigation-buttons/index.ts | 1 + modules/navigation-buttons/network-logger.tsx | 37 ++++ modules/use-debounce/index.ts | 10 + modules/use-debounce/package.json | 3 + package-lock.json | 3 + source/navigation/routes.tsx | 5 + source/navigation/types.tsx | 2 + source/views/settings/index.ts | 7 +- .../settings/screens/api-test/api-test.tsx | 190 ------------------ .../settings/screens/api-test/detail.tsx | 125 ++++++++++++ .../views/settings/screens/api-test/index.ts | 6 +- .../views/settings/screens/api-test/list.tsx | 139 +++++++++++++ .../settings/screens/api-test/routes-list.tsx | 81 -------- 13 files changed, 336 insertions(+), 273 deletions(-) create mode 100644 modules/navigation-buttons/network-logger.tsx delete mode 100644 source/views/settings/screens/api-test/api-test.tsx create mode 100644 source/views/settings/screens/api-test/detail.tsx create mode 100644 source/views/settings/screens/api-test/list.tsx delete mode 100644 source/views/settings/screens/api-test/routes-list.tsx diff --git a/modules/navigation-buttons/index.ts b/modules/navigation-buttons/index.ts index 722e06e0c5..83da2353eb 100644 --- a/modules/navigation-buttons/index.ts +++ b/modules/navigation-buttons/index.ts @@ -4,3 +4,4 @@ export {OpenSettingsButton} from './open-settings' export {FavoriteButton} from './favorite' export {DebugNoticeButton} from './debug' export {SearchButton} from './search' +export {NetworkLoggerButton} from './network-logger' diff --git a/modules/navigation-buttons/network-logger.tsx b/modules/navigation-buttons/network-logger.tsx new file mode 100644 index 0000000000..2655def2f0 --- /dev/null +++ b/modules/navigation-buttons/network-logger.tsx @@ -0,0 +1,37 @@ +import * as React from 'react' +import {Platform, StyleSheet, Text} from 'react-native' +import {Touchable} from '@frogpond/touchable' +import {useNavigation, useTheme} from '@react-navigation/native' +import {commonStyles, rightButtonStyles as styles} from './styles' + +export const buttonStyles = StyleSheet.create({ + text: { + ...Platform.select({ + ios: { + fontWeight: '600', + }, + android: { + fontWeight: '400', + }, + }), + }, +}) + +export const NetworkLoggerButton: React.FC = () => { + const navigation = useNavigation() + let {colors} = useTheme() + + return ( + navigation.navigate('NetworkLogger')} + style={styles.button} + > + + Log + + + ) +} diff --git a/modules/use-debounce/index.ts b/modules/use-debounce/index.ts index 2502d09874..e66a6d0713 100644 --- a/modules/use-debounce/index.ts +++ b/modules/use-debounce/index.ts @@ -1,3 +1,4 @@ +import {debounce} from 'lodash' import {useState, useEffect} from 'react' export function useDebounce(value: T, delay: number): T { @@ -15,3 +16,12 @@ export function useDebounce(value: T, delay: number): T { return debouncedValue } + +export const debounceSearch = debounce( + (query: string, callback: () => void) => { + if (query.length >= 2) { + callback() + } + }, + 1500, +) diff --git a/modules/use-debounce/package.json b/modules/use-debounce/package.json index 75f58bea37..00129e6cd3 100644 --- a/modules/use-debounce/package.json +++ b/modules/use-debounce/package.json @@ -8,6 +8,9 @@ "scripts": { "test": "jest" }, + "dependencies": { + "lodash": "4.17.21" + }, "peerDependencies": { "react": "^18.0.0" } diff --git a/package-lock.json b/package-lock.json index e6ed57b2ee..8c296813c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -576,6 +576,9 @@ "name": "@frogpond/use-debounce", "version": "1.0.0", "license": "ISC", + "dependencies": { + "lodash": "4.17.21" + }, "peerDependencies": { "react": "^18.0.0" } diff --git a/source/navigation/routes.tsx b/source/navigation/routes.tsx index 250d668654..e0c0f2400b 100644 --- a/source/navigation/routes.tsx +++ b/source/navigation/routes.tsx @@ -294,6 +294,11 @@ const SettingsStackScreens = () => { name="APITest" options={settings.APITestNavigationOptions} /> + { - let [searchPath, setSearchPath] = React.useState('') - let path = useDebounce(searchPath.trim().toLowerCase(), 500) - - let [displayMode, setDisplayMode] = React.useState('raw') - - let {data, isLoading, error} = useQuery({ - queryKey: ['api-test', path], - queryFn: ({signal, queryKey: [_group, path]}) => { - if (!path) { - return '' - } - return client.get(path, {signal, cache: 'no-store'}).text() - }, - staleTime: 0, - cacheTime: 0, - }) - - let navigation = useNavigation() - - const HeaderLeftButton = () => ( - setSearchPath('')} - style={commonStyles.button} - > - Reset - - ) - - React.useLayoutEffect(() => { - navigation.setOptions({ - headerLeft: () => Platform.OS === 'ios' && , - headerSearchBarOptions: { - autoCapitalize: 'none', - barTintColor: c.systemFill, - // android-only - autoFocus: true, - hideNavigationBar: false, - hideWhenScrolling: false, - onChangeText: (event: ChangeTextEvent) => - setSearchPath(event.nativeEvent.text), - placeholder: 'path/to/uri', - }, - }) - }, [navigation]) - - const JSONView = (): JSX.Element => { - if (data === undefined) { - return <> - } - - const parsed = JSON.parse(data ?? '') - const formatted = JSON.stringify(parsed, null, 2) - const highlighted = syntaxHighlight(formatted) - - const HTML_CONTENT = ` - ${CSS_CODE_STYLES} -
${highlighted}
- ` - - return ( - - ) - } - - const EmptySearch = () => { - return ( - - - Search for an API Endpoint - - ) - } - - let APIResponse = - error !== null ? ( - - ) : !path ? ( - - ) : isLoading ? ( - - ) : displayMode === 'raw' ? ( - - ) : ( - - ) - - const SearchResponseView = () => ( - - setDisplayMode(val ? 'parsed' : 'raw')} - value={displayMode === 'parsed'} - /> - - {APIResponse} - - ) - - return path.length ? ( - - ) : ( - - ) -} - -export const NavigationOptions: NativeStackNavigationOptions = { - title: 'API Tester', - presentation: 'modal', - headerRight: () => Platform.OS === 'ios' && , - gestureEnabled: Platform.OS === 'ios' && false, -} diff --git a/source/views/settings/screens/api-test/detail.tsx b/source/views/settings/screens/api-test/detail.tsx new file mode 100644 index 0000000000..a449e3c452 --- /dev/null +++ b/source/views/settings/screens/api-test/detail.tsx @@ -0,0 +1,125 @@ +import React from 'react' +import {View, StyleSheet, Platform, TextInput} from 'react-native' + +import {LoadingView, NoticeView} from '@frogpond/notice' +import * as c from '@frogpond/colors' + +import {RouteProp, useNavigation, useRoute} from '@react-navigation/native' +import {NativeStackNavigationOptions} from '@react-navigation/native-stack' +import {SettingsStackParamList} from '../../../../navigation/types' +import {useQuery} from '@tanstack/react-query' +import {client} from '@frogpond/api' +import {iOSUIKit, material} from 'react-native-typography' +import {HtmlContent} from '@frogpond/html-content' +import {CellToggle} from '@frogpond/tableview/cells' +import {NetworkLoggerButton} from '@frogpond/navigation-buttons' +import {CSS_CODE_STYLES} from './util/highlight-styles' +import {syntaxHighlight} from './util/highlight' +import {DebugView} from '../debug' + +type DisplayMode = 'raw' | 'parsed' + +export const APITestDetailView = (): JSX.Element => { + let navigation = useNavigation() + let route = useRoute>() + + const {displayName} = route.params.query + const cleanedName = displayName.trim().toLowerCase() + let [displayMode, setDisplayMode] = React.useState('raw') + + let {data, isLoading, error} = useQuery({ + queryKey: ['api-test', cleanedName], + queryFn: ({signal, queryKey: [_group]}) => { + if (!cleanedName) { + return '' + } + return client.get(cleanedName, {signal, cache: 'no-store'}).text() + }, + staleTime: 0, + cacheTime: 0, + }) + + React.useLayoutEffect(() => { + navigation.setOptions({ + title: cleanedName, + headerRight: () => , + }) + }, [cleanedName, navigation]) + + const JSONView = React.useCallback((): JSX.Element => { + if (data === undefined) { + return <> + } + + const parsed = JSON.parse(data ?? '') + const formatted = JSON.stringify(parsed, null, 2) + const highlighted = syntaxHighlight(formatted) + + const HTML_CONTENT = ` + ${CSS_CODE_STYLES} +
${highlighted}
+ ` + + return ( + + ) + }, [data]) + + let APIResponse = + error !== null ? ( + + ) : !isLoading && !cleanedName ? ( + + ) : isLoading ? ( + + ) : displayMode === 'raw' ? ( + + ) : ( + + ) + + return ( + + setDisplayMode(val ? 'parsed' : 'raw')} + value={displayMode === 'parsed'} + /> + + {APIResponse} + + ) +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: c.systemBackground, + flex: 1, + }, + error: { + padding: 10, + color: c.brickRed, + }, + output: { + marginVertical: 3, + paddingRight: 4, + ...Platform.select({ + ios: iOSUIKit.bodyObject, + android: material.body1Object, + }), + }, +}) + +export const NavigationOptions: NativeStackNavigationOptions = {} diff --git a/source/views/settings/screens/api-test/index.ts b/source/views/settings/screens/api-test/index.ts index 9d8c22dbf3..f93c5389ab 100644 --- a/source/views/settings/screens/api-test/index.ts +++ b/source/views/settings/screens/api-test/index.ts @@ -1,4 +1,8 @@ export { APITestView, NavigationOptions as APITestNavigationOptions, -} from './api-test' +} from './list' +export { + APITestDetailView, + NavigationOptions as APITestDetailNavigationOptions, +} from './detail' diff --git a/source/views/settings/screens/api-test/list.tsx b/source/views/settings/screens/api-test/list.tsx new file mode 100644 index 0000000000..dc17392338 --- /dev/null +++ b/source/views/settings/screens/api-test/list.tsx @@ -0,0 +1,139 @@ +import * as c from '@frogpond/colors' +import {LoadingView, NoticeView} from '@frogpond/notice' +import {SearchButton} from '@frogpond/navigation-buttons' +import {debounceSearch} from '@frogpond/use-debounce' +import {useNavigation} from '@react-navigation/native' +import {NativeStackNavigationOptions} from '@react-navigation/native-stack' +import {fromPairs} from 'lodash' +import * as React from 'react' +import {ScrollView, StyleSheet, View} from 'react-native' +import {ChangeTextEvent} from '../../../navigation/types' +import {useAppSelector} from '../../../redux' +import { + selectRecentFilters, + selectRecentSearches, +} from '../../../redux/parts/courses' +import {RecentItemsList} from '../components/recents-list' +import {useFilters} from './lib/build-filters' + +export const NavigationOptions: NativeStackNavigationOptions = { + title: 'Course Catalog', +} + +export const CourseSearchView = (): JSX.Element => { + let navigation = useNavigation() + + let {data: basicFilters = [], isLoading, error} = useFilters() + + let recentFilters = useAppSelector(selectRecentFilters) + let recentSearches = useAppSelector(selectRecentSearches) + + let [typedQuery, setTypedQuery] = React.useState('') + + React.useLayoutEffect(() => { + navigation.setOptions({ + headerRight: () => ( + + navigation.navigate('CourseSearchResults', {initialQuery: ''}) + } + title="Browse" + /> + ), + headerSearchBarOptions: { + barTintColor: c.quaternarySystemFill, + onChangeText: (event: ChangeTextEvent) => { + setTypedQuery(event.nativeEvent.text) + }, + }, + }) + }, [navigation, typedQuery]) + + let showSearchResult = React.useCallback( + (query: string) => { + navigation.navigate('CourseSearchResults', {initialQuery: query}) + }, + [navigation], + ) + + React.useEffect(() => { + debounceSearch(typedQuery, () => { + showSearchResult(typedQuery) + }) + }, [showSearchResult, typedQuery]) + + let onRecentFilterPress = React.useCallback( + (text: string) => { + let selectedFilterCombo = recentFilters.find( + (f) => f.description === text, + ) + + let selectedFilters = basicFilters + if (selectedFilterCombo) { + let filterLookup = fromPairs( + selectedFilterCombo.filters.map((f) => [f.key, f]), + ) + selectedFilters = basicFilters.map((f) => filterLookup[f.key] || f) + } + + navigation.navigate('CourseSearchResults', { + initialFilters: selectedFilters, + }) + }, + [basicFilters, navigation, recentFilters], + ) + + if (isLoading) { + return + } + + if (error) { + return ( + + ) + } + + let recentFilterDescriptions = recentFilters.map((f) => f.description) + + return ( + + + + + + + ) +} + +let styles = StyleSheet.create({ + bottomContainer: { + paddingTop: 12, + }, + container: { + flex: 1, + }, + common: { + backgroundColor: c.systemBackground, + }, +}) diff --git a/source/views/settings/screens/api-test/routes-list.tsx b/source/views/settings/screens/api-test/routes-list.tsx deleted file mode 100644 index e986d21868..0000000000 --- a/source/views/settings/screens/api-test/routes-list.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react' -import {View, SectionList, SafeAreaView, StyleSheet} from 'react-native' - -import {ListRow, ListSectionHeader, ListSeparator, Title} from '@frogpond/lists' -import {Column} from '@frogpond/layout' -import {LoadingView, NoticeView} from '@frogpond/notice' -import * as c from '@frogpond/colors' - -import {useServerRoutes} from './query' - -interface ServerRoutesListParams { - setSearchPath: React.Dispatch> -} - -export const ServerRoutesListView = ( - props: ServerRoutesListParams, -): JSX.Element => { - let { - data: groupedRoutes = [], - error: routesError, - isLoading: isRoutesLoading, - isError: isRoutesError, - refetch: routesRefetch, - } = useServerRoutes() - - return ( - - {isRoutesLoading ? ( - - ) : isRoutesError && routesError instanceof Error ? ( - - ) : !groupedRoutes ? ( - - ) : ( - `${item.path}-${index}`} - keyboardDismissMode="on-drag" - keyboardShouldPersistTaps="never" - onRefresh={routesRefetch} - refreshing={isRoutesLoading} - renderItem={({item}) => { - return ( - - props.setSearchPath(item.displayName)} - style={styles.serverRouteRow} - > - - {item.displayName} - - - - ) - }} - renderSectionHeader={({section: {title}}) => ( - - )} - sections={groupedRoutes} - /> - )} - - ) -} - -const styles = StyleSheet.create({ - serverRouteContainer: { - flex: 1, - backgroundColor: c.systemBackground, - }, - serverRouteRow: { - flexDirection: 'row', - alignItems: 'center', - }, -}) From e1982766ecee4ccaf16b596c7513e45db0f8712f Mon Sep 17 00:00:00 2001 From: Drew Volz Date: Sun, 19 May 2024 01:32:15 +0000 Subject: [PATCH 2/2] update api tester list to separaate concerns --- .../views/settings/screens/api-test/list.tsx | 203 ++++++++---------- 1 file changed, 93 insertions(+), 110 deletions(-) diff --git a/source/views/settings/screens/api-test/list.tsx b/source/views/settings/screens/api-test/list.tsx index dc17392338..2f66abe81a 100644 --- a/source/views/settings/screens/api-test/list.tsx +++ b/source/views/settings/screens/api-test/list.tsx @@ -1,139 +1,122 @@ -import * as c from '@frogpond/colors' +import React from 'react' +import {View, SectionList, SafeAreaView, StyleSheet} from 'react-native' + +import {ListRow, ListSectionHeader, ListSeparator, Title} from '@frogpond/lists' +import {Column} from '@frogpond/layout' import {LoadingView, NoticeView} from '@frogpond/notice' -import {SearchButton} from '@frogpond/navigation-buttons' +import * as c from '@frogpond/colors' import {debounceSearch} from '@frogpond/use-debounce' +import {NetworkLoggerButton} from '@frogpond/navigation-buttons' + +import {ServerRoute, useServerRoutes} from './query' +import {ChangeTextEvent} from '../../../../navigation/types' import {useNavigation} from '@react-navigation/native' import {NativeStackNavigationOptions} from '@react-navigation/native-stack' -import {fromPairs} from 'lodash' -import * as React from 'react' -import {ScrollView, StyleSheet, View} from 'react-native' -import {ChangeTextEvent} from '../../../navigation/types' -import {useAppSelector} from '../../../redux' -import { - selectRecentFilters, - selectRecentSearches, -} from '../../../redux/parts/courses' -import {RecentItemsList} from '../components/recents-list' -import {useFilters} from './lib/build-filters' - -export const NavigationOptions: NativeStackNavigationOptions = { - title: 'Course Catalog', -} -export const CourseSearchView = (): JSX.Element => { +export const APITestView = (): JSX.Element => { let navigation = useNavigation() - let {data: basicFilters = [], isLoading, error} = useFilters() + let [filterPath, setFilterPath] = React.useState('') - let recentFilters = useAppSelector(selectRecentFilters) - let recentSearches = useAppSelector(selectRecentSearches) - - let [typedQuery, setTypedQuery] = React.useState('') + let { + data: groupedRoutes = [], + error: routesError, + isLoading: isRoutesLoading, + isError: isRoutesError, + refetch: routesRefetch, + } = useServerRoutes() React.useLayoutEffect(() => { navigation.setOptions({ - headerRight: () => ( - - navigation.navigate('CourseSearchResults', {initialQuery: ''}) - } - title="Browse" - /> - ), headerSearchBarOptions: { - barTintColor: c.quaternarySystemFill, - onChangeText: (event: ChangeTextEvent) => { - setTypedQuery(event.nativeEvent.text) - }, + autoCapitalize: 'none', + barTintColor: c.systemFill, + // android-only + autoFocus: true, + hideNavigationBar: false, + hideWhenScrolling: false, + onChangeText: (event: ChangeTextEvent) => + setFilterPath(event.nativeEvent.text), + placeholder: '/path/to/uri', }, + headerRight: () => , }) - }, [navigation, typedQuery]) - - let showSearchResult = React.useCallback( - (query: string) => { - navigation.navigate('CourseSearchResults', {initialQuery: query}) - }, - [navigation], - ) - - React.useEffect(() => { - debounceSearch(typedQuery, () => { - showSearchResult(typedQuery) + }, [navigation]) + + let showSearchResult = React.useCallback(() => { + navigation.navigate('APITestDetail', { + query: { + displayName: filterPath, + path: filterPath, + params: [], + }, }) - }, [showSearchResult, typedQuery]) - - let onRecentFilterPress = React.useCallback( - (text: string) => { - let selectedFilterCombo = recentFilters.find( - (f) => f.description === text, - ) + }, [filterPath, navigation]) - let selectedFilters = basicFilters - if (selectedFilterCombo) { - let filterLookup = fromPairs( - selectedFilterCombo.filters.map((f) => [f.key, f]), - ) - selectedFilters = basicFilters.map((f) => filterLookup[f.key] || f) - } - - navigation.navigate('CourseSearchResults', { - initialFilters: selectedFilters, - }) - }, - [basicFilters, navigation, recentFilters], + React.useEffect(() => { + debounceSearch(filterPath, () => showSearchResult()) + }, [filterPath, navigation, showSearchResult]) + + const renderItem = React.useCallback( + (item: ServerRoute) => ( + + navigation.navigate('APITestDetail', {query: item})} + style={styles.serverRouteRow} + > + + {item.displayName} + + + + ), + [navigation], ) - if (isLoading) { - return - } - - if (error) { - return ( - - ) - } - - let recentFilterDescriptions = recentFilters.map((f) => f.description) - return ( - - - + {isRoutesLoading ? ( + + ) : isRoutesError && routesError instanceof Error ? ( + - + ) : ( + `${item.path}-${index}`} + keyboardDismissMode="on-drag" + keyboardShouldPersistTaps="never" + onRefresh={routesRefetch} + refreshing={isRoutesLoading} + renderItem={({item}) => renderItem(item)} + renderSectionHeader={({section: {title}}) => ( + + )} + sections={groupedRoutes} /> - + )} ) } -let styles = StyleSheet.create({ - bottomContainer: { - paddingTop: 12, - }, - container: { +const styles = StyleSheet.create({ + serverRouteContainer: { flex: 1, - }, - common: { backgroundColor: c.systemBackground, }, + serverRouteRow: { + flexDirection: 'row', + alignItems: 'center', + }, }) + +export const NavigationOptions: NativeStackNavigationOptions = { + title: 'API Tester', +}