// Wizard "Novo serviço" (view=new_service): tipo → formulário por tipo,
// espelhando o fluxo do antigo remote-client (Application / Database /
// Compose / Template).
component {
resources {
component(name = SemTemplates) {
text(class = u-muted, content = "Nenhum template encontrado.")
}
}
column(class = content) {
row(class = page-head) {
column(class = page-head__titles) {
text(class = page-head__title, content = "Novo Serviço — @project_name")
text(
class = page-head__subtitle,
content = "Escolha o tipo e configure o serviço antes de criar.",
)
}
row(class = env__add) {
button(class = "btn btn--ghost", on_click = new_service_back, disabled = @busy_new_service_back, text = "‹ Voltar")
button(class = "btn btn--ghost", on_click = new_service_cancel, disabled = @busy_new_service_cancel, text = Cancelar)
}
}
row(class = busy-row, if = @new_service_message, not_equals = "") {
spinner(class = busy-row__spin, if = @new_service_busy)
text(class = u-warn, content = "@new_service_message")
}
// ── Passo 1: tipo de serviço ────────────────────────────────────────
if @new_service_step == "pick_type" {
column(class = form-panel) {
text(class = u-label-cap, content = "ESCOLHA O TIPO DE SERVIÇO")
column(class = kv-list) {
PickerRow(
title = Application,
subtitle = "Web app via Git ou imagem",
button_label = "Selecionar ›",
action = new_service_kind:application,
)
PickerRow(
title = Database,
subtitle = "Banco de dados gerenciado",
button_label = "Selecionar ›",
action = new_service_kind:database,
)
PickerRow(
title = Broker,
subtitle = "Mensageria: Kafka, RabbitMQ, NATS",
button_label = "Selecionar ›",
action = new_service_kind:broker,
)
PickerRow(
title = Compose,
subtitle = "Stack Docker Compose",
button_label = "Selecionar ›",
action = new_service_kind:compose,
)
PickerRow(
title = Template,
subtitle = "A partir de um preset",
button_label = "Selecionar ›",
action = new_service_kind:template,
)
PickerRow(
title = Importar,
subtitle = "Recriar um serviço exportado de outro servidor",
button_label = "Selecionar ›",
action = new_service_kind:import,
)
}
}
}
// ── Importar serviço exportado (handlers/bundle_import.luau) ────────
if @new_service_step == "import_form" {
column(class = form-panel) {
text(class = u-label-cap, content = "IMPORTAR SERVIÇO EXPORTADO")
text(
class = u-muted,
content = """
O serviço é criado no projeto @project_name, sem deploy — revise e
clique em Deploy depois. Só a configuração vem no arquivo; os dados
(volumes, bancos) não.
""",
)
text(class = u-error, if = @import_error, not_equals = "", content = "@import_error")
text(class = u-warn, if = @import_message, not_equals = "", content = "@import_message")
if @import_report == "true" {
row(class = env__add) {
text(class = u-muted, content = "Pacote: @import_file")
button(
class = "btn btn--ghost",
on_click = import_clear,
disabled = @busy_import_clear,
text = "Trocar pacote",
)
}
text(class = u-label-cap, content = "NOME DO SERVIÇO NOVO")
input(class = field__input, :value = import_name, on_change = field:import_name, placeholder = nome)
text(
class = u-error,
if = @import_conflict,
equals = true,
content = "Já existe um serviço com esse nome neste projeto — escolha outro e clique em Atualizar análise.",
)
if @import_has_warnings == "true" {
text(class = u-label-cap, content = "ATENÇÃO")
column(class = kv-list) {
template(:for-each = import_warnings, var = w) {
text(class = u-muted, content = "@w.text")
}
}
}
if @import_has_service_missing == "true" {
text(class = u-label-cap, content = "VARIÁVEIS DO SERVIÇO SEM VALOR (OBRIGATÓRIO)")
text(class = u-muted, content = "Complete o valor depois do = (uma por linha).")
textarea(class = env__editor, font = mono, :value = import_service_text)
}
if @import_has_project_missing == "true" {
text(class = u-label-cap, content = "VARIÁVEIS DO PROJETO SEM VALOR (OBRIGATÓRIO)")
text(class = u-muted, content = "Complete o valor depois do = (uma por linha).")
textarea(class = env__editor, font = mono, :value = import_project_text)
}
if @import_has_secret_missing == "true" {
text(class = u-label-cap, content = "SECRETS QUE ESTE PROJETO NÃO TEM")
text(
class = u-muted,
content = "Informe o valor para criá-los, ou deixe vazio para seguir sem (a referência fica quebrada até você criar o secret).",
)
textarea(class = env__editor, font = mono, :value = import_secret_text)
}
if @import_missing_provider != "" {
text(class = u-label-cap, content = "GIT PROVIDER “@import_missing_provider” NÃO EXISTE AQUI")
text(class = u-muted, content = "Escolha um provider deste servidor, ou siga sem.")
column(class = kv-list) {
template(:for-each = import_providers, var = p) {
button(
class = "btn btn--ghost",
on_click = import_use_provider:@p.id,
disabled = @busy_import_use_provider,
text = "@p.mark @p.label",
)
}
}
}
if @import_has_project_env == "true" {
text(class = u-label-cap, content = "VARIÁVEIS DO PROJETO TRAZIDAS")
text(class = u-muted, content = "Nada do projeto é sobrescrito sem você escolher. Clique para trocar a escolha.")
column(class = kv-list) {
template(:for-each = import_project_env, var = v) {
row(class = kv-list__row) {
text(class = env__key, content = "@v.key")
text(class = u-muted, content = "@v.info")
if @v.can_choose == "true" {
button(
class = "btn btn--ghost",
on_click = import_choice:@v.key,
disabled = @busy_import_choice,
text = "@v.choice",
)
} else {
text(class = u-muted, content = "já existe igual — nada a fazer")
}
}
}
}
}
checkbox(
:checked = import_drop_domains,
on_toggle = field:import_drop_domains,
label = "Não trazer os domínios",
)
checkbox(
:checked = import_deploy,
on_toggle = field:import_deploy,
label = "Fazer deploy logo depois de criar",
)
row(class = env__add) {
button(
class = "btn btn--primary",
on_click = import_create,
disabled = @busy_import_create,
text = "Criar serviço",
)
button(
class = "btn btn--blue btn--wide",
on_click = import_analyze,
disabled = @busy_import_analyze,
text = "Atualizar análise",
)
}
} else {
button(
class = "btn btn--blue btn--wide",
on_click = import_pick_file,
disabled = @busy_import_pick_file,
text = "Escolher arquivo…",
)
text(class = u-label-cap, content = "…OU COLE O TEXTO DO PACOTE")
textarea(
class = env__editor,
font = mono,
:value = import_text,
placeholder = l"""
apiVersion: rustploy/v1
kind: Service
…
""",
)
button(
class = "btn btn--primary",
on_click = import_analyze,
disabled = @busy_import_analyze,
text = Analisar,
)
}
}
}
// ── Passo 2 (Database): escolha do banco ───────────────────────────
if @new_service_step == "pick_db" {
column(class = form-panel) {
text(class = u-label-cap, content = "ESCOLHA O BANCO DE DADOS")
column(class = kv-list) {
PickerRow(
:for-each = new_service_databases,
var = d,
title = @d.label,
subtitle = @d.image,
button_label = "Selecionar ›",
action = new_service_database:@d.id,
)
}
}
}
// ── Passo 2 (Broker): escolha do broker ────────────────────────────
if @new_service_step == "pick_broker" {
column(class = form-panel) {
text(class = u-label-cap, content = "ESCOLHA O BROKER")
column(class = kv-list) {
PickerRow(
:for-each = new_service_brokers,
var = b,
title = @b.label,
subtitle = @b.image,
button_label = "Selecionar ›",
action = new_service_broker:@b.id,
)
}
}
}
// ── Application ─────────────────────────────────────────────────────
if @new_service_step == "app_form" {
form(name = new_service_app, on_submit = new_service_create, width = fill) {
column(class = form-panel) {
text(class = u-label-cap, content = "NOVA APPLICATION")
text(class = u-label-cap, content = "NOME")
input(
placeholder = minha-app,
class = field__input,
form_control = new_service_name,
:value = new_service_name,
on_change = field:new_service_name,
)
text(
if = @error_new_service_name,
not_equals = "",
class = u-error,
content = "@error_new_service_name",
)
text(
class = u-label-cap,
content = "APP NAME (OPCIONAL — usado no lugar do nome)",
)
input(
placeholder = "(opcional)",
class = field__input,
form_control = new_service_app_name,
:value = new_service_app_name,
on_change = field:new_service_app_name,
)
text(
class = u-muted,
content = """
Configure a origem (Git ou imagem) na aba General do serviço
depois de criar.
""",
)
row(class = env__add) {
button(
class = "btn btn--primary",
on_click = new_service_create,
disabled = @busy_new_service_create,
text = "Criar Application",
)
}
}
}
}
// ── Compose ─────────────────────────────────────────────────────────
if @new_service_step == "compose_form" {
form(name = new_service_compose, on_submit = new_service_create, width = fill) {
column(class = form-panel) {
text(class = u-label-cap, content = "NOVA COMPOSE STACK")
text(class = u-label-cap, content = "NOME")
input(
placeholder = minha-stack,
class = field__input,
form_control = new_service_name,
:value = new_service_name,
on_change = field:new_service_name,
)
text(
if = @error_new_service_name,
not_equals = "",
class = u-error,
content = "@error_new_service_name",
)
text(
class = u-label-cap,
content = "APP NAME (OPCIONAL — usado no lugar do nome)",
)
input(
placeholder = "(opcional)",
class = field__input,
form_control = new_service_app_name,
:value = new_service_app_name,
on_change = field:new_service_app_name,
)
text(
class = u-muted,
content = """
Configure o compose file dentro do serviço antes de fazer deploy.
""",
)
row(class = env__add) {
button(
class = "btn btn--primary",
on_click = new_service_create,
disabled = @busy_new_service_create,
text = "Criar Compose Stack",
)
}
}
}
}
// ── Database (form do banco escolhido) ──────────────────────────────
if @new_service_step == "db_form" {
scrollable(direction = vertical, class = u-fill-all) {
form(name = new_service_database, on_submit = new_service_create, width = fill) {
column(class = form-panel) {
text(class = u-label-cap, content = "NOVA @new_service_database_label")
text(class = u-label-cap, content = "NOME")
input(
placeholder = meu-banco,
class = field__input,
form_control = new_service_name,
:value = new_service_name,
on_change = field:new_service_name,
)
text(
if = @error_new_service_name,
not_equals = "",
class = u-error,
content = "@error_new_service_name",
)
if @new_service_database_has_dbname == "true" {
text(class = u-label-cap, content = "DATABASE NAME")
input(
placeholder = app,
class = field__input,
form_control = new_service_database_name,
:value = new_service_database_name,
on_change = field:new_service_database_name,
)
}
if @new_service_database_has_user == "true" {
text(class = u-label-cap, content = "USER")
input(
placeholder = user,
class = field__input,
form_control = new_service_database_user,
:value = new_service_database_user,
on_change = field:new_service_database_user,
)
}
text(class = u-label-cap, content = "PASSWORD (gerada — pode trocar)")
input(
placeholder = "(vazio = sem senha, só no Redis)",
class = field__input,
form_control = new_service_database_password,
:value = new_service_database_password,
on_change = field:new_service_database_password,
)
if @new_service_database_has_rootpw == "true" {
text(
class = u-label-cap,
content = "ROOT PASSWORD (gerada — pode trocar)",
)
input(
placeholder = "",
class = field__input,
form_control = new_service_database_root_password,
:value = new_service_database_root_password,
on_change = field:new_service_database_root_password,
)
}
checkbox(
label = "Use Replica Sets",
if = @new_service_database_has_replica,
equals = true,
:checked = new_service_use_replica,
on_toggle = field:new_service_use_replica,
)
checkbox(
label = "Gerar URL de conexão externa (porta e firewall automáticos)",
:checked = new_service_expose_external,
on_toggle = field:new_service_expose_external,
)
text(
if = @new_service_expose_external,
equals = true,
class = u-muted,
content = """
O serviço ficará acessível pela internet numa porta alocada pelo
rustploy, protegido pela senha acima.
""",
)
text(class = u-label-cap, content = "DOCKER IMAGE")
row(class = kv-list__row) {
text(class = "kv-list__val kv-list__val--compact", content = "@new_service_image")
}
row(class = env__add) {
button(
class = "btn btn--primary",
on_click = new_service_create,
disabled = @busy_new_service_create,
text = "Criar @new_service_database_label",
)
}
}
}
}
}
// ── Template: catálogo ───────────────────────────────────────────────
if @new_service_step == "pick_template" {
column(class = env) {
column(class = healthcheck__field) {
text(class = u-label-cap, content = "BUSCAR")
input(
placeholder = "buscar templates…",
class = field__input,
:value = new_service_template_search,
on_change = new_service_template_search,
)
}
scrollable(direction = vertical, class = u-fill-all) {
column(class = kv-list) {
each @new_service_templates as t (fallback = SemTemplates) {
TemplateRow(spread = @t, action = new_service_pick_template:@t.id)
}
}
}
}
}
// ── Template: configuração ───────────────────────────────────────────
if @new_service_step == "template_form" {
scrollable(direction = vertical, class = u-fill-all) {
form(name = new_service_template, on_submit = new_service_create, width = fill) {
column(class = form-panel) {
text(class = u-label-cap, content = "@new_service_template_name — CONFIGURAR")
text(class = u-label-cap, content = "NOME DO SERVIÇO")
input(
placeholder = @new_service_template_name,
class = field__input,
form_control = new_service_name,
:value = new_service_name,
on_change = field:new_service_name,
)
template(:for-each = new_service_template_vars, var = v) {
column(class = healthcheck__field) {
text(class = u-label-cap, content = "@v.label")
input(
placeholder = @v.placeholder,
class = field__input,
:value = new_service_template_variable_value_@v.idx,
on_change = field:new_service_template_variable_value_@v.idx,
)
}
}
row(class = env__add) {
button(
class = "btn btn--primary",
on_click = new_service_create,
disabled = @busy_new_service_create,
text = "Criar @new_service_template_name",
)
}
}
}
}
}
}
}