Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions assets/setup-ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,17 @@
'</div>';

app.innerHTML = html;
// Widen the centered container when the form contains a table (kind:
// List) — 7 columns of inputs need more horizontal room than the
// default 620px. Removed when navigating to a non-table form.
var hasTable = questions.some(function (q) {
return q.kind === "List" && q.list_columns && q.list_columns.length > 0;
});
document.body.classList.toggle("has-wide-form", !!hasTable);
restoreFormValues(questions);
if (typeof setupTableQuestions === "function") {
setupTableQuestions(questions);
}
setupVisibility(questions);
app.querySelectorAll("#form-area input, #form-area select, #form-area textarea").forEach(function (el) {
var handler = function () {
Expand Down
6 changes: 6 additions & 0 deletions assets/setup-ui/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
body{font-family:var(--font);background:var(--bg);color:var(--fg);min-height:100dvh;display:flex;justify-content:center;padding:3rem 1rem}
.container{width:100%;max-width:620px}
/* Wider container when the form contains a table (kind: List) so 5–7
columns of inputs aren't squeezed. JS toggles this class on `<body>`
after rendering a question that includes `list_columns`. */
body.has-wide-form .container{max-width:1180px}
.table-wrap{overflow-x:auto}
.row-table{min-width:max-content}

/* ── Animation ── */
.fade-in{animation:fadeIn .25s ease}
Expand Down
7 changes: 7 additions & 0 deletions src/bin/greentic_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,12 @@ fn run_ui_mode(cli: &Cli, i18n: &CliI18n) -> Result<()> {
let bundle_dir = resolve_bundle_source(&bundle_path, i18n)?;
bundle::validate_bundle_exists(&bundle_dir).context(i18n.t("cli.error.invalid_bundle"))?;

// Compute the write-back target up front so that after the UI session
// completes the extracted bundle dir gets re-packed (or copied) back
// to the user's original input — matches the behaviour of
// run_simple_setup which calls gtbundle::create_gtbundle directly.
let output_target = setup_output_target(&bundle_path)?;

// Load answers from --answers file for UI pre-fill (values + scope).
let (prefill_answers, answers_tenant, answers_team, answers_env) = if let Some(answers_path) =
&cli.answers
Expand Down Expand Up @@ -375,6 +381,7 @@ fn run_ui_mode(cli: &Cli, i18n: &CliI18n) -> Result<()> {
cli.locale.as_deref(),
prefill_answers,
scope_from_answers,
output_target,
))
}

Expand Down
1 change: 1 addition & 0 deletions src/cli_helpers/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub fn resolve_bundle_source(path: &std::path::Path, i18n: &CliI18n) -> Result<P
}

/// Persistent output target for simple setup flows.
#[derive(Clone)]
pub enum SetupOutputTarget {
Directory(PathBuf),
Archive(PathBuf),
Expand Down
75 changes: 73 additions & 2 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod assets;
use std::path::{Path, PathBuf};
use std::sync::Mutex;

use anyhow::Result;
use anyhow::{Context, Result};
use axum::extract::State;
use axum::http::header;
use axum::response::IntoResponse;
Expand Down Expand Up @@ -43,6 +43,12 @@ struct UiState {
/// When true the tenant/env came from an answers file and should not be
/// overridden by bundle auto-detection.
scope_from_answers: bool,
/// Where the on-disk artifact should be written back after a successful
/// setup. `Some(Archive)` means re-pack the extracted bundle dir into
/// a `.gtbundle`; `Some(Directory)` means copy the dir; `None` means
/// the user passed a directory and the working dir IS the artifact, so
/// no copy/repack is needed.
output_target: Option<crate::cli_helpers::SetupOutputTarget>,
shutdown_tx: broadcast::Sender<()>,
#[allow(dead_code)]
result: Mutex<Option<ExecutionResult>>,
Expand Down Expand Up @@ -159,6 +165,7 @@ pub async fn launch(
locale: Option<&str>,
prefill_answers: Option<JsonMap<String, Value>>,
scope_from_answers: bool,
output_target: Option<crate::cli_helpers::SetupOutputTarget>,
) -> Result<()> {
let (shutdown_tx, _) = broadcast::channel::<()>(1);

Expand All @@ -171,6 +178,7 @@ pub async fn launch(
locale: locale.map(String::from),
prefill_answers,
scope_from_answers,
output_target,
shutdown_tx: shutdown_tx.clone(),
result: Mutex::new(None),
});
Expand Down Expand Up @@ -645,7 +653,8 @@ async fn post_execute(
let _ = crate::platform_setup::persist_tunnel_artifact(&state.bundle_path, &tunnel);
}

let result = tokio::task::spawn_blocking(move || {
let bundle_path_for_repack = bundle_path.clone();
let mut result = tokio::task::spawn_blocking(move || {
execute_setup(&bundle_path, &tenant, team.as_deref(), &env, answers)
})
.await
Expand All @@ -656,6 +665,68 @@ async fn post_execute(
manual_steps: vec![],
});

// After a successful UI setup, re-pack the extracted bundle dir back
// to its original `.gtbundle` archive (or copy it to a directory
// output) so the on-disk artifact reflects the answers the user just
// saved. Without this the simple-mode CLI did the write-back but the
// UI mode silently dropped it — see bin/greentic_setup.rs:run_ui_mode.
if result.success
&& let Some(target) = state.output_target.clone()
{
let repack = tokio::task::spawn_blocking(move || -> Result<String, anyhow::Error> {
use crate::cli_helpers::{SetupOutputTarget, copy_dir_recursive};
use crate::gtbundle;
match target {
SetupOutputTarget::Archive(out) => {
gtbundle::create_gtbundle(&bundle_path_for_repack, &out).with_context(
|| {
format!(
"failed to write configured .gtbundle archive to {}",
out.display()
)
},
)?;
Ok(format!("Configured bundle written to: {}", out.display()))
}
SetupOutputTarget::Directory(out) => {
if out.exists() {
if out.is_dir() {
std::fs::remove_dir_all(&out).with_context(|| {
format!(
"failed to replace existing bundle directory {}",
out.display()
)
})?;
} else {
std::fs::remove_file(&out).with_context(|| {
format!("failed to replace existing bundle file {}", out.display())
})?;
}
}
copy_dir_recursive(&bundle_path_for_repack, &out, false)
.context("failed to write configured local bundle directory")?;
Ok(format!("Configured bundle written to: {}", out.display()))
}
}
})
.await;
match repack {
Ok(Ok(msg)) => result.stdout.push_str(&format!("\n{msg}\n")),
Ok(Err(e)) => {
result.success = false;
result
.stderr
.push_str(&format!("\nWrite-back failed: {e:#}\n"));
}
Err(e) => {
result.success = false;
result
.stderr
.push_str(&format!("\nWrite-back panicked: {e}\n"));
}
}
}

*state.result.lock().unwrap() = Some(result.clone());
Json(result)
}
Expand Down
Loading