Skip to content

Commit 57bbea3

Browse files
authored
Added sorting configuration on timeline type (#750)
* Added sorting configuration on timeline type * fixed failed unit tests
1 parent 134359b commit 57bbea3

34 files changed

Lines changed: 975 additions & 319 deletions

File tree

.agents/skills/khi_parser/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,11 @@ var (
188188
"dns",
189189
0.6,
190190
style.ColorWhite,
191-
style.ColorWhite,
191+
style.ColorBlack,
192192
style.MustForceConvertSRGBHex("#4285F4"),
193193
true,
194194
1000,
195+
style.AlphabeticalSortPolicy(),
195196
)
196197

197198
// VerbCustomAppProcess is the verb style for Custom App state updates.

pkg/generated/khifile/v6/style.pb.go

Lines changed: 264 additions & 91 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/model/khifile/v6/style/timeline.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,61 @@ func (c Color) toProto() *pb.HDRColor4 {
129129
}
130130
}
131131

132+
// TimelineSortOpt is an interface to configure the sorting policy of a timeline type.
133+
type TimelineSortOpt interface {
134+
// ApplyToTimelineType applies the sort policy configuration to the given TimelineType.
135+
ApplyToTimelineType(t *pb.TimelineType)
136+
}
137+
138+
type alphabeticalSortOpt struct {
139+
prioritizedNames []string
140+
}
141+
142+
// ApplyToTimelineType applies the alphabetical sort policy configuration to the given TimelineType.
143+
func (o *alphabeticalSortOpt) ApplyToTimelineType(t *pb.TimelineType) {
144+
t.SortPolicyConfig = &pb.TimelineType_AlphabeticalPolicy{
145+
AlphabeticalPolicy: &pb.AlphabeticalSortPolicy{
146+
PrioritizedNames: o.prioritizedNames,
147+
},
148+
}
149+
}
150+
151+
var _ TimelineSortOpt = (*alphabeticalSortOpt)(nil)
152+
153+
// AlphabeticalSortPolicy returns a TimelineSortOpt that configures a timeline type to sort its child timelines alphabetically.
154+
// prioritizedNames is a list of timeline names that should appear first in the specified order.
155+
func AlphabeticalSortPolicy(prioritizedNames ...string) TimelineSortOpt {
156+
return &alphabeticalSortOpt{
157+
prioritizedNames: prioritizedNames,
158+
}
159+
}
160+
161+
type chronologicalSortOpt struct {
162+
chronologicalSearchDepth int32
163+
}
164+
165+
// ApplyToTimelineType applies the chronological sort policy configuration to the given TimelineType.
166+
func (o *chronologicalSortOpt) ApplyToTimelineType(t *pb.TimelineType) {
167+
t.SortPolicyConfig = &pb.TimelineType_ChronologicalPolicy{
168+
ChronologicalPolicy: &pb.ChronologicalSortPolicy{
169+
ChronologicalSearchDepth: proto.Int32(o.chronologicalSearchDepth),
170+
},
171+
}
172+
}
173+
174+
var _ TimelineSortOpt = (*chronologicalSortOpt)(nil)
175+
176+
// ChronologicalSortPolicy returns a TimelineSortOpt that configures a timeline type to sort its child timelines chronologically.
177+
// chronologicalSearchDepth defines the maximum recursion depth for searching child timelines to find the oldest log entry.
178+
func ChronologicalSortPolicy(chronologicalSearchDepth int32) TimelineSortOpt {
179+
return &chronologicalSortOpt{
180+
chronologicalSearchDepth: chronologicalSearchDepth,
181+
}
182+
}
183+
132184
// MustRegisterTimelineType registers a TimelineType, assigns a unique ID to it,
133185
// and returns the generated pointer. This allows for global inline initialization in plugins.
134-
func MustRegisterTimelineType(label string, description string, icon string, height float32, backgroundColor Color, foregroundColor Color, typeChipBackgroundColor Color, visible bool, sortPriority int32) *pb.TimelineType {
186+
func MustRegisterTimelineType(label string, description string, icon string, height float32, backgroundColor Color, foregroundColor Color, typeChipBackgroundColor Color, visible bool, sortPriority int32, sortOpt TimelineSortOpt) *pb.TimelineType {
135187
if err := backgroundColor.Verify(); err != nil {
136188
panic(fmt.Sprintf("invalid background color for timeline type %q: %v", label, err))
137189
}
@@ -161,6 +213,9 @@ func MustRegisterTimelineType(label string, description string, icon string, hei
161213
Visible: proto.Bool(visible),
162214
SortPriority: proto.Int32(sortPriority),
163215
}
216+
if sortOpt != nil {
217+
sortOpt.ApplyToTimelineType(t)
218+
}
164219
timelineTypes = append(timelineTypes, t)
165220
return t
166221
}

pkg/model/khifile/v6/style/timeline_test.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ import (
2020
"testing"
2121

2222
pb "github.com/GoogleCloudPlatform/khi/pkg/generated/khifile/v6"
23+
"github.com/google/go-cmp/cmp"
24+
"google.golang.org/protobuf/proto"
25+
"google.golang.org/protobuf/testing/protocmp"
2326
)
2427

2528
func TestRegisterTimelineType(t *testing.T) {
2629
reset()
2730

28-
res1 := MustRegisterTimelineType("Type 1", "Desc 1", "icon-1", 1.0, Color{1, 1, 1, 1}, Color{0, 0, 0, 1}, Color{0.5, 0.5, 0.5, 1}, true, 1)
29-
res2 := MustRegisterTimelineType("Type 2", "Desc 2", "icon-2", 1.0, Color{1, 1, 1, 1}, Color{0, 0, 0, 1}, Color{0.5, 0.5, 0.5, 1}, true, 2)
31+
res1 := MustRegisterTimelineType("Type 1", "Desc 1", "icon-1", 1.0, Color{1, 1, 1, 1}, Color{0, 0, 0, 1}, Color{0.5, 0.5, 0.5, 1}, true, 1, nil)
32+
res2 := MustRegisterTimelineType("Type 2", "Desc 2", "icon-2", 1.0, Color{1, 1, 1, 1}, Color{0, 0, 0, 1}, Color{0.5, 0.5, 0.5, 1}, true, 2, nil)
3033

3134
// Verify IDs were assigned starting from 1
3235
if res1.Id == nil || *res1.Id != 1 {
@@ -90,7 +93,7 @@ func TestGenerateChunkHasAllSlices(t *testing.T) {
9093
MustRegisterVerb("Verb", Color{1, 1, 1, 1}, Color{0, 0, 0, 1}, true)
9194
MustRegisterLogType("Log", "Desc", Color{1, 1, 1, 1}, Color{0, 0, 0, 1})
9295
MustRegisterRevisionState("RevState", "icon", "Desc", Color{1, 1, 1, 1}, pb.RevisionStateStyle_REVISION_STATE_STYLE_NORMAL)
93-
MustRegisterTimelineType("Timeline", "Desc", "icon", 1.0, Color{1, 1, 1, 1}, Color{0, 0, 0, 1}, Color{0.5, 0.5, 0.5, 1}, true, 1)
96+
MustRegisterTimelineType("Timeline", "Desc", "icon", 1.0, Color{1, 1, 1, 1}, Color{0, 0, 0, 1}, Color{0.5, 0.5, 0.5, 1}, true, 1, nil)
9497

9598
chunk := GenerateChunk()
9699

@@ -207,7 +210,7 @@ func TestLockRegistry(t *testing.T) {
207210
label: "T",
208211
styleClass: "timeline type",
209212
fn: func() {
210-
MustRegisterTimelineType("T", "D", "icon", 1, Color{1, 1, 1, 1}, Color{0, 0, 0, 1}, Color{0, 0, 0, 1}, true, 10)
213+
MustRegisterTimelineType("T", "D", "icon", 1, Color{1, 1, 1, 1}, Color{0, 0, 0, 1}, Color{0, 0, 0, 1}, true, 10, nil)
211214
},
212215
},
213216
{
@@ -270,3 +273,48 @@ func TestLockRegistry(t *testing.T) {
270273
})
271274
}
272275
}
276+
277+
func TestRegisterTimelineTypeWithSortPolicy(t *testing.T) {
278+
testCases := []struct {
279+
name string
280+
sortOpt TimelineSortOpt
281+
wantType *pb.TimelineType
282+
}{
283+
{
284+
name: "alphabetical sort policy",
285+
sortOpt: AlphabeticalSortPolicy("a", "b"),
286+
wantType: &pb.TimelineType{
287+
SortPolicyConfig: &pb.TimelineType_AlphabeticalPolicy{
288+
AlphabeticalPolicy: &pb.AlphabeticalSortPolicy{
289+
PrioritizedNames: []string{"a", "b"},
290+
},
291+
},
292+
},
293+
},
294+
{
295+
name: "chronological sort policy",
296+
sortOpt: ChronologicalSortPolicy(5),
297+
wantType: &pb.TimelineType{
298+
SortPolicyConfig: &pb.TimelineType_ChronologicalPolicy{
299+
ChronologicalPolicy: &pb.ChronologicalSortPolicy{
300+
ChronologicalSearchDepth: proto.Int32(5),
301+
},
302+
},
303+
},
304+
},
305+
}
306+
307+
for _, tc := range testCases {
308+
t.Run(tc.name, func(t *testing.T) {
309+
reset()
310+
registered := MustRegisterTimelineType("T", "D", "icon", 1, Color{1, 1, 1, 1}, Color{0, 0, 0, 1}, Color{0, 0, 0, 1}, true, 10, tc.sortOpt)
311+
312+
// We only compare the SortPolicyConfig portion
313+
got := registered.SortPolicyConfig
314+
want := tc.wantType.SortPolicyConfig
315+
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
316+
t.Errorf("MustRegisterTimelineType() sort policy config mismatch (-want +got):\n%s", diff)
317+
}
318+
})
319+
}
320+
}

pkg/model/khifile/v6/timeline_registry.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"errors"
1919
"iter"
2020
"sync"
21+
22+
pb "github.com/GoogleCloudPlatform/khi/pkg/generated/khifile/v6"
2123
)
2224

