Skip to content

Commit 54bd10a

Browse files
committed
add global (by user) filename search
1 parent 257aaa7 commit 54bd10a

File tree

7 files changed

+173
-8
lines changed

7 files changed

+173
-8
lines changed

src/packages/frontend/components/search-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface Props {
2525
on_change?: (value: string, opts: { ctrl_down: boolean }) => void;
2626
on_clear?: () => void;
2727
on_submit?: (value: string, opts: { ctrl_down: boolean }) => void;
28-
buttonAfter?: JSX.Element;
28+
buttonAfter?;
2929
disabled?: boolean;
3030
clear_on_submit?: boolean;
3131
on_down?: () => void;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
Search for any file you've edited in the last year.
3+
*/
4+
5+
import { useState } from "../app-framework";
6+
import { Input, Tooltip } from "antd";
7+
import api from "@cocalc/frontend/client/api";
8+
import ShowError from "@cocalc/frontend/components/error";
9+
import { TimeAgo } from "@cocalc/frontend/components/time-ago";
10+
import { PathLink } from "@cocalc/frontend/components/path-link";
11+
import { ProjectTitle } from "@cocalc/frontend/projects/project-title";
12+
import { MAX_FILENAME_SEARCH_RESULTS } from "@cocalc/util/db-schema/projects";
13+
14+
const { Search } = Input;
15+
16+
interface Props {}
17+
18+
export function FilenameSearch({}: Props) {
19+
const [search, setSearch] = useState<string>("");
20+
const [error, setError] = useState<string>("");
21+
const [loading, setLoading] = useState<boolean>(false);
22+
const [results, setResult] = useState<
23+
{ project_id: string; filename: string; time: Date }[] | null
24+
>(null);
25+
const [searched, setSearched] = useState<string>("");
26+
27+
const doSearch = async () => {
28+
try {
29+
setLoading(true);
30+
setResult(null);
31+
setSearched(search.trim());
32+
if (search.trim()) {
33+
setResult(
34+
await api("projects/filename-search", { search: search.trim() }),
35+
);
36+
}
37+
} catch (err) {
38+
setError(`${err}`);
39+
} finally {
40+
setLoading(false);
41+
}
42+
};
43+
44+
return (
45+
<div>
46+
<Tooltip
47+
title={`Search filenames of files you edited in the last year. Use % as wildcard. At most ${MAX_FILENAME_SEARCH_RESULTS} results shown.`}
48+
>
49+
<Search
50+
allowClear
51+
loading={loading}
52+
value={search}
53+
onChange={(e) => {
54+
const search = e.target.value;
55+
setSearch(search);
56+
if (!search.trim()) {
57+
setResult(null);
58+
}
59+
}}
60+
placeholder="Search for filenames you edited..."
61+
enterButton
62+
onSearch={doSearch}
63+
/>
64+
</Tooltip>
65+
{((results != null && searched == search.trim()) || error) && (
66+
<div
67+
style={{
68+
position: "absolute",
69+
zIndex: 1,
70+
background: "white",
71+
padding: "15px",
72+
border: "1px solid #ddd",
73+
boxShadow: "0 0 15px #aaa",
74+
overflow: "scroll",
75+
maxHeight: "70vh",
76+
left: "10px",
77+
right: "10px",
78+
}}
79+
>
80+
<ShowError error={error} setError={setError} />
81+
{results != null && results.length > 0 && (
82+
<div>
83+
{results.map(({ project_id, filename, time }) => (
84+
<div key={`${project_id}${filename}`}>
85+
<PathLink
86+
path={filename}
87+
project_id={project_id}
88+
trunc={20}
89+
/>{" "}
90+
in <ProjectTitle project_id={project_id} trunc={20} />{" "}
91+
<TimeAgo style={{ float: "right" }} date={time} />
92+
</div>
93+
))}
94+
</div>
95+
)}
96+
{results != null && results.length == 0 && <div>no results</div>}
97+
</div>
98+
)}
99+
</div>
100+
);
101+
}

src/packages/frontend/projects/projects-page.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import ProjectList from "./project-list";
2727
import { ProjectsListingDescription } from "./project-list-desc";
2828
import { ProjectsFilterButtons } from "./projects-filter-buttons";
2929
import { ProjectsSearch } from "./search";
30+
import { FilenameSearch } from "./filename-search";
3031
import ProjectsPageTour from "./tour";
3132
import { get_visible_hashtags, get_visible_projects } from "./util";
3233
import { COLORS } from "@cocalc/util/theme";
@@ -56,7 +57,7 @@ export const ProjectsPage: React.FC = () => {
5657

5758
const all_projects_have_been_loaded = useTypedRedux(
5859
"projects",
59-
"all_projects_have_been_loaded"
60+
"all_projects_have_been_loaded",
6061
);
6162
const hidden = !!useTypedRedux("projects", "hidden");
6263
const deleted = !!useTypedRedux("projects", "deleted");
@@ -68,7 +69,7 @@ export const ProjectsPage: React.FC = () => {
6869

6970
const selected_hashtags: Map<string, Set<string>> = useTypedRedux(
7071
"projects",
71-
"selected_hashtags"
72+
"selected_hashtags",
7273
);
7374

7475
const project_map = useTypedRedux("projects", "project_map");
@@ -82,18 +83,18 @@ export const ProjectsPage: React.FC = () => {
8283
search,
8384
deleted,
8485
hidden,
85-
"last_edited" /* "user_last_active" was confusing */
86+
"last_edited" /* "user_last_active" was confusing */,
8687
),
87-
[project_map, user_map, deleted, hidden, filter, selected_hashtags, search]
88+
[project_map, user_map, deleted, hidden, filter, selected_hashtags, search],
8889
);
8990
const all_projects: string[] = useMemo(
9091
() => project_map?.keySeq().toJS() ?? [],
91-
[project_map?.size]
92+
[project_map?.size],
9293
);
9394

9495
const visible_hashtags: string[] = useMemo(
9596
() => get_visible_hashtags(project_map, visible_projects),
96-
[visible_projects, project_map]
97+
[visible_projects, project_map],
9798
);
9899

99100
function clear_filters_and_focus_search_input(): void {
@@ -189,13 +190,18 @@ export const ProjectsPage: React.FC = () => {
189190
/>
190191
</div>
191192
</Col>
192-
<Col sm={8}>
193+
<Col sm={4}>
193194
<Hashtags
194195
hashtags={visible_hashtags}
195196
selected_hashtags={selected_hashtags?.get(filter)}
196197
toggle_hashtag={(tag) => actions.toggle_hashtag(filter, tag)}
197198
/>
198199
</Col>
200+
<Col sm={4}>
201+
<div ref={searchRef}>
202+
<FilenameSearch />
203+
</div>
204+
</Col>
199205
</Row>
200206
<Row>
201207
<Col sm={12} style={{ marginTop: "1ex" }}>

src/packages/frontend/projects/search.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const ProjectsSearch: React.FC<Props> = ({
5858
}}
5959
placeholder="Search for projects (use /re/ for regexp)..."
6060
on_submit={(_, opts) => on_submit?.(!opts.ctrl_down)}
61+
buttonAfter
6162
/>
6263
);
6364
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
API endpoint to find files that you've edited in the
3+
last year or so by their filename.
4+
It's under 'projects' since it's also a way to find the
5+
project you want to open.
6+
*/
7+
8+
import getAccountId from "lib/account/get-account";
9+
import getParams from "lib/api/get-params";
10+
import { filenameSearch } from "@cocalc/server/projects/filename-search";
11+
12+
export default async function handle(req, res) {
13+
const { search } = getParams(req);
14+
try {
15+
const account_id = await getAccountId(req);
16+
if (!account_id) {
17+
throw Error("must be signed in");
18+
}
19+
res.json(await filenameSearch({ search, account_id }));
20+
} catch (err) {
21+
res.json({ error: `${err.message}` });
22+
}
23+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Returns the most recent time the given user edited a file
2+
// whose name contains the search string.
3+
4+
import getPool from "@cocalc/database/pool";
5+
import { MAX_FILENAME_SEARCH_RESULTS } from "@cocalc/util/db-schema/projects";
6+
7+
export async function filenameSearch({
8+
search,
9+
account_id,
10+
}: {
11+
search: string;
12+
account_id: string;
13+
}): Promise<{ project_id: string; filename: string; time: Date }[]> {
14+
const pool = getPool("long");
15+
const { rows } = await pool.query(
16+
`
17+
SELECT project_id, filename, time
18+
FROM (
19+
SELECT project_id, filename, time,
20+
ROW_NUMBER() OVER(PARTITION BY filename ORDER BY time DESC) AS rn
21+
FROM file_access_log
22+
WHERE account_id = $1
23+
AND filename ILIKE '%' || $2 || '%'
24+
) tmp
25+
WHERE rn = 1
26+
ORDER BY time DESC
27+
LIMIT ${MAX_FILENAME_SEARCH_RESULTS};
28+
`,
29+
[account_id, search],
30+
);
31+
return rows;
32+
}

src/packages/util/db-schema/projects.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { Table } from "./types";
1111
import { State } from "../compute-states";
1212
import { NOTES } from "./crm";
1313

14+
export const MAX_FILENAME_SEARCH_RESULTS = 100;
15+
1416
Table({
1517
name: "projects",
1618
rules: {

0 commit comments

Comments
 (0)