| Index: milo/appengine/swarming/build.go
 | 
| diff --git a/milo/appengine/swarming/build.go b/milo/appengine/swarming/build.go
 | 
| index c2d9037f8073df2b297c4cc45f911129e593b764..87f7afcd6679f691b81623f2d03d435d635eb395 100644
 | 
| --- a/milo/appengine/swarming/build.go
 | 
| +++ b/milo/appengine/swarming/build.go
 | 
| @@ -25,6 +25,7 @@ import (
 | 
|  	"github.com/luci/luci-go/logdog/common/types"
 | 
|  	"github.com/luci/luci-go/milo/api/resp"
 | 
|  	"github.com/luci/luci-go/milo/appengine/common"
 | 
| +	"github.com/luci/luci-go/milo/appengine/common/model"
 | 
|  	"github.com/luci/luci-go/milo/appengine/logdog"
 | 
|  	"github.com/luci/luci-go/server/auth"
 | 
|  )
 | 
| @@ -264,10 +265,8 @@ func addBuilderLink(c context.Context, build *resp.MiloBuild, tags map[string]st
 | 
|  	bucket := tags["buildbucket_bucket"]
 | 
|  	builder := tags["builder"]
 | 
|  	if bucket != "" && builder != "" {
 | 
| -		build.Summary.ParentLabel = &resp.Link{
 | 
| -			Label: builder,
 | 
| -			URL:   fmt.Sprintf("/buildbucket/%s/%s", bucket, builder),
 | 
| -		}
 | 
| +		build.Summary.ParentLabel = resp.NewLink(
 | 
| +			builder, fmt.Sprintf("/buildbucket/%s/%s", bucket, builder))
 | 
|  	}
 | 
|  }
 | 
|  
 | 
| @@ -408,10 +407,8 @@ func addBuildsetInfo(build *resp.MiloBuild, tags map[string]string) {
 | 
|  		if build.SourceStamp == nil {
 | 
|  			build.SourceStamp = &resp.SourceStamp{}
 | 
|  		}
 | 
| -		build.SourceStamp.Changelist = &resp.Link{
 | 
| -			Label: "Gerrit CL",
 | 
| -			URL:   fmt.Sprintf("https://%s/c/%s/%s", parts[0], parts[1], parts[2]),
 | 
| -		}
 | 
| +		build.SourceStamp.Changelist = resp.NewLink(
 | 
| +			"Gerrit CL", fmt.Sprintf("https://%s/c/%s/%s", parts[0], parts[1], parts[2]))
 | 
|  
 | 
|  	}
 | 
|  }
 | 
| @@ -435,20 +432,14 @@ func addRecipeLink(build *resp.MiloBuild, tags map[string]string) {
 | 
|  			}
 | 
|  			name += " @ " + revision
 | 
|  		}
 | 
| -		build.Summary.Recipe = &resp.Link{
 | 
| -			Label: name,
 | 
| -			URL:   repoURL,
 | 
| -		}
 | 
| +		build.Summary.Recipe = resp.NewLink(name, repoURL)
 | 
|  	}
 | 
|  }
 | 
|  
 | 
