From d13bc75c1e9cbf02bec42751580431bdee57fbc7 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 09 Oct 2021 14:57:28 +0100 Subject: [PATCH] Implement search results for crates --- chartered-db/src/crates.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ chartered-frontend/src/index.tsx | 6 +----- chartered-frontend/src/pages/Search.tsx | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- chartered-frontend/src/sections/Nav.tsx | 4 ++-- chartered-frontend/src/pages/crate/Members.tsx | 6 +++++- chartered-web/src/endpoints/web_api/crates/mod.rs | 4 +++- chartered-web/src/endpoints/web_api/crates/search.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 311 insertions(+), 54 deletions(-) diff --git a/chartered-db/src/crates.rs b/chartered-db/src/crates.rs index 5840386..ce553b6 100644 --- a/chartered-db/src/crates.rs +++ a/chartered-db/src/crates.rs @@ -92,6 +92,50 @@ } impl Crate { + pub async fn search( + conn: ConnectionPool, + requesting_user_id: i32, + terms: String, + limit: i64, + ) -> Result>> { + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + let crates = crate_with_permissions!(requesting_user_id) + .inner_join(organisations::table) + .filter( + select_permissions!() + .bitwise_and(UserPermission::VISIBLE.bits()) + .eq(UserPermission::VISIBLE.bits()), + ) + .filter( + (organisations::name.concat("/").concat(crates::name)) + .like(&format!("%{}%", terms)), + ) + .select(( + organisations::all_columns, + crates::all_columns, + select_permissions!(), + )) + .limit(limit) + .load(&conn)? + .into_iter() + .map(|(organisation, crate_, permissions)| { + ( + organisation, + CrateWithPermissions { + crate_, + permissions, + }, + ) + }) + .into_group_map(); + + Ok(crates) + }) + .await? + } + pub async fn list_with_versions( conn: ConnectionPool, requesting_user_id: i32, diff --git a/chartered-frontend/src/index.tsx b/chartered-frontend/src/index.tsx index 4635203..4309469 100644 --- a/chartered-frontend/src/index.tsx +++ a/chartered-frontend/src/index.tsx @@ -94,11 +94,7 @@ path="/organisations/create" component={() => } /> - } - /> + } /> diff --git a/chartered-frontend/src/pages/Search.tsx b/chartered-frontend/src/pages/Search.tsx index f7048eb..b1106b9 100644 --- a/chartered-frontend/src/pages/Search.tsx +++ a/chartered-frontend/src/pages/Search.tsx @@ -1,31 +1,27 @@ import React = require("react"); import { useState, useEffect } from "react"; import { Link, useHistory, useLocation } from "react-router-dom"; import Nav from "../sections/Nav"; import { useAuth } from "../useAuth"; -import { authenticatedEndpoint, ProfilePicture, useAuthenticatedRequest } from "../util"; +import { + authenticatedEndpoint, + ProfilePicture, + useAuthenticatedRequest, +} from "../util"; -import { Plus } from "react-bootstrap-icons"; +import { BoxSeam, Plus } from "react-bootstrap-icons"; import { LoadingSpinner } from "./Loading"; +import { result } from "lodash"; -interface UsersSearchResponse { - users: UserSearchResponseUser[]; -} - -interface UserSearchResponseUser { - user_uuid: string; - display_name: string; - picture_url: string; -} - export default function Search() { const auth = useAuth(); const location = useLocation(); - const query = location.pathname === '/search' - ? new URLSearchParams(location.search).get("q") || "" - : ""; + const query = + location.pathname === "/search" + ? new URLSearchParams(location.search).get("q") || "" + : ""; return (
@@ -35,41 +31,182 @@

Search Results {query ? <>for '{query}' : <>}

+
); +} + +interface UsersSearchResponse { + users: UserSearchResponseUser[]; +} + +interface UserSearchResponseUser { + user_uuid: string; + display_name: string; + picture_url: string; } function UsersResults({ query }: { query: string }) { - const auth = useAuth(); - - const { response: results, error } = - useAuthenticatedRequest({ - auth, - endpoint: "users/search?q=" + encodeURIComponent(query), - }, [query]); - - if (!results) { - return
-
- {[0, 1, 2].map((i) => ( - - ))} -
+ const auth = useAuth(); + + const { response: results, error } = + useAuthenticatedRequest( + { + auth, + endpoint: "users/search?q=" + encodeURIComponent(query), + }, + [query] + ); + + if (!results) { + return ( +
+
+ {[0, 1, 2].map((i) => ( + + ))}
- } - - if (results?.users.length === 0) { - return <>; - } - - return
-
- {results.users.map((user, i) => ( - - - - ))} +
+ ); + } + + if (results?.users.length === 0) { + return <>; + } + + return ( +
+
+ {results.users.map((user, i) => ( + + + + ))} +
+
+ ); +} + +interface CrateSearchResponse { + crates: CrateSearchResponseCrate[]; +} + +interface CrateSearchResponseCrate { + organisation: string; + name: string; + description: string; + homepage?: string; + repository?: string; +} + +function CrateResults({ + query, + className, +}: { + query: string; + className?: string; +}) { + const auth = useAuth(); + + const { response: results, error } = + useAuthenticatedRequest( + { + auth, + endpoint: "crates/search?q=" + encodeURIComponent(query), + }, + [query] + ); + + if (!results) { + return ( +
+
+
-
; -}+
+ ); + } + + if (results?.crates.length === 0) { + return <>; + } + + return ( +
+
+ + + {results?.crates.map((crate, i) => ( + + + + ))} + +
+
+
+ +
+ +

+ + {crate.organisation} + + /{crate.name} +

+ +
0.1.2
+
+ +

{crate.description}

+ + {crate.homepage || crate.repository ? ( +
+ {crate.homepage ? ( + + Homepage + + ) : ( + <> + )} + {crate.repository ? ( + + Repository + + ) : ( + <> + )} +
+ ) : ( + <> + )} +
+
+
+ ); +} diff --git a/chartered-frontend/src/sections/Nav.tsx b/chartered-frontend/src/sections/Nav.tsx index b795532..772f065 100644 --- a/chartered-frontend/src/sections/Nav.tsx +++ a/chartered-frontend/src/sections/Nav.tsx @@ -16,7 +16,7 @@ }; const [search, setSearch] = React.useState( - location.pathname === '/search' + location.pathname === "/search" ? new URLSearchParams(location.search).get("q") || "" : "" ); @@ -24,7 +24,7 @@ e.preventDefault(); if (search != "") { - history.push(`/search?q=${encodeURIComponent(search)}`) + history.push(`/search?q=${encodeURIComponent(search)}`); } }; diff --git a/chartered-frontend/src/pages/crate/Members.tsx b/chartered-frontend/src/pages/crate/Members.tsx index 4e2463e..b44eb75 100644 --- a/chartered-frontend/src/pages/crate/Members.tsx +++ a/chartered-frontend/src/pages/crate/Members.tsx @@ -292,7 +292,11 @@ }; const handleChange = (selected) => { - onInsert(selected[0].display_name, selected[0].picture_url, selected[0].user_uuid); + onInsert( + selected[0].display_name, + selected[0].picture_url, + selected[0].user_uuid + ); searchRef.current.clear(); }; diff --git a/chartered-web/src/endpoints/web_api/crates/mod.rs b/chartered-web/src/endpoints/web_api/crates/mod.rs index 5f6b636..28f6bd4 100644 --- a/chartered-web/src/endpoints/web_api/crates/mod.rs +++ a/chartered-web/src/endpoints/web_api/crates/mod.rs @@ -1,6 +1,7 @@ mod info; mod members; mod recently_updated; +mod search; use axum::{ body::{Body, BoxBody}, @@ -26,5 +27,6 @@ .route("/:org/:crate/members", patch(members::handle_patch)) .route("/:org/:crate/members", put(members::handle_put)) .route("/:org/:crate/members", delete(members::handle_delete)) - .route("/recently-updated", get(recently_updated::handle))) + .route("/recently-updated", get(recently_updated::handle)) + .route("/search", get(search::handle))) } diff --git a/chartered-web/src/endpoints/web_api/crates/search.rs b/chartered-web/src/endpoints/web_api/crates/search.rs new file mode 100644 index 0000000..f8a2dd4 100644 --- /dev/null +++ a/chartered-web/src/endpoints/web_api/crates/search.rs @@ -1,0 +1,74 @@ +use axum::{extract, Json}; +use chartered_db::{crates::Crate, permissions::UserPermission, users::User, ConnectionPool}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use thiserror::Error; + +#[derive(Deserialize)] +pub struct RequestParams { + q: String, +} + +#[derive(Serialize)] +pub struct Response { + crates: Vec, +} + +#[derive(Serialize)] +pub struct ResponseCrate { + organisation: String, + name: String, + description: Option, + version: String, + homepage: Option, + repository: Option, + updated: DateTime, + permissions: UserPermission, +} + +pub async fn handle( + extract::Extension(db): extract::Extension, + extract::Extension(user): extract::Extension>, + extract::Query(req): extract::Query, +) -> Result, Error> { + let crates = Crate::search(db, user.id, req.q, 5) + .await? + .into_iter() + .map(|(org, crates_with_permissions)| { + crates_with_permissions + .into_iter() + .map(move |v| ResponseCrate { + organisation: org.name.to_string(), + name: v.crate_.name, + description: v.crate_.description, + version: "test".to_string(), + homepage: v.crate_.homepage, + repository: v.crate_.repository, + updated: Utc::now(), // todo + permissions: v.permissions, + }) + }) + .flatten() + .collect(); + + Ok(Json(Response { crates })) +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + use axum::http::StatusCode; + + match self { + Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +define_error_response!(Error); -- rgit 0.1.3