mirror of
https://github.com/Nutomic/ibis.git
synced 2025-01-23 21:05:48 +00:00
Add instance name, topic and instance card with update time (fixes #106)
This commit is contained in:
parent
218c621afb
commit
cdcc992b75
22 changed files with 307 additions and 65 deletions
2
migrations/2025-01-23-112938_instance-topic/down.sql
Normal file
2
migrations/2025-01-23-112938_instance-topic/down.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
alter table instance rename topic to description;
|
||||
alter table instance drop column name;
|
2
migrations/2025-01-23-112938_instance-topic/up.sql
Normal file
2
migrations/2025-01-23-112938_instance-topic/up.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
alter table instance rename description to topic;
|
||||
alter table instance add column name text;
|
|
@ -1,11 +1,18 @@
|
|||
use super::empty_to_none;
|
||||
use crate::{
|
||||
backend::{
|
||||
database::IbisContext,
|
||||
database::{instance::DbInstanceUpdateForm, IbisContext},
|
||||
federation::activities::follow::Follow,
|
||||
utils::error::MyResult,
|
||||
},
|
||||
common::{
|
||||
instance::{DbInstance, FollowInstanceParams, GetInstanceParams, InstanceView},
|
||||
instance::{
|
||||
DbInstance,
|
||||
FollowInstanceParams,
|
||||
GetInstanceParams,
|
||||
InstanceView,
|
||||
UpdateInstanceParams,
|
||||
},
|
||||
user::LocalUserView,
|
||||
ResolveObjectParams,
|
||||
SuccessResponse,
|
||||
|
@ -25,6 +32,19 @@ pub(in crate::backend::api) async fn get_instance(
|
|||
Ok(Json(local_instance))
|
||||
}
|
||||
|
||||
pub(in crate::backend::api) async fn update_instance(
|
||||
context: Data<IbisContext>,
|
||||
Form(mut params): Form<UpdateInstanceParams>,
|
||||
) -> MyResult<Json<DbInstance>> {
|
||||
empty_to_none(&mut params.name);
|
||||
empty_to_none(&mut params.topic);
|
||||
let form = DbInstanceUpdateForm {
|
||||
name: params.name,
|
||||
topic: params.topic,
|
||||
};
|
||||
Ok(Json(DbInstance::update(form, &context)?))
|
||||
}
|
||||
|
||||
/// Make the local instance follow a given remote instance, to receive activities about new and
|
||||
/// updated articles.
|
||||
#[debug_handler]
|
||||
|
@ -53,9 +73,9 @@ pub(super) async fn resolve_instance(
|
|||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub(in crate::backend::api) async fn list_remote_instances(
|
||||
pub(in crate::backend::api) async fn list_instances(
|
||||
context: Data<IbisContext>,
|
||||
) -> MyResult<Json<Vec<DbInstance>>> {
|
||||
let instances = DbInstance::read_remote(&context)?;
|
||||
let instances = DbInstance::list(false, &context)?;
|
||||
Ok(Json(instances))
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ use axum::{
|
|||
Router,
|
||||
};
|
||||
use axum_macros::debug_handler;
|
||||
use instance::list_remote_instances;
|
||||
use instance::{list_instances, update_instance};
|
||||
use user::{count_notifications, list_notifications, update_user_profile};
|
||||
|
||||
mod article;
|
||||
|
@ -60,9 +60,10 @@ pub fn api_routes() -> Router<()> {
|
|||
.route("/comment", post(create_comment))
|
||||
.route("/comment", patch(edit_comment))
|
||||
.route("/instance", get(get_instance))
|
||||
.route("/instance", patch(update_instance))
|
||||
.route("/instance/follow", post(follow_instance))
|
||||
.route("/instance/resolve", get(resolve_instance))
|
||||
.route("/instance/list", get(list_remote_instances))
|
||||
.route("/instance/list", get(list_instances))
|
||||
.route("/search", get(search_article))
|
||||
.route("/user", get(get_user))
|
||||
.route("/user/notifications/list", get(list_notifications))
|
||||
|
|
|
@ -23,6 +23,7 @@ use activitypub_federation::{
|
|||
use chrono::{DateTime, Utc};
|
||||
use diesel::{
|
||||
insert_into,
|
||||
update,
|
||||
AsChangeset,
|
||||
ExpressionMethods,
|
||||
Insertable,
|
||||
|
@ -37,7 +38,7 @@ use std::{fmt::Debug, ops::DerefMut};
|
|||
pub struct DbInstanceForm {
|
||||
pub domain: String,
|
||||
pub ap_id: ObjectId<DbInstance>,
|
||||
pub description: Option<String>,
|
||||
pub topic: Option<String>,
|
||||
pub articles_url: Option<CollectionId<DbArticleCollection>>,
|
||||
pub inbox_url: String,
|
||||
pub public_key: String,
|
||||
|
@ -45,6 +46,14 @@ pub struct DbInstanceForm {
|
|||
pub last_refreshed_at: DateTime<Utc>,
|
||||
pub local: bool,
|
||||
pub instances_url: Option<CollectionId<DbInstanceCollection>>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, AsChangeset)]
|
||||
#[diesel(table_name = instance, check_for_backend(diesel::pg::Pg))]
|
||||
pub struct DbInstanceUpdateForm {
|
||||
pub topic: Option<String>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl DbInstance {
|
||||
|
@ -63,6 +72,14 @@ impl DbInstance {
|
|||
Ok(instance::table.find(id).get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn update(form: DbInstanceUpdateForm, context: &IbisContext) -> MyResult<Self> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(update(instance::table)
|
||||
.filter(instance::local)
|
||||
.set(form)
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read_from_ap_id(
|
||||
ap_id: &ObjectId<DbInstance>,
|
||||
context: &Data<IbisContext>,
|
||||
|
@ -130,11 +147,13 @@ impl DbInstance {
|
|||
.get_results(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn read_remote(context: &Data<IbisContext>) -> MyResult<Vec<DbInstance>> {
|
||||
pub fn list(only_remote: bool, context: &Data<IbisContext>) -> MyResult<Vec<DbInstance>> {
|
||||
let mut conn = context.db_pool.get()?;
|
||||
Ok(instance::table
|
||||
.filter(instance::local.eq(false))
|
||||
.get_results(conn.deref_mut())?)
|
||||
let mut query = instance::table.into_boxed();
|
||||
if only_remote {
|
||||
query = query.filter(instance::local.eq(false));
|
||||
}
|
||||
Ok(query.get_results(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
/// Read the instance where an article is hosted, based on a comment id.
|
||||
|
|
|
@ -67,7 +67,7 @@ diesel::table! {
|
|||
domain -> Text,
|
||||
#[max_length = 255]
|
||||
ap_id -> Varchar,
|
||||
description -> Nullable<Text>,
|
||||
topic -> Nullable<Text>,
|
||||
#[max_length = 255]
|
||||
articles_url -> Nullable<Varchar>,
|
||||
#[max_length = 255]
|
||||
|
@ -78,6 +78,7 @@ diesel::table! {
|
|||
local -> Bool,
|
||||
#[max_length = 255]
|
||||
instances_url -> Nullable<Varchar>,
|
||||
name -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,8 @@ pub struct ApubInstance {
|
|||
#[serde(rename = "type")]
|
||||
kind: ServiceType,
|
||||
pub id: ObjectId<DbInstance>,
|
||||
content: Option<String>,
|
||||
name: Option<String>,
|
||||
summary: Option<String>,
|
||||
articles: Option<CollectionId<DbArticleCollection>>,
|
||||
instances: Option<CollectionId<DbInstanceCollection>>,
|
||||
inbox: Url,
|
||||
|
@ -89,11 +90,12 @@ impl Object for DbInstance {
|
|||
Ok(ApubInstance {
|
||||
kind: Default::default(),
|
||||
id: self.ap_id.clone(),
|
||||
content: self.description.clone(),
|
||||
summary: self.topic.clone(),
|
||||
articles: self.articles_url.clone(),
|
||||
instances: self.instances_url.clone(),
|
||||
inbox: Url::parse(&self.inbox_url)?,
|
||||
public_key: self.public_key(),
|
||||
name: self.name,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -115,7 +117,7 @@ impl Object for DbInstance {
|
|||
let form = DbInstanceForm {
|
||||
domain,
|
||||
ap_id: json.id,
|
||||
description: json.content,
|
||||
topic: json.summary,
|
||||
articles_url: json.articles,
|
||||
instances_url: json.instances,
|
||||
inbox_url: json.inbox.to_string(),
|
||||
|
@ -123,6 +125,7 @@ impl Object for DbInstance {
|
|||
private_key: None,
|
||||
last_refreshed_at: Utc::now(),
|
||||
local: false,
|
||||
name: json.name,
|
||||
};
|
||||
let instance = DbInstance::create(&form, context)?;
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ impl Collection for DbInstanceCollection {
|
|||
_owner: &Self::Owner,
|
||||
context: &Data<Self::DataType>,
|
||||
) -> Result<Self::Kind, Self::Error> {
|
||||
let instances = DbInstance::read_remote(context)?;
|
||||
let instances = DbInstance::list(true, context)?;
|
||||
let instances = future::try_join_all(
|
||||
instances
|
||||
.into_iter()
|
||||
|
|
|
@ -97,7 +97,6 @@ async fn setup(context: &Data<IbisContext>) -> Result<(), Error> {
|
|||
let form = DbInstanceForm {
|
||||
domain: domain.to_string(),
|
||||
ap_id,
|
||||
description: Some("New Ibis instance".to_string()),
|
||||
articles_url: Some(local_articles_url(domain)?),
|
||||
instances_url: Some(linked_instances_url(domain)?),
|
||||
inbox_url,
|
||||
|
@ -105,6 +104,8 @@ async fn setup(context: &Data<IbisContext>) -> Result<(), Error> {
|
|||
private_key: Some(keypair.private_key),
|
||||
last_refreshed_at: Utc::now(),
|
||||
local: true,
|
||||
topic: None,
|
||||
name: None,
|
||||
};
|
||||
let instance = DbInstance::create(&form, context)?;
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ pub struct DbInstance {
|
|||
pub ap_id: ObjectId<DbInstance>,
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub ap_id: String,
|
||||
pub description: Option<String>,
|
||||
pub topic: Option<String>,
|
||||
#[cfg(feature = "ssr")]
|
||||
pub articles_url: Option<CollectionId<DbArticleCollection>>,
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
|
@ -42,6 +42,7 @@ pub struct DbInstance {
|
|||
pub local: bool,
|
||||
#[cfg(feature = "ssr")]
|
||||
pub instances_url: Option<CollectionId<DbInstanceCollection>>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl DbInstance {
|
||||
|
@ -91,3 +92,9 @@ pub struct GetInstanceParams {
|
|||
pub struct FollowInstanceParams {
|
||||
pub id: InstanceId,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct UpdateInstanceParams {
|
||||
pub name: Option<String>,
|
||||
pub topic: Option<String>,
|
||||
}
|
||||
|
|
|
@ -80,16 +80,14 @@ impl ApiClient {
|
|||
&self,
|
||||
data: &CreateArticleParams,
|
||||
) -> Result<DbArticleView, ServerFnError> {
|
||||
self.send(Method::POST, "/api/v1/article", Some(&data))
|
||||
.await
|
||||
self.post("/api/v1/article", Some(&data)).await
|
||||
}
|
||||
|
||||
pub async fn edit_article_with_conflict(
|
||||
&self,
|
||||
params: &EditArticleParams,
|
||||
) -> Result<Option<ApiConflict>, ServerFnError> {
|
||||
self.send(Method::PATCH, "/api/v1/article", Some(¶ms))
|
||||
.await
|
||||
self.patch("/api/v1/article", Some(¶ms)).await
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -120,8 +118,7 @@ impl ApiClient {
|
|||
&self,
|
||||
params: &EditCommentParams,
|
||||
) -> Result<DbCommentView, ServerFnError> {
|
||||
self.send(Method::PATCH, "/api/v1/comment", Some(¶ms))
|
||||
.await
|
||||
self.patch("/api/v1/comment", Some(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn notifications_list(&self) -> Option<Vec<Notification>> {
|
||||
|
@ -169,6 +166,13 @@ impl ApiClient {
|
|||
self.get("/api/v1/instance/list", None::<i32>).await
|
||||
}
|
||||
|
||||
pub async fn update_local_instance(
|
||||
&self,
|
||||
params: &UpdateInstanceParams,
|
||||
) -> Result<DbInstance, ServerFnError> {
|
||||
self.patch("/api/v1/instance", Some(params)).await
|
||||
}
|
||||
|
||||
pub async fn follow_instance_with_resolve(&self, follow_instance: &str) -> Option<DbInstance> {
|
||||
// fetch beta instance on alpha
|
||||
let params = ResolveObjectParams {
|
||||
|
@ -272,6 +276,14 @@ impl ApiClient {
|
|||
self.send(Method::POST, endpoint, query).await
|
||||
}
|
||||
|
||||
async fn patch<T, R>(&self, endpoint: &str, query: Option<R>) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
R: Serialize + Debug,
|
||||
{
|
||||
self.send(Method::PATCH, endpoint, query).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn send<P, T>(
|
||||
&self,
|
||||
|
|
|
@ -4,6 +4,7 @@ use crate::{
|
|||
api::CLIENT,
|
||||
components::{nav::Nav, protected_route::IbisProtectedRoute},
|
||||
dark_mode::DarkMode,
|
||||
instance_title,
|
||||
pages::{
|
||||
article::{
|
||||
actions::ArticleActions,
|
||||
|
@ -15,7 +16,7 @@ use crate::{
|
|||
read::ReadArticle,
|
||||
},
|
||||
diff::EditDiff,
|
||||
instance::{details::InstanceDetails, list::ListInstances},
|
||||
instance::{details::InstanceDetails, list::ListInstances, settings::InstanceSettings},
|
||||
login::Login,
|
||||
notifications::Notifications,
|
||||
register::Register,
|
||||
|
@ -94,15 +95,29 @@ pub fn App() -> impl IntoView {
|
|||
let darkmode = DarkMode::init();
|
||||
provide_context(darkmode.clone());
|
||||
|
||||
// TODO: use instance name/description for title
|
||||
let instance = Resource::new(
|
||||
|| (),
|
||||
|_| async move { CLIENT.get_local_instance().await.unwrap() },
|
||||
);
|
||||
view! {
|
||||
<Html attr:data-theme=darkmode.theme {..} class="h-full" />
|
||||
<Title formatter=|text| format!("{text} — Ibis") />
|
||||
<Body {..} class="h-full max-sm:flex max-sm:flex-col" />
|
||||
<>
|
||||
<Stylesheet id="ibis" href="/pkg/ibis.css" />
|
||||
<Stylesheet id="katex" href="/katex.min.css" />
|
||||
<Router>
|
||||
<Suspense>
|
||||
{move || {
|
||||
instance
|
||||
.get()
|
||||
.map(|i| {
|
||||
let formatter = move |text| {
|
||||
format!("{text} — {}", instance_title(&i.instance))
|
||||
};
|
||||
view! { <Title formatter /> }
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
<Nav />
|
||||
<main class="p-4 md:ml-64">
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
|
@ -129,6 +144,7 @@ pub fn App() -> impl IntoView {
|
|||
<Route path=path!("/search") view=Search />
|
||||
<IbisProtectedRoute path=path!("/edit_profile") view=UserEditProfile />
|
||||
<IbisProtectedRoute path=path!("/notifications") view=Notifications />
|
||||
<IbisProtectedRoute path=path!("/settings") view=InstanceSettings />
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
|
|
@ -72,9 +72,9 @@ pub fn CommentView(
|
|||
view! {
|
||||
<CommentEditorView
|
||||
article=article
|
||||
parent_id=Some(comment.comment.id)
|
||||
set_show_editor=Some(show_editor.1)
|
||||
edit_params=Some(edit_params.clone())
|
||||
parent_id=comment.comment.id
|
||||
set_show_editor=show_editor.1
|
||||
edit_params=edit_params.clone()
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
@ -115,9 +115,8 @@ pub fn CommentView(
|
|||
<Show when=move || show_editor.0.get() == comment.comment.id>
|
||||
<CommentEditorView
|
||||
article=article
|
||||
parent_id=Some(comment.comment.id)
|
||||
set_show_editor=Some(show_editor.1)
|
||||
edit_params=None
|
||||
parent_id=comment.comment.id
|
||||
set_show_editor=show_editor.1
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -19,10 +19,12 @@ pub struct EditParams {
|
|||
#[component]
|
||||
pub fn CommentEditorView(
|
||||
article: Resource<DbArticleView>,
|
||||
parent_id: Option<CommentId>,
|
||||
#[prop(optional)] parent_id: Option<CommentId>,
|
||||
/// Set this to CommentId(-1) to hide all editors
|
||||
#[prop(optional)]
|
||||
set_show_editor: Option<WriteSignal<CommentId>>,
|
||||
/// If this is present we are editing an existing comment
|
||||
#[prop(optional)]
|
||||
edit_params: Option<EditParams>,
|
||||
) -> impl IntoView {
|
||||
let textarea_ref = NodeRef::<Textarea>::new();
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::frontend::{
|
||||
api::CLIENT,
|
||||
app::{is_logged_in, site, DefaultResource},
|
||||
app::{is_admin, is_logged_in, site, DefaultResource},
|
||||
dark_mode::DarkMode,
|
||||
instance_title,
|
||||
};
|
||||
use leptos::{component, prelude::*, view, IntoView, *};
|
||||
use leptos_router::hooks::use_navigate;
|
||||
|
@ -16,6 +17,10 @@ pub fn Nav() -> impl IntoView {
|
|||
|| (),
|
||||
move |_| async move { CLIENT.notifications_count().await.unwrap_or_default() },
|
||||
);
|
||||
let instance = Resource::new(
|
||||
|| (),
|
||||
|_| async move { CLIENT.get_local_instance().await.unwrap() },
|
||||
);
|
||||
|
||||
let (search_query, set_search_query) = signal(String::new());
|
||||
let mut dark_mode = expect_context::<DarkMode>();
|
||||
|
@ -27,17 +32,15 @@ pub fn Nav() -> impl IntoView {
|
|||
>
|
||||
<h1 class="w-min font-serif text-3xl font-bold md:hidden">Ibis</h1>
|
||||
<div class="flex-grow md:hidden"></div>
|
||||
<button tabindex="0" class="lg:hidden btn btn-outline">
|
||||
Menu
|
||||
</button>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="p-2 md:h-full menu dropdown-content max-sm:rounded-box max-sm:z-[1] max-sm:shadow"
|
||||
>
|
||||
<button class="lg:hidden btn btn-outline">Menu</button>
|
||||
<div class="md:h-full menu dropdown-content max-sm:rounded-box max-sm:z-[1] max-sm:shadow">
|
||||
<Transition>
|
||||
<a href="/">
|
||||
<img src="/logo.png" class="m-auto max-sm:hidden" />
|
||||
</a>
|
||||
<h2 class="m-4 font-serif text-xl font-bold">
|
||||
{move || { instance.get().map(|i| instance_title(&i.instance)) }}
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">"Main Page"</a>
|
||||
|
@ -61,6 +64,11 @@ pub fn Nav() -> impl IntoView {
|
|||
</a>
|
||||
</li>
|
||||
</Show>
|
||||
<Show when=is_admin>
|
||||
<li>
|
||||
<a href="/settings">"Settings"</a>
|
||||
</li>
|
||||
</Show>
|
||||
<li>
|
||||
<form
|
||||
class="p-1 m-0 form-control"
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
use crate::common::{article::DbArticle, user::DbPerson, utils::extract_domain};
|
||||
use crate::common::{
|
||||
article::DbArticle,
|
||||
instance::DbInstance,
|
||||
user::DbPerson,
|
||||
utils::extract_domain,
|
||||
};
|
||||
use chrono::{DateTime, Duration, Local, Utc};
|
||||
use codee::string::FromToStringCodec;
|
||||
use leptos::prelude::*;
|
||||
|
@ -103,3 +108,25 @@ fn time_ago(time: DateTime<Utc>) -> String {
|
|||
let duration = std::time::Duration::from_secs(secs.try_into().unwrap_or_default());
|
||||
INSTANCE.get_or_init(Formatter::new).convert(duration)
|
||||
}
|
||||
|
||||
fn instance_title_with_domain(instance: &DbInstance) -> String {
|
||||
let name = instance.name.clone();
|
||||
let domain = instance.domain.clone();
|
||||
if let Some(name) = name {
|
||||
format!("{name} ({domain})")
|
||||
} else {
|
||||
domain
|
||||
}
|
||||
}
|
||||
|
||||
fn instance_title(instance: &DbInstance) -> String {
|
||||
instance.name.clone().unwrap_or(instance.domain.clone())
|
||||
}
|
||||
|
||||
fn instance_updated(instance: &DbInstance) -> String {
|
||||
if instance.local {
|
||||
"Local".to_string()
|
||||
} else {
|
||||
format!("Updated {}", time_ago(instance.last_refreshed_at))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,12 +21,7 @@ pub fn ArticleDiscussion() -> impl IntoView {
|
|||
view! {
|
||||
<ArticleNav article=article active_tab=ActiveTab::Discussion />
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<CommentEditorView
|
||||
article=article
|
||||
parent_id=None
|
||||
set_show_editor=None
|
||||
edit_params=None
|
||||
/>
|
||||
<CommentEditorView article=article />
|
||||
<div>
|
||||
<For
|
||||
each=move || {
|
||||
|
|
|
@ -5,6 +5,8 @@ use crate::{
|
|||
article_path,
|
||||
article_title,
|
||||
components::instance_follow_button::InstanceFollowButton,
|
||||
instance_title_with_domain,
|
||||
instance_updated,
|
||||
},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
|
@ -41,20 +43,19 @@ pub fn InstanceDetails() -> impl IntoView {
|
|||
.unwrap()
|
||||
},
|
||||
);
|
||||
let title = instance.clone().description.unwrap_or(instance.clone().domain);
|
||||
let title = instance_title_with_domain(&instance);
|
||||
let instance_ = instance.clone();
|
||||
view! {
|
||||
<Title text=title />
|
||||
<Title text=title.clone() />
|
||||
<div class="grid gap-3 mt-4">
|
||||
<div class="flex flex-row items-center">
|
||||
<h1 class="w-full font-serif text-4xl font-bold">
|
||||
{instance.domain}
|
||||
</h1>
|
||||
<h1 class="w-full font-serif text-4xl font-bold">{title}</h1>
|
||||
{instance_updated(&instance_)}
|
||||
<InstanceFollowButton instance=instance_.clone() />
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<div>{instance.description}</div>
|
||||
<div>{instance.topic}</div>
|
||||
<h2 class="font-serif text-xl font-bold">Articles</h2>
|
||||
<ul class="list-none">
|
||||
<Suspense>
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
use crate::frontend::{api::CLIENT, components::connect::ConnectView};
|
||||
use crate::frontend::{
|
||||
api::CLIENT,
|
||||
components::connect::ConnectView,
|
||||
instance_title_with_domain,
|
||||
instance_updated,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Title;
|
||||
|
||||
|
@ -26,12 +31,20 @@ pub fn ListInstances() -> impl IntoView {
|
|||
.map(|ref i| {
|
||||
view! {
|
||||
<li>
|
||||
<a
|
||||
class="text-lg link"
|
||||
href=format!("/instance/{}", i.domain)
|
||||
>
|
||||
{i.domain.to_string()}
|
||||
</a>
|
||||
<div class="m-4 shadow card bg-base-100">
|
||||
<div class="p-4 card-body">
|
||||
<div class="flex">
|
||||
<a
|
||||
class="card-title grow"
|
||||
href=format!("/instance/{}", i.domain)
|
||||
>
|
||||
{instance_title_with_domain(i)}
|
||||
</a>
|
||||
{instance_updated(i)}
|
||||
</div>
|
||||
<p>{i.topic.clone()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
pub(crate) mod details;
|
||||
pub(crate) mod list;
|
||||
pub(crate) mod settings;
|
||||
|
|
112
src/frontend/pages/instance/settings.rs
Normal file
112
src/frontend/pages/instance/settings.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use crate::{common::instance::UpdateInstanceParams, frontend::api::CLIENT};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Title;
|
||||
|
||||
#[component]
|
||||
pub fn InstanceSettings() -> impl IntoView {
|
||||
let (saved, set_saved) = signal(false);
|
||||
let (submit_error, set_submit_error) = signal(None::<String>);
|
||||
let instance = Resource::new(
|
||||
|| (),
|
||||
|_| async move { CLIENT.get_local_instance().await.unwrap() },
|
||||
);
|
||||
|
||||
let submit_action = Action::new(move |params: &UpdateInstanceParams| {
|
||||
let params = params.clone();
|
||||
async move {
|
||||
let result = CLIENT.update_local_instance(¶ms).await;
|
||||
match result {
|
||||
Ok(_res) => {
|
||||
instance.refetch();
|
||||
set_saved.set(true);
|
||||
set_submit_error.set(None);
|
||||
}
|
||||
Err(err) => {
|
||||
let msg = err.to_string();
|
||||
log::warn!("Unable to update profile: {msg}");
|
||||
set_submit_error.set(Some(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: It would make sense to use a table for the labels and inputs, but for some reason
|
||||
// that completely breaks reactivity.
|
||||
view! {
|
||||
<Title text="Instance Settings" />
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
{move || Suspend::new(async move {
|
||||
let instance = instance.await;
|
||||
let (name, set_name) = signal(instance.instance.name.unwrap_or_default());
|
||||
let (topic, set_topic) = signal(instance.instance.topic.unwrap_or_default());
|
||||
view! {
|
||||
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
|
||||
"Instance Settings"
|
||||
</h1>
|
||||
{move || {
|
||||
submit_error
|
||||
.get()
|
||||
.map(|err| {
|
||||
view! { <p class="alert alert-error">{err}</p> }
|
||||
})
|
||||
}}
|
||||
<div class="flex flex-row mb-2">
|
||||
<label class="block w-20" for="name">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
class="w-80 input input-secondary input-bordered"
|
||||
prop:value=name
|
||||
value=name
|
||||
on:change=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_name.set(val);
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row mb-2">
|
||||
<label class="block w-20" for="topic">
|
||||
"Topic"
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
class="w-80 input input-secondary input-bordered"
|
||||
prop:value=topic
|
||||
value=topic
|
||||
on:change=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_topic.set(val);
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click=move |_| {
|
||||
let form = UpdateInstanceParams {
|
||||
name: Some(name.get()),
|
||||
topic: Some(topic.get()),
|
||||
};
|
||||
submit_action.dispatch(form);
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
|
||||
<Show when=move || saved.get()>
|
||||
<div class="toast">
|
||||
<div class="alert alert-info">
|
||||
<span>Saved!</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
})}
|
||||
|
||||
</Suspense>
|
||||
}
|
||||
}
|
|
@ -763,7 +763,7 @@ async fn test_synchronize_instances() -> Result<()> {
|
|||
.await
|
||||
.unwrap();
|
||||
let beta_instances = beta.list_instances().await.unwrap();
|
||||
assert_eq!(1, beta_instances.len());
|
||||
assert_eq!(2, beta_instances.len());
|
||||
|
||||
// fetch beta instance on gamma
|
||||
gamma
|
||||
|
@ -777,7 +777,7 @@ async fn test_synchronize_instances() -> Result<()> {
|
|||
let res = gamma.list_instances().await;
|
||||
match res {
|
||||
None => Err(RetryPolicy::<String>::Retry(None)),
|
||||
Some(i) if i.len() < 2 => Err(RetryPolicy::Retry(None)),
|
||||
Some(i) if i.len() < 3 => Err(RetryPolicy::Retry(None)),
|
||||
Some(i) => Ok(i),
|
||||
}
|
||||
},
|
||||
|
@ -786,7 +786,7 @@ async fn test_synchronize_instances() -> Result<()> {
|
|||
.await?;
|
||||
|
||||
// now gamma also knows about alpha
|
||||
assert_eq!(2, gamma_instances.len());
|
||||
assert_eq!(3, gamma_instances.len());
|
||||
assert!(gamma_instances.iter().any(|i| i.domain == alpha.hostname));
|
||||
|
||||
TestData::stop(alpha, beta, gamma)
|
||||
|
|
Loading…
Reference in a new issue