Skip to content

Commit 7b35804

Browse files
committed
feat(fxcore): Add task grouping support to dashboard sidebar
1 parent 7cc4258 commit 7b35804

File tree

7 files changed

+160
-7
lines changed

7 files changed

+160
-7
lines changed

docs/modules/fxcore.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,33 @@ func Register() fx.Option {
380380

381381
Note: you can also use `AsTasks()` to register several tasks at once.
382382

383+
#### Task grouping
384+
385+
If you have many tasks, you can group them in the dashboard sidebar by also implementing the `GroupedTask` interface:
386+
387+
```go title="internal/tasks/example.go"
388+
func (t *ExampleTask) Group() string {
389+
return "my group"
390+
}
391+
```
392+
393+
Tasks sharing the same group name will be collapsed under a single expandable entry in the sidebar, sorted alphabetically by group name alongside standalone tasks.
394+
395+
#### Input customisation
396+
397+
By default, each task is rendered in the dashboard with a single-row textarea and a generic placeholder. You can customise this by implementing the `TaskWithTemplateSettings` interface:
398+
399+
```go title="internal/tasks/example.go"
400+
func (t *ExampleTask) TemplateSettings(settings fxcore.TaskTemplateSettings) fxcore.TaskTemplateSettings {
401+
settings.Placeholder = "Enter a user ID..." // input placeholder text (default: "Optional input...")
402+
settings.DefaultValue = "42" // pre-filled input value (default: "")
403+
settings.Rows = 5 // number of textarea rows (default: 1)
404+
settings.EscapeContent = false // set to false to render result message as HTML (default: true)
405+
406+
return settings
407+
}
408+
```
409+
383410
It'll be then available on the core dashboard for execution:
384411

385412
![](../../assets/images/dash-tasks-light.png#only-light)

fxcore/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ github.com/ankorstore/yokai/httpserver v1.6.0 h1:Xq3Jh1UM8tMQAnCM1wwGgi+Bm9NZwzJ
2020
github.com/ankorstore/yokai/httpserver v1.6.0/go.mod h1:AOCL4cK2bPKrtGFULvOvc8mKHAOw2bLW30CKJra2BB0=
2121
github.com/ankorstore/yokai/log v1.2.0 h1:jiuDiC0dtqIGIOsFQslUHYoFJ1qjI+rOMa6dI1LBf2Y=
2222
github.com/ankorstore/yokai/log v1.2.0/go.mod h1:MVvUcms1AYGo0BT6l88B9KJdvtK6/qGKdgyKVXfbmyc=
23-
github.com/ankorstore/yokai/trace v1.3.0 h1:0ji32oymIcxTmH5h6GRWLo5ypwBbWrZkXRf9rWF9070=
24-
github.com/ankorstore/yokai/trace v1.3.0/go.mod h1:m7EL2MRBilgCtrly5gA4F0jkGSXR2EbG6LsotbTJ4nA=
2523
github.com/ankorstore/yokai/trace v1.4.0 h1:AdEQs/4TEuqOJ9p/EfsQmrtmkSG3pcmE7r/l+FQFxY8=
2624
github.com/ankorstore/yokai/trace v1.4.0/go.mod h1:m7EL2MRBilgCtrly5gA4F0jkGSXR2EbG6LsotbTJ4nA=
2725
github.com/arl/statsviz v0.6.0 h1:jbW1QJkEYQkufd//4NDYRSNBpwJNrdzPahF7ZmoGdyE=

fxcore/module.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ func withHandlers(coreServer *echo.Echo, p FxCoreParam) (*echo.Echo, error) {
521521
"overviewTraceProcessorExpose": overviewTraceProcessorExpose,
522522
"tasksExpose": tasksExpose,
523523
"tasksPath": tasksPath,
524-
"tasksNames": p.TaskRegistry.Names(),
524+
"tasksItems": p.TaskRegistry.ListItems(),
525525
"tasksTemplateSettings": p.TaskRegistry.TemplateSettings(),
526526
"metricsExpose": metricsExpose,
527527
"metricsPath": metricsPath,

fxcore/task.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ type Task interface {
2121
Run(ctx context.Context, input []byte) TaskResult
2222
}
2323

24+
type GroupedTask interface {
25+
Group() string
26+
}
27+
2428
type TaskWithTemplateSettings interface {
2529
TemplateSettings(settings TaskTemplateSettings) TaskTemplateSettings
2630
}
@@ -77,6 +81,38 @@ func (r *TaskRegistry) Names() []string {
7781
return names
7882
}
7983

84+
// TaskListItem represents either a standalone task or a group of tasks in the dashboard sidebar.
85+
type TaskListItem struct {
86+
Name string
87+
IsGroup bool
88+
Tasks []string
89+
}
90+
91+
// ListItems returns the list of TaskListItem for the dashboard sidebar, sorted by name, collapsing
92+
// tasks that share the same group under a single group entry.
93+
func (r *TaskRegistry) ListItems() []TaskListItem {
94+
groups := make(map[string][]string)
95+
var items []TaskListItem
96+
97+
for _, name := range r.Names() {
98+
if gt, ok := r.tasks[name].(GroupedTask); ok {
99+
groups[gt.Group()] = append(groups[gt.Group()], name)
100+
} else {
101+
items = append(items, TaskListItem{Name: name})
102+
}
103+
}
104+
105+
for group, tasks := range groups {
106+
items = append(items, TaskListItem{Name: group, IsGroup: true, Tasks: tasks})
107+
}
108+
109+
sort.Slice(items, func(i, j int) bool {
110+
return items[i].Name < items[j].Name
111+
})
112+
113+
return items
114+
}
115+
80116
func (r *TaskRegistry) TemplateSettings() map[string]TaskTemplateSettings {
81117
settings := make(map[string]TaskTemplateSettings)
82118

fxcore/task_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,47 @@ func TestTaskRegistry(t *testing.T) {
110110
assert.Equal(t, 5, templateSettings.Rows)
111111
assert.False(t, templateSettings.EscapeContent)
112112
})
113+
114+
t.Run("test list items without groups", func(t *testing.T) {
115+
t.Parallel()
116+
117+
registry := createRegistry(t)
118+
items := registry.ListItems()
119+
120+
assert.Equal(t, []fxcore.TaskListItem{
121+
{Name: "error"},
122+
{Name: "success"},
123+
{Name: "template-settings"},
124+
}, items)
125+
})
126+
127+
t.Run("test list items with groups", func(t *testing.T) {
128+
t.Parallel()
129+
130+
cfg, err := config.NewDefaultConfigFactory().Create(
131+
config.WithFilePaths("./testdata/config"),
132+
)
133+
assert.NoError(t, err)
134+
135+
registry := fxcore.NewTaskRegistry(fxcore.TaskRegistryParams{
136+
Tasks: []fxcore.Task{
137+
tasks.NewSuccessTask(cfg), // standalone: "success"
138+
tasks.NewGroupedTask("alpha", "my group"), // group "my group"
139+
tasks.NewGroupedTask("beta", "my group"), // group "my group"
140+
tasks.NewGroupedTask("gamma", "other group"), // group "other group"
141+
tasks.NewErrorTask(), // standalone: "error"
142+
},
143+
})
144+
145+
items := registry.ListItems()
146+
147+
// Items sorted by name: "error", "my group", "other group", "success"
148+
// Tasks within groups are in alphabetical order (from r.Names() iteration)
149+
assert.Equal(t, []fxcore.TaskListItem{
150+
{Name: "error"},
151+
{Name: "my group", IsGroup: true, Tasks: []string{"alpha", "beta"}},
152+
{Name: "other group", IsGroup: true, Tasks: []string{"gamma"}},
153+
{Name: "success"},
154+
}, items)
155+
})
113156
}

fxcore/templates/dashboard.html

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,26 @@
124124
<i class="bi bi-clipboard2-check"></i>&nbsp;&nbsp;Tasks
125125
</div>
126126
<div class="list-group list-group-flush">
127-
{{ range $taskName := .tasksNames }}
128-
{{ $settings := index $.tasksTemplateSettings $taskName }}
129-
<a @click="loadContent" href="#" role="button" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Task {{ $taskName }}" data-title='<i class="bi bi-clipboard2"></i>&nbsp;&nbsp;Task {{ $taskName }}' data-type="task" data-task="{{ $taskName }}" data-view="task" data-placeholder="{{ $settings.Placeholder }}" data-default-value="{{ $settings.DefaultValue }}" data-rows="{{ $settings.Rows }}" data-escape-content="{{ $settings.EscapeContent }}">
130-
<span><i class="bi bi-clipboard2"></i>&nbsp;&nbsp;{{ $taskName }}</span>
127+
{{ range $i, $item := .tasksItems }}
128+
{{ if $item.IsGroup }}
129+
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#task-group-{{ $i }}" role="button" aria-expanded="false" aria-controls="task-group-{{ $i }}">
130+
<span><i class="bi bi-folder2"></i>&nbsp;&nbsp;{{ $item.Name }}</span>
131+
<i class="bi bi-chevron-down"></i>
131132
</a>
133+
<div class="collapse" id="task-group-{{ $i }}">
134+
{{ range $taskName := $item.Tasks }}
135+
{{ $settings := index $.tasksTemplateSettings $taskName }}
136+
<a @click="loadContent" href="#" role="button" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center ps-4" title="Task {{ $taskName }}" data-title='<i class="bi bi-clipboard2"></i>&nbsp;&nbsp;Task {{ $taskName }}' data-type="task" data-task="{{ $taskName }}" data-view="task" data-placeholder="{{ $settings.Placeholder }}" data-default-value="{{ $settings.DefaultValue }}" data-rows="{{ $settings.Rows }}" data-escape-content="{{ $settings.EscapeContent }}">
137+
<span><i class="bi bi-clipboard2"></i>&nbsp;&nbsp;{{ $taskName }}</span>
138+
</a>
139+
{{ end }}
140+
</div>
141+
{{ else }}
142+
{{ $settings := index $.tasksTemplateSettings $item.Name }}
143+
<a @click="loadContent" href="#" role="button" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Task {{ $item.Name }}" data-title='<i class="bi bi-clipboard2"></i>&nbsp;&nbsp;Task {{ $item.Name }}' data-type="task" data-task="{{ $item.Name }}" data-view="task" data-placeholder="{{ $settings.Placeholder }}" data-default-value="{{ $settings.DefaultValue }}" data-rows="{{ $settings.Rows }}" data-escape-content="{{ $settings.EscapeContent }}">
144+
<span><i class="bi bi-clipboard2"></i>&nbsp;&nbsp;{{ $item.Name }}</span>
145+
</a>
146+
{{ end }}
132147
{{ else }}
133148
<a class="list-group-item list-group-item-action disabled" aria-disabled="true">
134149
<i class="bi bi-slash-circle"></i>&nbsp;&nbsp;n/a
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package tasks
2+
3+
import (
4+
"context"
5+
6+
"github.com/ankorstore/yokai/fxcore"
7+
)
8+
9+
var _ fxcore.Task = (*GroupedTask)(nil)
10+
var _ fxcore.GroupedTask = (*GroupedTask)(nil)
11+
12+
type GroupedTask struct {
13+
name string
14+
group string
15+
}
16+
17+
func NewGroupedTask(name, group string) *GroupedTask {
18+
return &GroupedTask{name: name, group: group}
19+
}
20+
21+
func (t *GroupedTask) Name() string {
22+
return t.name
23+
}
24+
25+
func (t *GroupedTask) Group() string {
26+
return t.group
27+
}
28+
29+
func (t *GroupedTask) Run(context.Context, []byte) fxcore.TaskResult {
30+
return fxcore.TaskResult{
31+
Success: true,
32+
Message: "grouped task " + t.name,
33+
}
34+
}

0 commit comments

Comments
 (0)