|  func addTaskToBuild(c context.Context, server string, sr *swarming.SwarmingRpcsTaskResult, build *resp.MiloBuild) error {
 | 
|  	build.Summary.Label = sr.TaskId
 | 
|  	build.Summary.Type = resp.Recipe
 | 
| -	build.Summary.Source = &resp.Link{
 | 
| -		Label: "Task " + sr.TaskId,
 | 
| -		URL:   taskPageURL(server, sr.TaskId),
 | 
| -	}
 | 
| +	build.Summary.Source = resp.NewLink("Task "+sr.TaskId, taskPageURL(server, sr.TaskId))
 | 
|  
 | 
|  	// Extract more swarming specific information into the properties.
 | 
|  	if props := taskProperties(sr); len(props.Property) > 0 {
 | 
| @@ -463,10 +454,7 @@ func addTaskToBuild(c context.Context, server string, sr *swarming.SwarmingRpcsT
 | 
|  
 | 
|  	// Add a link to the bot.
 | 
|  	if sr.BotId != "" {
 | 
| -		build.Summary.Bot = &resp.Link{
 | 
| -			Label: sr.BotId,
 | 
| -			URL:   botPageURL(server, sr.BotId),
 | 
| -		}
 | 
| +		build.Summary.Bot = resp.NewLink(sr.BotId, botPageURL(server, sr.BotId))
 | 
|  	}
 | 
|  
 | 
|  	return nil
 | 
| @@ -538,7 +526,7 @@ func (bl *buildLoader) newEmptyAnnotationStream(c context.Context, addr *types.S
 | 
|  // to add information that would've otherwise been in the annotation stream.
 | 
|  func failedToStart(c context.Context, build *resp.MiloBuild, res *swarming.SwarmingRpcsTaskResult, host string) error {
 | 
|  	var err error
 | 
| -	build.Summary.Status = resp.InfraFailure
 | 
| +	build.Summary.Status = model.InfraFailure
 | 
|  	build.Summary.Started, err = time.Parse(SwarmingTimeLayout, res.StartedTs)
 | 
|  	if err != nil {
 | 
|  		return err
 | 
| @@ -548,7 +536,7 @@ func failedToStart(c context.Context, build *resp.MiloBuild, res *swarming.Swarm
 | 
|  		return err
 | 
|  	}
 | 
|  	build.Summary.Duration = build.Summary.Finished.Sub(build.Summary.Started)
 | 
| -	infoComp := infoComponent(resp.InfraFailure,
 | 
| +	infoComp := infoComponent(model.InfraFailure,
 | 
|  		"LogDog stream not found", "Job likely failed to start.")
 | 
|  	infoComp.Started = build.Summary.Started
 | 
|  	infoComp.Finished = build.Summary.Finished
 | 
| @@ -637,17 +625,17 @@ func (bl *buildLoader) swarmingBuildImpl(c context.Context, svc swarmingService,
 | 
|  					return &build, err
 | 
|  				}
 | 
|  				logging.WithError(err).Errorf(c, "User cannot access stream.")
 | 
| -				build.Components = append(build.Components, infoComponent(resp.Running,
 | 
| +				build.Components = append(build.Components, infoComponent(model.Running,
 | 
|  					"Waiting...", "waiting for annotation stream"))
 | 
|  
 | 
|  			case coordinator.ErrNoAccess:
 | 
|  				logging.WithError(err).Errorf(c, "User cannot access stream.")
 | 
| -				build.Components = append(build.Components, infoComponent(resp.Failure,
 | 
| +				build.Components = append(build.Components, infoComponent(model.Failure,
 | 
|  					"No Access", "no access to annotation stream"))
 | 
|  
 | 
|  			default:
 | 
|  				logging.WithError(err).Errorf(c, "Failed to load LogDog annotation stream.")
 | 
| -				build.Components = append(build.Components, infoComponent(resp.InfraFailure,
 | 
| +				build.Components = append(build.Components, infoComponent(model.InfraFailure,
 | 
|  					"Error", "failed to load annotation stream"))
 | 
|  			}
 | 
|  		}
 | 
| @@ -659,11 +647,10 @@ func (bl *buildLoader) swarmingBuildImpl(c context.Context, svc swarmingService,
 | 
|  		var err error
 | 
|  		lds, err = streamsFromAnnotatedLog(c, fr.log)
 | 
|  		if err != nil {
 | 
| -			comp := infoComponent(resp.InfraFailure, "Milo annotation parser", err.Error())
 | 
| -			comp.SubLink = append(comp.SubLink, resp.LinkSet{&resp.Link{
 | 
| -				Label: "swarming task",
 | 
| -				URL:   taskPageURL(svc.getHost(), taskID),
 | 
| -			}})
 | 
| +			comp := infoComponent(model.InfraFailure, "Milo annotation parser", err.Error())
 | 
| +			comp.SubLink = append(comp.SubLink, resp.LinkSet{
 | 
| +				resp.NewLink("swarming task", taskPageURL(svc.getHost(), taskID)),
 | 
| +			})
 | 
|  			build.Components = append(build.Components, comp)
 | 
|  		}
 | 
|  
 | 
| @@ -691,7 +678,7 @@ func (bl *buildLoader) swarmingBuildImpl(c context.Context, svc swarmingService,
 | 
|  	return &build, nil
 | 
|  }
 | 
|  
 | 
| -func infoComponent(st resp.Status, label, text string) *resp.BuildComponent {
 | 
| +func infoComponent(st model.Status, label, text string) *resp.BuildComponent {
 | 
|  	return &resp.BuildComponent{
 | 
|  		Type:   resp.Summary,
 | 
|  		Label:  label,
 | 
| @@ -762,20 +749,14 @@ func (b swarmingURLBuilder) BuildLink(l *miloProto.Link) *resp.Link {
 | 
|  		} else {
 | 
|  			u.Path = strings.TrimSuffix(u.Path, "/") + "/" + ls.Name
 | 
|  		}
 | 
| -		link := resp.Link{
 | 
| -			Label: l.Label,
 | 
| -			URL:   u.String(),
 | 
| -		}
 | 
| +		link := resp.NewLink(l.Label, u.String())
 | 
|  		if link.Label == "" {
 | 
|  			link.Label = ls.Name
 | 
|  		}
 | 
| -		return &link
 | 
| +		return link
 | 
|  
 | 
|  	case *miloProto.Link_Url:
 | 
| -		return &resp.Link{
 | 
| -			Label: l.Label,
 | 
| -			URL:   t.Url,
 | 
| -		}
 | 
| +		return resp.NewLink(l.Label, t.Url)
 | 
|  
 | 
|  	default:
 | 
|  		return nil
 | 
| 
 |