feat(apps): stepped creation wizard, branch previews, and app-creation fixes
This session (frontend focus):
- Rebuild /apps/new as a 4-step wizard (Basics → Configure → Trigger → Review):
WizardRail, SourceKindPicker card grid, AppManifest review, per-step validation,
ConfirmDialog-based unsaved-changes guard.
- Extract lib/workload/sourceForms.ts (single source of truth for source_config)
+ {Image,Compose,Static,Dockerfile}SourceForm + StaticDiscoveryWizard; fold the
/apps/[id] edit form onto the same components (removes the duplication). Add
vitest + sourceForms unit tests.
- Branch preview environments UI: /chain is_preview/preview_branch + a Preview
environments panel on /apps/[id] (per-branch URLs, ConfirmDialog teardown, armed
state); RegistryImagePicker on the registry trigger and the image source.
- Fixes: image-inspect 404 -> admin-gated POST /api/discovery/image/inspect;
conflict-panel blur flicker; friendly localized discovery errors; CPU/Memory
label hints; dashboard + /apps "Total workloads" count only source_kind workloads
(drop stale trigger_kind gate); NPM cert/access-list name cache; EntityPicker
empty-list guard.
- Update CLAUDE.md frontend conventions + add a Build & Test section.
Also captures pre-existing in-progress platform work (not from this session):
workload notifications, Prometheus metrics export, store lockfile, health probes,
backup hardening, and related store/webhook/scheduler changes.
This commit is contained in:
@@ -32,6 +32,23 @@ type Config struct {
|
||||
|
||||
type source struct{}
|
||||
|
||||
// composeRunner is the slice of stack.Compose this plugin actually
|
||||
// drives. Defined locally per the "interfaces where they are used"
|
||||
// idiom so the plugin can be unit-tested without a real docker compose
|
||||
// binary. `*stack.Compose` satisfies it implicitly.
|
||||
type composeRunner interface {
|
||||
Up(ctx context.Context, projectName, yamlPath string) (string, error)
|
||||
Down(ctx context.Context, projectName string, removeVolumes bool) (string, error)
|
||||
Ps(ctx context.Context, projectName, yamlPath string) ([]stack.Service, error)
|
||||
}
|
||||
|
||||
// newComposeRunner returns the runner the plugin should call. Tests
|
||||
// swap this var with a fake; production code never touches it. The
|
||||
// indirection costs one function-pointer dereference per Deploy /
|
||||
// Teardown / Reconcile call — negligible against the docker compose
|
||||
// exec it gates.
|
||||
var newComposeRunner = func() composeRunner { return stack.NewCompose("") }
|
||||
|
||||
func init() { plugin.RegisterSource(&source{}) }
|
||||
|
||||
func (*source) Kind() string { return "compose" }
|
||||
@@ -82,7 +99,7 @@ func (*source) Deploy(ctx context.Context, deps plugin.Deps, w plugin.Workload,
|
||||
return fmt.Errorf("compose source: write yaml: %w", err)
|
||||
}
|
||||
|
||||
compose := stack.NewCompose("")
|
||||
compose := newComposeRunner()
|
||||
out, err := compose.Up(ctx, projectName, yamlPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compose source: docker compose up: %w (output: %s)", err, truncate(out, 1024))
|
||||
@@ -105,7 +122,7 @@ func (*source) Teardown(ctx context.Context, deps plugin.Deps, w plugin.Workload
|
||||
cfg, _ := plugin.SourceConfigOf[Config](w)
|
||||
projectName := composeProjectName(cfg.ComposeProjectName, w)
|
||||
|
||||
compose := stack.NewCompose("")
|
||||
compose := newComposeRunner()
|
||||
if _, err := compose.Down(ctx, projectName, true); err != nil {
|
||||
// Log but proceed — the DB rows must not be orphaned.
|
||||
slog.Warn("compose source: docker compose down", "workload", w.ID, "error", err)
|
||||
@@ -139,7 +156,7 @@ func (*source) Reconcile(ctx context.Context, deps plugin.Deps, w plugin.Workloa
|
||||
projectName := composeProjectName(cfg.ComposeProjectName, w)
|
||||
yamlPath, _ := writeYAMLIfChanged(w.ID, cfg.ComposeYAML)
|
||||
|
||||
compose := stack.NewCompose("")
|
||||
compose := newComposeRunner()
|
||||
services, err := compose.Ps(ctx, projectName, yamlPath)
|
||||
if err != nil {
|
||||
// Likely no compose project running for this workload. Mark
|
||||
@@ -162,7 +179,7 @@ func (*source) Reconcile(ctx context.Context, deps plugin.Deps, w plugin.Workloa
|
||||
|
||||
// syncContainers shares its body with Reconcile minus the missing-row
|
||||
// fallback — Deploy expects compose ps to succeed since `up` just ran.
|
||||
func syncContainers(ctx context.Context, deps plugin.Deps, compose *stack.Compose, w plugin.Workload, projectName, yamlPath string) error {
|
||||
func syncContainers(ctx context.Context, deps plugin.Deps, compose composeRunner, w plugin.Workload, projectName, yamlPath string) error {
|
||||
services, err := compose.Ps(ctx, projectName, yamlPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compose ps: %w", err)
|
||||
@@ -204,7 +221,17 @@ var projectNameSanitizer = regexp.MustCompile(`[^a-z0-9_-]`)
|
||||
|
||||
func composeProjectName(explicit string, w plugin.Workload) string {
|
||||
if explicit != "" {
|
||||
return explicit
|
||||
// Apply the same sanitizer to operator-supplied names so a value
|
||||
// like "--foo" cannot reach the docker CLI and be re-parsed as a
|
||||
// flag. Reuses the canonical lower+[^a-z0-9_-]→"-" + trim path.
|
||||
san := strings.ToLower(explicit)
|
||||
san = projectNameSanitizer.ReplaceAllString(san, "-")
|
||||
san = strings.Trim(san, "-")
|
||||
if san != "" {
|
||||
return san
|
||||
}
|
||||
// Fall through to the derived name if sanitization stripped
|
||||
// everything (operator passed e.g. "---" — degenerate input).
|
||||
}
|
||||
name := strings.ToLower(w.Name)
|
||||
name = projectNameSanitizer.ReplaceAllString(name, "-")
|
||||
|
||||
Reference in New Issue
Block a user