Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Launcher handling multiple different agents/config files (esp. for dev and main) #459

Merged
merged 8 commits into from
Feb 29, 2024
72 changes: 72 additions & 0 deletions ui/src-tauri/src/app_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::fs::{File, OpenOptions};
use std::io::prelude::*;
use std::path::PathBuf;
use serde::{Serialize, Deserialize};
use tauri::api::path::home_dir;

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AgentList {
pub name: String,
pub path: PathBuf,
pub bootstrap: Option<PathBuf>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct LauncherState {
pub agent_list: Vec<AgentList>,
pub selected_agent: Option<AgentList>,
}

impl LauncherState {
pub fn save(&mut self) -> std::io::Result<()> {
let path = home_dir().expect("Could not get home dir").join("ad4m-state.json");
let mut file = File::create(&path)?;
let data = serde_json::to_string(&self).unwrap();
file.write_all(data.as_bytes())?;
Ok(())
}

pub fn load() -> std::io::Result<LauncherState> {
let path = home_dir().expect("Could not get home dir").join("ad4m-state.json");
let mut file = OpenOptions::new().read(true).write(true).create(true).open(&path)?;
let mut data = String::new();
file.read_to_string(&mut data)?;

let state = match serde_json::from_str(&data) {
Ok(state) => state,
Err(_) => {
let agent = AgentList {
name: "Main Net".to_string(),
path: PathBuf::from(home_dir().expect("Could not get home dir")).join(".ad4m".to_string()),
bootstrap: None
};

LauncherState {
agent_list: vec![{
agent.clone()
}],
selected_agent: Some(agent)
}
}
};

Ok(state)
}

pub fn add_agent(&mut self, agent: AgentList) {
if !self.is_agent_taken(&agent.name, &agent.path) {
self.agent_list.push(agent);
}
}

pub fn remove_agent(&mut self, agent: AgentList) {
self.agent_list.retain(|a| a.name != agent.name && &a.path != &agent.path);
}

pub fn is_agent_taken(&self, new_name: &str, new_path: &PathBuf) -> bool {
self.agent_list.iter().any(|agent| agent.name == new_name && (&agent.path == new_path))
}
}



40 changes: 40 additions & 0 deletions ui/src-tauri/src/commands/app.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
extern crate remove_dir_all;
use crate::app_state::{AgentList, LauncherState};
use crate::util::create_tray_message_windows;
use crate::{config::data_path, get_main_window};

use remove_dir_all::*;

use tauri::api::path::home_dir;
use tauri::{LogicalSize, Manager};
use tauri::Size;
use tauri_plugin_positioner::{Position, WindowExt};
Expand Down Expand Up @@ -38,6 +40,44 @@ pub fn open_tray(app_handle: tauri::AppHandle) {
}
}

#[tauri::command]
pub fn add_app_agent_state(agent: AgentList) {
let mut state = LauncherState::load().unwrap();

let mut new_agent = agent.clone();

new_agent.path = home_dir().unwrap().join(agent.path);

state.add_agent(new_agent);

state.save().unwrap();
}

#[tauri::command]
pub fn remove_app_agent_state(agent: AgentList) {
let mut state = LauncherState::load().unwrap();

state.remove_agent(agent.clone());

state.save().unwrap();
}

#[tauri::command]
pub fn set_selected_agent(agent: AgentList) {
let mut state = LauncherState::load().unwrap();

state.selected_agent = Some(agent);

state.save().unwrap();
}

#[tauri::command]
pub fn get_app_agent_list() -> Option<String> {
let state = LauncherState::load().unwrap();

serde_json::to_string(&state).ok()
}

#[tauri::command]
#[cfg(feature = "custom-protocol")]
pub fn open_tray_message(app_handle: tauri::AppHandle) {
Expand Down
8 changes: 8 additions & 0 deletions ui/src-tauri/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ pub fn data_path() -> PathBuf {
home_dir().expect("Could not get home dir").join(".ad4m")
}

pub fn data_dev_path() -> PathBuf {
home_dir().expect("Could not get home dir").join(".ad4m-dev")
}

pub fn log_path() -> PathBuf {
data_path().join("ad4m.log")
}

pub fn log_dev_path() -> PathBuf {
data_path().join("ad4m.log")
}

#[cfg(feature = "custom-protocol")]
pub fn app_url() -> String {
"index.html".to_string()
Expand Down
28 changes: 20 additions & 8 deletions ui/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ mod util;
mod system_tray;
mod menu;
mod commands;
mod app_state;

use tauri::Manager;
use crate::app_state::LauncherState;
use crate::commands::proxy::{get_proxy, login_proxy, setup_proxy, stop_proxy};
use crate::commands::state::{get_port, request_credential};
use crate::commands::app::{close_application, close_main_window, clear_state, open_tray, open_tray_message, open_dapp};
use crate::config::data_path;
use crate::commands::app::{add_app_agent_state, get_app_agent_list, remove_app_agent_state, set_selected_agent, close_application, close_main_window, clear_state, open_tray, open_tray_message, open_dapp};
use crate::config::{data_dev_path, data_path};
use crate::config::log_path;
use crate::util::find_port;
use crate::menu::{handle_menu_event, open_logs_folder};
Expand Down Expand Up @@ -109,11 +111,17 @@ fn rlim_execute() {
fn main() {
env::set_var("RUST_LOG", "holochain=warn,wasmer_compiler_cranelift=warn,rust_executor=info,warp::server");

let state = LauncherState::load().unwrap();

let selected_agent = state.selected_agent.clone().unwrap();
let app_path = selected_agent.path;
let bootstrap_path = selected_agent.bootstrap;

#[cfg(not(target_os = "windows"))]
rlim_execute();

if !data_path().exists() {
let _ = fs::create_dir_all(data_path());
if !app_path.exists() {
let _ = fs::create_dir_all(app_path.clone());
}

if log_path().exists() {
Expand Down Expand Up @@ -170,8 +178,8 @@ fn main() {
save_executor_port(free_port);

match rust_executor::init::init(
Some(String::from(data_path().to_str().unwrap())),
None
Some(String::from(app_path.to_str().unwrap())),
bootstrap_path.and_then(|path| path.to_str().map(|s| s.to_string())),
) {
Ok(()) => {
println!("Ad4m initialized sucessfully");
Expand Down Expand Up @@ -208,7 +216,11 @@ fn main() {
clear_state,
open_tray,
open_tray_message,
open_dapp
open_dapp,
add_app_agent_state,
get_app_agent_list,
set_selected_agent,
remove_app_agent_state
])
.setup(move |app| {
// Hides the dock icon
Expand All @@ -225,7 +237,7 @@ fn main() {

let mut config = Ad4mConfig::default();
config.admin_credential = Some(req_credential.to_string());
config.app_data_path = Some(String::from(data_path().to_str().unwrap()));
config.app_data_path = Some(String::from(app_path.to_str().unwrap()));
config.gql_port = Some(free_port);
config.network_bootstrap_seed = None;
config.run_dapp_server = Some(true);
Expand Down
115 changes: 114 additions & 1 deletion ui/src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,17 @@ const Profile = (props: Props) => {
methods: { toggleExpertMode },
} = useContext(Ad4minContext);

const [appState, setAppState] = useState({} as any);

const [trustedAgents, setTrustedAgents] = useState<any[]>([]);

const [trustedAgentModalOpen, settrustedAgentModalOpen] = useState(false);

const [clearAgentModalOpen, setClearAgentModalOpen] = useState(false);
const [showAgentSelection, setShowAgentSelection] = useState(false);
const [createAgent, setCreateAgent] = useState(false);
const [newAgentName, setNewAgentName] = useState("");
const [file, setFile] = useState<File | null>(null);

const [proxy, setProxy] = useState("");

Expand Down Expand Up @@ -89,6 +95,11 @@ const Profile = (props: Props) => {
setPassword(value);
};

const getAppState = useCallback(async () => {
const state = await invoke("get_app_agent_list");
setAppState(JSON.parse(state))
}, []);

const getTrustedAgents = useCallback(async () => {
if (url) {
const client = await buildAd4mClient(url, false);
Expand Down Expand Up @@ -126,7 +137,8 @@ const Profile = (props: Props) => {
useEffect(() => {
fetchCurrentAgentProfile();
getTrustedAgents();
}, [fetchCurrentAgentProfile, getTrustedAgents]);
getAppState();
}, [fetchCurrentAgentProfile, getTrustedAgents, getAppState]);

useEffect(() => {
const getProxy = async () => {
Expand Down Expand Up @@ -195,6 +207,19 @@ const Profile = (props: Props) => {
}
};

const creatAgentFunc = async () => {
await invoke("add_app_agent_state", {
agent: {
name: newAgentName,
path: `.${newAgentName.toLowerCase()}`,
bootstrap: file,
}
});

setCreateAgent(false);
getAppState();
}

return (
<div>
<j-box px="500" my="500">
Expand Down Expand Up @@ -260,6 +285,13 @@ const Profile = (props: Props) => {
</j-button>
</j-box>

<j-box px="500" my="500">
<j-button onClick={() => setShowAgentSelection(true)} full variant="secondary">
<j-icon size="sm" slot="start" name="server"></j-icon>
Select agent
</j-button>
</j-box>

<j-box px="500" my="500">
<j-button
onClick={() => setClearAgentModalOpen(true)}
Expand Down Expand Up @@ -331,6 +363,87 @@ const Profile = (props: Props) => {
</j-modal>
)}

{showAgentSelection && (
<j-modal
size="fullscreen"
open={showAgentSelection}
onToggle={(e: any) => setShowAgentSelection(e.target.open)}
>
<j-box px="400" py="600">
<j-box pb="500">
<j-text nomargin size="600" color="black" weight="600">
Select agent
</j-text>
</j-box>
<j-text>
Disclaimer: After changing the agent you will have to restart the launcher
for it to start using the new agent
</j-text>
<j-box p="200"></j-box>
{
appState.agent_list.map((agent: any) => (
<>
<j-button
full
variant={ agent.path === appState.selected_agent.path ? "primary" : "secondary"}
onClick={() => {
invoke("set_selected_agent", { agent });
getAppState();
setShowAgentSelection(false);
}}
>
{agent.name}
</j-button>
<j-box p="300"></j-box>
</>
))
}
<j-button full onCLick={() => setCreateAgent(true)}>
<j-icon name="plus"></j-icon>
Add new agent
</j-button>
</j-box>
</j-modal>
)}

{
createAgent && (
<j-modal
open={createAgent}
onToggle={(e: any) => setCreateAgent(e.target.open)}
>
<j-box px="400" py="600">
<j-box pb="500">
<j-text nomargin size="600" color="black" weight="600">
Create new agent
</j-text>
</j-box>
<j-input
placeholder="Agent name"
label="Name"
size="lg"
onInput={(e) => setNewAgentName(e.target.value)}
required></j-input>
<j-box p="200"></j-box>
<j-input
label="Bootstrap file path (absolute path)"
size="lg"
placeholder="ex. /path/to/agent-bootstrap.json"
onInput={(e) => setFile(e.target.value)}
required
></j-input>
<j-box p="200"></j-box>
<j-button
variant="primary"
full
onClick={creatAgentFunc}>
Create Agent
</j-button>
</j-box>
</j-modal>
)
}

{clearAgentModalOpen && (
<j-modal
size="fullscreen"
Expand Down