Skip to content

Commit 685429f

Browse files
committed
(WIP) search feature
1 parent 59f9096 commit 685429f

6 files changed

Lines changed: 217 additions & 2 deletions

File tree

frontend/layout/appdefaultlayout.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { Breadcrumb } from 'f61ui/component/breadcrumbtrail';
44
import { NavLink } from 'f61ui/component/navigation';
55
import { globalConfig } from 'f61ui/globalconfig';
66
import { DefaultLayout } from 'f61ui/layout/defaultlayout';
7-
import { browseUrl, gettingStartedUrl, serverInfoUrl } from 'generated/frontend_uiroutes';
7+
import {
8+
browseUrl,
9+
gettingStartedUrl,
10+
searchUrl,
11+
serverInfoUrl,
12+
} from 'generated/frontend_uiroutes';
813
import { RootFolderId } from 'generated/stoserver/stoservertypes_types';
914
import { version } from 'generated/version';
1015
import * as React from 'react';
@@ -32,6 +37,7 @@ export class AppDefaultLayout extends React.Component<AppDefaultLayoutProps, {}>
3237

3338
const navLinks: NavLink[] = [
3439
mkLink('Browse', 'folder-open', browseUrl({ dir: RootFolderId })),
40+
mkLink('Search', 'search', searchUrl({ q: '' })),
3541
mkLink('Help', 'book', gettingStartedUrl({ section: 'welcome' })),
3642
mkLink('Admin', 'cog', serverInfoUrl()),
3743
];

frontend/main.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import NodesPage from 'pages/NodesPage';
1616
import ReplicationPoliciesPage from 'pages/ReplicationPoliciesPage';
1717
import RootRedirectPage from 'pages/RootRedirectPage';
1818
import ScheduledJobsPage from 'pages/ScheduledJobsPage';
19+
import SearchPage from 'pages/SearchPage';
1920
import ServerInfoPage from 'pages/ServerInfoPage';
2021
import SubsystemsPage from 'pages/SubsystemsPage';
2122
import UsersPage from 'pages/UsersPage';
@@ -64,6 +65,10 @@ class Handlers implements r.RouteHandlers {
6465
return <UsersPage />;
6566
}
6667

68+
search(opts: r.SearchOpts) {
69+
return <SearchPage query={opts.q} />;
70+
}
71+
6772
serverInfo() {
6873
return <ServerInfoPage />;
6974
}

