Merge branch 'rugvip/entity-page' into alund/home-v1

This commit is contained in:
Patrik Oldsberg
2020-02-07 10:11:08 +01:00
37 changed files with 586 additions and 303 deletions
+11
View File
@@ -56,6 +56,10 @@ func (c *Client) ListWorkflowRuns(ctx context.Context, owner, repo string) (*Wor
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Upstream fetch failed with status %d %s", res.StatusCode, res.Status)
}
var resData WorkflowRunsListResponse
if err := json.NewDecoder(res.Body).Decode(&resData); err != nil {
return nil, err
@@ -76,6 +80,13 @@ func (c *Client) GetWorkflowRun(ctx context.Context, owner, repo, runId string)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
if res.StatusCode == http.StatusNotFound {
return nil, nil
}
return nil, fmt.Errorf("Upstream fetch failed with status %d %s", res.StatusCode, res.Status)
}
var resData WorkflowRunResponse
if err := json.NewDecoder(res.Body).Decode(&resData); err != nil {
return nil, err
+5 -1
View File
@@ -55,6 +55,9 @@ func (s *service) GetBuild(ctx context.Context, req *buildsv1.GetBuildRequest) (
if err != nil {
return nil, status.Errorf(codes.Internal, "Failed to fetch workflow run for %s/%s/%s, %s", owner, repo, runID, err)
}
if run == nil {
return nil, status.Errorf(codes.NotFound, "Workflow run %s/%s/%s not found", owner, repo, runID)
}
return &buildsv1.GetBuildReply{
Build: s.transformBuild(owner, repo, run),
@@ -96,6 +99,7 @@ func (s *service) transformBuild(owner, repo string, run *ghactions.WorkflowRunR
Uri: fmt.Sprintf("entity:build:%s/%s/%d", owner, repo, run.ID),
CommitId: run.HeadCommit.ID,
Message: run.HeadCommit.Message,
Branch: run.HeadBranch,
Status: stat,
}
}
@@ -108,7 +112,7 @@ func (s *service) parseBuildURI(uri string) (owner, repo, runID string, err erro
}
match := entityURIRegex.FindStringSubmatch(uri)
if err != nil {
if match == nil {
return "", "", "", fmt.Errorf("uri does not match")
}
+3 -3
View File
@@ -22,9 +22,9 @@ func (s *Server) ListEntities(ctx context.Context, req *pb.ListEntitiesRequest)
return nil, status.Error(codes.Internal, "could not list entities")
}
result := make([]*pb.Entity, len(entities))
for i, v := range entities {
result[i] = &pb.Entity{Uri: v}
var result []*pb.Entity
for _, v := range entities {
result = append(result, &pb.Entity{Uri: v})
}
return &pb.ListEntitiesReply{Entities: result}, nil
+84 -78
View File
@@ -13,12 +13,14 @@ import (
pb "github.com/spotify/backstage/proto/inventory/v1"
)
var entityURI = "boss://test/test"
func TestServerListEntities(t *testing.T) {
testStorage := NewTestStorage()
defer testStorage.Close()
s := Server{Storage: testStorage.Storage}
entity := &pb.Entity{Uri: "boss://test/test"}
entity := &pb.Entity{Uri: entityURI}
_, err := s.CreateEntity(context.Background(), &pb.CreateEntityRequest{Entity: entity})
if err != nil {
@@ -32,7 +34,7 @@ func TestServerListEntities(t *testing.T) {
if len(list.GetEntities()) != 1 {
t.Errorf("ServerTest(TestServerListEntities) expected %v items, got %v", 1, len(list.GetEntities()))
}
if list.GetEntities()[0].GetUri() != "boss://test/test" {
if list.GetEntities()[0].GetUri() != entityURI {
t.Errorf("ServerTest(TestServerListEntities) expected uri %v, got %v", "boss://test/test", list.GetEntities()[0].GetUri())
}
@@ -50,7 +52,7 @@ func TestServerCreateEntity(t *testing.T) {
defer testStorage.Close()
s := Server{Storage: testStorage.Storage}
entity := &pb.Entity{Uri: "boss://test/test"}
entity := &pb.Entity{Uri: entityURI}
createReq := &pb.CreateEntityRequest{Entity: entity}
resp, err := s.CreateEntity(context.Background(), createReq)
if err != nil {
@@ -62,93 +64,97 @@ func TestServerCreateEntity(t *testing.T) {
}
func TestServerGetEntity(t *testing.T) {
testStorage := NewTestStorage()
defer testStorage.Close()
s := Server{Storage: testStorage.Storage}
t.Run("Get entity", func(t *testing.T) {
testStorage := NewTestStorage()
defer testStorage.Close()
s := Server{Storage: testStorage.Storage}
entity := &pb.Entity{Uri: "boss://test/test"}
createReq := &pb.CreateEntityRequest{Entity: entity}
s.CreateEntity(context.Background(), createReq)
entity := &pb.Entity{Uri: entityURI}
createReq := &pb.CreateEntityRequest{Entity: entity}
s.CreateEntity(context.Background(), createReq)
req := &pb.GetEntityRequest{Entity: entity}
resp, err := s.GetEntity(context.Background(), req)
if err != nil {
t.Errorf("ServerTest(GetEntity) got unexpected error %v", err)
}
if resp == nil {
t.Errorf("ServerTest(GetEntity) returned nil")
}
if resp.GetEntity().GetUri() != entity.GetUri() {
t.Errorf("ServerTest(GetEntity) got %v, wanted %v", resp.GetEntity().GetUri(), entity.GetUri())
}
}
req := &pb.GetEntityRequest{Entity: entity}
resp, err := s.GetEntity(context.Background(), req)
if err != nil {
t.Errorf("ServerTest(GetEntity) got unexpected error %v", err)
}
if resp == nil {
t.Errorf("ServerTest(GetEntity) returned nil")
}
if resp.GetEntity().GetUri() != entity.GetUri() {
t.Errorf("ServerTest(GetEntity) got %v, wanted %v", resp.GetEntity().GetUri(), entity.GetUri())
}
})
func TestServerGetEntityWithIncludedFacts(t *testing.T) {
testStorage := NewTestStorage()
defer testStorage.Close()
s := Server{Storage: testStorage.Storage}
t.Run("Get entity with included facts", func(t *testing.T) {
testStorage := NewTestStorage()
defer testStorage.Close()
s := Server{Storage: testStorage.Storage}
setFactReq := &pb.SetFactRequest{EntityUri: entityURI, Name: "test-name", Value: "test-value"}
s.SetFact(context.Background(), setFactReq)
entityUri := "boss://test/test"
setFactReq := &pb.SetFactRequest{EntityUri: entityUri, Name: "test-name", Value: "test-value"}
s.SetFact(context.Background(), setFactReq)
entity := &pb.Entity{Uri: entityURI}
req := &pb.GetEntityRequest{Entity: entity, IncludeFacts: []string{"test-name"}}
entity := &pb.Entity{Uri: entityUri}
req := &pb.GetEntityRequest{Entity: entity, IncludeFacts: []string{"test-name"}}
resp, err := s.GetEntity(context.Background(), req)
if err != nil {
t.Errorf("ServerTest(GetEntity) got unexpected error %v", err)
}
if resp == nil {
t.Errorf("ServerTest(GetEntity) returned nil")
}
expectedFacts := []*pb.Fact{{Name: "test-name", Value: "test-value"}}
if !reflect.DeepEqual(resp.GetFacts(), expectedFacts) {
t.Errorf("ServerTest(GetEntity) got %v, wanted %v", resp.GetFacts(), expectedFacts)
}
resp, err := s.GetEntity(context.Background(), req)
if err != nil {
t.Errorf("ServerTest(GetEntity) got unexpected error %v", err)
}
if resp == nil {
t.Errorf("ServerTest(GetEntity) returned nil")
}
expectedFacts := []*pb.Fact{{Name: "test-name", Value: "test-value"}}
if !reflect.DeepEqual(resp.GetFacts(), expectedFacts) {
t.Errorf("ServerTest(GetEntity) got %v, wanted %v", resp.GetFacts(), expectedFacts)
}
})
}
func TestServerSetFactForExistingEntity(t *testing.T) {
testStorage := NewTestStorage()
defer testStorage.Close()
s := Server{Storage: testStorage.Storage}
t.Run("Set fact for existing entity", func(t *testing.T) {
testStorage := NewTestStorage()
defer testStorage.Close()
s := Server{Storage: testStorage.Storage}
entity := &pb.Entity{Uri: "boss://test/test"}
createReq := &pb.CreateEntityRequest{Entity: entity}
s.CreateEntity(context.Background(), createReq)
entity := &pb.Entity{Uri: entityURI}
createReq := &pb.CreateEntityRequest{Entity: entity}
s.CreateEntity(context.Background(), createReq)
req := &pb.SetFactRequest{EntityUri: "boss://test/test", Name: "test-name", Value: "test-value"}
resp, err := s.SetFact(context.Background(), req)
if err != nil {
t.Errorf("ServerTest(SetFact) got unexpected error %v", err)
}
if resp == nil {
t.Errorf("ServerTest(SetFact) returned nil")
}
fact := &pb.Fact{Name: req.GetName(), Value: req.GetValue()}
if !reflect.DeepEqual(resp.GetFact(), fact) {
t.Errorf("ServerTest(SetFact) got %v, wanted %v", resp.GetFact(), fact)
}
req := &pb.SetFactRequest{EntityUri: entityURI, Name: "test-name", Value: "test-value"}
resp, err := s.SetFact(context.Background(), req)
if err != nil {
t.Errorf("ServerTest(SetFact) got unexpected error %v", err)
}
if resp == nil {
t.Errorf("ServerTest(SetFact) returned nil")
}
fact := &pb.Fact{Name: req.GetName(), Value: req.GetValue()}
assertFact(t, resp.GetFact(), fact)
})
t.Run("Set fact for non-existing entity", func(t *testing.T) {
testStorage := NewTestStorage()
defer testStorage.Close()
s := Server{Storage: testStorage.Storage}
req := &pb.SetFactRequest{EntityUri: entityURI, Name: "test-name", Value: "test-value"}
resp, err := s.SetFact(context.Background(), req)
if err != nil {
t.Errorf("ServerTest(SetFact) got unexpected error %v", err)
}
if resp == nil {
t.Errorf("ServerTest(SetFact) returned nil")
}
fact := &pb.Fact{Name: req.GetName(), Value: req.GetValue()}
assertFact(t, resp.GetFact(), fact)
})
}
func TestServerSetFactForNonExistingEntity(t *testing.T) {
testStorage := NewTestStorage()
defer testStorage.Close()
s := Server{Storage: testStorage.Storage}
entityUri := "boss://test/test"
req := &pb.SetFactRequest{EntityUri: entityUri, Name: "test-name", Value: "test-value"}
resp, err := s.SetFact(context.Background(), req)
if err != nil {
t.Errorf("ServerTest(SetFact) got unexpected error %v", err)
}
if resp == nil {
t.Errorf("ServerTest(SetFact) returned nil")
}
fact := &pb.Fact{Name: req.GetName(), Value: req.GetValue()}
if !reflect.DeepEqual(resp.GetFact(), fact) {
t.Errorf("ServerTest(SetFact) got %v, wanted %v", resp.GetFact(), fact)
func assertFact(t *testing.T, got, want *pb.Fact) {
if !reflect.DeepEqual(got, want) {
t.Errorf("ServerTest(SetFact) got %v, wanted %v", got, want)
}
}
+39 -30
View File
@@ -234,7 +234,8 @@ type Build struct {
Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"`
CommitId string `protobuf:"bytes,2,opt,name=commit_id,json=commitId,proto3" json:"commit_id,omitempty"`
Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
Status BuildStatus `protobuf:"varint,4,opt,name=status,proto3,enum=spotify.backstage.builds.v1.BuildStatus" json:"status,omitempty"`
Branch string `protobuf:"bytes,4,opt,name=branch,proto3" json:"branch,omitempty"`
Status BuildStatus `protobuf:"varint,5,opt,name=status,proto3,enum=spotify.backstage.builds.v1.BuildStatus" json:"status,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@@ -286,6 +287,13 @@ func (m *Build) GetMessage() string {
return ""
}
func (m *Build) GetBranch() string {
if m != nil {
return m.Branch
}
return ""
}
func (m *Build) GetStatus() BuildStatus {
if m != nil {
return m.Status
@@ -361,35 +369,36 @@ func init() {
func init() { proto.RegisterFile("builds/v1/builds.proto", fileDescriptor_05a627abb7f9adb4) }
var fileDescriptor_05a627abb7f9adb4 = []byte{
// 447 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x53, 0x41, 0x6f, 0xd3, 0x30,
0x18, 0x25, 0x6b, 0x97, 0xb6, 0x5f, 0x07, 0x0b, 0x3e, 0x8c, 0x68, 0x13, 0x52, 0xc9, 0xa9, 0x4c,
0x28, 0x53, 0xcb, 0x05, 0x71, 0x82, 0x75, 0x65, 0xaa, 0xa8, 0x22, 0xe4, 0x2a, 0x17, 0x2e, 0x55,
0xda, 0x98, 0x62, 0x70, 0x71, 0xb0, 0x9d, 0xa0, 0xfc, 0x09, 0x0e, 0xfc, 0x3c, 0x7e, 0x0d, 0xb2,
0x9d, 0xa8, 0x11, 0x93, 0xda, 0xde, 0xfc, 0xbe, 0xf7, 0xbd, 0xbc, 0xf7, 0x49, 0x2f, 0x70, 0xb1,
0xca, 0x29, 0x4b, 0xe5, 0x4d, 0x31, 0xba, 0xb1, 0xaf, 0x30, 0x13, 0x5c, 0x71, 0x74, 0x25, 0x33,
0xae, 0xe8, 0x97, 0x32, 0x5c, 0x25, 0xeb, 0xef, 0x52, 0x25, 0x1b, 0x12, 0x56, 0x7c, 0x31, 0x0a,
0xc6, 0xf0, 0x74, 0x4e, 0xa5, 0xba, 0x35, 0x03, 0x4c, 0x7e, 0xe6, 0x44, 0x2a, 0xf4, 0x1c, 0x80,
0xfc, 0x50, 0x54, 0x95, 0xcb, 0x5c, 0x50, 0xdf, 0x19, 0x38, 0xc3, 0x1e, 0xee, 0xd9, 0x49, 0x2c,
0x68, 0xc0, 0xe0, 0xbc, 0xa9, 0xc9, 0x58, 0x79, 0x40, 0x81, 0xde, 0x82, 0x6b, 0x2d, 0xfd, 0x93,
0x41, 0x6b, 0xd8, 0x1f, 0x07, 0xe1, 0x9e, 0x4c, 0xa1, 0xf9, 0x30, 0xae, 0x14, 0x41, 0x08, 0xe7,
0xf7, 0xc4, 0x9a, 0xd5, 0xf9, 0xae, 0xa0, 0x67, 0xc8, 0x86, 0x59, 0xd7, 0x0c, 0x74, 0xba, 0xdf,
0x0e, 0x3c, 0xde, 0x09, 0x74, 0xb8, 0x37, 0x70, 0x6a, 0x58, 0xb3, 0x7a, 0x9c, 0xb9, 0x15, 0xa0,
0x09, 0x74, 0x52, 0xa2, 0x12, 0xca, 0x74, 0x70, 0xad, 0x7d, 0x79, 0x58, 0x7b, 0x67, 0x05, 0xb8,
0x56, 0x06, 0x7f, 0x1c, 0x38, 0x35, 0x0c, 0xf2, 0xa0, 0xb5, 0x4b, 0xac, 0x9f, 0xfa, 0x92, 0x35,
0xdf, 0x6e, 0xa9, 0x5a, 0xd2, 0xd4, 0x58, 0xf4, 0x70, 0xd7, 0x0e, 0x66, 0x29, 0xf2, 0xa1, 0xb3,
0x25, 0x52, 0x26, 0x1b, 0xe2, 0xb7, 0x0c, 0x55, 0x43, 0xf4, 0x0e, 0x5c, 0xa9, 0x12, 0x95, 0x4b,
0xbf, 0x3d, 0x70, 0x86, 0x4f, 0xc6, 0xc3, 0xc3, 0xb1, 0x16, 0x66, 0x1f, 0x57, 0xba, 0x60, 0x05,
0x67, 0xcd, 0xb4, 0xe8, 0x02, 0xdc, 0x24, 0x57, 0x5f, 0xb9, 0xa8, 0xd2, 0x55, 0x08, 0xbd, 0x80,
0x33, 0x5e, 0x10, 0x51, 0x50, 0xf2, 0x6b, 0x99, 0x0b, 0x56, 0x65, 0xec, 0xd7, 0xb3, 0x58, 0x30,
0xf4, 0x0c, 0x3a, 0x8c, 0x6f, 0x0c, 0x6b, 0x63, 0xba, 0x8c, 0x6f, 0x62, 0xc1, 0xae, 0x3f, 0x42,
0xbf, 0x61, 0x8d, 0xba, 0xd0, 0x8e, 0xe2, 0xf9, 0xdc, 0x7b, 0x84, 0xfa, 0xd0, 0x59, 0xc4, 0x93,
0xc9, 0x74, 0xb1, 0xf0, 0x1c, 0x0d, 0x3e, 0xbc, 0x9f, 0xcd, 0x63, 0x3c, 0xf5, 0x4e, 0x34, 0xf8,
0x34, 0x8d, 0xee, 0x66, 0xd1, 0xbd, 0xd7, 0xd2, 0x00, 0xc7, 0x51, 0xa4, 0x41, 0x7b, 0xfc, 0xd7,
0x01, 0xd7, 0x36, 0x0e, 0x7d, 0x03, 0xd8, 0xf5, 0x0f, 0x85, 0x7b, 0x6f, 0x7f, 0x50, 0xee, 0xcb,
0x57, 0x47, 0xef, 0xeb, 0xee, 0xa4, 0xd0, 0xad, 0xcb, 0x84, 0xf6, 0x2b, 0xff, 0x2b, 0xe9, 0xe5,
0xf5, 0x91, 0xdb, 0x19, 0x2b, 0x6f, 0xe1, 0xb3, 0xed, 0xaf, 0x2c, 0x46, 0x2b, 0xd7, 0xfc, 0xb5,
0xaf, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0x40, 0xf3, 0x96, 0x0b, 0xcf, 0x03, 0x00, 0x00,
// 459 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x53, 0xd1, 0x6e, 0xd3, 0x30,
0x14, 0x25, 0x6b, 0x9b, 0xb6, 0xb7, 0x83, 0x05, 0x3f, 0x8c, 0x68, 0x13, 0x52, 0xc9, 0x53, 0x99,
0x50, 0xa6, 0x96, 0x17, 0xc4, 0x13, 0xac, 0x2b, 0x53, 0x45, 0x15, 0x21, 0x57, 0x79, 0xe1, 0xa5,
0x4a, 0x1a, 0xd3, 0x19, 0xdc, 0xb9, 0xd8, 0x4e, 0x50, 0x7f, 0x82, 0x0f, 0xe1, 0x93, 0xf8, 0x1a,
0x64, 0x3b, 0x51, 0x23, 0x26, 0xb5, 0x7d, 0xf3, 0xb9, 0xe7, 0x9e, 0x9c, 0x73, 0xa5, 0x13, 0x38,
0x4f, 0x73, 0xca, 0x32, 0x79, 0x5d, 0x0c, 0xaf, 0xed, 0x2b, 0xdc, 0x08, 0xae, 0x38, 0xba, 0x94,
0x1b, 0xae, 0xe8, 0xb7, 0x6d, 0x98, 0x26, 0xcb, 0x1f, 0x52, 0x25, 0x2b, 0x12, 0x96, 0x7c, 0x31,
0x0c, 0x46, 0xf0, 0x7c, 0x46, 0xa5, 0xba, 0x31, 0x03, 0x4c, 0x7e, 0xe6, 0x44, 0x2a, 0xf4, 0x12,
0x80, 0x3c, 0x28, 0xaa, 0xb6, 0x8b, 0x5c, 0x50, 0xdf, 0xe9, 0x3b, 0x83, 0x2e, 0xee, 0xda, 0x49,
0x2c, 0x68, 0xc0, 0xe0, 0xac, 0xae, 0xd9, 0xb0, 0xed, 0x01, 0x05, 0x7a, 0x0f, 0xae, 0xb5, 0xf4,
0x4f, 0xfa, 0x8d, 0x41, 0x6f, 0x14, 0x84, 0x7b, 0x32, 0x85, 0xe6, 0xc3, 0xb8, 0x54, 0x04, 0x21,
0x9c, 0xdd, 0x11, 0x6b, 0x56, 0xe5, 0xbb, 0x84, 0xae, 0x21, 0x6b, 0x66, 0x1d, 0x33, 0xd0, 0xe9,
0x7e, 0x3b, 0xf0, 0x74, 0x27, 0xd0, 0xe1, 0xde, 0x41, 0xcb, 0xb0, 0x66, 0xf5, 0x38, 0x73, 0x2b,
0x40, 0x63, 0x68, 0x67, 0x44, 0x25, 0x94, 0xe9, 0xe0, 0x5a, 0xfb, 0xfa, 0xb0, 0xf6, 0xd6, 0x0a,
0x70, 0xa5, 0x0c, 0xfe, 0x38, 0xd0, 0x32, 0x0c, 0xf2, 0xa0, 0xb1, 0x4b, 0xac, 0x9f, 0xfa, 0x92,
0x25, 0x5f, 0xaf, 0xa9, 0x5a, 0xd0, 0xcc, 0x58, 0x74, 0x71, 0xc7, 0x0e, 0xa6, 0x19, 0xf2, 0xa1,
0xbd, 0x26, 0x52, 0x26, 0x2b, 0xe2, 0x37, 0x0c, 0x55, 0x41, 0x74, 0x0e, 0x6e, 0x2a, 0x92, 0x87,
0xe5, 0xbd, 0xdf, 0x34, 0x44, 0x89, 0xd0, 0x07, 0x70, 0xa5, 0x4a, 0x54, 0x2e, 0xfd, 0x56, 0xdf,
0x19, 0x3c, 0x1b, 0x0d, 0x0e, 0xc7, 0x9d, 0x9b, 0x7d, 0x5c, 0xea, 0x82, 0x14, 0x4e, 0xeb, 0x57,
0x68, 0xa7, 0x24, 0x57, 0xf7, 0x5c, 0x94, 0xa9, 0x4b, 0x84, 0x5e, 0xc1, 0x29, 0x2f, 0x88, 0x28,
0x28, 0xf9, 0xb5, 0xc8, 0x05, 0x2b, 0xb3, 0xf7, 0xaa, 0x59, 0x2c, 0x18, 0x7a, 0x01, 0x6d, 0xc6,
0x57, 0x86, 0xb5, 0xf1, 0x5d, 0xc6, 0x57, 0xb1, 0x60, 0x57, 0x9f, 0xa1, 0x57, 0xb3, 0x46, 0x1d,
0x68, 0x46, 0xf1, 0x6c, 0xe6, 0x3d, 0x41, 0x3d, 0x68, 0xcf, 0xe3, 0xf1, 0x78, 0x32, 0x9f, 0x7b,
0x8e, 0x06, 0x9f, 0x3e, 0x4e, 0x67, 0x31, 0x9e, 0x78, 0x27, 0x1a, 0x7c, 0x99, 0x44, 0xb7, 0xd3,
0xe8, 0xce, 0x6b, 0x68, 0x80, 0xe3, 0x28, 0xd2, 0xa0, 0x39, 0xfa, 0xeb, 0x80, 0x6b, 0x9b, 0x88,
0xbe, 0x03, 0xec, 0x7a, 0x89, 0xc2, 0xbd, 0xb7, 0x3f, 0x2a, 0xfd, 0xc5, 0x9b, 0xa3, 0xf7, 0x75,
0xa7, 0x32, 0xe8, 0x54, 0x25, 0x43, 0xfb, 0x95, 0xff, 0x95, 0xf7, 0xe2, 0xea, 0xc8, 0xed, 0x0d,
0xdb, 0xde, 0xc0, 0x57, 0xdb, 0x6b, 0x59, 0x0c, 0x53, 0xd7, 0xfc, 0xcd, 0x6f, 0xff, 0x05, 0x00,
0x00, 0xff, 0xff, 0xc2, 0xff, 0xd7, 0x62, 0xe7, 0x03, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
+3 -28
View File
@@ -1,18 +1,10 @@
import {
BackstageTheme,
createApp,
Header,
InfoCard,
Page,
theme,
} from '@backstage/core';
import { BackstageTheme, createApp, InfoCard } from '@backstage/core';
import HomePagePlugin from '@backstage/plugin-home-page';
//import PageHeader from './components/PageHeader';
import { LoginComponent } from '@backstage/plugin-login';
import { CssBaseline, makeStyles, ThemeProvider } from '@material-ui/core';
import React, { FC } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import HomePageTimer from './components/HomepageTimer';
import SideBar from './components/SideBar';
import entities from './entities';
import { LoginBarrier } from './login/LoginBarrier';
@@ -37,20 +29,10 @@ const useStyles = makeStyles(theme => ({
display: 'grid',
// FIXME: Don't used a fixed width here
gridTemplateColumns: '64px auto',
gridTemplateRows: 'auto 1fr',
gridTemplateRows: '1fr',
width: '100%',
height: '100vh',
},
mainContentArea: {
overflowX: 'hidden',
overflowY: 'auto',
},
pageBody: {
padding: theme.spacing(2),
},
avatarButton: {
padding: theme.spacing(2),
},
}));
const currentUser = new MockCurrentUser();
@@ -69,14 +51,7 @@ const AppShell: FC<{}> = ({ children }) => {
return (
<div className={classes.root}>
<SideBar />
<Page theme={theme.home}>
<div className={classes.mainContentArea}>
<Header title="This is Backstage!">
<HomePageTimer />
</Header>
<div className={classes.pageBody}>{children}</div>
</div>
</Page>
{children}
</div>
);
};
@@ -158,7 +158,7 @@ const useStyles = makeStyles(theme => ({
display: 'flex',
flexFlow: 'column nowrap',
alignItems: 'flex-start',
position: 'absolute',
position: 'fixed',
left: 0,
top: 0,
bottom: 0,
+4 -2
View File
@@ -4,6 +4,8 @@ import {
createEntityPage,
} from '@backstage/core';
import ComputerIcon from '@material-ui/icons/Computer';
import WebIcon from '@material-ui/icons/Web';
import DnsIcon from '@material-ui/icons/Dns';
import MockEntityPage from './MockEntityPage';
import MockEntityCard from './MockEntityCard';
import GithubActionsPlugin from '@backstage/plugin-github-actions';
@@ -14,9 +16,9 @@ const serviceOverviewPage = createWidgetView()
.addComponent(MockEntityCard);
const serviceView = createEntityPage()
.addPage('Overview', '/overview', serviceOverviewPage)
.addPage('Overview', WebIcon, '/overview', serviceOverviewPage)
.register(GithubActionsPlugin)
.addComponent('Deployment', '/deployment', MockEntityPage);
.addComponent('Deployment', DnsIcon, '/deployment', MockEntityPage);
const serviceEntity = createEntityKind({
kind: 'service',
+2
View File
@@ -4,6 +4,8 @@
"main": "src/index.ts",
"main:src": "src/index.ts",
"devDependencies": {
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@spotify/web-scripts": "^6.0.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
@@ -1,10 +1,11 @@
import { ComponentType } from 'react';
import { AppComponentBuilder } from '../app/types';
import { IconComponent } from '../types';
export type EntityConfig = {
kind: string;
title: string;
icon: React.ComponentType<{ fontSize: number }>;
icon: IconComponent;
color: {
primary: string;
secondary: string;
@@ -1,83 +1,21 @@
import React, { ComponentType, FC } from 'react';
import { Route, Redirect, Switch } from 'react-router-dom';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import { AppComponentBuilder, App } from '../app/types';
import { useEntity, useEntityUri, useEntityConfig } from './EntityContext';
import EntityLink from '../../components/EntityLink/EntityLink';
import React, { ComponentType } from 'react';
import DefaultEntityPage from '../../components/DefaultEntityPage';
import { App, AppComponentBuilder } from '../app/types';
import BackstagePlugin from '../plugin/Plugin';
import { EntityPageNavItem, EntityPageView } from './types';
import { IconComponent } from '../types';
const EntityLayout: FC<{}> = ({ children }) => {
const config = useEntityConfig();
return (
<div style={{ backgroundColor: config.color.primary }}>{children}</div>
);
};
const EntitySidebar: FC<{}> = ({ children }) => {
return <List>{children}</List>;
};
const EntitySidebarItem: FC<{ title: string; path: string }> = ({
title,
path,
}) => {
const entityUri = useEntityUri();
return (
<ListItem>
<EntityLink uri={entityUri} subPath={path}>
{title}
</EntityLink>
</ListItem>
);
};
type EntityPageNavItem = {
title: string;
target: string;
};
type EntityPageView = {
path: string;
component: ComponentType<any>;
};
type Props = {
navItems: EntityPageNavItem[];
views: EntityPageView[];
};
const EntityPageComponent: FC<Props> = ({ navItems, views }) => {
const { kind, id } = useEntity();
const basePath = `/entity/${kind}/${id}`;
return (
<EntityLayout>
<EntitySidebar>
{navItems.map(({ title, target }) => (
<EntitySidebarItem key={target} title={title} path={target} />
))}
</EntitySidebar>
<Switch>
{views.map(({ path, component }) => (
<Route
key={path}
exact
path={`${basePath}${path}`}
component={component}
/>
))}
<Redirect from={basePath} to={`${basePath}${views[0].path}`} />
</Switch>
</EntityLayout>
);
};
// type AppComponents = {
// EntityPage: ComponentType<EntityPageProps>;
// EntityPageNavbar: ComponentType<EntityPageNavbarProps>;
// EntityPageHeader: ComponentType<EntityPageHeaderProps>;
// };
type EntityPageRegistration =
| {
type: 'page';
title: string;
icon: IconComponent;
path: string;
page: AppComponentBuilder;
}
@@ -88,6 +26,7 @@ type EntityPageRegistration =
| {
type: 'component';
title: string;
icon: IconComponent;
path: string;
component: ComponentType<any>;
};
@@ -97,19 +36,27 @@ export default class EntityPageBuilder extends AppComponentBuilder {
addPage(
title: string,
icon: IconComponent,
path: string,
page: AppComponentBuilder,
): EntityPageBuilder {
this.registrations.push({ type: 'page', title, path, page });
this.registrations.push({ type: 'page', title, icon, path, page });
return this;
}
addComponent(
title: string,
icon: IconComponent,
path: string,
component: ComponentType<any>,
): EntityPageBuilder {
this.registrations.push({ type: 'component', title, path, component });
this.registrations.push({
type: 'component',
title,
icon,
path,
component,
});
return this;
}
@@ -125,14 +72,14 @@ export default class EntityPageBuilder extends AppComponentBuilder {
for (const reg of this.registrations) {
switch (reg.type) {
case 'page': {
const { title, path, page } = reg;
navItems.push({ title, target: path });
const { title, icon, path, page } = reg;
navItems.push({ title, icon, target: path });
views.push({ path, component: page.build(app) });
break;
}
case 'component': {
const { title, path, component } = reg;
navItems.push({ title, target: path });
const { title, icon, path, component } = reg;
navItems.push({ title, icon, target: path });
views.push({ path, component });
break;
}
@@ -141,8 +88,8 @@ export default class EntityPageBuilder extends AppComponentBuilder {
for (const output of reg.plugin.output()) {
switch (output.type) {
case 'entity-page-nav-item':
const { title, target } = output;
navItems.push({ title, target });
const { title, icon, target } = output;
navItems.push({ title, icon, target });
added = true;
break;
case 'entity-page-view-route':
@@ -162,6 +109,6 @@ export default class EntityPageBuilder extends AppComponentBuilder {
}
}
return () => <EntityPageComponent navItems={navItems} views={views} />;
return () => <DefaultEntityPage navItems={navItems} views={views} />;
}
}
@@ -0,0 +1,24 @@
import { ComponentType } from 'react';
import { IconComponent } from '../types';
export type EntityPageNavItem = {
icon: IconComponent;
title: string;
target: string;
};
export type EntityPageView = {
path: string;
component: ComponentType<any>;
};
export type EntityPageProps = {
navItems: EntityPageNavItem[];
views: EntityPageView[];
};
export type EntityPageNavbarProps = {
navItems: EntityPageNavItem[];
};
export type EntityPageHeaderProps = {};
@@ -1,5 +1,6 @@
import { ComponentType } from 'react';
import { PluginOutput, RoutePath, RouteOptions } from './types';
import { IconComponent } from '../types';
export type PluginConfig = {
id: string;
@@ -27,6 +28,7 @@ export type RouterHooks = {
type EntityPageSidebarItemOptions = {
title: string;
icon: IconComponent;
target: RoutePath;
};
@@ -79,11 +81,12 @@ export default class Plugin {
},
},
entityPage: {
navItem({ title, target }) {
navItem({ title, icon, target }) {
outputs.push({
type: 'entity-page-nav-item',
target,
title,
icon,
target,
});
},
route(path, component, options) {
@@ -1,4 +1,5 @@
import { ComponentType } from 'react';
import { IconComponent } from '../types';
export type RouteOptions = {
// Whether the route path must match exactly, defaults to true.
@@ -31,6 +32,7 @@ export type EntityPageViewRouteOutput = {
export type EntityPageNavItemOutput = {
type: 'entity-page-nav-item';
title: string;
icon: IconComponent;
target: RoutePath;
};
+4 -9
View File
@@ -1,10 +1,5 @@
export type User = {
id: string;
email: string;
};
import { ComponentType } from 'react';
export type UserApi = {
isLoggedIn(): Promise<boolean>;
getUser(): Promise<User>;
};
export type IconComponent = ComponentType<{
fontSize: 'inherit' | 'default' | 'small' | 'large';
}>;
@@ -0,0 +1,62 @@
import React, { FC } from 'react';
import { makeStyles, Theme } from '@material-ui/core';
import { EntityPageProps } from '../../api/entityView/types';
import { useEntity } from '../../api';
import { Switch, Route, Redirect } from 'react-router-dom';
import DefaultEntityPageHeader from '../DefaultEntityPageHeader';
import DefaultEntityPageNavbar from '../DefaultEntityPageNavbar';
const useStyles = makeStyles<Theme>(theme => ({
root: {
display: 'grid',
gridTemplateAreas: `
'header header'
'navbar content'
`,
gridTemplateRows: 'auto 1fr',
gridTemplateColumns: 'auto 1fr',
minHeight: '100%',
paddingBottom: theme.spacing(3),
},
header: {
gridArea: 'header',
},
navbar: {
gridArea: 'navbar',
},
content: {
gridArea: 'content',
},
}));
const DefaultEntityPage: FC<EntityPageProps> = ({ navItems, views }) => {
const classes = useStyles();
const { kind, id } = useEntity();
const basePath = `/entity/${kind}/${id}`;
return (
<div className={classes.root}>
<div className={classes.header}>
<DefaultEntityPageHeader />
</div>
<div className={classes.navbar}>
<DefaultEntityPageNavbar navItems={navItems} />
</div>
<div className={classes.content}>
<Switch>
{views.map(({ path, component }) => (
<Route
key={path}
exact
path={`${basePath}${path}`}
component={component}
/>
))}
<Redirect from={basePath} to={`${basePath}${views[0].path}`} />
</Switch>
</div>
</div>
);
};
export default DefaultEntityPage;
@@ -0,0 +1 @@
export { default } from './DefaultEntityPage';
@@ -0,0 +1,18 @@
import React, { FC } from 'react';
import { Header, useEntity, useEntityConfig, theme } from '../..';
import { EntityPageHeaderProps } from '../../api/entityView/types';
import { Theme } from '../../layout/Page/Page';
const DefaultEntityPageHeader: FC<EntityPageHeaderProps> = () => {
const { id } = useEntity();
const config = useEntityConfig();
// TODO(rugvip): provide theme through entity config
return (
<Theme.Provider value={theme.service}>
<Header title={`${config.title} - ${id}`} />
</Theme.Provider>
);
};
export default DefaultEntityPageHeader;
@@ -0,0 +1 @@
export { default } from './DefaultEntityPageHeader';
@@ -0,0 +1,36 @@
import React, { FC } from 'react';
import { EntityPageNavbarProps } from '../../api/entityView/types';
import { useEntityUri } from '../..';
import { List, makeStyles, Theme } from '@material-ui/core';
import NavbarItem from './NavbarItem';
const useStyles = makeStyles<Theme>({
nav: {
gridArea: 'pageNav',
width: 220,
transition: 'width 0.07s, height 0s',
transitionTimingFunction: 'ease-in',
backgroundColor: '#eeeeee',
boxShadow: '0px 0 4px 0px rgba(0,0,0,0.35)',
},
list: {
padding: 0,
},
});
const DefaultEntityPageNavbar: FC<EntityPageNavbarProps> = ({ navItems }) => {
const classes = useStyles();
const entityUri = useEntityUri();
return (
<nav className={classes.nav}>
<List className={classes.list}>
{navItems.map((navItem, index) => (
<NavbarItem key={index} navItem={navItem} entityUri={entityUri} />
))}
</List>
</nav>
);
};
export default DefaultEntityPageNavbar;
@@ -0,0 +1,72 @@
import React, { FC } from 'react';
import { EntityLink } from '../..';
import {
ListItem,
makeStyles,
Theme,
ListItemIcon,
ListItemText,
Typography,
} from '@material-ui/core';
import { EntityPageNavItem } from '../../api/entityView/types';
const useStyles = makeStyles<Theme>(theme => ({
root: {
display: 'block',
overflow: 'hidden',
borderBottom: `1px solid #d9d9d9`,
paddingLeft: theme.spacing(1),
},
label: {
color: '#333',
fontWeight: 'bolder',
whiteSpace: 'nowrap',
lineHeight: 1.0,
},
iconImg: {
width: 24,
height: 24,
},
icon: {
margin: theme.spacing(0.5, 2, 0.5, 0),
minWidth: 0,
fontSize: 24,
},
expand: {
color: 'white',
},
}));
type Props = {
navItem: EntityPageNavItem;
entityUri: string;
};
const NavbarItem: FC<Props> = ({ navItem, entityUri }) => {
const classes = useStyles();
const IconComponent = navItem.icon;
return (
<EntityLink
className={classes.root}
uri={entityUri}
subPath={navItem.target}
>
<ListItem button className={`${classes.listItemGutters}`}>
<ListItemIcon className={classes.icon}>
<IconComponent fontSize="inherit" />
</ListItemIcon>
<ListItemText
primary={
<Typography variant="subtitle1" className={classes.label}>
{navItem.title}
</Typography>
}
disableTypography
/>
</ListItem>
</EntityLink>
);
};
export default NavbarItem;
@@ -0,0 +1 @@
export { default } from './DefaultEntityPageNavbar';
@@ -1,17 +1,17 @@
import React, { FC } from 'react';
import { Link } from 'react-router-dom';
import { Link, LinkProps } from 'react-router-dom';
type Props = {
type Props = Omit<LinkProps, 'to'> & {
subPath?: string;
} & (
| {
kind: string;
id?: string;
}
| {
uri: string;
}
);
| {
kind: string;
id?: string;
}
| {
uri: string;
}
);
export function buildPath(kind: string, id?: string, subPath?: string) {
if (id) {
@@ -26,7 +26,11 @@ export function buildPath(kind: string, id?: string, subPath?: string) {
const EntityLink: FC<Props> = ({ subPath, children, ...props }) => {
if ('kind' in props) {
const { kind, id } = props;
return <Link to={buildPath(kind, id, subPath)}>{children}</Link>;
return (
<Link to={buildPath(kind, id, subPath)} {...props}>
{children}
</Link>
);
} else {
const match = props.uri.match(/entity:([^:]+)(:[^:]+)?/);
if (!match) {
@@ -36,7 +40,11 @@ const EntityLink: FC<Props> = ({ subPath, children, ...props }) => {
const [, kind, maybeId] = match;
const id = maybeId ? maybeId.slice(1) : undefined;
return <Link to={buildPath(kind, id, subPath)}>{children}</Link>;
return (
<Link to={buildPath(kind, id, subPath)} {...props}>
{children}
</Link>
);
}
};
+8 -8
View File
@@ -3,14 +3,14 @@ export {
default as EntityLink,
RelativeEntityLink,
} from './components/EntityLink';
export { default as Page } from '../src/layout/Page';
export { gradients, theme } from '../src/layout/Page';
export { default as Header } from '../src/layout/Header/Header';
export { default as HeaderLabel } from '../src/layout/HeaderLabel';
export { default as InfoCard } from '../src/layout/InfoCard';
export { default as ErrorBoundary } from '../src/layout/ErrorBoundary';
export { default as BackstageTheme } from '../src/theme/BackstageTheme';
export { COLORS } from '../src/theme/BackstageTheme';
export { default as Page } from './layout/Page';
export { gradients, theme } from './layout/Page';
export { default as Header } from './layout/Header/Header';
export { default as HeaderLabel } from './layout/HeaderLabel';
export { default as InfoCard } from './layout/InfoCard';
export { default as ErrorBoundary } from './layout/ErrorBoundary';
export { default as BackstageTheme } from './theme/BackstageTheme';
export { COLORS } from './theme/BackstageTheme';
export { default as HorizontalScrollGrid } from './components/HorizontalScrollGrid';
export { default as ProgressCard } from './components/ProgressCard';
export { default as CircleProgress } from './components/CircleProgress';
@@ -103,7 +103,7 @@ const styles = theme => ({
gridArea: 'pageHeader',
padding: theme.spacing(3),
minHeight: 118,
width: '100vw',
width: '100%',
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
position: 'relative',
zIndex: 100,
@@ -52,6 +52,7 @@ export default class BuildsClient {
return {
commitId: build.getCommitId(),
message: build.getMessage(),
branch: build.getBranch(),
status: statusTable[build.getStatus()] || BuildStatus.Null,
uri: build.getUri(),
};
@@ -9,6 +9,7 @@ export enum BuildStatus {
export type Build = {
commitId: string;
message: string;
branch: string;
status: BuildStatus;
uri: string;
};
@@ -38,8 +38,8 @@ const BuildDetailsPage: FC<Props> = () => {
}
if (status.error) {
return (
<Typography variant="h4" color="error">
Failed to load build, {status.error}
<Typography variant="h6" color="error">
Failed to load build, {status.error.message}
</Typography>
);
}
@@ -50,6 +50,12 @@ const BuildDetailsPage: FC<Props> = () => {
<TableContainer component={Paper} className={classes.root}>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Typography noWrap>Branch</Typography>
</TableCell>
<TableCell>{details?.build.branch}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Message</Typography>
@@ -10,6 +10,8 @@ import {
LinearProgress,
Typography,
Tooltip,
makeStyles,
Theme,
} from '@material-ui/core';
import { RelativeEntityLink } from '@backstage/core';
import { BuildsClient } from '../../apis/builds';
@@ -28,35 +30,51 @@ const LongText: FC<{ text: string; max: number }> = ({ text, max }) => {
);
};
const useStyles = makeStyles<Theme>(theme => ({
root: {
padding: theme.spacing(2),
},
title: {
paddingBottom: theme.spacing(2),
},
}));
const BuildListPage: FC<{}> = () => {
const classes = useStyles();
const status = useAsync(() => client.listBuilds('entity:spotify:backstage'));
let content: JSX.Element;
if (status.loading) {
return <LinearProgress />;
}
if (status.error) {
return (
content = <LinearProgress />;
} else if (status.error) {
content = (
<Typography variant="h4" color="error">
Failed to load builds, {status.error}
Failed to load builds, {status.error.message}
</Typography>
);
}
return (
<>
<Typography variant="h4">CI/CD Builds</Typography>
} else {
content = (
<TableContainer component={Paper}>
<Table aria-label="CI/CD builds table">
<TableHead>
<TableRow>
<TableCell>Status</TableCell>
<TableCell>Branch</TableCell>
<TableCell>Message</TableCell>
<TableCell>Commit</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{status.value!.map(build => (
<TableRow key={build.uri}>
{/* TODO: make this an indicating blobby thing */}
<TableCell>{build.status}</TableCell>
<TableCell>
<Typography>
<LongText text={build.branch} max={30} />
</Typography>
</TableCell>
<TableCell>
<RelativeEntityLink
view={`builds/${encodeURIComponent(build.uri)}`}
@@ -73,13 +91,21 @@ const BuildListPage: FC<{}> = () => {
</Typography>
</Tooltip>
</TableCell>
<TableCell>{build.status}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
);
}
return (
<div className={classes.root}>
<Typography variant="h4" className={classes.title}>
CI/CD Builds
</Typography>
{content}
</div>
);
};
@@ -1,6 +1,7 @@
import { createPlugin } from '@backstage/core';
import BuildDetailsPage from './components/BuildDetailsPage';
import BuildListPage from './components/BuildListPage';
import BuildIcon from '@material-ui/icons/Build';
// export const buildListRoute = createEntityRoute<[]>('/builds')
// export const buildDetailsRoute = createEntityRoute<[number]>('/builds/:buildId')
@@ -9,7 +10,7 @@ export default createPlugin({
id: 'github-actions',
register({ entityPage }) {
entityPage.navItem({ title: 'CI/CD', target: '/builds' });
entityPage.navItem({ title: 'CI/CD', icon: BuildIcon, target: '/builds' });
entityPage.route('/builds', BuildListPage);
entityPage.route('/builds/:buildUri', BuildDetailsPage);
},
@@ -1,10 +1,16 @@
import React from 'react';
import { render } from '@testing-library/react';
import HomePage from './HomePage';
import { ThemeProvider } from '@material-ui/core';
import { BackstageTheme } from '@backstage/core';
describe('HomePage', () => {
it('should render', () => {
const rendered = render(<HomePage />);
const rendered = render(
<ThemeProvider theme={BackstageTheme}>
<HomePage />
</ThemeProvider>,
);
expect(rendered.baseElement).toBeInTheDocument();
});
});
@@ -1,10 +1,31 @@
import React, { FC } from 'react';
import { EntityLink, InfoCard, SortableTable } from '@backstage/core';
import { Typography, makeStyles, Theme, Grid } from '@material-ui/core';
import HomePageTimer from '../HomepageTimer';
import {
EntityLink,
InfoCard,
SortableTable,
Header,
Page,
theme,
} from '@backstage/core';
import SquadTechHealth from './SquadTechHealth';
import { Grid, Typography } from '@material-ui/core';
const useStyles = makeStyles<Theme>(theme => ({
mainContentArea: {
overflowX: 'hidden',
overflowY: 'auto',
},
pageBody: {
padding: theme.spacing(2),
},
avatarButton: {
padding: theme.spacing(2),
},
}));
const HomePage: FC<{}> = () => {
const classes = useStyles();
const data = [
{ id: 'service-1', system: 'system' },
{ id: 'service-2', system: 'system' },
@@ -23,25 +44,34 @@ const HomePage: FC<{}> = () => {
];
return (
<Grid container direction="column" spacing={6}>
<Grid item>
<SquadTechHealth />
</Grid>
<Grid item>
<InfoCard title="Stuff you own">
<SortableTable data={data} columns={columns} orderBy="id" />
<Typography variant="body1">Welcome to Backstage!</Typography>
<div>
<EntityLink kind="service" id="backstage-backend">
Backstage Backend
</EntityLink>
<EntityLink uri="entity:service:backstage-lb" subPath="ci-cd">
Backstage LB CI/CD
</EntityLink>
</div>
</InfoCard>
</Grid>
</Grid>
<Page theme={theme.home}>
<div className={classes.mainContentArea}>
<Header title="This is Backstage!">
<HomePageTimer />
</Header>
<div className={classes.pageBody}>
<Grid container direction="column" spacing={6}>
<Grid item>
<SquadTechHealth />
</Grid>
<Grid item>
<InfoCard title="Stuff you own">
<SortableTable data={data} columns={columns} orderBy="id" />
<Typography variant="body1">Welcome to Backstage!</Typography>
<div>
<EntityLink kind="service" id="backstage-backend">
Backstage Backend
</EntityLink>
<EntityLink uri="entity:service:backstage-lb" subPath="ci-cd">
Backstage LB CI/CD
</EntityLink>
</div>
</InfoCard>
</Grid>
</Grid>
</div>
</div>
</Page>
);
};
@@ -96,6 +96,9 @@ export class Build extends jspb.Message {
getMessage(): string;
setMessage(value: string): void;
getBranch(): string;
setBranch(value: string): void;
getStatus(): BuildStatus;
setStatus(value: BuildStatus): void;
@@ -112,6 +115,7 @@ export namespace Build {
uri: string,
commitId: string,
message: string,
branch: string,
status: BuildStatus,
}
}
@@ -810,7 +810,8 @@ proto.spotify.backstage.builds.v1.Build.toObject = function(includeInstance, msg
uri: jspb.Message.getFieldWithDefault(msg, 1, ""),
commitId: jspb.Message.getFieldWithDefault(msg, 2, ""),
message: jspb.Message.getFieldWithDefault(msg, 3, ""),
status: jspb.Message.getFieldWithDefault(msg, 4, 0)
branch: jspb.Message.getFieldWithDefault(msg, 4, ""),
status: jspb.Message.getFieldWithDefault(msg, 5, 0)
};
if (includeInstance) {
@@ -860,6 +861,10 @@ proto.spotify.backstage.builds.v1.Build.deserializeBinaryFromReader = function(m
msg.setMessage(value);
break;
case 4:
var value = /** @type {string} */ (reader.readString());
msg.setBranch(value);
break;
case 5:
var value = /** @type {!proto.spotify.backstage.builds.v1.BuildStatus} */ (reader.readEnum());
msg.setStatus(value);
break;
@@ -913,10 +918,17 @@ proto.spotify.backstage.builds.v1.Build.serializeBinaryToWriter = function(messa
f
);
}
f = message.getBranch();
if (f.length > 0) {
writer.writeString(
4,
f
);
}
f = message.getStatus();
if (f !== 0.0) {
writer.writeEnum(
4,
5,
f
);
}
@@ -969,17 +981,32 @@ proto.spotify.backstage.builds.v1.Build.prototype.setMessage = function(value) {
/**
* optional BuildStatus status = 4;
* optional string branch = 4;
* @return {string}
*/
proto.spotify.backstage.builds.v1.Build.prototype.getBranch = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, ""));
};
/** @param {string} value */
proto.spotify.backstage.builds.v1.Build.prototype.setBranch = function(value) {
jspb.Message.setProto3StringField(this, 4, value);
};
/**
* optional BuildStatus status = 5;
* @return {!proto.spotify.backstage.builds.v1.BuildStatus}
*/
proto.spotify.backstage.builds.v1.Build.prototype.getStatus = function() {
return /** @type {!proto.spotify.backstage.builds.v1.BuildStatus} */ (jspb.Message.getFieldWithDefault(this, 4, 0));
return /** @type {!proto.spotify.backstage.builds.v1.BuildStatus} */ (jspb.Message.getFieldWithDefault(this, 5, 0));
};
/** @param {!proto.spotify.backstage.builds.v1.BuildStatus} value */
proto.spotify.backstage.builds.v1.Build.prototype.setStatus = function(value) {
jspb.Message.setProto3EnumField(this, 4, value);
jspb.Message.setProto3EnumField(this, 5, value);
};
+2 -1
View File
@@ -31,7 +31,8 @@ message Build {
string uri = 1;
string commit_id = 2;
string message = 3;
BuildStatus status = 4;
string branch = 4;
BuildStatus status = 5;
}
message BuildDetails {