master
go 256 lines 7.13 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package funcapi
4
5 // DefaultMaxQueryLength is the default maximum length for query text display.
6 const DefaultMaxQueryLength = 4096
7
8 // ColumnSet wraps a typed column slice with its accessor function.
9 // This allows all builder methods to work without repeating the accessor.
10 //
11 // Usage:
12 //
13 // cs := funcapi.Columns(cols, func(c myColumn) funcapi.ColumnMeta { return c.ColumnMeta })
14 // response := &funcapi.FunctionResponse{
15 // Columns: cs.BuildColumns(),
16 // Charts: cs.BuildCharts(),
17 // DefaultCharts: cs.BuildDefaultCharts(),
18 // GroupBy: cs.BuildGroupBy(),
19 // }
20 type ColumnSet[T any] struct {
21 cols []T
22 getMeta func(T) ColumnMeta
23 }
24
25 // Columns creates a new ColumnSet from a typed slice and accessor.
26 func Columns[T any](cols []T, getMeta func(T) ColumnMeta) ColumnSet[T] {
27 return ColumnSet[T]{cols: cols, getMeta: getMeta}
28 }
29
30 // Len returns the number of columns.
31 func (cs ColumnSet[T]) Len() int {
32 return len(cs.cols)
33 }
34
35 // BuildColumns builds the columns map for FunctionResponse.
36 func (cs ColumnSet[T]) BuildColumns() map[string]any {
37 result := make(map[string]any, len(cs.cols))
38 for i, col := range cs.cols {
39 meta := cs.getMeta(col)
40 vis := meta.Visualization
41 if vis == FieldVisualValue && meta.Type == FieldTypeDuration {
42 vis = FieldVisualBar
43 }
44 c := Column{
45 Index: i,
46 Name: meta.Tooltip,
47 Type: meta.Type,
48 Units: meta.Units,
49 Visualization: vis,
50 Sort: meta.Sort,
51 Sortable: meta.Sortable,
52 Sticky: meta.Sticky,
53 Summary: meta.Summary,
54 Filter: meta.Filter,
55 FullWidth: meta.FullWidth,
56 Wrap: meta.Wrap,
57 DefaultExpandedFilter: meta.ExpandFilter,
58 UniqueKey: meta.UniqueKey,
59 Visible: meta.Visible,
60 ValueOptions: ValueOptions{
61 Transform: meta.Transform,
62 DecimalPoints: meta.DecimalPoints,
63 },
64 }
65 result[meta.Name] = c.BuildColumn()
66 }
67 return result
68 }
69
70 // BuildCharts builds chart configuration from column metadata.
71 // Columns with Chart != nil are grouped by Chart.Group.
72 func (cs ColumnSet[T]) BuildCharts() map[string]ChartConfig {
73 charts := make(map[string]ChartConfig)
74 for _, col := range cs.cols {
75 meta := cs.getMeta(col)
76 if meta.Chart == nil || meta.Chart.Group == "" {
77 continue
78 }
79 cfg, ok := charts[meta.Chart.Group]
80 if !ok {
81 title := meta.Chart.Title
82 if title == "" {
83 title = meta.Chart.Group
84 }
85 cfg = ChartConfig{Name: title, Type: "stacked-bar"}
86 }
87 cfg.Columns = append(cfg.Columns, meta.Name)
88 charts[meta.Chart.Group] = cfg
89 }
90 return charts
91 }
92
93 // BuildGroupBy builds group-by configuration from columns with GroupBy != nil.
94 func (cs ColumnSet[T]) BuildGroupBy() map[string]GroupByConfig {
95 result := make(map[string]GroupByConfig)
96 for _, col := range cs.cols {
97 meta := cs.getMeta(col)
98 if meta.GroupBy == nil {
99 continue
100 }
101 result[meta.Name] = GroupByConfig{
102 Name: "Group by " + meta.Tooltip,
103 Columns: []string{meta.Name},
104 }
105 }
106 return result
107 }
108
109 // BuildDefaultCharts builds default chart configurations.
110 // Each chart uses its own DefaultGroupBy if set, otherwise falls back to global default.
111 func (cs ColumnSet[T]) BuildDefaultCharts() DefaultCharts {
112 globalGroupBy := cs.FindDefaultGroupBy()
113 groups := cs.FindDefaultChartGroups()
114 var result DefaultCharts
115 for _, g := range groups {
116 groupBy := cs.findChartGroupBy(g)
117 if groupBy == "" {
118 groupBy = globalGroupBy
119 }
120 if groupBy == "" {
121 continue
122 }
123 result = append(result, DefaultChart{Chart: g, GroupBy: groupBy})
124 }
125 return result
126 }
127
128 // findChartGroupBy returns the DefaultGroupBy for a specific chart group.
129 func (cs ColumnSet[T]) findChartGroupBy(chartGroup string) string {
130 for _, col := range cs.cols {
131 meta := cs.getMeta(col)
132 if meta.Chart != nil && meta.Chart.Group == chartGroup && meta.Chart.DefaultGroupBy != "" {
133 return meta.Chart.DefaultGroupBy
134 }
135 }
136 return ""
137 }
138
139 // BuildCharting builds the complete charting configuration.
140 func (cs ColumnSet[T]) BuildCharting() ChartingConfig {
141 return ChartingConfig{
142 Charts: cs.BuildCharts(),
143 DefaultCharts: cs.BuildDefaultCharts(),
144 GroupBy: cs.BuildGroupBy(),
145 }
146 }
147
148 // FindDefaultGroupBy finds the default grouping column name.
149 // Returns the column with GroupBy.IsDefault, or falls back to any GroupBy column.
150 func (cs ColumnSet[T]) FindDefaultGroupBy() string {
151 for _, col := range cs.cols {
152 meta := cs.getMeta(col)
153 if meta.GroupBy != nil && meta.GroupBy.IsDefault {
154 return meta.Name
155 }
156 }
157 for _, col := range cs.cols {
158 meta := cs.getMeta(col)
159 if meta.GroupBy != nil {
160 return meta.Name
161 }
162 }
163 return ""
164 }
165
166 // FindDefaultChartGroups returns chart groups, preferring ones with IsDefault.
167 func (cs ColumnSet[T]) FindDefaultChartGroups() []string {
168 var groups []string
169 seen := make(map[string]bool)
170
171 // First pass: chart groups marked as default
172 for _, col := range cs.cols {
173 meta := cs.getMeta(col)
174 if meta.Chart != nil && meta.Chart.Group != "" && meta.Chart.IsDefault && !seen[meta.Chart.Group] {
175 seen[meta.Chart.Group] = true
176 groups = append(groups, meta.Chart.Group)
177 }
178 }
179 if len(groups) > 0 {
180 return groups
181 }
182
183 // Fallback: all chart groups
184 for _, col := range cs.cols {
185 meta := cs.getMeta(col)
186 if meta.Chart != nil && meta.Chart.Group != "" && !seen[meta.Chart.Group] {
187 seen[meta.Chart.Group] = true
188 groups = append(groups, meta.Chart.Group)
189 }
190 }
191 return groups
192 }
193
194 // ContainsColumn checks if a column name exists in the set.
195 func (cs ColumnSet[T]) ContainsColumn(name string) bool {
196 for _, col := range cs.cols {
197 if cs.getMeta(col).Name == name {
198 return true
199 }
200 }
201 return false
202 }
203
204 // Names returns all column names.
205 func (cs ColumnSet[T]) Names() []string {
206 names := make([]string, len(cs.cols))
207 for i, col := range cs.cols {
208 names[i] = cs.getMeta(col).Name
209 }
210 return names
211 }
212
213 // SortableColumn is an interface for columns that can provide sort options.
214 // Collectors implement this on their column types to use BuildSortParam.
215 type SortableColumn interface {
216 IsSortOption() bool
217 SortLabel() string
218 IsDefaultSort() bool
219 ColumnName() string
220 SortColumn() string // Returns the column value for sorting; empty string uses ColumnName()
221 }
222
223 // BuildSortParam builds a sort parameter configuration from sortable columns.
224 // Only columns where IsSortOption() returns true are included as options.
225 // Uses SortColumn() for the Column value if non-empty, otherwise ColumnName().
226 func BuildSortParam[T SortableColumn](cols []T) ParamConfig {
227 var options []ParamOption
228 sortDir := FieldSortDescending
229 for _, col := range cols {
230 if !col.IsSortOption() {
231 continue
232 }
233 column := col.SortColumn()
234 if column == "" {
235 column = col.ColumnName()
236 }
237 opt := ParamOption{
238 ID: col.ColumnName(),
239 Column: column,
240 Name: col.SortLabel(),
241 Sort: &sortDir,
242 }
243 if col.IsDefaultSort() {
244 opt.Default = true
245 }
246 options = append(options, opt)
247 }
248 return ParamConfig{
249 ID: "__sort",
250 Name: "Filter By",
251 Help: "Select the primary sort column",
252 Selection: ParamSelect,
253 Options: options,
254 UniqueView: true,
255 }
256 }