diff --git a/app/src/DashboardPage.js b/app/src/DashboardPage.js
index a3ac7352..66af8fae 100644
--- a/app/src/DashboardPage.js
+++ b/app/src/DashboardPage.js
@@ -156,19 +156,34 @@ export function Getters() {
export function DashboardPage() {
const navigate = useNavigate();
+ // The search the user submitted.
const {searchStr, setSearchStr, activeTags} = useSearch();
+ // The search that the user is typing.
+ const [localSearchStr, setLocalSearchStr] = React.useState(searchStr);
+ console.log('searchStr', searchStr, 'localSearchStr', localSearchStr);
const {
suggestionsResults,
handleResultSelect,
resultRenderer,
loading,
- setSearchStr: setSuggestionSearchStr
+ setSearchStr: setSuggestionSearchStr,
+ setSearchTags,
} = useContext(SearchSuggestionsContext);
const {status} = useContext(StatusContext);
React.useEffect(() => {
- setSuggestionSearchStr(searchStr);
- }, [searchStr])
+ setSuggestionSearchStr(localSearchStr);
+ }, [localSearchStr]);
+
+ React.useEffect(() => {
+ setLocalSearchStr(searchStr);
+ }, [searchStr]);
+
+ React.useEffect(() => {
+ if (activeTags) {
+ setSearchTags(activeTags);
+ }
+ }, [JSON.stringify(activeTags)]);
let title = 'Dashboard';
if (searchStr) {
@@ -192,24 +207,28 @@ export function DashboardPage() {
body = ;
}
+ const getSearchResultsInput = (props) => {
+ return navigate('/')}
+ style={{marginBottom: '2em'}}
+ results={suggestionsResults}
+ handleResultSelect={handleResultSelect}
+ resultRenderer={resultRenderer}
+ loading={loading}
+ {...props}
+ />;
+ };
+
return
- navigate('/')}
- style={{marginBottom: '2em'}}
- results={suggestionsResults}
- handleResultSelect={handleResultSelect}
- resultRenderer={resultRenderer}
- loading={loading}
- />
+ {getSearchResultsInput({size: 'large'})}
@@ -221,19 +240,7 @@ export function DashboardPage() {
- navigate('/')}
- style={{marginBottom: '2em'}}
- results={suggestionsResults}
- handleResultSelect={handleResultSelect}
- resultRenderer={resultRenderer}
- loading={loading}
- />
+ {getSearchResultsInput({size: 'big'})}
diff --git a/app/src/api.js b/app/src/api.js
index 142b4c2c..0808aed8 100644
--- a/app/src/api.js
+++ b/app/src/api.js
@@ -1003,36 +1003,23 @@ export async function fetchDecoded(vinNumber) {
}
}
-export async function searchSuggestions(search_str) {
- const body = {search_str: search_str};
+export async function searchSuggestions(search_str, tagNames) {
+ const body = {search_str: search_str, tag_names: tagNames};
const response = await apiPost(`${API_URI}/search_suggestions`, body);
if (response.ok) {
const content = await response.json();
const fileGroups = content.file_groups;
- const zimsEstimates = content.zims_estimates;
const channels = content.channels;
const domains = content.domains;
return {
fileGroups,
- zimsEstimates,
channels,
domains,
}
}
}
-export async function searchEstimateFiles(search_str, tagNames) {
- const body = {search_str: search_str, tag_names: tagNames};
- const response = await apiPost(`${API_URI}/files/search_estimates`, body);
- if (response.ok) {
- const content = await response.json();
- return {
- fileGroups: content.file_groups,
- }
- }
-}
-
export async function searchEstimateZims(search_str, tagNames) {
const body = {search_str: search_str, tag_names: tagNames};
const response = await apiPost(`${API_URI}/zim/search_estimates`, body);
diff --git a/app/src/components/Common.js b/app/src/components/Common.js
index db313488..31021fb2 100644
--- a/app/src/components/Common.js
+++ b/app/src/components/Common.js
@@ -1396,7 +1396,10 @@ export function TagIcon() {
}
export function normalizeEstimate(estimate) {
- return estimate > 999 ? '>999' : estimate;
+ if (Number.isInteger(estimate)) {
+ return estimate > 999 ? '>999' : estimate.toString();
+ }
+ return '?';
}
export function useAPIButton(
diff --git a/app/src/components/Search.js b/app/src/components/Search.js
index ebe24404..7b5e0f99 100644
--- a/app/src/components/Search.js
+++ b/app/src/components/Search.js
@@ -3,7 +3,7 @@ import {createSearchParams, Route, Routes, useNavigate} from "react-router-dom";
import {FileSearchFilterButton, FilesSearchView} from "./Files";
import {useLatestRequest, usePages, useQuery} from "../hooks/customHooks";
import {ZimSearchView} from "./Zim";
-import {searchEstimateFiles, searchEstimateZims, searchSuggestions} from "../api";
+import {searchEstimateZims, searchSuggestions} from "../api";
import {fuzzyMatch, normalizeEstimate, SearchResultsInput, TabLinks} from "./Common";
import _ from "lodash";
import {TagsContext} from "../Tags";
@@ -11,35 +11,28 @@ import {Header as SHeader, Label} from "semantic-ui-react";
import {Button, Modal, ModalContent} from "./Theme";
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
-export const SearchSuggestionsContext = React.createContext({
- suggestionsResults: {},
- suggestions: {},
- suggestionsSums: {},
- handleResultSelect: null,
- resultRenderer: null,
- loading: false,
- searchStr: '',
- setSearchStr: null,
-});
-
-
-export const SearchSuggestionsProvider = (props) => {
- const value = useSearchSuggestions();
-
- return
- {props.children}
-
-}
-
const SUGGESTED_APPS = [
- {location: '/more/otp', title: 'One Time Pad', description: 'Encrypt and Decrypt messages'},
- {location: '/inventory', title: 'Inventory', description: 'Track and organize your food storage'},
- {location: '/more/vin', title: 'Vin Decoder', description: 'Decode and analyze vehicle VIN numbers'},
- {location: '/admin', title: 'Downloads', description: 'View your downloads'},
+ {location: '/admin', title: 'Downloads', description: 'View and control your downloads'},
+ {location: '/admin/settings', title: 'Hotspot', description: 'Control your hotspot'},
+ {location: '/admin/settings', title: 'Restart', description: 'Restart your WROLPi'},
{location: '/admin/settings', title: 'Settings', description: 'View and modify your settings'},
+ {location: '/admin/settings', title: 'Shutdown', description: 'Shutdown your WROLPi'},
{location: '/admin/status', title: 'Status', description: 'View the status of this WROLPi server'},
{location: '/admin/wrol', title: 'WROL Mode', description: 'Enable or disable WROL Mode'},
+ {location: '/archive', title: 'Archives', description: 'View your Archives'},
+ {location: '/archive/domains', title: 'Domains', description: 'View the domains of your Archives'},
+ {location: '/files', title: 'Files', description: 'View your files'},
{location: '/help', title: 'Help', description: 'Help documents for WROLPi'},
+ {location: '/inventory', title: 'Inventory', description: 'Track and organize your food storage'},
+ {location: '/map', title: 'Map', description: 'View your Map'},
+ {location: '/map/manage', title: 'Manage Map', description: 'Manage your Map'},
+ {location: '/more/otp', title: 'One Time Pad', description: 'Encrypt and Decrypt messages'},
+ {location: '/more/statistics', title: 'Statistics', description: 'View the statistics of your WROLPi'},
+ {location: '/more/vin', title: 'Vin Decoder', description: 'Decode and analyze vehicle VIN numbers'},
+ {location: '/videos', title: 'Videos', description: 'View your Videos'},
+ {location: '/videos/channel', title: 'Channels', description: 'View the Channels of your Videos'},
+ {location: '/zim', title: 'Zim', description: 'View your Zims'},
+ {location: '/zim/manage', title: 'Manage Zim', description: 'Manage your Zims'},
];
export const useSearch = (defaultLimit = 48, totalPages = 0, emptySearch = false, model) => {
@@ -101,27 +94,65 @@ export const useSearch = (defaultLimit = 48, totalPages = 0, emptySearch = false
}
}
-export function useSuggestions(searchStr) {
- const {data, sendRequest, loading} = useLatestRequest(500);
+export const SearchSuggestionsContext = React.createContext({
+ suggestions: {}, // from the api
+ suggestionsResults: {}, // suggestions converted for results.
+ suggestionsSums: {}, // suggestions from the api summarized.
+ handleResultSelect: null, // user clicks "result"
+ resultRenderer: null,
+ loading: false,
+ searchStr: '',
+ setSearchStr: null,
+ setSearchTags: null,
+});
+
+export function useSuggestions(searchStr, tagNames) {
+ const defaultSuggestions = {
+ fileGroups: [],
+ channels: [],
+ domains: [],
+ zimsEstimates: [],
+ }
+ const [suggestions, setSuggestions] = React.useState(defaultSuggestions);
+ // fileGroups/channels/domains.
+ const {data, sendRequest, loading} = useLatestRequest(500, true);
+ // Zims are slow, so they are separate.
+ const {data: zimData, sendRequest: sendZimRequest, loading: zimLoading} = useLatestRequest(500, true);
React.useEffect(() => {
- if (!searchStr || searchStr.length === 0) {
- return;
+ if ((searchStr && searchStr.length > 0) || (tagNames && tagNames.length > 0)) {
+ sendRequest(async () => await searchSuggestions(searchStr, tagNames));
+ sendZimRequest(async () => await searchEstimateZims(searchStr, tagNames));
}
+ }, [searchStr, sendRequest, sendZimRequest, JSON.stringify(tagNames)]);
- // Use the `useLatestRequest` to handle user typing.
- sendRequest(async () => await searchSuggestions(searchStr));
- }, [searchStr, sendRequest]);
+ React.useEffect(() => {
+ if (!_.isEmpty(data)) {
+ setSuggestions({
+ ...suggestions,
+ channels: data.channels,
+ fileGroups: data.fileGroups,
+ domains: data.domains,
+ });
+ }
+ }, [JSON.stringify(data)]);
- return {suggestions: data, loading}
+ React.useEffect(() => {
+ if (!_.isEmpty(zimData)) {
+ setSuggestions({...suggestions, zimsEstimates: zimData.zimsEstimates});
+ }
+ }, [JSON.stringify(zimData)]);
+
+ return {suggestions, loading: loading || zimLoading}
}
-export function useSearchSuggestions(defaultSearchStr) {
+export function useSearchSuggestions(defaultSearchStr, defaultTagNames) {
const navigate = useNavigate();
const [searchStr, setSearchStr] = React.useState(defaultSearchStr || '');
+ const [searchTags, setSearchTags] = React.useState(defaultTagNames);
const {SingleTag, fuzzyMatchTagsByName} = React.useContext(TagsContext);
- const {suggestions, loading} = useSuggestions(searchStr);
+ const {suggestions, loading} = useSuggestions(searchStr, searchTags);
// The results that will be displayed by .
const [suggestionsResults, setSuggestionsResults] = useState({});
@@ -130,7 +161,7 @@ export function useSearchSuggestions(defaultSearchStr) {
const normalizeSuggestionsResults = (newSuggestions) => {
// Convert the suggestions from the Backend to what the Semantic expects.
- const lowerSearchStr = searchStr.toLowerCase();
+ const lowerSearchStr = searchStr ? searchStr.toLowerCase() : '';
let results = {};
@@ -147,8 +178,10 @@ export function useSearchSuggestions(defaultSearchStr) {
};
}
- const zimSum = newSuggestions.zimsEstimates.reduce((i, j) => i + j, 0);
- if (zimSum > 0) {
+ const zimSum = newSuggestions.zimsEstimates && newSuggestions.zimsEstimates.length > 0
+ ? newSuggestions.zimsEstimates.reduce((i, j) => i + j.estimate, 0)
+ : null;
+ if (newSuggestions && zimSum > 0) {
results.zimsSum = {
name: 'Zims', results: [
{title: zimSum.toString(), type: 'zims', location: `/search/zim?q=${encodeURIComponent(searchStr)}`}
@@ -176,7 +209,8 @@ export function useSearchSuggestions(defaultSearchStr) {
}
}
- const matchingTags = fuzzyMatchTagsByName(searchStr);
+ // Match at most 5 Tags.
+ const matchingTags = searchStr ? fuzzyMatchTagsByName(searchStr).slice(0, 5) : null;
if (matchingTags && matchingTags.length > 0) {
results.tags = {
name: 'Tags', results: matchingTags.map(i => {
@@ -189,7 +223,8 @@ export function useSearchSuggestions(defaultSearchStr) {
i.title.toLowerCase().includes(lowerSearchStr)
|| fuzzyMatch(i.title.toLowerCase(), lowerSearchStr));
if (matchingApps && matchingApps.length > 0) {
- results.apps = {name: 'Apps', results: matchingApps};
+ // Match at most 5 apps.
+ results.apps = {name: 'Apps', results: matchingApps.slice(0, 5)};
}
setSuggestionsResults(results);
@@ -198,8 +233,8 @@ export function useSearchSuggestions(defaultSearchStr) {
zims: zimSum,
channels: newSuggestions.channels.length,
domains: newSuggestions.domains.length,
- tags: matchingTags.length,
- apps: matchingApps.length,
+ tags: matchingTags?.length,
+ apps: matchingApps?.length,
});
}
@@ -236,6 +271,7 @@ export function useSearchSuggestions(defaultSearchStr) {
suggestionsSums,
searchStr,
setSearchStr,
+ setSearchTags,
handleResultSelect,
resultRenderer,
loading
@@ -243,67 +279,20 @@ export function useSearchSuggestions(defaultSearchStr) {
}
-export function useSearchEstimate(searchStr, tagNames) {
- const {data: filesData, sendRequest: sendFilesRequest, loading: loadingFiles} = useLatestRequest(250);
- const {data: zimsData, sendRequest: sendZimsRequest, loading: loadingZims} = useLatestRequest(250);
+export const SearchSuggestionsProvider = (props) => {
+ const value = useSearchSuggestions();
- // The estimates from the response.
- const [estimates, setEstimates] = React.useState({
- fileGroups: [],
- zims: [],
- });
- // The estimates summarized.
- const [estimatesSums, setEstimatesSums] = React.useState({
- fileGroups: 0,
- zims: 0,
- });
-
- React.useEffect(() => {
- // Handle results from `sendRequest`.
- if (_.isEmpty(filesData)) {
- return;
- }
-
- setEstimates({...estimates, fileGroups: filesData.fileGroups});
- setEstimatesSums({...estimatesSums, fileGroups: filesData.fileGroups});
- }, [JSON.stringify(filesData)]);
-
- React.useEffect(() => {
- sendFilesRequest(async () => await searchEstimateFiles(searchStr, tagNames));
- }, [searchStr, JSON.stringify(tagNames)], sendFilesRequest);
-
- React.useEffect(() => {
- // Handle results from `sendRequest`.
- if (_.isEmpty(zimsData)) {
- return;
- }
-
- setEstimates({...estimates, zimsEstimates: zimsData.zimsEstimates});
- setEstimatesSums({...estimatesSums, zims: zimsData.zimsEstimates.reduce((i, j) => i + j['estimate'], 0)});
- }, [JSON.stringify(zimsData)]);
-
- React.useEffect(() => {
- sendZimsRequest(async () => await searchEstimateZims(searchStr, tagNames));
- }, [searchStr, JSON.stringify(tagNames)], sendZimsRequest);
-
- return {
- estimates,
- estimatesSums,
- loading: loadingFiles || loadingZims,
- }
+ return
+ {props.children}
+
}
export function SearchView() {
- const {searchStr, activeTags} = useSearch();
- const {estimates, estimatesSums, loading} = useSearchEstimate(searchStr, activeTags);
+ const {suggestions, suggestionsSums, loading} = useContext(SearchSuggestionsContext);
- let filesTabName = Files ;
- let zimsTabName = Zims;
- if (!_.isEmpty(estimatesSums)) {
- filesTabName = Files ;
- zimsTabName = Zims ;
- }
+ const filesTabName = Files ;
+ const zimsTabName = Zims ;
const links = [
{text: filesTabName, to: '/search', key: 'filesSearch', end: true},
@@ -314,7 +303,7 @@ export function SearchView() {
}/>
- }/>
+ }/>
}
diff --git a/app/src/components/Zim.js b/app/src/components/Zim.js
index 00e00b92..bb42c5a6 100644
--- a/app/src/components/Zim.js
+++ b/app/src/components/Zim.js
@@ -287,10 +287,10 @@ const ZimsRefreshWarning = () => {
;
}
-export const ZimSearchView = ({estimates, loading}) => {
+export const ZimSearchView = ({suggestions, loading}) => {
const [activeIndex, setActiveIndex] = React.useState(null);
const {searchStr, activeTags, setTags} = useSearch();
- const {zimsEstimates} = estimates;
+ const {zimsEstimates} = suggestions;
const handleClick = (index, activeIndex_) => {
setActiveIndex(index === activeIndex_ ? -1 : index);
@@ -308,7 +308,11 @@ export const ZimSearchView = ({estimates, loading}) => {
onClick={handleClick}
/>);
} else if (loading) {
- body = ;
+ body =
+
+
+
+ ;
} else if (_.isEmpty(zimsEstimates)) {
body = ;
}
diff --git a/app/src/hooks/customHooks.js b/app/src/hooks/customHooks.js
index 0d9abbb6..8e8bb4bb 100644
--- a/app/src/hooks/customHooks.js
+++ b/app/src/hooks/customHooks.js
@@ -63,7 +63,7 @@ export const useRecurringTimeout = (callback, delay) => {
}, [delay])
}
-export const useLatestRequest = (delay = 300) => {
+export const useLatestRequest = (delay = 300, defaultLoading = false) => {
// A hook which ignores older requests and will only set `data` to the latest response's data.
// usage: sendRequest(async () => await yourAPICall(...args));
@@ -72,7 +72,7 @@ export const useLatestRequest = (delay = 300) => {
const latestRequestRef = React.useRef(0);
const debounceTimerRef = React.useRef(null);
// Loading while awaiting.
- const [loading, setLoading] = React.useState(false);
+ const [loading, setLoading] = React.useState(defaultLoading);
const sendRequest = React.useCallback((fetchFunction) => {
if (debounceTimerRef.current) {
diff --git a/wrolpi/root_api.py b/wrolpi/root_api.py
index 23d0b071..232a8170 100644
--- a/wrolpi/root_api.py
+++ b/wrolpi/root_api.py
@@ -509,25 +509,18 @@ async def post_shutdown(_: Request):
@api_bp.post('/search_suggestions')
@validate(json=schema.SearchSuggestionsRequest)
async def post_search_suggestions(_: Request, body: schema.SearchSuggestionsRequest):
- """Used by the Global search to suggest related Channels/Domains/etc. to the user.
-
- @note: Does not handle Tags, unlike search_estimates"""
+ """Used by the Global search to suggest related Channels/Domains/etc. to the user."""
from modules.videos.channel.lib import search_channels_by_name
from modules.archive.lib import search_domains_by_name
- from modules.zim import lib as zim_lib
file_groups, channels, domains = await asyncio.gather(
- estimate_search(body.search_str, []),
+ estimate_search(body.search_str, body.tag_names),
search_channels_by_name(body.search_str),
search_domains_by_name(body.search_str),
)
- # Get estimates using libzim.
- zims_estimates = zim_lib.get_estimates(body.search_str)
-
ret = dict(
file_groups=file_groups,
- zims_estimates=zims_estimates,
channels=channels,
domains=domains,
)
diff --git a/wrolpi/schema.py b/wrolpi/schema.py
index 99aab86f..e73b8711 100644
--- a/wrolpi/schema.py
+++ b/wrolpi/schema.py
@@ -140,9 +140,4 @@ class VINDecoderResponse:
@dataclass
class SearchSuggestionsRequest:
search_str: Optional[str] = None
-
-
-@dataclass
-class SearchEstimateRequest:
- search_str: Optional[str] = None
tag_names: List[str] = field(default_factory=lambda: list())
diff --git a/wrolpi/test/test_root_api.py b/wrolpi/test/test_root_api.py
index 90d0cb96..83e9ac39 100644
--- a/wrolpi/test/test_root_api.py
+++ b/wrolpi/test/test_root_api.py
@@ -468,15 +468,13 @@ async def test_post_vin_number_decoder(test_async_client):
@pytest.mark.asyncio
-async def test_search_suggestions(test_session, test_async_client, channel_factory, archive_factory, tag_factory,
- zim_factory):
+async def test_search_suggestions(test_session, test_async_client, channel_factory, archive_factory, tag_factory):
# WARNING results are cached, this test uses unique queries to avoid conflicts.
channel_factory(name='Foo')
channel_factory(name='Fool')
channel_factory(name='Bar')
archive_factory(domain='foo.com', contents='contents of foo')
archive_factory(domain='bar.com', contents='contents of bar')
- zim_factory('test zim')
test_session.commit()
body = dict(search_str='foo')
@@ -489,8 +487,6 @@ async def test_search_suggestions(test_session, test_async_client, channel_facto
'url': 'https://example.com/Fool'}]
assert response.json['domains'] == [{'directory': 'archive/foo.com', 'domain': 'foo.com', 'id': 1}]
assert response.json['file_groups'] == 1
- assert 'zims_estimates' in response.json and len(response.json['zims_estimates']) == 1
- assert response.json['zims_estimates'][0] == 0
body = dict(search_str='bar')
request, response = await test_async_client.post('/api/search_suggestions', json=body)
@@ -498,7 +494,6 @@ async def test_search_suggestions(test_session, test_async_client, channel_facto
assert response.json['channels'] == [{'directory': 'Bar', 'id': 3, 'name': 'Bar', 'url': 'https://example.com/Bar'}]
assert response.json['domains'] == [{'directory': 'archive/bar.com', 'domain': 'bar.com', 'id': 2}]
assert response.json['file_groups'] == 1
- assert response.json['zims_estimates'][0] == 0
body = dict(search_str='one')
request, response = await test_async_client.post('/api/search_suggestions', json=body)
@@ -506,7 +501,6 @@ async def test_search_suggestions(test_session, test_async_client, channel_facto
assert response.json['channels'] == []
assert response.json['domains'] == []
assert response.json['file_groups'] == 0
- assert response.json['zims_estimates'][0] == 2
def test_recursive_errors():