Add proof-of-concept search UI page
Some checks failed
continuous-integration/drone the build failed
Some checks failed
continuous-integration/drone the build failed
This commit is contained in:
parent
9c7dfb93f1
commit
d26b4271ce
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3435,6 +3435,7 @@ dependencies = [
|
||||
"env_logger",
|
||||
"itertools",
|
||||
"log",
|
||||
"quickpeep_index",
|
||||
"ron",
|
||||
"serde",
|
||||
"sqlx",
|
||||
|
@ -18,3 +18,5 @@ env_logger = "0.9.0"
|
||||
sqlx = { version = "0.5.11", features = ["sqlite", "runtime-tokio-rustls"] }
|
||||
itertools = "0.10.3"
|
||||
colour = "0.6.0"
|
||||
|
||||
quickpeep_index = { path = "../quickpeep_index" }
|
||||
|
@ -5,9 +5,12 @@ use axum::routing::{get, get_service, post};
|
||||
use axum::Router;
|
||||
use env_logger::Env;
|
||||
use quickpeep::config::WebConfig;
|
||||
use quickpeep::web::searcher::{search_root, search_search};
|
||||
use quickpeep::web::seed_collector::{seed_collection_root, seed_collection_root_post};
|
||||
use quickpeep::web::IndexAccess;
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
#[tokio::main]
|
||||
@ -27,9 +30,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
let file_bytes = std::fs::read(&config_path).context("Failed to read web config file")?;
|
||||
let web_config: WebConfig =
|
||||
ron::de::from_bytes(&file_bytes).context("Failed to parse web config")?;
|
||||
let web_config = WebConfig::load(&config_path)?;
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.min_connections(1)
|
||||
@ -58,11 +59,18 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
sqlx::migrate!().run(&pool).await?;
|
||||
|
||||
let backend = Arc::new(web_config.open_indexer_backend()?);
|
||||
|
||||
let index_access = IndexAccess { backend };
|
||||
|
||||
let app = Router::new()
|
||||
.route("/seeds/", get(seed_collection_root))
|
||||
.route("/seeds/", post(seed_collection_root_post))
|
||||
.route("/", get(search_root))
|
||||
.route("/search", get(search_search))
|
||||
.layer(Extension(web_config))
|
||||
.layer(Extension(pool))
|
||||
.layer(Extension(index_access))
|
||||
.nest(
|
||||
"/static",
|
||||
get_service(ServeDir::new("./quickpeep_static/dist")).handle_error(
|
||||
|
@ -1,6 +1,10 @@
|
||||
use crate::web::seed_collector::SeedTagColumn;
|
||||
use anyhow::Context;
|
||||
use quickpeep_index::backend::tantivy::TantivyBackend;
|
||||
use quickpeep_index::backend::Backend;
|
||||
use quickpeep_index::config::BackendConfig;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct SeedCollectionConfig {
|
||||
@ -15,4 +19,37 @@ pub struct SeedCollectionConfig {
|
||||
pub struct WebConfig {
|
||||
pub seed_collection: SeedCollectionConfig,
|
||||
pub sqlite_db_path: PathBuf,
|
||||
pub index: BackendConfig,
|
||||
}
|
||||
|
||||
impl WebConfig {
|
||||
/// Loads a config at the specified path.
|
||||
/// Will resolve all the paths in the IndexerConfig for you.
|
||||
pub fn load(path: &Path) -> anyhow::Result<WebConfig> {
|
||||
let config_dir = path.parent().context("Can't get parent of config file.")?;
|
||||
let file_bytes = std::fs::read(path).context("Failed to read web config file")?;
|
||||
let mut web_config: WebConfig =
|
||||
ron::de::from_bytes(&file_bytes).context("Failed to parse web config")?;
|
||||
|
||||
match &mut web_config.index {
|
||||
BackendConfig::Tantivy(tantivy) => {
|
||||
tantivy.index_dir = config_dir.join(&tantivy.index_dir);
|
||||
}
|
||||
BackendConfig::Meili(_) => {}
|
||||
}
|
||||
|
||||
Ok(web_config)
|
||||
}
|
||||
|
||||
pub fn open_indexer_backend(&self) -> anyhow::Result<Box<dyn Backend>> {
|
||||
// TODO deduplicate with the indexer crate
|
||||
match &self.index {
|
||||
BackendConfig::Tantivy(tantivy) => {
|
||||
Ok(Box::new(TantivyBackend::open(&tantivy.index_dir)?))
|
||||
}
|
||||
BackendConfig::Meili(_) => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,10 @@
|
||||
use quickpeep_index::backend::Backend;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod searcher;
|
||||
pub mod seed_collector;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexAccess {
|
||||
pub backend: Arc<Box<dyn Backend>>,
|
||||
}
|
||||
|
@ -1 +1,83 @@
|
||||
use crate::config::WebConfig;
|
||||
use crate::web::IndexAccess;
|
||||
use crate::webutil::{internal_error, TemplatedHtml};
|
||||
use askama::Template;
|
||||
use axum::extract::{Extension, Query};
|
||||
use axum::response::IntoResponse;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Template)]
|
||||
#[template(path = "search.html.askama")]
|
||||
pub struct SearchTemplate {
|
||||
pub search_term: String,
|
||||
pub results: Vec<SearchResult>,
|
||||
pub contact: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SearchResult {
|
||||
pub favicon_url: String,
|
||||
pub url: String,
|
||||
pub title: String,
|
||||
pub excerpt: Vec<ExcerptFragment>,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExcerptFragment {
|
||||
pub text: String,
|
||||
pub mark: bool,
|
||||
}
|
||||
|
||||
pub async fn search_root(Extension(web_config): Extension<WebConfig>) -> impl IntoResponse {
|
||||
let seed_config = &web_config.seed_collection;
|
||||
TemplatedHtml(SearchTemplate {
|
||||
search_term: String::with_capacity(0),
|
||||
results: vec![],
|
||||
contact: seed_config.contact.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct QueryParameters {
|
||||
/// The search query
|
||||
q: String,
|
||||
}
|
||||
|
||||
pub async fn search_search(
|
||||
web_config: Extension<WebConfig>,
|
||||
index_access: Extension<IndexAccess>,
|
||||
params: Query<QueryParameters>,
|
||||
) -> impl IntoResponse {
|
||||
search_search_inner(web_config, index_access, params)
|
||||
.await
|
||||
.map_err(internal_error)
|
||||
}
|
||||
|
||||
pub async fn search_search_inner(
|
||||
Extension(web_config): Extension<WebConfig>,
|
||||
Extension(index_access): Extension<IndexAccess>,
|
||||
Query(params): Query<QueryParameters>,
|
||||
) -> anyhow::Result<impl IntoResponse> {
|
||||
let seed_config = &web_config.seed_collection;
|
||||
|
||||
let raw_results = index_access.backend.query(params.q.clone())?;
|
||||
|
||||
let mut results = Vec::with_capacity(raw_results.len());
|
||||
|
||||
for search_doc in raw_results {
|
||||
results.push(SearchResult {
|
||||
favicon_url: "".to_string(),
|
||||
url: search_doc.url,
|
||||
title: search_doc.title,
|
||||
excerpt: vec![], // TODO
|
||||
tags: vec!["Software".to_owned(), "Blog".to_owned()], // TODO
|
||||
})
|
||||
}
|
||||
|
||||
Ok(TemplatedHtml(SearchTemplate {
|
||||
search_term: params.q.clone(),
|
||||
results,
|
||||
contact: seed_config.contact.clone(),
|
||||
}))
|
||||
}
|
||||
|
66
quickpeep/templates/search.html.askama
Normal file
66
quickpeep/templates/search.html.askama
Normal file
@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ search_term }} — QuickPeep</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Header -->
|
||||
<div class="container_overall">
|
||||
<div class="left_side_container">
|
||||
<header>
|
||||
<form method="GET" action="search">
|
||||
<fieldset class="horizontal">
|
||||
<img src="/static/quickpeep_logo_sml.png" class="bar_logo">
|
||||
<input type="search" id="search" name="q" placeholder="..." value="{{ search_term }}" class="grow">
|
||||
|
||||
<input type="submit" value="Search" class="shrink">
|
||||
</fieldset>
|
||||
</form>
|
||||
</header><!-- ./ Header -->
|
||||
|
||||
<!-- Main -->
|
||||
<main class="search">
|
||||
<ul class="search_results">
|
||||
{%- for result in results %}
|
||||
<li>
|
||||
<img src="{{ result.favicon_url }}">
|
||||
<div class="result_title"><a href="{{ result.url }}" rel="nofollow noreferrer">{{ result.title }}</a></div>
|
||||
<div class="result_excerpt">
|
||||
{%- for excerpt_chunk in result.excerpt -%}
|
||||
{%- if excerpt_chunk.mark %}
|
||||
<b>{{ excerpt_chunk.text }}</b>
|
||||
{%- else %}
|
||||
{{ excerpt_chunk.text }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
<ul class="result_tags">
|
||||
{%- for tag in result.tags -%}
|
||||
<li>{{ tag }}</li>
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
<div class="result_url">{{ result.url }}</div>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="right_side_container">
|
||||
<!-- Preview pane -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<footer class="container">
|
||||
{% for (method, url) in contact %}
|
||||
<a href="{{ url }}">{{ method }}</a> •
|
||||
{% endfor %}
|
||||
<a href="/">Return to QuickPeep Root</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -5,7 +5,7 @@ pub mod tantivy;
|
||||
|
||||
/// Trait representing a search index backend;
|
||||
/// either Tantivy (embedded) or Meilisearch (via HTTP API).
|
||||
pub trait Backend {
|
||||
pub trait Backend: Send + Sync {
|
||||
fn add_document(&mut self, document: BackendIndependentDocument) -> anyhow::Result<()>;
|
||||
|
||||
fn flush(&mut self) -> anyhow::Result<()>;
|
||||
|
BIN
quickpeep_static/assets/quickpeep_logo_sml.png
Normal file
BIN
quickpeep_static/assets/quickpeep_logo_sml.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
@ -4,7 +4,8 @@
|
||||
"license": "QuickPeep Licence",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "parcel build style/main.scss"
|
||||
"build": "parcel build style/main.scss && cp assets/*.png dist/",
|
||||
"watch": "parcel watch style/main.scss"
|
||||
},
|
||||
"dependencies": {
|
||||
"@picocss/pico": "^1.5.0"
|
||||
|
@ -15,3 +15,116 @@
|
||||
background-color: #11391F;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media only screen and (max-width: 960px) {
|
||||
.left_side_container {
|
||||
@extends(.container);
|
||||
}
|
||||
.right_side_container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 961px) {
|
||||
.container_overall {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.left_side_container {
|
||||
width: 66%;
|
||||
padding: 1em;
|
||||
}
|
||||
.right_side_container {
|
||||
width: 33%;
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fieldset.horizontal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.shrink {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
main.search {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.bar_logo {
|
||||
height: 60px;
|
||||
align-self: center;
|
||||
display: block;
|
||||
margin-bottom: var(--spacing);
|
||||
margin-right: var(--spacing);
|
||||
}
|
||||
|
||||
ul.search_results {
|
||||
> li {
|
||||
list-style-type: none;
|
||||
|
||||
margin-left: 32px;
|
||||
|
||||
|
||||
position: relative;
|
||||
> img {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -50px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
padding-bottom: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
.result_title a {
|
||||
color: palegoldenrod;
|
||||
}
|
||||
|
||||
.result_excerpt {
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.result_url {
|
||||
font-size: .7em;
|
||||
opacity: 0.7;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.result_tags {
|
||||
padding-left: 0;
|
||||
font-size: .7em;
|
||||
> li {
|
||||
list-style-type: none;
|
||||
display: inline-block;
|
||||
//background-color: palegreen;
|
||||
color: palegreen;
|
||||
//padding: 0.2em;
|
||||
//border-radius: 8px;
|
||||
|
||||
//text-decoration: underline;
|
||||
//text-decoration-style: double;
|
||||
//text-decoration-thickness: 5px;
|
||||
|
||||
border-bottom: 1px solid palegreen;
|
||||
opacity: .7;
|
||||
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user