|
|
@@ -1,29 +1,56 @@
|
|
|
mod data;
|
|
|
mod db;
|
|
|
+mod menu;
|
|
|
+mod model;
|
|
|
|
|
|
-use iced::widget::{button, text, text_input};
|
|
|
-use iced::{Element, Task};
|
|
|
+use std::collections::HashMap;
|
|
|
+use std::i64;
|
|
|
+
|
|
|
+use iced::widget::{row, text};
|
|
|
+use iced::{Element, Length, Task};
|
|
|
use sqlx::SqlitePool;
|
|
|
|
|
|
-use crate::model::Workspace;
|
|
|
+use crate::data::{
|
|
|
+ TemplateCollection, TemplateEntry, TemplateEnvironmentVariable, TemplateRequest,
|
|
|
+ TemplateWorkspace, TemplateWorkspaceEnvironment,
|
|
|
+};
|
|
|
+use crate::menu::{WorkspaceMenu, WorkspaceMenuMessage};
|
|
|
+use crate::model::{RequestParams, WorkspaceEntry, WorkspaceEntryType};
|
|
|
|
|
|
fn main() -> iced::Result {
|
|
|
tracing_subscriber::fmt::init();
|
|
|
- iced::run("RestEZ", update, view)
|
|
|
+ iced::run("restEZ", update, view)
|
|
|
+}
|
|
|
+
|
|
|
+macro_rules! unwrap {
|
|
|
+ ($result:ident, $msg:path) => {
|
|
|
+ match $result {
|
|
|
+ Ok(ws) => $msg(ws),
|
|
|
+ Err(e) => {
|
|
|
+ tracing::error!("{e}");
|
|
|
+ Message::Noop
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ ($result:ident, $msg:path, $err_msg:literal $(,)? $($args:expr),*) => {
|
|
|
+ match $result {
|
|
|
+ Ok(ws) => $msg(ws),
|
|
|
+ Err(e) => {
|
|
|
+ tracing::error!($err_msg, $($args),*);
|
|
|
+ Message::Noop
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
fn update(state: &mut AppState, message: Message) -> Task<Message> {
|
|
|
match message {
|
|
|
Message::WorkspaceAdded => {
|
|
|
- return Task::perform(list_workspaces(state.db.clone()), |ws| match ws {
|
|
|
- Ok(ws) => Message::ReloadWorkspaces(ws),
|
|
|
- Err(e) => {
|
|
|
- tracing::error!("Error loading workspaces {e}");
|
|
|
- Message::Noop
|
|
|
- }
|
|
|
+ return Task::perform(list_workspaces(state.db.clone()), |ws| {
|
|
|
+ unwrap!(ws, Message::ReloadWorkspaces)
|
|
|
});
|
|
|
}
|
|
|
- Message::ContentChange(content) => {
|
|
|
+ Message::NewWsBufferContentChange(content) => {
|
|
|
state.ws_buffer = content;
|
|
|
}
|
|
|
Message::AddWorkspace => {
|
|
|
@@ -36,8 +63,48 @@ fn update(state: &mut AppState, message: Message) -> Task<Message> {
|
|
|
|_| Message::WorkspaceAdded,
|
|
|
);
|
|
|
}
|
|
|
- Message::ReloadWorkspaces(ws) => {
|
|
|
- state.workspaces = ws;
|
|
|
+ Message::ReloadWorkspaces(mut ws) => {
|
|
|
+ ws.sort_by(|a, b| a.name.cmp(&b.name));
|
|
|
+ state.workspaces = ws.into_iter().map(TemplateWorkspace::from).collect();
|
|
|
+ state.ws_menu.choices = (0..state.workspaces.len()).collect()
|
|
|
+ }
|
|
|
+ Message::WorkspaceMenu(msg) => {
|
|
|
+ state.ws_menu.update(&msg);
|
|
|
+ match msg {
|
|
|
+ WorkspaceMenuMessage::Select(i) => {
|
|
|
+ let workspace = state.workspaces.swap_remove(i);
|
|
|
+ let id = workspace.id;
|
|
|
+ state.ws_current = Some(workspace);
|
|
|
+ return Task::perform(get_entries(state.db.clone(), id as i64), |entries| {
|
|
|
+ unwrap!(entries, Message::WorkspaceEntriesLoaded)
|
|
|
+ })
|
|
|
+ .chain(Task::perform(
|
|
|
+ get_environments(state.db.clone(), id as i64),
|
|
|
+ |envs| unwrap!(envs, Message::WorkspaceEnvsLoaded),
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ _ => {}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Message::WorkspaceEnvsLoaded(envs) => {
|
|
|
+ let Some(workspace) = &mut state.ws_current else {
|
|
|
+ tracing::warn!("Workspace env loaded, but no active workspace");
|
|
|
+ return Task::none();
|
|
|
+ };
|
|
|
+
|
|
|
+ workspace.environments = envs;
|
|
|
+
|
|
|
+ if let Some(env) = workspace.environments.values().next() {
|
|
|
+ workspace.env_current = Some(env.id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Message::WorkspaceEntriesLoaded(items) => {
|
|
|
+ let Some(workspace) = &mut state.ws_current else {
|
|
|
+ tracing::warn!("Workspace entries loaded, but no active workspace");
|
|
|
+ return Task::none();
|
|
|
+ };
|
|
|
+
|
|
|
+ workspace.update_entries(items);
|
|
|
}
|
|
|
Message::Noop => {}
|
|
|
}
|
|
|
@@ -45,33 +112,61 @@ fn update(state: &mut AppState, message: Message) -> Task<Message> {
|
|
|
}
|
|
|
|
|
|
fn view(state: &AppState) -> Element<'_, Message> {
|
|
|
- let add_new: Element<_> = button(text("Add")).on_press(Message::AddWorkspace).into();
|
|
|
+ let menus = state.ws_menu.view(&state.workspaces, &state.ws_buffer);
|
|
|
|
|
|
- let input: Element<_> = text_input("Workspace name", &state.ws_buffer)
|
|
|
- .on_input(Message::ContentChange)
|
|
|
- .into();
|
|
|
+ let main = match state.ws_current {
|
|
|
+ Some(ref ws) => {
|
|
|
+ // let sidebar = column![text(ws.name)].width(Length::Fill);
|
|
|
+ match ws.req_current {
|
|
|
+ Some(ref _req) => {
|
|
|
+ // TODO: Display request
|
|
|
+ iced::widget::container("TODO: Request editor")
|
|
|
+ }
|
|
|
|
|
|
- let mut column = iced::widget::column![add_new, input];
|
|
|
- for workspace in state.workspaces.iter() {
|
|
|
- column = column.extend(vec![text(workspace.name.clone()).into()]);
|
|
|
+ None => {
|
|
|
+ // TODO: Display workspace stuff
|
|
|
+ iced::widget::container("TODO: Workspace stuff")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ None => iced::widget::container("Select a workspace to start working."),
|
|
|
}
|
|
|
- column.into()
|
|
|
+ .center(Length::Fill)
|
|
|
+ .padding(10);
|
|
|
+
|
|
|
+ iced::widget::column![
|
|
|
+ menus,
|
|
|
+ main,
|
|
|
+ row![text(format!(
|
|
|
+ "Workspace: {}",
|
|
|
+ state.ws_current.as_ref().map_or("", |w| &w.name)
|
|
|
+ ))]
|
|
|
+ ]
|
|
|
+ .into()
|
|
|
}
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
enum Message {
|
|
|
- ReloadWorkspaces(Vec<Workspace>),
|
|
|
+ ReloadWorkspaces(Vec<model::Workspace>),
|
|
|
WorkspaceAdded,
|
|
|
AddWorkspace,
|
|
|
- ContentChange(String),
|
|
|
+ NewWsBufferContentChange(String),
|
|
|
Noop,
|
|
|
+ WorkspaceMenu(WorkspaceMenuMessage),
|
|
|
+
|
|
|
+ WorkspaceEnvsLoaded(HashMap<i64, TemplateWorkspaceEnvironment>),
|
|
|
+ WorkspaceEntriesLoaded(Vec<TemplateEntry>),
|
|
|
}
|
|
|
|
|
|
pub struct AppState {
|
|
|
/// Sqlite database. Just an Arc so cheap to clone.
|
|
|
- pub db: sqlx::sqlite::SqlitePool,
|
|
|
- pub workspaces: Vec<model::Workspace>,
|
|
|
- pub ws_buffer: String,
|
|
|
+ db: sqlx::sqlite::SqlitePool,
|
|
|
+
|
|
|
+ /// Workspace buffer. Populated by entries only when necessary.
|
|
|
+ workspaces: Vec<TemplateWorkspace>,
|
|
|
+ ws_buffer: String,
|
|
|
+ ws_menu: WorkspaceMenu,
|
|
|
+ ws_current: Option<TemplateWorkspace>,
|
|
|
}
|
|
|
|
|
|
impl AppState {
|
|
|
@@ -79,12 +174,26 @@ impl AppState {
|
|
|
tracing::info!("Connecting to DB");
|
|
|
let db = db::init("sqlite:/home/biblius/codium/rusty/restez/restez.db").await;
|
|
|
|
|
|
- let workspaces = list_workspaces(db.clone()).await.unwrap();
|
|
|
+ let workspaces: Vec<TemplateWorkspace> = list_workspaces(db.clone())
|
|
|
+ .await
|
|
|
+ .unwrap()
|
|
|
+ .into_iter()
|
|
|
+ .map(TemplateWorkspace::from)
|
|
|
+ .collect();
|
|
|
+
|
|
|
+ let ws_menu = WorkspaceMenu {
|
|
|
+ choices: (0..workspaces.len()).collect(),
|
|
|
+ expanded: false,
|
|
|
+ };
|
|
|
+
|
|
|
tracing::info!("State loaded");
|
|
|
+
|
|
|
Self {
|
|
|
db,
|
|
|
workspaces,
|
|
|
ws_buffer: String::with_capacity(64),
|
|
|
+ ws_menu,
|
|
|
+ ws_current: None,
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -119,71 +228,189 @@ async fn list_workspaces(db: SqlitePool) -> Result<Vec<model::Workspace>, String
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-pub mod model {
|
|
|
- use serde::{Deserialize, Serialize};
|
|
|
+async fn get_entries(db: SqlitePool, workspace_id: i64) -> Result<Vec<TemplateEntry>, String> {
|
|
|
+ let entries = sqlx::query_as!(
|
|
|
+ model::WorkspaceEntry,
|
|
|
+ "SELECT id, workspace_id, parent_id, name, type FROM workspace_entries WHERE workspace_id = ?",
|
|
|
+ workspace_id,
|
|
|
+ )
|
|
|
+ .fetch_all(&db)
|
|
|
+ .await
|
|
|
+ .map_err(|e| e.to_string())?;
|
|
|
|
|
|
- #[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
- pub struct Workspace {
|
|
|
- pub id: i64,
|
|
|
- pub name: String,
|
|
|
- }
|
|
|
+ let mut request_params: HashMap<i64, RequestParams> = sqlx::query_as!(
|
|
|
+ RequestParams,
|
|
|
+ r#"
|
|
|
+ SELECT rp.request_id as id, method as 'method!', url as 'url!', content_type, body
|
|
|
+ FROM request_params rp
|
|
|
+ LEFT JOIN request_bodies rb ON rp.request_id = rb.request_id
|
|
|
+ WHERE workspace_id = ?
|
|
|
+ "#,
|
|
|
+ workspace_id
|
|
|
+ )
|
|
|
+ .fetch_all(&db)
|
|
|
+ .await
|
|
|
+ .map_err(|e| e.to_string())?
|
|
|
+ .into_iter()
|
|
|
+ .map(|req| (req.id, req))
|
|
|
+ .collect();
|
|
|
|
|
|
- #[derive(Debug, Serialize, Deserialize)]
|
|
|
- pub struct WorkspaceEnv {
|
|
|
- pub id: i64,
|
|
|
- pub workspace_id: i64,
|
|
|
- pub name: String,
|
|
|
- }
|
|
|
+ let mut out: Vec<TemplateEntry> = vec![];
|
|
|
+ let mut children: HashMap<i64, Vec<TemplateCollection>> = HashMap::new();
|
|
|
+ let mut child_requests = HashMap::<i64, Vec<TemplateRequest>>::new();
|
|
|
+
|
|
|
+ for entry in entries {
|
|
|
+ match entry.r#type {
|
|
|
+ WorkspaceEntryType::Request => {
|
|
|
+ let headers = sqlx::query_as!(
|
|
|
+ model::RequestHeader,
|
|
|
+ "SELECT name, value FROM request_headers WHERE request_id = ?",
|
|
|
+ entry.id
|
|
|
+ )
|
|
|
+ .fetch_all(&db)
|
|
|
+ .await
|
|
|
+ .map_err(|e| e.to_string())?;
|
|
|
|
|
|
- #[derive(Debug, Serialize, Deserialize)]
|
|
|
- pub struct WorkspaceEnvVariable {
|
|
|
- pub id: i64,
|
|
|
- pub env_id: i64,
|
|
|
- pub name: String,
|
|
|
- pub value: String,
|
|
|
- pub secret: bool,
|
|
|
+ let Some(params) = request_params.remove(&entry.id) else {
|
|
|
+ tracing::warn!("request {} has no params!", entry.id);
|
|
|
+ continue;
|
|
|
+ };
|
|
|
+
|
|
|
+ let parent_id = entry.parent_id;
|
|
|
+ let req = TemplateRequest::from_params_and_headers(entry, params, headers);
|
|
|
+ if let Some(parent) = parent_id {
|
|
|
+ child_requests
|
|
|
+ .entry(parent)
|
|
|
+ .and_modify(|reqs| reqs.push(req.clone()))
|
|
|
+ .or_insert(vec![req.clone()]);
|
|
|
+ } else {
|
|
|
+ out.push(TemplateEntry::Request(req));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ WorkspaceEntryType::Collection => {
|
|
|
+ let col = TemplateCollection {
|
|
|
+ id: entry.id,
|
|
|
+ entries: HashMap::new(),
|
|
|
+ };
|
|
|
+ if let Some(parent) = entry.parent_id {
|
|
|
+ children
|
|
|
+ .entry(parent)
|
|
|
+ .and_modify(|p| p.push(col.clone()))
|
|
|
+ .or_insert(vec![col]);
|
|
|
+ } else {
|
|
|
+ out.push(TemplateEntry::Collection(col));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- #[derive(Debug, Serialize, Deserialize)]
|
|
|
- pub struct Collection {
|
|
|
- pub id: i64,
|
|
|
- pub workspace_id: i64,
|
|
|
- pub parent_id: Option<i64>,
|
|
|
- pub name: String,
|
|
|
- pub r#type: String, // `type` is a reserved keyword in Rust
|
|
|
+ fn extend_recursive(
|
|
|
+ collection: &mut TemplateCollection,
|
|
|
+ children: &mut HashMap<i64, Vec<TemplateCollection>>,
|
|
|
+ requests: &mut HashMap<i64, Vec<TemplateRequest>>,
|
|
|
+ ) {
|
|
|
+ let id = collection.id as i64;
|
|
|
+ collection.entries.extend(
|
|
|
+ children
|
|
|
+ .remove(&id)
|
|
|
+ .unwrap_or_default()
|
|
|
+ .into_iter()
|
|
|
+ .map(|child| (child.id, TemplateEntry::Collection(child))),
|
|
|
+ );
|
|
|
+
|
|
|
+ collection.entries.extend(
|
|
|
+ requests
|
|
|
+ .remove(&id)
|
|
|
+ .unwrap_or_default()
|
|
|
+ .into_iter()
|
|
|
+ .map(|req| (req.id, TemplateEntry::Request(req))),
|
|
|
+ );
|
|
|
+
|
|
|
+ for entry in collection.entries.values_mut() {
|
|
|
+ let TemplateEntry::Collection(collection) = entry else {
|
|
|
+ continue;
|
|
|
+ };
|
|
|
+ extend_recursive(collection, children, requests);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- #[derive(Debug, Serialize, Deserialize)]
|
|
|
- pub struct CollectionVariable {
|
|
|
- pub id: i64,
|
|
|
- pub collection_id: i64,
|
|
|
- pub name: String,
|
|
|
- pub value: String,
|
|
|
+ for entry in out.iter_mut() {
|
|
|
+ let TemplateEntry::Collection(collection) = entry else {
|
|
|
+ continue;
|
|
|
+ };
|
|
|
+ extend_recursive(collection, &mut children, &mut child_requests);
|
|
|
}
|
|
|
|
|
|
- #[derive(Debug, Serialize, Deserialize)]
|
|
|
- pub struct HttpRequest {
|
|
|
- pub id: i64,
|
|
|
- pub workspace_id: i64,
|
|
|
- pub collection_id: Option<i64>,
|
|
|
- pub name: String,
|
|
|
- pub method: String,
|
|
|
- pub url: String,
|
|
|
+ Ok(out)
|
|
|
+}
|
|
|
+
|
|
|
+async fn get_environments(
|
|
|
+ db: SqlitePool,
|
|
|
+ workspace_id: i64,
|
|
|
+) -> Result<HashMap<usize, TemplateWorkspaceEnvironment>, String> {
|
|
|
+ let envs = get_workspace_envs(db.clone(), workspace_id).await?;
|
|
|
+
|
|
|
+ let mut out = HashMap::with_capacity(envs.len());
|
|
|
+
|
|
|
+ for env in envs {
|
|
|
+ let variables = get_workspace_env_variables(db.clone(), env.id as i64).await?;
|
|
|
+
|
|
|
+ out.insert(
|
|
|
+ env.id as usize,
|
|
|
+ TemplateWorkspaceEnvironment {
|
|
|
+ id: env.id,
|
|
|
+ name: env.name,
|
|
|
+ variables: variables
|
|
|
+ .into_iter()
|
|
|
+ .map(|v| {
|
|
|
+ (
|
|
|
+ v.name.clone(),
|
|
|
+ TemplateEnvironmentVariable {
|
|
|
+ id: v.id,
|
|
|
+ name: v.name,
|
|
|
+ value: v.value,
|
|
|
+ secret: v.secret,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ })
|
|
|
+ .collect(),
|
|
|
+ },
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
- #[derive(Debug, Serialize, Deserialize)]
|
|
|
- pub struct RequestBody {
|
|
|
- pub id: i64,
|
|
|
- pub request_id: i64,
|
|
|
- pub content_type: String,
|
|
|
- pub body: String,
|
|
|
+ Ok(out)
|
|
|
+}
|
|
|
+
|
|
|
+async fn get_workspace_envs(
|
|
|
+ db: SqlitePool,
|
|
|
+ workspace_id: i64,
|
|
|
+) -> Result<Vec<model::WorkspaceEnv>, String> {
|
|
|
+ match sqlx::query_as!(
|
|
|
+ model::WorkspaceEnv,
|
|
|
+ "SELECT id, workspace_id, name FROM workspace_envs WHERE workspace_id = $1",
|
|
|
+ workspace_id
|
|
|
+ )
|
|
|
+ .fetch_all(&db)
|
|
|
+ .await
|
|
|
+ {
|
|
|
+ Ok(envs) => Ok(envs),
|
|
|
+ Err(e) => Err(e.to_string()),
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- #[derive(Debug, Serialize, Deserialize)]
|
|
|
- pub struct RequestHeader {
|
|
|
- pub id: i64,
|
|
|
- pub request_id: i64,
|
|
|
- pub name: String,
|
|
|
- pub value: String,
|
|
|
+async fn get_workspace_env_variables(
|
|
|
+ db: SqlitePool,
|
|
|
+ env_id: i64,
|
|
|
+) -> Result<Vec<model::WorkspaceEnvVariable>, String> {
|
|
|
+ match sqlx::query_as!(
|
|
|
+ model::WorkspaceEnvVariable,
|
|
|
+ "SELECT id, env_id, name, value, secret FROM workspace_env_variables WHERE env_id = $1",
|
|
|
+ env_id
|
|
|
+ )
|
|
|
+ .fetch_all(&db)
|
|
|
+ .await
|
|
|
+ {
|
|
|
+ Ok(envs) => Ok(envs),
|
|
|
+ Err(e) => Err(e.to_string()),
|
|
|
}
|
|
|
}
|