package workflow_executions import ( "context" "fmt" "sort" "strings" . "git.gorlug.de/code/ersteller" "git.gorlug.de/code/ersteller/schema/ent" "git.gorlug.de/code/ersteller/schema/ent/generalqueue" "git.gorlug.de/code/ersteller/workflow" hx "maragu.dev/gomponents-htmx" . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" ) const WorkflowExecutionsPath = "/workflow-executions" const WorkflowExecutionsPathDe = "/workflow-ausfuehrungen" var texts *Texts type Texts struct { PageTitle I18nText PageDescription I18nText HeroTitle I18nText TriggerPlaceholder I18nText TriggerButton I18nText } type Page struct { createPage CreateHtmxPageFunc db *ent.Client wf *workflow.Workflow ViewRoute HtmxRoute TriggerRoute HtmxRoute } func NewPage(createPage CreateHtmxPageFunc, server HtmxServer, path *ActivePath, db *ent.Client, wf *workflow.Workflow) *Page { if texts == nil { createTexts() } p := &Page{createPage: createPage, db: db, wf: wf} p.ViewRoute = NewHtmxGetRoute(p.View, LanguagePaths{En: WorkflowExecutionsPath, De: WorkflowExecutionsPathDe}).SetActivePath(path) p.ViewRoute.Add(server) p.TriggerRoute = NewHtmxPostRoute(p.Trigger, LanguagePaths{En: WorkflowExecutionsPath + "/trigger", De: WorkflowExecutionsPathDe + "/trigger"}) p.TriggerRoute.Add(server) return p } func createTexts() { texts = &Texts{ PageTitle: NewI18nText(map[Language]string{En: "Workflow Executions", De: "Workflow-Ausführungen"}), PageDescription: NewI18nText(map[Language]string{En: "Monitor workflow executions", De: "Überwache Workflow-Ausführungen"}), HeroTitle: NewI18nText(map[Language]string{En: "Workflow Executions", De: "Workflow-Ausführungen"}), TriggerPlaceholder: NewI18nText(map[Language]string{En: "Input for workflow", De: "Input für Workflow"}), TriggerButton: NewI18nText(map[Language]string{En: "Trigger Workflow", De: "Workflow starten"}), } } func (p *Page) getMetaData() PageWebsiteMetaData { return PageWebsiteMetaData{ Title: texts.PageTitle, Lang: En, Description: texts.PageDescription, } } func (p *Page) View(c HtmxContext) { language := c.GetLanguage() // Query all jobs ordered by created_at desc jobs, _ := p.db.GeneralQueue.Query(). Order(ent.Desc(generalqueue.FieldCreatedAt)). All(context.Background()) // Group jobs by workflow_id executions := make(map[string][]*ent.GeneralQueue) var workflowIds []string for _, j := range jobs { if j.WorkflowID == "" { continue } if _, ok := executions[j.WorkflowID]; !ok { workflowIds = append(workflowIds, j.WorkflowID) } executions[j.WorkflowID] = append(executions[j.WorkflowID], j) } content := Group{ Div(Class("hero-section"), H1(Class("hero-title"), Text(texts.HeroTitle.FromLang(language))), ), Div(Class("content-section"), p.triggerForm(c), Div(ID("executions-list"), p.executionsList(c, workflowIds, executions)), ), } p.createPage(c, p.getMetaData(), content) } func (p *Page) triggerForm(c HtmxContext) Node { lang := c.GetLanguage() return Form(p.TriggerRoute.GetHtmx(lang), hx.Target("#executions-list"), hx.Swap("outerHTML"), Div(Class("form-row"), Input(Type("text"), Name("input"), Placeholder(texts.TriggerPlaceholder.FromLang(lang))), Button(Type("submit"), Text(texts.TriggerButton.FromLang(lang))), ), ) } func (p *Page) executionsList(c HtmxContext, workflowIds []string, executions map[string][]*ent.GeneralQueue) Node { items := make([]Node, 0, len(workflowIds)) for _, id := range workflowIds { items = append(items, p.executionItem(id, executions[id])) } return Div(Class("executions-container"), Group(items)) } func (p *Page) executionItem(workflowId string, jobs []*ent.GeneralQueue) Node { // Sort jobs by created_at within execution sort.Slice(jobs, func(i, j int) bool { return jobs[i].CreatedAt.Before(jobs[j].CreatedAt) }) jobNodes := make([]Node, 0, len(jobs)) for _, j := range jobs { jobNodes = append(jobNodes, Div(Class("job-step"), H4(Text(fmt.Sprintf("Step: %s", j.Name))), P(Text(fmt.Sprintf("Status: %s", j.Status))), P(Text(fmt.Sprintf("Tries: %d/%d", j.NumberOfTries, j.MaxRetries))), If(j.ErrorMessage != "", P(Class("error-message"), Text(fmt.Sprintf("Error: %s", j.ErrorMessage)))), If(len(j.Payload) > 0, Details(Summary(Text("Payload")), Pre(Class("payload-pre"), Text(fmt.Sprintf("%+v", j.Payload))))), If(len(j.ResultPayload) > 0, Details(Summary(Text("Result")), Pre(Class("result-pre"), Text(fmt.Sprintf("%+v", j.ResultPayload))))), )) } return Div(Class("execution-card"), H3(Text(fmt.Sprintf("Execution: %s", workflowId))), Div(Class("steps-list"), Group(jobNodes)), Hr(), ) } func (p *Page) Trigger(c HtmxContext) { input := strings.TrimSpace(c.GetFormValue("input")) payload := map[string]interface{}{ "workflowId": p.wf.GenerateId(), "input": input, } err := p.wf.Execute(context.Background(), payload) if err != nil { // handle error? } p.renderList(c) } func (p *Page) renderList(c HtmxContext) { jobs, _ := p.db.GeneralQueue.Query(). Order(ent.Desc(generalqueue.FieldCreatedAt)). All(context.Background()) executions := make(map[string][]*ent.GeneralQueue) var workflowIds []string for _, j := range jobs { if j.WorkflowID == "" { continue } if _, ok := executions[j.WorkflowID]; !ok { workflowIds = append(workflowIds, j.WorkflowID) } executions[j.WorkflowID] = append(executions[j.WorkflowID], j) } c.Render(Div(ID("executions-list"), p.executionsList(c, workflowIds, executions))) }