From 1c1c68a8af16bf37a62e19b8560a0e29b57d60cd Mon Sep 17 00:00:00 2001 From: Achim Rohn Date: Sun, 5 Apr 2026 11:53:23 +0200 Subject: [PATCH] Improved workflow styling --- starter/static/styles.css | 219 ++++++++++++++++++ .../workflow_executions.go | 50 ++-- 2 files changed, 251 insertions(+), 18 deletions(-) diff --git a/starter/static/styles.css b/starter/static/styles.css index 6e2388d..6f18d24 100644 --- a/starter/static/styles.css +++ b/starter/static/styles.css @@ -512,3 +512,222 @@ a.disabled { } } + +/* Workflow Executions Styles */ +.executions-container { + display: flex; + flex-direction: column; + gap: 30px; + margin-top: 30px; +} + +.execution-card { + background: #ffffff; + border-radius: 12px; + padding: 24px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1); + border: 1px solid #edf2f7; +} + +.execution-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + border-bottom: 1px solid #edf2f7; + padding-bottom: 12px; +} + +.execution-id { + font-size: 0.9rem; + color: #718096; + font-family: monospace; +} + +.steps-flow { + display: flex; + align-items: flex-start; + gap: 0; + overflow-x: auto; + padding: 20px 0; + scrollbar-width: thin; +} + +.step-node { + display: flex; + flex-direction: column; + align-items: center; + min-width: 150px; + position: relative; +} + +.step-connector { + flex-grow: 1; + height: 2px; + background: #e2e8f0; + margin-top: 25px; + min-width: 40px; + position: relative; +} + +.step-connector::after { + content: '▶'; + position: absolute; + right: -5px; + top: -7px; + color: #e2e8f0; + font-size: 12px; +} + +.step-content { + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 8px; + padding: 12px; + width: 140px; + text-align: center; + transition: all 0.2s ease; + cursor: pointer; + position: relative; + z-index: 1; +} + +.step-node:hover .step-content { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.step-name { + font-weight: 600; + font-size: 0.85rem; + margin-bottom: 4px; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.step-status-badge { + font-size: 0.7rem; + padding: 2px 8px; + border-radius: 10px; + text-transform: uppercase; + font-weight: bold; +} + +/* Status colors */ +.status-completed .step-content { border-color: #48bb78; background-color: #f0fff4; } +.status-completed .step-status-badge { background: #48bb78; color: white; } +.status-completed + .step-connector { background: #48bb78; } +.status-completed + .step-connector::after { color: #48bb78; } + +.status-failed .step-content { border-color: #f56565; background-color: #fff5f5; } +.status-failed .step-status-badge { background: #f56565; color: white; } + +.status-in_progress .step-content { border-color: #4299e1; background-color: #ebf8ff; } +.status-in_progress .step-status-badge { background: #4299e1; color: white; } + +.status-pending .step-content { border-color: #a0aec0; background-color: #f7fafc; } +.status-pending .step-status-badge { background: #a0aec0; color: white; } + +.step-details { + margin-top: 10px; + font-size: 0.75rem; + color: #4a5568; + width: 100%; +} + +.step-details details { + margin-top: 5px; + text-align: left; +} + +.step-details summary { + cursor: pointer; + color: #4a90e2; +} + +.step-details pre { + background: #1a202c; + color: #e2e8f0; + padding: 8px; + border-radius: 4px; + margin-top: 4px; + white-space: pre-wrap; + word-break: break-all; + max-height: 150px; + overflow-y: auto; +} + +.trigger-form-card { + background: #ffffff; + border-radius: 12px; + padding: 24px; + margin-bottom: 30px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + +.form-row { + display: flex; + gap: 12px; +} + +.form-row input[type="text"] { + flex-grow: 1; + padding: 12px 16px; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.2s; +} + +.form-row input[type="text"]:focus { + outline: none; + border-color: #667eea; +} + +.form-row button { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + font-weight: bold; + cursor: pointer; + transition: transform 0.2s; +} + +.form-row button:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +@media (max-width: 768px) { + .steps-flow { + flex-direction: column; + align-items: stretch; + } + .step-node { + flex-direction: row; + width: 100%; + min-width: unset; + gap: 20px; + } + .step-connector { + width: 2px; + height: 30px; + min-width: unset; + margin-top: 0; + margin-left: 70px; /* half of step-content width (140/2) */ + } + .step-connector::after { + content: '▼'; + right: -5px; + bottom: -10px; + top: unset; + } + .step-content { + width: 100%; + max-width: 200px; + } +} diff --git a/starter/workflow_executions/workflow_executions.go b/starter/workflow_executions/workflow_executions.go index 84af99c..00bbe3b 100644 --- a/starter/workflow_executions/workflow_executions.go +++ b/starter/workflow_executions/workflow_executions.go @@ -104,12 +104,14 @@ func (p *Page) View(c HtmxContext) { 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))), + return Div(Class("trigger-form-card"), + 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))), + ), ), ) } @@ -128,22 +130,34 @@ func (p *Page) executionItem(workflowId string, jobs []*ent.GeneralQueue) Node { 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))))), + jobNodes := make([]Node, 0, len(jobs)*2) + for i, j := range jobs { + statusClass := fmt.Sprintf("status-%s", j.Status) + + jobNodes = append(jobNodes, Div(Class("step-node "+statusClass), + Div(Class("step-content"), + Span(Class("step-name"), Text(j.Name)), + Span(Class("step-status-badge"), Text(string(j.Status))), + ), + Div(Class("step-details"), + If(j.ErrorMessage != "", P(Class("error-message"), Text(fmt.Sprintf("Error: %s", j.ErrorMessage)))), + If(len(j.Payload) > 0, Details(Summary(Text("Payload")), Pre(Text(fmt.Sprintf("%+v", j.Payload))))), + If(len(j.ResultPayload) > 0, Details(Summary(Text("Result")), Pre(Text(fmt.Sprintf("%+v", j.ResultPayload))))), + ), )) + + // Add connector if not last + if i < len(jobs)-1 { + jobNodes = append(jobNodes, Div(Class("step-connector "+statusClass))) + } } return Div(Class("execution-card"), - H3(Text(fmt.Sprintf("Execution: %s", workflowId))), - Div(Class("steps-list"), Group(jobNodes)), - Hr(), + Div(Class("execution-header"), + H3(Text("Workflow Execution")), + Span(Class("execution-id"), Text(fmt.Sprintf("ID: %s", workflowId))), + ), + Div(Class("steps-flow"), Group(jobNodes)), ) }