diff --git a/app/src/DashboardPage.js b/app/src/DashboardPage.js
index 04ff4f24..a3ac7352 100644
--- a/app/src/DashboardPage.js
+++ b/app/src/DashboardPage.js
@@ -154,13 +154,22 @@ export function Getters() {
}
export function DashboardPage() {
-
const navigate = useNavigate();
const {searchStr, setSearchStr, activeTags} = useSearch();
- const {suggestionsResults, handleResultSelect, resultRenderer, loading} = useContext(SearchSuggestionsContext);
+ const {
+ suggestionsResults,
+ handleResultSelect,
+ resultRenderer,
+ loading,
+ setSearchStr: setSuggestionSearchStr
+ } = useContext(SearchSuggestionsContext);
const {status} = useContext(StatusContext);
+ React.useEffect(() => {
+ setSuggestionSearchStr(searchStr);
+ }, [searchStr])
+
let title = 'Dashboard';
if (searchStr) {
title = `Search: ${searchStr} - Dashboard`;
diff --git a/app/src/api.js b/app/src/api.js
index 3b4d9628..142b4c2c 100644
--- a/app/src/api.js
+++ b/app/src/api.js
@@ -1022,17 +1022,24 @@ export async function searchSuggestions(search_str) {
}
}
-export async function searchEstimate(search_str, tagNames) {
+export async function searchEstimateFiles(search_str, tagNames) {
const body = {search_str: search_str, tag_names: tagNames};
- const response = await apiPost(`${API_URI}/search_estimates`, body);
+ const response = await apiPost(`${API_URI}/files/search_estimates`, body);
if (response.ok) {
const content = await response.json();
-
- const fileGroups = content.file_groups;
- const zimsEstimates = content.zims_estimates;
return {
- fileGroups,
- zimsEstimates,
+ 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);
+ if (response.ok) {
+ const content = await response.json();
+ return {
+ zimsEstimates: content.zims_estimates,
}
}
}
diff --git a/app/src/components/Nav.js b/app/src/components/Nav.js
index 18bfaaab..2a0d409e 100644
--- a/app/src/components/Nav.js
+++ b/app/src/components/Nav.js
@@ -6,6 +6,7 @@ import Icon from "semantic-ui-react/dist/commonjs/elements/Icon";
import {CPUTemperatureIcon, DarkModeToggle, HotspotStatusIcon, NAME, SystemLoadIcon} from "./Common";
import {ShareButton} from "./Share";
import {useWROLMode} from "../hooks/customHooks";
+import {SearchIconButton} from "./Search";
const links = [
{text: 'Videos', to: '/videos', key: 'videos'},
@@ -76,6 +77,7 @@ export function NavBar() {
+
;
return <>
diff --git a/app/src/components/Search.js b/app/src/components/Search.js
index 4b33e81e..ebe24404 100644
--- a/app/src/components/Search.js
+++ b/app/src/components/Search.js
@@ -1,13 +1,15 @@
-import React, {useState} from "react";
+import React, {useContext, useState} from "react";
import {createSearchParams, Route, Routes, useNavigate} from "react-router-dom";
-import {FilesSearchView} from "./Files";
+import {FileSearchFilterButton, FilesSearchView} from "./Files";
import {useLatestRequest, usePages, useQuery} from "../hooks/customHooks";
import {ZimSearchView} from "./Zim";
-import {searchEstimate, searchSuggestions} from "../api";
-import {fuzzyMatch, normalizeEstimate, TabLinks} from "./Common";
+import {searchEstimateFiles, searchEstimateZims, searchSuggestions} from "../api";
+import {fuzzyMatch, normalizeEstimate, SearchResultsInput, TabLinks} from "./Common";
import _ from "lodash";
import {TagsContext} from "../Tags";
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: {},
@@ -16,6 +18,8 @@ export const SearchSuggestionsContext = React.createContext({
handleResultSelect: null,
resultRenderer: null,
loading: false,
+ searchStr: '',
+ setSearchStr: null,
});
@@ -113,10 +117,10 @@ export function useSuggestions(searchStr) {
}
-export function useSearchSuggestions() {
+export function useSearchSuggestions(defaultSearchStr) {
const navigate = useNavigate();
+ const [searchStr, setSearchStr] = React.useState(defaultSearchStr || '');
const {SingleTag, fuzzyMatchTagsByName} = React.useContext(TagsContext);
- const {searchStr} = useSearch();
const {suggestions, loading} = useSuggestions(searchStr);
// The results that will be displayed by .
@@ -226,43 +230,73 @@ export function useSearchSuggestions() {
return {title}
};
- return {suggestions, suggestionsResults, suggestionsSums, searchStr, handleResultSelect, resultRenderer, loading}
+ return {
+ suggestions,
+ suggestionsResults,
+ suggestionsSums,
+ searchStr,
+ setSearchStr,
+ handleResultSelect,
+ resultRenderer,
+ loading
+ }
}
export function useSearchEstimate(searchStr, tagNames) {
- const {data, sendRequest, loading} = useLatestRequest(250);
+ const {data: filesData, sendRequest: sendFilesRequest, loading: loadingFiles} = useLatestRequest(250);
+ const {data: zimsData, sendRequest: sendZimsRequest, loading: loadingZims} = useLatestRequest(250);
// The estimates from the response.
- const [estimates, setEstimates] = React.useState({});
+ const [estimates, setEstimates] = React.useState({
+ fileGroups: [],
+ zims: [],
+ });
// The estimates summarized.
- const [estimatesSums, setEstimatesSums] = React.useState({});
+ const [estimatesSums, setEstimatesSums] = React.useState({
+ fileGroups: 0,
+ zims: 0,
+ });
React.useEffect(() => {
// Handle results from `sendRequest`.
- if (_.isEmpty(data)) {
+ if (_.isEmpty(filesData)) {
return;
}
- setEstimates(data);
- setEstimatesSums({
- fileGroups: data.fileGroups,
- zims: data.zimsEstimates.reduce((i, j) => i + j['estimate'], 0),
- });
- }, [JSON.stringify(data)]);
+ setEstimates({...estimates, fileGroups: filesData.fileGroups});
+ setEstimatesSums({...estimatesSums, fileGroups: filesData.fileGroups});
+ }, [JSON.stringify(filesData)]);
React.useEffect(() => {
- // Use the `useLatestRequest` to handle user typing.
- sendRequest(async () => await searchEstimate(searchStr, tagNames));
- }, [searchStr, JSON.stringify(tagNames)], sendRequest);
+ sendFilesRequest(async () => await searchEstimateFiles(searchStr, tagNames));
+ }, [searchStr, JSON.stringify(tagNames)], sendFilesRequest);
- return {estimates, estimatesSums, loading}
+ 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,
+ }
}
export function SearchView() {
const {searchStr, activeTags} = useSearch();
- const {estimates, estimatesSums} = useSearchEstimate(searchStr, activeTags);
+ const {estimates, estimatesSums, loading} = useSearchEstimate(searchStr, activeTags);
let filesTabName = Files ;
let zimsTabName = Zims;
@@ -280,7 +314,54 @@ export function SearchView() {
}/>
- }/>
+ }/>
}
+
+export function SearchIconButton() {
+ // A single button which displays a modal for search suggestions.
+ const {
+ suggestionsResults,
+ handleResultSelect,
+ resultRenderer,
+ loading,
+ searchStr,
+ setSearchStr,
+ } = useContext(SearchSuggestionsContext);
+ const [open, setOpen] = React.useState(false);
+
+ const localHandleResultSelect = (i) => {
+ // Close modal when user selects a result.
+ setOpen(false);
+ handleResultSelect(i);
+ }
+
+ return
+
+}
diff --git a/app/src/components/Zim.js b/app/src/components/Zim.js
index ffca307b..00e00b92 100644
--- a/app/src/components/Zim.js
+++ b/app/src/components/Zim.js
@@ -5,6 +5,7 @@ import {
Divider,
Header,
Icon,
+ Loader,
Modal,
ModalActions,
ModalContent,
@@ -286,7 +287,7 @@ const ZimsRefreshWarning = () => {
;
}
-export const ZimSearchView = ({estimates}) => {
+export const ZimSearchView = ({estimates, loading}) => {
const [activeIndex, setActiveIndex] = React.useState(null);
const {searchStr, activeTags, setTags} = useSearch();
const {zimsEstimates} = estimates;
@@ -295,7 +296,7 @@ export const ZimSearchView = ({estimates}) => {
setActiveIndex(index === activeIndex_ ? -1 : index);
}
- let body = ;
+ let body;
if (!_.isEmpty(zimsEstimates)) {
body = zimsEstimates.map((i, index) => {
activeTags={activeTags}
onClick={handleClick}
/>);
+ } else if (loading) {
+ body = ;
+ } else if (_.isEmpty(zimsEstimates)) {
+ body = ;
}
return <>
diff --git a/modules/zim/api.py b/modules/zim/api.py
index fa0ba5b5..144a7c6e 100644
--- a/modules/zim/api.py
+++ b/modules/zim/api.py
@@ -12,6 +12,7 @@ from wrolpi.downloader import download_manager
from wrolpi.events import Events
from wrolpi.root_api import get_blueprint, json_response
from . import lib, schema
+from .models import Zims
bp = get_blueprint('Zim', '/api/zim')
@@ -158,3 +159,36 @@ async def delete_outdated_zims(_: Request):
lib.flag_outdated_zim_files()
Events.send_deleted(f'Deleted {deleted_count} outdated Zims')
return response.empty(HTTPStatus.NO_CONTENT)
+
+
+@bp.post('/search_estimates')
+@validate(json=schema.SearchEstimateRequest)
+async def post_search_estimates(_: Request, body: schema.SearchEstimateRequest):
+ """Get an estimated count of FileGroups/Zims which may or may not have been tagged."""
+
+ if not body.search_str and not body.tag_names:
+ return response.empty(HTTPStatus.BAD_REQUEST)
+
+ if body.tag_names:
+ # Get actual count of entries tagged with the tag names.
+ zims_estimates = list()
+ for zim, count in Zims.entries_with_tags(body.tag_names).items():
+ d = dict(
+ estimate=count,
+ **zim.__json__(),
+ )
+ zims_estimates.append(d)
+ else:
+ # Get estimates using libzim.
+ zims_estimates = list()
+ for zim, estimate in Zims.estimate(body.search_str).items():
+ d = dict(
+ estimate=estimate,
+ **zim.__json__(),
+ )
+ zims_estimates.append(d)
+
+ ret = dict(
+ zims_estimates=zims_estimates
+ )
+ return json_response(ret)
diff --git a/modules/zim/schema.py b/modules/zim/schema.py
index 9558bcfa..a6786450 100644
--- a/modules/zim/schema.py
+++ b/modules/zim/schema.py
@@ -93,3 +93,9 @@ class ZimSubscribeRequest:
class OutdatedZims:
outdated: List[str]
current: List[str]
+
+
+@dataclass
+class SearchEstimateRequest:
+ search_str: Optional[str] = None
+ tag_names: List[str] = field(default_factory=lambda: list())
diff --git a/modules/zim/test/test_api.py b/modules/zim/test/test_api.py
index 1ad70503..0f6cb47f 100644
--- a/modules/zim/test/test_api.py
+++ b/modules/zim/test/test_api.py
@@ -218,3 +218,15 @@ async def test_find_outdated_zim_files(test_session, test_directory, test_zim_by
assert wikipedia_two.is_file()
assert asdf.is_file()
assert empty.is_file()
+
+
+@pytest.mark.asyncio
+async def test_search_estimates(test_session, test_async_client, zim_factory):
+ zim_factory('test zim')
+ test_session.commit()
+
+ body = dict(search_str='one')
+ request, response = await test_async_client.post('/api/zim/search_estimates', json=body)
+ assert response.status_code == HTTPStatus.OK
+ assert len(response.json['zims_estimates']) == 1
+ assert response.json['zims_estimates'][0]['estimate'] == 2
diff --git a/wrolpi/files/api.py b/wrolpi/files/api.py
index adde7cf0..85c8dae2 100644
--- a/wrolpi/files/api.py
+++ b/wrolpi/files/api.py
@@ -372,3 +372,17 @@ async def post_upload(request: Request):
# Request the next chunk.
return json_response({'expected_chunk': expected_chunk_num}, HTTPStatus.OK)
+
+
+@bp.post('/search_estimates')
+@validate(json=schema.SearchEstimateRequest)
+async def post_search_estimates(_: Request, body: schema.SearchEstimateRequest):
+ """Get an estimated count of FileGroups which may or may not have been tagged."""
+ if not body.search_str and not body.tag_names:
+ return response.empty(HTTPStatus.BAD_REQUEST)
+
+ file_groups = await lib.estimate_search(body.search_str, body.tag_names)
+ ret = dict(
+ file_groups=file_groups,
+ )
+ return json_response(ret)
diff --git a/wrolpi/files/schema.py b/wrolpi/files/schema.py
index 8891e531..2612038a 100644
--- a/wrolpi/files/schema.py
+++ b/wrolpi/files/schema.py
@@ -71,3 +71,9 @@ class Move:
class Rename:
path: str
new_name: str
+
+
+@dataclass
+class SearchEstimateRequest:
+ search_str: Optional[str] = None
+ tag_names: List[str] = field(default_factory=lambda: list())
diff --git a/wrolpi/files/test/test_api.py b/wrolpi/files/test/test_api.py
index 8e79862d..923ae03e 100644
--- a/wrolpi/files/test/test_api.py
+++ b/wrolpi/files/test/test_api.py
@@ -585,3 +585,15 @@ async def test_delete_directory_recursive(test_session, test_directory, make_fil
empty = test_session.query(FileGroup).one()
assert empty.primary_path.name == 'empty'
+
+
+@pytest.mark.asyncio
+async def test_search_estimates(test_session, test_async_client, archive_factory):
+ archive_factory(domain='foo.com', contents='contents of foo')
+ archive_factory(domain='bar.com', contents='contents of bar')
+ test_session.commit()
+
+ body = dict(search_str='one')
+ request, response = await test_async_client.post('/api/files/search_estimates', json=body)
+ assert response.status_code == HTTPStatus.OK
+ assert response.json['file_groups'] == 0
diff --git a/wrolpi/root_api.py b/wrolpi/root_api.py
index 66d5be17..23d0b071 100644
--- a/wrolpi/root_api.py
+++ b/wrolpi/root_api.py
@@ -534,43 +534,6 @@ async def post_search_suggestions(_: Request, body: schema.SearchSuggestionsRequ
return json_response(ret)
-@api_bp.post('/search_estimates')
-@validate(json=schema.SearchEstimateRequest)
-async def post_search_estimates(_: Request, body: schema.SearchEstimateRequest):
- """Get an estimated count of FileGroups/Zims which may or may not have been tagged."""
- from modules.zim.models import Zims
-
- if not body.search_str and not body.tag_names:
- return response.empty(HTTPStatus.BAD_REQUEST)
-
- file_groups = await estimate_search(body.search_str, body.tag_names)
-
- if body.tag_names:
- # Get actual count of entries tagged with the tag names.
- zims_estimates = list()
- for zim, count in Zims.entries_with_tags(body.tag_names).items():
- d = dict(
- estimate=count,
- **zim.__json__(),
- )
- zims_estimates.append(d)
- else:
- # Get estimates using libzim.
- zims_estimates = list()
- for zim, estimate in Zims.estimate(body.search_str).items():
- d = dict(
- estimate=estimate,
- **zim.__json__(),
- )
- zims_estimates.append(d)
-
- ret = dict(
- file_groups=file_groups,
- zims_estimates=zims_estimates
- )
- return json_response(ret)
-
-
class CustomJSONEncoder(json.JSONEncoder):
def default(self, obj):
diff --git a/wrolpi/test/test_root_api.py b/wrolpi/test/test_root_api.py
index 2abb484b..90d0cb96 100644
--- a/wrolpi/test/test_root_api.py
+++ b/wrolpi/test/test_root_api.py
@@ -509,21 +509,6 @@ async def test_search_suggestions(test_session, test_async_client, channel_facto
assert response.json['zims_estimates'][0] == 2
-@pytest.mark.asyncio
-async def test_search_estimates(test_session, test_async_client, archive_factory, zim_factory):
- 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='one')
- request, response = await test_async_client.post('/api/search_estimates', json=body)
- assert response.status_code == HTTPStatus.OK
- assert response.json['file_groups'] == 0
- assert len(response.json['zims_estimates']) == 1
- assert response.json['zims_estimates'][0]['estimate'] == 2
-
-
def test_recursive_errors():
"""Errors are reported recursively."""