mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Adding search icon button to navbar.
This commit is contained in:
parent
81419b8353
commit
377c69e8a7
13 changed files with 222 additions and 86 deletions
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<NavIconWrapper><ShareButton/></NavIconWrapper>
|
||||
<NavIconWrapper><HotspotStatusIcon/></NavIconWrapper>
|
||||
<NavIconWrapper><DarkModeToggle/></NavIconWrapper>
|
||||
<span style={{marginTop: '0.5em'}}><SearchIconButton/></span>
|
||||
</React.Fragment>;
|
||||
|
||||
return <>
|
||||
|
|
|
|||
|
|
@ -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 <Search>.
|
||||
|
|
@ -226,43 +230,73 @@ export function useSearchSuggestions() {
|
|||
return <span>{title}</span>
|
||||
};
|
||||
|
||||
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 = <span>Files <Label>?</Label></span>;
|
||||
let zimsTabName = <span>Zims<Label>?</Label></span>;
|
||||
|
|
@ -280,7 +314,54 @@ export function SearchView() {
|
|||
<TabLinks links={links}/>
|
||||
<Routes>
|
||||
<Route path='/*' element={<FilesSearchView/>}/>
|
||||
<Route path='/zim' exact element={<ZimSearchView estimates={estimates}/>}/>
|
||||
<Route path='/zim' exact element={<ZimSearchView estimates={estimates} loading={loading}/>}/>
|
||||
</Routes>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
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 <React.Fragment>
|
||||
<Button icon='search' onClick={() => setOpen(!open)}/>
|
||||
<Modal open={open} onClose={() => setOpen(false)} centered={false}>
|
||||
<ModalContent>
|
||||
<Grid columns={2}>
|
||||
<Grid.Row>
|
||||
<Grid.Column mobile={13} computer={14}>
|
||||
<SearchResultsInput clearable
|
||||
searchStr={searchStr}
|
||||
onChange={setSearchStr}
|
||||
onSubmit={setSearchStr}
|
||||
size='large'
|
||||
placeholder='Search everywhere...'
|
||||
results={suggestionsResults}
|
||||
handleResultSelect={localHandleResultSelect}
|
||||
resultRenderer={resultRenderer}
|
||||
loading={loading}
|
||||
/>
|
||||
</Grid.Column>
|
||||
<Grid.Column mobile={3} computer={2}>
|
||||
<FileSearchFilterButton/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
Divider,
|
||||
Header,
|
||||
Icon,
|
||||
Loader,
|
||||
Modal,
|
||||
ModalActions,
|
||||
ModalContent,
|
||||
|
|
@ -286,7 +287,7 @@ const ZimsRefreshWarning = () => {
|
|||
</Message>;
|
||||
}
|
||||
|
||||
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 = <ZimsRefreshWarning/>;
|
||||
let body;
|
||||
if (!_.isEmpty(zimsEstimates)) {
|
||||
body = zimsEstimates.map((i, index) => <ZimAccordion
|
||||
key={i['path']}
|
||||
|
|
@ -306,6 +307,10 @@ export const ZimSearchView = ({estimates}) => {
|
|||
activeTags={activeTags}
|
||||
onClick={handleClick}
|
||||
/>);
|
||||
} else if (loading) {
|
||||
body = <Loader active={true}/>;
|
||||
} else if (_.isEmpty(zimsEstimates)) {
|
||||
body = <ZimsRefreshWarning/>;
|
||||
}
|
||||
|
||||
return <>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue