Skip to content

Commit 29aca6d

Browse files
authored
feat(workspaces): Add set command to switch default workspace
1 parent 0fa05b2 commit 29aca6d

File tree

5 files changed

+134
-28
lines changed

5 files changed

+134
-28
lines changed

src/auth.rs

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ pub fn status(profile: &str) {
2626
let api_key = match &profile_config.api_key {
2727
Some(key) if key != "PLACEHOLDER" => key.clone(),
2828
_ => {
29-
print_row("Profile", &profile.white().to_string());
3029
print_row("Authenticated", &"No".red().to_string());
3130
print_row("API Key", &"Not configured".red().to_string());
3231
return;
@@ -42,17 +41,18 @@ pub fn status(profile: &str) {
4241
.send()
4342
{
4443
Ok(resp) if resp.status().is_success() => {
45-
print_row("Profile", &profile.white().to_string());
4644
print_row("API URL", &profile_config.api_url.cyan().to_string());
4745
print_row("Authenticated", &"Yes".green().to_string());
4846
print_row("API Key", &format!("{}{source_label}", "Valid".green()));
4947
match profile_config.workspaces.first() {
50-
Some(w) => print_row("Default Workspace", &format!("{} ({})", w.public_id, w.name).cyan().to_string()),
51-
None => print_row("Default Workspace", &"None".dark_grey().to_string()),
48+
Some(w) => {
49+
print_row("Workspace", &format!("{} {}", w.name.as_str().cyan(), format!("({})", w.public_id).dark_grey()));
50+
print_row("", &"use 'hotdata workspaces set' to switch workspaces".dark_grey().to_string());
51+
}
52+
None => print_row("Current Workspace", &"None".dark_grey().to_string()),
5253
}
5354
}
5455
Ok(resp) => {
55-
print_row("Profile", &profile.white().to_string());
5656
print_row("API URL", &profile_config.api_url.cyan().to_string());
5757
print_row("Authenticated", &"No".red().to_string());
5858
print_row(
@@ -230,7 +230,7 @@ pub fn login() {
230230
let entries: Vec<config::WorkspaceEntry> = ws.workspaces.into_iter()
231231
.map(|w| config::WorkspaceEntry { public_id: w.public_id, name: w.name })
232232
.collect();
233-
let first = entries.first().map(|w| format!("{} ({})", w.public_id, w.name));
233+
let first = entries.first().cloned();
234234
let _ = config::save_workspaces("default", entries);
235235
first
236236
} else { None }
@@ -246,24 +246,11 @@ pub fn login() {
246246
.unwrap();
247247

248248
match default_workspace {
249-
Some(id) => {
250-
stdout()
251-
.execute(SetForegroundColor(Color::DarkGrey))
252-
.unwrap()
253-
.execute(Print(format!("Default workspace: {id}\n")))
254-
.unwrap()
255-
.execute(ResetColor)
256-
.unwrap();
257-
}
258-
None => {
259-
stdout()
260-
.execute(SetForegroundColor(Color::DarkGrey))
261-
.unwrap()
262-
.execute(Print("No default workspace configured.\n"))
263-
.unwrap()
264-
.execute(ResetColor)
265-
.unwrap();
249+
Some(w) => {
250+
print_row("Workspace", &format!("{} {}", w.name.as_str().cyan(), format!("({})", w.public_id).dark_grey()));
251+
print_row("", &"use 'hotdata workspaces set' to switch workspaces".dark_grey().to_string());
266252
}
253+
None => print_row("Workspace", &"None".dark_grey().to_string()),
267254
}
268255
}
269256
Ok(r) => {
@@ -309,7 +296,7 @@ fn print_row(label: &str, value: &str) {
309296
stdout()
310297
.execute(SetForegroundColor(Color::DarkGrey))
311298
.unwrap()
312-
.execute(Print(format!("{:<20}", format!("{label}:"))))
299+
.execute(Print(format!("{:<16}", if label.is_empty() { String::new() } else { format!("{label}:") })))
313300
.unwrap()
314301
.execute(ResetColor)
315302
.unwrap()

src/command.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,16 @@ pub enum WorkspaceCommands {
242242
/// List all workspaces
243243
List {
244244
/// Output format
245-
#[arg(long, default_value = "yaml", value_parser = ["table", "json", "yaml"])]
245+
#[arg(long, default_value = "table", value_parser = ["table", "json", "yaml"])]
246246
format: String,
247247
},
248248

249+
/// Set the default workspace
250+
Set {
251+
/// Workspace ID to set as default (omit for interactive selection)
252+
workspace_id: Option<String>,
253+
},
254+
249255
/// Get details for a workspace
250256
Get {
251257
/// Workspace ID (defaults to first workspace from login)

src/config.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,27 @@ pub fn save_workspaces(profile: &str, workspaces: Vec<WorkspaceEntry>) -> Result
150150
fs::write(&config_path, content).map_err(|e| format!("error writing config file: {e}"))
151151
}
152152

153+
pub fn save_default_workspace(profile: &str, workspace: WorkspaceEntry) -> Result<(), String> {
154+
let user_dirs = UserDirs::new().ok_or("could not determine home directory")?;
155+
let config_path = user_dirs.home_dir().join(".hotdata").join("config.yml");
156+
157+
let mut config_file: ConfigFile = if config_path.exists() {
158+
let content = fs::read_to_string(&config_path)
159+
.map_err(|e| format!("error reading config file: {e}"))?;
160+
serde_yaml::from_str(&content).map_err(|e| format!("error parsing config file: {e}"))?
161+
} else {
162+
ConfigFile { profiles: HashMap::new() }
163+
};
164+
165+
let entry = config_file.profiles.entry(profile.to_string()).or_default();
166+
entry.workspaces.retain(|w| w.public_id != workspace.public_id);
167+
entry.workspaces.insert(0, workspace);
168+
169+
let content = serde_yaml::to_string(&config_file)
170+
.map_err(|e| format!("error serializing config: {e}"))?;
171+
fs::write(&config_path, content).map_err(|e| format!("error writing config file: {e}"))
172+
}
173+
153174
pub fn resolve_workspace_id(provided: Option<String>, profile_config: &ProfileConfig) -> Result<String, String> {
154175
if let Some(id) = provided {
155176
return Ok(id);

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ fn main() {
9595
Commands::Profile { .. } => eprintln!("not yet implemented"),
9696
Commands::Workspaces { command } => match command {
9797
WorkspaceCommands::List { format } => workspace::list(&format),
98+
WorkspaceCommands::Set { workspace_id } => workspace::set(workspace_id.as_deref()),
9899
_ => eprintln!("not yet implemented"),
99100
},
100101
Commands::Connections { workspace_id, command } => {

src/workspace.rs

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,94 @@ struct ListResponse {
1515
workspaces: Vec<Workspace>,
1616
}
1717

18+
fn load_client() -> (reqwest::blocking::Client, String, String) {
19+
let profile_config = match config::load("default") {
20+
Ok(c) => c,
21+
Err(e) => {
22+
eprintln!("{e}");
23+
std::process::exit(1);
24+
}
25+
};
26+
let api_key = match &profile_config.api_key {
27+
Some(key) if key != "PLACEHOLDER" => key.clone(),
28+
_ => {
29+
eprintln!("error: not authenticated. Run 'hotdata auth login' to log in.");
30+
std::process::exit(1);
31+
}
32+
};
33+
let api_url = profile_config.api_url.to_string();
34+
(reqwest::blocking::Client::new(), api_key, api_url)
35+
}
36+
37+
fn fetch_all_workspaces(client: &reqwest::blocking::Client, api_key: &str, api_url: &str) -> Vec<Workspace> {
38+
let url = format!("{api_url}/workspaces");
39+
let resp = match client
40+
.get(&url)
41+
.header("Authorization", format!("Bearer {api_key}"))
42+
.send()
43+
{
44+
Ok(r) => r,
45+
Err(e) => {
46+
eprintln!("error connecting to API: {e}");
47+
std::process::exit(1);
48+
}
49+
};
50+
if !resp.status().is_success() {
51+
eprintln!("error: {}", crate::util::api_error(resp.text().unwrap_or_default()));
52+
std::process::exit(1);
53+
}
54+
match resp.json::<ListResponse>() {
55+
Ok(b) => b.workspaces,
56+
Err(e) => {
57+
eprintln!("error parsing response: {e}");
58+
std::process::exit(1);
59+
}
60+
}
61+
}
62+
63+
pub fn set(workspace_id: Option<&str>) {
64+
let (client, api_key, api_url) = load_client();
65+
let workspaces = fetch_all_workspaces(&client, &api_key, &api_url);
66+
67+
let chosen = match workspace_id {
68+
Some(id) => {
69+
match workspaces.iter().find(|w| w.public_id == id) {
70+
Some(w) => config::WorkspaceEntry { public_id: w.public_id.clone(), name: w.name.clone() },
71+
None => {
72+
eprintln!("error: workspace '{id}' not found or you don't have access to it.");
73+
std::process::exit(1);
74+
}
75+
}
76+
}
77+
None => {
78+
if workspaces.is_empty() {
79+
eprintln!("error: no workspaces available.");
80+
std::process::exit(1);
81+
}
82+
let options: Vec<String> = workspaces.iter()
83+
.map(|w| format!("{} ({})", w.name, w.public_id))
84+
.collect();
85+
let selection = match inquire::Select::new("Select default workspace:", options.clone()).prompt() {
86+
Ok(s) => s,
87+
Err(_) => std::process::exit(1),
88+
};
89+
let idx = options.iter().position(|o| o == &selection).unwrap();
90+
let w = &workspaces[idx];
91+
config::WorkspaceEntry { public_id: w.public_id.clone(), name: w.name.clone() }
92+
}
93+
};
94+
95+
if let Err(e) = config::save_default_workspace("default", chosen.clone()) {
96+
eprintln!("error saving config: {e}");
97+
std::process::exit(1);
98+
}
99+
100+
use crossterm::style::Stylize;
101+
println!("{}", "Default workspace updated".green());
102+
println!("id: {}", chosen.public_id);
103+
println!("name: {}", chosen.name);
104+
}
105+
18106
pub fn list(format: &str) {
19107
let profile_config = match config::load("default") {
20108
Ok(c) => c,
@@ -32,6 +120,8 @@ pub fn list(format: &str) {
32120
}
33121
};
34122

123+
let default_id = profile_config.workspaces.first().map(|w| w.public_id.as_str()).unwrap_or("").to_string();
124+
35125
let url = format!("{}/workspaces", profile_config.api_url);
36126
let client = reqwest::blocking::Client::new();
37127

@@ -48,7 +138,7 @@ pub fn list(format: &str) {
48138
};
49139

50140
if !resp.status().is_success() {
51-
eprintln!("error: HTTP {}", resp.status());
141+
eprintln!("error: {}", crate::util::api_error(resp.text().unwrap_or_default()));
52142
std::process::exit(1);
53143
}
54144

@@ -69,9 +159,10 @@ pub fn list(format: &str) {
69159
}
70160
"table" => {
71161
let mut table = crate::util::make_table();
72-
table.set_header(["PUBLIC_ID", "NAME", "ACTIVE", "FAVORITE", "PROVISION_STATUS"]);
162+
table.set_header(["DEFAULT", "PUBLIC_ID", "NAME", "PROVISION_STATUS"]);
73163
for w in &body.workspaces {
74-
table.add_row([&w.public_id, &w.name, &w.active.to_string(), &w.favorite.to_string(), &w.provision_status]);
164+
let marker = if w.public_id == default_id { "*" } else { "" };
165+
table.add_row([marker, &w.public_id, &w.name, &w.provision_status]);
75166
}
76167
println!("{table}");
77168
}

0 commit comments

Comments
 (0)