2325
// TimelineRegistry manages the registration and retrieval of TimelineBuilders mapped by their unique TimelinePath.
@@ -120,3 +122,11 @@ func (r *TimelineRegistry) ResolveBuilderFromID(id uint32) *TimelineBuilder {
120122
}
121123
return nil
122124
}
125+
126+
// GetLog retrieves a log entry by its ID from the underlying accumulator.
127+
func (r *TimelineRegistry) GetLog(id uint32) *pb.Log {
128+
if r.logAcc == nil {
129+
return nil
130+
}
131+
return r.logAcc.GetLog(id)
132+
}

pkg/model/khifile/v6/timeline_serializer.go

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package khifilev6
1717
import (
1818
"cmp"
1919
"slices"
20-
"strings"
2120

2221
pb "github.com/GoogleCloudPlatform/khi/pkg/generated/khifile/v6"
2322
)
@@ -54,49 +53,7 @@ func extractTimelineItems(registry *TimelineRegistry) []*pb.TimelineItems {
5453
// extractTimelines iterates over the TimelinePathPool to generate Timeline protobufs
5554
// sorted in parent-to-child (pre-order tree traversal) order.
5655
func extractTimelines(pool *TimelinePathPool, registry *TimelineRegistry) []*pb.Timeline {
57-
// 1. Build parent-to-children adjacency list
58-
parentToChildren := make(map[*TimelinePath][]*TimelinePath)
59-
var roots []*TimelinePath
60-
61-
for path := range pool.Paths() {
62-
if path.Parent == nil {
63-
roots = append(roots, path)
64-
} else {
65-
parentToChildren[path.Parent] = append(parentToChildren[path.Parent], path)
66-
}
67-
}
68-
69-
// 2. Traverse tree in pre-order (depth-first, parent-first)
70-
var sortedPaths []*TimelinePath
71-
var traverse func(p *TimelinePath)
72-
73-
traverse = func(p *TimelinePath) {
74-
sortedPaths = append(sortedPaths, p)
75-
children := parentToChildren[p]
76-
// Sort sibling timelines deterministically by its priority and name
77-
slices.SortFunc(children, func(a, b *TimelinePath) int {
78-
priorityDiff := int(a.Type.GetSortPriority() - b.Type.GetSortPriority())
79-
if priorityDiff != 0 {
80-
return priorityDiff
81-
}
82-
return strings.Compare(a.Name.Resolve(), b.Name.Resolve())
83-
})
84-
for _, child := range children {
85-
traverse(child)
86-
}
87-
}
88-
89-
// Sort root timelines deterministically by its priority and name
90-
slices.SortFunc(roots, func(a, b *TimelinePath) int {
91-
priorityDiff := int(a.Type.GetSortPriority() - b.Type.GetSortPriority())
92-
if priorityDiff != 0 {
93-
return priorityDiff
94-
}
95-
return strings.Compare(a.Name.Resolve(), b.Name.Resolve())
96-
})
97-
for _, r := range roots {
98-
traverse(r)
99-
}
56+
sortedPaths := sortTimelinePaths(pool.Paths(), registry)
10057

10158
// 3. Generate pb.Timeline messages in the sorted order
10259
var timelines []*pb.Timeline

pkg/model/khifile/v6/timeline_serializer_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ func TestExtractTimelinesAndItemsChunkSource(t *testing.T) {
2929
id2 := uint32(2)
3030
priorityA := int32(100)
3131
priorityB := int32(200)
32-
typeA := &pb.TimelineType{Id: &id1, SortPriority: &priorityA}
33-
typeB := &pb.TimelineType{Id: &id2, SortPriority: &priorityB}
32+
typeA := &pb.TimelineType{Id: &id1, SortPriority: &priorityA, SortPolicyConfig: &pb.TimelineType_AlphabeticalPolicy{}}
33+
typeB := &pb.TimelineType{Id: &id2, SortPriority: &priorityB, SortPolicyConfig: &pb.TimelineType_AlphabeticalPolicy{}}
3434

3535
type timelineDef struct {
3636
id string

0 commit comments

Comments
 (0)