Set up the user interface state model and set up the admin user onboarding #283

Merged
savanni merged 12 commits from visions-admin into main 2024-12-18 14:18:16 +00:00
9 changed files with 124 additions and 6 deletions
Showing only changes of commit e8bc0590c6 - Show all commits

View File

@ -8,7 +8,7 @@ use uuid::Uuid;
use crate::{
asset_db::{self, AssetId, Assets},
database::{CharacterId, Database, Error},
database::{CharacterId, Database, Error, UserId},
types::{AppError, FatalError, Message, Tabletop, RGB},
};
@ -81,6 +81,18 @@ impl Core {
}
}
pub async fn list_users(&self) -> ResultExt<Vec<(UserId, String)>, AppError, FatalError> {
let users = self.0.write().await.db.users().await;
match users {
ResultExt::Ok(users) => ResultExt::Ok(users),
ResultExt::Err(err) => {
println!("Database error: {:?}", err);
ResultExt::Ok(vec![])
}
ResultExt::Fatal(users) => ResultExt::Fatal(users),
}
}
pub async fn tabletop(&self) -> Tabletop {
self.0.read().await.tabletop.clone()
}

View File

@ -29,6 +29,7 @@ pub enum Error {
#[derive(Debug)]
enum Request {
Charsheet(CharacterId),
Users,
}
#[derive(Debug)]
@ -39,6 +40,7 @@ struct DatabaseRequest {
#[derive(Debug)]
enum DatabaseResponse {
Users(Vec<(UserId, String)>),
Charsheet(Option<CharsheetRow>),
}
@ -142,6 +144,8 @@ pub struct CharsheetRow {
#[async_trait]
pub trait Database: Send + Sync {
async fn users(&mut self) -> result_extended::ResultExt<Vec<(UserId, String)>, Error, FatalError>;
async fn character(
&mut self,
id: CharacterId,
@ -240,6 +244,17 @@ impl DiskDb {
}
}
fn users(&self) -> Result<Vec<(UserId, String)>, FatalError> {
let mut stmt = self.conn.prepare("SELECT * FROM USERS")
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
let items = stmt.query_map([], |row| {
let userid: String = row.get(0).unwrap();
let username = row.get(1).unwrap();
Ok((UserId::from(userid), username))
}).unwrap().collect::<Result<Vec<(UserId, String)>, rusqlite::Error>>().unwrap();
Ok(items)
}
fn save_user(
&self,
user_id: Option<UserId>,
@ -374,6 +389,15 @@ async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
_ => unimplemented!(),
}
}
Request::Users => {
let users = db.users();
match users {
Ok(users) => {
tx.send(DatabaseResponse::Users(users)).await.unwrap();
}
_ => unimplemented!(),
}
}
}
}
println!("ending db_handler");
@ -402,6 +426,26 @@ impl DbConn {
#[async_trait]
impl Database for DbConn {
async fn users(&mut self) -> ResultExt<Vec<(UserId, String)>, Error, FatalError> {
let (tx, rx) = bounded::<DatabaseResponse>(1);
let request = DatabaseRequest {
tx,
req: Request::Users,
};
match self.conn.send(request).await {
Ok(()) => (),
Err(_) => return fatal(FatalError::DatabaseConnectionLost),
};
match rx.recv().await {
Ok(DatabaseResponse::Users(lst)) => ok(lst),
Ok(_) => fatal(FatalError::MessageMismatch),
Err(_) => error(Error::NoResponse),
}
}
async fn character(
&mut self,
id: CharacterId,
@ -420,7 +464,7 @@ impl Database for DbConn {
match rx.recv().await {
Ok(DatabaseResponse::Charsheet(row)) => ok(row),
// Ok(_) => fatal(FatalError::MessageMismatch),
Ok(_) => fatal(FatalError::MessageMismatch),
Err(_err) => error(Error::NoResponse),
}
}

View File

@ -5,7 +5,12 @@ use result_extended::{ok, return_error, ResultExt};
use serde::{Deserialize, Serialize};
use warp::{http::Response, http::StatusCode, reply::Reply, ws::Message};
use crate::{asset_db::AssetId, core::Core, database::CharacterId, types::{AppError, FatalError}};
use crate::{
asset_db::AssetId,
core::Core,
database::CharacterId,
types::{AppError, FatalError},
};
/*
pub async fn handle_auth(
@ -164,6 +169,23 @@ pub async fn handle_set_background_image(core: Core, image_name: String) -> impl
.await
}
pub async fn handle_get_users(core: Core) -> impl Reply {
handler(async move {
let users = match core.list_users().await {
ResultExt::Ok(users) => users,
ResultExt::Err(err) => return ResultExt::Err(err),
ResultExt::Fatal(err) => return ResultExt::Fatal(err),
};
ok(Response::builder()
.header("Access-Control-Allow-Origin", "*")
.header("Content-Type", "application/json")
.body(serde_json::to_vec(&users).unwrap())
.unwrap())
})
.await
}
pub async fn handle_get_charsheet(core: Core, charid: String) -> impl Reply {
handler(async move {
let sheet = match core.get_charsheet(CharacterId::from(charid)).await {

View File

@ -8,7 +8,7 @@ use asset_db::{AssetId, FsAssets};
use authdb::AuthError;
use database::DbConn;
use handlers::{
handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_register_client, handle_set_background_image, handle_unregister_client, RegisterRequest
handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_get_users, handle_register_client, handle_set_background_image, handle_unregister_client, RegisterRequest
};
use warp::{
// header,
@ -160,6 +160,13 @@ pub async fn main() {
})
.with(log);
let route_get_users = warp::path!("api" / "v1" / "users")
.and(warp::get())
.then({
let core = core.clone();
move || handle_get_users(core.clone())
});
let route_get_charsheet = warp::path!("api" / "v1" / "charsheet" / String)
.and(warp::get())
.then({
@ -174,6 +181,7 @@ pub async fn main() {
.or(route_available_images)
.or(route_set_bg_image_options)
.or(route_set_bg_image)
.or(route_get_users)
.or(route_get_charsheet)
.recover(handle_rejection);

View File

@ -6,8 +6,11 @@ import { DesignPage } from './views/Design/Design';
import { GmView } from './views/GmView/GmView';
import { WebsocketProvider } from './components/WebsocketProvider';
import { PlayerView } from './views/PlayerView/PlayerView';
import { Admin } from './views/Admin/Admin';
import Candela from './plugins/Candela';
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803";
interface AppProps {
client: Client;
}
@ -15,7 +18,7 @@ interface AppProps {
const CandelaCharsheet = ({ client }: { client: Client }) => {
let [sheet, setSheet] = useState(undefined);
useEffect(
() => { client.charsheet("db7a2585-5dcf-4909-8743-2741111f8b9a").then((c) => setSheet(c)); },
() => { client.charsheet(TEST_CHARSHEET_UUID).then((c) => setSheet(c)); },
[client, setSheet]
);
@ -36,6 +39,10 @@ const App = ({ client }: AppProps) => {
path: "/gm",
element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <GmView client={client} /> </WebsocketProvider> : <div> </div>
},
{
path: "/admin",
element: <Admin client={client} />
},
{
path: "/",
element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <PlayerView client={client} /> </WebsocketProvider> : <div> </div>

View File

@ -45,6 +45,12 @@ export class Client {
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) });
}
async users() {
const url = new URL(this.base);
url.pathname = '/api/v1/users/';
return fetch(url).then((response) => response.json());
}
async charsheet(id: string) {
const url = new URL(this.base);
url.pathname = `/api/v1/charsheet/${id}`;

View File

View File

@ -0,0 +1,17 @@
import React, { useEffect, useState } from 'react';
import { Client } from '../../client';
interface AdminProps {
client: Client,
}
export const Admin = ({ client }: AdminProps) => {
const [users, setUsers] = useState([]);
useEffect(() => {
client.users().then(setUsers);
}, [client, setUsers]);
return <ul>
{users.map(([uuid, username]) => <li> {username} </li>) }
</ul>;
}

View File

@ -5,6 +5,8 @@ import { Client } from '../../client';
import { TabletopElement } from '../../components/Tabletop/Tabletop';
import Candela from '../../plugins/Candela';
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803";
interface PlayerViewProps {
client: Client;
}
@ -16,7 +18,7 @@ export const PlayerView = ({ client }: PlayerViewProps) => {
useEffect(
() => {
client.charsheet("db7a2585-5dcf-4909-8743-2741111f8b9a").then((c) => {
client.charsheet(TEST_CHARSHEET_UUID).then((c) => {
setCharsheet(c)
});
},