frontend/pages/SearchPage.tsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Glyphicon, Panel, tableClassStripedHover } from 'f61ui/component/bootstrap';
2+
import { Result } from 'f61ui/component/result';
3+
import { browseUrl, collectionUrl, searchUrl } from 'generated/frontend_uiroutes';
4+
import { search } from 'generated/stoserver/stoservertypes_endpoints';
5+
import { SearchResult } from 'generated/stoserver/stoservertypes_types';
6+
import { AppDefaultLayout } from 'layout/appdefaultlayout';
7+
import * as React from 'react';
8+
9+
interface SearchPageProps {
10+
query: string;
11+
}
12+
13+
interface SearchPageState {
14+
queryTransient: string;
15+
results: Result<SearchResult[]>;
16+
}
17+
18+
export default class SearchPage extends React.Component<SearchPageProps, SearchPageState> {
19+
state: SearchPageState = {
20+
queryTransient: this.props.query,
21+
results: new Result<SearchResult[]>((_) => {
22+
this.setState({ results: _ });
23+
}),
24+
};
25+
26+
componentDidMount() {
27+
this.fetchData();
28+
}
29+
30+
componentWillReceiveProps() {
31+
this.fetchData();
32+
}
33+
34+
render() {
35+
return (
36+
<AppDefaultLayout title="Search" breadcrumbs={[]}>
37+
<Panel heading="Search">{this.renderData()}</Panel>
38+
</AppDefaultLayout>
39+
);
40+
}
41+
42+
private renderData() {
43+
const [results, loadingOrError] = this.state.results.unwrap();
44+
45+
return (
46+
<div>
47+
<form action={searchUrl({ q: '' })} method="get">
48+
<div className="input-group">
49+
<input
50+
className="form-control"
51+
name="q"
52+
value={this.state.queryTransient}
53+
onChange={(e) => {
54+
this.setState({ queryTransient: e.target.value });
55+
}}
56+
autoFocus={true}
57+
/>
58+
<span className="input-group-btn">
59+
<button className="btn btn-default" type="submit">
60+
🔍
61+
</button>
62+
</span>
63+
</div>
64+
</form>
65+
66+
<table className={tableClassStripedHover}>
67+
<thead>
68+
<tr>
69+
<th>Kind</th>
70+
<th>Result</th>
71+
</tr>
72+
</thead>
73+
<tbody>
74+
{(results || []).map((result: SearchResult) => {
75+
const kindIndicator = ((): [string, JSX.Element, JSX.Element] => {
76+
if (result.Collection) {
77+
return [
78+
`coll-${result.Collection.Id}`,
79+
<Glyphicon icon="duplicate" />,
80+
<a href={collectionUrl({ id: result.Collection.Id })}>
81+
{result.Collection.Name}
82+
</a>,
83+
];
84+
}
85+
if (result.Directory) {
86+
return [
87+
`dir-${result.Directory.Directory.Directory.Id}`,
88+
<Glyphicon icon="folder-open" />,
89+
<a
90+
href={browseUrl({
91+
dir: result.Directory.Directory.Directory.Id,
92+
})}>
93+
{result.Directory.Directory.Directory.Name}
94+
</a>,
95+
];
96+
}
97+
throw new Error('should not happen');
98+
})();
99+
100+
return (
101+
<tr id={kindIndicator[0]}>
102+
<td>{kindIndicator[1]}</td>
103+
<td>{kindIndicator[2]}</td>
104+
</tr>
105+
);
106+
})}
107+
</tbody>
108+
<tfoot>{loadingOrError}</tfoot>
109+
</table>
110+
</div>
111+
);
112+
}
113+
114+
private fetchData() {
115+
this.state.results.load(() => search(this.props.query));
116+
}
117+
}

pkg/frontend/ui-routes.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
{"key": "view", "type": {"_": "string", "nullable": true}}
1111
]
1212
},
13+
{
14+
"id": "search",
15+
"path": "/search",
16+
"query_params": [
17+
{"key": "q", "type": {"_": "string"}}
18+
]
19+
},
1320
{
1421
"id": "collection",
1522
"path": "/coll/{id}",
@@ -106,4 +113,4 @@
106113
"id": "scheduledJobs",
107114
"path": "/admin/scheduled_jobs"
108115
}
109-
]
116+
]

pkg/stoserver/restapi.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,6 +1463,77 @@ func (h *handlers) GenerateIds(rctx *httpauth.RequestContext, w http.ResponseWri
14631463
}
14641464
}
14651465

