Merge branch 'rugvip/entity-page' into alund/home-v1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
+18
@@ -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';
|
||||
+36
@@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
+8
-2
@@ -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>
|
||||
|
||||
+39
-13
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user