1466+
func (h *handlers) Search(rctx *httpauth.RequestContext, w http.ResponseWriter, r *http.Request) *[]stoservertypes.SearchResult {
1467+
//nolint:unparam // false positive for this pattern
1468+
withErr := func(err error) *[]stoservertypes.SearchResult {
1469+
http.Error(w, err.Error(), http.StatusInternalServerError)
1470+
return nil
1471+
}
1472+
1473+
tx, err := h.db.Begin(false)
1474+
if err != nil {
1475+
return withErr(err)
1476+
}
1477+
defer func() { ignoreError(tx.Rollback()) }()
1478+
1479+
results := []stoservertypes.SearchResult{}
1480+
1481+
queryLowercased := strings.ToLower(r.URL.Query().Get("q"))
1482+
1483+
if queryLowercased == "" {
1484+
return &results
1485+
}
1486+
1487+
if err := stodb.DirectoryRepository.Each(func(record any) error {
1488+
dir := record.(*stotypes.Directory)
1489+
1490+
queryMatches := strings.Contains(strings.ToLower(dir.Name), queryLowercased)
1491+
if queryMatches {
1492+
results = append(results, stoservertypes.SearchResult{
1493+
BreadcrumbItems: []string{},
1494+
Directory: &stoservertypes.DirectoryOutput{
1495+
// Collections: []stoservertypes.CollectionSubsetWithMeta{},
1496+
Directory: stoservertypes.DirectoryAndMeta{
1497+
Directory: convertDir(*dir),
1498+
// MetaCollection: &stoservertypes.CollectionSubsetWithMeta{},
1499+
},
1500+
// Parents: []stoservertypes.DirectoryAndMeta{},
1501+
// SubDirectories: []stoservertypes.DirectoryAndMeta{},
1502+
},
1503+
})
1504+
}
1505+
1506+
return nil
1507+
}, tx); err != nil {
1508+
return withErr(err)
1509+
}
1510+
1511+
if err := stodb.CollectionRepository.Each(func(record any) error {
1512+
coll := record.(*stotypes.Collection)
1513+
1514+
queryMatches := strings.Contains(strings.ToLower(coll.Name), queryLowercased)
1515+
if queryMatches {
1516+
1517+
collState, err := stateresolver.ComputeStateAtHead(*coll)
1518+
if err != nil {
1519+
return err
1520+
}
1521+
converted := convertDBCollection(*coll, nil, collState)
1522+
1523+
results = append(results, stoservertypes.SearchResult{
1524+
BreadcrumbItems: []string{},
1525+
Collection: &converted.Collection,
1526+
})
1527+
}
1528+
1529+
return nil
1530+
}, tx); err != nil {
1531+
return withErr(err)
1532+
}
1533+
1534+
return &results
1535+
}
1536+
14661537
func (h *handlers) whichInitialVolumeToWriteCollectionBlobsTo(collectionID string) (int, error) {
14671538
var volumeID int
14681539
return volumeID, h.db.View(func(tx *bbolt.Tx) error {

pkg/stoserver/stoservertypes/types.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
{ "chain": "public", "method": "GET", "path": "/api_v2/health", "produces": {"_": "Health"}, "name": "getHealth" },
3232
{ "chain": "public", "method": "GET", "path": "/api_v2/config/{id}", "produces": {"_": "ConfigValue"}, "name": "getConfig" },
3333
{ "chain": "public", "method": "GET", "path": "/api_v2/logs", "produces": {"_": "list", "of": {"_": "string"}}, "name": "getLogs" },
34+
{ "chain": "public", "method": "GET", "path": "/api_v2/search?q={query}", "produces": {"_": "list", "of": {"_": "SearchResult"}}, "name": "search" },
3435
{ "chain": "public", "method": "GET", "path": "/api_v2/external/tmdb/movies?q={query}", "produces": {"_": "list", "of": {"_": "TmdbSearchResult"}}, "name": "searchTmdbMovies" },
3536
{ "chain": "public", "method": "GET", "path": "/api_v2/external/tmdb/tv?q={query}", "produces": {"_": "list", "of": {"_": "TmdbSearchResult"}}, "name": "searchTmdbTv" },
3637
{ "chain": "public", "method": "GET", "path": "/api_v2/external/tmdb/credits?collection={collection}", "produces": {"_": "list", "of": {"_": "TmdbCredit"}}, "name": "tmdbCredits" },
@@ -456,6 +457,14 @@
456457
"ServerArch": {"_": "string"}
457458
}}
458459
},
460+
{
461+
"name": "SearchResult",
462+
"type": {"_": "object", "fields": {
463+
"BreadcrumbItems": {"_": "list", "of": {"_": "string"}},
464+
"Directory": {"_": "DirectoryOutput", "nullable": true},
465+
"Collection": {"_": "CollectionSubset", "nullable": true}
466+
}}
467+
},
459468
{
460469
"name": "GeneratedIds",
461470
"type": {"_": "object", "fields": {

0 commit comments

Comments
 (0)